ir
eigenkit.core.ir
The intermediate representation (IR) of quantum circuits.
The IR is plain data and does not depend on any third-party package. Every object is a frozen dataclass, so circuits can be hashed and pickled, and they can be shared safely between threads or processes.
The parameter expressions (Scale, Negate, Sum and Multiply) let encoding
blocks write angles such as RY(2π * x) or RY(λ * x), so the user does not have
to scale the inputs beforehand. They also let transforms such as adjoint work on
symbolic references instead of fixed numbers.
Example
from eigenkit import Encoding, Variational
from eigenkit.core import assemble
from eigenkit.core.ir import Observable, adjoint
spec = assemble([Encoding(in_features=2), Variational(depth=1)])
print(spec.n_qubits, len(spec.gates)) # 2 5
print(spec.gates[0].name, spec.gates[0].qubits) # RY (0,)
# adjoint reverses the gate order and negates every angle
print(adjoint(spec).gates[0].name) # CNOT
# an observable can be parsed from a sum of Pauli terms written as a string
print(Observable.from_string("0.5 Z0 X1").terms[0].coeff) # 0.5
CircuitSpec
dataclass
A whole circuit as data.
It holds the list of gates, the number of qubits, and the lengths of the weight
and input vectors that the gates refer to. Every provider compiles this object,
and assemble builds it from a list of blocks. It holds no weights and no
observables: the weights are passed at each call, and the observables are set
by the Circuit that wraps the spec.
Attributes:
| Name | Type | Description |
|---|---|---|
n_qubits |
int
|
The number of qubits of the circuit. |
n_weights |
int
|
The length of the weight vector that the gates refer to
through |
n_inputs |
int
|
The length of the input vector that the gates refer to through
|
gates |
tuple[Gate, ...]
|
The gates, in the order in which they are applied. |
Constant
dataclass
Bases: _ParamOps
A fixed angle, used where a block needs a rotation by a set amount.
Attributes:
| Name | Type | Description |
|---|---|---|
value |
float
|
The angle, in radians. |
Gate
dataclass
One gate of a circuit: which gate it is, where it acts, and its angles.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
GateName
|
The name of the gate, from the fixed list in |
qubits |
tuple[int, ...]
|
The indices of the qubits that the gate acts on, in the order that the gate expects (the controls before the targets). |
params |
tuple[Parameter, ...]
|
The angle expressions, one for each rotation parameter. Each one is
a |
data |
tuple
|
The constant arrays of the gates that carry data
( |
adjoint |
bool
|
If True, the conjugate transpose of the gate is applied instead
of the gate itself. It is used only for |
InputRef
dataclass
Bases: _ParamOps
A placeholder for the input feature at index in the input vector.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
int
|
The position in the input vector of the circuit. |
Multiply
dataclass
Bases: _ParamOps
The product of two parameter expressions.
It is used when a gate angle is the product of two symbolic terms. The most
common case is a trainable scaling of a feature (Schuld et al. 2021):
RY(λ * x), where λ is a WeightRef and x is an InputRef. When one of
the two factors is a constant number, use Scale instead, so that backends can
see the number directly.
Backends that do not support the product of two symbolic terms raise
NotImplementedError when this node appears. Currently, this is the case of
the CUDA Quantum backend.
Attributes:
| Name | Type | Description |
|---|---|---|
a |
'Parameter'
|
The left factor. |
b |
'Parameter'
|
The right factor. |
Negate
dataclass
Bases: _ParamOps
The negation of another expression: -expression.
The adjoint transform builds this node when it reverses a rotation. When the
angle is a Constant or a Scale, it changes the sign of the number instead,
and when the angle is already a Negate, it removes the negation.
Attributes:
| Name | Type | Description |
|---|---|---|
expression |
'Parameter'
|
The expression to negate. |
Observable
dataclass
A Hermitian operator to measure, written as a sum of Pauli terms.
You can build one with the z, x and y methods for a single Pauli
operator on one qubit, with from_string for a whole sum, or with the X,
Y and Z functions of eigenkit.core.observables, which support
arithmetic.
Attributes:
| Name | Type | Description |
|---|---|---|
terms |
tuple[PauliTerm, ...]
|
The Pauli terms of the sum. It can be empty: |
from_string(text)
classmethod
Parse a sum of Pauli terms written as a string.
Examples of such strings are "0.5 Z0 X1 + 0.3 Y2" and "-Z0 + Z1".
The grammar is as follows. The terms are joined by + or -. Each term
starts with an optional numeric coefficient, in plain or scientific
notation (1e-3 is accepted). The coefficient is followed by an optional
* and by one or more [XYZ]<qubit> operators on distinct qubits. Any
amount of whitespace is accepted, including none.
Every part of the string must be a valid term, and every term must contain
Pauli operators. A constant term ("0.5 + Z0", "0.5 I") raises an error
instead of being dropped from the sum: an Observable holds only Pauli
terms, and the constant must be kept next to it as an offset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The string to parse. |
required |
Returns:
| Type | Description |
|---|---|
'Observable'
|
The parsed observable. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
x(qubit)
classmethod
Return the observable X on the single qubit qubit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The index of the qubit. |
required |
y(qubit)
classmethod
Return the observable Y on the single qubit qubit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The index of the qubit. |
required |
z(qubit)
classmethod
Return the observable Z on the single qubit qubit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The index of the qubit. |
required |
PauliTerm
dataclass
A coefficient times a product of Pauli operators on distinct qubits.
PauliTerm(0.5, (("Z", 0), ("X", 1))) is the term 0.5 Z0 X1.
Attributes:
| Name | Type | Description |
|---|---|---|
coeff |
float
|
The real coefficient. Complex values are rejected, because an
|
paulis |
tuple[tuple[str, int], ...]
|
Pairs of a Pauli letter ( |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Scale
dataclass
Bases: _ParamOps
A constant multiple of another expression: factor * expression.
Use it instead of Multiply when one of the two factors is a plain number. A
backend can then include the constant factor in the gate angle, instead of
treating it as a second symbolic operand.
Attributes:
| Name | Type | Description |
|---|---|---|
expression |
'Parameter'
|
The expression to scale. |
factor |
float
|
The constant factor. |
Sum
dataclass
Bases: _ParamOps
The sum of several expressions.
Attributes:
| Name | Type | Description |
|---|---|---|
expressions |
tuple['Parameter', ...]
|
The terms to add. There must be at least one. An empty |
WeightRef
dataclass
Bases: _ParamOps
A placeholder for the trainable weight at index in the weight vector.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
int
|
The position in the weight vector of the circuit. Each block numbers
the weights of its own fragment from |
adjoint(circuit)
Reverse the order of the gates and invert each gate.
The transform does not depend on the provider. It returns another CircuitSpec
that any backend can compile, so the transform does not have to be written
again for each backend.
Each type of gate is inverted as follows:
- Rotations with parameters (
RX,RY,RZ,PhaseShift,CRY,CPHASE): the angle parameters are negated. - Gates without parameters (
H,X,Z,CNOT,SWAP,MCZ,MCX): they are copied unchanged, because applying them twice gives the identity.SandSdgare not their own inverse, so each one is replaced by the other. QubitUnitary(U): the stored matrix is replaced by its conjugate transposeU†. It is computed without NumPy, so that the intermediate representation stays pure Python.ControlledQubitUnitary(U): the target unitary (data[0]) is replaced by its conjugate transpose, and the control values (data[1]) are kept unchanged.AmplitudeEncoding: the transform flipsgate.adjointand leaves the inversion to the backend. PennyLane, for example, applies the conjugate transpose of the state-preparation unitary. The Qiskit backend does not support this adjoint:Circuitrejects such a circuit with aValueError, and the backend itself raisesNotImplementedError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to invert. |
required |
Returns:
| Type | Description |
|---|---|
CircuitSpec
|
The inverted circuit, with the gates in reverse order. |
chain_factor(parameter, weight_index, weights, inputs)
Compute the partial derivative of parameter with respect to one weight.
The derivative with respect to weights[weight_index] is evaluated
numerically at the given weights and inputs. The parameter-shift Jacobian
that applies the chain rule uses it to turn the gradient with respect to a gate
angle into its contribution to the gradient with respect to one weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The angle expression to differentiate. |
required |
weight_index
|
int
|
The index of the weight to differentiate with respect to. |
required |
weights
|
ndarray | Sequence[float]
|
The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. |
required |
inputs
|
ndarray | Sequence[float]
|
The input vector. It can be any
container that |
required |
Returns:
| Type | Description |
|---|---|
float
|
The partial derivative. For a single sample, it is a float. When |
chain_factor_input(parameter, input_index, weights, inputs)
Compute the partial derivative of parameter with respect to one input.
The derivative with respect to inputs[input_index] is evaluated numerically
at the given weights and inputs. This function is the counterpart of
chain_factor for the inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The angle expression to differentiate. |
required |
input_index
|
int
|
The index of the input to differentiate with respect to. |
required |
weights
|
ndarray | Sequence[float]
|
The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. |
required |
inputs
|
ndarray | Sequence[float]
|
The input vector. It can be any
container that |
required |
Returns:
| Type | Description |
|---|---|
float
|
The partial derivative. For a single sample, it is a float. When |
collect_input_dependencies(parameter)
Return the set of input indices that a parameter expression depends on.
This function is the counterpart of collect_weight_dependencies for the
inputs. The parameter-shift Jacobian that applies the chain rule uses it to
find the gates that contribute to the gradient with respect to a given input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The angle expression to inspect. |
required |
Returns:
| Type | Description |
|---|---|
set[int]
|
The indices of the inputs that the expression reads. |
collect_weight_dependencies(parameter)
Return the set of weight indices that a parameter expression depends on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The angle expression to inspect. |
required |
Returns:
| Type | Description |
|---|---|
set[int]
|
The indices of the weights that the expression reads. |
find_input_shift_unsafe_gates(circuit)
Return the gates whose input angle needs the chain-rule path.
A gate is included when its angle wraps an InputRef in an arithmetic node,
or when it uses an input that another angle also uses. Reuploading does this
when it encodes each feature once per layer.
An arithmetic node is a Scale, Multiply, Sum or Negate. This function
is the counterpart of find_parameter_shift_unsafe_gates for the inputs. When
the list is empty, the fast path, which shifts each input directly, can be
used. Otherwise, the gradients are correct only with the chain-rule path,
which shifts single gate angles with shift_gate_angle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to inspect. |
required |
Returns:
| Type | Description |
|---|---|
list['Gate']
|
The gates that need the chain-rule path. |
find_parameter_shift_unsafe_gates(circuit)
Return the gates whose weight angle needs the chain-rule path.
A gate is included when its angle wraps a WeightRef in an arithmetic node,
which is a Scale, Multiply, Sum or Negate, or when it uses a weight
that another angle also uses. Circuit reads this list to choose how to
compute gradients. A circuit without such gates takes the fast path, which
applies the parameter-shift rule directly to each weight. A circuit with such
gates takes the chain-rule path. Two examples are
Reuploading(trainable_scale=True), which builds Multiply(weight, input),
and a builder circuit that uses one weight in two gates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to inspect. |
required |
Returns:
| Type | Description |
|---|---|
list['Gate']
|
The gates that need the chain-rule path. |
inputs_needing_four_term_shift(circuit)
Return the inputs that the two-term parameter-shift rule cannot differentiate.
This function is the counterpart of weights_needing_four_term_shift for the
inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to analyse. |
required |
Returns:
| Type | Description |
|---|---|
frozenset[int]
|
The indices of the inputs that need the four-term rule. |
negate_parameter(parameter)
Negate a parameter expression of any kind, symbolically.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The angle expression to negate. |
required |
Returns:
| Type | Description |
|---|---|
Parameter
|
The negated expression. |
resolve(parameter, weights, inputs)
Compute the numeric value of a parameter expression, recursively.
weights and inputs can be any container that supports indexing (a NumPy
array, a PyTorch tensor or a list). The type of the result follows the type of
these containers, so the same function serves both the NumPy and the PyTorch
evaluation paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The angle expression to evaluate. |
required |
weights
|
ndarray | Sequence[float]
|
The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. |
required |
inputs
|
ndarray | Sequence[float]
|
The input vector. It can be any
container that |
required |
Returns:
| Type | Description |
|---|---|
float
|
The numeric angle. |
shift_gate_angle(circuit, gate_index, shift_value)
Return a copy of the circuit in which the angle of one gate is shifted.
In the new CircuitSpec, the first parameter of the gate at gate_index is
replaced by Sum((original, Constant(shift_value))). The parameter-shift
Jacobian that applies the chain rule uses it to shift the angle of one gate,
without changing the weights or the other gates. The shifts are ±π/2, and
also ±3π/2 for the gates in MULTI_FREQUENCY_GATES.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to shift. |
required |
gate_index
|
int
|
The position of the gate in |
required |
shift_value
|
float
|
The amount added to the first angle of that gate. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitSpec'
|
A new |
weights_needing_four_term_shift(circuit)
Return the weights that the two-term parameter-shift rule cannot differentiate.
A weight is included when one of the gates that it feeds is in
MULTI_FREQUENCY_GATES. A weight that feeds more than one gate never uses
this direct shift: the circuit takes the chain-rule path, which shifts each
gate on its own (see find_parameter_shift_unsafe_gates).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to analyse. |
required |
Returns:
| Type | Description |
|---|---|
frozenset[int]
|
The indices of the weights that need the four-term rule. |