pdft 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. {pdft-0.2.2 → pdft-0.2.3}/.claude/CLAUDE.md +2 -2
  2. {pdft-0.2.2 → pdft-0.2.3}/PKG-INFO +1 -1
  3. {pdft-0.2.2 → pdft-0.2.3}/pyproject.toml +1 -1
  4. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/__init__.py +5 -1
  5. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/__init__.py +18 -4
  6. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/base.py +124 -4
  7. pdft-0.2.3/src/pdft/bases/block/__init__.py +15 -0
  8. pdft-0.2.3/src/pdft/bases/circuit/__init__.py +30 -0
  9. pdft-0.2.3/src/pdft/bases/circuit/dct4.py +189 -0
  10. pdft-0.2.3/src/pdft/bases/circuit/freeze.py +116 -0
  11. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/circuit/mera.py +47 -14
  12. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/circuit/tebd.py +52 -37
  13. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/circuit/__init__.py +2 -0
  14. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/circuit/builder.py +56 -22
  15. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/manifolds.py +13 -22
  16. pdft-0.2.3/tests/bases/circuit/test_dct4.py +186 -0
  17. pdft-0.2.3/tests/bases/circuit/test_freeze.py +170 -0
  18. pdft-0.2.3/tests/circuit/test_cry_gate.py +38 -0
  19. pdft-0.2.3/tests/circuit/test_sorted_gate_program.py +26 -0
  20. {pdft-0.2.2 → pdft-0.2.3}/tests/test_manifolds.py +22 -0
  21. {pdft-0.2.2 → pdft-0.2.3}/tests/training/test_batched.py +10 -17
  22. pdft-0.2.2/src/pdft/bases/block/__init__.py +0 -12
  23. pdft-0.2.2/src/pdft/bases/circuit/__init__.py +0 -15
  24. {pdft-0.2.2 → pdft-0.2.3}/.github/workflows/ci.yml +0 -0
  25. {pdft-0.2.2 → pdft-0.2.3}/.github/workflows/publish.yml +0 -0
  26. {pdft-0.2.2 → pdft-0.2.3}/.github/workflows/verify-upstream-pin.yml +0 -0
  27. {pdft-0.2.2 → pdft-0.2.3}/.gitignore +0 -0
  28. {pdft-0.2.2 → pdft-0.2.3}/.pre-commit-config.yaml +0 -0
  29. {pdft-0.2.2 → pdft-0.2.3}/LICENSE +0 -0
  30. {pdft-0.2.2 → pdft-0.2.3}/Makefile +0 -0
  31. {pdft-0.2.2 → pdft-0.2.3}/README.md +0 -0
  32. {pdft-0.2.2 → pdft-0.2.3}/examples/basis_demo.py +0 -0
  33. {pdft-0.2.2 → pdft-0.2.3}/examples/mera_demo.py +0 -0
  34. {pdft-0.2.2 → pdft-0.2.3}/examples/optimizer_benchmark.py +0 -0
  35. {pdft-0.2.2 → pdft-0.2.3}/reference/README.md +0 -0
  36. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/adam_trajectory_4x4.npz +0 -0
  37. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/basis_roundtrip.npz +0 -0
  38. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/basis_roundtrip_hash.txt +0 -0
  39. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/compression_roundtrip_4x4.npz +0 -0
  40. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/entangled_qft_4x4.npz +0 -0
  41. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/ft_mat_roundtrip.npz +0 -0
  42. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/loss_values.npz +0 -0
  43. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/manifest.json +0 -0
  44. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/manifold_project_retract.npz +0 -0
  45. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/mera_4x4.npz +0 -0
  46. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/phase_extraction_3x3.npz +0 -0
  47. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/qft_basis_trained.json +0 -0
  48. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/qft_code_16x16.npz +0 -0
  49. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/qft_code_4x4.npz +0 -0
  50. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/tebd_4x4.npz +0 -0
  51. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/train_trajectory_4x4.npz +0 -0
  52. {pdft-0.2.2 → pdft-0.2.3}/reference/goldens/train_trajectory_4x4_long.npz +0 -0
  53. {pdft-0.2.2 → pdft-0.2.3}/reference/julia/Manifest.toml +0 -0
  54. {pdft-0.2.2 → pdft-0.2.3}/reference/julia/Project.toml +0 -0
  55. {pdft-0.2.2 → pdft-0.2.3}/reference/julia/generate_goldens.jl +0 -0
  56. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/block/block.py +0 -0
  57. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/circuit/entangled_qft.py +0 -0
  58. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/bases/circuit/qft.py +0 -0
  59. {pdft-0.2.2/src/pdft/bases/block → pdft-0.2.3/src/pdft/bases/circuit}/real_rich.py +0 -0
  60. {pdft-0.2.2/src/pdft/bases/block → pdft-0.2.3/src/pdft/bases/circuit}/rich.py +0 -0
  61. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/circuit/cache.py +0 -0
  62. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/io/__init__.py +0 -0
  63. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/io/compression.py +0 -0
  64. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/io/serialize.py +0 -0
  65. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/loss.py +0 -0
  66. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/optimizers/__init__.py +0 -0
  67. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/optimizers/adam.py +0 -0
  68. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/optimizers/core.py +0 -0
  69. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/optimizers/gd.py +0 -0
  70. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/optimizers/loop.py +0 -0
  71. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/profiling.py +0 -0
  72. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/__init__.py +0 -0
  73. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/adam_step.py +0 -0
  74. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/batched.py +0 -0
  75. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/eval_loop.py +0 -0
  76. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/result.py +0 -0
  77. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/schedules.py +0 -0
  78. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/training/single.py +0 -0
  79. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/viz/__init__.py +0 -0
  80. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/viz/circuit.py +0 -0
  81. {pdft-0.2.2 → pdft-0.2.3}/src/pdft/viz/loss.py +0 -0
  82. {pdft-0.2.2 → pdft-0.2.3}/tests/__init__.py +0 -0
  83. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/__init__.py +0 -0
  84. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/block/__init__.py +0 -0
  85. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/block/test_block.py +0 -0
  86. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/circuit/__init__.py +0 -0
  87. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/circuit/test_qft.py +0 -0
  88. {pdft-0.2.2/tests/bases/block → pdft-0.2.3/tests/bases/circuit}/test_real_rich.py +0 -0
  89. {pdft-0.2.2/tests/bases/block → pdft-0.2.3/tests/bases/circuit}/test_rich.py +0 -0
  90. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/conftest.py +0 -0
  91. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/test_base.py +0 -0
  92. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/test_new_bases.py +0 -0
  93. {pdft-0.2.2 → pdft-0.2.3}/tests/bases/test_phase_extraction.py +0 -0
  94. {pdft-0.2.2 → pdft-0.2.3}/tests/circuit/__init__.py +0 -0
  95. {pdft-0.2.2 → pdft-0.2.3}/tests/circuit/test_cache.py +0 -0
  96. {pdft-0.2.2 → pdft-0.2.3}/tests/circuit/test_h_inverse_leg_swap.py +0 -0
  97. {pdft-0.2.2 → pdft-0.2.3}/tests/circuit/test_high_qubit_circuit.py +0 -0
  98. {pdft-0.2.2 → pdft-0.2.3}/tests/circuit/test_legacy_builder.py +0 -0
  99. {pdft-0.2.2 → pdft-0.2.3}/tests/conftest.py +0 -0
  100. {pdft-0.2.2 → pdft-0.2.3}/tests/io/__init__.py +0 -0
  101. {pdft-0.2.2 → pdft-0.2.3}/tests/io/conftest.py +0 -0
  102. {pdft-0.2.2 → pdft-0.2.3}/tests/io/test_compression.py +0 -0
  103. {pdft-0.2.2 → pdft-0.2.3}/tests/io/test_serialize.py +0 -0
  104. {pdft-0.2.2 → pdft-0.2.3}/tests/optimizers/__init__.py +0 -0
  105. {pdft-0.2.2 → pdft-0.2.3}/tests/optimizers/conftest.py +0 -0
  106. {pdft-0.2.2 → pdft-0.2.3}/tests/optimizers/test_adam.py +0 -0
  107. {pdft-0.2.2 → pdft-0.2.3}/tests/optimizers/test_gd.py +0 -0
  108. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/__init__.py +0 -0
  109. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/conftest.py +0 -0
  110. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_adam.py +0 -0
  111. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_compression.py +0 -0
  112. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_long_run.py +0 -0
  113. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_loss.py +0 -0
  114. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_manifolds.py +0 -0
  115. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_new_bases.py +0 -0
  116. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_qft.py +0 -0
  117. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_scale.py +0 -0
  118. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_serialize.py +0 -0
  119. {pdft-0.2.2 → pdft-0.2.3}/tests/parity/test_training.py +0 -0
  120. {pdft-0.2.2 → pdft-0.2.3}/tests/test_basis_seeded_init.py +0 -0
  121. {pdft-0.2.2 → pdft-0.2.3}/tests/test_loss.py +0 -0
  122. {pdft-0.2.2 → pdft-0.2.3}/tests/test_profiling.py +0 -0
  123. {pdft-0.2.2 → pdft-0.2.3}/tests/test_smoke.py +0 -0
  124. {pdft-0.2.2 → pdft-0.2.3}/tests/training/__init__.py +0 -0
  125. {pdft-0.2.2 → pdft-0.2.3}/tests/training/conftest.py +0 -0
  126. {pdft-0.2.2 → pdft-0.2.3}/tests/training/test_eval_loop.py +0 -0
  127. {pdft-0.2.2 → pdft-0.2.3}/tests/training/test_integration.py +0 -0
  128. {pdft-0.2.2 → pdft-0.2.3}/tests/training/test_single.py +0 -0
  129. {pdft-0.2.2 → pdft-0.2.3}/tests/viz/__init__.py +0 -0
  130. {pdft-0.2.2 → pdft-0.2.3}/tests/viz/test_loss.py +0 -0
