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 ADDED
@@ -0,0 +1,86 @@
1
+ """pdft: Python port of ParametricDFT.jl (faithful JAX reference).
2
+
3
+ Importing this package enables JAX's x64 mode globally. This is required
4
+ to match Julia's ComplexF64 numerical behavior; without it, parity
5
+ tolerances are unreachable. See docs/superpowers/specs/2026-04-24-pdft-migration-design.md
6
+ Section 2 and 8.1.
7
+
8
+ Also enables a persistent JAX compilation cache by default, so the
9
+ ~20-second JIT-compile cost of the Adam step is paid once per basis
10
+ shape across all runs. Override the cache dir with JAX_COMPILATION_CACHE_DIR
11
+ or disable entirely by setting PDFT_DISABLE_COMPILE_CACHE=1.
12
+
13
+ Public API: import from subpackages directly, e.g.
14
+
15
+ from pdft.bases.circuit import QFTBasis
16
+ from pdft.training import train_basis, train_basis_batched
17
+ from pdft.optimizers import RiemannianGD, RiemannianAdam
18
+ from pdft.loss import L1Norm, MSELoss
19
+ from pdft.io import save_basis, load_basis, compress
20
+
21
+ The names re-exported at the package root below are kept only for the
22
+ small set most commonly used in interactive sessions and notebooks.
23
+ """
24
+
25
+ import os as _os
26
+ from pathlib import Path as _Path
27
+
28
+ import jax as _jax
29
+
30
+ _jax.config.update("jax_enable_x64", True)
31
+
32
+ if not _os.environ.get("PDFT_DISABLE_COMPILE_CACHE"):
33
+ _cache_dir = _os.environ.get("JAX_COMPILATION_CACHE_DIR")
34
+ if not _cache_dir:
35
+ _xdg = _os.environ.get("XDG_CACHE_HOME") or str(_Path.home() / ".cache")
36
+ _cache_dir = str(_Path(_xdg) / "pdft" / "jax-compile-cache")
37
+ try:
38
+ _Path(_cache_dir).mkdir(parents=True, exist_ok=True)
39
+ _jax.config.update("jax_compilation_cache_dir", _cache_dir)
40
+ _jax.config.update("jax_persistent_cache_min_compile_time_secs", 0.0)
41
+ except OSError:
42
+ pass
43
+
44
+ __version__ = "0.2.1"
45
+ __upstream_ref__ = "nzy1997/ParametricDFT.jl@a201a27e47df2f0f3ab460f83d49b6e5f5d1e9ef"
46
+
47
+ # Slim public re-export hub: most-used names only. Anything else: import
48
+ # from the relevant subpackage directly.
49
+ from .bases import ( # noqa: E402
50
+ AbstractSparseBasis,
51
+ BlockedBasis,
52
+ EntangledQFTBasis,
53
+ MERABasis,
54
+ QFTBasis,
55
+ RealRichBasis,
56
+ RichBasis,
57
+ TEBDBasis,
58
+ bases_allclose,
59
+ )
60
+ from .loss import AbstractLoss, L1Norm, MSELoss, loss_function # noqa: E402
61
+ from .optimizers import RiemannianAdam, RiemannianGD, optimize # noqa: E402
62
+ from .training import TrainingResult, train_basis, train_basis_batched # noqa: E402
63
+
64
+ __all__ = [
65
+ "AbstractLoss",
66
+ "AbstractSparseBasis",
67
+ "BlockedBasis",
68
+ "EntangledQFTBasis",
69
+ "L1Norm",
70
+ "MERABasis",
71
+ "MSELoss",
72
+ "QFTBasis",
73
+ "RealRichBasis",
74
+ "RichBasis",
75
+ "RiemannianAdam",
76
+ "RiemannianGD",
77
+ "TEBDBasis",
78
+ "TrainingResult",
79
+ "__upstream_ref__",
80
+ "__version__",
81
+ "bases_allclose",
82
+ "loss_function",
83
+ "optimize",
84
+ "train_basis",
85
+ "train_basis_batched",
86
+ ]
pdft/bases/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """Sparse-basis subpackage.
2
+
3
+ Two families:
4
+ - bases.circuit — full circuit topologies (QFT, EntangledQFT, TEBD, MERA),
5
+ comparable to FFT/DCT.
6
+ - bases.block — parameter-efficient block-structured bases (Blocked,
7
+ Rich, RealRich) over arbitrary block partitions.
8
+
9
+ The abstract base class and bases_allclose helper live in bases.base.
10
+ """
11
+
12
+ from .base import (
13
+ AbstractSparseBasis,
14
+ EntangledQFTBasis,
15
+ MERABasis,
16
+ QFTBasis,
17
+ TEBDBasis,
18
+ bases_allclose,
19
+ )
20
+ from .block import BlockedBasis, RealRichBasis, RichBasis, fit_to_dct
21
+ from .circuit import entangled_qft_code, ft_mat, ift_mat, mera_code, qft_code, tebd_code
22
+
23
+ __all__ = [
24
+ "AbstractSparseBasis",
25
+ "BlockedBasis",
26
+ "EntangledQFTBasis",
27
+ "MERABasis",
28
+ "QFTBasis",
29
+ "RealRichBasis",
30
+ "RichBasis",
31
+ "TEBDBasis",
32
+ "bases_allclose",
33
+ "entangled_qft_code",
34
+ "fit_to_dct",
35
+ "ft_mat",
36
+ "ift_mat",
37
+ "mera_code",
38
+ "qft_code",
39
+ "tebd_code",
40
+ ]
pdft/bases/base.py ADDED
@@ -0,0 +1,470 @@
1
+ """Sparse basis abstraction + concrete bases.
2
+
3
+ Mirror of upstream src/basis.jl. Phase 1 shipped QFTBasis; Phase 3 adds
4
+ EntangledQFTBasis, TEBDBasis, and MERABasis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+ from dataclasses import dataclass, field
11
+ from typing import Protocol, runtime_checkable
12
+
13
+ import jax
14
+ import jax.numpy as jnp
15
+ from jax import tree_util
16
+
17
+ from .circuit.qft import qft_code
18
+
19
+ Array = jax.Array
20
+
21
+
22
+ @runtime_checkable
23
+ class AbstractSparseBasis(Protocol):
24
+ @property
25
+ def image_size(self) -> tuple[int, int]: ...
26
+ @property
27
+ def num_parameters(self) -> int: ...
28
+ def forward_transform(self, pic: Array) -> Array: ...
29
+ def inverse_transform(self, pic: Array) -> Array: ...
30
+
31
+
32
+ @dataclass
33
+ class QFTBasis:
34
+ """QFT tensor-network basis. Mirror of `ParametricDFT.jl/src/basis.jl::QFTBasis`.
35
+
36
+ Stores ONE tensor list. `inverse_transform` applies `conj(tensors)` through
37
+ `inv_code` — exactly like Julia's `inverse_transform(basis::QFTBasis, ...)`
38
+ which does `basis.inverse_code(conj.(basis.tensors)..., ...)`.
39
+
40
+ `code` and `inv_code` are jit-compiled einsum closures and compare by
41
+ identity; they are marked `compare=False` so the dataclass-generated
42
+ `__eq__` doesn't consider them. Use `bases_allclose` for semantic
43
+ comparison.
44
+
45
+ Pytree contract:
46
+ leaves = tensors (one list)
47
+ aux data = (m, n, len(tensors), code, inv_code)
48
+ """
49
+
50
+ m: int
51
+ n: int
52
+ tensors: list[Array]
53
+ code: object = field(compare=False, repr=False)
54
+ inv_code: object = field(compare=False, repr=False)
55
+
56
+ def __init__(
57
+ self,
58
+ m: int,
59
+ n: int,
60
+ tensors: Sequence[Array] | None = None,
61
+ code: object | None = None,
62
+ inv_code: object | None = None,
63
+ ):
64
+ if m < 1 or n < 1:
65
+ raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
66
+ self.m = m
67
+ self.n = n
68
+ _code, init_tensors = qft_code(m, n)
69
+ _inv_code, _ = qft_code(m, n, inverse=True)
70
+ self.tensors = list(tensors) if tensors is not None else init_tensors
71
+ self.code = code if code is not None else _code
72
+ self.inv_code = inv_code if inv_code is not None else _inv_code
73
+
74
+ @property
75
+ def inv_tensors(self) -> list[Array]:
76
+ """Back-compat alias: Julia stores one tensor list. The "inverse"
77
+ is computed by applying `conj(tensors)` through `inv_code`. We
78
+ expose this property so older callers that read `basis.inv_tensors`
79
+ keep working — they get the same arrays as `basis.tensors`.
80
+ """
81
+ return self.tensors
82
+
83
+ # ------ AbstractSparseBasis interface ------
84
+
85
+ @property
86
+ def image_size(self) -> tuple[int, int]:
87
+ return (2**self.m, 2**self.n)
88
+
89
+ @property
90
+ def num_parameters(self) -> int:
91
+ return sum(int(t.size) for t in self.tensors)
92
+
93
+ def forward_transform(self, pic: Array) -> Array:
94
+ from .circuit.qft import ft_mat
95
+
96
+ return ft_mat(self.tensors, self.code, self.m, self.n, pic)
97
+
98
+ def inverse_transform(self, pic: Array) -> Array:
99
+ from .circuit.qft import ift_mat
100
+
101
+ return ift_mat(
102
+ [jnp.conj(t) for t in self.tensors],
103
+ self.inv_code,
104
+ self.m,
105
+ self.n,
106
+ pic,
107
+ )
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # JAX pytree registration
112
+ # ---------------------------------------------------------------------------
113
+
114
+
115
+ def _qftbasis_flatten(b: QFTBasis):
116
+ leaves = tuple(b.tensors)
117
+ aux = (b.m, b.n, len(b.tensors), b.code, b.inv_code)
118
+ return leaves, aux
119
+
120
+
121
+ def _qftbasis_unflatten(aux, leaves) -> QFTBasis:
122
+ m, n, n_fwd, code, inv_code = aux
123
+ assert len(leaves) == n_fwd
124
+ tensors = list(leaves)
125
+ return QFTBasis(m=m, n=n, tensors=tensors, code=code, inv_code=inv_code)
126
+
127
+
128
+ tree_util.register_pytree_node(QFTBasis, _qftbasis_flatten, _qftbasis_unflatten)
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Equality helper
133
+ # ---------------------------------------------------------------------------
134
+
135
+
136
+ def bases_allclose(a, b, *, atol: float = 1e-10) -> bool:
137
+ """Semantic equality across any of the registered basis types.
138
+
139
+ Checks same concrete type, same (m, n), and all `tensors` within `atol`
140
+ (rtol=0). `code` / `inv_code` are ignored. Per Julia's design we no
141
+ longer store a separate `inv_tensors` to compare.
142
+ """
143
+ if type(a) is not type(b):
144
+ return False
145
+ if (a.m, a.n) != (b.m, b.n):
146
+ return False
147
+ if len(a.tensors) != len(b.tensors):
148
+ return False
149
+ for x, y in zip(a.tensors, b.tensors):
150
+ if not jnp.allclose(x, y, atol=atol, rtol=0.0):
151
+ return False
152
+ return True
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # EntangledQFTBasis
157
+ # ---------------------------------------------------------------------------
158
+
159
+
160
+ @dataclass
161
+ class EntangledQFTBasis:
162
+ """QFT + appended entanglement layer on `min(m, n)` row/col qubit pairs.
163
+
164
+ Mirror of upstream src/basis.jl:280-500 (entangle_position=:back only).
165
+ """
166
+
167
+ m: int
168
+ n: int
169
+ n_entangle: int
170
+ tensors: list[Array]
171
+ code: object = field(compare=False, repr=False)
172
+ inv_code: object = field(compare=False, repr=False)
173
+
174
+ def __init__(
175
+ self,
176
+ m: int,
177
+ n: int,
178
+ tensors: Sequence[Array] | None = None,
179
+ entangle_phases: Sequence[float] | None = None,
180
+ entangle_position: str = "back",
181
+ code: object | None = None,
182
+ inv_code: object | None = None,
183
+ seed: int | None = None,
184
+ ):
185
+ """Mirror of `_init_circuit(::Type{EntangledQFTBasis}, ...)` from
186
+ ParametricDFT.jl/src/training.jl. When `seed` is provided AND
187
+ `entangle_phases` is None, draws phases from
188
+ `np.random.default_rng(seed).normal(0, 0.1, n_entangle)` — Julia's
189
+ `randn(n_gates) * 0.1` convention. This breaks the symmetry-collapse
190
+ where `EntangledQFTBasis` initialises identically to `QFTBasis` when
191
+ all entanglement phases are zero.
192
+ """
193
+ import numpy as np
194
+
195
+ from .circuit.entangled_qft import entangled_qft_code
196
+
197
+ if m < 1 or n < 1:
198
+ raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
199
+ self.m = m
200
+ self.n = n
201
+ n_entangle = min(m, n)
202
+ if entangle_phases is None and seed is not None:
203
+ entangle_phases = list(np.random.default_rng(seed).normal(0.0, 0.1, n_entangle))
204
+ _code, init_tensors, self.n_entangle = entangled_qft_code(
205
+ m, n, entangle_phases=entangle_phases, entangle_position=entangle_position
206
+ )
207
+ _inv_code, _, _ = entangled_qft_code(
208
+ m, n, entangle_phases=entangle_phases, entangle_position=entangle_position, inverse=True
209
+ )
210
+ self.tensors = list(tensors) if tensors is not None else init_tensors
211
+ self.code = code if code is not None else _code
212
+ self.inv_code = inv_code if inv_code is not None else _inv_code
213
+
214
+ @property
215
+ def inv_tensors(self) -> list[Array]:
216
+ return self.tensors
217
+
218
+ @property
219
+ def image_size(self) -> tuple[int, int]:
220
+ return (2**self.m, 2**self.n)
221
+
222
+ @property
223
+ def num_parameters(self) -> int:
224
+ return sum(int(t.size) for t in self.tensors)
225
+
226
+ def forward_transform(self, pic: Array) -> Array:
227
+ from ..circuit.builder import apply_circuit
228
+
229
+ return apply_circuit(self.tensors, self.code, self.m, self.n, pic)
230
+
231
+ def inverse_transform(self, pic: Array) -> Array:
232
+ from ..circuit.builder import apply_circuit
233
+
234
+ return apply_circuit(
235
+ [jnp.conj(t) for t in self.tensors],
236
+ self.inv_code,
237
+ self.m,
238
+ self.n,
239
+ pic,
240
+ )
241
+
242
+
243
+ def _entangled_flatten(b: EntangledQFTBasis):
244
+ leaves = tuple(b.tensors)
245
+ aux = (b.m, b.n, b.n_entangle, len(b.tensors), b.code, b.inv_code)
246
+ return leaves, aux
247
+
248
+
249
+ def _entangled_unflatten(aux, leaves) -> EntangledQFTBasis:
250
+ m, n, n_entangle, n_fwd, code, inv_code = aux
251
+ tensors = list(leaves)
252
+ out = EntangledQFTBasis.__new__(EntangledQFTBasis)
253
+ out.m = m
254
+ out.n = n
255
+ out.n_entangle = n_entangle
256
+ out.tensors = tensors
257
+ out.code = code
258
+ out.inv_code = inv_code
259
+ return out
260
+
261
+
262
+ tree_util.register_pytree_node(EntangledQFTBasis, _entangled_flatten, _entangled_unflatten)
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # TEBDBasis
267
+ # ---------------------------------------------------------------------------
268
+
269
+
270
+ @dataclass
271
+ class TEBDBasis:
272
+ """2D TEBD basis with row + column rings of controlled-phase gates.
273
+
274
+ Mirror of upstream src/basis.jl:600-745.
275
+ """
276
+
277
+ m: int
278
+ n: int
279
+ n_row_gates: int
280
+ n_col_gates: int
281
+ tensors: list[Array]
282
+ code: object = field(compare=False, repr=False)
283
+ inv_code: object = field(compare=False, repr=False)
284
+
285
+ def __init__(
286
+ self,
287
+ m: int,
288
+ n: int,
289
+ tensors: Sequence[Array] | None = None,
290
+ phases: Sequence[float] | None = None,
291
+ code: object | None = None,
292
+ inv_code: object | None = None,
293
+ seed: int | None = None,
294
+ ):
295
+ """When `seed` is provided AND `phases` is None, draws phases from
296
+ `np.random.default_rng(seed).normal(0, 0.1, n_row_gates + n_col_gates)`
297
+ — Julia's `randn(n_gates) * 0.1` convention from
298
+ `ParametricDFT.jl/src/training.jl::_init_circuit`.
299
+ """
300
+ import numpy as np
301
+
302
+ from .circuit.tebd import tebd_code
303
+
304
+ if m < 1 or n < 1:
305
+ raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
306
+ self.m = m
307
+ self.n = n
308
+ if phases is None and seed is not None:
309
+ phases = list(np.random.default_rng(seed).normal(0.0, 0.1, m + n))
310
+ _code, init_tensors, self.n_row_gates, self.n_col_gates = tebd_code(m, n, phases=phases)
311
+ _inv_code, _, _, _ = tebd_code(m, n, phases=phases, inverse=True)
312
+ self.tensors = list(tensors) if tensors is not None else init_tensors
313
+ self.code = code if code is not None else _code
314
+ self.inv_code = inv_code if inv_code is not None else _inv_code
315
+
316
+ @property
317
+ def inv_tensors(self) -> list[Array]:
318
+ return self.tensors
319
+
320
+ @property
321
+ def image_size(self) -> tuple[int, int]:
322
+ return (2**self.m, 2**self.n)
323
+
324
+ @property
325
+ def num_parameters(self) -> int:
326
+ return sum(int(t.size) for t in self.tensors)
327
+
328
+ def forward_transform(self, pic: Array) -> Array:
329
+ from ..circuit.builder import apply_circuit
330
+
331
+ return apply_circuit(self.tensors, self.code, self.m, self.n, pic)
332
+
333
+ def inverse_transform(self, pic: Array) -> Array:
334
+ from ..circuit.builder import apply_circuit
335
+
336
+ return apply_circuit(
337
+ [jnp.conj(t) for t in self.tensors],
338
+ self.inv_code,
339
+ self.m,
340
+ self.n,
341
+ pic,
342
+ )
343
+
344
+
345
+ def _tebd_flatten(b: TEBDBasis):
346
+ leaves = tuple(b.tensors)
347
+ aux = (b.m, b.n, b.n_row_gates, b.n_col_gates, len(b.tensors), b.code, b.inv_code)
348
+ return leaves, aux
349
+
350
+
351
+ def _tebd_unflatten(aux, leaves) -> TEBDBasis:
352
+ m, n, nr, nc, n_fwd, code, inv_code = aux
353
+ tensors = list(leaves)
354
+ out = TEBDBasis.__new__(TEBDBasis)
355
+ out.m = m
356
+ out.n = n
357
+ out.n_row_gates = nr
358
+ out.n_col_gates = nc
359
+ out.tensors = tensors
360
+ out.code = code
361
+ out.inv_code = inv_code
362
+ return out
363
+
364
+
365
+ tree_util.register_pytree_node(TEBDBasis, _tebd_flatten, _tebd_unflatten)
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # MERABasis
370
+ # ---------------------------------------------------------------------------
371
+
372
+
373
+ @dataclass
374
+ class MERABasis:
375
+ """2D MERA basis. Each dimension with >=2 qubits must be a power of 2.
376
+
377
+ Mirror of upstream src/basis.jl:840-1070.
378
+ """
379
+
380
+ m: int
381
+ n: int
382
+ n_row_gates: int
383
+ n_col_gates: int
384
+ tensors: list[Array]
385
+ code: object = field(compare=False, repr=False)
386
+ inv_code: object = field(compare=False, repr=False)
387
+
388
+ def __init__(
389
+ self,
390
+ m: int,
391
+ n: int,
392
+ tensors: Sequence[Array] | None = None,
393
+ phases: Sequence[float] | None = None,
394
+ code: object | None = None,
395
+ inv_code: object | None = None,
396
+ seed: int | None = None,
397
+ ):
398
+ """When `seed` is provided AND `phases` is None, draws phases from
399
+ `np.random.default_rng(seed).normal(0, 0.1, n_gates)` — Julia's
400
+ `randn(n_gates) * 0.1` convention from
401
+ `ParametricDFT.jl/src/training.jl::_init_circuit`.
402
+ """
403
+ import numpy as np
404
+
405
+ from .circuit.mera import _n_mera_gates, mera_code
406
+
407
+ if m < 1 or n < 1:
408
+ raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
409
+ self.m = m
410
+ self.n = n
411
+ if phases is None and seed is not None:
412
+ n_row = _n_mera_gates(m) if m >= 2 else 0
413
+ n_col = _n_mera_gates(n) if n >= 2 else 0
414
+ phases = list(np.random.default_rng(seed).normal(0.0, 0.1, n_row + n_col))
415
+ _code, init_tensors, self.n_row_gates, self.n_col_gates = mera_code(m, n, phases=phases)
416
+ _inv_code, _, _, _ = mera_code(m, n, phases=phases, inverse=True)
417
+ self.tensors = list(tensors) if tensors is not None else init_tensors
418
+ self.code = code if code is not None else _code
419
+ self.inv_code = inv_code if inv_code is not None else _inv_code
420
+
421
+ @property
422
+ def inv_tensors(self) -> list[Array]:
423
+ return self.tensors
424
+
425
+ @property
426
+ def image_size(self) -> tuple[int, int]:
427
+ return (2**self.m, 2**self.n)
428
+
429
+ @property
430
+ def num_parameters(self) -> int:
431
+ return sum(int(t.size) for t in self.tensors)
432
+
433
+ def forward_transform(self, pic: Array) -> Array:
434
+ from ..circuit.builder import apply_circuit
435
+
436
+ return apply_circuit(self.tensors, self.code, self.m, self.n, pic)
437
+
438
+ def inverse_transform(self, pic: Array) -> Array:
439
+ from ..circuit.builder import apply_circuit
440
+
441
+ return apply_circuit(
442
+ [jnp.conj(t) for t in self.tensors],
443
+ self.inv_code,
444
+ self.m,
445
+ self.n,
446
+ pic,
447
+ )
448
+
449
+
450
+ def _mera_flatten(b: MERABasis):
451
+ leaves = tuple(b.tensors)
452
+ aux = (b.m, b.n, b.n_row_gates, b.n_col_gates, len(b.tensors), b.code, b.inv_code)
453
+ return leaves, aux
454
+
455
+
456
+ def _mera_unflatten(aux, leaves) -> MERABasis:
457
+ m, n, nr, nc, n_fwd, code, inv_code = aux
458
+ tensors = list(leaves)
459
+ out = MERABasis.__new__(MERABasis)
460
+ out.m = m
461
+ out.n = n
462
+ out.n_row_gates = nr
463
+ out.n_col_gates = nc
464
+ out.tensors = tensors
465
+ out.code = code
466
+ out.inv_code = inv_code
467
+ return out
468
+
469
+
470
+ tree_util.register_pytree_node(MERABasis, _mera_flatten, _mera_unflatten)
@@ -0,0 +1,12 @@
1
+ """Block-structured bases: Blocked, Rich, RealRich."""
2
+
3
+ from .block import BlockedBasis
4
+ from .rich import RichBasis, fit_to_dct
5
+ from .real_rich import RealRichBasis
6
+
7
+ __all__ = [
8
+ "BlockedBasis",
9
+ "RealRichBasis",
10
+ "RichBasis",
11
+ "fit_to_dct",
12
+ ]