qmlkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. qmlkit/__init__.py +495 -0
  2. qmlkit/_aliases.py +135 -0
  3. qmlkit/algorithms/__init__.py +82 -0
  4. qmlkit/algorithms/adapt.py +297 -0
  5. qmlkit/algorithms/autoencoder.py +206 -0
  6. qmlkit/algorithms/chemistry.py +222 -0
  7. qmlkit/algorithms/clustering.py +149 -0
  8. qmlkit/algorithms/hamiltonians.py +143 -0
  9. qmlkit/algorithms/molecule.py +442 -0
  10. qmlkit/algorithms/qaoa.py +208 -0
  11. qmlkit/algorithms/rl.py +198 -0
  12. qmlkit/algorithms/vqe.py +198 -0
  13. qmlkit/ansatz/__init__.py +68 -0
  14. qmlkit/ansatz/blocks.py +348 -0
  15. qmlkit/ansatz/library.py +570 -0
  16. qmlkit/ansatz/reupload.py +168 -0
  17. qmlkit/baselines.py +604 -0
  18. qmlkit/budget.py +234 -0
  19. qmlkit/core/__init__.py +1 -0
  20. qmlkit/core/backends/__init__.py +22 -0
  21. qmlkit/core/backends/_sampling.py +43 -0
  22. qmlkit/core/backends/base.py +256 -0
  23. qmlkit/core/backends/cirq_backend.py +110 -0
  24. qmlkit/core/backends/cirq_density_backend.py +71 -0
  25. qmlkit/core/backends/noisy.py +86 -0
  26. qmlkit/core/backends/numpy_backend.py +276 -0
  27. qmlkit/core/backends/qiskit_aer_backend.py +79 -0
  28. qmlkit/core/backends/qiskit_backend.py +104 -0
  29. qmlkit/core/backends/registry.py +210 -0
  30. qmlkit/core/backends/spinqit_backend.py +233 -0
  31. qmlkit/core/backends/torch_backend.py +185 -0
  32. qmlkit/core/builder.py +189 -0
  33. qmlkit/core/execute.py +193 -0
  34. qmlkit/core/gates.py +243 -0
  35. qmlkit/core/ir.py +320 -0
  36. qmlkit/core/observables.py +269 -0
  37. qmlkit/datasets.py +178 -0
  38. qmlkit/diagnostics.py +719 -0
  39. qmlkit/draw.py +177 -0
  40. qmlkit/encoding/__init__.py +63 -0
  41. qmlkit/encoding/amplitude.py +178 -0
  42. qmlkit/encoding/angle.py +61 -0
  43. qmlkit/encoding/feature_maps.py +353 -0
  44. qmlkit/encoding/hamiltonian.py +206 -0
  45. qmlkit/encoding/pipeline.py +198 -0
  46. qmlkit/encoding/scaling.py +139 -0
  47. qmlkit/evaluate.py +686 -0
  48. qmlkit/fourier.py +124 -0
  49. qmlkit/generative.py +406 -0
  50. qmlkit/gradients/__init__.py +61 -0
  51. qmlkit/gradients/adjoint.py +138 -0
  52. qmlkit/gradients/batch.py +275 -0
  53. qmlkit/gradients/dispatch.py +247 -0
  54. qmlkit/gradients/hadamard.py +108 -0
  55. qmlkit/gradients/parameter_shift.py +142 -0
  56. qmlkit/gradients/rules.py +151 -0
  57. qmlkit/gradients/spsa.py +134 -0
  58. qmlkit/imbalance.py +335 -0
  59. qmlkit/info.py +153 -0
  60. qmlkit/interop.py +778 -0
  61. qmlkit/kernels/__init__.py +69 -0
  62. qmlkit/kernels/estimators.py +206 -0
  63. qmlkit/kernels/matrix.py +439 -0
  64. qmlkit/kernels/models.py +315 -0
  65. qmlkit/metrics.py +394 -0
  66. qmlkit/nn/__init__.py +18 -0
  67. qmlkit/nn/advanced.py +254 -0
  68. qmlkit/nn/layer.py +343 -0
  69. qmlkit/nn/losses.py +124 -0
  70. qmlkit/nn/models.py +245 -0
  71. qmlkit/optim.py +306 -0
  72. qmlkit/provenance.py +271 -0
  73. qmlkit/py.typed +0 -0
  74. qmlkit/search.py +561 -0
  75. qmlkit/shadows.py +117 -0
  76. qmlkit/utils/__init__.py +19 -0
  77. qmlkit/utils/errors.py +130 -0
  78. qmlkit/utils/shots.py +55 -0
  79. qmlkit-0.1.0.dist-info/METADATA +745 -0
  80. qmlkit-0.1.0.dist-info/RECORD +83 -0
  81. qmlkit-0.1.0.dist-info/WHEEL +4 -0
  82. qmlkit-0.1.0.dist-info/licenses/LICENSE +202 -0
  83. qmlkit-0.1.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,570 @@