@@ -80,8 +80,8 @@ src/pdft/
80
80
  ├── profiling.py Cross-cutting profiling helpers
81
81
  ├── bases/
82
82
  │ ├── base.py AbstractSparseBasis + bases_allclose + 4 basis dataclasses
83
- │ ├── circuit/ QFT, EntangledQFT, TEBD, MERA (full circuit topologies)
84
- │ └── block/ Blocked, Rich, RealRich (parameter-efficient)
83
+ │ ├── circuit/ QFT, EntangledQFT, TEBD, MERA, Rich, RealRich + freeze_as_blocked
84
+ │ └── block/ BlockedBasis (Rich/RealRich re-exported from circuit for back-compat)
85
85
  ├── circuit/ Einsum builder (builder.py) + JIT closure cache (cache.py)
86
86
  ├── optimizers/ core, gd (RiemannianGD + Armijo), adam (RiemannianAdam), loop
87
87
  ├── training/ schedules, single (train_basis), batched, adam_step, eval_loop
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdft
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Python port of ParametricDFT.jl: learning parametric quantum Fourier transforms via manifold optimization.
5
5
  Project-URL: Homepage, https://github.com/zazabap/pdft
6
6
  Project-URL: Issues, https://github.com/zazabap/pdft/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pdft"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Python port of ParametricDFT.jl: learning parametric quantum Fourier transforms via manifold optimization."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -41,7 +41,7 @@ if not _os.environ.get("PDFT_DISABLE_COMPILE_CACHE"):
