DefaultHybrid

class hybridlane.devices.default_hybrid.device.DefaultHybrid(fock_level=None, wire_dims=None, wires=None, seed='global', shots=None, max_workers=None)

Bases: pennylane.devices.device_api.Device

A hybridlane device written in Python capable of backpropagation

Parameters:
  • fock_level (int | None) – The default truncation level for all qumodes.

  • wire_dims (Mapping[Any, int] | None) – A mapping from wires to their dimensions. Use this to provide non-uniform truncation levels across qumodes. Note that only one of fock_level or wire_dims may be specified.

  • max_workers (int | None) – The maximum number of worker processes to use when executing multiple circuits in parallel. If None, execution will be performed serially in the main process.

  • seed (Any) – The seed for the random number generator. This can be an integer or a jax.Array. If “global”, the seed will be drawn from the global random state of numpy.

  • wires (pennylane.wires.WiresLike | None)

  • shots (int | None)

Example

import pennylane as qp
import hybridlane as hl

def circuit(alpha):
    qp.CatState(alpha, 0, 0, wires=0)

    hl.D(alpha, 0, 0)  # |0> + |2α>
    qp.H(1)
    hl.SQR(np.pi, np.pi / 2, 0, wires=[1, 0])  # Ry(pi)|0><0|
    qp.H(1)

    return hl.expval(qp.Z(1))
>>> tape = qp.tape.make_qscript(circuit)(0.123)
>>> dev = DefaultHybrid(fock_level=8)
>>> program, execution_config = dev.preprocess()
>>> new_batch, postprocessing_fn = program([tape])
>>> results = dev.execute(new_batch, execution_config=execution_config)
>>> postprocessing_fn(results)
(np.float64(-0.970195190896443),)

This device supports backpropagation:

>>> from pennylane.devices import ExecutionConfig
>>> dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"))
True

It is mostly compatible with Jax and can be used to take gradients

import jax

def circuit(alpha):
    hl.D(alpha, 0, wires=0)
    return hl.expval(hl.X(0))

@jax.jit
def f(x):
    tape = qp.tape.make_qscript(circuit)(x)
    program, execution_config = dev.preprocess()
    new_batch, postprocessing_fn = program([tape])
    results = dev.execute(new_batch, execution_config=execution_config)
    return postprocessing_fn(results)[0]
>>> f(jnp.array(0.123))
Array(0.1739, dtype=float64)
>>> jax.grad(f)(jnp.array(0.123))
Array(1.4142, dtype=float64, weak_type=True)

Details

This device performs dense statevector simulation in Fock space, and is therefore unlikely to be scalable. However, it serves as a useful reference implementation for testing and debugging, and also provides a template for how to implement a hybrid device using the PennyLane device API.

Supported measurements

Measurement

numpy

jax.jit

expval (analytic)

expval (finite)

var (analytic)

var (finite)

state

density_matrix

sample

Currently the device does not support shot partitioning.

Other limitations

  • Mid-circuit measurements aren’t supported yet. #51

  • Operator batching isn’t supported. If you want to batch operations, consider using jax.vmap. #52

name = 'default.hybrid'

The name of the device or set of devices.

This property can either be the name of the class, or an alias to be used in the device() constructor, such as "default.qubit" or "lightning.qubit".

author = 'PNNL'
version
pennylane_requires = '>=0.45.0'
supports_derivatives(execution_config=None, circuit=None)

Determine whether or not a device provided derivative is potentially available.

Default behaviour assumes first order device derivatives for all circuits exist if compute_derivatives() is overriden.

Parameters:
  • execution_config (ExecutionConfig) – A description of the hyperparameters for the desired computation.

  • circuit (None, QuantumTape) – A specific circuit to check differentation for.

Returns:

Bool

Return type:

bool

The device can support multiple different types of “device derivatives”, chosen via execution_config.gradient_method. For example, a device can natively calculate "parameter-shift" derivatives, in which case compute_derivatives() will be called for the derivative instead of execute() with a batch of circuits.

>>> config = ExecutionConfig(gradient_method="parameter-shift")
>>> custom_device.supports_derivatives(config)
True

In this case, compute_derivatives() or execute_and_compute_derivatives() will be called instead of execute() with a batch of circuits.

If circuit is not provided, then the method should return whether or not device derivatives exist for any circuit.

Example:

For example, the Python device will support device differentiation via the adjoint differentiation algorithm if the order is 1 and the execution occurs with no shots (shots=None).