1
+ """The `Ansatz` type, the built-in zoo, and the registry.
2
+
3
+ An ansatz is a width plus a block. That is the whole type — which is what makes
4
+ proposing a new one a one-liner rather than a subclass:
5
+
6
+ brick = Ansatz(6, repeat(3, RotationLayer("ry") + EntanglerLayer("cz", "alternating")))
7
+
8
+ Parameter counts are **inferred** from a dry build, never hand-counted, so a
9
+ miscount is not a failure mode. Everything downstream — gradients, resources, the
10
+ torch layer — reads the resulting IR, so a new ansatz cannot be missing a
11
+ capability it never had to opt into.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Callable, Sequence
17
+ from typing import Any
18
+
19
+ import numpy as np
20
+ import numpy.typing as npt
21
+ from numpy.typing import ArrayLike
22
+
23
+ from qmlkit.ansatz.blocks import (
24
+ Block,
25
+ BuildContext,
26
+ Custom,
27
+ EntanglerLayer,
28
+ ParametricEntangler,
29
+ PoolLayer,
30
+ RotationLayer,
31
+ repeat,
32
+ share,
33
+ )
34
+ from qmlkit.core.builder import QCircuit, entangler_pairs
35
+ from qmlkit.core.ir import CircuitSpec
36
+ from qmlkit.utils.errors import unknown
37
+
38
+ __all__ = [
39
+ "Ansatz",
40
+ "register_ansatz",
41
+ "get_ansatz",
42
+ "list_ansatze",
43
+ "hardware_efficient",
44
+ "strongly_entangling",
45
+ "simplified_two_design",
46
+ "tree_tensor_network",
47
+ "mps_ansatz",
48
+ "qcnn_ansatz",
49
+ "qaoa_ansatz",
50
+ "conv_block",
51
+ "register_conv_filter",
52
+ "list_conv_filters",
53
+ "basic_entangler",
54
+ "two_local",
55
+ "random_layers",
56
+ ]
57
+
58
+
59
+ class Ansatz:
60
+ """A trainable circuit: a qubit count and a composable block."""
61
+
62
+ def __init__(
63
+ self, n_qubits: int, block: Block, name: str = "ansatz", n_inputs: int = 0
64
+ ) -> None:
65
+ if n_qubits < 1:
66
+ raise ValueError("n_qubits must be at least 1")
67
+ self.n_qubits = n_qubits
68
+ self.block = block
69
+ self.name = name
70
+ self.n_inputs = n_inputs
71
+ self._spec: CircuitSpec | None = None
72
+
73
+ # ------------------------------------------------------------------ build --
74
+ def _template(self) -> CircuitSpec:
75
+ """The unbound circuit, built once and cached."""
76
+ if self._spec is None:
77
+ qc = QCircuit(self.n_qubits)
78
+ ctx = BuildContext(self.n_qubits, self.n_inputs)
79
+ self.block.emit(qc, ctx)
80
+ self._spec = qc.to_spec()
81
+ return self._spec
82
+
83
+ def build(self, theta: ArrayLike | None = None) -> CircuitSpec:
84
+ """The circuit, bound to ``theta`` if given.
85
+
86
+ ``theta`` is the **full** parameter vector: any reserved input slots first,
87
+ then the weights. When ``n_inputs`` is 0 that is just the weights, but a
88
+ re-uploading model reserves input slots — use :meth:`bind` there, which takes
89
+ data and weights separately.
90
+ """
91
+ spec = self._template()
92
+ if theta is None:
93
+ return spec
94
+ arr = np.asarray(theta, dtype=float).ravel()
95
+ if self.n_inputs and arr.size == self.n_weights:
96
+ raise ValueError(
97
+ f"{type(self).__name__} reserves {self.n_inputs} input slots, so build() "
98
+ f"expects {self.n_params} values (inputs first, then {self.n_weights} "
99
+ "weights). Use .bind(x, weights) to pass data and weights separately."
100
+ )
101
+ return spec.bind(arr)
102
+
103
+ def bind(self, x: ArrayLike, weights: ArrayLike) -> CircuitSpec:
104
+ """Bind data and weights separately, in that order."""
105
+ angles = self.angles(x) if hasattr(self, "angles") else np.asarray(x, dtype=float)
106
+ return self.build(np.concatenate([np.ravel(angles), np.ravel(weights)]))
107
+
108
+ def __call__(self, theta: ArrayLike | None = None) -> CircuitSpec:
109
+ return self.build(theta)
110
+
111
+ # ------------------------------------------------------------------ shape --
112
+ @property
113
+ def n_params(self) -> int:
114
+ """Every parameter, inputs included. Inferred — never hand-counted."""
115
+ return self._template().n_params
116
+
117
+ @property
118
+ def n_weights(self) -> int:
119
+ """Trainable parameters only, excluding reserved input slots."""
120
+ return self.n_params - self.n_inputs
121
+
122
+ @property
123
+ def param_shape(self) -> tuple[int, ...]:
124
+ return (self.n_weights,)
125
+
126
+ def init(
127
+ self, method: str = "small", seed: int | None = None, scale: float = 0.1
128
+ ) -> npt.NDArray[Any]:
129
+ """Initial parameters.
130
+
131
+ ``small`` (default) keeps angles near zero, which keeps the circuit near
132
+ identity and the gradient away from the barren-plateau regime. ``uniform``
133
+ samples the full range — the standard way to *land* on a plateau, useful
134
+ when that is what you are studying. ``zeros`` is exactly identity.
135
+ """
136
+ rng = np.random.default_rng(seed)
137
+ n = self.n_weights
138
+ if method == "small":
139
+ return rng.normal(0.0, scale, n)
140
+ if method == "uniform":
141
+ return rng.uniform(-np.pi, np.pi, n)
142
+ if method == "zeros":
143
+ return np.zeros(n)
144
+ raise unknown("init method", method, ("small", "uniform", "zeros"))
145
+
146
+ # -------------------------------------------------------------- resources --
147
+ def resources(self) -> dict[str, object]:
148
+ """Gate counts, depth, and the *real* gradient cost."""
149
+ from qmlkit.gradients.parameter_shift import grad_circuit_cost
150
+
151
+ spec = self._template()
152
+ out = dict(spec.resources())
153
+ out["grad_circuits"] = grad_circuit_cost(spec)
154
+ return out
155
+
156
+ def __repr__(self) -> str:
157
+ return f"Ansatz({self.name!r}, n_qubits={self.n_qubits}, n_params={self.n_params})"
158
+
159
+
160
+ # --------------------------------------------------------------------------- #
161
+ # registry
162
+ # --------------------------------------------------------------------------- #
163
+ AnsatzFactory = Callable[..., Ansatz]
164
+ _REGISTRY: dict[str, AnsatzFactory] = {}
165
+
166
+
167
+ def register_ansatz(
168
+ name: str, factory: AnsatzFactory | None = None
169
+ ) -> Callable[[AnsatzFactory], AnsatzFactory] | AnsatzFactory:
170
+ """Register an ansatz factory. Usable as a decorator or a direct call.
171
+
172
+ def brick_wall(n_qubits, n_layers=3):
173
+ return Ansatz(n_qubits, repeat(n_layers, RotationLayer("ry")
174
+ + EntanglerLayer("cz", "alternating")))
175
+ """
176
+
177
+ def _register(f: AnsatzFactory) -> AnsatzFactory:
178
+ if name in _REGISTRY:
179
+ raise ValueError(f"ansatz {name!r} is already registered")
180
+ _REGISTRY[name] = f
181
+ return f
182
+
183
+ return _register if factory is None else _register(factory)
184
+
185
+
186
+ def get_ansatz(name: str, **kwargs: object) -> Ansatz:
187
+ """Build a registered ansatz by name."""
188
+ try:
189
+ factory = _REGISTRY[name]
190
+ except KeyError:
191
+ raise unknown(
192
+ "ansatz",
193
+ name,
194
+ list_ansatze(),
195
+ hint="Add your own with register_ansatz(name, factory).",
196
+ error=KeyError,
197
+ ) from None
198
+ return factory(**kwargs)
199
+
200
+
201
+ def list_ansatze() -> tuple[str, ...]:
202
+ return tuple(sorted(_REGISTRY))
203
+
204
+
205
+ # --------------------------------------------------------------------------- #
206
+ # the built-in zoo — every one is a short expression in the vocabulary
207
+ # --------------------------------------------------------------------------- #
208
+ def hardware_efficient(
209
+ n_qubits: int,
210
+ n_layers: int = 2,
211
+ rotations: Sequence[str] = ("ry", "rz"),
212
+ entangler: str = "cx",
213
+ pattern: str = "chain",
214
+ ) -> Ansatz:
215
+ """Rotations then entanglers, repeated. General-purpose, barren-plateau prone."""
216
+ return Ansatz(
217
+ n_qubits,
218
+ repeat(n_layers, RotationLayer(rotations) + EntanglerLayer(entangler, pattern)),
219
+ "hardware_efficient",
220
+ )
221
+
222
+
223
+ def strongly_entangling(n_qubits: int, n_layers: int = 2) -> Ansatz:
224
+ """Three rotations per wire, plus a ring of CX per layer."""
225
+ return Ansatz(
226
+ n_qubits,
227
+ repeat(n_layers, RotationLayer(("rz", "ry", "rz")) + EntanglerLayer("cx", "ring")),
228
+ "strongly_entangling",
229
+ )
230
+
231
+
232
+ def simplified_two_design(n_qubits: int, n_layers: int = 2) -> Ansatz:
233
+ """The standard reference ansatz in barren-plateau studies."""
234
+ return Ansatz(
235
+ n_qubits,
236
+ RotationLayer("ry")
237
+ + repeat(n_layers, EntanglerLayer("cz", "alternating") + RotationLayer("ry")),
238
+ "simplified_two_design",
239
+ )
240
+
241
+
242
+ def tree_tensor_network(
243
+ n_qubits: int,
244
+ filter: str | tuple[ConvFilter, int] = "ry_cx", # noqa: A002 - the domain word
245
+ tied: bool = False,
246
+ ) -> Ansatz:
247
+ """Log-depth merge tree — shallow, and resistant to barren plateaus.
248
+
249
+ Each merge is a two-qubit ``filter`` from the same registry a QCNN convolves
250
+ with, so the tensor at every node is as general as you choose to pay for.
251
+ """
252
+ fn, n_params = _resolve_filter(filter)
253
+
254
+ def build(qc: QCircuit, ctx: BuildContext) -> None:
255
+ nodes = list(ctx.active)
256
+ shared = tuple(ctx.new_param() for _ in range(n_params)) if tied else None
257
+ while len(nodes) > 1:
258
+ nxt = []
259
+ for i in range(0, len(nodes) - 1, 2):
260
+ a, b = nodes[i], nodes[i + 1]
261
+ params = shared or tuple(ctx.new_param() for _ in range(n_params))
262
+ fn(qc, a, b, params)
263
+ nxt.append(b)
264
+ if len(nodes) % 2:
265
+ nxt.append(nodes[-1])
266
+ nodes = nxt
267
+ ctx.active = nodes
268
+
269
+ return Ansatz(n_qubits, Custom(build, "ttn"), "tree_tensor_network")
270
+
271
+
272
+ def mps_ansatz(
273
+ n_qubits: int,
274
+ filter: str | tuple[ConvFilter, int] = "ry_cx", # noqa: A002 - the domain word
275
+ tied: bool = False,
276
+ ) -> Ansatz:
277
+ """A staircase of two-qubit blocks — a bond-dimension-2 matrix product state.
278
+
279
+ The block is the same two-qubit *filter* a QCNN convolves with, so it comes from
280
+ the same registry: ``"su4"`` gives a genuine bond-dimension-2 MPS with arbitrary
281
+ tensors, ``"ry_cx"`` the cheap real-valued one. ``tied=True`` reuses one tensor
282
+ down the whole chain, which is the translation-invariant MPS.
283
+ """
284
+ fn, n_params = _resolve_filter(filter)
285
+
286
+ def build(qc: QCircuit, ctx: BuildContext) -> None:
287
+ wires = ctx.active
288
+ shared = tuple(ctx.new_param() for _ in range(n_params)) if tied else None
289
+ for i in range(len(wires) - 1):
290
+ params = shared or tuple(ctx.new_param() for _ in range(n_params))
291
+ fn(qc, wires[i], wires[i + 1], params)
292
+
293
+ return Ansatz(n_qubits, Custom(build, "mps"), "mps")
294
+
295
+
296
+ # --------------------------------------------------------------------------- #
297
+ # QCNN convolution filters
298
+ #
299
+ # A QCNN is a *pattern* — convolve, pool, repeat — not one circuit. The literature
300
+ # differs mainly in which two-qubit filter slides across the register: Cong, Choi &
301
+ # Lukin (2019) use a general SU(4); Hur, Kim & Park (2022) benchmark eight cheaper
302
+ # ones. So the filter is a value you pass in, not a subclass you pick, and adding
303
+ # your own is a function rather than a fork.
304
+ #
305
+ # A filter is ``fn(qc, a, b, params)`` plus the number of parameters it consumes.
306
+ # --------------------------------------------------------------------------- #
307
+ ConvFilter = Callable[[QCircuit, int, int, "Sequence[Any]"], None]
308
+
309
+ _FILTERS: dict[str, tuple[ConvFilter, int]] = {}
310
+
311
+
312
+ def register_conv_filter(name: str, fn: ConvFilter, n_params: int) -> None:
313
+ """Make a two-qubit filter reachable by name from :func:`conv_block`."""
314
+ if name in _FILTERS:
315
+ raise ValueError(f"conv filter {name!r} is already registered")
316
+ _FILTERS[name] = (fn, n_params)
317
+
318
+
319
+ def list_conv_filters() -> tuple[str, ...]:
320
+ return tuple(sorted(_FILTERS))
321
+
322
+
323
+ def _f_ry_cx(qc: QCircuit, a: int, b: int, p: Sequence[Any]) -> None:
324
+ """The cheapest useful filter: one rotation each, one entangler."""
325
+ qc.ry(a, p[0])
326
+ qc.ry(b, p[1])
327
+ qc.cx(a, b)
328
+
329
+
330
+ def _f_real(qc: QCircuit, a: int, b: int, p: Sequence[Any]) -> None:
331
+ """Real-amplitude block — rotations either side of the entangler."""
332
+ qc.ry(a, p[0])
333
+ qc.ry(b, p[1])
334
+ qc.cx(a, b)
335
+ qc.ry(a, p[2])
336
+ qc.ry(b, p[3])
337
+
338
+
339
+ def _f_zz(qc: QCircuit, a: int, b: int, p: Sequence[Any]) -> None:
340
+ """Single-qubit Ry rotations plus a genuine ZZ interaction.
341
+
342
+ The single-qubit part is deliberately ``ry`` rather than ``rz``: an all-``rz``
343
+ filter is diagonal, so from ``|0...0>`` it does *nothing at all* — measured, not
344
+ guessed, and the reason ``test_no_shipped_filter_is_inert`` exists.
345
+ """
346
+ qc.ry(a, p[0])
347
+ qc.ry(b, p[1])
348
+ qc.cx(a, b)
349
+ qc.rz(b, p[2])
350
+ qc.cx(a, b)
351
+
352
+
353
+ def _u3(qc: QCircuit, wire: int, p: Sequence[Any]) -> None:
354
+ """An arbitrary single-qubit unitary, as Rz-Ry-Rz."""
355
+ qc.rz(wire, p[0])
356
+ qc.ry(wire, p[1])
357
+ qc.rz(wire, p[2])
358
+
359
+
360
+ def _f_su4(qc: QCircuit, a: int, b: int, p: Sequence[Any]) -> None:
361
+ """A general two-qubit unitary — the Vatan-Williams form, 3 CNOTs, 15 angles.
362
+
363
+ This is what Cong, Choi & Lukin's QCNN uses. It can express *any* two-qubit
364
+ gate, which is the most expressive filter possible and also the most expensive.
365
+ """
366
+ _u3(qc, a, p[0:3])
367
+ _u3(qc, b, p[3:6])
368
+ qc.cx(b, a)
369
+ qc.rz(a, p[6])
370
+ qc.ry(b, p[7])
371
+ qc.cx(a, b)
372
+ qc.ry(b, p[8])
373
+ qc.cx(b, a)
374
+ _u3(qc, a, p[9:12])
375
+ _u3(qc, b, p[12:15])
376
+
377
+
378
+ register_conv_filter("ry_cx", _f_ry_cx, 2)
379
+ register_conv_filter("real", _f_real, 4)
380
+ register_conv_filter("zz", _f_zz, 3)
381
+ register_conv_filter("su4", _f_su4, 15)
382
+
383
+
384
+ def _resolve_filter(spec: str | tuple[ConvFilter, int]) -> tuple[ConvFilter, int]:
385
+ if isinstance(spec, str):
386
+ try:
387
+ return _FILTERS[spec]
388
+ except KeyError:
389
+ raise unknown(
390
+ "conv filter",
391
+ spec,
392
+ list_conv_filters(),
393
+ hint="Add your own with register_conv_filter(name, factory).",
394
+ error=KeyError,
395
+ ) from None
396
+ fn, n_params = spec
397
+ return fn, int(n_params)
398
+
399
+
400
+ def conv_block(
401
+ pattern: str = "chain",
402
+ tied: bool = True,
403
+ filter: str | tuple[ConvFilter, int] = "ry_cx", # noqa: A002 - the domain word
404
+ ) -> Block:
405
+ """A QCNN convolution layer: slide one two-qubit ``filter`` across ``pattern``.
406
+
407
+ ``tied=True`` allocates **one** filter and reuses it at every pair — the genuine
408
+ convolutional structure, and the reason the gradient code sums over occurrences.
409
+ ``tied=False`` gives each pair its own weights.
410
+
411
+ ``filter`` is a registered name (:func:`list_conv_filters`) or a
412
+ ``(fn, n_params)`` pair, where ``fn(qc, a, b, params)`` writes the filter. That
413
+ is the whole extension point: a filter from a paper we have never heard of is a
414
+ function you pass in, not a class you subclass.
415
+ """
416
+ fn, n_params = _resolve_filter(filter)
417
+ label = filter if isinstance(filter, str) else getattr(filter[0], "__name__", "custom")
418
+
419
+ def build(qc: QCircuit, ctx: BuildContext) -> None:
420
+ wires = ctx.active
421
+ pairs = entangler_pairs(len(wires), pattern)
422
+ if not pairs:
423
+ return
424
+ shared = tuple(ctx.new_param() for _ in range(n_params)) if tied else None
425
+ for a, b in pairs:
426
+ params = (
427
+ shared if shared is not None else tuple(ctx.new_param() for _ in range(n_params))
428
+ )
429
+ fn(qc, wires[a], wires[b], params)
430
+
431
+ return Custom(build, f"conv_{label}{'_tied' if tied else ''}")
432
+
433
+
434
+ def qcnn_ansatz(
435
+ n_qubits: int,
436
+ tie_weights: bool = True,
437
+ filter: str | tuple[ConvFilter, int] = "ry_cx", # noqa: A002 - the domain word
438
+ pattern: str = "chain",
439
+ pool: str = "discard",
440
+ keep: str = "odd",
441
+ ) -> Ansatz:
442
+ """Convolution + pooling, halving the register until one qubit is left.
443
+
444
+ There is no single "the QCNN": papers differ in the two-qubit filter and in how
445
+ pooling discards a wire. Rather than shipping one class per paper, this is the
446
+ shared skeleton with both choices exposed — so reproducing a particular variant
447
+ is a keyword, and inventing one is a function.
448
+
449
+ ``tie_weights=True`` shares one filter across all applications in a layer — the
450
+ genuine convolutional structure, and the case whose gradient needs a sum over
451
+ occurrences.
452
+ """
453
+ import math
454
+
455
+ n_layers = max(1, int(math.ceil(math.log2(n_qubits))))
456
+ layer = conv_block(pattern=pattern, tied=tie_weights, filter=filter) + PoolLayer(
457
+ keep, mode=pool, tied=tie_weights
458
+ )
459
+ return Ansatz(n_qubits, repeat(n_layers, layer), "qcnn")
460
+
461
+
462
+ def qaoa_ansatz(
463
+ n_qubits: int,
464
+ edges: Sequence[tuple[int, int]] | None = None,
465
+ p: int = 1,
466
+ mixer: str = "x",
467
+ ) -> Ansatz:
468
+ """Cost and mixer layers — only ``2p`` parameters, whatever the width.
469
+
470
+ Both angles in a round are shared across all their gates, which is what keeps
471
+ the parameter count at ``2p`` rather than growing with the graph.
472
+ """
473
+ graph = list(edges) if edges is not None else list(entangler_pairs(n_qubits, "chain"))
474
+ if mixer not in ("x", "y", "xy"):
475
+ raise unknown("mixer", mixer, ("x", "y", "xy"))
476
+
477
+ def build(qc: QCircuit, ctx: BuildContext) -> None:
478
+ for q in ctx.active:
479
+ qc.h(q)
480
+ for _ in range(p):
481
+ gamma = ctx.new_param() # one cost angle for the whole round
482
+ for a, b in graph:
483
+ qc.cx(a, b)
484
+ qc.apply("rz", b, gamma)
485
+ qc.cx(a, b)
486
+ beta = ctx.new_param() # one mixer angle for the whole round
487
+ if mixer in ("x", "xy"):
488
+ for q in ctx.active:
489
+ qc.apply("rx", q, beta)
490
+ if mixer in ("y", "xy"):
491
+ for q in ctx.active:
492
+ qc.apply("ry", q, beta)
493
+
494
+ return Ansatz(n_qubits, Custom(build, "qaoa"), "qaoa")
495
+
496
+
497
+ # Registered after definition rather than by decorator: the decorator returns a
498
+ # union that mypy cannot see through, and keeping these functions plainly typed is
499
+ # worth more than the sugar.
500
+ _BUILTINS: list[tuple[str, AnsatzFactory]] = [
501
+ ("hardware_efficient", hardware_efficient),
502
+ ("strongly_entangling", strongly_entangling),
503
+ ("simplified_two_design", simplified_two_design),
504
+ ("tree_tensor_network", tree_tensor_network),
505
+ ("mps", mps_ansatz),
506
+ ("qcnn", qcnn_ansatz),
507
+ ("qaoa", qaoa_ansatz),
508
+ ]
509
+ for _name, _factory in _BUILTINS:
510
+ register_ansatz(_name, _factory)
511
+
512
+
513
+ def basic_entangler(n_qubits: int, n_layers: int = 2, rotation: str = "rx") -> Ansatz:
514
+ """One rotation per wire plus a ring of CNOTs — the minimal useful template."""
515
+ return Ansatz(
516
+ n_qubits,
517
+ repeat(n_layers, RotationLayer((rotation,)) + EntanglerLayer("cx", "ring")),
518
+ "basic_entangler",
519
+ )
520
+
521
+
522
+ def two_local(
523
+ n_qubits: int,
524
+ n_layers: int = 2,
525
+ rotations: Sequence[str] = ("ry",),
526
+ entangler: str = "cx",
527
+ pattern: str = "full",
528
+ ) -> Ansatz:
529
+ """A configurable rotation/entangler alternation, ending on a rotation layer."""
530
+ return Ansatz(
531
+ n_qubits,
532
+ repeat(n_layers, RotationLayer(rotations) + EntanglerLayer(entangler, pattern))
533
+ + RotationLayer(rotations),
534
+ "two_local",
535
+ )
536
+
537
+
538
+ def random_layers(
539
+ n_qubits: int, n_layers: int = 2, ratio_imprimitive: float = 0.3, seed: int | None = None
540
+ ) -> Ansatz:
541
+ """Randomly placed rotations and CNOTs — the baseline a new ansatz must beat."""
542
+ rng = np.random.default_rng(seed)
543
+ plan: list[tuple[str, tuple[int, ...]]] = []
544
+ for _ in range(n_layers):
545
+ for q in range(n_qubits):
546
+ plan.append((str(rng.choice(["rx", "ry", "rz"])), (q,)))
547
+ if n_qubits > 1 and rng.random() < ratio_imprimitive:
548
+ other = int(rng.choice([w for w in range(n_qubits) if w != q]))
549
+ plan.append(("cx", (q, other)))
550
+
551
+ def build(qc: QCircuit, ctx: BuildContext) -> None:
552
+ for gate, wires in plan:
553
+ if gate == "cx":
554
+ qc.cx(*wires)
555
+ else:
556
+ qc.apply(gate, wires[0], ctx.new_param())
557
+
558
+ return Ansatz(n_qubits, Custom(build, "random"), "random_layers")
559
+
560
+
561
+ _LATE: list[tuple[str, AnsatzFactory]] = [
562
+ ("basic_entangler", basic_entangler),
563
+ ("two_local", two_local),
564
+ ("random_layers", random_layers),
565
+ ]
566
+ for _name, _factory in _LATE:
567
+ register_ansatz(_name, _factory)
568
+
569
+ # re-exported so `from qmlkit.ansatz import ...` reaches the vocabulary too
570
+ _VOCAB = (RotationLayer, EntanglerLayer, ParametricEntangler, PoolLayer, Custom, repeat, share)