41
41
  except OSError:
42
42
  pass
43
43
 
44
- __version__ = "0.2.2"
44
+ __version__ = "0.2.3"
45
45
  __upstream_ref__ = "nzy1997/ParametricDFT.jl@a201a27e47df2f0f3ab460f83d49b6e5f5d1e9ef"
46
46
 
47
47
  # Slim public re-export hub: most-used names only. Anything else: import
@@ -49,6 +49,7 @@ __upstream_ref__ = "nzy1997/ParametricDFT.jl@a201a27e47df2f0f3ab460f83d49b6e5f5d
49
49
  from .bases import ( # noqa: E402
50
50
  AbstractSparseBasis,
51
51
  BlockedBasis,
52
+ DCT4Basis,
52
53
  EntangledQFTBasis,
53
54
  MERABasis,
54
55
  QFTBasis,
@@ -56,6 +57,7 @@ from .bases import ( # noqa: E402
56
57
  RichBasis,
57
58
  TEBDBasis,
58
59
  bases_allclose,
60
+ freeze_as_blocked,
59
61
  )
60
62
  from .loss import AbstractLoss, L1Norm, MSELoss, loss_function # noqa: E402
61
63
  from .optimizers import RiemannianAdam, RiemannianGD, optimize # noqa: E402
@@ -65,6 +67,7 @@ __all__ = [
65
67
  "AbstractLoss",
66
68
  "AbstractSparseBasis",
67
69
  "BlockedBasis",
70
+ "DCT4Basis",
68
71
  "EntangledQFTBasis",
69
72
  "L1Norm",
70
73
  "MERABasis",
@@ -79,6 +82,7 @@ __all__ = [
79
82
  "__upstream_ref__",
80
83
  "__version__",
81
84
  "bases_allclose",
85
+ "freeze_as_blocked",
82
86
  "loss_function",
83
87
  "optimize",
84
88
  "train_basis",
@@ -3,26 +3,39 @@
3
3
  Two families:
4
4
  - bases.circuit — full circuit topologies (QFT, EntangledQFT, TEBD, MERA),
5
5
  comparable to FFT/DCT.
6
- - bases.block — parameter-efficient block-structured bases (Blocked,
7
- Rich, RealRich) over arbitrary block partitions.
6
+ - bases.block — BlockedBasis over arbitrary block partitions (Rich/RealRich
7
+ are re-exported here for back-compat; they live in circuit).
8
8
 
9
9
  The abstract base class and bases_allclose helper live in bases.base.
10
10
  """
11
11
 
12
12
  from .base import (
13
13
  AbstractSparseBasis,
14
+ DCT4Basis,
14
15
  EntangledQFTBasis,
15
16
  MERABasis,
16
17
  QFTBasis,
17
18
  TEBDBasis,
18
19
  bases_allclose,
19
20
  )
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
21
+ from .block import BlockedBasis
22
+ from .circuit import (
23
+ RealRichBasis,
24
+ RichBasis,
25
+ entangled_qft_code,
26
+ fit_to_dct,
27
+ freeze_as_blocked,
28
+ ft_mat,
29
+ ift_mat,
30
+ mera_code,
31
+ qft_code,
32
+ tebd_code,
33
+ )
22
34
 
23
35
  __all__ = [
24
36
  "AbstractSparseBasis",
25
37
  "BlockedBasis",
38
+ "DCT4Basis",
26
39
  "EntangledQFTBasis",
27
40
  "MERABasis",
28
41
  "QFTBasis",
@@ -32,6 +45,7 @@ __all__ = [
32
45
  "bases_allclose",
33
46
  "entangled_qft_code",
34
47
  "fit_to_dct",
48
+ "freeze_as_blocked",
35
49
  "ft_mat",
36
50
  "ift_mat",
37
51
  "mera_code",
@@ -291,11 +291,15 @@ class TEBDBasis:
291
291
  code: object | None = None,
292
292
  inv_code: object | None = None,
293
293
  seed: int | None = None,
294
+ parametrization: str = "cp",
294
295
  ):
295
296
  """When `seed` is provided AND `phases` is None, draws phases from
296
297
  `np.random.default_rng(seed).normal(0, 0.1, n_row_gates + n_col_gates)`
297
298
  — Julia's `randn(n_gates) * 0.1` convention from
298
299
  `ParametricDFT.jl/src/training.jl::_init_circuit`.
300
+
301
+ `parametrization` is "cp" (diagonal ring gates on U(1)^4) or "u4"
302
+ (dense two-qubit ring gates on U(4), the canonical TEBD gate).
299
303
  """
300
304
  import numpy as np
301
305
 
@@ -307,8 +311,12 @@ class TEBDBasis:
307
311
  self.n = n
308
312
  if phases is None and seed is not None:
309
313
  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)
314
+ _code, init_tensors, self.n_row_gates, self.n_col_gates = tebd_code(
315
+ m, n, phases=phases, parametrization=parametrization
316
+ )
317
+ _inv_code, _, _, _ = tebd_code(
318
+ m, n, phases=phases, inverse=True, parametrization=parametrization
319
+ )
312
320
  self.tensors = list(tensors) if tensors is not None else init_tensors
313
321
  self.code = code if code is not None else _code
314
322
  self.inv_code = inv_code if inv_code is not None else _inv_code
@@ -394,11 +402,15 @@ class MERABasis:
394
402
  code: object | None = None,
395
403
  inv_code: object | None = None,
396
404
  seed: int | None = None,
405
+ parametrization: str = "cp",
397
406
  ):
398
407
  """When `seed` is provided AND `phases` is None, draws phases from
399
408
  `np.random.default_rng(seed).normal(0, 0.1, n_gates)` — Julia's
400
409
  `randn(n_gates) * 0.1` convention from
401
410
  `ParametricDFT.jl/src/training.jl::_init_circuit`.
411
+
412
+ `parametrization` is "cp" (diagonal disentanglers/isometries on
413
+ U(1)^4) or "u4" (dense two-qubit gates on U(4), the canonical form).
402
414
  """
403
415
  import numpy as np
404
416
 
@@ -412,8 +424,12 @@ class MERABasis:
412
424
  n_row = _n_mera_gates(m) if m >= 2 else 0
413
425
  n_col = _n_mera_gates(n) if n >= 2 else 0
414
426
  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)
427
+ _code, init_tensors, self.n_row_gates, self.n_col_gates = mera_code(
428
+ m, n, phases=phases, parametrization=parametrization
429
+ )
430
+ _inv_code, _, _, _ = mera_code(
431
+ m, n, phases=phases, inverse=True, parametrization=parametrization
432
+ )
417
433
  self.tensors = list(tensors) if tensors is not None else init_tensors
418
434
  self.code = code if code is not None else _code
419
435
  self.inv_code = inv_code if inv_code is not None else _inv_code
@@ -468,3 +484,107 @@ def _mera_unflatten(aux, leaves) -> MERABasis:
468
484
 
469
485
 
470
486
  tree_util.register_pytree_node(MERABasis, _mera_flatten, _mera_unflatten)
487
+
488
+
489
+ # ---------------------------------------------------------------------------
490
+ # DCT4Basis
491
+ # ---------------------------------------------------------------------------
492
+
493
+
494
+ @dataclass
495
+ class DCT4Basis:
496
+ """DCT-IV tensor-network basis: the real-orthogonal, ancilla-free analogue
497
+ of :class:`QFTBasis`.
498
+
499
+ Mirrors QFTBasis (one tensor list; `inverse_transform` applies
500
+ `conj(tensors)` through `inv_code`). The gates are emitted by
501
+ :func:`pdft.bases.circuit.dct4.dct4_code`; at initialization the forward
502
+ operator is the bit-reversed orthonormal DCT-IV per dimension, and — since
503
+ DCT-IV is self-inverse — the basis reconstructs exactly. ``tensors`` holds
504
+ every gate — the affine ``R_y`` rotation layer, the branch Hadamards, the
505
+ mirror-``Q`` CNOT permutations and the ``Delta`` sign — each a learnable
506
+ leaf on its auto-selected Riemannian manifold (O(2) / O(4) / phase). The
507
+ gate tensors are real-valued (stored complex128, zero imaginary), so the
508
+ unitary manifold trains the real-orthogonal subset under a real objective:
509
+ exact DCT-IV at init, then relaxed, just as QFT relaxes within U.
510
+
511
+ Pytree contract:
512
+ leaves = tensors
513
+ aux data = (m, n, len(tensors), code, inv_code)
514
+ """
515
+
516
+ m: int
517
+ n: int
518
+ tensors: list[Array]
519
+ code: object = field(compare=False, repr=False)
520
+ inv_code: object = field(compare=False, repr=False)
521
+
522
+ def __init__(
523
+ self,
524
+ m: int,
525
+ n: int,
526
+ tensors: Sequence[Array] | None = None,
527
+ code: object | None = None,
528
+ inv_code: object | None = None,
529
+ parametrization: str = "o4",
530
+ ):
531
+ if m < 1 or n < 1:
532
+ raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
533
+ from .circuit.dct4 import dct4_code
534
+
535
+ self.m = m
536
+ self.n = n
537
+ _code, init_tensors = dct4_code(m, n, parametrization=parametrization)
538
+ _inv_code, _ = dct4_code(m, n, inverse=True, parametrization=parametrization)
539
+ self.tensors = list(tensors) if tensors is not None else init_tensors
540
+ self.code = code if code is not None else _code
541
+ self.inv_code = inv_code if inv_code is not None else _inv_code
542
+
543
+ @property
544
+ def inv_tensors(self) -> list[Array]:
545
+ return self.tensors
546
+
547
+ @property
548
+ def image_size(self) -> tuple[int, int]:
549
+ return (2**self.m, 2**self.n)
550
+
551
+ @property
552
+ def num_parameters(self) -> int:
553
+ return sum(int(t.size) for t in self.tensors)
554
+
555
+ def forward_transform(self, pic: Array) -> Array:
556
+ from .circuit.dct4 import dct4_ft_mat
557
+
558
+ return dct4_ft_mat(self.tensors, self.code, self.m, self.n, pic)
559
+
560
+ def inverse_transform(self, pic: Array) -> Array:
561
+ from .circuit.dct4 import dct4_ift_mat
562
+
563
+ return dct4_ift_mat(
564
+ [jnp.conj(t) for t in self.tensors],
565
+ self.inv_code,
566
+ self.m,
567
+ self.n,
568
+ pic,
569
+ )
570
+
571
+
572
+ def _dct4basis_flatten(b: DCT4Basis):
573
+ leaves = tuple(b.tensors)
574
+ aux = (b.m, b.n, len(b.tensors), b.code, b.inv_code)
575
+ return leaves, aux
576
+
577
+
578
+ def _dct4basis_unflatten(aux, leaves) -> DCT4Basis:
579
+ m, n, n_fwd, code, inv_code = aux
580
+ assert len(leaves) == n_fwd
581
+ out = DCT4Basis.__new__(DCT4Basis)
582
+ out.m = m
583
+ out.n = n
584
+ out.tensors = list(leaves)
585
+ out.code = code
586
+ out.inv_code = inv_code
587
+ return out
588
+
589
+
590
+ tree_util.register_pytree_node(DCT4Basis, _dct4basis_flatten, _dct4basis_unflatten)
@@ -0,0 +1,15 @@
1
+ """Block-structured bases: BlockedBasis.
2
+
3
+ RichBasis/RealRichBasis/fit_to_dct now live in bases.circuit (they are full
4
+ circuits); re-exported here for back-compat.
5
+ """
6
+
7
+ from ..circuit import RealRichBasis, RichBasis, fit_to_dct
8
+ from .block import BlockedBasis
9
+
10
+ __all__ = [
11
+ "BlockedBasis",
12
+ "RealRichBasis",
13
+ "RichBasis",
14
+ "fit_to_dct",
15
+ ]
@@ -0,0 +1,30 @@
1
+ """Circuit-topology bases: QFT, DCT-IV, EntangledQFT, TEBD, MERA, Rich, RealRich.
2
+
3
+ Rich/RealRich are H + U(4) circuits on the QFT topology — full-image
4
+ transforms usable standalone or as a BlockedBasis inner.
5
+ """
6
+
7
+ from .qft import ft_mat, ift_mat, qft_code
8
+ from .dct4 import dct4_code, dct4_ft_mat, dct4_ift_mat
9
+ from .entangled_qft import entangled_qft_code
10
+ from .mera import mera_code
11
+ from .tebd import tebd_code
12
+ from .rich import RichBasis, fit_to_dct
13
+ from .real_rich import RealRichBasis
14
+ from .freeze import freeze_as_blocked
15
+
16
+ __all__ = [
17
+ "RealRichBasis",
18
+ "RichBasis",
19
+ "dct4_code",
20
+ "dct4_ft_mat",
21
+ "dct4_ift_mat",
22
+ "entangled_qft_code",
23
+ "fit_to_dct",
24
+ "freeze_as_blocked",
25
+ "ft_mat",
26
+ "ift_mat",
27
+ "mera_code",
28
+ "qft_code",
29
+ "tebd_code",
30
+ ]
@@ -0,0 +1,189 @@
1
+ """DCT-IV circuit as a hand-rolled tensor network.
2
+
3
+ Real-orthogonal analogue of the QFT (cf. :mod:`pdft.bases.circuit.qft`). The
4
+ DCT-IV is the self-dual "bulk" cosine transform: real, orthogonal, and its
5
+ own inverse. Unlike the DCT-II it is **ancilla-free** and uses only 1- and
6
+ 2-qubit gates, so it drops straight into the H + 2-qubit-gate tensor-network
7
+ builder with no multi-controlled scaffolding.
8
+
9
+ The 1D recursion is *uniform* — both halves run the same child, so DCT-IV
10
+ unrolls into n qubit-local stages with no multi-controls (the real-orthogonal
11
+ analogue of the QFT butterfly). For N = 2M, with top qubit b splitting the
12
+ register (cf. ``all_real_dct_one_ancilla.tex`` §3.2):
13
+
14
+ C^IV_{2M} = P_{2M} (H (x) I_M) (I_M (+) D_M) (I_2 (x) C^IV_M)
15
+ (I_M (+) R_M) T_{2M} Q_{2M}
16
+
17
+ Per level k on the within-block qubits (b = top qubit), in emission order
18
+ (= the operator product above read right-to-left):
19
+
20
+ * Q pair layout : CX(control=b, target=q) for each q below b
21
+ * T rotation : R_y(pi / 2N) on b, then CR_y(pi 2^p / N) on b controlled
22
+ by each lower bit — the affine phase gradient (one base
23
+ rotation + one controlled rotation per bit, never a 2^m
24
+ multiplexer)
25
+ * R reverse : the mirror-Q permutation repeated (odd-branch reversal)
26
+ * recurse : C^IV_M on the lower register, uncontrolled (I_2 (x) C^IV_M)
27
+ * D sign Delta : CZ(control=b, target=next qubit)
28
+ * H merge : H on b
29
+
30
+ The interleave P_{2M} is deferred: per-level interleaves compose into a single
31
+ bit reversal, realised as the bit-reversed output convention (matching
32
+ :func:`qft_code`), so no explicit P gate is emitted.
33
+
34
+ At init the forward operator equals the bit-reversed orthonormal DCT-IV per
35
+ dimension. Every gate is a learnable leaf on its auto-selected Riemannian
36
+ manifold — R_y and branch-H on O(2), CR_y and mirror-Q on O(4), the Delta
37
+ sign on the phase manifold. Like QFT/RealRich, real init plus a real objective
38
+ keep the operator real-orthogonal as it relaxes from the exact transform (cf.
39
+ ``all_real_dct_zero_ancilla.tex``).
40
+
41
+ 2D DCT-IV = (m-qubit DCT-IV on row qubits) tensor (n-qubit DCT-IV on col
42
+ qubits); no entanglement between blocks.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from collections.abc import Callable
48
+
49
+ import jax
50
+ import jax.numpy as jnp
51
+ import numpy as np
52
+
53
+ from ...circuit.builder import (
54
+ HADAMARD,
55
+ Gate,
56
+ apply_circuit,
57
+ compile_circuit,
58
+ controlled_phase_diag,
59
+ )
60
+
61
+ Array = jax.Array
62
+
63
+
64
+ __all__ = [
65
+ "dct4_code",
66
+ "dct4_ft_mat",
67
+ "dct4_ift_mat",
68
+ "_dct4_gates_1d",
69
+ "_cry",
70
+ ]
71
+
72
+
73
+ def _ry(theta: float) -> Array:
74
+ """Single-qubit real R_y(theta) as a (2, 2) complex tensor `T[out, in]`."""
75
+ c, s = np.cos(theta / 2.0), np.sin(theta / 2.0)
76
+ return jnp.asarray(np.array([[c, -s], [s, c]]), dtype=jnp.complex128)
77
+
78
+
79
+ def _cnot_u4() -> Array:
80
+ """CNOT as a (2, 2, 2, 2) tensor `T[out_ctrl, out_tgt, in_ctrl, in_tgt]`."""
81
+ M = np.zeros((2, 2, 2, 2))
82
+ for ic in (0, 1):
83
+ for it in (0, 1):
84
+ M[ic, it ^ ic, ic, it] = 1.0
85
+ return jnp.asarray(M, dtype=jnp.complex128)
86
+
87
+
88
+ def _cry_u4(theta: float) -> Array:
89
+ """Controlled-R_y(theta) as a (2, 2, 2, 2) tensor (control fires on 1)."""
90
+ c, s = np.cos(theta / 2.0), np.sin(theta / 2.0)
91
+ R = np.array([[c, -s], [s, c]])
92
+ M = np.zeros((2, 2, 2, 2))
93
+ for it in (0, 1):
94
+ M[0, it, 0, it] = 1.0 # control = 0 -> identity on target
95
+ for ot in (0, 1):
96
+ for it in (0, 1):
97
+ M[1, ot, 1, it] = R[ot, it] # control = 1 -> R_y on target
98
+ return jnp.asarray(M, dtype=jnp.complex128)
99
+
100
+
101
+ def _cry(theta: float) -> Array:
102
+ """Controlled-R_y trainable leaf: just the (2, 2) R_y block.
103
+
104
+ Applied with control structure by the builder's ``CRY`` kind (identity on
105
+ control = 0, this block on control = 1), so the leaf trains on O(2) — the
106
+ structured, dense-O(4)-free parametrization of the twiddle.
107
+ """
108
+ return _ry(theta)
109
+
110
+
111
+ def _dct4_gates_1d(n_qubits: int, offset: int, parametrization: str = "o4") -> list[Gate]:
112
+ """Emit the 1D DCT-IV gate sequence on qubits (offset+1, ..., offset+n_qubits).
113
+
114
+ Within-block qubit ``q`` (0 = top/most significant) maps to builder qubit
115
+ ``offset + n_qubits - q`` (Yao little-endian), which reproduces the
116
+ bit-reversed orthonormal DCT-IV exactly.
117
+ """
118
+
119
+ def Q(q: int) -> int:
120
+ return offset + n_qubits - q
121
+
122
+ gates: list[Gate] = []
123
+
124
+ def level(k: int) -> None:
125
+ if k == n_qubits:
126
+ return
127
+ size = 2 ** (n_qubits - k)
128
+ b = k
129
+ lower = list(range(k + 1, n_qubits))
130
+ # Q: pair-layout permutation — controlled bit-complement of the lower
131
+ # register (exact CNOT at init; a learnable leaf like every gate)
132
+ for q in lower:
133
+ gates.append(Gate(kind="U4", qubits=(Q(b), Q(q)), tensor=_cnot_u4(), phase=0.0))
134
+ # T: affine R_y phase-gradient (one base R_y on b, one CR_y per lower bit)
135
+ gates.append(Gate(kind="H", qubits=(Q(b),), tensor=_ry(np.pi / (2 * size)), phase=0.0))
136
+ for p in range(n_qubits - 1 - k):
137
+ theta = np.pi * (2**p) / size
138
+ if parametrization == "controlled":
139
+ gates.append(Gate(kind="CRY", qubits=(Q(n_qubits - 1 - p), Q(b)),
140
+ tensor=_cry(theta), phase=0.0))
141
+ else:
142
+ gates.append(Gate(kind="U4", qubits=(Q(n_qubits - 1 - p), Q(b)),
143
+ tensor=_cry_u4(theta), phase=0.0))
144
+ # R: odd-branch reversal — the mirror-Q permutation repeated
145
+ for q in lower:
146
+ gates.append(Gate(kind="U4", qubits=(Q(b), Q(q)), tensor=_cnot_u4(), phase=0.0))
147
+ # recurse on the lower register (uncontrolled)
148
+ level(k + 1)
149
+ # D: Delta sign + H: branch Hadamard merge
150
+ if lower:
151
+ gates.append(
152
+ Gate(kind="CP", qubits=(Q(b), Q(k + 1)), tensor=controlled_phase_diag(float(np.pi)), phase=float(np.pi))
153
+ )
154
+ gates.append(Gate(kind="H", qubits=(Q(b),), tensor=HADAMARD, phase=0.0))
155
+
156
+ level(0)
157
+ return gates
158
+
159
+
160
+ def dct4_code(
161
+ m: int, n: int, *, inverse: bool = False, parametrization: str = "o4"
162
+ ) -> tuple[Callable[..., Array], list[Array]]:
163
+ """Return `(einsum_fn, initial_tensors)` for 2D DCT-IV on (2^m, 2^n) images.
164
+
165
+ ``parametrization`` selects how the affine twiddle is stored: ``"o4"``
166
+ (default) emits a dense ``(2, 2, 2, 2)`` controlled-R_y trained on O(4);
167
+ ``"controlled"`` emits a single-angle ``CRY`` gate whose trainable leaf is
168
+ a ``(2, 2)`` block on O(2) (the mirror-Q CNOTs stay dense U4).
169
+ """
170
+ if m < 1 or n < 1:
171
+ raise ValueError(f"m and n must be >= 1, got m={m}, n={n}")
172
+ if parametrization not in ("o4", "controlled"):
173
+ raise ValueError(
174
+ f"parametrization must be 'o4' or 'controlled', got {parametrization!r}"
175
+ )
176
+ gates = _dct4_gates_1d(m, offset=0, parametrization=parametrization) + _dct4_gates_1d(
177
+ n, offset=m, parametrization=parametrization
178
+ )
179
+ return compile_circuit(gates, m, n, inverse=inverse)
180
+
181
+
182
+ def dct4_ft_mat(tensors: list[Array], code: Callable, m: int, n: int, pic: Array) -> Array:
183
+ """Apply 2D DCT-IV circuit to a (2^m, 2^n) image."""
184
+ return apply_circuit(tensors, code, m, n, pic)
185
+
186
+
187
+ def dct4_ift_mat(tensors: list[Array], code: Callable, m: int, n: int, pic: Array) -> Array:
188
+ """Apply 2D inverse DCT-IV circuit. Caller must have conjugated the tensors."""
189
+ return apply_circuit(tensors, code, m, n, pic)
@@ -0,0 +1,116 @@
1
+ """freeze_as_blocked: reduce a full circuit to its blocked equivalent.
2
+
3
+ Resets the outer (block-index) gates of a QFT/Rich/RealRich circuit to
4
+ identity and returns the frozen tensor indices, so that
5
+
6
+ train_basis_batched(frozen_basis, frozen_indices=frozen, ...)
7
+
8
+ reproduces BlockedBasis(inner, block_log_m, block_log_n) training dynamics.
9
+ See docs/superpowers/specs/2026-05-24-circuit-rich-frozen-blocked-design.md.
10
+
11
+ Gradient-norm clipping note: frozen slots have their Euclidean gradient zeroed
12
+ before manifold projection, so their Riemannian gradient is zero and they
13
+ contribute zero to the global grad-norm. The clip factor therefore matches
14
+ BlockedBasis exactly — parity holds with max_grad_norm set (see
15
+ test_frozen_qft_outer_gates_matches_blocked_qft_training at max_grad_norm=1.0).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ import jax
23
+ import jax.numpy as jnp
24
+
25
+ from ...circuit.builder import controlled_phase_diag, sorted_gate_program
26
+
27
+ Array = jax.Array
28
+
29
+ __all__ = ["freeze_as_blocked"]
30
+
31
+
32
+ def _identity_for_kind(kind: str) -> Array:
33
+ """Identity-acting tensor for a gate kind, as complex128."""
34
+ if kind == "H":
35
+ return jnp.eye(2, dtype=jnp.complex128)
36
+ if kind == "U4":
37
+ return jnp.eye(4, dtype=jnp.complex128).reshape(2, 2, 2, 2)
38
+ if kind == "CP":
39
+ return controlled_phase_diag(0.0) # diag(1,1,1,1) in compact 2x2 form
40
+ raise AssertionError(f"unknown gate kind: {kind}")
41
+
42
+
43
+ def freeze_as_blocked(basis: Any, block_log_m: int, block_log_n: int) -> tuple[Any, list[int]]:
44
+ """Return ``(basis_copy_with_identity_outer_gates, frozen_indices)``.
45
+
46
+ Training the returned basis with ``frozen_indices`` reproduces
47
+ ``BlockedBasis(inner, block_log_m, block_log_n)`` training dynamics, where
48
+ ``inner`` is the same basis type at ``(m - block_log_m, n - block_log_n)``.
49
+ The outer (block-index) gates are reset to identity; inner gates keep the
50
+ input basis's tensor values.
51
+
52
+ Supports ``QFTBasis``, ``RichBasis``, ``RealRichBasis``.
53
+
54
+ ``block_log_m == block_log_n == 0`` is a valid no-op: no qubits are
55
+ block-index qubits, so nothing is frozen and ``frozen_indices`` is empty
56
+ (the returned basis is a tensor-copy of the input).
57
+ """
58
+ # Lazy imports avoid a real bases.base <-> bases.circuit import cycle.
59
+ # base.py runs `from .circuit.qft import qft_code` at module load; importing
60
+ # that submodule first executes bases/circuit/__init__.py, which imports
61
+ # THIS module. A module-top `from ..base import QFTBasis` would then touch
62
+ # base.py while it is still partially initialised (before QFTBasis exists)
63
+ # and raise ImportError. Verified: hoisting these to the top breaks
64
+ # `import pdft`. Keep them inside the function.
65
+ from ..base import QFTBasis
66
+ from .qft import _qft_gates_1d
67
+ from .real_rich import RealRichBasis, _real_rich_qft_gates_1d
68
+ from .rich import RichBasis, _rich_qft_gates_1d
69
+
70
+ gate_builders = {
71
+ QFTBasis: _qft_gates_1d,
72
+ RichBasis: _rich_qft_gates_1d,
73
+ RealRichBasis: _real_rich_qft_gates_1d,
74
+ }
75
+ btype = type(basis)
76
+ if btype not in gate_builders:
77
+ raise TypeError(
78
+ "freeze_as_blocked supports QFTBasis, RichBasis, RealRichBasis; "
79
+ f"got {btype.__name__}"
80
+ )
81
+ if block_log_m < 0 or block_log_n < 0:
82
+ raise ValueError(
83
+ "block_log_m and block_log_n must be >= 0; got "
84
+ f"block_log_m={block_log_m}, block_log_n={block_log_n}"
85
+ )
86
+ m, n = basis.m, basis.n
87
+ if m - block_log_m < 1 or n - block_log_n < 1:
88
+ raise ValueError(
89
+ f"block partition leaves no inner qubits: m={m}, n={n}, "
90
+ f"block_log_m={block_log_m}, block_log_n={block_log_n} "
91
+ "(need m - block_log_m >= 1 and n - block_log_n >= 1)"
92
+ )
93
+
94
+ # Block-index qubits: higher-numbered qubits per dim (Yao little-endian),
95
+ # matching BlockedBasis.
96
+ block_qubits = set(range(m - block_log_m + 1, m + 1)) | set(
97
+ range(m + n - block_log_n + 1, m + n + 1)
98
+ )
99
+
100
+ builder = gate_builders[btype]
101
+ gates = builder(m, offset=0) + builder(n, offset=m)
102
+ program = sorted_gate_program(gates)
103
+ if len(program) != len(basis.tensors):
104
+ raise AssertionError(
105
+ f"gate program length {len(program)} != tensor count {len(basis.tensors)}"
106
+ )
107
+
108
+ new_tensors = [jnp.array(t, copy=True) for t in basis.tensors]
109
+ frozen_indices: list[int] = []
110
+ for i, (kind, qubits) in enumerate(program):
111
+ if set(qubits) & block_qubits:
112
+ new_tensors[i] = _identity_for_kind(kind).astype(new_tensors[i].dtype)
113
+ frozen_indices.append(i)
114
+
115
+ new_basis = btype(m=m, n=n, tensors=new_tensors, code=basis.code, inv_code=basis.inv_code)
116
+ return new_basis, frozen_indices