qml.devices.default_clifford.DefaultClifford

class DefaultClifford(wires=None, shots=None, check_clifford=True, tableau=True, seed='global', max_workers=None)[source]

Bases: pennylane.devices.device_api.Device

A PennyLane device for fast simulation of Clifford circuits using stim.

Parameters
  • wires (int, Iterable[Number, str]) – Number of wires present on the device, or iterable that contains unique labels for the wires as numbers (i.e., [-1, 0, 2]) or strings (['aux_wire', 'q1', 'q2']). Default None if not specified.

  • shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]) – The default number of shots to use in executions involving this device.

  • check_clifford (bool) – Check if all the gate operations in the circuits to be executed are Clifford. Default is True.

  • tableau (bool) – Determines what should be returned when the device’s state is computed with qml.state. When True, the device returns the final evolved Tableau. Alternatively, one may make it False to obtain the evolved state vector. Note that the latter might not be computationally feasible for larger qubit numbers.

  • seed (Union[str, None, int, array_like[int], SeedSequence, BitGenerator, Generator]) – A seed-like parameter matching that of seed for numpy.random.default_rng, or a request to seed from numpy’s global random number generator. The default, seed="global" pulls a seed from numpy’s global generator. seed=None will pull a seed from the OS entropy.

  • max_workers (int) – A ProcessPoolExecutor executes tapes asynchronously using a pool of at most max_workers processes. If max_workers is None, only the current process executes tapes. If you experience any issue, try setting max_workers to None.

Example:

dev = qml.device("default.clifford", tableau=True)

@qml.qnode(dev)
def circuit():
    qml.CNOT(wires=[0, 1])
    qml.X(1)
    qml.ISWAP(wires=[0, 1])
    qml.Hadamard(wires=[0])
    return qml.state()
>>> circuit()
array([[0, 1, 1, 0, 0],
        [1, 0, 1, 1, 1],
        [0, 0, 0, 1, 0],
        [1, 0, 0, 1, 1]])

The devices execution pipeline can be investigated more closely with the following:

num_qscripts = 5

qscripts = [
    qml.tape.QuantumScript(
        [qml.Hadamard(wires=[0]), qml.CNOT(wires=[0, 1])],
        [qml.expval(qml.Z(0))]
    )
] * num_qscripts
>>> dev = DefaultClifford()
>>> program, execution_config = dev.preprocess()
>>> new_batch, post_processing_fn = program(qscripts)
>>> results = dev.execute(new_batch, execution_config=execution_config)
>>> post_processing_fn(results)
(array(0), array(0), array(0), array(0), array(0))

The device’s internal state is represented by the following Tableau described in the Sec. III, Aaronson & Gottesman (2004):

\[\begin{split}\begin{bmatrix} x_{11} & \cdots & x_{1n} & & z_{11} & \cdots & z_{1n} & &r_{1}\\ \vdots & \ddots & \vdots & & \vdots & \ddots & \vdots & &\vdots\\ x_{n1} & \cdots & x_{nn} & & z_{n1} & \cdots & z_{nn} & &r_{n}\\ & & & & & & & & \\ x_{\left( n+1\right) 1} & \cdots & x_{\left( n+1\right) n} & & z_{\left( n+1\right) 1} & \cdots & z_{\left( n+1\right) n} & & r_{n+1}\\ \vdots & \ddots & \vdots & & \vdots & \ddots & \vdots & & \vdots\\ x_{\left( 2n\right) 1} & \cdots & x_{\left( 2n\right) n} & & z_{\left( 2n\right) 1} & \cdots & z_{\left( 2n\right) n} & & r_{2n} \end{bmatrix}\end{split}\]

The tableau’s first n rows represent a destabilizer generator, while the remaining n rows represent the stabilizer generators. The Pauli representation for all of these generators are described using the binary vector made from the binary variables \(x_{ij},\ z_{ij}\), \(\forall i\in\left\{1,\ldots,2n\right\}, j\in\left\{1,\ldots,n\right\}\) and they together form the complete Pauli group.

Finally, the last column of the tableau, with binary variables \(r_{i},\ \forall i\in\left\{1,\ldots,2n\right\}\), denotes whether the phase is negative (\(r_i = 1\)) or not, for each generator. Maintaining and working with this tableau representation instead of the complete state vector makes the calculations of increasingly large Clifford circuits more efficient on this device.

