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
.
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))
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 abstractQuantumCircuit
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': e1fd1076-f7bb-4048-a612-92830c5bf19d, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rz('angle': 31ba4283-a3bb-4f3b-b409-1804ea562f91, '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': 59b89efe-9d60-45ab-8aa5-eceb59e31d3a, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': ead03196-f942-46cb-bbf8-371e230f97ff, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 06e9c48e-94f2-4f44-ac15-d1a61dc1bc8b, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 68b8bfbf-e2a1-4f0f-9694-bd8402b72245, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 47f6f066-f7e1-45a0-b61f-8b3e6f54971b, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 244572ae-950b-4d33-8cd5-813a9be2381f, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': c3d19496-1bcf-4aa2-8ee7-5e7a674e1a5c, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': ba633008-7477-426c-aca1-71a2d471a518, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': fd5b789c-60c1-4c8d-8066-2a3ce7dda469, '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_2: tensor([0.2771], requires_grad=True)
theta_6: tensor([0.1154], requires_grad=True)
theta_3: tensor([0.4624], requires_grad=True)
theta_4: tensor([0.8324], requires_grad=True)
theta_0: tensor([0.8457], requires_grad=True)
theta_1: tensor([0.9694], requires_grad=True)
theta_8: tensor([0.1614], requires_grad=True)
theta_5: tensor([0.7537], requires_grad=True)
theta_7: tensor([0.6049], requires_grad=True)
}
embedded = {
e1fd1076-f7bb-4048-a612-92830c5bf19d: tensor([3., 3.], grad_fn=<ViewBackward0>)
31ba4283-a3bb-4f3b-b409-1804ea562f91: tensor([2., 2.])
59b89efe-9d60-45ab-8aa5-eceb59e31d3a: tensor([0.8457], grad_fn=<ViewBackward0>)
ead03196-f942-46cb-bbf8-371e230f97ff: tensor([0.9694], grad_fn=<ViewBackward0>)
06e9c48e-94f2-4f44-ac15-d1a61dc1bc8b: tensor([0.2771], grad_fn=<ViewBackward0>)
68b8bfbf-e2a1-4f0f-9694-bd8402b72245: tensor([0.4624], grad_fn=<ViewBackward0>)
47f6f066-f7e1-45a0-b61f-8b3e6f54971b: tensor([0.8324], grad_fn=<ViewBackward0>)
244572ae-950b-4d33-8cd5-813a9be2381f: tensor([0.7537], grad_fn=<ViewBackward0>)
c3d19496-1bcf-4aa2-8ee7-5e7a674e1a5c: tensor([0.1154], grad_fn=<ViewBackward0>)
ba633008-7477-426c-aca1-71a2d471a518: tensor([0.6049], grad_fn=<ViewBackward0>)
fd5b789c-60c1-4c8d-8066-2a3ce7dda469: tensor([0.1614], 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_2: tensor([0.2771], grad_fn=<ViewBackward0>)
y: tensor([2., 2.])
theta_6: tensor([0.1154], grad_fn=<ViewBackward0>)
theta_3: tensor([0.4624], grad_fn=<ViewBackward0>)
theta_4: tensor([0.8324], grad_fn=<ViewBackward0>)
theta_0: tensor([0.8457], grad_fn=<ViewBackward0>)
theta_1: tensor([0.9694], grad_fn=<ViewBackward0>)
theta_8: tensor([0.1614], grad_fn=<ViewBackward0>)
theta_7: tensor([0.6049], grad_fn=<ViewBackward0>)
theta_5: tensor([0.7537], grad_fn=<ViewBackward0>)
3*x: tensor([3., 3.], grad_fn=<ViewBackward0>)
orig_param_values: {'x': tensor([1., 1.]), '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 = }")
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)
T : | 0 | 1 | 2 | 3 | 4 |5|6|
q0 : -Rx(e1fd1076-f7bb-4048-a612-92830c5bf19d)-C----------------------------------------Rx(59b89efe-9d60-45ab-8aa5-eceb59e31d3a)-Ry(68b8bfbf-e2a1-4f0f-9694-bd8402b72245)-Rx(c3d19496-1bcf-4aa2-8ee7-5e7a674e1a5c)-C---
| |
q1 : -Rz(31ba4283-a3bb-4f3b-b409-1804ea562f91)-X----------------------------------------Rx(ead03196-f942-46cb-bbf8-371e230f97ff)-Ry(47f6f066-f7e1-45a0-b61f-8b3e6f54971b)-Rx(ba633008-7477-426c-aca1-71a2d471a518)-X-C-
|
q2 : -Rx(06e9c48e-94f2-4f44-ac15-d1a61dc1bc8b)-Ry(244572ae-950b-4d33-8cd5-813a9be2381f)-Rx(fd5b789c-60c1-4c8d-8066-2a3ce7dda469)-------------------------------------------------------------------------------------X-
T : | 0 | 1 | 2 | 3 | 4 |5|6|
Unassigned parameters: [06e9c48e-94f2-4f44-ac15-d1a61dc1bc8b, 244572ae-950b-4d33-8cd5-813a9be2381f, 31ba4283-a3bb-4f3b-b409-1804ea562f91, 47f6f066-f7e1-45a0-b61f-8b3e6f54971b, 59b89efe-9d60-45ab-8aa5-eceb59e31d3a, 68b8bfbf-e2a1-4f0f-9694-bd8402b72245, ba633008-7477-426c-aca1-71a2d471a518, c3d19496-1bcf-4aa2-8ee7-5e7a674e1a5c, e1fd1076-f7bb-4048-a612-92830c5bf19d, ead03196-f942-46cb-bbf8-371e230f97ff, fd5b789c-60c1-4c8d-8066-2a3ce7dda469].
T : | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
q0 : -Rx(1.36)-DEPO(0.1)-C--------DEPO(0.1)-Rx(0.85)-DEPO(0.1)-Ry(0.46)-DEPO(0.1)-Rx(0.12)-DEPO(0.1)-C-DEPO(0.1)-------------
| |
q1 : -Rz(0.87)-DEPO(0.1)-X--------DEPO(0.1)-Rx(0.97)-DEPO(0.1)-Ry(0.83)-DEPO(0.1)-Rx(0.60)-DEPO(0.1)-X-DEPO(0.1)-C-DEPO(0.1)-
|
q2 : -Rx(0.28)-DEPO(0.1)-Ry(0.75)-DEPO(0.1)-Rx(0.16)-DEPO(0.1)---------------------------------------------------X-DEPO(0.1)-
T : | 0 | 1 | 2 | 3 | 4 | 5 | 6 |