pdft 0.2.4__tar.gz → 0.3.0__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.
- pdft-0.3.0/.claude/CLAUDE.md +272 -0
- pdft-0.3.0/.github/workflows/docs.yml +59 -0
- {pdft-0.2.4 → pdft-0.3.0}/.gitignore +6 -0
- pdft-0.3.0/Makefile +17 -0
- pdft-0.3.0/PKG-INFO +223 -0
- pdft-0.3.0/README.md +155 -0
- pdft-0.3.0/docs/_static/custom.css +34 -0
- pdft-0.3.0/docs/_static/favicon.png +0 -0
- pdft-0.3.0/docs/_static/favicon.svg +17 -0
- pdft-0.3.0/docs/_static/logo-dark.svg +17 -0
- pdft-0.3.0/docs/_static/logo-light.svg +17 -0
- pdft-0.3.0/docs/_static/mark.svg +17 -0
- pdft-0.3.0/docs/api/bases.rst +46 -0
- pdft-0.3.0/docs/api/circuit.rst +46 -0
- pdft-0.3.0/docs/api/coherence.rst +25 -0
- pdft-0.3.0/docs/api/index.rst +28 -0
- pdft-0.3.0/docs/api/io.rst +22 -0
- pdft-0.3.0/docs/api/loss.rst +23 -0
- pdft-0.3.0/docs/api/manifolds.rst +32 -0
- pdft-0.3.0/docs/api/optimizers.rst +20 -0
- pdft-0.3.0/docs/api/profiling.rst +19 -0
- pdft-0.3.0/docs/api/tasks.rst +27 -0
- pdft-0.3.0/docs/api/training.rst +21 -0
- pdft-0.3.0/docs/api/viz.rst +22 -0
- pdft-0.3.0/docs/conf.py +148 -0
- pdft-0.3.0/docs/index.md +96 -0
- pdft-0.3.0/docs/logo/build_logo.py +183 -0
- pdft-0.3.0/docs/paper.md +47 -0
- pdft-0.3.0/examples/README.txt +13 -0
- {pdft-0.2.4 → pdft-0.3.0}/examples/basis_demo.py +27 -3
- pdft-0.3.0/examples/completion_demo.py +145 -0
- {pdft-0.2.4 → pdft-0.3.0}/examples/mera_demo.py +5 -2
- {pdft-0.2.4 → pdft-0.3.0}/examples/optimizer_benchmark.py +5 -2
- {pdft-0.2.4 → pdft-0.3.0}/pyproject.toml +14 -1
- {pdft-0.2.4 → pdft-0.3.0}/reference/README.md +9 -0
- pdft-0.3.0/reference/completion/generate_golden.py +107 -0
- pdft-0.3.0/reference/completion/golden.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/__init__.py +8 -7
- pdft-0.3.0/src/pdft/bases/__init__.py +75 -0
- pdft-0.3.0/src/pdft/bases/base.py +184 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/bases/block/block.py +58 -108
- pdft-0.3.0/src/pdft/bases/circuit/__init__.py +40 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/bases/circuit/dct4.py +55 -62
- pdft-0.3.0/src/pdft/bases/circuit/entangled_qft.py +122 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/bases/circuit/freeze.py +14 -43
- pdft-0.3.0/src/pdft/bases/circuit/mera.py +137 -0
- pdft-0.3.0/src/pdft/bases/circuit/qft.py +92 -0
- pdft-0.3.0/src/pdft/bases/circuit/real_rich.py +76 -0
- pdft-0.3.0/src/pdft/bases/circuit/rich.py +150 -0
- pdft-0.3.0/src/pdft/bases/circuit/tebd.py +108 -0
- pdft-0.3.0/src/pdft/bases/core.py +361 -0
- pdft-0.3.0/src/pdft/circuit/__init__.py +72 -0
- pdft-0.3.0/src/pdft/circuit/builder.py +549 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/coherence.py +114 -11
- pdft-0.3.0/src/pdft/io/__init__.py +19 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/io/serialize.py +12 -12
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/loss.py +66 -24
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/manifolds.py +117 -80
- pdft-0.3.0/src/pdft/optimizers/adam.py +115 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/optimizers/core.py +31 -14
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/optimizers/loop.py +16 -28
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/profiling.py +18 -43
- pdft-0.3.0/src/pdft/tasks/__init__.py +38 -0
- pdft-0.3.0/src/pdft/tasks/completion.py +116 -0
- {pdft-0.2.4/src/pdft/io → pdft-0.3.0/src/pdft/tasks}/compression.py +21 -27
- pdft-0.3.0/src/pdft/training/__init__.py +26 -0
- pdft-0.3.0/src/pdft/training/adam_step.py +190 -0
- pdft-0.3.0/src/pdft/training/batched.py +323 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/training/eval_loop.py +1 -5
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/training/single.py +9 -18
- pdft-0.3.0/src/pdft/training/steps.py +182 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/viz/__init__.py +1 -1
- pdft-0.3.0/src/pdft/viz/_figure.py +20 -0
- pdft-0.3.0/src/pdft/viz/circuit.py +68 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/viz/loss.py +17 -27
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/block/test_block.py +13 -4
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/circuit/test_dct4.py +1 -6
- pdft-0.3.0/tests/bases/circuit/test_emitters.py +142 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/circuit/test_freeze.py +1 -14
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/circuit/test_rich.py +63 -3
- pdft-0.3.0/tests/bases/test_contracts.py +233 -0
- pdft-0.3.0/tests/bases/test_core.py +250 -0
- pdft-0.3.0/tests/bases/test_phase_view.py +209 -0
- pdft-0.3.0/tests/circuit/test_applier.py +240 -0
- pdft-0.3.0/tests/circuit/test_bit_reverse.py +53 -0
- pdft-0.3.0/tests/circuit/test_gates.py +147 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/circuit/test_high_qubit_circuit.py +6 -6
- pdft-0.3.0/tests/circuit/test_program.py +116 -0
- pdft-0.3.0/tests/helpers.py +122 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/io/test_serialize.py +8 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/optimizers/test_adam.py +40 -0
- pdft-0.3.0/tests/optimizers/test_loop.py +122 -0
- pdft-0.3.0/tests/parity/test_completion.py +94 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_compression.py +1 -1
- pdft-0.3.0/tests/tasks/test_completion.py +209 -0
- {pdft-0.2.4/tests/io → pdft-0.3.0/tests/tasks}/test_compression.py +41 -1
- {pdft-0.2.4 → pdft-0.3.0}/tests/test_basis_seeded_init.py +12 -0
- pdft-0.3.0/tests/test_coherence.py +314 -0
- pdft-0.3.0/tests/test_docs_api_pages.py +28 -0
- pdft-0.3.0/tests/test_loss.py +218 -0
- pdft-0.3.0/tests/test_manifolds.py +283 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/test_profiling.py +30 -0
- pdft-0.3.0/tests/training/test_adam_step.py +58 -0
- pdft-0.3.0/tests/training/test_batched.py +791 -0
- pdft-0.3.0/tests/training/test_steps.py +265 -0
- pdft-0.3.0/tests/viz/__init__.py +0 -0
- pdft-0.3.0/tests/viz/test_circuit.py +62 -0
- pdft-0.3.0/tests/viz/test_loss.py +63 -0
- pdft-0.2.4/.claude/CLAUDE.md +0 -184
- pdft-0.2.4/Makefile +0 -8
- pdft-0.2.4/PKG-INFO +0 -202
- pdft-0.2.4/README.md +0 -143
- pdft-0.2.4/src/pdft/bases/__init__.py +0 -54
- pdft-0.2.4/src/pdft/bases/base.py +0 -590
- pdft-0.2.4/src/pdft/bases/circuit/__init__.py +0 -30
- pdft-0.2.4/src/pdft/bases/circuit/entangled_qft.py +0 -137
- pdft-0.2.4/src/pdft/bases/circuit/mera.py +0 -200
- pdft-0.2.4/src/pdft/bases/circuit/qft.py +0 -84
- pdft-0.2.4/src/pdft/bases/circuit/real_rich.py +0 -164
- pdft-0.2.4/src/pdft/bases/circuit/rich.py +0 -265
- pdft-0.2.4/src/pdft/bases/circuit/tebd.py +0 -145
- pdft-0.2.4/src/pdft/circuit/__init__.py +0 -36
- pdft-0.2.4/src/pdft/circuit/builder.py +0 -411
- pdft-0.2.4/src/pdft/circuit/cache.py +0 -52
- pdft-0.2.4/src/pdft/io/__init__.py +0 -39
- pdft-0.2.4/src/pdft/optimizers/adam.py +0 -86
- pdft-0.2.4/src/pdft/training/__init__.py +0 -19
- pdft-0.2.4/src/pdft/training/adam_step.py +0 -158
- pdft-0.2.4/src/pdft/training/batched.py +0 -402
- pdft-0.2.4/src/pdft/viz/circuit.py +0 -86
- pdft-0.2.4/tests/circuit/test_cache.py +0 -23
- pdft-0.2.4/tests/circuit/test_legacy_builder.py +0 -146
- pdft-0.2.4/tests/circuit/test_sorted_gate_program.py +0 -26
- pdft-0.2.4/tests/test_coherence.py +0 -147
- pdft-0.2.4/tests/test_loss.py +0 -93
- pdft-0.2.4/tests/test_manifolds.py +0 -151
- pdft-0.2.4/tests/training/test_batched.py +0 -502
- pdft-0.2.4/tests/viz/test_loss.py +0 -49
- {pdft-0.2.4 → pdft-0.3.0}/.github/workflows/ci.yml +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/.github/workflows/publish.yml +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/.github/workflows/verify-upstream-pin.yml +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/.pre-commit-config.yaml +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/LICENSE +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/adam_trajectory_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/basis_roundtrip.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/basis_roundtrip_hash.txt +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/compression_roundtrip_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/entangled_qft_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/ft_mat_roundtrip.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/loss_values.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/manifest.json +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/manifold_project_retract.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/mera_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/phase_extraction_3x3.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/qft_basis_trained.json +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/qft_code_16x16.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/qft_code_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/tebd_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/train_trajectory_4x4.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/goldens/train_trajectory_4x4_long.npz +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/julia/Manifest.toml +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/julia/Project.toml +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/reference/julia/generate_goldens.jl +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/bases/block/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/optimizers/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/optimizers/gd.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/training/result.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/src/pdft/training/schedules.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/block/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/circuit/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/circuit/test_qft.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/circuit/test_real_rich.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/conftest.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/test_base.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/test_new_bases.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/bases/test_phase_extraction.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/circuit/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/circuit/test_cry_gate.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/circuit/test_h_inverse_leg_swap.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/conftest.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/io/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/io/conftest.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/optimizers/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/optimizers/conftest.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/optimizers/test_gd.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/conftest.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_adam.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_long_run.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_loss.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_manifolds.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_new_bases.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_qft.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_scale.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_serialize.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/parity/test_training.py +0 -0
- {pdft-0.2.4/tests/training → pdft-0.3.0/tests/tasks}/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/test_smoke.py +0 -0
- {pdft-0.2.4/tests/viz → pdft-0.3.0/tests/training}/__init__.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/training/conftest.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/training/test_eval_loop.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/training/test_integration.py +0 -0
- {pdft-0.2.4 → pdft-0.3.0}/tests/training/test_single.py +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Project-specific guidance for Claude Code working in this repo. Read this before making non-trivial changes — several conventions are *not* discoverable from the code without painful debugging, and at least one is documented here only because the original Python port spent hours rediscovering it.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`pdft` is a faithful Python port of [ParametricDFT.jl](https://github.com/nzy1997/ParametricDFT.jl) using JAX. Goal: a Python user can reproduce Julia results bit-for-bit (or within documented tolerances) on the same input. **Behavior parity with Julia is the primary correctness criterion** — never sacrifice it for Pythonic-ness.
|
|
8
|
+
|
|
9
|
+
- Upstream has four bases: QFT, EntangledQFT, TEBD and MERA. `RichBasis`, `RealRichBasis`, `DCT4Basis`, `BlockedBasis`, the `"u4"` parametrizations, `freeze_as_blocked` and `pdft.coherence` exist only here. For those there is no Julia behaviour to match and no golden; their property tests are the specification.
|
|
10
|
+
- Image completion (`pdft.tasks.completion`, `train_basis_steps`, the parameter views, the flat manifold) is not in upstream either. Its reference is the inpainting paper's own code (arXiv:2609.17298, the private `pdft-completion` repository): `reference/completion/golden.npz` is a run of it and `tests/parity/test_completion.py` holds the package to that run. Section 14 says what the golden can and cannot pin.
|
|
11
|
+
- Design spec: `docs/superpowers/specs/2026-04-24-pdft-migration-design.md` (referred to below as "the spec"; the file is not in this repository)
|
|
12
|
+
- Roadmap: GitHub issue #1
|
|
13
|
+
- Reliability hardening backlog: GitHub issue #2
|
|
14
|
+
- Upstream is pinned to `nzy1997/ParametricDFT.jl@a201a27e47df2f0f3ab460f83d49b6e5f5d1e9ef`. The pin lives in two places that must stay in sync: `reference/julia/generate_goldens.jl` (`UPSTREAM_SHA`) and `src/pdft/__init__.py` (`__upstream_ref__`).
|
|
15
|
+
|
|
16
|
+
## Critical conventions (do not break)
|
|
17
|
+
|
|
18
|
+
These are the non-obvious invariants that took the original port multiple iterations to find. Violating any of them silently breaks parity with Julia.
|
|
19
|
+
|
|
20
|
+
### 1. Wirtinger gradient conjugation
|
|
21
|
+
|
|
22
|
+
**JAX and Julia's Zygote return conjugate gradients** for real-valued functions of complex inputs: `jax.grad` gives `∂f/∂x − i ∂f/∂y` (twice `∂f/∂z`; for `|z|²` at `1+2i` it returns `2−4i`), Zygote gives `∂f/∂x + i ∂f/∂y` (twice `∂f/∂z̄`, the direction of steepest ascent). `optimize()` in `src/pdft/optimizers/loop.py` conjugates `raw_grads` immediately after `grad_fn(...)`:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
raw_grads = grad_fn(state.current_tensors)
|
|
26
|
+
raw_grads = [jnp.conj(g) for g in raw_grads] # MUST stay
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Without this line, GD trajectories drift ~10% over 50 steps and Adam outright diverges. The fused batched step in `training/adam_step.py` applies the same conjugation, right after its own `value_and_grad`. Those are the only two places a gradient is turned into an update (the trainers and `fit_to_dct` hand their gradient function to `optimize`); any new optimizer or training step must do the same, or define a custom JAX `vjp` that matches Julia's convention.
|
|
30
|
+
|
|
31
|
+
### 2. Yao little-endian qubit ordering
|
|
32
|
+
|
|
33
|
+
`pic.reshape((2,)*(m+n))` gives axes in big-endian bit order, but Julia's Yao treats qubit 1 as the LSB. `circuit.builder._axis_of_qubit` is where the applier reverses the qubit-to-axis mapping within each register:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
if 1 <= q <= m:
|
|
37
|
+
return m - q # axis 0 ↔ qubit m, axis m-1 ↔ qubit 1
|
|
38
|
+
if m + 1 <= q <= m + n:
|
|
39
|
+
return m + (m + n - q) # axis m ↔ qubit m+n, axis m+n-1 ↔ qubit m+1
|
|
40
|
+
raise ValueError(...)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The applier (`_walk`) reads every gate's axes through it, forward and inverse. Four other places assume the same layout without calling it and have to change with it: `BlockCode` (which axes index the blocks), `freeze_as_blocked` (which qubits index the blocks), `bit_reverse`, and the DCT-IV emitter's `Q()`. Don't undo this without first making the `ft_mat` parity tests fail in a controlled way.
|
|
44
|
+
|
|
45
|
+
### 3. Compact 2×2 CP tensor representation
|
|
46
|
+
|
|
47
|
+
A controlled-phase gate is `diag(1, 1, 1, exp(iφ))` as a 4×4 matrix, but Yao's `yao2einsum` emits the **compact 2×2 form**:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
controlled_phase_diag(φ) = [[1, 1], [1, exp(iφ)]]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The gate is diagonal, so each wire's value is unchanged and only a multiplicative phase is injected: the applier broadcasts the 2×2 tensor onto the control and target axes and multiplies, transposing it when the control sits on the higher axis. Every gate of kind `CP` uses this representation; never substitute the 4×4 form for one even though it would be mathematically equivalent — Julia goldens won't match. The dense form exists as its own gate kind (`U4`: Rich, RealRich, DCT4, and the `"u4"` parametrization of TEBD and MERA), a different parametrization on a different manifold, chosen on purpose.
|
|
54
|
+
|
|
55
|
+
### 4. Hadamard-first tensor sort
|
|
56
|
+
|
|
57
|
+
Julia's `qft_code` does `perm_vec = sortperm(tn.tensors, by=x -> !(x ≈ mat(H)))` — Hadamards come first, everything else after, in emission order. Python's `circuit.builder.compile_program` applies the same sort (`_hadamard_first_perm`), once, from the initial tensor values. A basis stores its tensors in that order; the `Program` keeps the gates in the order they act and `Program.slot` maps each gate to its stored tensor, so the sort never has to be redone or guessed (`Program.sorted_steps` gives `(kind, qubits)` per stored tensor). **JSON serialization assumes this order**; cross-language interop breaks if you change it.
|
|
58
|
+
|
|
59
|
+
### 5. JAX x64 mode at import time
|
|
60
|
+
|
|
61
|
+
`src/pdft/__init__.py` calls `jax.config.update("jax_enable_x64", True)` before any other import. `tests/conftest.py` does the same. Without x64, JAX uses complex64 / float32 and parity tolerances become unreachable. Any new test file that uses `jax.numpy` directly without importing `pdft` first must include the same call (or import pdft to force it).
|
|
62
|
+
|
|
63
|
+
### 6. Column-major iteration for cross-language hashes
|
|
64
|
+
|
|
65
|
+
`io.serialize.basis_hash` and `tasks.compression` use **Julia 1-based column-major** flat indices. NumPy is row-major by default. Use `array.flatten(order="F")` or `np.unravel_index(..., order="F")`. Never use plain `array.flatten()` for serialization.
|
|
66
|
+
|
|
67
|
+
### 7. Julia-compatible float formatting
|
|
68
|
+
|
|
69
|
+
Python's `repr(5e-7)` gives `"5e-07"`; Julia's `string(5e-7)` gives `"5.0e-7"`. `io.serialize.format_float_julia_like` post-processes `repr()` to match Julia. Use it (not `repr` or `str`) for any value that participates in a cross-language hash or JSON byte-comparison.
|
|
70
|
+
|
|
71
|
+
### 8. L1-cusp horizon for trajectory parity
|
|
72
|
+
|
|
73
|
+
GD on L1 loss agrees with Julia to about `1e-14` for ~50 steps (the parity tests assert `atol=1e-10`; it is not equal to the bit) and stays within `atol=1e-3` over 200 steps. Beyond that, FP accumulation eventually pushes Python and Julia across the same loss cusp at slightly different alphas in Armijo line search. Both still converge to the same loss basin. Don't tighten `tests/parity/test_long_run.py` to `atol=1e-10` over 200 steps — this is mathematically expected on non-smooth losses, not a bug. Smooth losses (MSE without truncation) don't have this problem.
|
|
74
|
+
|
|
75
|
+
### 9. The upstream phase extractors classify by tensor values
|
|
76
|
+
|
|
77
|
+
`get_*_gate_indices` (`select_last_n_cp_indices` under Julia's names) walks the tensor list and tags any 2×2 tensor whose four entries have unit-modulus magnitudes (within `atol=0.15`) as a CP gate, then returns the LAST `n_gates` such positions. After training, individual entries can drift slightly off the unit circle; the moderate tolerance accommodates that. If you tighten the tolerance, do so in tandem with a regression test on a trained basis.
|
|
78
|
+
|
|
79
|
+
These mirror upstream and are kept for parity with it, limits included: "the last `n` CP tensors" is the entangle layer only for `entangle_position="back"`, and a `"u4"` parametrization has no compact-CP tensors to find. New code should not use them. The program has the exact answer (§12): the entangle layer is `tensor_indices(kind="CP", register="both")` wherever it sits, and the ring or layer gates of TEBD and MERA are the two-qubit gates, `kind="CP"` or `kind="U4"` by parametrization. For a `BlockedBasis` go through `program_of(basis)`. The other value-based classifier that stays is `manifolds.classify_manifold`, which picks each tensor's manifold as upstream does; a test asserts it agrees with the gate kind at the initial tensors of every basis.
|
|
80
|
+
|
|
81
|
+
### 10. One applier, and every basis keeps its program
|
|
82
|
+
|
|
83
|
+
There is one arithmetic by which a circuit reaches an image: a family emits a gate list (`<family>_gates`), `compile_program` turns it into a `Program` (structure only, hashable) plus the tensors in stored order, and `CircuitCode(program, inverse, slices)` applies it one gate at a time through the jitted `_walk`. `CircuitCode` compares and hashes by its program, so two bases with the same circuit share one compiled applier and have equal pytree structures. The inverse is the same walk backwards with each gate's legs swapped (the transpose); the caller conjugates the tensors, as Julia's `inverse_code(conj.(tensors)...)` does. The entry points onto that walk are `apply_circuit` and `contract_circuit` (one image; §13), `apply_program` (leading batch axes, precision taken from the image), and `BlockCode`, which maps an inner code over the blocks of a `BlockedBasis`.
|
|
84
|
+
|
|
85
|
+
`CircuitBasis` (`bases/core.py`) stores `program` next to `tensors` and derives the rest for every family: `code`, `inv_code`, the transforms, the parameter count and the pytree. A new family is its emitter plus `emit = staticmethod(<family>_gates)` (or a constructor that calls `_init` when it has options of its own). A `code` passed to a constructor defines the circuit: when it is a `CircuitCode` the basis takes its program from it, and one passed alone (as `code` or as `inv_code`) brings its counterpart, the same code in the other direction, so a basis rebuilt with another instance's tensors and codes stays consistent. Do not add a second applier, a per-family `forward_transform`, a per-family pytree registration, or code that recovers the gate sequence from tensor values: read `basis.program`.
|
|
86
|
+
|
|
87
|
+
The default arithmetic (`tensordot` with `precision="highest"`) is bit-identical to what the package computed before the gate-centred refactor on a CPU, in both precisions, and on a GPU in double precision. On a GPU in single precision it is not, on purpose: without `precision="highest"` XLA runs a complex64 contraction in TF32, and the transforms and losses of Rich, RealRich and Blocked bases were off by about `1e-3`; they are now within `3e-7` of the double-precision result. `slices=True` is the same operator in different arithmetic (measured in double precision: on a GPU from 1.2 to 2.5 times faster for a 3+3-qubit QFT up to 10 times for 10+10 qubits, and 2 to 6 times for a gradient; on a CPU about even for large circuits and up to twice slower for small ones) and is opt-in because it changes the low bits. To use it, construct the basis with `code=dataclasses.replace(basis.code, slices=True)` (for a `BlockedBasis`, construct its inner basis so).
|
|
88
|
+
|
|
89
|
+
### 11. `QFTBasis` is the DFT of the bit-reversed image, with `e^{+2 pi i kx/N}`
|
|
90
|
+
|
|
91
|
+
Yao numbers qubits from the least significant bit and the QFT circuit has no final swap layer, so at its initial tensors
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
QFTBasis(m, n).forward_transform(bit_reverse(x)) == np.fft.ifft2(x, norm="ortho")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
where `circuit.bit_reverse` reverses the bits of the row index and of the column index (`Pi x Pi`, an involution). Two things to keep straight: the sign is numpy's *inverse* transform, and the frame is a fixed pixel permutation of the image. Neither is a bug and neither may be "fixed" in the basis: the Julia goldens encode both. Sparsity does not care about a pixel permutation. Anything defined on the pixels does (a sampling mask drawn from a seed, a figure): apply `bit_reverse` to the image going in and to the reconstruction coming out. `tests/circuit/test_bit_reverse.py` pins the identity.
|
|
98
|
+
|
|
99
|
+
### 12. Model variants are views of a basis, not new representations
|
|
100
|
+
|
|
101
|
+
A model that trains part of a circuit, or trains it through other parameters, is expressed on the basis that already exists:
|
|
102
|
+
|
|
103
|
+
- **Freeze by gate kind or register:** `basis.program.tensor_indices(kind="H")` (also `register="row" | "column" | "both"`) is a `frozen_indices` list. `program_of(basis)` reaches through a `BlockedBasis`.
|
|
104
|
+
- **Phase-only training:** `cp_phases(basis)` reads the controlled-phase angles as a real array and `with_cp_phases(basis, angles)` puts them back; `cp_diagonals` / `with_cp_diagonals` do the same for all four phases of each gate. Both are traceable, so a loss differentiates through them.
|
|
105
|
+
- **Parameter views:** a `ParameterView` (`bases/core.py`) is what a trainer moves: `read(basis)` gives a list of arrays, `write(basis, params)` a basis holding them, `manifolds(params)` the manifold of each or `None` for "classify by value as always". `tensors_view` is the tensors on their manifolds, the default everywhere; `cp_phases_view` and `cp_diagonals_view` are the angles as free real numbers on `EuclideanManifold`, read in double precision whatever the tensors' precision. On a flat view the package's one Adam update (`optimizers/adam.py::_adam_update`) is plain Adam as Kingma and Ba state it; there is no second update function, and there must not be one. Lowercase names on purpose: a constant `CP_PHASES` next to `cp_phases` collided on case-insensitive filesystems in the docs build.
|
|
106
|
+
- **Coherence:** `coherence.diagonal_tensor_indices` reads the gate kind from the program; `certify_flat_modulus` certifies `mu == 1` for a frozen configuration (it checks that the basis is flat now and that every trainable tensor is a `CP` gate, not that there is one Hadamard per wire, which holds for every basis here) and `sampled_flat_modulus` measures it.
|
|
107
|
+
|
|
108
|
+
Do not add a second way to hold a circuit's parameters (an angle vector with its own applier, a gate dict) and a bridge between the two. A new parametrization is a new view.
|
|
109
|
+
|
|
110
|
+
### 13. Two ways a transform reaches its circuit, on purpose
|
|
111
|
+
|
|
112
|
+
`apply_circuit` checks that the image is `(2**m, 2**n)` and casts it to complex128. `contract_circuit` does neither: any image with the right number of elements is reshaped, and the result has whatever precision the tensors and the image promote to.
|
|
113
|
+
|
|
114
|
+
- QFT, EntangledQFT, TEBD, MERA and DCT4 transforms use `apply_circuit`.
|
|
115
|
+
- Rich, RealRich and Blocked transforms, and `loss_function`, use `contract_circuit`. With single-precision tensors and a single-precision image they therefore compute in single precision: the transform returns complex64, and `loss_function` a float32 loss with complex64 gradients. A double-precision image promotes the result. Training follows the optimizer, in both trainers: `RiemannianGD` returns tensors in the precision they came in with, `RiemannianAdam` returns complex128 whatever went in (a tensor frozen during the run comes back as it went in). `train_basis_batched` also casts every image to complex128.
|
|
116
|
+
|
|
117
|
+
This asymmetry is how the package behaved before the refactor (`BasisTransforms._apply` now names it). It is not a convention worth defending, but unifying it changes results wherever single-precision tensors meet a single-precision image (`loss_function`, `train_basis`, the three transforms). It was once "cleaned up" by accident and no test noticed, because every test used double-precision tensors. `tests/bases/test_contracts.py` now pins both halves. Change it only as a decision, with the change declared.
|
|
118
|
+
|
|
119
|
+
### 14. Tasks work in the basis's own frame, and the step trainer owns the mask
|
|
120
|
+
|
|
121
|
+
A task (`pdft/tasks/`) takes a basis and data and reaches the circuit only through `forward_transform` and `inverse_transform`, so it runs on every basis, blocked ones included. `compress` / `recover` mirror upstream's `compression.jl`; `complete` (iterative hard thresholding from the observed pixels) and `completion_loss` are new. Four things to keep straight:
|
|
122
|
+
|
|
123
|
+
- **Frame.** Everything in `tasks` is in the basis's own frame, as `compress` always was. A mask lives on the pixels, so for a QFT-topology basis the caller bit-reverses the observation and the mask going in and the reconstruction coming out (§11). The frame is a property of the basis, not of the task: `DCT4Basis` and `BlockedBasis` lose several dB if bit-reversed. Do not put `bit_reverse` inside a task.
|
|
124
|
+
- **The mask belongs to `train_basis_steps`.** `train_basis_batched` mirrors Julia, where an example is an image and the loss a function of it; it never sees a mask and must not grow one. `train_basis_steps` (`training/steps.py`) runs a fixed number of Adam steps, each on a fresh batch under a fresh mask, drawn in the inpainting paper's order from one `np.random.default_rng(seed)`: the batch (`choice`, without replacement) and then its masks (`random(batch.shape) < rate`). That order is what makes a seed name the same batches and masks as in the paper's code; keep it. Its `frame=` maps batch and mask into the basis's frame. Below the loop nothing is separate: the fused step of `training/adam_step.py`, the one Adam update, the views.
|
|
125
|
+
- **The tie band.** At the Fourier point the coefficients of a real image come in conjugate pairs of equal magnitude; a cut that falls inside a pair (an even `k`, and at small sizes an odd `k` too, when a Nyquist bin enters the top `k`) was decided by rounding and changed the gradient by 1e-3 with the device and the arithmetic. `topk_truncate(x, k, rtol=)` counts magnitudes within `rtol` of the k-th largest as tied and settles ties by position; `complete` passes `sqrt(eps)`. The default `rtol=0` is upstream's rule, computed by upstream's two masks on the magnitudes as they are (a float band would round integers above 2**53); `MSELoss` and compression stay on it.
|
|
126
|
+
- **The barrier.** `complete` takes its real part as `jnp.real(jax.lax.optimization_barrier(z))`. With JAX 0.11.2 XLA carries a real or imaginary part back through the complex gates before it and compiling takes exponentially long in the depth of the circuit: 104 s against 0.1 s for a 16x16 `QFTBasis`, and a 30-minute CI job. The barrier changes no number; a test pins it because nothing numerical would notice its removal. The existing losses and trainers take magnitudes and are unaffected. Any new code that jits `jnp.real` or `jnp.imag` of a transform's output needs the same barrier.
|
|
127
|
+
|
|
128
|
+
The golden from the paper's code was generated with settings where no threshold in the run comes near a tie (smallest relative gap 3.7e-4): the paper's threshold keeps every magnitude that reaches the k-th, so `k + 1` at an exact tie, and the two codes agree only away from ties. The paper's own run differs between a CPU and a GPU by several percent when a tie is hit, so a golden from new settings must be checked that way first (`reference/completion/generate_golden.py` says how). With the same seed, odd `k` and the paper's settings at 128x128, `train_basis_steps` reproduces the paper's loss history to 2.5e-11 over 100 steps; the paper's Table 1 on DIV2K reproduces to 0.01 dB.
|
|
129
|
+
|
|
130
|
+
## Repo layout
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
src/pdft/
|
|
134
|
+
├── manifolds.py Mathematical core (UnitaryManifold, PhaseManifold, EuclideanManifold, batched ops)
|
|
135
|
+
├── loss.py L1Norm, MSELoss, topk_truncate (with the optional tie band), loss_function (public)
|
|
136
|
+
├── profiling.py Cross-cutting profiling helpers
|
|
137
|
+
├── bases/
|
|
138
|
+
│ ├── core.py CircuitBasis (program + tensors -> transforms, pytree), AbstractSparseBasis, bases_allclose, the parameter views
|
|
139
|
+
│ ├── base.py QFT, EntangledQFT, TEBD, MERA, DCT4 basis classes: each names its emitter
|
|
140
|
+
│ ├── circuit/ Gate emitters per family (qft, entangled_qft, tebd, mera, dct4, rich, real_rich) + freeze_as_blocked
|
|
141
|
+
│ └── block/ BlockedBasis (Rich/RealRich re-exported from circuit for back-compat)
|
|
142
|
+
├── circuit/ builder.py: Gate, Program, compile_program, CircuitCode (the one applier), gate constructors
|
|
143
|
+
├── coherence.py Mutual coherence of a basis and the flat-modulus certificate
|
|
144
|
+
├── optimizers/ core, gd (RiemannianGD + Armijo), adam (RiemannianAdam + the one Adam update both drivers use), loop
|
|
145
|
+
├── training/ schedules, single (train_basis), batched (one epoch loop; Adam and GD supply the batch and the step), steps (train_basis_steps: a fresh batch and mask per step), adam_step (the fused step both dataset trainers use), eval_loop, result (TrainingResult)
|
|
146
|
+
├── tasks/ compression (mirror of upstream compression.jl, with its JSON form), completion (complete, completion_loss)
|
|
147
|
+
├── io/ serialize (basis JSON)
|
|
148
|
+
└── viz/ loss (matplotlib loss plots), circuit (the gate sequence a basis keeps), _figure (shared import guard + save)
|
|
149
|
+
|
|
150
|
+
reference/julia/ Julia harness — needed only to regenerate goldens
|
|
151
|
+
reference/goldens/ Committed .npz + .json files (<200 KB total)
|
|
152
|
+
reference/completion/ The completion golden and the script that generated it in the paper's repository
|
|
153
|
+
examples/ 4 runnable demos, a few seconds each
|
|
154
|
+
tests/ pytest; mirrors src/pdft/ layout (tests/bases/, tests/optimizers/, ...)
|
|
155
|
+
docs/ Sphinx site (conf.py, index.md, api/*.rst, paper.md); deployed to GitHub Pages by docs.yml
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Benchmarks live in a separate repo: https://github.com/zazabap/pdft-benchmarks (split from this repo at pdft v0.2.0; pinned via its `pyproject.toml`).
|
|
159
|
+
|
|
160
|
+
`DCTBasis` was removed during the modular-src refactor (PR #11). For a cosine transform use `DCT4Basis`; `RichBasis` / `RealRichBasis` are the general parametric alternatives.
|
|
161
|
+
|
|
162
|
+
`pdft/__init__.py` re-exports a small set of most-used names (basis classes,
|
|
163
|
+
trainer, optimizer, loss). Less-public names live in their subpackages —
|
|
164
|
+
e.g. `pdft.io.save_basis`, `pdft.tasks.compress`, `pdft.manifolds.UnitaryManifold`,
|
|
165
|
+
`pdft.profiling.profile_training`, `pdft.bases.circuit.qft_code`.
|
|
166
|
+
|
|
167
|
+
`AbstractRiemannianOptimizer` is `RiemannianGD | RiemannianAdam` — a structural union, not a Protocol. Adding a third optimizer means extending that union, adding an `isinstance` branch in `optimize()`, and teaching `training/batched.py` about it (`_resolve_optimizer` and the step the epoch loop takes).
|
|
168
|
+
|
|
169
|
+
`train_basis` is generic over basis type via JAX pytree flatten/unflatten. The convention: the leaves of a basis pytree are its `tensors`, in order, and everything else (m, n, program, code, inv_code, counts) is aux data. There is no separate inverse tensor list: `inv_tensors` is the same list, and the inverse applies `conj(tensors)` through `inv_code`. `CircuitBasis` registers every subclass this way; `BlockedBasis` delegates to its inner basis. New basis types must follow this convention. `bases.with_tensors(basis, tensors)` is the one way to get a copy of a basis holding other tensors (the trainers, `freeze_as_blocked` and the parameter views all use it); don't re-derive it with `tree_flatten` / `tree_unflatten` or by calling the constructor again.
|
|
170
|
+
|
|
171
|
+
## Dev workflow
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Install (Python 3.11+)
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
|
|
177
|
+
# Run tests
|
|
178
|
+
pytest # full suite
|
|
179
|
+
pytest --cov=pdft --cov-fail-under=90 # CI gate
|
|
180
|
+
pytest tests/parity # parity-only (one more golden-backed test is tests/bases/test_phase_extraction.py; tests/parity/test_completion.py is against the paper's code, not Julia)
|
|
181
|
+
|
|
182
|
+
# Lint
|
|
183
|
+
ruff check src tests # the CI gate: fails the build if dirty
|
|
184
|
+
ruff format <the files you touched> # CI does not run it; four test files from before are unformatted, leave them
|
|
185
|
+
|
|
186
|
+
# Run examples
|
|
187
|
+
python examples/basis_demo.py
|
|
188
|
+
python examples/optimizer_benchmark.py
|
|
189
|
+
python examples/mera_demo.py
|
|
190
|
+
|
|
191
|
+
# Build the docs site (Sphinx + sphinx-book-theme; same -W command CI runs)
|
|
192
|
+
pip install -e ".[docs]"
|
|
193
|
+
make docs # -> docs/_build/html
|
|
194
|
+
|
|
195
|
+
# Regenerate Julia goldens (requires Julia 1.10+)
|
|
196
|
+
make goldens
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
After regenerating goldens: also update `__upstream_ref__` in `src/pdft/__init__.py` to match `manifest.json`'s `upstream_sha`.
|
|
200
|
+
|
|
201
|
+
## Tests and parity
|
|
202
|
+
|
|
203
|
+
Three layers (per spec section 7):
|
|
204
|
+
|
|
205
|
+
1. **Parity tests** (`tests/parity/test_*.py`) — load committed `.npz` / `.json` goldens from `reference/goldens/` and assert Python matches Julia. These are the load-bearing correctness tests. `test_completion.py` is the one exception: its golden is `reference/completion/golden.npz`, a run of the inpainting paper's code (§14).
|
|
206
|
+
2. **Property tests** (`tests/test_<module>.py` and `tests/<subpackage>/`) — math-invariant checks (unitarity preserved, round-trip identity, monotone descent, …). Don't depend on Julia. The test tree mirrors `src/`: `tests/circuit/` covers `circuit/builder.py` (`test_gates.py`, `test_program.py`, `test_applier.py`, `test_bit_reverse.py`, and three regression files that are older: `test_cry_gate.py`, `test_h_inverse_leg_swap.py`, `test_high_qubit_circuit.py`), `tests/bases/circuit/` the family emitters, `tests/bases/` the shared basis machinery. Name a test file after what it tests, not after the change that added it. `tests/circuit/test_applier.py` checks the gate walk against a single einsum, a second statement of what a gate list means, for every registered circuit the einsum can express (no controlled-rotation gate, not blocked) at random tensors. `tests/bases/test_contracts.py` runs what every basis has in common over one registry of basis configurations (`BASES` in `tests/helpers.py`), including the precision and shape each family's transforms accept and return (§13); add a new basis to that registry. `tests/helpers.py` also holds the random inputs several files share, and `PlainAdam`, the numpy oracle for the update on flat parameters: use them before writing another. `tests/tasks/` covers `pdft/tasks/`; `tests/training/test_steps.py` the step trainer.
|
|
207
|
+
3. **Smoke / integration** (`tests/test_smoke.py`, `tests/training/test_integration.py`).
|
|
208
|
+
|
|
209
|
+
Coverage gate is `--cov-fail-under=90`. Don't add tests that reduce per-module coverage below the line; if a new module legitimately needs more code, also add the property tests for it.
|
|
210
|
+
|
|
211
|
+
`tests/conftest.py` enables x64. Don't import jax in a test before pdft unless the test also calls `jax.config.update("jax_enable_x64", True)`.
|
|
212
|
+
|
|
213
|
+
## When to break parity
|
|
214
|
+
|
|
215
|
+
The default answer is "never". The only acceptable cases:
|
|
216
|
+
|
|
217
|
+
- **Float-precision noise**: test tolerances are documented per case; if you cross a tolerance boundary, investigate before relaxing.
|
|
218
|
+
- **Tie-breaking divergences** (e.g. argpartition vs partialsortperm): document and assert downstream behavior matches instead.
|
|
219
|
+
|
|
220
|
+
If you find another mismatch:
|
|
221
|
+
1. Diagnose with `reference/julia/` (write a small Julia script to dump intermediate values).
|
|
222
|
+
2. Compare to Python at the same intermediate point.
|
|
223
|
+
3. **The bug is in Python**, almost always.
|
|
224
|
+
4. If the bug is in upstream Julia, file an issue there and document the divergence in this file.
|
|
225
|
+
|
|
226
|
+
## Known defects
|
|
227
|
+
|
|
228
|
+
Found while refactoring and left as they are, because fixing each one changes behaviour:
|
|
229
|
+
|
|
230
|
+
- `DCT4Basis` is not exactly real: its sign gate is stored as `exp(i*pi)`, whose imaginary part is `1.2e-16`, and the optimisers amplify it (about `1e-2` in the tensors after twenty Adam steps on real images). `RealRichBasis`, whose tensors are exactly real, stays real.
|
|
231
|
+
- `TEBDBasis` with a one-qubit register (`m == 1` or `n == 1`) constructs and fails at its first transform: the ring gate lands on `(q, q)`.
|
|
232
|
+
- `==` on two bases raises `ValueError` as soon as it reaches two distinct arrays (the dataclass comparison reaches lists of arrays), and is `True` only when they share every tensor object, as two `QFTBasis(1, 1)` do. Use `bases_allclose`.
|
|
233
|
+
- JSON serialization is for `QFTBasis` only, and `basis_to_dict` does not refuse another basis: it labels it `"QFTBasis"`. Upstream serialises all four of its bases, and has an `entangle_position=:middle` that is not ported.
|
|
234
|
+
- `classify_manifold` goes by tensor values, as upstream does, so a unitary gate that has drifted too far is trained as a phase tensor. The test is `jnp.allclose(T T†, I, atol=1e-6)`, which adds a relative `1e-5`; upstream's `isapprox(t * t', I, atol=1e-6)` has no relative part, so the two disagree for a drift between those.
|
|
235
|
+
- On a GPU with single-precision tensors that same test runs in TF32 and misses unitarity by `2e-4`: Hadamards and dense gates are classified as phase tensors, a few steps of either optimizer then leave them far from unitary (round-trip error of order 10), and `DCT4Basis` fails to stack. Train in double precision on a GPU. The suite passes on a GPU without noticing. The flat views (`cp_phases_view`, `cp_diagonals_view`) name their manifold instead of classifying by value and are not affected.
|
|
236
|
+
- `topk_truncate` with `rtol > 0` compares through a float band, so integer inputs above `2**53` are rounded there; at `rtol=0` they are compared exactly, as on upstream.
|
|
237
|
+
- `complete` with a `QFTBasis` runs in double precision whatever the image's precision, because the QFT transforms cast (§13); with single-precision Rich tensors and a float32 image it runs in single precision.
|
|
238
|
+
- `certify_flat_modulus` does not check the proposition's "one Hadamard per wire"; a hand-built circuit with two on a wire is certified although `mu` can reach 2.
|
|
239
|
+
- Under `RiemannianGD` a frozen phase tensor can come back one rounding (`1e-16`) away from where it started; under Adam it is bit-identical.
|
|
240
|
+
- `pdft.coherence` at the package root is the function, which shadows the module: `from pdft.coherence import ...` works, `import pdft.coherence as c` gives the function.
|
|
241
|
+
- A code passed alone (`code=` or `inv_code=`) brings its counterpart only when it is a `CircuitCode`; any other callable is paired with the default circuit's.
|
|
242
|
+
- `profile_training` compiles its step twice: the record it tags as the first `"warm"` step includes a second compilation (about a thousand times the later steps), so drop it too when reading timings.
|
|
243
|
+
- `is_flat_modulus` compares with `jnp.allclose`, so `atol` is not the whole tolerance (a relative `1e-5` is added). `flat_modulus_deviation` gives the number.
|
|
244
|
+
|
|
245
|
+
## CI gotchas
|
|
246
|
+
|
|
247
|
+
- Ruff is strict (`F401`, `F841`, `E741`, `E731` all errors). `--fix` removes unused imports; the other three need a hand.
|
|
248
|
+
- Coverage gate runs only on the actual test suite, not examples; don't rely on examples for coverage.
|
|
249
|
+
- The `verify-upstream-pin.yml` workflow runs only on PRs touching `reference/` (or the workflow file itself). It uses the GitHub API to confirm the pinned sha exists in upstream — don't push a sha that's only in a fork.
|
|
250
|
+
- The matrix is 3.11 / 3.12 / 3.13. JAX requires Python 3.11 from 0.7.0; do not lower the floor.
|
|
251
|
+
- The matrix installs whatever JAX is current, and the jobs can get different versions (one run had 0.10.2 on 3.11 and 0.11.2 on the others). A green run that took 30 minutes on two jobs and 4 on the third was the real-part compile blow-up of §14. Look at job durations, not only at the ticks, and test under the newest JAX before pushing a change to anything that jits a transform.
|
|
252
|
+
- `docs.yml` deploys with `actions/deploy-pages`, which needs the repository's Pages source set to **GitHub Actions** (Settings → Pages, one-time). Until that is set the `deploy` job fails with a 404 while `build` stays green.
|
|
253
|
+
|
|
254
|
+
## What NOT to do
|
|
255
|
+
|
|
256
|
+
- **Don't add `optax`** as a dependency. The optimizer logic is hand-rolled to match Julia's exact moment-update math. `optax`'s defaults and FP order will diverge from Julia.
|
|
257
|
+
- **Don't bring back a whole-circuit einsum in the package.** Circuits are applied one gate at a time: no contraction path to search, no 52-label limit. The einsum form survives only as the oracle in `tests/circuit/test_applier.py`, which uses the `"greedy"` path; `"optimal"` is exponential in tensor count and hangs on the 3×3 QFT (12 tensors).
|
|
258
|
+
- **Don't add explicit JIT to `train_basis`.** It calls a basis-typed loss closure with Python-list pytrees; JIT decisions are best left to inner functions where the static-vs-leaf split is clearer.
|
|
259
|
+
- **Don't introduce backwards-compat shims** for the JSON schema. If the schema changes, bump its version and regenerate goldens.
|
|
260
|
+
- **Don't add ML scaffolding** (no DataLoader, no Trainer-like classes, no Lightning). `train_basis` is `optimize` on one target image, which is what the goldens harness runs (`reference/julia/generate_goldens.jl` calls `optimize!` directly; upstream has no one-image trainer), `train_basis_batched` is upstream's `train_basis` / `_train_basis_core`, and `train_basis_steps` is the inpainting paper's `adam_loop`; all three are plain functions.
|
|
261
|
+
- **Don't add a second optimizer for "plain" Adam, and don't add a second training loop for a new task.** Plain Adam is the Riemannian update on a flat view (§12); a task supplies an objective `(basis, images, masks) -> scalar` to `train_basis_steps`. The old completion stack (#34–#44) had its own kernel, bridge, Adam and loop, about 2,800 lines; #61 replaced it with about 600, and that is the shape to keep.
|
|
262
|
+
- **Don't jit `jnp.real` or `jnp.imag` of a transform's output without `jax.lax.optimization_barrier`** (§14).
|
|
263
|
+
- **Don't run examples in the test CI.** They write to `out/` (gitignored) and are not coverage-relevant. The one place they do run is the docs workflow (`docs.yml`): sphinx-gallery executes `examples/*.py` to render the example gallery, under `-W`, so a broken example fails the docs build rather than the test matrix.
|
|
264
|
+
|
|
265
|
+
## When making changes
|
|
266
|
+
|
|
267
|
+
1. Make the change.
|
|
268
|
+
2. Run `ruff check src tests`, and `ruff format` on the files you touched.
|
|
269
|
+
3. Run `pytest --cov=pdft --cov-fail-under=90`.
|
|
270
|
+
4. If you touched a circuit, basis, optimizer, or io module: also run `tests/parity` to confirm Julia-parity hasn't drifted. For a change meant to alter no numbers, compare old and new on the same machine before saying so: bytes, not `allclose`, on the CPU (on a GPU two runs of the same code need not agree to the bit), and with single-precision tensors as well as double (§13).
|
|
271
|
+
5. If you changed the upstream pin: update both `reference/julia/generate_goldens.jl` (`UPSTREAM_SHA`) and `src/pdft/__init__.py` (`__upstream_ref__`), regenerate goldens, and verify all parity tests still pass.
|
|
272
|
+
6. Push and watch the GitHub Actions matrix — three Python versions must all be green before merging.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
paths:
|
|
8
|
+
- docs/**
|
|
9
|
+
- examples/**
|
|
10
|
+
- README.md # docs/index.md includes its quick start
|
|
11
|
+
- src/**
|
|
12
|
+
- pyproject.toml
|
|
13
|
+
- Makefile # defines the `make docs` target this workflow runs
|
|
14
|
+
- .github/workflows/docs.yml
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
concurrency:
|
|
21
|
+
group: docs-${{ github.ref }}
|
|
22
|
+
# Cancel superseded PR builds, but never a main-branch run that may be
|
|
23
|
+
# mid-deploy: a cancelled deploy-pages step leaves a failed deployment.
|
|
24
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: actions/setup-python@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: "3.12"
|
|
34
|
+
cache: pip
|
|
35
|
+
- name: Install
|
|
36
|
+
run: pip install -e ".[docs]"
|
|
37
|
+
# The example gallery executes examples/*.py here, not in the test CI.
|
|
38
|
+
# A second attempt covers a transient intersphinx fetch failure, which
|
|
39
|
+
# Sphinx reports as an untyped warning that -W cannot be told to ignore.
|
|
40
|
+
- name: Build
|
|
41
|
+
run: make docs || (echo '::warning::first build failed, retrying once' && make docs)
|
|
42
|
+
- uses: actions/upload-pages-artifact@v3
|
|
43
|
+
if: github.ref == 'refs/heads/main'
|
|
44
|
+
with:
|
|
45
|
+
path: docs/_build/html
|
|
46
|
+
|
|
47
|
+
deploy:
|
|
48
|
+
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
|
49
|
+
needs: build
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
permissions:
|
|
52
|
+
pages: write
|
|
53
|
+
id-token: write
|
|
54
|
+
environment:
|
|
55
|
+
name: github-pages
|
|
56
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
57
|
+
steps:
|
|
58
|
+
- id: deployment
|
|
59
|
+
uses: actions/deploy-pages@v4
|
pdft-0.3.0/Makefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.PHONY: docs docs-clean goldens test
|
|
2
|
+
|
|
3
|
+
test:
|
|
4
|
+
pytest
|
|
5
|
+
|
|
6
|
+
goldens:
|
|
7
|
+
cd reference/julia && julia --project=. -e 'using Pkg; Pkg.instantiate()'
|
|
8
|
+
julia --project=reference/julia reference/julia/generate_goldens.jl
|
|
9
|
+
|
|
10
|
+
# Generated inputs (autosummary stubs, executed gallery) are removed first so a
|
|
11
|
+
# local -W build sees what CI's clean checkout sees: no stale stubs for
|
|
12
|
+
# renamed entries, no cached figures for changed examples.
|
|
13
|
+
docs-clean:
|
|
14
|
+
rm -rf docs/_build docs/api/generated docs/auto_examples docs/sg_execution_times.rst
|
|
15
|
+
|
|
16
|
+
docs: docs-clean
|
|
17
|
+
sphinx-build -W --keep-going -b html -d docs/_build/doctrees docs docs/_build/html
|
pdft-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pdft
|
|
3
|
+
Version: 0.3.0
|
|
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: Documentation, https://zazabap.github.io/pdft/
|
|
8
|
+
Project-URL: Upstream, https://github.com/nzy1997/ParametricDFT.jl
|
|
9
|
+
Author: zazabap
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 zazabap
|
|
13
|
+
|
|
14
|
+
Portions of this project are a Python port of ParametricDFT.jl
|
|
15
|
+
(https://github.com/nzy1997/ParametricDFT.jl), Copyright (c) 2025 nzy1997,
|
|
16
|
+
originally released under the MIT License.
|
|
17
|
+
|
|
18
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
19
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
20
|
+
in the Software without restriction, including without limitation the rights
|
|
21
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
22
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
23
|
+
furnished to do so, subject to the following conditions:
|
|
24
|
+
|
|
25
|
+
The above copyright notice and this permission notice shall be included in all
|
|
26
|
+
copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
29
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
30
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
31
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
32
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
33
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
34
|
+
SOFTWARE.
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Keywords: dft,fourier,jax,manifold-optimization,quantum,variational
|
|
37
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
38
|
+
Classifier: Intended Audience :: Science/Research
|
|
39
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
40
|
+
Classifier: Programming Language :: Python :: 3
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
45
|
+
Requires-Python: >=3.11
|
|
46
|
+
Requires-Dist: jax>=0.10.0
|
|
47
|
+
Requires-Dist: typing-extensions>=4.5
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: jaxtyping>=0.2; extra == 'dev'
|
|
50
|
+
Requires-Dist: matplotlib>=3.8; extra == 'dev'
|
|
51
|
+
Requires-Dist: numpy>=1.26; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-cov>=4; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
54
|
+
Requires-Dist: ruff<0.17,>=0.6; extra == 'dev'
|
|
55
|
+
Provides-Extra: docs
|
|
56
|
+
Requires-Dist: matplotlib>=3.8; extra == 'docs'
|
|
57
|
+
Requires-Dist: myst-parser<6,>=4; extra == 'docs'
|
|
58
|
+
Requires-Dist: sphinx-book-theme<2,>=1.1; extra == 'docs'
|
|
59
|
+
Requires-Dist: sphinx-copybutton<1,>=0.5; extra == 'docs'
|
|
60
|
+
Requires-Dist: sphinx-design<1,>=0.6; extra == 'docs'
|
|
61
|
+
Requires-Dist: sphinx-gallery<1,>=0.17; extra == 'docs'
|
|
62
|
+
Requires-Dist: sphinx<10,>=8; extra == 'docs'
|
|
63
|
+
Provides-Extra: gpu
|
|
64
|
+
Requires-Dist: jax[cuda12]>=0.10.0; extra == 'gpu'
|
|
65
|
+
Provides-Extra: plot
|
|
66
|
+
Requires-Dist: matplotlib>=3.8; extra == 'plot'
|
|
67
|
+
Description-Content-Type: text/markdown
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<picture>
|
|
71
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/zazabap/pdft/main/docs/_static/logo-dark.svg">
|
|
72
|
+
<img alt="pdft" src="https://raw.githubusercontent.com/zazabap/pdft/main/docs/_static/logo-light.svg" width="340">
|
|
73
|
+
</picture>
|
|
74
|
+
</p>
|
|
75
|
+
|
|
76
|
+
# pdft
|
|
77
|
+
|
|
78
|
+
[](https://arxiv.org/abs/2608.00053)
|
|
79
|
+
[](https://arxiv.org/abs/2609.17298)
|
|
80
|
+
[](https://zazabap.github.io/pdft/)
|
|
81
|
+
|
|
82
|
+
Trainable quantum-circuit transforms for images, in JAX. A circuit of
|
|
83
|
+
Hadamard and controlled-phase gates starts as the quantum Fourier transform;
|
|
84
|
+
its gates are trained so that images become sparse in the basis (compression),
|
|
85
|
+
or so that a sparse-recovery solver fills them in from a fraction of their
|
|
86
|
+
pixels (inpainting). Training moves the gates on their unitary manifolds, or
|
|
87
|
+
the phases alone as free numbers, and the circuit's structure keeps its
|
|
88
|
+
coherence with the pixel basis at the minimum throughout.
|
|
89
|
+
|
|
90
|
+
`pdft` is a Python port of [ParametricDFT.jl](https://github.com/nzy1997/ParametricDFT.jl)
|
|
91
|
+
and the reference implementation of the two papers above. The API reference
|
|
92
|
+
and example gallery are at [zazabap.github.io/pdft](https://zazabap.github.io/pdft/).
|
|
93
|
+
|
|
94
|
+
> Parity with the Julia reference is verified by committed goldens: for
|
|
95
|
+
> `QFTBasis`, the transform, the losses and top-k truncation, the manifold
|
|
96
|
+
> operations, both optimizers' trajectories, the JSON format and compression;
|
|
97
|
+
> for entangled QFT, TEBD and MERA, the forward transform at default options,
|
|
98
|
+
> and for entangled QFT the phase extraction. Rich/RealRich, DCT-IV, the
|
|
99
|
+
> blocked bases and everything under `pdft.tasks.completion` have no Julia
|
|
100
|
+
> counterpart: they are covered by property tests, and completion by a golden
|
|
101
|
+
> from the inpainting paper's own code. Not everything upstream exports is
|
|
102
|
+
> ported: JSON for bases other than `QFTBasis`, the `:middle` entangle
|
|
103
|
+
> position, the loss-history files, device transfer and some of the plots.
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
From PyPI (Python 3.11+):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install "pdft>=0.3.0"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **Note:** 0.3.0 is the first release with the completion task, the step
|
|
114
|
+
> trainer and the parameter views, and it moves `compress` / `recover` from
|
|
115
|
+
> `pdft.io` to `pdft.tasks`. The older `pdft==0.2.2` wheel predates
|
|
116
|
+
> `DCT4Basis` and the `parametrization="u4"` option of `TEBDBasis` /
|
|
117
|
+
> `MERABasis`.
|
|
118
|
+
|
|
119
|
+
From source:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git clone https://github.com/zazabap/pdft.git
|
|
123
|
+
cd pdft
|
|
124
|
+
pip install -e ".[dev]"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Quick start
|
|
128
|
+
|
|
129
|
+
Train a parametric QFT basis on a target image with Riemannian gradient
|
|
130
|
+
descent, the compression objective:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import jax
|
|
134
|
+
import jax.numpy as jnp
|
|
135
|
+
import pdft
|
|
136
|
+
|
|
137
|
+
target = jax.random.normal(jax.random.PRNGKey(7), (4, 4)).astype(jnp.complex128)
|
|
138
|
+
basis = pdft.QFTBasis(m=2, n=2)
|
|
139
|
+
|
|
140
|
+
result = pdft.train_basis(
|
|
141
|
+
basis,
|
|
142
|
+
target=target,
|
|
143
|
+
loss=pdft.L1Norm(),
|
|
144
|
+
optimizer=pdft.RiemannianGD(lr=0.01),
|
|
145
|
+
steps=50,
|
|
146
|
+
seed=0,
|
|
147
|
+
)
|
|
148
|
+
print(result.loss_history[0], "->", result.loss_history[-1])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Fill in an image from a tenth of its pixels, then train the phases of the
|
|
152
|
+
circuit through that solver (the inpainting paper's Model B):
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from pdft.bases import cp_diagonals_view
|
|
156
|
+
from pdft.circuit import bit_reverse
|
|
157
|
+
from pdft.tasks import complete, completion_loss
|
|
158
|
+
|
|
159
|
+
# image: a (64, 64) array in [0, 1]; mask: bool, True where a pixel was observed;
|
|
160
|
+
# images: a stack of training images. A QFTBasis works on the bit-reversed
|
|
161
|
+
# image, and a mask lives on the pixels, hence the reversals.
|
|
162
|
+
basis = pdft.QFTBasis(m=6, n=6)
|
|
163
|
+
filled = bit_reverse(complete(basis, bit_reverse(image * mask), bit_reverse(mask), k=61, steps=60))
|
|
164
|
+
|
|
165
|
+
result = pdft.train_basis_steps(
|
|
166
|
+
basis,
|
|
167
|
+
dataset=images, # in the image's own frame
|
|
168
|
+
objective=completion_loss(k=61, steps=10),
|
|
169
|
+
view=cp_diagonals_view, # the four phases of every gate, plain Adam
|
|
170
|
+
optimizer=pdft.RiemannianAdam(lr=0.02),
|
|
171
|
+
steps=60, rate=0.10, batch_size=2, seed=0,
|
|
172
|
+
frame=bit_reverse,
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Runnable demos live in [`examples/`](examples/) (each takes a few seconds):
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python examples/basis_demo.py # train a QFTBasis, plot the loss
|
|
180
|
+
python examples/optimizer_benchmark.py # GD vs Adam comparison
|
|
181
|
+
python examples/mera_demo.py # MERA basis training
|
|
182
|
+
python examples/completion_demo.py # fill in an image, train through the solver
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Coherence with the pixel basis
|
|
186
|
+
|
|
187
|
+
Compression cares only how few coefficients a basis needs. Recovering an
|
|
188
|
+
image from a *subset of its pixels* is governed by a second quantity, the
|
|
189
|
+
coherence `mu(U) = N max_ij |U_ij|^2` in `[1, N]`: `mu = 1` is maximal
|
|
190
|
+
incoherence with the pixel basis, `mu = N` an atom living on one pixel.
|
|
191
|
+
|
|
192
|
+
The QFT-family bases start at `mu = 1`, and there is a structural reason it
|
|
193
|
+
can stay there: if the only non-diagonal gates are one Hadamard per wire,
|
|
194
|
+
then `|U_ij| = N^{-1/2}` for every parameter value. Training the
|
|
195
|
+
controlled-phase gates, on any objective, cannot move `mu`; training the
|
|
196
|
+
Hadamard or `U(4)` gates can and does. `certify_flat_modulus` answers that
|
|
197
|
+
before a run, with the same `frozen_indices` the trainers take, and the
|
|
198
|
+
phase views (`cp_phases_view`, `cp_diagonals_view`) train nothing else:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from pdft.coherence import certify_flat_modulus
|
|
202
|
+
|
|
203
|
+
basis = pdft.QFTBasis(m=3, n=3)
|
|
204
|
+
cert = certify_flat_modulus(basis) # False: the Hadamards are trainable
|
|
205
|
+
frozen = cert.offending_indices # exactly what must be held fixed
|
|
206
|
+
assert certify_flat_modulus(basis, frozen_indices=frozen)
|
|
207
|
+
result = pdft.train_basis_batched(basis, frozen_indices=frozen, ...)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Background
|
|
211
|
+
|
|
212
|
+
- [Fast Trainable Multilinear Bases for Image Compression](https://arxiv.org/abs/2608.00053)
|
|
213
|
+
(An, Ni, Zhou, Liu, 2026): the bases, the losses, the Riemannian optimizers.
|
|
214
|
+
- [Quantum-Inspired Trainable and Parameter-Efficient Tensor Networks for Image Inpainting](https://arxiv.org/abs/2609.17298)
|
|
215
|
+
(An, Slavakis, 2026): the completion solver, training through it, and the
|
|
216
|
+
coherence argument.
|
|
217
|
+
- The upstream notes: [`note/stepbystep.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/stepbystep.pdf),
|
|
218
|
+
[`note/main.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/main.pdf).
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT. See [LICENSE](LICENSE). This project is a derivative port of
|
|
223
|
+
ParametricDFT.jl (Copyright © 2025 nzy1997, MIT).
|