pdft 0.2.1__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.
- pdft/__init__.py +86 -0
- pdft/bases/__init__.py +40 -0
- pdft/bases/base.py +470 -0
- pdft/bases/block/__init__.py +12 -0
- pdft/bases/block/block.py +261 -0
- pdft/bases/block/real_rich.py +164 -0
- pdft/bases/block/rich.py +265 -0
- pdft/bases/circuit/__init__.py +15 -0
- pdft/bases/circuit/entangled_qft.py +137 -0
- pdft/bases/circuit/mera.py +167 -0
- pdft/bases/circuit/qft.py +84 -0
- pdft/bases/circuit/tebd.py +130 -0
- pdft/circuit/__init__.py +34 -0
- pdft/circuit/builder.py +242 -0
- pdft/circuit/cache.py +52 -0
- pdft/io/__init__.py +39 -0
- pdft/io/compression.py +189 -0
- pdft/io/serialize.py +221 -0
- pdft/loss.py +159 -0
- pdft/manifolds.py +350 -0
- pdft/optimizers/__init__.py +19 -0
- pdft/optimizers/adam.py +86 -0
- pdft/optimizers/core.py +61 -0
- pdft/optimizers/gd.py +68 -0
- pdft/optimizers/loop.py +104 -0
- pdft/profiling.py +292 -0
- pdft/training/__init__.py +19 -0
- pdft/training/adam_step.py +131 -0
- pdft/training/batched.py +341 -0
- pdft/training/eval_loop.py +57 -0
- pdft/training/result.py +17 -0
- pdft/training/schedules.py +27 -0
- pdft/training/single.py +69 -0
- pdft/viz/__init__.py +24 -0
- pdft/viz/circuit.py +85 -0
- pdft/viz/loss.py +119 -0
- pdft-0.2.1.dist-info/METADATA +100 -0
- pdft-0.2.1.dist-info/RECORD +40 -0
- pdft-0.2.1.dist-info/WHEEL +4 -0
- pdft-0.2.1.dist-info/licenses/LICENSE +25 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""BlockedBasis: apply any inner parametric basis independently to each block.
|
|
2
|
+
|
|
3
|
+
Motivation: the paper's central finding is that **block size dominates the
|
|
4
|
+
within-block basis** — BlockDCT 8x8 beats every full-image transform tested.
|
|
5
|
+
The full-image learned circuits (QFT/EntangledQFT/TEBD/MERA) are competitive
|
|
6
|
+
among full-image bases but lose to BlockDCT 8x8 by ~3 dB.
|
|
7
|
+
|
|
8
|
+
`BlockedBasis` directly responds to that finding. It wraps an inner basis at
|
|
9
|
+
smaller m_inner, n_inner and applies it independently to each
|
|
10
|
+
(2^m_inner, 2^n_inner) block of a larger (2^m_outer, 2^n_outer) image.
|
|
11
|
+
Parameters are SHARED across blocks (one basis tiled over many blocks).
|
|
12
|
+
|
|
13
|
+
Concretely: with m_outer = m_inner + block_log_m and n_outer = n_inner +
|
|
14
|
+
block_log_n, the image is conceptually decomposed into a grid of
|
|
15
|
+
2^block_log_m * 2^block_log_n independent blocks, each transformed by the
|
|
16
|
+
inner basis.
|
|
17
|
+
|
|
18
|
+
Implementation strategy: BlockedBasis exposes ``m, n, tensors, code, inv_code``
|
|
19
|
+
just like any other basis, so the existing training pipeline
|
|
20
|
+
(``train_basis_batched``, ``loss_function``, ``_build_jit_adam_step``) works
|
|
21
|
+
unchanged. The trick is in ``code``/``inv_code``: they are closures that
|
|
22
|
+
permute axes, vmap the inner code over the block-index dims, and permute back.
|
|
23
|
+
|
|
24
|
+
Yao little-endian convention preserved: block-index qubits are the
|
|
25
|
+
HIGHER-numbered qubits per dimension (qubits m_inner+1..m_outer for rows),
|
|
26
|
+
which correspond to LOW-INDEXED axes [0..block_log_m) in the (2,)^(m+n)
|
|
27
|
+
tensor layout. See CLAUDE.md §2.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import jax
|
|
37
|
+
import jax.numpy as jnp
|
|
38
|
+
from jax import tree_util
|
|
39
|
+
|
|
40
|
+
Array = jax.Array
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Block-aware einsum closure
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_block_code(
|
|
49
|
+
inner_code: Callable[..., Array],
|
|
50
|
+
*,
|
|
51
|
+
m_inner: int,
|
|
52
|
+
n_inner: int,
|
|
53
|
+
block_log_m: int,
|
|
54
|
+
block_log_n: int,
|
|
55
|
+
) -> Callable[..., Array]:
|
|
56
|
+
"""Return a callable with the same signature as ``inner_code`` that operates
|
|
57
|
+
on a (2,)^(m_outer+n_outer) image by vmap-ing ``inner_code`` over the
|
|
58
|
+
block-index axes.
|
|
59
|
+
|
|
60
|
+
Outer axis layout (Yao little-endian, see _circuit.build_circuit_einsum):
|
|
61
|
+
[0..block_log_m) -- block-index ROW qubits (msbs)
|
|
62
|
+
[block_log_m..m_outer) -- within-block ROW qubits (lsbs, m_inner of them)
|
|
63
|
+
[m_outer..m_outer+block_log_n) -- block-index COL qubits (msbs)
|
|
64
|
+
[m_outer+block_log_n..) -- within-block COL qubits (lsbs, n_inner of them)
|
|
65
|
+
"""
|
|
66
|
+
m_outer = m_inner + block_log_m
|
|
67
|
+
n_outer = n_inner + block_log_n
|
|
68
|
+
n_blocks = 2 ** (block_log_m + block_log_n)
|
|
69
|
+
inner_shape = (2,) * (m_inner + n_inner)
|
|
70
|
+
outer_shape = (2,) * (m_outer + n_outer)
|
|
71
|
+
|
|
72
|
+
# Permutation: block-index axes (rows then cols) to the front, within-block
|
|
73
|
+
# axes (rows then cols) trailing. The trailing layout matches the inner
|
|
74
|
+
# einsum's expected (2,)^(m_inner+n_inner) shape.
|
|
75
|
+
perm = (
|
|
76
|
+
list(range(0, block_log_m)) # block row axes
|
|
77
|
+
+ list(range(m_outer, m_outer + block_log_n)) # block col axes
|
|
78
|
+
+ list(range(block_log_m, m_outer)) # within row axes
|
|
79
|
+
+ list(range(m_outer + block_log_n, m_outer + n_outer)) # within col axes
|
|
80
|
+
)
|
|
81
|
+
inv_perm = [0] * len(perm)
|
|
82
|
+
for i, p in enumerate(perm):
|
|
83
|
+
inv_perm[p] = i
|
|
84
|
+
|
|
85
|
+
leading_block_shape = (2,) * (block_log_m + block_log_n)
|
|
86
|
+
|
|
87
|
+
def block_code(*args: Any) -> Array:
|
|
88
|
+
*tensors, image = args
|
|
89
|
+
if image.shape != outer_shape:
|
|
90
|
+
raise ValueError(f"BlockedBasis expected image shape {outer_shape}, got {image.shape}")
|
|
91
|
+
x = jnp.transpose(image, perm)
|
|
92
|
+
x_flat = x.reshape((n_blocks,) + inner_shape)
|
|
93
|
+
|
|
94
|
+
def apply_one(img_one: Array) -> Array:
|
|
95
|
+
return inner_code(*tensors, img_one)
|
|
96
|
+
|
|
97
|
+
out_flat = jax.vmap(apply_one)(x_flat)
|
|
98
|
+
out = out_flat.reshape(leading_block_shape + inner_shape)
|
|
99
|
+
return jnp.transpose(out, inv_perm)
|
|
100
|
+
|
|
101
|
+
return block_code
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# BlockedBasis
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class BlockedBasis:
|
|
111
|
+
"""Wraps an inner parametric basis as a within-block transform.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
inner : any pdft basis (QFTBasis, EntangledQFTBasis, TEBDBasis, MERABasis)
|
|
116
|
+
Within-block parametric circuit at smaller m_inner = inner.m,
|
|
117
|
+
n_inner = inner.n.
|
|
118
|
+
block_log_m, block_log_n : int
|
|
119
|
+
Number of block-index qubits per dimension. The image is
|
|
120
|
+
(2^(inner.m + block_log_m), 2^(inner.n + block_log_n)).
|
|
121
|
+
block_log_m=0 (and =0) reduces to the inner basis.
|
|
122
|
+
|
|
123
|
+
Notes
|
|
124
|
+
-----
|
|
125
|
+
All learnable parameters live in ``inner.tensors``; ``BlockedBasis`` is
|
|
126
|
+
a pure structural wrapper. Block parameters are SHARED across blocks
|
|
127
|
+
(one inner basis tiled).
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
inner: Any
|
|
131
|
+
block_log_m: int
|
|
132
|
+
block_log_n: int
|
|
133
|
+
code: object = field(compare=False, repr=False)
|
|
134
|
+
inv_code: object = field(compare=False, repr=False)
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
inner: Any,
|
|
139
|
+
block_log_m: int,
|
|
140
|
+
block_log_n: int,
|
|
141
|
+
code: object | None = None,
|
|
142
|
+
inv_code: object | None = None,
|
|
143
|
+
):
|
|
144
|
+
if not hasattr(inner, "m") or not hasattr(inner, "n"):
|
|
145
|
+
raise TypeError(
|
|
146
|
+
f"inner must be a pdft basis with m, n attributes; got {type(inner).__name__}"
|
|
147
|
+
)
|
|
148
|
+
if block_log_m < 0 or block_log_n < 0:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"block_log_m and block_log_n must be >= 0; got "
|
|
151
|
+
f"block_log_m={block_log_m}, block_log_n={block_log_n}"
|
|
152
|
+
)
|
|
153
|
+
self.inner = inner
|
|
154
|
+
self.block_log_m = block_log_m
|
|
155
|
+
self.block_log_n = block_log_n
|
|
156
|
+
if code is None or inv_code is None:
|
|
157
|
+
built_code = _make_block_code(
|
|
158
|
+
inner.code,
|
|
159
|
+
m_inner=inner.m,
|
|
160
|
+
n_inner=inner.n,
|
|
161
|
+
block_log_m=block_log_m,
|
|
162
|
+
block_log_n=block_log_n,
|
|
163
|
+
)
|
|
164
|
+
built_inv = _make_block_code(
|
|
165
|
+
inner.inv_code,
|
|
166
|
+
m_inner=inner.m,
|
|
167
|
+
n_inner=inner.n,
|
|
168
|
+
block_log_m=block_log_m,
|
|
169
|
+
block_log_n=block_log_n,
|
|
170
|
+
)
|
|
171
|
+
self.code = code if code is not None else built_code
|
|
172
|
+
self.inv_code = inv_code if inv_code is not None else built_inv
|
|
173
|
+
else:
|
|
174
|
+
self.code = code
|
|
175
|
+
self.inv_code = inv_code
|
|
176
|
+
|
|
177
|
+
# ---- AbstractSparseBasis interface (matches QFTBasis) ----
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def m(self) -> int:
|
|
181
|
+
return self.inner.m + self.block_log_m
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def n(self) -> int:
|
|
185
|
+
return self.inner.n + self.block_log_n
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def tensors(self) -> list[Array]:
|
|
189
|
+
"""Forward to inner — pytree leaves order is `inner.tensors`."""
|
|
190
|
+
return self.inner.tensors
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def inv_tensors(self) -> list[Array]:
|
|
194
|
+
return self.inner.tensors
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def image_size(self) -> tuple[int, int]:
|
|
198
|
+
return (2**self.m, 2**self.n)
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def num_parameters(self) -> int:
|
|
202
|
+
return self.inner.num_parameters
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def num_blocks(self) -> int:
|
|
206
|
+
return 2 ** (self.block_log_m + self.block_log_n)
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def block_shape(self) -> tuple[int, int]:
|
|
210
|
+
return (2**self.inner.m, 2**self.inner.n)
|
|
211
|
+
|
|
212
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
213
|
+
from ...loss import _apply_circuit
|
|
214
|
+
|
|
215
|
+
return _apply_circuit(self.tensors, self.code, self.m, self.n, pic)
|
|
216
|
+
|
|
217
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
218
|
+
from ...loss import _apply_circuit
|
|
219
|
+
|
|
220
|
+
return _apply_circuit(
|
|
221
|
+
[jnp.conj(t) for t in self.tensors],
|
|
222
|
+
self.inv_code,
|
|
223
|
+
self.m,
|
|
224
|
+
self.n,
|
|
225
|
+
pic,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# JAX pytree registration
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _blockedbasis_flatten(b: BlockedBasis):
|
|
235
|
+
"""Flatten by recursing into inner via JAX's pytree machinery.
|
|
236
|
+
|
|
237
|
+
This delegates inner reconstruction to JAX's tree_unflatten, which
|
|
238
|
+
works for ANY pytree-registered inner — including nested wrappers
|
|
239
|
+
like StackedBasis whose constructor doesn't take (m, n, tensors, ...).
|
|
240
|
+
"""
|
|
241
|
+
inner_leaves, inner_treedef = jax.tree_util.tree_flatten(b.inner)
|
|
242
|
+
aux = (inner_treedef, b.block_log_m, b.block_log_n, b.code, b.inv_code)
|
|
243
|
+
return tuple(inner_leaves), aux
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _blockedbasis_unflatten(aux, leaves) -> BlockedBasis:
|
|
247
|
+
inner_treedef, block_log_m, block_log_n, code, inv_code = aux
|
|
248
|
+
inner = jax.tree_util.tree_unflatten(inner_treedef, list(leaves))
|
|
249
|
+
return BlockedBasis(
|
|
250
|
+
inner=inner,
|
|
251
|
+
block_log_m=block_log_m,
|
|
252
|
+
block_log_n=block_log_n,
|
|
253
|
+
code=code,
|
|
254
|
+
inv_code=inv_code,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
tree_util.register_pytree_node(BlockedBasis, _blockedbasis_flatten, _blockedbasis_unflatten)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = ["BlockedBasis"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""RealRichBasis (Approach A): real-orthogonal restriction of RichBasis.
|
|
2
|
+
|
|
3
|
+
Motivation: RichBasis (54 free real params per dim, complex U(4) gates) is
|
|
4
|
+
a strict 54-dim submanifold of SU(8) — does not contain DCT, can only
|
|
5
|
+
approximate. But fully complex U(4) is also wasteful for the natural-image
|
|
6
|
+
problem: DCT is real-valued (lives in O(8)), so the IMAGINARY parts of
|
|
7
|
+
RichBasis's parameters are doing no useful work for natural images.
|
|
8
|
+
|
|
9
|
+
RealRichBasis keeps the same QFT topology and gate count but constrains
|
|
10
|
+
each tensor to be REAL-valued:
|
|
11
|
+
- 3 H gates per dim → 2×2 real-orthogonal matrices (init: Hadamard).
|
|
12
|
+
Free params per gate: 1 (rotation angle of the connected component
|
|
13
|
+
of Hadamard in O(2)).
|
|
14
|
+
- 3 "U(4)" gates per dim → real 4×4 orthogonal matrices, 6 free real
|
|
15
|
+
params each (init: identity).
|
|
16
|
+
|
|
17
|
+
Total per dim: 3·1 + 3·6 = 21 free real params (BELOW dim O(8) = 28).
|
|
18
|
+
Strict submanifold of O(8); whether DCT is in this family is empirical.
|
|
19
|
+
|
|
20
|
+
Storage: tensors are stored as complex128 with all-zero imaginary parts.
|
|
21
|
+
Cayley retraction on the real-image gradient preserves real-ness
|
|
22
|
+
automatically — no manifold change needed; the existing UnitaryManifold
|
|
23
|
+
trains the orthogonal subset correctly when initialised with real values
|
|
24
|
+
on a real-valued objective.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections.abc import Sequence
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
|
|
32
|
+
import jax
|
|
33
|
+
import jax.numpy as jnp
|
|
34
|
+
import numpy as np
|
|
35
|
+
from jax import tree_util
|
|
36
|
+
|
|
37
|
+
from ...circuit.builder import HADAMARD, Gate, compile_circuit
|
|
38
|
+
|
|
39
|
+
Array = jax.Array
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _real_eye_u4() -> Array:
|
|
43
|
+
"""4x4 identity reshaped to (2, 2, 2, 2), as real complex128."""
|
|
44
|
+
return jnp.asarray(np.eye(4).reshape(2, 2, 2, 2), dtype=jnp.complex128)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _real_rich_qft_gates_1d(n_qubits: int, offset: int) -> list[Gate]:
|
|
48
|
+
"""QFT topology with H + REAL-orthogonal 2-qubit gates.
|
|
49
|
+
|
|
50
|
+
H slots are the canonical Hadamard. 2-qubit slots are the 4×4 identity
|
|
51
|
+
(a real-orthogonal matrix). Both are within the connected component of
|
|
52
|
+
O(d) reachable via Cayley retraction with real updates.
|
|
53
|
+
"""
|
|
54
|
+
eye_u4 = _real_eye_u4()
|
|
55
|
+
gates: list[Gate] = []
|
|
56
|
+
for j in range(1, n_qubits + 1):
|
|
57
|
+
q = offset + j
|
|
58
|
+
gates.append(Gate(kind="H", qubits=(q,), tensor=HADAMARD, phase=0.0))
|
|
59
|
+
for target in range(j + 1, n_qubits + 1):
|
|
60
|
+
t = offset + target
|
|
61
|
+
gates.append(
|
|
62
|
+
Gate(
|
|
63
|
+
kind="U4",
|
|
64
|
+
qubits=(t, q),
|
|
65
|
+
tensor=eye_u4,
|
|
66
|
+
phase=0.0,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
return gates
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _real_rich_code(m: int, n: int, *, inverse: bool):
|
|
73
|
+
if m < 1 or n < 1:
|
|
74
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
75
|
+
gates = _real_rich_qft_gates_1d(m, offset=0) + _real_rich_qft_gates_1d(n, offset=m)
|
|
76
|
+
return compile_circuit(gates, m, n, inverse=inverse)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class RealRichBasis:
|
|
81
|
+
"""QFT topology with H + real-orthogonal 2-qubit gates.
|
|
82
|
+
|
|
83
|
+
The U(4) slots are *initialised* to the 4×4 identity (real-orthogonal,
|
|
84
|
+
not the complex controlled-phase) so the basis is NOT bit-identical
|
|
85
|
+
to QFTBasis at training step 0 — the forward circuit at init is
|
|
86
|
+
H ⊗ H ⊗ H per dim followed by identity 2-qubit ops, i.e. just the
|
|
87
|
+
Walsh-Hadamard transform. This is the appropriate starting point for
|
|
88
|
+
a real-valued search; the Walsh-Hadamard is the simplest real-orthogonal
|
|
89
|
+
basis and a natural baseline for natural-image transforms.
|
|
90
|
+
|
|
91
|
+
Pytree contract:
|
|
92
|
+
leaves = tensors (one list)
|
|
93
|
+
aux data = (m, n, len(tensors), code, inv_code)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
m: int
|
|
97
|
+
n: int
|
|
98
|
+
tensors: list[Array]
|
|
99
|
+
code: object = field(compare=False, repr=False)
|
|
100
|
+
inv_code: object = field(compare=False, repr=False)
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
m: int,
|
|
105
|
+
n: int,
|
|
106
|
+
tensors: Sequence[Array] | None = None,
|
|
107
|
+
code: object | None = None,
|
|
108
|
+
inv_code: object | None = None,
|
|
109
|
+
):
|
|
110
|
+
if m < 1 or n < 1:
|
|
111
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
112
|
+
self.m = m
|
|
113
|
+
self.n = n
|
|
114
|
+
_code, init_tensors = _real_rich_code(m, n, inverse=False)
|
|
115
|
+
_inv_code, _ = _real_rich_code(m, n, inverse=True)
|
|
116
|
+
self.tensors = list(tensors) if tensors is not None else init_tensors
|
|
117
|
+
self.code = code if code is not None else _code
|
|
118
|
+
self.inv_code = inv_code if inv_code is not None else _inv_code
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def inv_tensors(self) -> list[Array]:
|
|
122
|
+
return self.tensors
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def image_size(self) -> tuple[int, int]:
|
|
126
|
+
return (2**self.m, 2**self.n)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def num_parameters(self) -> int:
|
|
130
|
+
return sum(int(t.size) for t in self.tensors)
|
|
131
|
+
|
|
132
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
133
|
+
from ...loss import _apply_circuit
|
|
134
|
+
|
|
135
|
+
return _apply_circuit(self.tensors, self.code, self.m, self.n, pic)
|
|
136
|
+
|
|
137
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
138
|
+
from ...loss import _apply_circuit
|
|
139
|
+
|
|
140
|
+
return _apply_circuit(
|
|
141
|
+
[jnp.conj(t) for t in self.tensors],
|
|
142
|
+
self.inv_code,
|
|
143
|
+
self.m,
|
|
144
|
+
self.n,
|
|
145
|
+
pic,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _realrichbasis_flatten(b: RealRichBasis):
|
|
150
|
+
leaves = tuple(b.tensors)
|
|
151
|
+
aux = (b.m, b.n, len(b.tensors), b.code, b.inv_code)
|
|
152
|
+
return leaves, aux
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _realrichbasis_unflatten(aux, leaves) -> RealRichBasis:
|
|
156
|
+
m, n, n_fwd, code, inv_code = aux
|
|
157
|
+
assert len(leaves) == n_fwd
|
|
158
|
+
return RealRichBasis(m=m, n=n, tensors=list(leaves), code=code, inv_code=inv_code)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
tree_util.register_pytree_node(RealRichBasis, _realrichbasis_flatten, _realrichbasis_unflatten)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = ["RealRichBasis"]
|
pdft/bases/block/rich.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""RichBasis: single-layer parametric circuit with full 2-qubit unitary gates.
|
|
2
|
+
|
|
3
|
+
Motivation: at small block sizes (m=n=3 = 8x8) the existing H+CP gate
|
|
4
|
+
family hits an expressivity ceiling — all topologies converge ~1.75 dB
|
|
5
|
+
below 8x8 DCT. The cause is that diagonal CP gates have only 1 free
|
|
6
|
+
parameter each. A general 2-qubit unitary (U(4)) has 15 free parameters,
|
|
7
|
+
and the H + U(4) gate family is provably universal for SU(2^n) at any
|
|
8
|
+
qubit count >= 2, so it CONTAINS DCT as a special case.
|
|
9
|
+
|
|
10
|
+
RichBasis emits the same QFT topology gate sequence (H per qubit + 2-qubit
|
|
11
|
+
gates between qubit pairs) but each 2-qubit gate is a learnable U(4)
|
|
12
|
+
instead of a 1-parameter CP. Initialised so the circuit is BIT-IDENTICAL
|
|
13
|
+
to QFT at training step 0 (each U(4) gate equals the 4×4 controlled-phase
|
|
14
|
+
diag(1, 1, 1, exp(iφ)) at its standard QFT phase). This gives Adam a
|
|
15
|
+
gentle starting point: the optimiser begins exactly where plain QFT does
|
|
16
|
+
and can only improve.
|
|
17
|
+
|
|
18
|
+
Parameter count at m=n=3 (8x8 block):
|
|
19
|
+
- 6 H gates (3 per dim) at 4 real params each = 24
|
|
20
|
+
- 6 U(4) gates (3 per dim) at 15 real params each = 90
|
|
21
|
+
- total: 114 real params per dim
|
|
22
|
+
vs SU(8) dimension = 63 free real params
|
|
23
|
+
→ strictly more parameters than needed for any 8x8 unitary.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
|
|
31
|
+
import jax
|
|
32
|
+
import jax.numpy as jnp
|
|
33
|
+
from jax import tree_util
|
|
34
|
+
|
|
35
|
+
from ...circuit.builder import HADAMARD, Gate, compile_circuit, u4_from_phase
|
|
36
|
+
|
|
37
|
+
Array = jax.Array
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _rich_qft_gates_1d(n_qubits: int, offset: int) -> list[Gate]:
|
|
41
|
+
"""Same QFT topology as qft._qft_gates_1d, but with U(4) gates instead of CP.
|
|
42
|
+
|
|
43
|
+
Each U(4) gate is initialised to the 4x4 unitary equivalent of the
|
|
44
|
+
standard QFT phase (so the basis is bit-identical to QFTBasis at init).
|
|
45
|
+
"""
|
|
46
|
+
gates: list[Gate] = []
|
|
47
|
+
for j in range(1, n_qubits + 1):
|
|
48
|
+
q = offset + j
|
|
49
|
+
gates.append(Gate(kind="H", qubits=(q,), tensor=HADAMARD, phase=0.0))
|
|
50
|
+
for target in range(j + 1, n_qubits + 1):
|
|
51
|
+
k = target - j + 1
|
|
52
|
+
t = offset + target
|
|
53
|
+
phi = float(2 * jnp.pi / (2**k))
|
|
54
|
+
gates.append(
|
|
55
|
+
Gate(
|
|
56
|
+
kind="U4",
|
|
57
|
+
qubits=(t, q), # control, target
|
|
58
|
+
tensor=u4_from_phase(phi),
|
|
59
|
+
phase=phi,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return gates
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _rich_code(m: int, n: int, *, inverse: bool):
|
|
66
|
+
"""Build the rich (H + U(4)) circuit einsum + initial tensor list."""
|
|
67
|
+
if m < 1 or n < 1:
|
|
68
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
69
|
+
gates = _rich_qft_gates_1d(m, offset=0) + _rich_qft_gates_1d(n, offset=m)
|
|
70
|
+
return compile_circuit(gates, m, n, inverse=inverse)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class RichBasis:
|
|
75
|
+
"""QFT topology with H + learnable U(4) gates instead of H + CP.
|
|
76
|
+
|
|
77
|
+
Parameter count at m=n=3 (within an 8x8 block):
|
|
78
|
+
- 3 H gates per dim × 3 free real params (SU(2)) = 9
|
|
79
|
+
- 3 U(4) gates per dim × 15 free real params (SU(4)) = 45
|
|
80
|
+
- total per dim: 54 (BELOW the 63-dim of SU(8) — meaningful structure)
|
|
81
|
+
|
|
82
|
+
Initialised so the circuit is BIT-IDENTICAL to QFTBasis at training step 0
|
|
83
|
+
(each U(4) starts at the 4×4 controlled-phase diag(1, 1, 1, exp(iφ)) of
|
|
84
|
+
its corresponding QFT slot). Adam can then deform the U(4) gates into any
|
|
85
|
+
4×4 unitary, but the family is a strict 54-dim submanifold of SU(8) and
|
|
86
|
+
does NOT contain 8×8 DCT exactly (empirically: fit_to_dct plateaus at
|
|
87
|
+
Frobenius² ≈ 63.7).
|
|
88
|
+
|
|
89
|
+
Pytree contract:
|
|
90
|
+
leaves = tensors (one list)
|
|
91
|
+
aux data = (m, n, len(tensors), code, inv_code)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
m: int
|
|
95
|
+
n: int
|
|
96
|
+
tensors: list[Array]
|
|
97
|
+
code: object = field(compare=False, repr=False)
|
|
98
|
+
inv_code: object = field(compare=False, repr=False)
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
m: int,
|
|
103
|
+
n: int,
|
|
104
|
+
tensors: Sequence[Array] | None = None,
|
|
105
|
+
code: object | None = None,
|
|
106
|
+
inv_code: object | None = None,
|
|
107
|
+
):
|
|
108
|
+
if m < 1 or n < 1:
|
|
109
|
+
raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
|
|
110
|
+
self.m = m
|
|
111
|
+
self.n = n
|
|
112
|
+
_code, init_tensors = _rich_code(m, n, inverse=False)
|
|
113
|
+
_inv_code, _ = _rich_code(m, n, inverse=True)
|
|
114
|
+
self.tensors = list(tensors) if tensors is not None else init_tensors
|
|
115
|
+
self.code = code if code is not None else _code
|
|
116
|
+
self.inv_code = inv_code if inv_code is not None else _inv_code
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def inv_tensors(self) -> list[Array]:
|
|
120
|
+
return self.tensors
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def image_size(self) -> tuple[int, int]:
|
|
124
|
+
return (2**self.m, 2**self.n)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def num_parameters(self) -> int:
|
|
128
|
+
return sum(int(t.size) for t in self.tensors)
|
|
129
|
+
|
|
130
|
+
def forward_transform(self, pic: Array) -> Array:
|
|
131
|
+
from ...loss import _apply_circuit
|
|
132
|
+
|
|
133
|
+
return _apply_circuit(self.tensors, self.code, self.m, self.n, pic)
|
|
134
|
+
|
|
135
|
+
def inverse_transform(self, pic: Array) -> Array:
|
|
136
|
+
from ...loss import _apply_circuit
|
|
137
|
+
|
|
138
|
+
return _apply_circuit(
|
|
139
|
+
[jnp.conj(t) for t in self.tensors],
|
|
140
|
+
self.inv_code,
|
|
141
|
+
self.m,
|
|
142
|
+
self.n,
|
|
143
|
+
pic,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _richbasis_flatten(b: RichBasis):
|
|
148
|
+
leaves = tuple(b.tensors)
|
|
149
|
+
aux = (b.m, b.n, len(b.tensors), b.code, b.inv_code)
|
|
150
|
+
return leaves, aux
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _richbasis_unflatten(aux, leaves) -> RichBasis:
|
|
154
|
+
m, n, n_fwd, code, inv_code = aux
|
|
155
|
+
assert len(leaves) == n_fwd
|
|
156
|
+
return RichBasis(m=m, n=n, tensors=list(leaves), code=code, inv_code=inv_code)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
tree_util.register_pytree_node(RichBasis, _richbasis_flatten, _richbasis_unflatten)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _dct_matrix(n: int) -> Array:
|
|
163
|
+
"""Orthonormal 1D DCT-II matrix of size n × n."""
|
|
164
|
+
import numpy as _np
|
|
165
|
+
|
|
166
|
+
k = _np.arange(n).reshape(-1, 1)
|
|
167
|
+
j = _np.arange(n)
|
|
168
|
+
M = _np.cos(_np.pi * (2 * j + 1) * k / (2 * n))
|
|
169
|
+
M[0, :] *= 1.0 / _np.sqrt(n)
|
|
170
|
+
M[1:, :] *= _np.sqrt(2.0 / n)
|
|
171
|
+
return jnp.asarray(M, dtype=jnp.complex128)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def fit_to_dct(
|
|
175
|
+
basis_factory,
|
|
176
|
+
*,
|
|
177
|
+
n_steps: int = 2000,
|
|
178
|
+
lr: float = 0.02,
|
|
179
|
+
) -> list[Array]:
|
|
180
|
+
"""Fit a parametric basis so its forward circuit ≈ DCT_2D.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
basis_factory : callable
|
|
185
|
+
Zero-argument callable returning a basis instance. Must expose
|
|
186
|
+
``.m, .n, .tensors, .code`` (any pdft basis class). The returned
|
|
187
|
+
tensor list has the same shapes as ``basis_factory().tensors``
|
|
188
|
+
and can be passed back as ``tensors=...`` for a DCT warm-start.
|
|
189
|
+
n_steps : int
|
|
190
|
+
Adam steps. 2000 is a generous default; convergence is typically
|
|
191
|
+
much faster when DCT lies in the parametric family.
|
|
192
|
+
lr : float
|
|
193
|
+
Adam learning rate.
|
|
194
|
+
|
|
195
|
+
The loss is the Frobenius² distance between the circuit's action on
|
|
196
|
+
a complete basis and ``DCT_{2^m} ⊗ DCT_{2^n}``. If the family does
|
|
197
|
+
not contain DCT, the loss plateaus at the closest reachable distance.
|
|
198
|
+
"""
|
|
199
|
+
import time as _time
|
|
200
|
+
|
|
201
|
+
from ...manifolds import group_by_manifold, stack_tensors
|
|
202
|
+
|
|
203
|
+
basis = basis_factory()
|
|
204
|
+
m, n = basis.m, basis.n
|
|
205
|
+
code = basis.code
|
|
206
|
+
init_tensors = basis.tensors
|
|
207
|
+
D_row = _dct_matrix(2**m)
|
|
208
|
+
D_col = _dct_matrix(2**n)
|
|
209
|
+
target = jnp.kron(D_row, D_col).reshape(2**m, 2**n, 2**m, 2**n)
|
|
210
|
+
|
|
211
|
+
eye = jnp.eye(2 ** (m + n), dtype=jnp.complex128).reshape((2 ** (m + n),) + (2,) * (m + n))
|
|
212
|
+
target_flat = target.reshape(2**m, 2**n, 2 ** (m + n))
|
|
213
|
+
|
|
214
|
+
def loss_fn(tensors):
|
|
215
|
+
outs = jax.vmap(lambda x: code(*tensors, x))(eye)
|
|
216
|
+
outs_mat = outs.reshape(2 ** (m + n), 2**m, 2**n).transpose(1, 2, 0)
|
|
217
|
+
return jnp.real(jnp.sum(jnp.abs(outs_mat - target_flat) ** 2))
|
|
218
|
+
|
|
219
|
+
grad_fn = jax.value_and_grad(loss_fn)
|
|
220
|
+
|
|
221
|
+
tensors = list(init_tensors)
|
|
222
|
+
groups = group_by_manifold(tensors)
|
|
223
|
+
manifolds = list(groups.keys())
|
|
224
|
+
indices = [tuple(groups[mfd]) for mfd in manifolds]
|
|
225
|
+
|
|
226
|
+
# Adam state per group.
|
|
227
|
+
m_state, v_state = [], []
|
|
228
|
+
for idxs in indices:
|
|
229
|
+
pb = stack_tensors(tensors, list(idxs))
|
|
230
|
+
m_state.append(jnp.zeros_like(pb))
|
|
231
|
+
v_state.append(jnp.zeros(pb.shape, dtype=jnp.float64))
|
|
232
|
+
|
|
233
|
+
beta1, beta2, eps = 0.9, 0.999, 1e-8
|
|
234
|
+
t0 = _time.perf_counter()
|
|
235
|
+
for step in range(1, n_steps + 1):
|
|
236
|
+
loss_val, raw_grads = grad_fn(tensors)
|
|
237
|
+
grads = [jnp.conj(g) for g in raw_grads]
|
|
238
|
+
|
|
239
|
+
for k, (mfd, idxs) in enumerate(zip(manifolds, indices)):
|
|
240
|
+
pb = stack_tensors(tensors, list(idxs))
|
|
241
|
+
gb = stack_tensors(grads, list(idxs))
|
|
242
|
+
rg = mfd.project(pb, gb)
|
|
243
|
+
new_m = beta1 * m_state[k] + (1 - beta1) * rg
|
|
244
|
+
new_v = beta2 * v_state[k] + (1 - beta2) * jnp.real(jnp.conj(rg) * rg)
|
|
245
|
+
bc1 = 1.0 - beta1**step
|
|
246
|
+
bc2 = 1.0 - beta2**step
|
|
247
|
+
direction = (new_m / bc1) / (jnp.sqrt(new_v / bc2) + eps)
|
|
248
|
+
new_pb = mfd.retract(pb, -direction, lr)
|
|
249
|
+
new_m = mfd.transport(pb, new_pb, new_m)
|
|
250
|
+
m_state[k] = new_m
|
|
251
|
+
v_state[k] = new_v
|
|
252
|
+
for k2, idx in enumerate(idxs):
|
|
253
|
+
tensors[idx] = new_pb[..., k2]
|
|
254
|
+
|
|
255
|
+
if step % 200 == 0 or step == 1:
|
|
256
|
+
print(
|
|
257
|
+
f" fit_to_dct step {step:>4d}: loss={float(loss_val):.4e} "
|
|
258
|
+
f"(elapsed {_time.perf_counter() - t0:.1f}s)",
|
|
259
|
+
flush=True,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return tensors
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
__all__ = ["RichBasis", "fit_to_dct"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Circuit-topology bases: QFT, EntangledQFT, TEBD, MERA."""
|
|
2
|
+
|
|
3
|
+
from .qft import ft_mat, ift_mat, qft_code
|
|
4
|
+
from .entangled_qft import entangled_qft_code
|
|
5
|
+
from .mera import mera_code
|
|
6
|
+
from .tebd import tebd_code
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"entangled_qft_code",
|
|
10
|
+
"ft_mat",
|
|
11
|
+
"ift_mat",
|
|
12
|
+
"mera_code",
|
|
13
|
+
"qft_code",
|
|
14
|
+
"tebd_code",
|
|
15
|
+
]
|