Skip to content

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 WeightRef.

n_inputs int

The length of the input vector that the gates refer to through InputRef.

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 GateName.

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 Constant, a WeightRef, an InputRef, or an arithmetic combination of them.

data tuple

The constant arrays of the gates that carry data (AmplitudeEncoding, QubitUnitary and ControlledQubitUnitary). They are stored as nested tuples so that the gate can still be hashed. The field is empty for every other gate. _array_to_data and _data_to_array convert between arrays and this form.

adjoint bool

If True, the conjugate transpose of the gate is applied instead of the gate itself. It is used only for AmplitudeEncoding gates, inside Grover iterates. The adjoint of a QubitUnitary is computed in the intermediate representation instead, by taking the conjugate transpose of the stored matrix, so this flag is never set for it.

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. assemble shifts it in the same way as the indices of WeightRef.

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: jordan_wigner returns an empty observable, together with an offset, when the operator reduces to a multiple of the identity. However, Circuit refuses to measure an empty observable, because there is nothing to measure.

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 text is empty, if it contains a constant term, if a part of it is not a valid term, or if a term uses the same qubit twice.

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 Observable represents only Hermitian operators.

paulis tuple[tuple[str, int], ...]

Pairs of a Pauli letter ("X", "Y" or "Z") and a qubit index. It must not be empty, because a term without Pauli operators is a constant, which an Observable does not hold. Each qubit can appear at most once, so reduce products such as Z * Z = I before building the term.

Raises:

Type Description
TypeError

If coeff is complex.

ValueError

If paulis is empty, if a Pauli letter is not X, Y or Z, or if a qubit appears more than once.

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 Sum is not evaluated as 0: resolve and the backends start from the first term, so they raise an IndexError on it.

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 0, and assemble shifts these indices to their positions in the whole circuit.

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. S and Sdg are not their own inverse, so each one is replaced by the other.
  • QubitUnitary(U): the stored matrix is replaced by its conjugate transpose U†. 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 flips gate.adjoint and 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: Circuit rejects such a circuit with a ValueError, and the backend itself raises NotImplementedError.

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 weights can be.

required

Returns:

Type Description
float

The partial derivative. For a single sample, it is a float. When inputs is a batch, transposed so that inputs[j] holds feature j of every sample, and the expression contains a Multiply, it can be an array with one value per sample.

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 weights can be.

required

Returns:

Type Description
float

The partial derivative. For a single sample, it is a float. When inputs is a batch, transposed so that inputs[j] holds feature j of every sample, and the expression contains a Multiply, it can be an array with one value per sample.

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 weights can be.

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 circuit.gates.

required
shift_value float

The amount added to the first angle of that gate.

required

Returns:

Type Description
'CircuitSpec'

A new CircuitSpec in which only that gate is shifted.

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.