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,348 @@
1
+ """A composable vocabulary for building ansätze.
2
+
3
+ Most published ansätze are a handful of primitives stacked in a pattern. Writing
4
+ them as expressions in that vocabulary — rather than as bespoke functions — means a
5
+ new one costs a line and inherits everything: correct parameter-shift rules,
6
+ resource counting, a torch layer, and a place in the registry.
7
+
8
+ hardware_efficient = repeat(n_layers, RotationLayer(("ry", "rz")) + EntanglerLayer("cx"))
9
+
10
+ Blocks allocate their own parameters through a build context, so nothing is ever
11
+ hand-counted. :func:`share` re-uses one set of parameters across repetitions —
12
+ weight tying — which the gradient code already handles per occurrence.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable, Iterator, Sequence
18
+ from contextlib import contextmanager
19
+
20
+ from qmlkit.core.builder import QCircuit, entangler_pairs
21
+ from qmlkit.core.gates import get_gate
22
+ from qmlkit.core.ir import ParamRef
23
+ from qmlkit.utils.errors import unknown
24
+
25
+ __all__ = [
26
+ "Block",
27
+ "BuildContext",
28
+ "RotationLayer",
29
+ "EntanglerLayer",
30
+ "ParametricEntangler",
31
+ "PoolLayer",
32
+ "EncodingLayer",
33
+ "Sequential",
34
+ "Repeat",
35
+ "Share",
36
+ "Custom",
37
+ "repeat",
38
+ "share",
39
+ ]
40
+
41
+
42
+ class BuildContext:
43
+ """Hands out parameter slots and tracks which qubits are still active.
44
+
45
+ ``active`` shrinks as pooling layers halve the register (the QCNN pattern), so
46
+ later blocks act on what is left rather than on the full width.
47
+ """
48
+
49
+ def __init__(self, n_qubits: int, n_inputs: int = 0) -> None:
50
+ self.n_qubits = n_qubits
51
+ self.n_inputs = n_inputs
52
+ self.active: list[int] = list(range(n_qubits))
53
+ # inputs occupy the first n_inputs indices; weights follow. That split is what
54
+ # lets one flat vector carry both while df/dx and df/dtheta stay separable.
55
+ self._next = n_inputs
56
+ self._replay: list[int] | None = None
57
+ self._replay_pos = 0
58
+ self._log: list[int] = []
59
+
60
+ def input_ref(self, i: int, scale: float = 1.0, offset: float = 0.0) -> ParamRef:
61
+ """Reference encoding angle ``i``. Re-uploads reuse the same reference."""
62
+ if not 0 <= i < self.n_inputs:
63
+ raise IndexError(f"input {i} out of range for n_inputs={self.n_inputs}")
64
+ return ParamRef(i, scale, offset)
65
+
66
+ def new_param(self, scale: float = 1.0, offset: float = 0.0) -> ParamRef:
67
+ """Allocate a parameter — or replay a shared one, when tying weights."""
68
+ if self._replay is not None:
69
+ index = self._replay[self._replay_pos % len(self._replay)]
70
+ self._replay_pos += 1
71
+ else:
72
+ index = self._next
73
+ self._next += 1
74
+ self._log.append(index)
75
+ return ParamRef(index, scale, offset)
76
+
77
+ @property
78
+ def n_params(self) -> int:
79
+ """Total parameters, inputs included."""
80
+ return self._next
81
+
82
+ @property
83
+ def n_weights(self) -> int:
84
+ """Trainable parameters only."""
85
+ return self._next - self.n_inputs
86
+
87
+ def mark(self) -> int:
88
+ return len(self._log)
89
+
90
+ def since(self, mark: int) -> list[int]:
91
+ return self._log[mark:]
92
+
93
+ @contextmanager
94
+ def replaying(self, indices: Sequence[int]) -> Iterator[None]:
95
+ """Re-issue these parameter indices instead of allocating new ones."""
96
+ prev, prev_pos = self._replay, self._replay_pos
97
+ self._replay, self._replay_pos = list(indices), 0
98
+ try:
99
+ yield
100
+ finally:
101
+ self._replay, self._replay_pos = prev, prev_pos
102
+
103
+
104
+ class Block:
105
+ """One piece of an ansatz. Compose with ``+``."""
106
+
107
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None: # pragma: no cover - abstract
108
+ raise NotImplementedError
109
+
110
+ def __add__(self, other: Block) -> Sequential:
111
+ left = self.blocks if isinstance(self, Sequential) else (self,)
112
+ right = other.blocks if isinstance(other, Sequential) else (other,)
113
+ return Sequential(left + right)
114
+
115
+ def __repr__(self) -> str:
116
+ return f"{type(self).__name__}()"
117
+
118
+
119
+ class Sequential(Block):
120
+ """Blocks applied in order."""
121
+
122
+ def __init__(self, blocks: Sequence[Block]) -> None:
123
+ self.blocks = tuple(blocks)
124
+
125
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
126
+ for b in self.blocks:
127
+ b.emit(qc, ctx)
128
+
129
+ def __repr__(self) -> str:
130
+ return " + ".join(repr(b) for b in self.blocks)
131
+
132
+
133
+ class RotationLayer(Block):
134
+ """One trainable rotation per gate per active wire."""
135
+
136
+ def __init__(self, gates: str | Sequence[str] = ("ry",), wires: Sequence[int] | None = None):
137
+ self.gates = (gates,) if isinstance(gates, str) else tuple(gates)
138
+ self.wires = tuple(wires) if wires is not None else None
139
+ for g in self.gates:
140
+ if not get_gate(g).is_parametric:
141
+ raise ValueError(f"{g!r} takes no parameters; use EntanglerLayer for fixed gates")
142
+
143
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
144
+ wires = self.wires if self.wires is not None else ctx.active
145
+ for q in wires:
146
+ for g in self.gates:
147
+ qc.apply(g, q, ctx.new_param())
148
+
149
+ def __repr__(self) -> str:
150
+ return f"RotationLayer({list(self.gates)})"
151
+
152
+
153
+ class EntanglerLayer(Block):
154
+ """A layer of fixed two-qubit gates following a named pattern."""
155
+
156
+ def __init__(self, gate: str = "cx", pattern: str = "chain"):
157
+ if get_gate(gate).is_parametric:
158
+ raise ValueError(f"{gate!r} is parameterised; use ParametricEntangler")
159
+ self.gate = gate
160
+ self.pattern = pattern
161
+
162
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
163
+ wires = ctx.active
164
+ for a, b in entangler_pairs(len(wires), self.pattern):
165
+ qc.apply(self.gate, (wires[a], wires[b]))
166
+
167
+ def __repr__(self) -> str:
168
+ return f"EntanglerLayer({self.gate!r}, {self.pattern!r})"
169
+
170
+
171
+ class ParametricEntangler(Block):
172
+ """A layer of *trainable* two-qubit gates — exercises the four-term shift rule."""
173
+
174
+ def __init__(self, gate: str = "crz", pattern: str = "ring"):
175
+ if not get_gate(gate).is_parametric:
176
+ raise ValueError(f"{gate!r} takes no parameters; use EntanglerLayer")
177
+ self.gate = gate
178
+ self.pattern = pattern
179
+
180
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
181
+ wires = ctx.active
182
+ for a, b in entangler_pairs(len(wires), self.pattern):
183
+ qc.apply(self.gate, (wires[a], wires[b]), ctx.new_param())
184
+
185
+ def __repr__(self) -> str:
186
+ return f"ParametricEntangler({self.gate!r}, {self.pattern!r})"
187
+
188
+
189
+ class PoolLayer(Block):
190
+ """Halve the active register — the pooling half of a QCNN.
191
+
192
+ ``keep="odd"`` retains every second wire starting from the second, matching the
193
+ convention where the surviving qubit is the target of the preceding entangler.
194
+
195
+ Two pooling modes, because the literature uses both:
196
+
197
+ ``mode="discard"``
198
+ Simply stop using the wire. Cheap, adds no parameters, and the information
199
+ it held survives only through whatever the convolution already moved.
200
+
201
+ ``mode="controlled"``
202
+ Before dropping a wire, apply a trainable ``crz`` from it onto its surviving
203
+ partner, so pooling *learns* what to carry forward. This is the simulator's
204
+ stand-in for the measure-and-conditionally-rotate pooling of Cong, Choi &
205
+ Lukin, which needs mid-circuit measurement and feed-forward.
206
+
207
+ ``tied`` shares one pooling angle across the whole layer, matching the way a
208
+ tied convolution shares one filter.
209
+ """
210
+
211
+ def __init__(self, keep: str = "odd", mode: str = "discard", tied: bool = True):
212
+ if keep not in ("even", "odd"):
213
+ raise unknown("keep", keep, ("even", "odd"))
214
+ if mode not in ("discard", "controlled"):
215
+ raise unknown("mode", mode, ("discard", "controlled"))
216
+ self.keep = keep
217
+ self.mode = mode
218
+ self.tied = tied
219
+
220
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
221
+ if len(ctx.active) <= 1:
222
+ return
223
+ survivors = ctx.active[1::2] if self.keep == "odd" else ctx.active[0::2]
224
+ if self.mode == "controlled":
225
+ dropped = [w for w in ctx.active if w not in set(survivors)]
226
+ shared = ctx.new_param() if self.tied else None
227
+ for source, target in zip(dropped, survivors, strict=False):
228
+ qc.crz(source, target, shared if shared is not None else ctx.new_param())
229
+ ctx.active = survivors
230
+
231
+ def __repr__(self) -> str:
232
+ return f"PoolLayer({self.keep!r}, mode={self.mode!r})"
233
+
234
+
235
+ class Custom(Block):
236
+ """Wrap an arbitrary ``fn(qc, ctx)`` as a block.
237
+
238
+ The escape hatch: anything the vocabulary cannot express, written directly
239
+ against the builder, still composes with everything else.
240
+ """
241
+
242
+ def __init__(self, fn: Callable[[QCircuit, BuildContext], None], name: str = "Custom"):
243
+ self.fn = fn
244
+ self.name = name
245
+
246
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
247
+ self.fn(qc, ctx)
248
+
249
+ def __repr__(self) -> str:
250
+ return f"Custom({self.name!r})"
251
+
252
+
253
+ class EncodingLayer(Block):
254
+ """Insert a feature map as a block, so re-uploading is just composition.
255
+
256
+ Data re-uploading is not one structure. It is *any* interleaving of an encoding
257
+ with a trainable block, and which encoding, which block, and in what order are
258
+ all design choices:
259
+
260
+ repeat(3, EncodingLayer(fmap) + RotationLayer(("rz", "ry", "rz")) + EntanglerLayer())
261
+ repeat(2, RotationLayer("ry") + EncodingLayer(fmap)) # W before S
262
+ EncodingLayer(fmap) + repeat(4, RotationLayer("ry")) # encode once, vary often
263
+ EncodingLayer(zz) + RotationLayer("ry") + EncodingLayer(angle) # two different maps
264
+
265
+ Every repeat references the **same** input angles: re-uploading means feeding the
266
+ same data in again, not consuming new features.
267
+
268
+ To learn the *frequencies* rather than inherit them from the encoding, put a
269
+ classical layer in front — ``nn.Sequential(nn.Linear(d, d), QuantumLayer(...))``.
270
+ That is a scaling of the inputs, which torch already differentiates; it does not
271
+ need to live inside the circuit.
272
+ """
273
+
274
+ def __init__(self, feature_map: object) -> None:
275
+ self.feature_map = feature_map
276
+
277
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
278
+ n_angles = int(self.feature_map.n_angles) # type: ignore[attr-defined]
279
+ if ctx.n_inputs < n_angles:
280
+ raise ValueError(
281
+ f"the circuit reserves {ctx.n_inputs} input slots but this feature map "
282
+ f"needs {n_angles}; build it with reupload() or pass n_inputs to Ansatz"
283
+ )
284
+ spec = self.feature_map._emit( # type: ignore[attr-defined]
285
+ [ctx.input_ref(i) for i in range(n_angles)]
286
+ )
287
+ for op in spec.ops:
288
+ qc.apply(op.gate, op.qubits, *op.params)
289
+
290
+ def __repr__(self) -> str:
291
+ return f"EncodingLayer({type(self.feature_map).__name__})"
292
+
293
+
294
+ class Repeat(Block):
295
+ """Apply a block ``times`` times, with **fresh** parameters each time."""
296
+
297
+ def __init__(self, times: int, block: Block):
298
+ if times < 1:
299
+ raise ValueError("times must be at least 1")
300
+ self.times = times
301
+ self.block = block
302
+
303
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
304
+ for _ in range(self.times):
305
+ self.block.emit(qc, ctx)
306
+
307
+ def __repr__(self) -> str:
308
+ return f"repeat({self.times}, {self.block!r})"
309
+
310
+
311
+ class Share(Block):
312
+ """Apply a block ``times`` times, **re-using** the same parameters.
313
+
314
+ Weight tying, as a QCNN's shared convolution filter does. The gradient code
315
+ sums over each occurrence separately, which is the whole reason the IR tracks
316
+ slots rather than just parameters.
317
+ """
318
+
319
+ def __init__(self, times: int, block: Block):
320
+ if times < 1:
321
+ raise ValueError("times must be at least 1")
322
+ self.times = times
323
+ self.block = block
324
+
325
+ def emit(self, qc: QCircuit, ctx: BuildContext) -> None:
326
+ mark = ctx.mark()
327
+ self.block.emit(qc, ctx)
328
+ allocated = ctx.since(mark)
329
+ if not allocated:
330
+ for _ in range(self.times - 1):
331
+ self.block.emit(qc, ctx)
332
+ return
333
+ for _ in range(self.times - 1):
334
+ with ctx.replaying(allocated):
335
+ self.block.emit(qc, ctx)
336
+
337
+ def __repr__(self) -> str:
338
+ return f"share({self.times}, {self.block!r})"
339
+
340
+
341
+ def repeat(times: int, block: Block) -> Repeat:
342
+ """``repeat(3, RotationLayer("ry"))`` — three layers, fresh weights each."""
343
+ return Repeat(times, block)
344
+
345
+
346
+ def share(times: int, block: Block) -> Share:
347
+ """``share(3, conv_block)`` — three applications of one tied weight set."""
348
+ return Share(times, block)