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-(in)dependent 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 PyQTorch 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 PyQTorch
backend.
from qadence import backend_factory
# Use only PyQtorch in non-differentiable mode:
backend = backend_factory("pyqtorch")
# 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 = QuantumCircuit(
(operations): ModuleList(
(0): Sequence(
(operations): ModuleList(
(0): Sequence(
(operations): ModuleList(
(0): RX(target: (0,), param: a9bbc203-adcd-4f0e-a689-d80af8796e0b)
(1): RZ(target: (1,), param: 49d73e9d-3655-4457-bf59-1f7fc1d48b08)
(2): CNOT(control: (0,), target: (1,))
)
)
(1): Sequence(
(operations): ModuleList(
(0): Sequence(
(operations): ModuleList(
(0): Merge(
(operations): ModuleList(
(0): RX(target: (0,), param: be6a545f-531f-4e57-8778-356c51e2e9e1)
(1): RY(target: (0,), param: 6480c1e2-569f-4c7d-a28b-6c711b3bd8da)
(2): RX(target: (0,), param: ba64254f-9f5d-4c04-911d-1f7a8804b98f)
)
)
(1): Merge(
(operations): ModuleList(
(0): RX(target: (1,), param: 9c45ac09-81c8-416b-961c-0d62a9a4c5da)
(1): RY(target: (1,), param: f706bb62-7cd6-478d-ab1e-c3c597d932a7)
(2): RX(target: (1,), param: d8c5930a-4fb4-4e14-bf83-789d2dac45de)
)
)
(2): Merge(
(operations): ModuleList(
(0): RX(target: (2,), param: 99a1c978-8e71-45b4-853e-21d6041d36a4)
(1): RY(target: (2,), param: 64012d83-39db-4773-8a46-76afd8492f65)
(2): RX(target: (2,), param: 743ff0eb-7a79-4e4d-bec0-0c5664535518)
)
)
)
)
(1): Sequence(
(operations): ModuleList(
(0): Sequence(
(operations): ModuleList(
(0): CNOT(control: (0,), target: (1,))
)
)
(1): Sequence(
(operations): ModuleList(
(0): CNOT(control: (1,), target: (2,))
)
)
)
)
)
)
)
)
)
)
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_0: tensor([0.8431], requires_grad=True)
theta_1: tensor([0.6186], requires_grad=True)
theta_8: tensor([0.6764], requires_grad=True)
theta_6: tensor([0.8632], requires_grad=True)
theta_5: tensor([0.1147], requires_grad=True)
theta_4: tensor([0.3033], requires_grad=True)
theta_2: tensor([0.8173], requires_grad=True)
theta_7: tensor([0.3063], requires_grad=True)
theta_3: tensor([0.3386], requires_grad=True)
}
embedded = {
a9bbc203-adcd-4f0e-a689-d80af8796e0b: tensor([3., 3.], grad_fn=<ViewBackward0>)
49d73e9d-3655-4457-bf59-1f7fc1d48b08: tensor([2., 2.])
be6a545f-531f-4e57-8778-356c51e2e9e1: tensor([0.8431], grad_fn=<ViewBackward0>)
6480c1e2-569f-4c7d-a28b-6c711b3bd8da: tensor([0.3386], grad_fn=<ViewBackward0>)
ba64254f-9f5d-4c04-911d-1f7a8804b98f: tensor([0.8632], grad_fn=<ViewBackward0>)
9c45ac09-81c8-416b-961c-0d62a9a4c5da: tensor([0.6186], grad_fn=<ViewBackward0>)
f706bb62-7cd6-478d-ab1e-c3c597d932a7: tensor([0.3033], grad_fn=<ViewBackward0>)
d8c5930a-4fb4-4e14-bf83-789d2dac45de: tensor([0.3063], grad_fn=<ViewBackward0>)
99a1c978-8e71-45b4-853e-21d6041d36a4: tensor([0.8173], grad_fn=<ViewBackward0>)
64012d83-39db-4773-8a46-76afd8492f65: tensor([0.1147], grad_fn=<ViewBackward0>)
743ff0eb-7a79-4e4d-bec0-0c5664535518: tensor([0.6764], grad_fn=<ViewBackward0>)
}
With the embedded parameters, QuantumModel
methods are accessible:
output = tensor([[ 0.2758-0.0176j, 0.0038-0.2567j, 0.4084+0.1885j, -0.1677+0.4542j,
-0.3735-0.2121j, -0.2236+0.3304j, 0.1683+0.0867j, -0.0788+0.1879j],
[ 0.2758-0.0176j, 0.0038-0.2567j, 0.4084+0.1885j, -0.1677+0.4542j,
-0.3735-0.2121j, -0.2236+0.3304j, 0.1683+0.0867j, -0.0788+0.1879j]],
grad_fn=<TBackward0>)
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, should one wish to use PyQtorch noise features directly instead of using the NoiseHandler
interface from Qadence:
from pyqtorch.noise import Depolarizing
inputs = {"x": torch.rand(1), "y":torch.rand(1)}
embedded = conv.embedding_fn(conv.params, inputs)
# Define a noise channel on qubit 0
noise = Depolarizing(0, error_probability=0.1)
# Add noise to circuit
conv.circuit.native.operations.append(noise)
When running With noise, one can see that the output is a density matrix: