Skip to content

Backends

Backends allow execution of Qadence abstract quantum circuits. They could be chosen from a variety of simulators, emulators and hardware and can enable circuit differentiability. The primary way to interact and configure a backend is via the high-level API QuantumModel.

Not all backends are equivalent

Not all backends support the same set of operations, especially while executing analog blocks. Qadence will throw descriptive errors in such cases.

Execution backends

PyQTorch: An efficient, large-scale simulator designed for quantum machine learning, seamlessly integrated with the popular PyTorch deep learning framework for automatic differentiability. It also offers analog computing for time-independent pulses. See PyQTorchBackend.

Pulser: A Python library for pulse-level/analog control of neutral atom devices. Execution via QuTiP. See PulserBackend.

Braket: A Python SDK for interacting with quantum devices on Amazon Braket. Currently, only the devices with the digital interface of Amazon Braket are supported and execution is performed using the local simulator. Execution on remote simulators and quantum processing units will be available soon. See BraketBackend

More: Proprietary Qadence extensions provide more high-performance backends based on tensor networks or differentiation engines. For more enquiries, please contact: info@pasqal.com.

Differentiation backend

The DifferentiableBackend class enables different differentiation modes for the given backend. This can be chosen from two types:

  • Automatic differentiation (AD): available for PyTorch based backends (PyQTorch).
  • Parameter Shift Rules (PSR): available for all backends. See this section for more information on differentiability and PSR.

In practice, only a diff_mode should be provided in the QuantumModel. Please note that diff_mode defaults to None:

import sympy
import torch
from qadence import Parameter, RX, RZ, Z, CNOT, QuantumCircuit, QuantumModel, chain, BackendName, DiffMode

x = Parameter("x", trainable=False)
y = Parameter("y", trainable=False)
fm = chain(
    RX(0, 3 * x),
    RX(0, x),
    RZ(1, sympy.exp(y)),
    RX(0, 3.14),
    RZ(1, "theta")
)

ansatz = CNOT(0, 1)
block = chain(fm, ansatz)

circuit = QuantumCircuit(2, block)

observable = Z(0)

# DiffMode.GPSR is available for any backend.
# DiffMode.AD is only available for natively differentiable backends.
model = QuantumModel(circuit, observable, backend=BackendName.PYQTORCH, diff_mode=DiffMode.GPSR)

# Get some values for the feature parameters.
values = {"x": (x := torch.tensor([0.5], requires_grad=True)), "y": torch.tensor([0.1])}

# Compute expectation.
exp = model.expectation(values)

# Differentiate the expectation wrt x.
dexp_dx = torch.autograd.grad(exp, x, torch.ones_like(exp))
dexp_dx = (tensor([3.6398]),)

Low-level backend_factory interface

Every backend in Qadence inherits from the abstract Backend class: Backend and implement the following methods:

  • run: propagate the initial state according to the quantum circuit and return the final wavefunction object.
  • sample: sample from a circuit.
  • expectation: computes the expectation of a circuit given an observable.
  • convert: convert the abstract QuantumCircuit object to its backend-native representation including a backend specific parameter embedding function.

Backends are purely functional objects which take as input the values for the circuit parameters and return the desired output from a call to a method. In order to use a backend directly, embedded parameters must be supplied as they are returned by the backend specific embedding function.

Here is a simple demonstration of the use of the Braket backend to execute a circuit in non-differentiable mode:

from qadence import QuantumCircuit, FeatureParameter, RX, RZ, CNOT, hea, chain

# Construct a feature map.
x = FeatureParameter("x")
z = FeatureParameter("y")
fm = chain(RX(0, 3 * x), RZ(1, z), CNOT(0, 1))

# Construct a circuit with an hardware-efficient ansatz.
circuit = QuantumCircuit(3, fm, hea(3,1))

The abstract QuantumCircuit can now be converted to its native representation via the Braket backend.

from qadence import backend_factory

# Use only Braket in non-differentiable mode:
backend = backend_factory("braket")

# The `Converted` object
# (contains a `ConvertedCircuit` with the original and native representation)
conv = backend.convert(circuit)
conv.circuit.original = ChainBlock(0,1,2)
├── ChainBlock(0,1)
   ├── RX(0) [params: ['3*x']]
   ├── RZ(1) [params: ['y']]
   └── CNOT(0, 1)
└── ChainBlock(0,1,2) [tag: HEA]
    ├── ChainBlock(0,1,2)
       ├── KronBlock(0,1,2)
          ├── RX(0) [params: ['theta_0']]
          ├── RX(1) [params: ['theta_1']]
          └── RX(2) [params: ['theta_2']]
       ├── KronBlock(0,1,2)
          ├── RY(0) [params: ['theta_3']]
          ├── RY(1) [params: ['theta_4']]
          └── RY(2) [params: ['theta_5']]
       └── KronBlock(0,1,2)
           ├── RX(0) [params: ['theta_6']]
           ├── RX(1) [params: ['theta_7']]
           └── RX(2) [params: ['theta_8']]
    └── ChainBlock(0,1,2)
        ├── KronBlock(0,1)
           └── CNOT(0, 1)
        └── KronBlock(1,2)
            └── CNOT(1, 2)
