pdft 0.2.2__tar.gz → 0.2.4__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 (134) hide show
  1. {pdft-0.2.2 → pdft-0.2.4}/.claude/CLAUDE.md +2 -2
  2. pdft-0.2.4/PKG-INFO +202 -0
  3. pdft-0.2.4/README.md +143 -0
  4. {pdft-0.2.2 → pdft-0.2.4}/pyproject.toml +25 -2
  5. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/__init__.py +13 -5
  6. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/__init__.py +18 -4
  7. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/base.py +124 -4
  8. pdft-0.2.4/src/pdft/bases/block/__init__.py +15 -0
  9. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/block/block.py +1 -1
  10. pdft-0.2.4/src/pdft/bases/circuit/__init__.py +30 -0
  11. pdft-0.2.4/src/pdft/bases/circuit/dct4.py +189 -0
  12. pdft-0.2.4/src/pdft/bases/circuit/freeze.py +116 -0
  13. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/circuit/mera.py +49 -16
  14. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/circuit/tebd.py +54 -39
  15. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/circuit/__init__.py +2 -0
  16. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/circuit/builder.py +56 -22
  17. pdft-0.2.4/src/pdft/coherence.py +203 -0
  18. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/io/serialize.py +1 -2
  19. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/loss.py +0 -2
  20. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/manifolds.py +13 -22
  21. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/optimizers/loop.py +1 -1
  22. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/profiling.py +1 -1
  23. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/batched.py +2 -2
  24. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/viz/circuit.py +2 -1
  25. pdft-0.2.4/tests/bases/circuit/test_dct4.py +186 -0
  26. pdft-0.2.4/tests/bases/circuit/test_freeze.py +169 -0
  27. pdft-0.2.4/tests/circuit/test_cry_gate.py +38 -0
  28. pdft-0.2.4/tests/circuit/test_sorted_gate_program.py +26 -0
  29. pdft-0.2.4/tests/test_coherence.py +147 -0
  30. {pdft-0.2.2 → pdft-0.2.4}/tests/test_manifolds.py +22 -0
  31. {pdft-0.2.2 → pdft-0.2.4}/tests/test_profiling.py +0 -1
  32. {pdft-0.2.2 → pdft-0.2.4}/tests/training/test_batched.py +26 -32
  33. {pdft-0.2.2 → pdft-0.2.4}/tests/viz/test_loss.py +7 -2
  34. pdft-0.2.2/PKG-INFO +0 -100
  35. pdft-0.2.2/README.md +0 -41
  36. pdft-0.2.2/src/pdft/bases/block/__init__.py +0 -12
  37. pdft-0.2.2/src/pdft/bases/circuit/__init__.py +0 -15
  38. {pdft-0.2.2 → pdft-0.2.4}/.github/workflows/ci.yml +0 -0
  39. {pdft-0.2.2 → pdft-0.2.4}/.github/workflows/publish.yml +0 -0
  40. {pdft-0.2.2 → pdft-0.2.4}/.github/workflows/verify-upstream-pin.yml +0 -0
  41. {pdft-0.2.2 → pdft-0.2.4}/.gitignore +0 -0
  42. {pdft-0.2.2 → pdft-0.2.4}/.pre-commit-config.yaml +0 -0
  43. {pdft-0.2.2 → pdft-0.2.4}/LICENSE +0 -0
  44. {pdft-0.2.2 → pdft-0.2.4}/Makefile +0 -0
  45. {pdft-0.2.2 → pdft-0.2.4}/examples/basis_demo.py +0 -0
  46. {pdft-0.2.2 → pdft-0.2.4}/examples/mera_demo.py +0 -0
  47. {pdft-0.2.2 → pdft-0.2.4}/examples/optimizer_benchmark.py +0 -0
  48. {pdft-0.2.2 → pdft-0.2.4}/reference/README.md +0 -0
  49. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/adam_trajectory_4x4.npz +0 -0
  50. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/basis_roundtrip.npz +0 -0
  51. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/basis_roundtrip_hash.txt +0 -0
  52. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/compression_roundtrip_4x4.npz +0 -0
  53. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/entangled_qft_4x4.npz +0 -0
  54. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/ft_mat_roundtrip.npz +0 -0
  55. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/loss_values.npz +0 -0
  56. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/manifest.json +0 -0
  57. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/manifold_project_retract.npz +0 -0
  58. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/mera_4x4.npz +0 -0
  59. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/phase_extraction_3x3.npz +0 -0
  60. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/qft_basis_trained.json +0 -0
  61. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/qft_code_16x16.npz +0 -0
  62. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/qft_code_4x4.npz +0 -0
  63. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/tebd_4x4.npz +0 -0
  64. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/train_trajectory_4x4.npz +0 -0
  65. {pdft-0.2.2 → pdft-0.2.4}/reference/goldens/train_trajectory_4x4_long.npz +0 -0
  66. {pdft-0.2.2 → pdft-0.2.4}/reference/julia/Manifest.toml +0 -0
  67. {pdft-0.2.2 → pdft-0.2.4}/reference/julia/Project.toml +0 -0
  68. {pdft-0.2.2 → pdft-0.2.4}/reference/julia/generate_goldens.jl +0 -0
  69. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/circuit/entangled_qft.py +2 -2
  70. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/bases/circuit/qft.py +2 -2
  71. {pdft-0.2.2/src/pdft/bases/block → pdft-0.2.4/src/pdft/bases/circuit}/real_rich.py +0 -0
  72. {pdft-0.2.2/src/pdft/bases/block → pdft-0.2.4/src/pdft/bases/circuit}/rich.py +0 -0
  73. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/circuit/cache.py +0 -0
  74. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/io/__init__.py +0 -0
  75. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/io/compression.py +0 -0
  76. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/optimizers/__init__.py +0 -0
  77. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/optimizers/adam.py +0 -0
  78. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/optimizers/core.py +0 -0
  79. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/optimizers/gd.py +0 -0
  80. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/__init__.py +0 -0
  81. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/adam_step.py +0 -0
  82. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/eval_loop.py +0 -0
  83. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/result.py +0 -0
  84. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/schedules.py +0 -0
  85. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/training/single.py +0 -0
  86. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/viz/__init__.py +1 -1
  87. {pdft-0.2.2 → pdft-0.2.4}/src/pdft/viz/loss.py +0 -0
  88. {pdft-0.2.2 → pdft-0.2.4}/tests/__init__.py +0 -0
  89. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/__init__.py +0 -0
  90. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/block/__init__.py +0 -0
  91. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/block/test_block.py +0 -0
  92. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/circuit/__init__.py +0 -0
  93. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/circuit/test_qft.py +0 -0
  94. {pdft-0.2.2/tests/bases/block → pdft-0.2.4/tests/bases/circuit}/test_real_rich.py +0 -0
  95. {pdft-0.2.2/tests/bases/block → pdft-0.2.4/tests/bases/circuit}/test_rich.py +0 -0
  96. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/conftest.py +0 -0
  97. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/test_base.py +0 -0
  98. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/test_new_bases.py +0 -0
  99. {pdft-0.2.2 → pdft-0.2.4}/tests/bases/test_phase_extraction.py +0 -0
  100. {pdft-0.2.2 → pdft-0.2.4}/tests/circuit/__init__.py +0 -0
  101. {pdft-0.2.2 → pdft-0.2.4}/tests/circuit/test_cache.py +0 -0
  102. {pdft-0.2.2 → pdft-0.2.4}/tests/circuit/test_h_inverse_leg_swap.py +0 -0
  103. {pdft-0.2.2 → pdft-0.2.4}/tests/circuit/test_high_qubit_circuit.py +0 -0
  104. {pdft-0.2.2 → pdft-0.2.4}/tests/circuit/test_legacy_builder.py +0 -0
  105. {pdft-0.2.2 → pdft-0.2.4}/tests/conftest.py +0 -0
  106. {pdft-0.2.2 → pdft-0.2.4}/tests/io/__init__.py +0 -0
  107. {pdft-0.2.2 → pdft-0.2.4}/tests/io/conftest.py +0 -0
  108. {pdft-0.2.2 → pdft-0.2.4}/tests/io/test_compression.py +0 -0
  109. {pdft-0.2.2 → pdft-0.2.4}/tests/io/test_serialize.py +0 -0
  110. {pdft-0.2.2 → pdft-0.2.4}/tests/optimizers/__init__.py +0 -0
  111. {pdft-0.2.2 → pdft-0.2.4}/tests/optimizers/conftest.py +0 -0
  112. {pdft-0.2.2 → pdft-0.2.4}/tests/optimizers/test_adam.py +0 -0
  113. {pdft-0.2.2 → pdft-0.2.4}/tests/optimizers/test_gd.py +0 -0
  114. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/__init__.py +0 -0
  115. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/conftest.py +0 -0
  116. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_adam.py +0 -0
  117. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_compression.py +0 -0
  118. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_long_run.py +0 -0
  119. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_loss.py +0 -0
  120. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_manifolds.py +0 -0
  121. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_new_bases.py +0 -0
  122. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_qft.py +0 -0
  123. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_scale.py +0 -0
  124. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_serialize.py +1 -1
  125. {pdft-0.2.2 → pdft-0.2.4}/tests/parity/test_training.py +0 -0
  126. {pdft-0.2.2 → pdft-0.2.4}/tests/test_basis_seeded_init.py +0 -0
  127. {pdft-0.2.2 → pdft-0.2.4}/tests/test_loss.py +0 -0
  128. {pdft-0.2.2 → pdft-0.2.4}/tests/test_smoke.py +0 -0
  129. {pdft-0.2.2 → pdft-0.2.4}/tests/training/__init__.py +0 -0
  130. {pdft-0.2.2 → pdft-0.2.4}/tests/training/conftest.py +0 -0
  131. {pdft-0.2.2 → pdft-0.2.4}/tests/training/test_eval_loop.py +0 -0
  132. {pdft-0.2.2 → pdft-0.2.4}/tests/training/test_integration.py +0 -0
  133. {pdft-0.2.2 → pdft-0.2.4}/tests/training/test_single.py +0 -0
  134. {pdft-0.2.2 → pdft-0.2.4}/tests/viz/__init__.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
