mlx-quantum 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.
@@ -0,0 +1,38 @@
1
+ from . import statevector
2
+ from .statevector import (
3
+ zero_state,
4
+ apply_1q,
5
+ apply_2q,
6
+ expval_z,
7
+ expval_all_z,
8
+ rx,
9
+ ry,
10
+ rz,
11
+ H,
12
+ X,
13
+ Y,
14
+ Z,
15
+ CX,
16
+ CZ,
17
+ )
18
+ from .layer import QuantumLayer
19
+
20
+ __all__ = [
21
+ "QuantumLayer",
22
+ "statevector",
23
+ "zero_state",
24
+ "apply_1q",
25
+ "apply_2q",
26
+ "expval_z",
27
+ "expval_all_z",
28
+ "rx",
29
+ "ry",
30
+ "rz",
31
+ "H",
32
+ "X",
33
+ "Y",
34
+ "Z",
35
+ "CX",
36
+ "CZ",
37
+ ]
38
+ __version__ = "0.1.0"
mlx_quantum/layer.py ADDED
@@ -0,0 +1,50 @@
1
+ import numpy as np
2
+ import mlx.core as mx
3
+ import mlx.nn as nn
4
+
5
+ from .statevector import zero_state, apply_1q, apply_2q, expval_all_z, H, ry, CX
6
+
7
+
8
+ class QuantumLayer(nn.Module):
9
+ """A trainable quantum layer that behaves like any other ``mlx.nn`` module.
10
+
11
+ Runs a hardware-efficient ansatz natively in MLX, so its weights train by
12
+ ordinary gradient descent on the Metal GPU — no Qiskit, no custom autodiff.
13
+
14
+ H on all qubits
15
+ RY(input[q]) on each qubit q (angle encoding)
16
+ reps x [ RY(weight) on each qubit; CX ring ]
17
+
18
+ Call with ``x`` of shape ``(num_qubits,)`` or ``(batch, num_qubits)``; returns
19
+ <Z> per qubit, shape ``(num_qubits,)`` or ``(batch, num_qubits)``.
20
+ """
21
+
22
+ def __init__(self, num_qubits: int, reps: int = 1, initial_weights: np.ndarray | None = None):
23
+ super().__init__()
24
+ self.num_qubits = num_qubits
25
+ self.reps = reps
26
+ if initial_weights is None:
27
+ initial_weights = np.random.uniform(-1, 1, reps * num_qubits)
28
+ self.weight = mx.array(np.asarray(initial_weights, np.float32))
29
+
30
+ def __call__(self, x: mx.array) -> mx.array:
31
+ was_1d = x.ndim == 1
32
+ if was_1d:
33
+ x = x[None]
34
+
35
+ n = self.num_qubits
36
+ state = zero_state(x.shape[0], n)
37
+ for q in range(n):
38
+ state = apply_1q(state, H, q)
39
+ for q in range(n):
40
+ state = apply_1q(state, ry(x[:, q]), q)
41
+
42
+ w = self.weight.reshape(self.reps, n)
43
+ for r in range(self.reps):
44
+ for q in range(n):
45
+ state = apply_1q(state, ry(w[r, q]), q)
46
+ for q in range(n - 1):
47
+ state = apply_2q(state, CX, q, q + 1)
48
+
49
+ out = expval_all_z(state)
50
+ return out[0] if was_1d else out
mlx_quantum/py.typed ADDED
File without changes
@@ -0,0 +1,114 @@
1
+ """Batched, differentiable statevector simulation in pure MLX.
2
+
3
+ A statevector is a complex ``mx.array`` of shape ``(batch,) + (2,) * num_qubits``.
4
+ Qubit ordering is little-endian to match Qiskit: qubit 0 is the fastest-varying
5
+ index, so flattening the state reproduces Qiskit's amplitude order exactly. Gates
6
+ are applied as ``einsum`` contractions, so the whole simulation runs on the Metal
7
+ GPU and is differentiable end to end via ``mx.grad`` — no custom vjp.
8
+ """
9
+
10
+ import math
11
+ import string
12
+
13
+ import numpy as np
14
+ import mlx.core as mx
15
+
16
+ _AXES = string.ascii_lowercase
17
+
18
+
19
+ def zero_state(batch: int, num_qubits: int) -> mx.array:
20
+ """The |0...0> state, broadcast over a batch."""
21
+ amp = np.zeros((1,) + (2,) * num_qubits, np.complex64)
22
+ amp.flat[0] = 1.0
23
+ return mx.array(np.broadcast_to(amp, (batch,) + (2,) * num_qubits).copy())
24
+
25
+
26
+ def _axis(qubit: int, n: int) -> int:
27
+ """Little-endian: qubit 0 is the last axis (fastest index when flattened)."""
28
+ return n - 1 - qubit
29
+
30
+
31
+ def apply_1q(state: mx.array, gate: mx.array, qubit: int) -> mx.array:
32
+ """Apply a single-qubit gate. ``gate`` is ``(2, 2)`` or, per-sample, ``(batch, 2, 2)``."""
33
+ n = state.ndim - 1
34
+ axes = _AXES[:n]
35
+ q = _axis(qubit, n)
36
+ src, tgt = "B" + axes, "B" + axes[:q] + "Z" + axes[q + 1:]
37
+ if gate.ndim == 2:
38
+ return mx.einsum(f"{src},Z{axes[q]}->{tgt}", state, gate)
39
+ return mx.einsum(f"{src},BZ{axes[q]}->{tgt}", state, gate)
40
+
41
+
42
+ def apply_2q(state: mx.array, gate: mx.array, q0: int, q1: int) -> mx.array:
43
+ """Apply a two-qubit gate given as a ``(2, 2, 2, 2)`` tensor ``[out0, out1, in0, in1]``."""
44
+ n = state.ndim - 1
45
+ axes = _AXES[:n]
46
+ a0, a1 = _axis(q0, n), _axis(q1, n)
47
+ tgt = list("B" + axes)
48
+ tgt[1 + a0], tgt[1 + a1] = "Y", "Z"
49
+ return mx.einsum(f"B{axes},YZ{axes[a0]}{axes[a1]}->{''.join(tgt)}", state, gate)
50
+
51
+
52
+ def expval_z(state: mx.array, qubit: int) -> mx.array:
53
+ """<Z> on one qubit, shape ``(batch,)``."""
54
+ probs = mx.abs(state) ** 2
55
+ keep = _axis(qubit, state.ndim - 1)
56
+ axes = [1 + j for j in range(state.ndim - 1) if j != keep]
57
+ marg = mx.sum(probs, axis=axes) if axes else probs
58
+ return marg[:, 0] - marg[:, 1]
59
+
60
+
61
+ def expval_all_z(state: mx.array) -> mx.array:
62
+ """<Z> on every qubit, shape ``(batch, num_qubits)``."""
63
+ return mx.stack([expval_z(state, q) for q in range(state.ndim - 1)], axis=1)
64
+
65
+
66
+ def _const(matrix) -> mx.array:
67
+ return mx.array(np.array(matrix, np.complex64))
68
+
69
+
70
+ H = _const([[1, 1], [1, -1]]) / math.sqrt(2)
71
+ X = _const([[0, 1], [1, 0]])
72
+ Y = _const([[0, -1j], [1j, 0]])
73
+ Z = _const([[1, 0], [0, -1]])
74
+
75
+
76
+ def _two_qubit(fn) -> mx.array:
77
+ g = np.zeros((2, 2, 2, 2), np.complex64)
78
+ for c in (0, 1):
79
+ for t in (0, 1):
80
+ oc, ot, val = fn(c, t)
81
+ g[oc, ot, c, t] = val
82
+ return mx.array(g)
83
+
84
+
85
+ CX = _two_qubit(lambda c, t: (c, t ^ c, 1.0))
86
+ CZ = _two_qubit(lambda c, t: (c, t, -1.0 if c and t else 1.0))
87
+
88
+
89
+ def rx(theta: mx.array) -> mx.array:
90
+ c, s = mx.cos(theta / 2), mx.sin(theta / 2)
91
+ return _rot(c, -1j * s, -1j * s, c)
92
+
93
+
94
+ def ry(theta: mx.array) -> mx.array:
95
+ c, s = mx.cos(theta / 2), mx.sin(theta / 2)
96
+ return _rot(c, -s, s, c)
97
+
98
+
99
+ def rz(theta: mx.array) -> mx.array:
100
+ p = _expi(theta / 2)
101
+ zero = mx.zeros_like(theta).astype(mx.complex64)
102
+ return _rot(mx.conj(p), zero, zero, p)
103
+
104
+
105
+ def _expi(phase: mx.array) -> mx.array:
106
+ return (mx.cos(phase) + 1j * mx.sin(phase)).astype(mx.complex64)
107
+
108
+
109
+ def _rot(a, b, c, d) -> mx.array:
110
+ """Stack four (scalar or batched) entries into a ``(*shape, 2, 2)`` gate."""
111
+ a, b, c, d = (mx.array(v).astype(mx.complex64) for v in (a, b, c, d))
112
+ row0 = mx.stack([a, b], axis=-1)
113
+ row1 = mx.stack([c, d], axis=-1)
114
+ return mx.stack([row0, row1], axis=-2)
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlx-quantum
3
+ Version: 0.1.0
4
+ Summary: Fast, differentiable quantum-machine-learning in pure Apple MLX — GPU statevector simulation with native autodiff.
5
+ Project-URL: Repository, https://github.com/UmarGit/mlx_quantum
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: apple-silicon,differentiable,mlx,quantum,quantum-machine-learning
9
+ Requires-Python: >=3.13
10
+ Requires-Dist: mlx>=0.31.2
11
+ Requires-Dist: numpy>=2.5.1
12
+ Provides-Extra: examples
13
+ Requires-Dist: matplotlib>=3.9; extra == 'examples'
14
+ Requires-Dist: qiskit-aer>=0.17.2; extra == 'examples'
15
+ Requires-Dist: qiskit-algorithms>=0.4.0; extra == 'examples'
16
+ Requires-Dist: qiskit-machine-learning>=0.9.0; extra == 'examples'
17
+ Requires-Dist: qiskit>=2.5.0; extra == 'examples'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # mlx-quantum
21
+
22
+ [![CI](https://github.com/UmarGit/mlx_quantum/actions/workflows/ci.yml/badge.svg)](https://github.com/UmarGit/mlx_quantum/actions/workflows/ci.yml)
23
+
24
+ Fast, differentiable quantum machine learning in pure [Apple MLX](https://github.com/ml-explore/mlx).
25
+
26
+ Statevector simulation runs on the Metal GPU and is differentiable end-to-end
27
+ through MLX autodiff — so a quantum layer trains like any other `mlx.nn` module,
28
+ with no custom gradient code. Forward values **and** gradients match Qiskit to
29
+ ~1e-6 (float32), and it is **~100–400× faster** end-to-end than driving Qiskit's
30
+ `EstimatorQNN` from Python. See [Validation](#validation) for the evidence.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ uv add mlx-quantum
36
+ # or
37
+ pip install mlx-quantum
38
+ ```
39
+
40
+ Requires Python ≥ 3.13 and Apple Silicon. The library depends only on MLX and
41
+ NumPy; Qiskit is optional and used solely to cross-validate/benchmark.
42
+
43
+ ## Quickstart
44
+
45
+ `QuantumLayer` is a trainable `mlx.nn.Module`. Drop it into a model and train:
46
+
47
+ ```python
48
+ import mlx.core as mx
49
+ import mlx.nn as nn
50
+ from mlx_quantum import QuantumLayer
51
+
52
+ class HybridMLP(nn.Module):
53
+ def __init__(self):
54
+ super().__init__()
55
+ self.pre = nn.Linear(8, 4)
56
+ self.qnn = QuantumLayer(num_qubits=4, reps=2) # trainable quantum layer
57
+ self.post = nn.Linear(4, 3)
58
+
59
+ def __call__(self, x):
60
+ x = mx.tanh(self.pre(x)) * mx.pi # encode into rotation angles
61
+ return self.post(self.qnn(x))
62
+ ```
63
+
64
+ The quantum weights are ordinary MLX parameters — `nn.value_and_grad` and any
65
+ optimizer update them automatically:
66
+
67
+ ```python
68
+ loss_and_grad = nn.value_and_grad(model, loss_fn)
69
+ loss, grads = loss_and_grad(model, x, y)
70
+ optimizer.update(model, grads)
71
+ ```
72
+
73
+ ## Building custom circuits
74
+
75
+ `QuantumLayer` runs a hardware-efficient ansatz, but the simulator primitives are
76
+ public — build any circuit as a plain differentiable function:
77
+
78
+ ```python
79
+ import mlx.core as mx
80
+ from mlx_quantum import zero_state, apply_1q, apply_2q, expval_all_z, H, ry, CX
81
+
82
+ def circuit(x, weights): # x: (batch, n) angles, weights: (n,)
83
+ n = x.shape[1]
84
+ state = zero_state(x.shape[0], n)
85
+ for q in range(n):
86
+ state = apply_1q(state, H, q)
87
+ for q in range(n):
88
+ state = apply_1q(state, ry(x[:, q]), q) # per-sample encoding
89
+ for q in range(n):
90
+ state = apply_1q(state, ry(weights[q]), q) # trainable
91
+ for q in range(n - 1):
92
+ state = apply_2q(state, CX, q, q + 1)
93
+ return expval_all_z(state) # <Z> per qubit, shape (batch, n)
94
+
95
+ grads = mx.grad(lambda w: mx.sum(circuit(x, w)))(weights) # just works
96
+ ```
97
+
98
+ Gates provided: `H, X, Y, Z, rx, ry, rz, CX, CZ`. Add your own — a single-qubit
99
+ gate is any `(2, 2)` complex `mx.array`; a two-qubit gate is a `(2, 2, 2, 2)`
100
+ tensor `[out0, out1, in0, in1]`.
101
+
102
+ ## How it works
103
+
104
+ A statevector is a complex `mx.array` of shape `(batch,) + (2,) * num_qubits`;
105
+ qubit ordering is little-endian to match Qiskit exactly (qubit 0 is the fastest
106
+ index, so flattening reproduces Qiskit's amplitude order). Gates are
107
+ applied as `einsum` contractions, so the entire simulation is differentiable and
108
+ GPU-resident. Because there is no custom `vjp` and no NumPy round-trip, `mx.grad`
109
+ differentiates the circuit directly — including through complex amplitudes.
110
+
111
+ Two MLX specifics the implementation works around: the initial state is built as a
112
+ constant (not an in-place assignment, which compiles to an unsupported complex
113
+ GPU scatter), and gates are contractions rather than `take`/gather (whose backward
114
+ is also a scatter).
115
+
116
+ ## Examples
117
+
118
+ ```bash
119
+ uv run python examples/simple_mlp.py # hybrid MLP training
120
+ uv run --extra examples python examples/benchmark_vs_qiskit.py # quick speed + accuracy vs Qiskit
121
+ ```
122
+
123
+ ## Validation
124
+
125
+ Two tracks — is it correct, and is the speed claim fair? All measurements are
126
+ **noiseless statevector, float32**. Regenerate with
127
+ `uv run --extra examples python benchmarks/validate.py` (details in
128
+ [`benchmarks/`](benchmarks/)).
129
+
130
+ **Correctness.** Forward values and gradients are compared against Qiskit
131
+ (`Statevector` and `ReverseEstimatorGradient`) over 142 random circuits covering
132
+ every gate (`H, X, Y, Z, rx, ry, rz, CX, CZ`); per-circuit error stays at ~1e-6.
133
+ (The *batch-summed* gradient error on the accuracy plot climbs to ~1e-5 by 8
134
+ qubits — that is float32 accumulation from summing 128 terms into one number,
135
+ still ≥5 significant figures, not a modelling error.) Gates are checked for
136
+ unitarity, the state norm is checked after every layer, and an asymmetric circuit
137
+ pins the little-endian qubit order to Qiskit's.
138
+
139
+ | | ![accuracy](benchmarks/accuracy_vs_qubits.png) | ![error distribution](benchmarks/error_distribution.png) |
140
+ |---|---|---|
141
+
142
+ **Performance.** Two honest baselines. End-to-end vs `EstimatorQNN` driven from
143
+ Python (~100–400×), and kernel-level vs Aer's compiled statevector estimator
144
+ (~1.7–3×, forward only) — so the win is not just deleted orchestration. Wall-time
145
+ is shown until MLX hits the memory cliff (~22–26 qubits, single statevector).
146
+
147
+ | | ![speedup](benchmarks/speedup_vs_qubits.png) | ![wall time](benchmarks/walltime_vs_qubits.png) |
148
+ |---|---|---|
149
+
150
+ **Trains identically.** Same circuit, init, data, and optimizer (SGD): the MLX
151
+ layer (`mx.grad`) and the Qiskit QNN (`qnn.backward`) produce the same loss curve
152
+ to ~1e-7 — same training, just faster.
153
+
154
+ ![training overlay](benchmarks/training_overlay.png)
155
+
156
+ ## Tests
157
+
158
+ ```bash
159
+ uv run pytest
160
+ ```
161
+
162
+ Covers gate/statevector correctness, gate unitarity and norm preservation, the
163
+ little-endian convention, layer training, a finite-difference gradient check, and
164
+ forward + weight-gradient + input-gradient parity with Qiskit across a random gate
165
+ sweep (Qiskit-dependent tests skip automatically if Qiskit is absent).
166
+
167
+ ## Changelog
168
+
169
+ See [CHANGELOG.md](CHANGELOG.md).
170
+
171
+ ## License
172
+
173
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ mlx_quantum/__init__.py,sha256=7qjKB4AZ1N3xiUX43BN4iilxspCnPB46KJnJBEDTs6A,479
2
+ mlx_quantum/layer.py,sha256=eSdMsA3vJNDAHTww4AP1yztWPJX7eTS67EA6Q7Fvu9I,1767
3
+ mlx_quantum/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ mlx_quantum/statevector.py,sha256=-4zG5KVPFXku6hO0iofgOP-0ULsjgJoVhpCKsS7PGuw,3743
5
+ mlx_quantum-0.1.0.dist-info/METADATA,sha256=2bcRDqPEtIu9GIEz5Krt069FYxNO43HFeEn5HVIQtiw,6701
6
+ mlx_quantum-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ mlx_quantum-0.1.0.dist-info/licenses/LICENSE,sha256=CthJDHR37kFu4ndMrsrTTwdJaBqDDyB-OyIn4AteLxM,1067
8
+ mlx_quantum-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Umar Ahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.