Quantum models
A quantum program can be expressed and executed using the QuantumModel type.
It serves three primary purposes:
Parameter handling: by conveniently handling and embedding the two parameter types that Qadence supports: feature and variational (see more details in the previous section).
Differentiability: by enabling a differentiable backend that supports two differentiable modes: automatic differentiation (AD) and parameter shift rules (PSR). The former is used general differentiation in statevector simulators based on PyTorch and JAX. The latter is a quantum specific method used to differentiate gate parameters, and is enabled for all backends.
Execution: by defining which backend the program is expected to be executed on. Qadence supports circuit compilation to the native backend representation.
The base QuantumModel exposes the following methods:
QuantumModel.run(): To extract the wavefunction after circuit execution. Not supported by all backends.QuantumModel.sample(): Sample a bitstring from the resulting quantum state after circuit execution. Supported by all backends.QuantumModel.expectation(): Compute the expectation value of an observable.
Every QuantumModel is an instance of a torch.nn.Module (external) that enables differentiability for its expectation method. For statevector simulators, AD also works for the statevector itself.
To construct a QuantumModel, the program block must first be initialized into a QuantumCircuit instance by combining it with a Register. An integer number can also be passed for the total number of qubits, which instantiates a Register automatically. The qubit register also includes topological information on the qubit layout, essential for digital-analog computations. However, we will explore that in a later tutorial. For now, let's construct a simple parametrized quantum circuit.
from qadence import QuantumCircuit, RX, RY, chain, kronfrom qadence import FeatureParameter, VariationalParameter
theta = VariationalParameter("theta")phi = FeatureParameter("phi")
block = chain( kron(RX(0, theta), RY(1, theta)), kron(RX(0, phi), RY(1, phi)),)
circuit = QuantumCircuit(2, block)unique_params = circuit.unique_parametersunique_params = [theta, phi]The model can then be instantiated. Similarly to the direct execution functions shown in the previous tutorial, the run, sample and expectation methods are available directly from the model.
import torchfrom qadence import QuantumModel, PI, Z
observable = Z(0) + Z(1)
model = QuantumModel(circuit, observable)
values = {"phi": torch.tensor([PI, PI/2])}
wf = model.run(values)xs = model.sample(values, n_shots=100)ex = model.expectation(values)wf = tensor([[ 0.0516+0.0000j, -0.2211+0.0000j, 0.0000+0.2211j, 0.0000-0.9484j], [ 0.2789+0.0000j, 0.4484+0.0000j, 0.0000-0.4484j, 0.0000-0.7211j]])xs = [OrderedCounter({'11': 91, '10': 6, '01': 3}), OrderedCounter({'11': 51, '01': 22, '10': 16, '00': 11})]ex = tensor([[-1.7938], [-0.8846]])By default, the forward method of QuantumModel calls model.run(). To define custom quantum models, the best way is to inherit from QuantumModel and override the forward method, as typically done with custom PyTorch Modules.
The QuantumModel class provides convenience methods to manipulate parameters. Being a torch.nn.Module, all torch methods are also available. As shown in the example below, you can pass, check and reset the model parameters. When entering new values for the VariationalParameter, they must match the number of existing variables.
# To pass onto a torch optimizerparameter_generator = model.parameters()
# Number of variational parametersnum_vparams = model.num_vparams
# Dictionary to see all the parameter valuesparams_values = model.params
# Dictionary to easily inspect variational parameters (parameters with gradient)vparams_values = model.vparams
# To reset current variational parameter to other valuesmodel.reset_vparams([torch.rand(1).item()])
vparams_values = model.vparamsold vparams_values = OrderedDict([('theta', tensor([0.4581]))])new vparams_values = OrderedDict([('theta', tensor([0.0654]))])Backend configuration in quantum models
Section titled “Backend configuration in quantum models”When initializing a quantum model, available configuration options are determined by the backend, with current support for PyQTorch and Pulser. Information on each configuration option can be found with model.show_config as in below example:
from qadence import QuantumModel, QuantumCircuit, RX, RY, kronfrom qadence import FeatureParameter, VariationalParameterfrom qadence import BackendConfigurationfrom qadence import BackendName, DiffMode
# Create a quantum circuittheta = VariationalParameter("theta")phi = FeatureParameter("phi")block = kron(RX(0, theta), RY(1, phi))circuit = QuantumCircuit(2, block)
# Choose your backend (PYQTORCH or PULSER)backend=BackendName.PYQTORCH# backend=BackendName.PULSER
model = QuantumModel(circuit, backend=backend, diff_mode=DiffMode.GPSR)# Check your available configuration options and current valuesName: use_sparse_observable - Type: bool - Current value: False - Default value: FalseName: use_gradient_checkpointing - Type: bool - Current value: False - Default value: FalseName: use_single_qubit_composition - Type: bool - Current value: True - Default value: FalseName: transpilation_passes - Type: list[Callable] | None - Current value: None - Default value: NoneName: algo_hevo - Type: AlgoHEvo - Current value: EXP - Default value: EXPName: ode_solver - Type: SolverType - Current value: dp5_se - Default value: dp5_seName: n_steps_hevo - Type: int - Current value: 100 - Default value: 100Name: loop_expectation - Type: bool - Current value: False - Default value: FalseName: noise - Type: NoiseHandler | None - Current value: None - Default value: NoneName: dropout_probability - Type: float - Current value: 0.0 - Default value: 0.0Name: dropout_mode - Type: DropoutMode - Current value: rotational_dropout - Default value: rotational_dropoutName: n_eqs - Type: int | None - Current value: None - Default value: NoneName: shift_prefac - Type: float - Current value: 0.5 - Default value: 0.5Name: gap_step - Type: float - Current value: 1.0 - Default value: 1.0Name: lb - Type: float | None - Current value: None - Default value: NoneName: ub - Type: float | None - Current value: None - Default value: NoneThe configuration of the quantum model can be changed by passing options_names and value in dictionary format. You can update the existing configuration values using model.change_config().
# change dropout_probability from 0 to 0.3model.change_config({"dropout_probability": 0.3})# shows modified configurationName: use_sparse_observable - Type: bool - Current value: False - Default value: FalseName: use_gradient_checkpointing - Type: bool - Current value: False - Default value: FalseName: use_single_qubit_composition - Type: bool - Current value: True - Default value: FalseName: transpilation_passes - Type: list[Callable] | None - Current value: None - Default value: NoneName: algo_hevo - Type: AlgoHEvo - Current value: EXP - Default value: EXPName: ode_solver - Type: SolverType - Current value: dp5_se - Default value: dp5_seName: n_steps_hevo - Type: int - Current value: 100 - Default value: 100Name: loop_expectation - Type: bool - Current value: False - Default value: FalseName: noise - Type: NoiseHandler | None - Current value: None - Default value: NoneName: dropout_probability - Type: float - Current value: 0.3 - Default value: 0.0Name: dropout_mode - Type: DropoutMode - Current value: rotational_dropout - Default value: rotational_dropoutName: n_eqs - Type: int | None - Current value: None - Default value: NoneName: shift_prefac - Type: float - Current value: 0.5 - Default value: 0.5Name: gap_step - Type: float - Current value: 1.0 - Default value: 1.0Name: lb - Type: float | None - Current value: None - Default value: NoneName: ub - Type: float | None - Current value: None - Default value: NoneModel output
Section titled “Model output”The output of a quantum model is typically encoded in the measurement of an expectation value. In Qadence, one way to customize the number of outputs is by batching the number of observables at model creation by passing a list of blocks.
from torch import tensorfrom qadence import chain, kron, VariationalParameter, FeatureParameterfrom qadence import QuantumModel, QuantumCircuit, PI, Z, RX, CNOT
theta = VariationalParameter("theta")phi = FeatureParameter("phi")
block = chain( kron(RX(0, phi), RX(1, phi)), CNOT(0, 1))
circuit = QuantumCircuit(2, block)
model = QuantumModel(circuit, [Z(0), Z(0) + Z(1)])
values = {"phi": tensor(PI)}
ex = model.expectation(values)ex = tensor([[-1.0000e+00, -7.4988e-33]])As mentioned in the previous tutorial, blocks can also be arbitrarily parameterized through multiplication, which allows the inclusion of trainable parameters in the definition of the observable.
from qadence import I, Z
a = VariationalParameter("a")b = VariationalParameter("b")
# Magnetization with a trainable shift and scaleobservable = a * I(0) + b * Z(0)
model = QuantumModel(circuit, observable)Quantum Neural Network (QNN)
Section titled “Quantum Neural Network (QNN)”The QNN is a subclass of the QuantumModel geared towards quantum machine learning and parameter optimisation. See the
quantum machine learning section section or the QNN API reference for more detailed
information. There are three main differences in interface when compared with the QuantumModel:
- It is initialized with a list of the input parameter names, and then supports direct
torch.Tensorinputs instead of the values dictionary shown above. The ordering of the input values should respect the order given in the input names. - Passing an observable is mandatory.
- The
forwardmethod callsmodel.expectation().
from torch import tensorfrom qadence import chain, kron, VariationalParameter, FeatureParameterfrom qadence import QNN, QuantumCircuit, PI, Z, RX, RY, CNOT
theta = FeatureParameter("theta")phi = FeatureParameter("phi")
block = chain( kron(RX(0, phi), RX(1, phi)), kron(RY(0, theta), RY(1, theta)), CNOT(0, 1))
circuit = QuantumCircuit(2, block)observable = Z(0) + Z(1)
model = QNN(circuit, observable, inputs = ["phi", "theta"])
# "phi" = PI, PI/2, "theta" = 0.0, 1.0values = tensor([[PI, 0.0], [PI/2, 1.0]])
ex = model(values)ex = tensor([[-7.4988e-33], [ 1.1102e-16]])