pdft 0.2.3__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 (132) hide show
  1. pdft-0.2.4/PKG-INFO +202 -0
  2. pdft-0.2.4/README.md +143 -0
  3. {pdft-0.2.3 → pdft-0.2.4}/pyproject.toml +25 -2
  4. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/__init__.py +9 -5
  5. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/block/block.py +1 -1
  6. pdft-0.2.4/src/pdft/coherence.py +203 -0
  7. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/io/serialize.py +1 -2
  8. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/loss.py +0 -2
  9. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/optimizers/loop.py +1 -1
  10. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/profiling.py +1 -1
  11. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/batched.py +2 -2
  12. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/viz/circuit.py +2 -1
  13. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/circuit/test_freeze.py +14 -15
  14. pdft-0.2.4/tests/test_coherence.py +147 -0
  15. {pdft-0.2.3 → pdft-0.2.4}/tests/test_profiling.py +0 -1
  16. {pdft-0.2.3 → pdft-0.2.4}/tests/training/test_batched.py +18 -17
  17. {pdft-0.2.3 → pdft-0.2.4}/tests/viz/test_loss.py +7 -2
  18. pdft-0.2.3/PKG-INFO +0 -100
  19. pdft-0.2.3/README.md +0 -41
  20. {pdft-0.2.3 → pdft-0.2.4}/.claude/CLAUDE.md +0 -0
  21. {pdft-0.2.3 → pdft-0.2.4}/.github/workflows/ci.yml +0 -0
  22. {pdft-0.2.3 → pdft-0.2.4}/.github/workflows/publish.yml +0 -0
  23. {pdft-0.2.3 → pdft-0.2.4}/.github/workflows/verify-upstream-pin.yml +0 -0
  24. {pdft-0.2.3 → pdft-0.2.4}/.gitignore +0 -0
  25. {pdft-0.2.3 → pdft-0.2.4}/.pre-commit-config.yaml +0 -0
  26. {pdft-0.2.3 → pdft-0.2.4}/LICENSE +0 -0
  27. {pdft-0.2.3 → pdft-0.2.4}/Makefile +0 -0
  28. {pdft-0.2.3 → pdft-0.2.4}/examples/basis_demo.py +0 -0
  29. {pdft-0.2.3 → pdft-0.2.4}/examples/mera_demo.py +0 -0
  30. {pdft-0.2.3 → pdft-0.2.4}/examples/optimizer_benchmark.py +0 -0
  31. {pdft-0.2.3 → pdft-0.2.4}/reference/README.md +0 -0
  32. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/adam_trajectory_4x4.npz +0 -0
  33. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/basis_roundtrip.npz +0 -0
  34. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/basis_roundtrip_hash.txt +0 -0
  35. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/compression_roundtrip_4x4.npz +0 -0
  36. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/entangled_qft_4x4.npz +0 -0
  37. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/ft_mat_roundtrip.npz +0 -0
  38. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/loss_values.npz +0 -0
  39. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/manifest.json +0 -0
  40. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/manifold_project_retract.npz +0 -0
  41. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/mera_4x4.npz +0 -0
  42. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/phase_extraction_3x3.npz +0 -0
  43. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/qft_basis_trained.json +0 -0
  44. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/qft_code_16x16.npz +0 -0
  45. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/qft_code_4x4.npz +0 -0
  46. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/tebd_4x4.npz +0 -0
  47. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/train_trajectory_4x4.npz +0 -0
  48. {pdft-0.2.3 → pdft-0.2.4}/reference/goldens/train_trajectory_4x4_long.npz +0 -0
  49. {pdft-0.2.3 → pdft-0.2.4}/reference/julia/Manifest.toml +0 -0
  50. {pdft-0.2.3 → pdft-0.2.4}/reference/julia/Project.toml +0 -0
  51. {pdft-0.2.3 → pdft-0.2.4}/reference/julia/generate_goldens.jl +0 -0
  52. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/__init__.py +0 -0
  53. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/base.py +0 -0
  54. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/block/__init__.py +0 -0
  55. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/__init__.py +4 -4
  56. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/dct4.py +2 -2
  57. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/entangled_qft.py +2 -2
  58. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/freeze.py +0 -0
  59. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/mera.py +2 -2
  60. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/qft.py +2 -2
  61. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/real_rich.py +0 -0
  62. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/rich.py +0 -0
  63. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/bases/circuit/tebd.py +2 -2
  64. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/circuit/__init__.py +0 -0
  65. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/circuit/builder.py +0 -0
  66. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/circuit/cache.py +0 -0
  67. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/io/__init__.py +0 -0
  68. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/io/compression.py +0 -0
  69. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/manifolds.py +0 -0
  70. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/optimizers/__init__.py +0 -0
  71. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/optimizers/adam.py +0 -0
  72. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/optimizers/core.py +0 -0
  73. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/optimizers/gd.py +0 -0
  74. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/__init__.py +0 -0
  75. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/adam_step.py +0 -0
  76. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/eval_loop.py +0 -0
  77. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/result.py +0 -0
  78. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/schedules.py +0 -0
  79. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/training/single.py +0 -0
  80. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/viz/__init__.py +1 -1
  81. {pdft-0.2.3 → pdft-0.2.4}/src/pdft/viz/loss.py +0 -0
  82. {pdft-0.2.3 → pdft-0.2.4}/tests/__init__.py +0 -0
  83. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/__init__.py +0 -0
  84. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/block/__init__.py +0 -0
  85. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/block/test_block.py +0 -0
  86. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/circuit/__init__.py +0 -0
  87. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/circuit/test_dct4.py +0 -0
  88. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/circuit/test_qft.py +0 -0
  89. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/circuit/test_real_rich.py +0 -0
  90. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/circuit/test_rich.py +0 -0
  91. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/conftest.py +0 -0
  92. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/test_base.py +0 -0
  93. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/test_new_bases.py +0 -0
  94. {pdft-0.2.3 → pdft-0.2.4}/tests/bases/test_phase_extraction.py +0 -0
  95. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/__init__.py +0 -0
  96. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/test_cache.py +0 -0
  97. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/test_cry_gate.py +0 -0
  98. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/test_h_inverse_leg_swap.py +0 -0
  99. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/test_high_qubit_circuit.py +0 -0
  100. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/test_legacy_builder.py +0 -0
  101. {pdft-0.2.3 → pdft-0.2.4}/tests/circuit/test_sorted_gate_program.py +0 -0
  102. {pdft-0.2.3 → pdft-0.2.4}/tests/conftest.py +0 -0
  103. {pdft-0.2.3 → pdft-0.2.4}/tests/io/__init__.py +0 -0
  104. {pdft-0.2.3 → pdft-0.2.4}/tests/io/conftest.py +0 -0
  105. {pdft-0.2.3 → pdft-0.2.4}/tests/io/test_compression.py +0 -0
  106. {pdft-0.2.3 → pdft-0.2.4}/tests/io/test_serialize.py +0 -0
  107. {pdft-0.2.3 → pdft-0.2.4}/tests/optimizers/__init__.py +0 -0
  108. {pdft-0.2.3 → pdft-0.2.4}/tests/optimizers/conftest.py +0 -0
  109. {pdft-0.2.3 → pdft-0.2.4}/tests/optimizers/test_adam.py +0 -0
  110. {pdft-0.2.3 → pdft-0.2.4}/tests/optimizers/test_gd.py +0 -0
  111. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/__init__.py +0 -0
  112. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/conftest.py +0 -0
  113. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_adam.py +0 -0
  114. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_compression.py +0 -0
  115. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_long_run.py +0 -0
  116. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_loss.py +0 -0
  117. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_manifolds.py +0 -0
  118. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_new_bases.py +0 -0
  119. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_qft.py +0 -0
  120. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_scale.py +0 -0
  121. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_serialize.py +1 -1
  122. {pdft-0.2.3 → pdft-0.2.4}/tests/parity/test_training.py +0 -0
  123. {pdft-0.2.3 → pdft-0.2.4}/tests/test_basis_seeded_init.py +0 -0
  124. {pdft-0.2.3 → pdft-0.2.4}/tests/test_loss.py +0 -0
  125. {pdft-0.2.3 → pdft-0.2.4}/tests/test_manifolds.py +0 -0
  126. {pdft-0.2.3 → pdft-0.2.4}/tests/test_smoke.py +0 -0
  127. {pdft-0.2.3 → pdft-0.2.4}/tests/training/__init__.py +0 -0
  128. {pdft-0.2.3 → pdft-0.2.4}/tests/training/conftest.py +0 -0
  129. {pdft-0.2.3 → pdft-0.2.4}/tests/training/test_eval_loop.py +0 -0
  130. {pdft-0.2.3 → pdft-0.2.4}/tests/training/test_integration.py +0 -0
  131. {pdft-0.2.3 → pdft-0.2.4}/tests/training/test_single.py +0 -0
  132. {pdft-0.2.3 → pdft-0.2.4}/tests/viz/__init__.py +0 -0
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.3"
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,12 +42,12 @@ if not _os.environ.get("PDFT_DISABLE_COMPILE_CACHE"):
41
42
  except OSError:
