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))
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': 7106b8d0-5f16-42d5-afe8-236c61ac8e45, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rz('angle': eab6c08d-87d4-4d1d-b0a9-28d711719517, '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': 7fd8e082-1e52-4e14-aa57-ae12700bc652, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 98392ede-ad8d-417f-bc6a-fe89ebf9994a, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 505b9684-509a-41b4-a107-d439b8d7dcf0, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 0ba7283e-cf71-43b7-adb6-48e0f4660fe9, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 22ba5254-09a4-466f-b04e-3dc4180a1337, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Ry('angle': 98e79e9c-040c-419c-a48c-3e6d96d3cb74, 'qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 0760d5b2-6fdf-4493-a98f-b7707ca7ddef, 'qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 5467abb1-5ec1-4434-9dd3-bd0ba9d9f7cb, 'qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1), Instruction('operator': Rx('angle': 5daef11d-1bb0-485d-8209-20cfd96d7a98, '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_5: tensor([0.3965], requires_grad=True)
theta_6: tensor([0.7306], requires_grad=True)
theta_7: tensor([0.6091], requires_grad=True)
theta_8: tensor([0.2952], requires_grad=True)
theta_1: tensor([0.2035], requires_grad=True)
theta_3: tensor([0.0577], requires_grad=True)
theta_4: tensor([0.9744], requires_grad=True)
theta_0: tensor([0.8941], requires_grad=True)
theta_2: tensor([0.0558], requires_grad=True)
}
embedded = {
7106b8d0-5f16-42d5-afe8-236c61ac8e45: tensor([3., 3.], grad_fn=<ViewBackward0>)
eab6c08d-87d4-4d1d-b0a9-28d711719517: tensor([2., 2.])
7fd8e082-1e52-4e14-aa57-ae12700bc652: tensor([0.8941], grad_fn=<ViewBackward0>)
98392ede-ad8d-417f-bc6a-fe89ebf9994a: tensor([0.2035], grad_fn=<ViewBackward0>)
505b9684-509a-41b4-a107-d439b8d7dcf0: tensor([0.0558], grad_fn=<ViewBackward0>)
0ba7283e-cf71-43b7-adb6-48e0f4660fe9: tensor([0.0577], grad_fn=<ViewBackward0>)
22ba5254-09a4-466f-b04e-3dc4180a1337: tensor([0.9744], grad_fn=<ViewBackward0>)
98e79e9c-040c-419c-a48c-3e6d96d3cb74: tensor([0.3965], grad_fn=<ViewBackward0>)
0760d5b2-6fdf-4493-a98f-b7707ca7ddef: tensor([0.7306], grad_fn=<ViewBackward0>)
5467abb1-5ec1-4434-9dd3-bd0ba9d9f7cb: tensor([0.6091], grad_fn=<ViewBackward0>)
5daef11d-1bb0-485d-8209-20cfd96d7a98: tensor([0.2952], 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_8: tensor([0.2952], grad_fn=<ViewBackward0>)
theta_5: tensor([0.3965], grad_fn=<ViewBackward0>)
y: tensor([2., 2.])
theta_6: tensor([0.7306], grad_fn=<ViewBackward0>)
theta_7: tensor([0.6091], grad_fn=<ViewBackward0>)
3*x: tensor([3., 3.], grad_fn=<ViewBackward0>)
theta_1: tensor([0.2035], grad_fn=<ViewBackward0>)
theta_3: tensor([0.0577], grad_fn=<ViewBackward0>)
theta_4: tensor([0.9744], grad_fn=<ViewBackward0>)
theta_0: tensor([0.8941], grad_fn=<ViewBackward0>)
theta_2: tensor([0.0558], grad_fn=<ViewBackward0>)
}
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(7106b8d0-5f16-42d5-afe8-236c61ac8e45)-C----------------------------------------Rx(7fd8e082-1e52-4e14-aa57-ae12700bc652)-Ry(0ba7283e-cf71-43b7-adb6-48e0f4660fe9)-Rx(0760d5b2-6fdf-4493-a98f-b7707ca7ddef)-C---
| |
q1 : -Rz(eab6c08d-87d4-4d1d-b0a9-28d711719517)-X----------------------------------------Rx(98392ede-ad8d-417f-bc6a-fe89ebf9994a)-Ry(22ba5254-09a4-466f-b04e-3dc4180a1337)-Rx(5467abb1-5ec1-4434-9dd3-bd0ba9d9f7cb)-X-C-
|
q2 : -Rx(505b9684-509a-41b4-a107-d439b8d7dcf0)-Ry(98e79e9c-040c-419c-a48c-3e6d96d3cb74)-Rx(5daef11d-1bb0-485d-8209-20cfd96d7a98)-------------------------------------------------------------------------------------X-
T : | 0 | 1 | 2 | 3 | 4 |5|6|
Unassigned parameters: [0760d5b2-6fdf-4493-a98f-b7707ca7ddef, 0ba7283e-cf71-43b7-adb6-48e0f4660fe9, 22ba5254-09a4-466f-b04e-3dc4180a1337, 505b9684-509a-41b4-a107-d439b8d7dcf0, 5467abb1-5ec1-4434-9dd3-bd0ba9d9f7cb, 5daef11d-1bb0-485d-8209-20cfd96d7a98, 7106b8d0-5f16-42d5-afe8-236c61ac8e45, 7fd8e082-1e52-4e14-aa57-ae12700bc652, 98392ede-ad8d-417f-bc6a-fe89ebf9994a, 98e79e9c-040c-419c-a48c-3e6d96d3cb74, eab6c08d-87d4-4d1d-b0a9-28d711719517].
T : | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
q0 : -Rx(2.81)-DEPO(0.1)-C--------DEPO(0.1)-Rx(0.89)-DEPO(0.1)-Ry(0.06)-DEPO(0.1)-Rx(0.73)-DEPO(0.1)-C-DEPO(0.1)-------------
| |
q1 : -Rz(0.45)-DEPO(0.1)-X--------DEPO(0.1)-Rx(0.20)-DEPO(0.1)-Ry(0.97)-DEPO(0.1)-Rx(0.61)-DEPO(0.1)-X-DEPO(0.1)-C-DEPO(0.1)-
|
q2 : -Rx(0.06)-DEPO(0.1)-Ry(0.40)-DEPO(0.1)-Rx(0.30)-DEPO(0.1)---------------------------------------------------X-DEPO(0.1)-
T : | 0 | 1 | 2 | 3 | 4 | 5 | 6 |