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.
@@ -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"]
@@ -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
+ ]