42
43
  pass
43
44
 
44
- __version__ = "0.2.3"
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,
52
53
  DCT4Basis,
@@ -59,9 +60,10 @@ from .bases import ( # noqa: E402
59
60
  bases_allclose,
60
61
  freeze_as_blocked,
61
62
  )
62
- from .loss import AbstractLoss, L1Norm, MSELoss, loss_function # noqa: E402
63
- from .optimizers import RiemannianAdam, RiemannianGD, optimize # noqa: E402
64
- 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
65
67
 
66
68
  __all__ = [
67
69
  "AbstractLoss",
@@ -82,6 +84,8 @@ __all__ = [
82
84
  "__upstream_ref__",
83
85
  "__version__",
84
86
  "bases_allclose",
87
+ "certify_flat_modulus",
88
+ "coherence",
85
89
  "freeze_as_blocked",
86
90
  "loss_function",
87
91
  "optimize",
@@ -73,7 +73,7 @@ def _make_block_code(
73
73
  # axes (rows then cols) trailing. The trailing layout matches the inner
74
74
  # einsum's expected (2,)^(m_inner+n_inner) shape.
75
75
  perm = (
76
- list(range(0, block_log_m)) # block row axes
76
+ list(range(block_log_m)) # block row axes
77
77
  + list(range(m_outer, m_outer + block_log_n)) # block col axes
78
78
  + list(range(block_log_m, m_outer)) # within row axes
79
79
  + list(range(m_outer + block_log_n, m_outer + n_outer)) # within col axes
@@ -0,0 +1,203 @@
1
+ """Mutual coherence of a learned basis with the pixel basis.
2
+
3
+ For compression, the only thing that matters about a basis is how few
4
+ coefficients it needs. For any task that recovers an image from a *subset of
5
+ its pixels* --- inpainting, completion, compressed sensing --- a second
6
+ quantity governs the outcome, and it is not sparsity:
7
+
8
+ mu(U) = N * max_ij |U_ij|^2 in [1, N]
9
+
10
+ with N the operator dimension. mu = 1 is maximal incoherence with the pixel
11
+ basis, the most favourable case for recovery from pointwise samples; mu = N is
12
+ the worst, an atom supported on a single pixel, invisible to any sample set
13
+ that misses it. Sample-complexity bounds for recovery from random pixel
14
+ observations scale linearly in mu (Candes & Plan 2011, Krahmer & Ward 2014).
15
+
16
+ A trained basis can drift in mu while its compression loss improves, so the
17
+ quantity is worth being able to measure. This module measures it --- and, more
18
+ usefully, certifies when it cannot drift at all.
19
+
20
+ The guarantee
21
+ -------------
22
+ Let the circuit act on n wires, and suppose every gate is diagonal in the
23
+ computational basis except for exactly one Hadamard per wire. Then
24
+
25
+ |U_ij| = N^{-1/2} for every i, j and every parameter value,
26
+
27
+ so mu(U) = 1 identically over the whole parameter space, and sqrt(N) U is a
28
+ complex Hadamard matrix (Tadej & Zyczkowski 2006).
29
+
30
+ *Proof.* Track the amplitude vector produced by an input basis vector.
31
+ Diagonal gates multiply amplitudes by unit moduli and never move amplitude
32
+ between the two values of any wire; a permutation permutes. Consider the moment
33
+ the Hadamard on wire q is applied. Every earlier gate is diagonal or a Hadamard
34
+ on a different wire, so none has moved amplitude across wire q, and the vector
35
+ is supported on one value of it: one branch carries a, the other 0. Since
36
+ H (a, 0)^T = (a, a)^T / sqrt(2), the gate replaces each modulus by two equal
37
+ moduli |a|/sqrt(2) and never combines unequal ones. Each wire receives exactly
38
+ one Hadamard, so every amplitude ends at 2^{-n/2}, and the remaining diagonal
39
+ gates and permutations preserve moduli. QED
40
+
41
+ The practical consequence is a classification of this package's bases by which
42
+ gates are left free during training:
43
+
44
+ * free only the controlled-phase gates --- the compact-CP tensors --- and
45
+ mu is pinned at 1 no matter how hard the basis is trained, on any
46
+ objective, with no penalty, constraint or monitoring;
47
+ * free the Hadamard / U(4) gates as well and the guarantee is void. It is
48
+ void in practice, not only in principle: randomising them on an (m, n) =
49
+ (3, 3) QFTBasis reaches mu = 24.8 of a possible 64, and on RichBasis ---
50
+ which has no diagonal gates at all --- mu = 32.5.
51
+
52
+ `certify_flat_modulus` performs that check for a given training configuration,
53
+ taking the same `frozen_indices` that `train_basis_batched` accepts, so the
54
+ question "does this training run preserve incoherence?" can be answered before
55
+ the run rather than measured after it.
56
+
57
+ Discovered while applying this package's basis family to image *completion*,
58
+ where the reversal matters: the transform that compresses an image best is not
59
+ the one that completes it best.
60
+ """
61
+
62
+ from __future__ import annotations
63
+
64
+ from dataclasses import dataclass, field
65
+
66
+ import jax
67
+ import jax.numpy as jnp
68
+
69
+ from .circuit.builder import is_compact_cp
70
+
71
+ Array = jax.Array
72
+
73
+ __all__ = [
74
+ "FlatModulusCertificate",
75
+ "certify_flat_modulus",
76
+ "coherence",
77
+ "dense_operator",
78
+ "diagonal_tensor_indices",
79
+ "is_flat_modulus",
80
+ ]
81
+
82
+
83
+ def dense_operator(basis) -> Array:
84
+ """The basis as an explicit matrix acting on `vec(image)`.
85
+
86
+ Column j is the transform of the image that is 1 at pixel j and 0
87
+ elsewhere, so the result is (2^m 2^n) x (2^m 2^n).
88
+
89
+ This is a diagnostic, not a code path: it costs N^2 transforms of an
90
+ N-pixel image and is meant for small m, n. Nothing in training needs it ---
91
+ `certify_flat_modulus` gives the parameter-space guarantee without forming
92
+ it.
93
+ """
94
+ rows, cols = basis.image_size
95
+ dim = rows * cols
96
+ eye = jnp.eye(dim, dtype=jnp.complex128)
97
+ return jax.vmap(lambda e: basis.forward_transform(e.reshape(rows, cols)).reshape(-1))(eye).T
98
+
99
+
100
+ def coherence(basis, operator: Array | None = None) -> float:
101
+ """mu(U) = N max_ij |U_ij|^2, in [1, N]. 1 is maximal incoherence.
102
+
103
+ Pass `operator` to reuse a matrix from `dense_operator`.
104
+ """
105
+ u = dense_operator(basis) if operator is None else operator
106
+ return float(u.shape[0] * jnp.max(jnp.abs(u) ** 2))
107
+
108
+
109
+ def is_flat_modulus(basis, operator: Array | None = None, atol: float = 1e-8) -> bool:
110
+ """True if |U_ij| = N^{-1/2} everywhere, i.e. sqrt(N) U is complex Hadamard."""
111
+ u = dense_operator(basis) if operator is None else operator
112
+ return bool(jnp.allclose(jnp.abs(u), u.shape[0] ** -0.5, atol=atol))
113
+
114
+
115
+ def diagonal_tensor_indices(basis) -> list[int]:
116
+ """Indices of the tensors that are diagonal in the computational basis.
117
+
118
+ These are the compact controlled-phase gates, `[[1, 1], [1, e^{i phi}]]`,
119
+ whose only freedom is a phase. Freeing exactly these is what preserves
120
+ mu == 1; see the module docstring.
121
+ """
122
+ return [i for i, t in enumerate(basis.tensors) if is_compact_cp(t)]
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class FlatModulusCertificate:
127
+ """Whether mu == 1 holds over the whole reachable parameter set.
128
+
129
+ `holds` is the answer; `reason` says why in one line. `mu` is the coherence
130
+ at the basis's current parameters, and `offending_indices` names the
131
+ trainable tensors that are not diagonal, which are exactly the ones that
132
+ would have to be frozen for the guarantee to apply.
133
+ """
134
+
135
+ holds: bool
136
+ reason: str
137
+ mu: float
138
+ offending_indices: list[int] = field(default_factory=list)
139
+
140
+ def __bool__(self) -> bool:
141
+ return self.holds
142
+
143
+
144
+ def certify_flat_modulus(
145
+ basis,
146
+ frozen_indices: list[int] | None = None,
147
+ *,
148
+ atol: float = 1e-8,
149
+ ) -> FlatModulusCertificate:
150
+ """Certify that training cannot raise mu above 1.
151
+
152
+ Two conditions, both necessary. The basis must be flat-modulus *now*, and
153
+ every tensor left trainable must be diagonal --- then by the proposition in
154
+ the module docstring, mu == 1 for every value those parameters can take.
155
+
156
+ `frozen_indices` is the same argument `train_basis_batched` accepts, so:
157
+
158
+ cert = certify_flat_modulus(basis, frozen_indices=frozen)
159
+ if not cert:
160
+ raise ValueError(cert.reason)
161
+ result = train_basis_batched(basis, frozen_indices=frozen, ...)
162
+
163
+ Frozen non-diagonal gates are fine: a fixed Hadamard is what the
164
+ proposition assumes. It is *training* them that voids it.
165
+ """
166
+ operator = dense_operator(basis)
167
+ mu = coherence(basis, operator)
168
+
169
+ if not is_flat_modulus(basis, operator, atol=atol):
170
+ return FlatModulusCertificate(
171
+ holds=False,
172
+ reason=(
173
+ f"basis is not flat-modulus at its current parameters "
174
+ f"(mu = {mu:.6g}); the guarantee applies to circuits with one "
175
+ f"Hadamard per wire and diagonal gates elsewhere"
176
+ ),
177
+ mu=mu,
178
+ )
179
+
180
+ frozen = set(frozen_indices or [])
181
+ diagonal = set(diagonal_tensor_indices(basis))
182
+ offending = [i for i in range(len(basis.tensors)) if i not in frozen and i not in diagonal]
183
+
184
+ if offending:
185
+ return FlatModulusCertificate(
186
+ holds=False,
187
+ reason=(
188
+ f"mu = {mu:.6g} now, but {len(offending)} trainable tensor(s) "
189
+ f"are not diagonal, so training may raise it; freeze indices "
190
+ f"{offending} to obtain the guarantee"
191
+ ),
192
+ mu=mu,
193
+ offending_indices=offending,
194
+ )
195
+
196
+ return FlatModulusCertificate(
197
+ holds=True,
198
+ reason=(
199
+ f"mu == 1 for every reachable parameter value: all "
200
+ f"{len(basis.tensors) - len(frozen)} trainable tensor(s) are diagonal"
201
+ ),
202
+ mu=mu,
203
+ )
@@ -71,8 +71,7 @@ def format_float_julia_like(x) -> str:
71
71
  mantissa, exp = s.split("e", 1)
72
72
 
73
73
  # Python always includes sign in exponent (e+20, e-07); Julia drops '+'.
74
- if exp.startswith("+"):
75
- exp = exp[1:]
74
+ exp = exp.removeprefix("+")
76
75
 
77
76
  # Strip leading zeros in the exponent magnitude, but keep at least one digit.
78
77
  if exp.startswith("-"):
@@ -19,14 +19,12 @@ Array = jax.Array
19
19
  class AbstractLoss(Protocol):
20
20
  """Marker protocol for loss types. No behavior; dispatch is functional."""
21
21
 
22
- ...
23
22
 
24
23
 
25
24
  @dataclass(frozen=True)
26
25
  class L1Norm:
27
26
  """L1 norm loss: minimizes `sum(|T(x)|)` to encourage sparsity."""
28
27
 
29
- pass
30
28
 
31
29
 
32
30
  @dataclass(frozen=True)
@@ -17,7 +17,7 @@ Array = jax.Array
17
17
 
18
18
 
19
19
  def optimize(
20
- opt: "RiemannianGD | RiemannianAdam",
20
+ opt: RiemannianGD | RiemannianAdam,
21
21
  tensors: list[Array],
22
22
  loss_fn: Callable[[list[Array]], Array],
23
23
  grad_fn: Callable[[list[Array]], list[Array]],