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/library.py
ADDED
|
@@ -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)
|