Basic Circuit
This example goes through the whole process: it builds a quantum circuit from blocks, chooses a provider, and runs the circuit on data.
Setup
import numpy as np
from eigenkit import Circuit, Encoding, Variational, all_z
encoding = Encoding(in_features=6, rotation="RX")
variational = Variational(depth=2, rotation="RY")
observables = all_z(6)
Choose a provider
from eigenkit import PennyLaneConfig
config = PennyLaneConfig(device_name="lightning.qubit", shots=None)
You can also pass a registered name, as a string ("pennylane", "qiskit" or
"braket"), or a backend instance. See Providers.
Build and run
circuit = Circuit(
blocks=[encoding, variational],
observables=observables,
provider=config,
)
# 6 qubits, 6 features. The numpy interface (default) takes weights per call;
# output has one expectation value per qubit.
weights = np.zeros(circuit.spec.n_weights) # or a trained vector
inputs = np.random.uniform(0, np.pi, size=(3, 6)) # batch of 3 samples
outputs = circuit(weights, inputs)
print(outputs.shape)
To get a differentiable torch.nn.Module that holds its own weights and fits into
a PyTorch model, build the circuit with interface="torch" (this needs the torch
extra), and call circuit(inputs). See Getting Started.
Inspecting the gate structure
circuit.draw() returns a drawing of the circuit, as text over several lines. The
drawing is built from the CircuitSpec, so it is the same for every backend:
Parametrised angles are drawn as symbols: x_i for the input slots, w_i for the
trainable weights, and the numbers themselves for Constant angles. Combined
expressions, such as Scale, Sum and Multiply, are written as ordinary formulas
(for example RY(2*x_0) and RX(w_0*x_1)).
Scalar output
The size of the output equals the number of observables. Pass a single observable,
or a PauliExpr, to get one scalar output for each sample:
from eigenkit import Z
scalar_circuit = Circuit(
blocks=[encoding, variational],
observables=Z(0), # only measure Z on qubit 0
provider=config,
)
print(scalar_circuit(weights, inputs).shape) # (3,)
Adding trainable feature scaling
Replace the separate Encoding and Variational blocks with a Reuploading
block. It repeats the encoding, and it can give each pair of layer and feature a
trainable scale:
from eigenkit import Reuploading
reuploading = Reuploading(
in_features=6,
depth=3,
encoding_rotation="RY",
variational_rotation="RY",
trainable_scale=True,
)
circuit = Circuit(blocks=[reuploading], observables=all_z(6))
See Reuploading for all the options.
Imperative construction
When no ready-made block fits, use CircuitBuilder. It is the interface for
adding one gate at a time, in the style of Qiskit, Braket and CUDA Quantum:
from eigenkit import Circuit, CircuitBuilder, all_z
qc = CircuitBuilder(n_qubits=4)
weights = qc.add_weights(8)
inputs = qc.add_inputs(4)
# Encoding.
for q in range(4):
qc.ry(inputs[q], qubit=q)
# Two variational layers + CNOT ladder.
for d in range(2):
for q in range(4):
qc.ry(weights[d * 4 + q], qubit=q)
for q in range(3):
qc.cnot(q, q + 1)
spec = qc.build()
circuit = Circuit(blocks=spec, observables=all_z(4))
qc.build() returns a CircuitSpec, which you pass directly to Circuit. For a
sub-circuit that you want to combine with the standard blocks, use qc.to_block()
instead. See the Circuit Builder guide for the
full set of gates, the parameter expressions, and an example of an inversion
circuit.