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.
- qmlkit/__init__.py +495 -0
- qmlkit/_aliases.py +135 -0
- qmlkit/algorithms/__init__.py +82 -0
- qmlkit/algorithms/adapt.py +297 -0
- qmlkit/algorithms/autoencoder.py +206 -0
- qmlkit/algorithms/chemistry.py +222 -0
- qmlkit/algorithms/clustering.py +149 -0
- qmlkit/algorithms/hamiltonians.py +143 -0
- qmlkit/algorithms/molecule.py +442 -0
- qmlkit/algorithms/qaoa.py +208 -0
- qmlkit/algorithms/rl.py +198 -0
- qmlkit/algorithms/vqe.py +198 -0
- qmlkit/ansatz/__init__.py +68 -0
- qmlkit/ansatz/blocks.py +348 -0
- qmlkit/ansatz/library.py +570 -0
- qmlkit/ansatz/reupload.py +168 -0
- qmlkit/baselines.py +604 -0
- qmlkit/budget.py +234 -0
- qmlkit/core/__init__.py +1 -0
- qmlkit/core/backends/__init__.py +22 -0
- qmlkit/core/backends/_sampling.py +43 -0
- qmlkit/core/backends/base.py +256 -0
- qmlkit/core/backends/cirq_backend.py +110 -0
- qmlkit/core/backends/cirq_density_backend.py +71 -0
- qmlkit/core/backends/noisy.py +86 -0
- qmlkit/core/backends/numpy_backend.py +276 -0
- qmlkit/core/backends/qiskit_aer_backend.py +79 -0
- qmlkit/core/backends/qiskit_backend.py +104 -0
- qmlkit/core/backends/registry.py +210 -0
- qmlkit/core/backends/spinqit_backend.py +233 -0
- qmlkit/core/backends/torch_backend.py +185 -0
- qmlkit/core/builder.py +189 -0
- qmlkit/core/execute.py +193 -0
- qmlkit/core/gates.py +243 -0
- qmlkit/core/ir.py +320 -0
- qmlkit/core/observables.py +269 -0
- qmlkit/datasets.py +178 -0
- qmlkit/diagnostics.py +719 -0
- qmlkit/draw.py +177 -0
- qmlkit/encoding/__init__.py +63 -0
- qmlkit/encoding/amplitude.py +178 -0
- qmlkit/encoding/angle.py +61 -0
- qmlkit/encoding/feature_maps.py +353 -0
- qmlkit/encoding/hamiltonian.py +206 -0
- qmlkit/encoding/pipeline.py +198 -0
- qmlkit/encoding/scaling.py +139 -0
- qmlkit/evaluate.py +686 -0
- qmlkit/fourier.py +124 -0
- qmlkit/generative.py +406 -0
- qmlkit/gradients/__init__.py +61 -0
- qmlkit/gradients/adjoint.py +138 -0
- qmlkit/gradients/batch.py +275 -0
- qmlkit/gradients/dispatch.py +247 -0
- qmlkit/gradients/hadamard.py +108 -0
- qmlkit/gradients/parameter_shift.py +142 -0
- qmlkit/gradients/rules.py +151 -0
- qmlkit/gradients/spsa.py +134 -0
- qmlkit/imbalance.py +335 -0
- qmlkit/info.py +153 -0
- qmlkit/interop.py +778 -0
- qmlkit/kernels/__init__.py +69 -0
- qmlkit/kernels/estimators.py +206 -0
- qmlkit/kernels/matrix.py +439 -0
- qmlkit/kernels/models.py +315 -0
- qmlkit/metrics.py +394 -0
- qmlkit/nn/__init__.py +18 -0
- qmlkit/nn/advanced.py +254 -0
- qmlkit/nn/layer.py +343 -0
- qmlkit/nn/losses.py +124 -0
- qmlkit/nn/models.py +245 -0
- qmlkit/optim.py +306 -0
- qmlkit/provenance.py +271 -0
- qmlkit/py.typed +0 -0
- qmlkit/search.py +561 -0
- qmlkit/shadows.py +117 -0
- qmlkit/utils/__init__.py +19 -0
- qmlkit/utils/errors.py +130 -0
- qmlkit/utils/shots.py +55 -0
- qmlkit-0.1.0.dist-info/METADATA +745 -0
- qmlkit-0.1.0.dist-info/RECORD +83 -0
- qmlkit-0.1.0.dist-info/WHEEL +4 -0
- qmlkit-0.1.0.dist-info/licenses/LICENSE +202 -0
- qmlkit-0.1.0.dist-info/licenses/NOTICE +4 -0
qmlkit/ansatz/blocks.py
ADDED
|
@@ -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)
|