Circuit Builder
CircuitBuilder builds a circuit one gate at a time. Its style follows Qiskit,
Braket and CUDA Quantum: qc.rx(angle, qubit=0) adds a gate and returns the
builder, so that calls can be chained. Use it when no ready-made block fits, or when
you want to write a part of a circuit and then use it in a larger composition.
from eigenkit import CircuitBuilder
qc = CircuitBuilder(n_qubits=3)
weights = qc.add_weights(6)
inputs = qc.add_inputs(3)
# Encoding.
for q in range(3):
qc.ry(inputs[q], qubit=q)
# Two variational layers + CNOT ladder.
for depth in range(2):
for q in range(3):
qc.ry(weights[depth * 3 + q], qubit=q)
for q in range(2):
qc.cnot(q, q + 1)
spec = qc.build()
spec is a CircuitSpec, the same data type, independent of any provider, that
assemble() returns. Pass it directly to Circuit:
from eigenkit import Circuit, all_z
circuit = Circuit(blocks=spec, observables=all_z(3)) # numpy interface + sim by default
Circuit also accepts the builder itself and calls build() for you, so
Circuit(blocks=qc, observables=all_z(3)) is the same circuit. In both cases, the
circuit gets the gates added so far: gates added to qc afterwards are not part of
it.
Declaring weights and inputs
add_weights(n) and add_inputs(n) return lists of WeightRef and InputRef.
These are symbolic handles to the trainable parameters and to the input vector. The
gate methods also accept constants, which are wrapped as Constant automatically:
qc = CircuitBuilder(n_qubits=2)
weights = qc.add_weights(2)
# Trainable rotation.
qc.rz(weights[0], qubit=0)
# Constant rotation: 0.5 is wrapped as Constant(0.5).
qc.rx(0.5, qubit=1)
Available gates
| Method | Gate | Parameters |
|---|---|---|
rx(angle, qubit) |
RX(angle) |
One Parameter. |
ry(angle, qubit) |
RY(angle) |
One Parameter. |
rz(angle, qubit) |
RZ(angle) |
One Parameter. |
h(qubit) |
The Hadamard gate. | None. |
cnot(control, target) |
The controlled-NOT (CNOT) gate. | None. |
x(qubit) |
The Pauli X gate. |
None. |
z(qubit) |
The Pauli Z gate. |
None. |
mcx(*qubits) |
A multi-controlled X gate. Every qubit except the last is a control. |
None. |
mcz(*qubits) |
A multi-controlled Z gate. Every qubit except the last is a control. |
None. |
phase_shift(angle, qubit) |
A phase shift, which applies e^(iφ) to \|1⟩ and leaves \|0⟩ unchanged. |
One Parameter. |
swap(qubit0, qubit1) |
The SWAP gate. | None. |
cry(angle, control, target) |
A controlled RY gate. |
One Parameter. |
s(qubit) |
The S gate, a quarter turn about Z. |
None. |
sdg(qubit) |
The S† gate, the inverse of s(). |
None. |
cphase(angle, control, target) |
A controlled phase shift, which is symmetric in its two qubits. | One Parameter. |
pauli_exponential(paulis, angle, coefficient=1.0) |
exp(-i · coefficient · angle · P) for a Pauli product P, given as (operator, qubit) pairs on different qubits. |
One Parameter. |
pauli_exponential is the Trotter building block that the cost and mixer layers of
the Quantum Approximate Optimization Algorithm (QAOA) and the excitation operators of
unitary coupled cluster with singles and doubles (UCCSD) are made of. It is
decomposed into the core set of gates.
to_block() is not a gate. It wraps what has been built so far as a CustomBlock,
so that a part of a circuit built gate by gate can be placed in a list of blocks.
add_weights(count), add_inputs(count) and build() are the other three methods
that are not gates.
Parameter expressions
Gate angles are not raw values. They are expression trees that the backends evaluate when the circuit runs. The most common nodes are these:
| Node | Meaning |
|---|---|
Constant(0.5) |
A literal value. An int or a float is wrapped automatically. |
WeightRef(i) |
The trainable weight number i. add_weights() returns these. |
InputRef(i) |
The input number i. add_inputs() returns these. |
Scale(expr, factor) |
The product factor * expr. |
Negate(expr) |
The negation -expr. |
Sum((a, b, ...)) |
The sum of the expressions. |
Multiply(a, b) |
The product of two expressions, for example lambda * x for trainable feature scaling. |
You rarely need to name these nodes yourself. The handles from add_weights() and
add_inputs() support ordinary Python arithmetic, and each operator builds exactly
the node that its constructor would build. So the two ways of writing give the same
intermediate representation, and you can mix them freely:
import math
qc = CircuitBuilder(n_qubits=2)
weights = qc.add_weights(2)
inputs = qc.add_inputs(2)
# 2*pi * x_0.
qc.rx(2 * math.pi * inputs[0], qubit=0)
# -w_0 (useful for adjoints).
qc.rz(-weights[0], qubit=1)
# w_0 + x_1.
qc.ry(weights[0] + inputs[1], qubit=0)
# Trainable feature scaling, w_1 * x_0.
qc.rx(weights[1] * inputs[0], qubit=1)
# A ZZ-style feature-map angle, 2 * (pi - x_0) * (pi - x_1).
qc.cnot(0, 1)
qc.phase_shift(2 * ((math.pi - inputs[0]) * (math.pi - inputs[1])), qubit=1)
qc.cnot(0, 1)
+, -, *, / and the unary - work on every node, with plain numbers allowed
on either side. Two details are worth knowing:
- Multiplication chooses the node for you. A constant factor becomes a
Scale, which a backend folds into the angle of the gate. Only a symbol times a symbol becomes aMultiply. It is easy to get this wrong when you build the nodes by hand, and nothing warns you when you do. - Addition is flattened.
a + b + cis oneSumwith three terms, not aSuminside anotherSum.
You can divide only by a number (x / 2 is Scale(x, 0.5)). The intermediate
representation has no division node, so x / y raises a TypeError. The explicit
constructors are still available from eigenkit.core.ir, for code that builds
expression trees programmatically:
from eigenkit.core.ir import Multiply, Negate, Scale, Sum
qc.rx(Scale(inputs[0], factor=2 * math.pi), qubit=0) # same as 2*pi * inputs[0]
qc.rz(Negate(weights[0]), qubit=1) # same as -weights[0]
qc.ry(Sum((weights[0], inputs[1])), qubit=0) # same as weights[0] + inputs[1]
qc.rx(Multiply(weights[1], inputs[0]), qubit=1) # same as weights[1] * inputs[0]
On backends without their own automatic differentiation, a Multiply makes the
gradient use the parameter-shift rule together with the chain rule. The gradients
match the automatic differentiation of PennyLane to machine precision on PennyLane,
Qiskit and Braket. CUDA Quantum does not support products of two symbols at the
moment.
Building an inversion circuit
A common pattern is to build U(x_1) followed by the adjoint of U(x_2), to read
out the fidelity |<0...0| U^dag(x_2) U(x_1) |0...0>|^2. This is what QuantumKernel
uses, and it serves any fidelity measure in the style of a swap test.
from eigenkit import Circuit, CircuitBuilder
from eigenkit.core.ir import Negate
n_features = 3
qc = CircuitBuilder(n_qubits=n_features)
x1 = qc.add_inputs(n_features)
x2 = qc.add_inputs(n_features)
# Forward U(x_1).
for q in range(n_features):
qc.rx(x1[q], qubit=q)
# Adjoint U^dag(x_2): reversed order, negated angles.
for q in reversed(range(n_features)):
qc.rx(Negate(x2[q]), qubit=q)
circuit = Circuit(blocks=qc.build())
# No trainable weights here, so pass an empty weight vector; the data is inputs.
print(circuit.zero_state_probability([], [0.4, -0.2, 0.6, 0.4, -0.2, 0.6])) # ~1.0
See Quantum Kernels for the full implementation of the kernel.
Inspecting the gates
circuit.draw() returns a drawing, as text over several lines, of the circuit that
you have just built. It is the easiest way to check a circuit written by hand before
training:
The drawing works in the same way for circuits assembled from blocks. See
Basic Circuit for an example with Encoding and
Variational.
Composing with blocks
qc.to_block() wraps the current state of the builder as a CustomBlock, which you
can use anywhere a Block is accepted. The assembler moves the local WeightRef and
InputRef indices of the block into the numbering of the whole circuit. In a list of
blocks, you can also leave out to_block() and put qc in the list directly. The
assembler then makes the same call for you.
from eigenkit import Circuit, CircuitBuilder, Variational, all_z
from eigenkit.core.ir import Scale
# Hand-written encoding: scale every input by 2*pi.
qc = CircuitBuilder(n_qubits=4)
inputs = qc.add_inputs(4)
for q in range(4):
qc.ry(Scale(inputs[q], factor=2 * 3.14159), qubit=q)
my_encoding = qc.to_block()
# Compose with a standard variational block.
circuit = Circuit(
blocks=[my_encoding, Variational(depth=2)],
observables=all_z(4),
)
When to use the builder
- You need a sequence of gates that no ready-made block expresses.
- You want full control over the parameter expressions, with your own patterns of
Multiply,SumandScale. - You want to write a part of a circuit gate by gate, and then use it in a list of
blocks through
.to_block(). - You are following a paper that uses a specific decomposition into gates.
For the usual shape of an encoding followed by a variational block, prefer the block interface (Encoding, Variational, Reuploading). It is shorter, and it stays readable as the circuit grows.
API reference
eigenkit.core.builder.CircuitBuilder
Build a CircuitSpec one gate at a time.
Example
qc = CircuitBuilder(n_qubits=3)
weights = qc.add_weights(6)
inputs = qc.add_inputs(3)
for qubit in range(3):
qc.ry(inputs[qubit], qubit=qubit)
for depth in range(2):
for qubit in range(3):
qc.ry(weights[depth * 3 + qubit], qubit=qubit)
for qubit in range(2):
qc.cnot(qubit, qubit + 1)
spec = qc.build()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_qubits
|
int
|
The number of qubits of the circuit being built. Every qubit
index that a gate uses must be between |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
n_inputs
property
The number of input features that add_inputs() has reserved so far.
The built CircuitSpec has the same number.
n_qubits
property
The number of qubits of the circuit, as given to the constructor.
Every qubit index that a gate uses must be smaller than this number, and the number never changes.
n_weights
property
The number of trainable weights that add_weights() has reserved so far.
The built CircuitSpec has the same number.
add_inputs(count)
Reserve count input features and return references to them.
This is the counterpart of add_weights() for data instead of trainable
parameters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
The number of input features to reserve. |
required |
Returns:
| Type | Description |
|---|---|
list[InputRef]
|
One |
add_weights(count)
Reserve count trainable weights and return references to them.
Pass a returned WeightRef wherever a gate takes an angle. The indices
are local to this builder and start at 0. When the block is combined
with others, assemble shifts them to their positions in the whole
circuit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
The number of weights to reserve. |
required |
Returns:
| Type | Description |
|---|---|
list[WeightRef]
|
One |
build()
Return the current state of the builder as an immutable CircuitSpec.
cnot(control, target)
Add a controlled-NOT (CNOT) gate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control
|
int
|
The qubit that controls the flip. |
required |
target
|
int
|
The qubit that is flipped. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
cphase(angle, control, target)
Add a controlled phase shift gate.
The gate is symmetric: the control and the target play the same role.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
ParameterLike
|
The phase. It can be a number or a symbolic reference. |
required |
control
|
int
|
One of the two qubits. |
required |
target
|
int
|
The other qubit. The gate acts in the same way if the two qubits are exchanged. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
cry(angle, control, target)
Add a controlled RY gate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
ParameterLike
|
The rotation angle. It can be a number or a symbolic reference. |
required |
control
|
int
|
The qubit that controls the rotation. |
required |
target
|
int
|
The qubit that is rotated. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
h(qubit)
Add a Hadamard gate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
mcx(*qubits)
Add a multi-controlled X gate.
All the qubits except the last one are controls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*qubits
|
int
|
Two or more distinct qubits. The controls come first, and the target is the last one. |
()
|
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than two qubits are given, or if a qubit is repeated. |
mcz(*qubits)
Add a multi-controlled Z gate.
All the qubits except the last one are controls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*qubits
|
int
|
Two or more distinct qubits. The controls come first, and the target is the last one. |
()
|
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than two qubits are given, or if a qubit is repeated. |
pauli_exponential(paulis, angle, coefficient=1.0)
Add the exponential of a product of Pauli operators.
The gate is exp(-i * coefficient * angle * P), where P is the product of
paulis. Each element of paulis is a pair (op, qubit), where op is
"X", "Y" or "Z", and the qubits are distinct.
This is the Trotter building block that the quantum approximate
optimisation algorithm (QAOA) needs for its cost and mixer layers, and that
the unitary coupled-cluster singles and doubles (UCCSD) ansatz needs for
its excitation operators. It is built from the core gates only. First,
H gates (for X) and RX(π/2) gates (for Y) change each factor to the
Z basis. Then a ladder of controlled-NOT (CNOT) gates accumulates the
parity on the last qubit, and a central RZ(2 * coefficient * angle) is
applied (because RZ(φ) = exp(-i φ Z / 2)). Finally, the inverse of the
first steps is applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paulis
|
Sequence[tuple[str, int]]
|
The Pauli product, as |
required |
angle
|
ParameterLike
|
The rotation angle. It can be a number or a symbolic reference. |
required |
coefficient
|
float
|
A number that multiplies the angle. This is where the
weight of a Hamiltonian term goes. Default: |
1.0
|
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
phase_shift(angle, qubit)
Add a phase shift gate R(φ).
The gate multiplies |1⟩ by e^(iφ) and leaves |0⟩ unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
ParameterLike
|
The phase |
required |
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
rx(angle, qubit)
Add a rotation about the X axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
ParameterLike
|
The rotation angle. It can be a number, or a |
required |
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
ry(angle, qubit)
Add a rotation about the Y axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
ParameterLike
|
The rotation angle. It can be a number, or a |
required |
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
rz(angle, qubit)
Add a rotation about the Z axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
ParameterLike
|
The rotation angle. It can be a number, or a |
required |
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
s(qubit)
Add an S gate, which is a quarter turn about the Z axis.
Its inverse is sdg().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
sdg(qubit)
Add an S-dagger gate (Sdg), which is the inverse of s().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
swap(qubit0, qubit1)
Add a SWAP gate, which exchanges the states of two qubits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit0
|
int
|
The first qubit. |
required |
qubit1
|
int
|
The second qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
to_block()
Wrap the current state of the builder in a CustomBlock.
This is useful to build a part of a circuit one gate at a time, and then to
combine it with other blocks. When the blocks are assembled, the indices of
the WeightRef and InputRef objects of this block are shifted to their
positions in the whole circuit.
x(qubit)
Add a Pauli X gate, which is a bit flip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |
z(qubit)
Add a Pauli Z gate, which is a phase flip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qubit
|
int
|
The target qubit. |
required |
Returns:
| Type | Description |
|---|---|
'CircuitBuilder'
|
This builder, so that calls can be chained. |