Ansatz¶
A block vocabulary, the templates written in it, and re-uploading as a composition.
qmlkit.ansatz.blocks¶
blocks ¶
A composable vocabulary for building ansätze.
Most published ansätze are a handful of primitives stacked in a pattern. Writing them as expressions in that vocabulary — rather than as bespoke functions — means a new one costs a line and inherits everything: correct parameter-shift rules, resource counting, a torch layer, and a place in the registry.
hardware_efficient = repeat(n_layers, RotationLayer(("ry", "rz")) + EntanglerLayer("cx"))
Blocks allocate their own parameters through a build context, so nothing is ever
hand-counted. :func:share re-uses one set of parameters across repetitions —
weight tying — which the gradient code already handles per occurrence.
BuildContext ¶
Hands out parameter slots and tracks which qubits are still active.
active shrinks as pooling layers halve the register (the QCNN pattern), so
later blocks act on what is left rather than on the full width.
Source code in src/qmlkit/ansatz/blocks.py
input_ref ¶
input_ref(
i: int, scale: float = 1.0, offset: float = 0.0
) -> ParamRef
Reference encoding angle i. Re-uploads reuse the same reference.
Source code in src/qmlkit/ansatz/blocks.py
input_slots ¶
input_slots(owner: object, n_angles: int) -> list[ParamRef]
The input slots belonging to owner, allocating them on first sight.
A slot holds an angle, and every feature map derives its angles differently
— ZZFeatureMap emits 2*x_i where AngleFeatureMap emits x_i. So
angle i of one map is not angle i of another, and two maps sharing a
slot would silently feed one map's transformed angles into the other. Each map
therefore owns a disjoint range, while the same map re-used gets the range it
already has — which is what makes re-uploading feed the same data in again.
Source code in src/qmlkit/ansatz/blocks.py
new_param ¶
new_param(
scale: float = 1.0, offset: float = 0.0
) -> ParamRef
Allocate a parameter — or replay a shared one, when tying weights.
Source code in src/qmlkit/ansatz/blocks.py
replaying ¶
Re-issue these parameter indices instead of allocating new ones.
Source code in src/qmlkit/ansatz/blocks.py
Block ¶
One piece of an ansatz. Compose with +.
RotationLayer ¶
Bases: Block
One trainable rotation per gate per active wire.
Source code in src/qmlkit/ansatz/blocks.py
EntanglerLayer ¶
Bases: Block
A layer of fixed two-qubit gates following a named pattern.
Source code in src/qmlkit/ansatz/blocks.py
ParametricEntangler ¶
Bases: Block
A layer of trainable two-qubit gates — exercises the four-term shift rule.
Source code in src/qmlkit/ansatz/blocks.py
PoolLayer ¶
Bases: Block
Halve the active register — the pooling half of a QCNN.
keep="odd" retains every second wire starting from the second, matching the
convention where the surviving qubit is the target of the preceding entangler.
Two pooling modes, because the literature uses both:
mode="discard"
Simply stop using the wire. Cheap, adds no parameters, and the information
it held survives only through whatever the convolution already moved.
mode="controlled"
Before dropping a wire, apply a trainable crz from it onto its surviving
partner, so pooling learns what to carry forward. This is the simulator's
stand-in for the measure-and-conditionally-rotate pooling of Cong, Choi &
Lukin, which needs mid-circuit measurement and feed-forward.
tied shares one pooling angle across the whole layer, matching the way a
tied convolution shares one filter.
Source code in src/qmlkit/ansatz/blocks.py
Custom ¶
Custom(
fn: Callable[[QCircuit, BuildContext], None],
name: str = "Custom",
)
Bases: Block
Wrap an arbitrary fn(qc, ctx) as a block.
The escape hatch: anything the vocabulary cannot express, written directly against the builder, still composes with everything else.
Source code in src/qmlkit/ansatz/blocks.py
EncodingLayer ¶
Bases: Block
Insert a feature map as a block, so re-uploading is just composition.
Data re-uploading is not one structure. It is any interleaving of an encoding with a trainable block, and which encoding, which block, and in what order are all design choices:
repeat(3, EncodingLayer(fmap) + RotationLayer(("rz", "ry", "rz")) + EntanglerLayer())
repeat(2, RotationLayer("ry") + EncodingLayer(fmap)) # W before S
EncodingLayer(fmap) + repeat(4, RotationLayer("ry")) # encode once, vary often
EncodingLayer(zz) + RotationLayer("ry") + EncodingLayer(angle) # two different maps
Which slots a layer reads. Every repeat of the same feature map references
the same input angles: re-uploading means feeding the same data in again, not
consuming new features. Two different maps get disjoint ranges, allocated in the
order they first appear, because a slot holds an angle and each map derives its
angles its own way — ZZFeatureMap(2).angles([0.3, 0.7]) is
[0.6, 1.4, 13.876], since a Z term follows the Rz(2 phi) convention, while
AngleFeatureMap(2).angles([0.3, 0.7]) is [0.3, 0.7]. Angle 0 of one is not
angle 0 of the other, so sharing the slot would encode the wrong number without
raising.
So the composition above reserves 3 + 2 = 5 input slots, and
:meth:~qmlkit.ansatz.library.Ansatz.angles concatenates the maps' angles in the
same order. n_inputs is inferred from the block; pass it only to assert a
total you already know.
Slots cannot instead be keyed by feature, so that both maps read the raw x:
a slot is referenced by a :class:~qmlkit.core.ir.ParamRef, which is affine in one
parameter (scale * theta[i] + offset), and a Pauli map's higher-order angle is
2 * prod_j (pi - x_j) — nonlinear, in several features at once. The map from
features to angles has to stay classical, which is what
:meth:~qmlkit.encoding.feature_maps.FeatureMap.angle_jacobian is for.
To learn the frequencies rather than inherit them from the encoding, put a
classical layer in front — nn.Sequential(nn.Linear(d, d), QuantumLayer(...)).
That is a scaling of the inputs, which torch already differentiates; it does not
need to live inside the circuit.
Source code in src/qmlkit/ansatz/blocks.py
Share ¶
Share(times: int, block: Block)
Bases: Block
Apply a block times times, re-using the same parameters.
Weight tying, as a QCNN's shared convolution filter does. The gradient code sums over each occurrence separately, which is the whole reason the IR tracks slots rather than just parameters.
Source code in src/qmlkit/ansatz/blocks.py
encoding_layers ¶
encoding_layers(block: Block) -> list[EncodingLayer]
Every :class:EncodingLayer in a block tree, in the order it is first emitted.
Used to infer how many input slots a model reserves. Repeat and Share
re-emit one child, and a repeated encoding re-uses its slots, so each node is
reported once — the count that matters here is distinct maps, not uploads.
Source code in src/qmlkit/ansatz/blocks.py
repeat ¶
share ¶
qmlkit.ansatz.library¶
library ¶
The Ansatz type, the built-in zoo, and the registry.
An ansatz is a width plus a block. That is the whole type — which is what makes proposing a new one a one-liner rather than a subclass:
brick = Ansatz(6, repeat(3, RotationLayer("ry") + EntanglerLayer("cz", "alternating")))
Parameter counts are inferred from a dry build, never hand-counted, so a miscount is not a failure mode. Everything downstream — gradients, resources, the torch layer — reads the resulting IR, so a new ansatz cannot be missing a capability it never had to opt into.
Ansatz ¶
Ansatz(
n_qubits: int,
block: Block,
name: str = "ansatz",
n_inputs: int | None = None,
)
A trainable circuit: a qubit count and a composable block.
n_inputs is inferred from the encoding layers in the block — each feature
map owns a disjoint range of input slots, and the same map re-used shares its own.
Pass it only to assert a total you already know; a value that disagrees raises.
Source code in src/qmlkit/ansatz/library.py
n_features
property
¶
How wide the data is. Every encoding reads the same x, so they agree.
With no encoding layer there is nothing to transform and the input slots are the features themselves.
build ¶
build(theta: ArrayLike | None = None) -> CircuitSpec
The circuit, bound to theta if given.
theta is the full parameter vector: any reserved input slots first,
then the weights. When n_inputs is 0 that is just the weights, but a
re-uploading model reserves input slots — use :meth:bind there, which takes
data and weights separately.
Source code in src/qmlkit/ansatz/library.py
bind ¶
bind(x: ArrayLike, weights: ArrayLike) -> CircuitSpec
Bind data and weights separately, in that order.
angles ¶
The input slots for x, in slot order.
One feature map's angles, or several maps' angles concatenated in the order their slots were allocated. With no encoding layer there is nothing to transform, so the values are the slot values themselves.
Source code in src/qmlkit/ansatz/library.py
angle_jacobian ¶
d(angle)/d(feature), shape (n_inputs, n_features) — one block per map.
Source code in src/qmlkit/ansatz/library.py
init ¶
Initial parameters.
small (default) keeps angles near zero, which keeps the circuit near
identity and the gradient away from the barren-plateau regime. uniform
samples the full range — the standard way to land on a plateau, useful
when that is what you are studying. zeros is exactly identity.
Source code in src/qmlkit/ansatz/library.py
resources ¶
Gate counts, depth, and the real gradient cost.
Source code in src/qmlkit/ansatz/library.py
register_ansatz ¶
register_ansatz(
name: str, factory: AnsatzFactory | None = None
) -> (
Callable[[AnsatzFactory], AnsatzFactory] | AnsatzFactory
)
Register an ansatz factory. Usable as a decorator or a direct call.
def brick_wall(n_qubits, n_layers=3): return Ansatz(n_qubits, repeat(n_layers, RotationLayer("ry") + EntanglerLayer("cz", "alternating")))
Source code in src/qmlkit/ansatz/library.py
get_ansatz ¶
get_ansatz(name: str, **kwargs: object) -> Ansatz
Build a registered ansatz by name.
Source code in src/qmlkit/ansatz/library.py
hardware_efficient ¶
hardware_efficient(
n_qubits: int,
n_layers: int = 2,
rotations: Sequence[str] = ("ry", "rz"),
entangler: str = "cx",
pattern: str = "chain",
) -> Ansatz
Rotations then entanglers, repeated. General-purpose, barren-plateau prone.
Source code in src/qmlkit/ansatz/library.py
strongly_entangling ¶
strongly_entangling(
n_qubits: int, n_layers: int = 2
) -> Ansatz
Three rotations per wire, plus a ring of CX per layer.
Source code in src/qmlkit/ansatz/library.py
simplified_two_design ¶
simplified_two_design(
n_qubits: int, n_layers: int = 2
) -> Ansatz
The standard reference ansatz in barren-plateau studies.
Source code in src/qmlkit/ansatz/library.py
tree_tensor_network ¶
tree_tensor_network(
n_qubits: int,
filter: str | tuple[ConvFilter, int] = "ry_cx",
tied: bool = False,
) -> Ansatz
Log-depth merge tree — shallow, and resistant to barren plateaus.
Each merge is a two-qubit filter from the same registry a QCNN convolves
with, so the tensor at every node is as general as you choose to pay for.
Source code in src/qmlkit/ansatz/library.py
mps_ansatz ¶
mps_ansatz(
n_qubits: int,
filter: str | tuple[ConvFilter, int] = "ry_cx",
tied: bool = False,
) -> Ansatz
A staircase of two-qubit blocks — a bond-dimension-2 matrix product state.
The block is the same two-qubit filter a QCNN convolves with, so it comes from
the same registry: "su4" gives a genuine bond-dimension-2 MPS with arbitrary
tensors, "ry_cx" the cheap real-valued one. tied=True reuses one tensor
down the whole chain, which is the translation-invariant MPS.
Source code in src/qmlkit/ansatz/library.py
register_conv_filter ¶
Make a two-qubit filter reachable by name from :func:conv_block.
Source code in src/qmlkit/ansatz/library.py
conv_block ¶
conv_block(
pattern: str = "chain",
tied: bool = True,
filter: str | tuple[ConvFilter, int] = "ry_cx",
) -> Block
A QCNN convolution layer: slide one two-qubit filter across pattern.
tied=True allocates one filter and reuses it at every pair — the genuine
convolutional structure, and the reason the gradient code sums over occurrences.
tied=False gives each pair its own weights.
filter is a registered name (:func:list_conv_filters) or a
(fn, n_params) pair, where fn(qc, a, b, params) writes the filter. That
is the whole extension point: a filter from a paper we have never heard of is a
function you pass in, not a class you subclass.
Source code in src/qmlkit/ansatz/library.py
qcnn_ansatz ¶
qcnn_ansatz(
n_qubits: int,
tie_weights: bool = True,
filter: str | tuple[ConvFilter, int] = "ry_cx",
pattern: str = "chain",
pool: str = "discard",
keep: str = "odd",
) -> Ansatz
Convolution + pooling, halving the register until one qubit is left.
There is no single "the QCNN": papers differ in the two-qubit filter and in how pooling discards a wire. Rather than shipping one class per paper, this is the shared skeleton with both choices exposed — so reproducing a particular variant is a keyword, and inventing one is a function.
tie_weights=True shares one filter across all applications in a layer — the
genuine convolutional structure, and the case whose gradient needs a sum over
occurrences.
Source code in src/qmlkit/ansatz/library.py
qaoa_ansatz ¶
qaoa_ansatz(
n_qubits: int,
edges: Sequence[tuple[int, int]] | None = None,
p: int = 1,
mixer: str = "x",
) -> Ansatz
Cost and mixer layers — only 2p parameters, whatever the width.
Both angles in a round are shared across all their gates, which is what keeps
the parameter count at 2p rather than growing with the graph.
Source code in src/qmlkit/ansatz/library.py
basic_entangler ¶
basic_entangler(
n_qubits: int, n_layers: int = 2, rotation: str = "rx"
) -> Ansatz
One rotation per wire plus a ring of CNOTs — the minimal useful template.
Source code in src/qmlkit/ansatz/library.py
two_local ¶
two_local(
n_qubits: int,
n_layers: int = 2,
rotations: Sequence[str] = ("ry",),
entangler: str = "cx",
pattern: str = "full",
) -> Ansatz
A configurable rotation/entangler alternation, ending on a rotation layer.
Source code in src/qmlkit/ansatz/library.py
random_layers ¶
random_layers(
n_qubits: int,
n_layers: int = 2,
ratio_imprimitive: float = 0.3,
seed: int | None = None,
) -> Ansatz
Randomly placed rotations and CNOTs — the baseline a new ansatz must beat.
Source code in src/qmlkit/ansatz/library.py
qmlkit.ansatz.reupload¶
reupload ¶
Data re-uploading, as a pattern rather than a fixed structure.
Re-uploading is any interleaving of an encoding with a trainable block. The encoding can be any feature map, the trainable block any ansatz block, and the order, depth and sharing are all free. Treating it as one hardcoded class is a category error — so it is not one here.
:func:reupload is a convenience over that freedom, not a replacement for it:
reupload(fmap, n_layers=3) # S W S W S W
reupload(fmap, n_layers=3, order="WS") # W S W S W S
reupload(fmap, n_layers=3, block=RotationLayer("ry") + EntanglerLayer("cz", "ring"))
reupload(fmap, n_layers=3, share_weights=True) # one tied block, reused
Anything it cannot express, compose directly — that is the same vocabulary:
Ansatz(n, EncodingLayer(zz) + RotationLayer("ry") + EncodingLayer(angle))
Frequencies. L uploads reach frequencies 0..L only when the trainable
block does not commute with the encoding rotation; if it does, the uploads merge
into a single rotation. :func:reupload checks this and warns.
ReuploadModel ¶
ReuploadModel(
n_qubits: int,
block: Block,
name: str,
n_inputs: int,
feature_map: object,
n_uploads: int,
)
Bases: Ansatz
An :class:Ansatz that also knows its encoding and upload count.
Source code in src/qmlkit/ansatz/reupload.py
reupload ¶
reupload(
feature_map: object,
n_layers: int = 3,
block: Block | None = None,
order: str = "SW",
entangler: str | None = "cx",
pattern: str = "chain",
rotations: Sequence[str] = ("rz", "ry", "rz"),
share_weights: bool = False,
name: str = "reupload",
) -> Ansatz
Build a re-uploading ansatz from any feature map and any trainable block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature_map
|
object
|
Any :class: |
required |
block
|
Block | None
|
The trainable block. Defaults to a rotation layer plus an entangler. |
None
|
order
|
str
|
|
'SW'
|
share_weights
|
bool
|
Tie one trainable block across every layer — far fewer parameters, and the gradient sums over occurrences. |
False
|