As the default.clifford device supports executing quantum circuits with a large number of qubits, the ability to compute the analytical probabilities for all computational basis states at once becomes computationally expensive and challenging as the system size increases. While we don’t manually restrict users from doing so for any circuit, one can expect the underlying computation to reach its limit with 20-24 qubits on a typical consumer grade machine.

As long as number of qubits are below this limit, one can simply use the qml.probs function with its usual arguments to compute probabilities for the complete computational basis states. We test this for a circuit that prepares the n-qubit Greenberger-Horne-Zeilinger (GHZ) state. This means that the probabilities for the basis states \(|0\rangle^{\otimes n}\) and \(|1\rangle^{\otimes n}\) should be \(0.5\), and \(0.0\) for the rest.

import pennylane as qml
import numpy as np
dev = qml.device("default.clifford")

num_wires = 3
@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=[0])
    for idx in range(num_wires):
        qml.CNOT(wires=[idx, idx+1])
    return qml.probs()
>>> circuit()
tensor([0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5], requires_grad=True)

Once above the limit (or even otherwise), one can obtain the probability of a single target basis state by computing the expectation value of the corresponding projector using qml.expval and qml.Projector.

num_wires = 4
@qml.qnode(dev)
def circuit(state):
    qml.Hadamard(wires=[0])
    for idx in range(num_wires):
        qml.CNOT(wires=[idx, idx+1])
    return qml.expval(qml.Projector(state, wires=range(num_wires)))
>>> basis_states = np.array([[0, 0, 0, 0], [0, 1, 0, 1], [1, 0, 1, 0]])
>>> circuit(basis_states[0])
tensor(0.5, requires_grad=True)
>>> circuit(basis_states[1])
tensor(0.0, requires_grad=True)
>>> circuit(basis_states[2])
tensor(0.0, requires_grad=True)

This device supports the finite-shot execution of quantum circuits with the following error channels that add Pauli noise, allowing for one to perform any sampling-based measurements.

import pennylane as qml
import numpy as np
dev = qml.device("default.clifford", shots=1024, seed=42)

num_wires = 3
@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=[0])
    for idx in range(num_wires):
        qml.CNOT(wires=[idx, idx+1])
    qml.BitFlip(0.2, wires=[1])
    return qml.counts()
>>> circuit()
{'0000': 417, '0100': 95, '1011': 104, '1111': 408}

DefaultClifford tracks:

  • executions: the number of unique circuits that would be required on quantum hardware

  • shots: the number of shots

  • resources: the Resources for the executed circuit.

  • simulations: the number of simulations performed. One simulation can cover multiple QPU executions, such as for non-commuting measurements and batched parameters.

  • batches: The number of times execute() is called.

  • results: The results of each call of execute().

See the details in DefaultQubit’s “Accelerate calculations with multiprocessing” section. Additional information regarding multiprocessing can be found in the multiprocessing docs page.

name

The name of the device.

shots

Default shots for execution workflows containing this device.

tracker

A Tracker that can store information about device executions, shots, batches, intermediate results, or any additional device dependent information.

wires

The device wires.

name

The name of the device.

shots

Default shots for execution workflows containing this device.

Note that the device itself should always pull shots from the provided QuantumTape and its shots, not from this property. This property is used to provide a default at the start of a workflow.

tracker: pennylane.tracker.Tracker = <pennylane.tracker.Tracker object>

A Tracker that can store information about device executions, shots, batches, intermediate results, or any additional device dependent information.

A plugin developer can store information in the tracker by:

# querying if the tracker is active
if self.tracker.active:

    # store any keyword: value pairs of information
    self.tracker.update(executions=1, shots=self._shots, results=results)

    # Calling a user-provided callback function
    self.tracker.record()
wires

The device wires.

Note that wires are optional, and the default value of None means any wires can be used. If a device has wires defined, they will only be used for certain features. This includes:

  • Validation of tapes being executed on the device

  • Defining the wires used when evaluating a state() measurement

compute_derivatives(circuits[, execution_config])

Calculate the jacobian of either a single or a batch of circuits on the device.

compute_jvp(circuits, tangents[, …])

The jacobian vector product used in forward mode calculation of derivatives.

compute_vjp(circuits, cotangents[, …])

The vector jacobian product used in reverse-mode differentiation.

execute(circuits[, execution_config])

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

execute_and_compute_derivatives(circuits[, …])

Compute the results and jacobians of circuits at the same time.

execute_and_compute_jvp(circuits, tangents)

Execute a batch of circuits and compute their jacobian vector products.

execute_and_compute_vjp(circuits, cotangents)