pdft-0.2.4/PKG-INFO ADDED
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: pdft
3
+ Version: 0.2.4
4
+ Summary: Python port of ParametricDFT.jl: learning parametric quantum Fourier transforms via manifold optimization.
5
+ Project-URL: Homepage, https://github.com/zazabap/pdft
6
+ Project-URL: Issues, https://github.com/zazabap/pdft/issues
7
+ Project-URL: Upstream, https://github.com/nzy1997/ParametricDFT.jl
8
+ Author: zazabap
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 zazabap
12
+
13
+ Portions of this project are a Python port of ParametricDFT.jl
14
+ (https://github.com/nzy1997/ParametricDFT.jl), Copyright (c) 2025 nzy1997,
15
+ originally released under the MIT License.
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
34
+ License-File: LICENSE
35
+ Keywords: dft,fourier,jax,manifold-optimization,quantum,variational
36
+ Classifier: Development Status :: 2 - Pre-Alpha
37
+ Classifier: Intended Audience :: Science/Research
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Scientific/Engineering :: Physics
44
+ Requires-Python: >=3.11
45
+ Requires-Dist: jax>=0.10.0
46
+ Requires-Dist: typing-extensions>=4.5
47
+ Provides-Extra: dev
48
+ Requires-Dist: jaxtyping>=0.2; extra == 'dev'
49
+ Requires-Dist: matplotlib>=3.8; extra == 'dev'
50
+ Requires-Dist: numpy>=1.26; extra == 'dev'
51
+ Requires-Dist: pytest-cov>=4; extra == 'dev'
52
+ Requires-Dist: pytest>=7; extra == 'dev'
53
+ Requires-Dist: ruff<0.17,>=0.6; extra == 'dev'
54
+ Provides-Extra: gpu
55
+ Requires-Dist: jax[cuda12]>=0.10.0; extra == 'gpu'
56
+ Provides-Extra: plot
57
+ Requires-Dist: matplotlib>=3.8; extra == 'plot'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # pdft
61
+
62
+ [![arXiv](https://img.shields.io/badge/arXiv-2608.00053-b31b1b.svg)](https://arxiv.org/abs/2608.00053)
63
+
64
+ A Python port of [ParametricDFT.jl](https://github.com/nzy1997/ParametricDFT.jl):
65
+ learning parametric quantum Fourier transforms via manifold optimization. The
66
+ package implements a variational approach that approximates the Discrete
67
+ Fourier Transform (DFT) with parameterized quantum circuits.
68
+
69
+ This is the reference implementation accompanying the paper
70
+ [*Fast Trainable Multilinear Bases for Image Compression*](https://arxiv.org/abs/2608.00053)
71
+ (An, Ni, Zhou, Liu, 2026).
72
+
73
+ > Status: feature-complete port. All bases (QFT, entangled QFT, TEBD, MERA,
74
+ > Rich/RealRich, DCT-IV, blocked), both Riemannian optimizers (GD + Adam),
75
+ > training, JSON/compression I/O, and visualization are implemented, with
76
+ > parity against the Julia reference verified by committed goldens.
77
+
78
+ ## Installation
79
+
80
+ From PyPI (Python 3.11+):
81
+
82
+ ```bash
83
+ pip install "pdft>=0.2.3"
84
+ ```
85
+
86
+ > **Note:** the older `pdft==0.2.2` wheel predates `DCT4Basis` and the
87
+ > `parametrization="u4"` option of `TEBDBasis` / `MERABasis`, so it cannot run
88
+ > the paper's DCT-IV, TEBD-U4, or MERA-U4 configurations. If
89
+ > `pdft.__version__` reports `0.2.2`, upgrade with `pip install -U pdft`.
90
+
91
+ From source:
92
+
93
+ ```bash
94
+ git clone https://github.com/zazabap/pdft.git
95
+ cd pdft
96
+ pip install -e ".[dev]"
97
+ ```
98
+
99
+ ## Quick start
100
+
101
+ Train a parametric QFT basis on a target image with Riemannian gradient
102
+ descent:
103
+
104
+ ```python
105
+ import jax
106
+ import jax.numpy as jnp
107
+ import pdft
108
+
109
+ target = jax.random.normal(jax.random.PRNGKey(7), (4, 4)).astype(jnp.complex128)
110
+ basis = pdft.QFTBasis(m=2, n=2)
111
+
112
+ result = pdft.train_basis(
113
+ basis,
114
+ target=target,
115
+ loss=pdft.L1Norm(),
116
+ optimizer=pdft.RiemannianGD(lr=0.01),
117
+ steps=50,
118
+ seed=0,
119
+ )
120
+ print(result.loss_history[0], "->", result.loss_history[-1])
121
+ ```
122
+
123
+ Runnable demos live in [`examples/`](examples/) (each finishes in under
124
+ 10 seconds):
125
+
126
+ ```bash
127
+ python examples/basis_demo.py # train a QFTBasis, plot the loss
128
+ python examples/optimizer_benchmark.py # GD vs Adam comparison
129
+ python examples/mera_demo.py # MERA basis training
130
+ ```
131
+
132
+ ## Coherence with the pixel basis
133
+
134
+ Compression cares only how few coefficients a basis needs. Anything that
135
+ recovers an image from a *subset of its pixels* — inpainting, completion,
136
+ compressed sensing — is governed by a second quantity, and it is not sparsity:
137
+
138
+ mu(U) = N max_ij |U_ij|^2 in [1, N]
139
+
140
+ `mu = 1` is maximal incoherence with the pixel basis, the best case for
141
+ recovery from pointwise samples; `mu = N` is an atom living on one pixel,
142
+ invisible to any sample set that misses it.
143
+
144
+ Every basis here starts at `mu = 1`, and there is a structural reason it can
145
+ stay there: if the only non-diagonal gates are one Hadamard per wire, then
146
+ `|U_ij| = N^{-1/2}` for **every** parameter value, so `mu = 1` identically and
147
+ `sqrt(N) U` is a complex Hadamard matrix. Training the controlled-phase gates
148
+ arbitrarily hard, on any objective, cannot move it. Training the Hadamard /
149
+ `U(4)` gates can and does — randomising them on a `(3, 3)` `QFTBasis` reaches
150
+ `mu = 24.8` out of 64, and on `RichBasis`, which has no diagonal gates at all,
151
+ `mu = 32.5`.
152
+
153
+ `certify_flat_modulus` answers that before a run rather than measuring it
154
+ after, using the same `frozen_indices` that `train_basis_batched` takes:
155
+
156
+ ```python
157
+ from pdft.coherence import certify_flat_modulus, coherence, diagonal_tensor_indices
158
+
159
+ basis = pdft.QFTBasis(m=3, n=3)
160
+ coherence(basis) # 1.0
161
+
162
+ cert = certify_flat_modulus(basis) # nothing frozen
163
+ print(cert.holds, cert.reason) # False: the Hadamards are trainable
164
+
165
+ frozen = cert.offending_indices # exactly what must be held fixed
166
+ assert certify_flat_modulus(basis, frozen_indices=frozen)
167
+ result = pdft.train_basis_batched(basis, frozen_indices=frozen, ...)
168
+ ```
169
+
170
+ Freezing gates is a real trade — it removes the freedom that `RichBasis` and
171
+ `TEBDBasis` add for compression. The point is that the trade is now visible and
172
+ checkable, so it can be made deliberately per task.
173
+
174
+ ## Background
175
+
176
+ For the theory, see the paper:
177
+ - [Fast Trainable Multilinear Bases for Image Compression](https://arxiv.org/abs/2608.00053) (arXiv:2608.00053)
178
+
179
+ and the upstream notes:
180
+ - [`note/stepbystep.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/stepbystep.pdf)
181
+ - [`note/main.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/main.pdf)
182
+
183
+ ## Citation
184
+
185
+ If you use this package in your research, please cite:
186
+
187
+ ```bibtex
188
+ @misc{an2026fast,
189
+ title = {Fast Trainable Multilinear Bases for Image Compression},
190
+ author = {An, Shiwen and Ni, Zhongyi and Zhou, Huanhai and Liu, Jin-Guo},
191
+ year = {2026},
192
+ eprint = {2608.00053},
193
+ archivePrefix = {arXiv},
194
+ primaryClass = {eess.IV},
195
+ url = {https://arxiv.org/abs/2608.00053},
196
+ }
197
+ ```
198
+
199
+ ## License
200
+
201
+ MIT. See [LICENSE](LICENSE). This project is a derivative port of
202
+ ParametricDFT.jl (Copyright © 2025 nzy1997, MIT).
pdft-0.2.4/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # pdft
2
+
3
+ [![arXiv](https://img.shields.io/badge/arXiv-2608.00053-b31b1b.svg)](https://arxiv.org/abs/2608.00053)
4
+
5
+ A Python port of [ParametricDFT.jl](https://github.com/nzy1997/ParametricDFT.jl):
6
+ learning parametric quantum Fourier transforms via manifold optimization. The
7
+ package implements a variational approach that approximates the Discrete
8
+ Fourier Transform (DFT) with parameterized quantum circuits.
9
+
10
+ This is the reference implementation accompanying the paper
11
+ [*Fast Trainable Multilinear Bases for Image Compression*](https://arxiv.org/abs/2608.00053)
12
+ (An, Ni, Zhou, Liu, 2026).
13
+
14
+ > Status: feature-complete port. All bases (QFT, entangled QFT, TEBD, MERA,
15
+ > Rich/RealRich, DCT-IV, blocked), both Riemannian optimizers (GD + Adam),
16
+ > training, JSON/compression I/O, and visualization are implemented, with
17
+ > parity against the Julia reference verified by committed goldens.
18
+
19
+ ## Installation
20
+
21
+ From PyPI (Python 3.11+):
22
+
23
+ ```bash
24
+ pip install "pdft>=0.2.3"
25
+ ```
26
+
27
+ > **Note:** the older `pdft==0.2.2` wheel predates `DCT4Basis` and the
28
+ > `parametrization="u4"` option of `TEBDBasis` / `MERABasis`, so it cannot run
29
+ > the paper's DCT-IV, TEBD-U4, or MERA-U4 configurations. If
30
+ > `pdft.__version__` reports `0.2.2`, upgrade with `pip install -U pdft`.
31
+
32
+ From source:
33
+
34
+ ```bash
35
+ git clone https://github.com/zazabap/pdft.git
36
+ cd pdft
37
+ pip install -e ".[dev]"
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ Train a parametric QFT basis on a target image with Riemannian gradient
43
+ descent:
44
+
45
+ ```python
46
+ import jax
47
+ import jax.numpy as jnp
48
+ import pdft
49
+
50
+ target = jax.random.normal(jax.random.PRNGKey(7), (4, 4)).astype(jnp.complex128)
51
+ basis = pdft.QFTBasis(m=2, n=2)
52
+
53
+ result = pdft.train_basis(
54
+ basis,
55
+ target=target,
56
+ loss=pdft.L1Norm(),
57
+ optimizer=pdft.RiemannianGD(lr=0.01),
58
+ steps=50,
59
+ seed=0,
60
+ )
61
+ print(result.loss_history[0], "->", result.loss_history[-1])
62
+ ```
63
+
64
+ Runnable demos live in [`examples/`](examples/) (each finishes in under
65
+ 10 seconds):
66
+
67
+ ```bash
68
+ python examples/basis_demo.py # train a QFTBasis, plot the loss
69
+ python examples/optimizer_benchmark.py # GD vs Adam comparison
70
+ python examples/mera_demo.py # MERA basis training
71
+ ```
72
+
73
+ ## Coherence with the pixel basis
74
+
75
+ Compression cares only how few coefficients a basis needs. Anything that
76
+ recovers an image from a *subset of its pixels* — inpainting, completion,
77
+ compressed sensing — is governed by a second quantity, and it is not sparsity:
78
+
79
+ mu(U) = N max_ij |U_ij|^2 in [1, N]
80
+
81
+ `mu = 1` is maximal incoherence with the pixel basis, the best case for
82
+ recovery from pointwise samples; `mu = N` is an atom living on one pixel,
83
+ invisible to any sample set that misses it.
84
+
85
+ Every basis here starts at `mu = 1`, and there is a structural reason it can
86
+ stay there: if the only non-diagonal gates are one Hadamard per wire, then
87
+ `|U_ij| = N^{-1/2}` for **every** parameter value, so `mu = 1` identically and
88
+ `sqrt(N) U` is a complex Hadamard matrix. Training the controlled-phase gates
89
+ arbitrarily hard, on any objective, cannot move it. Training the Hadamard /
90
+ `U(4)` gates can and does — randomising them on a `(3, 3)` `QFTBasis` reaches
91
+ `mu = 24.8` out of 64, and on `RichBasis`, which has no diagonal gates at all,
92
+ `mu = 32.5`.
93
+
94
+ `certify_flat_modulus` answers that before a run rather than measuring it
95
+ after, using the same `frozen_indices` that `train_basis_batched` takes:
96
+
97
+ ```python
98
+ from pdft.coherence import certify_flat_modulus, coherence, diagonal_tensor_indices
99
+
100
+ basis = pdft.QFTBasis(m=3, n=3)
101
+ coherence(basis) # 1.0
102
+
103
+ cert = certify_flat_modulus(basis) # nothing frozen
104
+ print(cert.holds, cert.reason) # False: the Hadamards are trainable
105
+
106
+ frozen = cert.offending_indices # exactly what must be held fixed
107
+ assert certify_flat_modulus(basis, frozen_indices=frozen)
108
+ result = pdft.train_basis_batched(basis, frozen_indices=frozen, ...)
109
+ ```
110
+
111
+ Freezing gates is a real trade — it removes the freedom that `RichBasis` and
112
+ `TEBDBasis` add for compression. The point is that the trade is now visible and
113
+ checkable, so it can be made deliberately per task.
114
+
115
+ ## Background
116
+
117
+ For the theory, see the paper:
118
+ - [Fast Trainable Multilinear Bases for Image Compression](https://arxiv.org/abs/2608.00053) (arXiv:2608.00053)
119
+
120
+ and the upstream notes:
121
+ - [`note/stepbystep.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/stepbystep.pdf)
122
+ - [`note/main.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/main.pdf)
123
+
124
+ ## Citation
125
+
126
+ If you use this package in your research, please cite:
127
+
128
+ ```bibtex
129
+ @misc{an2026fast,
130
+ title = {Fast Trainable Multilinear Bases for Image Compression},
131
+ author = {An, Shiwen and Ni, Zhongyi and Zhou, Huanhai and Liu, Jin-Guo},
132
+ year = {2026},
133
+ eprint = {2608.00053},
134
+ archivePrefix = {arXiv},
135
+ primaryClass = {eess.IV},
136
+ url = {https://arxiv.org/abs/2608.00053},
137
+ }
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT. See [LICENSE](LICENSE). This project is a derivative port of
143
+ ParametricDFT.jl (Copyright © 2025 nzy1997, MIT).
@@ -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.4"
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"
@@ -30,7 +30,7 @@ dependencies = [
30
30
  dev = [
31
31
  "pytest>=7",
32
32
  "pytest-cov>=4",
33
- "ruff>=0.5",
33
+ "ruff>=0.6,<0.17",
34
34
  "numpy>=1.26",
35
35
  "jaxtyping>=0.2",
36
36
  "matplotlib>=3.8",
@@ -50,6 +50,29 @@ packages = ["src/pdft"]
50
50
  line-length = 100
51
51
  target-version = "py311"
52
52
 
53
+ [tool.ruff.lint]
54
+ # Declared deliberately, because ruff's *default* rule set widens between
55
+ # releases. With no `select` here and an unpinned `ruff>=0.5` in [dev], lint
56
+ # was a property of the release calendar rather than of the repository: CI
57
+ # went red on `main` with no source change when a newer ruff turned on rules
58
+ # 0.5 did not have (RUF100, I001, C408, B008, ...).
59
+ #
60
+ # This set is pyflakes + import order + pyupgrade + comprehensions: the rules
61
+ # that catch real defects, all of which the codebase already satisfies.
62
+ select = ["E4", "E7", "E9", "F", "I", "UP", "C4"]
63
+
64
+ # The package configures jax x64 mode before importing anything that reads it,
65
+ # which is a module-level import after a statement by construction. This is
66
+ # what the scattered `# noqa: E402` comments were for; ignoring the rule once
67
+ # here is the same intent without directives that later ruff versions then
68
+ # flag as unused.
69
+ ignore = ["E402"]
70
+
71
+ # Not enabled, but each is a reasonable thing to opt into later:
72
+ # "B" -> 24 findings, almost all `zip()` without `strict=`
73
+ # "RUF" -> 40 findings, mostly ambiguous-unicode in maths docstrings where
74
+ # the multiplication sign is intended, so it needs per-site review
75
+
53
76
  [tool.pytest.ini_options]
54
77
  testpaths = ["tests"]
55
78
  addopts = "-ra --strict-markers"
@@ -17,6 +17,7 @@ Public API: import from subpackages directly, e.g.
17
17
  from pdft.optimizers import RiemannianGD, RiemannianAdam
18
18
  from pdft.loss import L1Norm, MSELoss
19
19
  from pdft.io import save_basis, load_basis, compress
20
+ from pdft.coherence import coherence, certify_flat_modulus
20
21
 
21
22
  The names re-exported at the package root below are kept only for the
22
23
  small set most commonly used in interactive sessions and notebooks.
@@ -41,14 +42,15 @@ if not _os.environ.get("PDFT_DISABLE_COMPILE_CACHE"):
41
42
  except OSError:
42
43
  pass
43
44
 
44
- __version__ = "0.2.2"
45
+ __version__ = "0.2.4"
45
46
  __upstream_ref__ = "nzy1997/ParametricDFT.jl@a201a27e47df2f0f3ab460f83d49b6e5f5d1e9ef"
46
47
 
47
48
  # Slim public re-export hub: most-used names only. Anything else: import
48
49
  # from the relevant subpackage directly.
49
- from .bases import ( # noqa: E402
50
+ from .bases import (
50
51
  AbstractSparseBasis,
51
52
  BlockedBasis,
53
+ DCT4Basis,
52
54
  EntangledQFTBasis,
53
55
  MERABasis,
54
56
  QFTBasis,
@@ -56,15 +58,18 @@ from .bases import ( # noqa: E402
56
58
  RichBasis,
57
59
  TEBDBasis,
58
60
  bases_allclose,
61
+ freeze_as_blocked,
59
62
  )
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
+ from .coherence import certify_flat_modulus, coherence
64
+ from .loss import AbstractLoss, L1Norm, MSELoss, loss_function
65
+ from .optimizers import RiemannianAdam, RiemannianGD, optimize
66
+ from .training import TrainingResult, train_basis, train_basis_batched
63
67
 
64
68
  __all__ = [
65
69
  "AbstractLoss",
66
70
  "AbstractSparseBasis",
67
71
  "BlockedBasis",
72
+ "DCT4Basis",
68
73
  "EntangledQFTBasis",
69
74
  "L1Norm",
70
75
  "MERABasis",
@@ -79,6 +84,9 @@ __all__ = [
79
84
  "__upstream_ref__",
80
85
  "__version__",
81
86
  "bases_allclose",
87
+ "certify_flat_modulus",
88
+ "coherence",
89
+ "freeze_as_blocked",
82
90
  "loss_function",
83
91
  "optimize",
84
92
  "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)