conv.circuit.native = Circuit('instructions': [Instruction('operator': Rx('angle': 1373dbff-8440-4775-b1d4-7a1a3421e5f5, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rz('angle': 0c182d52-94d6-492c-97af-caf550c5d28d, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(0), Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': bd1f963d-3140-4037-bf29-0111ff4f42f1, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 69237037-cf00-49f7-8609-a9435d56f051, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': acfe1714-d4b4-456a-bb2f-bc6e6b27ba5d, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': cb977755-01e1-4480-8176-2967d70bb47d, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 1a2f761a-415f-4895-965a-6fc4997d5cba, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': d3640a34-5747-4a7f-971e-f3c8e1e7c90b, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 1f50629a-986e-4ad7-a6ac-81eb92909a3c, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': ec07f179-fa18-46e4-8685-27bc796dbfb9, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 717ad41d-75cb-43c2-b9ae-61e8f30074eb, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(0), Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(1), Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)])

Additionally, Converted contains all fixed and variational parameters, as well as an embedding function which accepts feature parameters to construct a dictionary of circuit native parameters. These are needed as each backend uses a different representation of the circuit parameters:

import torch

# Contains fixed parameters and variational (from the HEA)
conv.params

inputs = {"x": torch.tensor([1., 1.]), "y":torch.tensor([2., 2.])}

# get all circuit parameters (including feature params)
embedded = conv.embedding_fn(conv.params, inputs)
conv.params = {
  theta_6: tensor([0.1293], requires_grad=True)
  theta_5: tensor([0.1514], requires_grad=True)
  theta_0: tensor([0.0855], requires_grad=True)
  theta_8: tensor([0.9126], requires_grad=True)
  theta_4: tensor([0.9829], requires_grad=True)
  theta_2: tensor([0.8557], requires_grad=True)
  theta_1: tensor([0.6079], requires_grad=True)
  theta_3: tensor([0.7997], requires_grad=True)
  theta_7: tensor([0.4321], requires_grad=True)
}
embedded = {
  1373dbff-8440-4775-b1d4-7a1a3421e5f5: tensor([3., 3.], grad_fn=<ViewBackward0>)
  0c182d52-94d6-492c-97af-caf550c5d28d: tensor([2., 2.])
  bd1f963d-3140-4037-bf29-0111ff4f42f1: tensor([0.0855], grad_fn=<ViewBackward0>)
  69237037-cf00-49f7-8609-a9435d56f051: tensor([0.6079], grad_fn=<ViewBackward0>)
  acfe1714-d4b4-456a-bb2f-bc6e6b27ba5d: tensor([0.8557], grad_fn=<ViewBackward0>)
  cb977755-01e1-4480-8176-2967d70bb47d: tensor([0.7997], grad_fn=<ViewBackward0>)
  1a2f761a-415f-4895-965a-6fc4997d5cba: tensor([0.9829], grad_fn=<ViewBackward0>)
  d3640a34-5747-4a7f-971e-f3c8e1e7c90b: tensor([0.1514], grad_fn=<ViewBackward0>)
  1f50629a-986e-4ad7-a6ac-81eb92909a3c: tensor([0.1293], grad_fn=<ViewBackward0>)
  ec07f179-fa18-46e4-8685-27bc796dbfb9: tensor([0.4321], grad_fn=<ViewBackward0>)
  717ad41d-75cb-43c2-b9ae-61e8f30074eb: tensor([0.9126], grad_fn=<ViewBackward0>)
}

Note that above the parameters keys have changed as they now address the keys on the Braket device. A more readable embedding is provided by the PyQTorch backend:

from qadence import BackendName, DiffMode
pyq_backend = backend_factory(backend=BackendName.PYQTORCH, diff_mode=DiffMode.AD)

# the `Converted` object
# (contains a `ConvertedCircuit` wiht the original and native representation)
pyq_conv = pyq_backend.convert(circuit)
embedded = pyq_conv.embedding_fn(pyq_conv.params, inputs)
embedded = {
  theta_6: tensor([0.1293], grad_fn=<ViewBackward0>)
  theta_5: tensor([0.1514], grad_fn=<ViewBackward0>)
  theta_0: tensor([0.0855], grad_fn=<ViewBackward0>)
  theta_8: tensor([0.9126], grad_fn=<ViewBackward0>)
  theta_4: tensor([0.9829], grad_fn=<ViewBackward0>)
  3*x: tensor([3., 3.], grad_fn=<ViewBackward0>)
  theta_2: tensor([0.8557], grad_fn=<ViewBackward0>)
  theta_1: tensor([0.6079], grad_fn=<ViewBackward0>)
  theta_3: tensor([0.7997], grad_fn=<ViewBackward0>)
  theta_7: tensor([0.4321], grad_fn=<ViewBackward0>)
  y: tensor([2., 2.])
}

With the embedded parameters, QuantumModel methods are accessible:

embedded = conv.embedding_fn(conv.params, inputs)
samples = backend.run(conv.circuit, embedded)
print(f"{samples = }")
samples = tensor([[ 0.0173-0.1884j, -0.2276-0.0444j,  0.1486-0.1517j,  0.1353+0.1081j,
         -0.3882-0.2313j, -0.3302+0.4449j,  0.4352-0.0724j,  0.0946+0.3473j],
        [ 0.0173-0.1884j, -0.2276-0.0444j,  0.1486-0.1517j,  0.1353+0.1081j,
         -0.3882-0.2313j, -0.3302+0.4449j,  0.4352-0.0724j,  0.0946+0.3473j]])

Lower-level: the Backend representation

If there is a requirement to work with a specific backend, it is possible to access directly the native circuit. For example, Braket noise features can be imported which are not exposed directly by Qadence.

from braket.circuits import Noise

# Get the native Braket circuit with the given parameters
inputs = {"x": torch.rand(1), "y":torch.rand(1)}
embedded = conv.embedding_fn(conv.params, inputs)
native = backend.assign_parameters(conv.circuit, embedded)

# Define a noise channel
noise = Noise.Depolarizing(probability=0.1)

# Add noise to every gate in the circuit
native.apply_gate_noise(noise)

In order to run this noisy circuit, the density matrix simulator is needed in Braket:

from braket.devices import LocalSimulator

device = LocalSimulator("braket_dm")
result = device.run(native, shots=1000).result().measurement_counts
print(result)
Counter({'110': 168, '101': 165, '100': 164, '111': 124, '010': 119, '001': 99, '000': 84, '011': 77})
print(conv.circuit.native.diagram())
T  : |                   0                    |                   1                    |                   2                    |                   3                    |                   4                    |5|6|

q0 : -Rx(1373dbff-8440-4775-b1d4-7a1a3421e5f5)-C----------------------------------------Rx(bd1f963d-3140-4037-bf29-0111ff4f42f1)-Ry(cb977755-01e1-4480-8176-2967d70bb47d)-Rx(1f50629a-986e-4ad7-a6ac-81eb92909a3c)-C---
                                               |                                                                                                                                                                   |   
q1 : -Rz(0c182d52-94d6-492c-97af-caf550c5d28d)-X----------------------------------------Rx(69237037-cf00-49f7-8609-a9435d56f051)-Ry(1a2f761a-415f-4895-965a-6fc4997d5cba)-Rx(ec07f179-fa18-46e4-8685-27bc796dbfb9)-X-C-
                                                                                                                                                                                                                     | 
q2 : -Rx(acfe1714-d4b4-456a-bb2f-bc6e6b27ba5d)-Ry(d3640a34-5747-4a7f-971e-f3c8e1e7c90b)-Rx(717ad41d-75cb-43c2-b9ae-61e8f30074eb)-------------------------------------------------------------------------------------X-

T  : |                   0                    |                   1                    |                   2                    |                   3                    |                   4                    |5|6|

Unassigned parameters: [0c182d52-94d6-492c-97af-caf550c5d28d, 1373dbff-8440-4775-b1d4-7a1a3421e5f5, 1a2f761a-415f-4895-965a-6fc4997d5cba, 1f50629a-986e-4ad7-a6ac-81eb92909a3c, 69237037-cf00-49f7-8609-a9435d56f051, 717ad41d-75cb-43c2-b9ae-61e8f30074eb, acfe1714-d4b4-456a-bb2f-bc6e6b27ba5d, bd1f963d-3140-4037-bf29-0111ff4f42f1, cb977755-01e1-4480-8176-2967d70bb47d, d3640a34-5747-4a7f-971e-f3c8e1e7c90b, ec07f179-fa18-46e4-8685-27bc796dbfb9].
print(native.diagram())
T  : |        0         |        1         |        2         |        3         |        4         |     5     |     6     |

q0 : -Rx(2.60)-DEPO(0.1)-C--------DEPO(0.1)-Rx(0.09)-DEPO(0.1)-Ry(0.80)-DEPO(0.1)-Rx(0.13)-DEPO(0.1)-C-DEPO(0.1)-------------
                         |                                                                           |                       
q1 : -Rz(0.04)-DEPO(0.1)-X--------DEPO(0.1)-Rx(0.61)-DEPO(0.1)-Ry(0.98)-DEPO(0.1)-Rx(0.43)-DEPO(0.1)-X-DEPO(0.1)-C-DEPO(0.1)-
                                                                                                                 |           
q2 : -Rx(0.86)-DEPO(0.1)-Ry(0.15)-DEPO(0.1)-Rx(0.91)-DEPO(0.1)---------------------------------------------------X-DEPO(0.1)-

T  : |        0         |        1         |        2         |        3         |        4         |     5     |     6     |