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,198 @@
1
+ """Quantum policy gradient — a variational circuit as an RL policy.
2
+
3
+ REINFORCE, with the policy replaced by a circuit: encode the observation, measure a
4
+ few observables, softmax them into action probabilities, and push up the log-
5
+ probability of whatever earned reward.
6
+
7
+ policy = QuantumPolicy(n_observations=2, n_actions=2)
8
+ result = train_reinforce(policy, ContextualBandit(seed=0), n_episodes=200, seed=0)
9
+
10
+ The environment is an argument with a three-method protocol (``reset``, ``step``,
11
+ ``n_observations``/``n_actions``), so a Gym environment wraps in a few lines and
12
+ nothing here needs Gym installed. The ansatz and feature map are arguments too, as
13
+ everywhere else.
14
+
15
+ The gradient is the ordinary policy-gradient one — ``grad log pi(a|s) * return`` —
16
+ computed through :func:`qmlkit.grad`, so it is exact rather than finite-differenced.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from collections.abc import Sequence
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Protocol
24
+
25
+ import numpy as np
26
+ import numpy.typing as npt
27
+
28
+ from qmlkit.ansatz.library import Ansatz, hardware_efficient
29
+ from qmlkit.core.execute import BackendLike, expval
30
+ from qmlkit.core.observables import Observable, Z
31
+ from qmlkit.encoding.feature_maps import AngleFeatureMap, FeatureMap
32
+ from qmlkit.gradients.dispatch import grad
33
+
34
+ __all__ = ["QuantumPolicy", "ContextualBandit", "train_reinforce", "ReinforceResult", "Environment"]
35
+
36
+
37
+ class Environment(Protocol):
38
+ """The three things an RL loop needs. Deliberately not Gym."""
39
+
40
+ n_observations: int
41
+ n_actions: int
42
+
43
+ def reset(self) -> npt.NDArray[Any]: ...
44
+
45
+ def step(self, action: int) -> tuple[npt.NDArray[Any], float, bool]: ...
46
+
47
+
48
+ class ContextualBandit:
49
+ """A tiny environment with a known optimal policy, so training is checkable.
50
+
51
+ The observation is a random vector; the correct action is the sign of its first
52
+ coordinate. One step per episode, reward 1 for right and 0 for wrong — so the
53
+ optimal return is exactly 1.0 and "did it learn" has an unambiguous answer.
54
+ """
55
+
56
+ n_observations = 2
57
+ n_actions = 2
58
+
59
+ def __init__(self, seed: int | None = None) -> None:
60
+ self._rng = np.random.default_rng(seed)
61
+ self._state: npt.NDArray[Any] = np.zeros(self.n_observations)
62
+
63
+ def reset(self) -> npt.NDArray[Any]:
64
+ self._state = self._rng.uniform(-1.0, 1.0, self.n_observations)
65
+ return self._state
66
+
67
+ def step(self, action: int) -> tuple[npt.NDArray[Any], float, bool]:
68
+ correct = int(self._state[0] > 0)
69
+ return self._state, float(action == correct), True
70
+
71
+ def optimal_return(self) -> float:
72
+ return 1.0
73
+
74
+
75
+ class QuantumPolicy:
76
+ """A circuit policy: observation in, action probabilities out."""
77
+
78
+ def __init__(
79
+ self,
80
+ n_observations: int,
81
+ n_actions: int,
82
+ feature_map: FeatureMap | None = None,
83
+ ansatz: Ansatz | None = None,
84
+ observables: Sequence[Observable] | None = None,
85
+ n_layers: int = 2,
86
+ beta: float = 2.0,
87
+ backend: BackendLike = None,
88
+ seed: int | None = None,
89
+ ) -> None:
90
+ n_qubits = max(n_observations, n_actions)
91
+ self.n_observations = n_observations
92
+ self.n_actions = n_actions
93
+ self.n_qubits = n_qubits
94
+ self.feature_map = feature_map or AngleFeatureMap(n_qubits, entangle=n_qubits > 1)
95
+ self.ansatz = ansatz or hardware_efficient(n_qubits, n_layers)
96
+ self.observables = list(observables) if observables else [Z(a) for a in range(n_actions)]
97
+ self.beta = beta # softmax inverse temperature
98
+ self.backend = backend
99
+ # Seeded from the argument, not from a constant: with seed baked in, every
100
+ # QuantumPolicy started from the same point and "average over seeds" was
101
+ # impossible to express.
102
+ self.theta = self.ansatz.init("small", seed=seed)
103
+ self._spec = self.feature_map.build_parametric().compose(self.ansatz.build())
104
+ self._n_inputs = self.feature_map.n_angles
105
+
106
+ def _full(self, observation: npt.NDArray[Any], theta: npt.NDArray[Any]) -> npt.NDArray[Any]:
107
+ padded = np.zeros(self.n_qubits)
108
+ padded[: self.n_observations] = np.asarray(observation, dtype=float).ravel()
109
+ return np.concatenate([self.feature_map.angles(padded), theta])
110
+
111
+ def logits(
112
+ self, observation: npt.NDArray[Any], theta: npt.NDArray[Any] | None = None
113
+ ) -> npt.NDArray[Any]:
114
+ full = self._full(observation, self.theta if theta is None else theta)
115
+ return np.array(
116
+ [expval(self._spec, o, theta=full, backend=self.backend) for o in self.observables]
117
+ )
118
+
119
+ def probabilities(
120
+ self, observation: npt.NDArray[Any], theta: npt.NDArray[Any] | None = None
121
+ ) -> npt.NDArray[Any]:
122
+ z = self.beta * self.logits(observation, theta)
123
+ z = z - z.max()
124
+ e = np.exp(z)
125
+ return e / e.sum()
126
+
127
+ def sample(self, observation: npt.NDArray[Any], rng: np.random.Generator) -> int:
128
+ return int(rng.choice(self.n_actions, p=self.probabilities(observation)))
129
+
130
+ def grad_log_prob(self, observation: npt.NDArray[Any], action: int) -> npt.NDArray[Any]:
131
+ r"""``d/dtheta log pi(a|s)``.
132
+
133
+ For a softmax over measured observables this is
134
+ ``beta * (dO_a/dtheta - sum_b pi_b dO_b/dtheta)`` — one exact circuit gradient
135
+ per action, no finite differences anywhere.
136
+ """
137
+ full = self._full(observation, self.theta)
138
+ probs = self.probabilities(observation)
139
+ jac = np.stack(
140
+ [
141
+ grad(self._spec, full, o, backend=self.backend)[self._n_inputs :]
142
+ for o in self.observables
143
+ ]
144
+ )
145
+ return self.beta * (jac[action] - probs @ jac)
146
+
147
+ def __repr__(self) -> str:
148
+ return (
149
+ f"QuantumPolicy(obs={self.n_observations}, actions={self.n_actions}, "
150
+ f"P={self.theta.size}, ansatz={self.ansatz.name!r})"
151
+ )
152
+
153
+
154
+ @dataclass
155
+ class ReinforceResult:
156
+ theta: npt.NDArray[Any]
157
+ returns: list[float] = field(default_factory=list)
158
+
159
+ def mean_return(self, last: int = 50) -> float:
160
+ return float(np.mean(self.returns[-last:])) if self.returns else 0.0
161
+
162
+ def __repr__(self) -> str:
163
+ return (
164
+ f"ReinforceResult(episodes={len(self.returns)}, "
165
+ f"final_mean_return={self.mean_return():.4f})"
166
+ )
167
+
168
+
169
+ def train_reinforce(
170
+ policy: QuantumPolicy,
171
+ env: Environment,
172
+ n_episodes: int = 200,
173
+ lr: float = 0.2,
174
+ baseline: bool = True,
175
+ seed: int | None = None,
176
+ ) -> ReinforceResult:
177
+ """REINFORCE with an optional moving-average baseline.
178
+
179
+ The baseline subtracts a running mean return before scaling the gradient. It does
180
+ not change what is being optimised, only the variance of the estimate — which for
181
+ a policy sampled one episode at a time is the thing that decides whether it
182
+ learns at all.
183
+ """
184
+ rng = np.random.default_rng(seed)
185
+ returns: list[float] = []
186
+ running = 0.0
187
+
188
+ for episode in range(n_episodes):
189
+ observation = env.reset()
190
+ action = policy.sample(observation, rng)
191
+ _, reward, _ = env.step(action)
192
+ returns.append(float(reward))
193
+
194
+ advantage = reward - running if baseline else reward
195
+ policy.theta = policy.theta + lr * advantage * policy.grad_log_prob(observation, action)
196
+ running += (reward - running) / (episode + 1)
197
+
198
+ return ReinforceResult(theta=policy.theta, returns=returns)
@@ -0,0 +1,198 @@
1
+ """The variational quantum eigensolver.
2
+
3
+ VQE is a loop, not an architecture: prepare a parameterised state, measure an energy,
4
+ step downhill, repeat. Everything that makes one VQE differ from another — the
5
+ ansatz, the optimiser, the gradient method, the shot budget — is therefore an
6
+ argument, and the class itself is thin on purpose.
7
+
8
+ from qmlkit.algorithms import VQE, ising_hamiltonian
9
+
10
+ H = ising_hamiltonian(4, j=1.0, h=0.5)
11
+ result = VQE(H, n_qubits=4).run(seed=0)
12
+ print(result.energy, result.error_vs_exact)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable, Sequence
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+ import numpy as np
22
+ import numpy.typing as npt
23
+
24
+ from qmlkit.algorithms.hamiltonians import exact_ground_energy
25
+ from qmlkit.ansatz.library import Ansatz, hardware_efficient
26
+ from qmlkit.core.execute import BackendLike, expectation
27
+ from qmlkit.core.observables import Observable, observable_support
28
+
29
+ __all__ = ["VQE", "VQEResult", "OPTIMIZERS"]
30
+
31
+ #: ``fn(loss, theta0, **kwargs) -> (theta, history)`` — the shape every optimiser here
32
+ #: already has, so a custom one is a function rather than an adapter class.
33
+ Optimizer = Callable[..., "tuple[npt.NDArray[Any], list[float]]"]
34
+
35
+
36
+ def _rotosolve(loss, theta0, **kw): # type: ignore[no-untyped-def]
37
+ from qmlkit.optim import minimize_rotosolve
38
+
39
+ return minimize_rotosolve(loss, theta0, **kw)
40
+
41
+
42
+ def _spsa(loss, theta0, **kw): # type: ignore[no-untyped-def]
43
+ from qmlkit.gradients.spsa import minimize_spsa
44
+
45
+ return minimize_spsa(loss, theta0, **kw)
46
+
47
+
48
+ def _gradient_descent(loss, theta0, *, grad, n_steps=100, lr=0.1, **kw): # type: ignore[no-untyped-def]
49
+ theta = np.asarray(theta0, dtype=float).copy()
50
+ history = [float(loss(theta))]
51
+ for _ in range(n_steps):
52
+ theta = theta - lr * grad(theta)
53
+ history.append(float(loss(theta)))
54
+ return theta, history
55
+
56
+
57
+ OPTIMIZERS: dict[str, Optimizer] = {
58
+ "rotosolve": _rotosolve,
59
+ "spsa": _spsa,
60
+ "gradient-descent": _gradient_descent,
61
+ }
62
+
63
+
64
+ @dataclass
65
+ class VQEResult:
66
+ """What a run produced, and how good it actually is."""
67
+
68
+ energy: float
69
+ theta: npt.NDArray[Any]
70
+ history: list[float] = field(default_factory=list)
71
+ exact: float | None = None
72
+ n_evaluations: int = 0
73
+
74
+ @property
75
+ def error_vs_exact(self) -> float | None:
76
+ """Absolute error against dense diagonalisation, when that was computed."""
77
+ return None if self.exact is None else abs(self.energy - self.exact)
78
+
79
+ def __repr__(self) -> str:
80
+ tail = (
81
+ ""
82
+ if self.exact is None
83
+ else f", exact={self.exact:.8f}, error={self.error_vs_exact:.2e}"
84
+ )
85
+ return f"VQEResult(energy={self.energy:.8f}{tail}, steps={len(self.history) - 1})"
86
+
87
+
88
+ class VQE:
89
+ """Minimise ``<H>`` over a parameterised state.
90
+
91
+ Parameters
92
+ ----------
93
+ hamiltonian
94
+ Any observable. :mod:`qmlkit.algorithms.hamiltonians` has constructors.
95
+ ansatz
96
+ The trial state. Defaults to a hardware-efficient circuit wide enough for
97
+ the Hamiltonian's support — replaceable with anything, including one you
98
+ invented, because an ``Ansatz`` is the only contract.
99
+ optimizer
100
+ A name from :data:`OPTIMIZERS` or any ``fn(loss, theta0, **kw)`` returning
101
+ ``(theta, history)``.
102
+ gradient
103
+ Passed through to :func:`qmlkit.grad`. Only consulted by gradient-based
104
+ optimisers; Rotosolve and SPSA never ask for one.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ hamiltonian: Observable,
110
+ ansatz: Ansatz | None = None,
111
+ n_qubits: int | None = None,
112
+ optimizer: str | Optimizer = "rotosolve",
113
+ gradient: str = "auto",
114
+ backend: BackendLike = None,
115
+ shots: int | None = None,
116
+ ) -> None:
117
+ support = observable_support(hamiltonian)
118
+ width = n_qubits or (max(support) + 1 if support else 1)
119
+ if ansatz is not None and ansatz.n_qubits < width:
120
+ raise ValueError(
121
+ f"the ansatz has {ansatz.n_qubits} qubits but the Hamiltonian acts on {width}"
122
+ )
123
+ self.hamiltonian = hamiltonian
124
+ self.ansatz = ansatz or hardware_efficient(width, n_layers=2)
125
+ self.n_qubits = self.ansatz.n_qubits
126
+ self.optimizer = optimizer
127
+ self.gradient = gradient
128
+ self.backend = backend
129
+ self.shots = shots
130
+ self._spec = self.ansatz.build()
131
+ self.n_evaluations = 0
132
+
133
+ # ------------------------------------------------------------------ energy --
134
+ def energy(self, theta: Sequence[float]) -> float:
135
+ """``<H>`` at these parameters."""
136
+ self.n_evaluations += 1
137
+ value = expectation(
138
+ self._spec,
139
+ self.hamiltonian,
140
+ theta=np.asarray(theta, dtype=float),
141
+ shots=self.shots,
142
+ backend=self.backend,
143
+ )
144
+ return float(value)
145
+
146
+ def gradient_of_energy(self, theta: Sequence[float]) -> npt.NDArray[Any]:
147
+ from qmlkit.gradients.dispatch import grad
148
+
149
+ return grad(
150
+ self._spec,
151
+ np.asarray(theta, dtype=float),
152
+ self.hamiltonian,
153
+ method=self.gradient,
154
+ backend=self.backend,
155
+ shots=self.shots,
156
+ )
157
+
158
+ # --------------------------------------------------------------------- run --
159
+ def run(
160
+ self,
161
+ theta0: Sequence[float] | None = None,
162
+ seed: int | None = None,
163
+ compare_exact: bool | None = None,
164
+ **optimizer_kwargs: Any,
165
+ ) -> VQEResult:
166
+ """Optimise, and report against the exact answer when that is affordable."""
167
+ start = (
168
+ np.asarray(theta0, dtype=float)
169
+ if theta0 is not None
170
+ else self.ansatz.init("small", seed=seed)
171
+ )
172
+ fn = OPTIMIZERS[self.optimizer] if isinstance(self.optimizer, str) else self.optimizer
173
+ if fn is _gradient_descent:
174
+ optimizer_kwargs.setdefault("grad", self.gradient_of_energy)
175
+ if fn is _spsa:
176
+ optimizer_kwargs.setdefault("seed", seed)
177
+
178
+ self.n_evaluations = 0
179
+ theta, history = fn(self.energy, start, **optimizer_kwargs)
180
+
181
+ # dense diagonalisation is exponential, so only offer it where it is cheap
182
+ if compare_exact is None:
183
+ compare_exact = self.n_qubits <= 12
184
+ exact = exact_ground_energy(self.hamiltonian, self.n_qubits) if compare_exact else None
185
+
186
+ return VQEResult(
187
+ energy=float(history[-1]),
188
+ theta=theta,
189
+ history=list(history),
190
+ exact=exact,
191
+ n_evaluations=self.n_evaluations,
192
+ )
193
+
194
+ def __repr__(self) -> str:
195
+ return (
196
+ f"VQE(n_qubits={self.n_qubits}, ansatz={self.ansatz.name!r}, "
197
+ f"P={self.ansatz.n_weights}, optimizer={self.optimizer!r})"
198
+ )
@@ -0,0 +1,68 @@
1
+ """Ansatz: the trainable block, and the vocabulary for inventing new ones."""
2
+
3
+ from qmlkit.ansatz.blocks import (
4
+ Block,
5
+ BuildContext,
6
+ Custom,
7
+ EncodingLayer,
8
+ EntanglerLayer,
9
+ ParametricEntangler,
10
+ PoolLayer,
11
+ RotationLayer,
12
+ Sequential,
13
+ repeat,
14
+ share,
15
+ )
16
+ from qmlkit.ansatz.library import (
17
+ Ansatz,
18
+ basic_entangler,
19
+ conv_block,
20
+ get_ansatz,
21
+ hardware_efficient,
22
+ list_ansatze,
23
+ list_conv_filters,
24
+ mps_ansatz,
25
+ qaoa_ansatz,
26
+ qcnn_ansatz,
27
+ random_layers,
28
+ register_ansatz,
29
+ register_conv_filter,
30
+ simplified_two_design,
31
+ strongly_entangling,
32
+ tree_tensor_network,
33
+ two_local,
34
+ )
35
+ from qmlkit.ansatz.reupload import ReuploadModel, reupload
36
+
37
+ __all__ = [
38
+ "Ansatz",
39
+ "register_ansatz",
40
+ "get_ansatz",
41
+ "list_ansatze",
42
+ "Block",
43
+ "BuildContext",
44
+ "Sequential",
45
+ "RotationLayer",
46
+ "EntanglerLayer",
47
+ "ParametricEntangler",
48
+ "PoolLayer",
49
+ "EncodingLayer",
50
+ "Custom",
51
+ "repeat",
52
+ "share",
53
+ "hardware_efficient",
54
+ "strongly_entangling",
55
+ "simplified_two_design",
56
+ "tree_tensor_network",
57
+ "mps_ansatz",
58
+ "qcnn_ansatz",
59
+ "qaoa_ansatz",
60
+ "conv_block",
61
+ "list_conv_filters",
62
+ "register_conv_filter",
63
+ "reupload",
64
+ "ReuploadModel",
65
+ "basic_entangler",
66
+ "two_local",
67
+ "random_layers",
68
+ ]