Calculate both the results and the vector jacobian product used in reverse-mode differentiation.

measure_analytical(circuit, stim_circuit, …)

Given a circuit, compute tableau and return the analytical measurement results.

measure_statistical(circuit, stim_circuit[, …])

Given a circuit, compute samples and return the statistical measurement results.

preprocess([execution_config])

This function defines the device transform program to be applied and an updated device configuration.

simulate(circuit[, seed, debugger])

Simulate a single quantum script.

supports_derivatives([execution_config, circuit])

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

supports_jvp([execution_config, circuit])

Whether or not a given device defines a custom jacobian vector product.

supports_vjp([execution_config, circuit])

Whether or not a given device defines a custom vector jacobian product.

compute_derivatives(circuits, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))

Calculate the jacobian of either a single or a batch of circuits on the device.

Parameters
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – the circuits to calculate derivatives for

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

Returns

The jacobian for each trainable parameter

Return type

Tuple

Execution Config:

The execution config has gradient_method and order property that describes the order of differentiation requested. If the requested method or order of gradient is not provided, the device should raise a NotImplementedError. The supports_derivatives() method can pre-validate supported orders and gradient methods.

Return Shape:

If a batch of quantum scripts is provided, this method should return a tuple with each entry being the gradient of each individual quantum script. If the batch is of length 1, then the return tuple should still be of length 1, not squeezed.

compute_jvp(circuits, tangents, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))

The jacobian vector product used in forward mode calculation of derivatives.

Parameters
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – the circuit or batch of circuits

  • tangents (tensor-like) – Gradient vector for input parameters.

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

Returns

A numeric result of computing the jacobian vector product

Return type

Tuple

Definition of jvp:

If we have a function with jacobian:

\[\vec{y} = f(\vec{x}) \qquad J_{i,j} = \frac{\partial y_i}{\partial x_j}\]

The Jacobian vector product is the inner product with the derivatives of \(x\), yielding only the derivatives of the output \(y\):

\[\text{d}y_i = \Sigma_{j} J_{i,j} \text{d}x_j\]

Shape of tangents:

The tangents tuple should be the same length as circuit.get_parameters() and have a single number per parameter. If a number is zero, then the gradient with respect to that parameter does not need to be computed.

compute_vjp(circuits, cotangents, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))

The vector jacobian product used in reverse-mode differentiation.

Parameters
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – the circuit or batch of circuits

  • cotangents (Tuple[Number, Tuple[Number]]) – Gradient-output vector. Must have shape matching the output shape of the corresponding circuit. If the circuit has a single output, cotangents may be a single number, not an iterable of numbers.

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

Returns

A numeric result of computing the vector jacobian product

Return type

tensor-like

Definition of vjp:

If we have a function with jacobian:

\[\vec{y} = f(\vec{x}) \qquad J_{i,j} = \frac{\partial y_i}{\partial x_j}\]

The vector jacobian product is the inner product of the derivatives of the output y with the Jacobian matrix. The derivatives of the output vector are sometimes called the cotangents.

\[\text{d}x_i = \Sigma_{i} \text{d}y_i J_{i,j}\]

Shape of cotangents:

The value provided to cotangents should match the output of execute().

execute(circuits, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))[source]

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.

See Return Type Specification for more detailed information.

The result for each QuantumTape must match the shape specified by shape.

The level of priority for dimensions from outer dimension to inner dimension is:

  1. Quantum Script in batch

  2. Shot choice in a shot vector

  3. Measurement in the quantum script

  4. Parameter broadcasting

  5. Measurement shape for array-valued measurements like probabilities

For a batch of quantum scripts with multiple measurements, a shot vector, and parameter broadcasting:

  • result[0]: the results for the first script

  • result[0][0]: the first shot number in the shot vector

  • result[0][0][0]: the first measurement in the quantum script

  • result[0][0][0][0]: the first parameter broadcasting choice

  • result[0][0][0][0][0]: the first value for an array-valued measurement

With the exception of quantum script batches, dimensions with only a single component should be eliminated.

For example:

With a single script and a single measurement process, execute should return just the measurement value in a numpy array. shape currently accepts a device, as historically devices stored shot information. In the future, this method will accept an ExecutionConfig instead.

>>> tape = qml.tape.QuantumTape(measurements=qml.expval(qml.Z(0))])
>>> tape.shape(dev)
()
>>> dev.execute(tape)
array(1.0)

If execute recieves a batch of scripts, then it should return a tuple of results:

>>> dev.execute([tape, tape])
(array(1.0), array(1.0))
>>> dev.execute([tape])
(array(1.0),)

If the script has multiple measurments, then the device should return a tuple of measurements.

>>> tape = qml.tape.QuantumTape(measurements=[qml.expval(qml.Z(0)), qml.probs(wires=(0,1))])
>>> tape.shape(dev)
((), (4,))
>>> dev.execute(tape)
(array(1.0), array([1., 0., 0., 0.]))
execute_and_compute_derivatives(circuits, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))

Compute the results and jacobians of circuits at the same time.

Parameters
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – the circuits or batch of circuits

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

Returns

A numeric result of the computation and the gradient.

Return type

tuple

See execute() and compute_derivatives() for more information about return shapes and behaviour. If compute_derivatives() is defined, this method should be as well.

This method can be used when the result and execution need to be computed at the same time, such as during a forward mode calculation of gradients. For certain gradient methods, such as adjoint diff gradients, calculating the result and gradient at the same can save computational work.

execute_and_compute_jvp(circuits, tangents, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))

Execute a batch of circuits and compute their jacobian vector products.

Parameters
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – circuit or batch of circuits

  • tangents (tensor-like) – Gradient vector for input parameters.

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

Returns

A numeric result of execution and of computing the jacobian vector product

Return type

Tuple, Tuple

See also

execute() and compute_jvp()

execute_and_compute_vjp(circuits, cotangents, execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))

Calculate both the results and the vector jacobian product used in reverse-mode differentiation.

Parameters
  • circuits (Union[QuantumTape, Sequence[QuantumTape]]) – the circuit or batch of circuits to be executed

  • cotangents (Tuple[Number, Tuple[Number]]) – Gradient-output vector. Must have shape matching the output shape of the corresponding circuit. If the circuit has a single output, cotangents may be a single number, not an iterable of numbers.

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

Returns

the result of executing the scripts and the numeric result of computing the vector jacobian product

Return type

Tuple, Tuple

See also

execute() and compute_vjp()

measure_analytical(circuit, stim_circuit, tableau_simulator, global_phase)[source]

Given a circuit, compute tableau and return the analytical measurement results.

measure_statistical(circuit, stim_circuit, seed=None)[source]

Given a circuit, compute samples and return the statistical measurement results.

preprocess(execution_config=ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None, derivative_order=1))[source]

This function defines the device transform program to be applied and an updated device configuration.

Parameters

execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]) – A data structure describing the parameters needed to fully describe the execution.

Returns

A transform program that when called returns QuantumTapes that the device can natively execute as well as a postprocessing function to be called after execution, and a configuration with unset specifications filled in.

Return type

TransformProgram, ExecutionConfig

This device currently does not intrinsically support parameter broadcasting.

simulate(circuit, seed=None, debugger=None)[source]

Simulate a single quantum script.

Parameters
  • circuit (QuantumTape) – The single circuit to simulate

  • debugger (_Debugger) – The debugger to use

Returns

The results of the simulation

Return type

tuple(TensorLike)

This function assumes that all operations are Clifford.

>>> qs = qml.tape.QuantumScript([qml.Hadamard(wires=0)], [qml.expval(qml.Z(0)), qml.state()])
>>> qml.devices.DefaultClifford().simulate(qs)
(array(0),
 array([[0, 1, 0],
        [1, 0, 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

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 = qml.tape.QuantumScript([qml.RX(0.1, wires=0)], [qml.expval(qml.Z(0))], shots=None)
>>> dev.supports_derivatives(config, circuit=circuit_analytic)
True
>>> circuit_finite_shots = qml.tape.QuantumScript([qml.RX(0.1, wires=0)], [qml.expval(qml.Z(0))], shots=10)
>>> dev.supports_derivatives(config, circuit = circuit_fintite_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, shots=None, gradient_method="adjoint")
>>> circuit = qml.tape.QuantumScript([qml.RX(2.0, wires=0)], [qml.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, shots=None, gradient_method="adjoint")
>>> circuit = qml.tape.QuantumScript([qml.Rot(1.2, 2.3, 3.4, wires=0)], [qml.expval(qml.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
supports_jvp(execution_config=None, circuit=None)

Whether or not a given device defines a custom jacobian vector product.

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

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

Default behaviour assumes this to be True if compute_jvp() is overridden.

supports_vjp(execution_config=None, circuit=None)

Whether or not a given device defines a custom vector jacobian product.

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

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

Default behaviour assumes this to be True if compute_vjp() is overridden.