Skip to content

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 next section).

Differentiability: by enabling a differentiable backend that supports two differentiable modes: automated differentiation (AD) and parameter shift rule (PSR). The former is used to differentiate non-gate parameters and enabled for PyTorch-based simulators only. The latter is 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.

Backends

Quantum models can execute on a number of different purpose backends: simulators, emulators or real hardware. By default, Qadence executes on the PyQTorch backend which implements a state vector simulator. Other choices include the Pulser backend (pulse sequences on programmable neutral atom arrays). For more information see the backend section.

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 that enables differentiability for its expectation method.

Upon construction of the model, a compiled version of the abstract QuantumCircuit is created:

from qadence import QuantumCircuit, QuantumModel, RX, Z, chain, BackendName, Parameter

# Construct a parametrized abstract circuit.
# At this point we cannot run anything yet.

x = Parameter("x")

n_qubits = 2
block = chain(RX(0, x), RX(1, x))
circuit = QuantumCircuit(n_qubits, block)
observable = Z(0)

# Construct a QuantumModel which will compile
# the abstract circuit to targetted backend.
# By default, diff_mode=DiffMode.AD.
model = QuantumModel(circuit, observable, backend=BackendName.PYQTORCH)

# The converted circuit is a private attribute and should not
# manually be tampered with, but we can at least verify its there
# by printing it.
model._circuit.native = 

QuantumCircuit(
  (operations): ModuleList(
    (0): QuantumCircuit(
      (operations): ModuleList(
        (0): ParametricPyQOperation(
          (operation): RX(qubits=(0,), n_qubits=2)
        )
        (1): ParametricPyQOperation(
          (operation): RX(qubits=(1,), n_qubits=2)
        )
      )
    )
  )
)

Now, the wavefunction, sample, or expectation value are computable by passing a batch of values :

import torch

# Set a batch of random parameter values.
values = {"x": torch.rand(3)}

wf = model.run(values)
xs = model.sample(values, n_shots=100)
ex = model.expectation(values)
wf = tensor([[ 0.9968+0.0000j,  0.0000-0.0567j,  0.0000-0.0567j, -0.0032+0.0000j],
        [ 0.9533+0.0000j,  0.0000-0.2109j,  0.0000-0.2109j, -0.0467+0.0000j],
        [ 0.9707+0.0000j,  0.0000-0.1686j,  0.0000-0.1686j, -0.0293+0.0000j]])
xs = [Counter({'00': 100}), Counter({'00': 92, '10': 5, '11': 2, '01': 1}), Counter({'00': 95, '01': 4, '10': 1})]
ex = tensor([[0.9936],
        [0.9067],
        [0.9414]], requires_grad=True)

You can also measure multiple observables by passing a list of blocks.

# By default, backend=BackendName.PYQTORCH.
model = QuantumModel(circuit, [Z(0), Z(1)])
ex = model.expectation(values)
ex = tensor([[0.9936, 0.9936],
        [0.9067, 0.9067],
        [0.9414, 0.9414]], requires_grad=True)

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, and the parametric program tutorial for parameterization.