>>> config = ExecutionConfig(derivative_order=1, gradient_method="adjoint")
>>> dev.supports_derivatives(config)
True
>>> circuit_analytic = qp.tape.QuantumScript([qp.RX(0.1, wires=0)], [qp.expval(qp.Z(0))], shots=None)
>>> dev.supports_derivatives(config, circuit=circuit_analytic)
True
>>> circuit_finite_shots = qp.tape.QuantumScript([qp.RX(0.1, wires=0)], [qp.expval(qp.Z(0))], shots=10)
>>> dev.supports_derivatives(config, circuit = circuit_finite_shots)
False
>>> config = ExecutionConfig(derivative_order=2, gradient_method="adjoint")
>>> dev.supports_derivatives(config)
False

Adjoint differentiation will only be supported for circuits with expectation value measurements. If a circuit is provided and it cannot be converted to a form supported by differentiation method by preprocess(), then supports_derivatives should return False.

>>> config = ExecutionConfig(derivative_order=1, gradient_method="adjoint")
>>> circuit = qp.tape.QuantumScript([qp.RX(2.0, wires=0)], [qp.probs(wires=(0,1))])
>>> dev.supports_derivatives(config, circuit=circuit)
False

If the circuit is not natively supported by the differentiation method but can be converted into a form that is supported, it should still return True. For example, Rot gates are not natively supported by adjoint differentation, as they do not have a generator, but they can be compiled into operations supported by adjoint differentiation. Therefore this method may reproduce compilation and validation steps performed by preprocess().

>>> config = ExecutionConfig(derivative_order=1, gradient_method="adjoint")
>>> circuit = qp.tape.QuantumScript([qp.Rot(1.2, 2.3, 3.4, wires=0)], [qp.expval(qp.Z(0))])
>>> dev.supports_derivatives(config, circuit=circuit)
True

Backpropagation:

This method is also used be to validate support for backpropagation derivatives. Backpropagation is only supported if the device is transparent to the machine learning framework from start to finish.

>>> config = ExecutionConfig(gradient_method="backprop")
>>> python_device.supports_derivatives(config)
True
>>> cpp_device.supports_derivatives(config)
False
setup_execution_config(config=None, circuit=None)

Sets up an ExecutionConfig that configures the execution behaviour.

The execution config stores information on how the device should perform the execution, as well as how PennyLane should interact with the device. See ExecutionConfig for all available options and what they mean.

An ExecutionConfig is constructed from arguments passed to the QNode, and this method allows the device to update the config object based on device-specific requirements or preferences. See Execution Config for more details.

Parameters:
  • config (ExecutionConfig) – The initial ExecutionConfig object that describes the parameters needed to configure the execution behaviour.

  • circuit (QuantumScript) – The quantum circuit to customize the execution config for.

Returns:

The updated ExecutionConfig object

Return type:

ExecutionConfig

preprocess_transforms(execution_config=None)

Returns the compile pileline to preprocess a circuit for execution.

Parameters:

execution_config (ExecutionConfig) – The execution configuration object

Returns:

A compile pileline that is called before execution

Return type:

CompilePipeline

The compile pileline is composed of a list of individual transforms, which may include:

  • Decomposition of operations and measurements to what is supported by the device.

  • Splitting a circuit with measurements of non-commuting observables or Hamiltonians into multiple executions.

  • Splitting a circuit with batched parameters into multiple executions.

  • Validation of wires, measurements, and observables.

  • Gradient specific preprocessing, such as making sure trainable operators have generators.

Example

All transforms that are part of the preprocessing compile pileline need to respect the transform contract defined in pennylane.transform().

from pennylane.tape import QuantumScriptBatch
from pennylane.typing import PostprocessingFn

@qp.transform
def my_preprocessing_transform(tape: qp.tape.QuantumScript) -> tuple[QuantumScriptBatch, PostprocessingFn]:
    # e.g. valid the measurements, expand the tape for the hardware execution, ...

    def blank_processing_fn(results):
        return results[0]

    return [tape], processing_fn

A compile pileline can hold an arbitrary number of individual transforms:

def preprocess(self, config):
    program = CompilePipeline()
    program.add_transform(my_preprocessing_transform)
    return program

See also

transform() and CompilePipeline

execute(circuits, execution_config=None)

Execute a circuit or a batch of circuits and turn it into results.

Parameters:
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – the quantum circuits to be executed

  • execution_config (ExecutionConfig) – a datastructure with additional information required for execution

Returns:

A numeric result of the computation.

Return type:

TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]

Interface parameters:

The provided circuits may contain interface specific data-types like torch.Tensor or jax.Array when gradient_method of "backprop" is requested. If the gradient method is not backpropagation, then only vanilla numpy parameters or builtins will be present in the circuits.