pdft 0.2.1__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 (119) hide show
  1. pdft-0.2.1/.claude/CLAUDE.md +184 -0
  2. pdft-0.2.1/.github/workflows/ci.yml +26 -0
  3. pdft-0.2.1/.github/workflows/publish.yml +63 -0
  4. pdft-0.2.1/.github/workflows/verify-upstream-pin.yml +28 -0
  5. pdft-0.2.1/.gitignore +59 -0
  6. pdft-0.2.1/.pre-commit-config.yaml +7 -0
  7. pdft-0.2.1/LICENSE +25 -0
  8. pdft-0.2.1/Makefile +8 -0
  9. pdft-0.2.1/PKG-INFO +100 -0
  10. pdft-0.2.1/README.md +41 -0
  11. pdft-0.2.1/examples/basis_demo.py +49 -0
  12. pdft-0.2.1/examples/mera_demo.py +51 -0
  13. pdft-0.2.1/examples/optimizer_benchmark.py +50 -0
  14. pdft-0.2.1/pyproject.toml +63 -0
  15. pdft-0.2.1/reference/README.md +27 -0
  16. pdft-0.2.1/reference/goldens/adam_trajectory_4x4.npz +0 -0
  17. pdft-0.2.1/reference/goldens/basis_roundtrip.npz +0 -0
  18. pdft-0.2.1/reference/goldens/basis_roundtrip_hash.txt +1 -0
  19. pdft-0.2.1/reference/goldens/compression_roundtrip_4x4.npz +0 -0
  20. pdft-0.2.1/reference/goldens/entangled_qft_4x4.npz +0 -0
  21. pdft-0.2.1/reference/goldens/ft_mat_roundtrip.npz +0 -0
  22. pdft-0.2.1/reference/goldens/loss_values.npz +0 -0
  23. pdft-0.2.1/reference/goldens/manifest.json +24 -0
  24. pdft-0.2.1/reference/goldens/manifold_project_retract.npz +0 -0
  25. pdft-0.2.1/reference/goldens/mera_4x4.npz +0 -0
  26. pdft-0.2.1/reference/goldens/phase_extraction_3x3.npz +0 -0
  27. pdft-0.2.1/reference/goldens/qft_basis_trained.json +117 -0
  28. pdft-0.2.1/reference/goldens/qft_code_16x16.npz +0 -0
  29. pdft-0.2.1/reference/goldens/qft_code_4x4.npz +0 -0
  30. pdft-0.2.1/reference/goldens/tebd_4x4.npz +0 -0
  31. pdft-0.2.1/reference/goldens/train_trajectory_4x4.npz +0 -0
  32. pdft-0.2.1/reference/goldens/train_trajectory_4x4_long.npz +0 -0
  33. pdft-0.2.1/reference/julia/Manifest.toml +2244 -0
  34. pdft-0.2.1/reference/julia/Project.toml +13 -0
  35. pdft-0.2.1/reference/julia/generate_goldens.jl +328 -0
  36. pdft-0.2.1/src/pdft/__init__.py +86 -0
  37. pdft-0.2.1/src/pdft/bases/__init__.py +40 -0
  38. pdft-0.2.1/src/pdft/bases/base.py +470 -0
  39. pdft-0.2.1/src/pdft/bases/block/__init__.py +12 -0
  40. pdft-0.2.1/src/pdft/bases/block/block.py +261 -0
  41. pdft-0.2.1/src/pdft/bases/block/real_rich.py +164 -0
  42. pdft-0.2.1/src/pdft/bases/block/rich.py +265 -0
  43. pdft-0.2.1/src/pdft/bases/circuit/__init__.py +15 -0
  44. pdft-0.2.1/src/pdft/bases/circuit/entangled_qft.py +137 -0
  45. pdft-0.2.1/src/pdft/bases/circuit/mera.py +167 -0
  46. pdft-0.2.1/src/pdft/bases/circuit/qft.py +84 -0
  47. pdft-0.2.1/src/pdft/bases/circuit/tebd.py +130 -0
  48. pdft-0.2.1/src/pdft/circuit/__init__.py +34 -0
  49. pdft-0.2.1/src/pdft/circuit/builder.py +242 -0
  50. pdft-0.2.1/src/pdft/circuit/cache.py +52 -0
  51. pdft-0.2.1/src/pdft/io/__init__.py +39 -0
  52. pdft-0.2.1/src/pdft/io/compression.py +189 -0
  53. pdft-0.2.1/src/pdft/io/serialize.py +221 -0
  54. pdft-0.2.1/src/pdft/loss.py +159 -0
  55. pdft-0.2.1/src/pdft/manifolds.py +350 -0
  56. pdft-0.2.1/src/pdft/optimizers/__init__.py +19 -0
  57. pdft-0.2.1/src/pdft/optimizers/adam.py +86 -0
  58. pdft-0.2.1/src/pdft/optimizers/core.py +61 -0
  59. pdft-0.2.1/src/pdft/optimizers/gd.py +68 -0
  60. pdft-0.2.1/src/pdft/optimizers/loop.py +104 -0
  61. pdft-0.2.1/src/pdft/profiling.py +292 -0
  62. pdft-0.2.1/src/pdft/training/__init__.py +19 -0
  63. pdft-0.2.1/src/pdft/training/adam_step.py +131 -0
  64. pdft-0.2.1/src/pdft/training/batched.py +341 -0
  65. pdft-0.2.1/src/pdft/training/eval_loop.py +57 -0
  66. pdft-0.2.1/src/pdft/training/result.py +17 -0
  67. pdft-0.2.1/src/pdft/training/schedules.py +27 -0
  68. pdft-0.2.1/src/pdft/training/single.py +69 -0
  69. pdft-0.2.1/src/pdft/viz/__init__.py +24 -0
  70. pdft-0.2.1/src/pdft/viz/circuit.py +85 -0
  71. pdft-0.2.1/src/pdft/viz/loss.py +119 -0
  72. pdft-0.2.1/tests/__init__.py +0 -0
  73. pdft-0.2.1/tests/bases/__init__.py +0 -0
  74. pdft-0.2.1/tests/bases/block/__init__.py +0 -0
  75. pdft-0.2.1/tests/bases/block/test_block.py +217 -0
  76. pdft-0.2.1/tests/bases/block/test_real_rich.py +132 -0
  77. pdft-0.2.1/tests/bases/block/test_rich.py +211 -0
  78. pdft-0.2.1/tests/bases/circuit/__init__.py +0 -0
  79. pdft-0.2.1/tests/bases/circuit/test_qft.py +90 -0
  80. pdft-0.2.1/tests/bases/conftest.py +11 -0
  81. pdft-0.2.1/tests/bases/test_base.py +66 -0
  82. pdft-0.2.1/tests/bases/test_new_bases.py +170 -0
  83. pdft-0.2.1/tests/bases/test_phase_extraction.py +63 -0
  84. pdft-0.2.1/tests/circuit/__init__.py +0 -0
  85. pdft-0.2.1/tests/circuit/test_cache.py +23 -0
  86. pdft-0.2.1/tests/conftest.py +31 -0
  87. pdft-0.2.1/tests/io/__init__.py +0 -0
  88. pdft-0.2.1/tests/io/conftest.py +9 -0
  89. pdft-0.2.1/tests/io/test_compression.py +101 -0
  90. pdft-0.2.1/tests/io/test_serialize.py +106 -0
  91. pdft-0.2.1/tests/optimizers/__init__.py +0 -0
  92. pdft-0.2.1/tests/optimizers/conftest.py +19 -0
  93. pdft-0.2.1/tests/optimizers/test_adam.py +65 -0
  94. pdft-0.2.1/tests/optimizers/test_gd.py +57 -0
  95. pdft-0.2.1/tests/parity/__init__.py +0 -0
  96. pdft-0.2.1/tests/parity/conftest.py +21 -0
  97. pdft-0.2.1/tests/parity/test_adam.py +41 -0
  98. pdft-0.2.1/tests/parity/test_compression.py +48 -0
  99. pdft-0.2.1/tests/parity/test_long_run.py +65 -0
  100. pdft-0.2.1/tests/parity/test_loss.py +16 -0
  101. pdft-0.2.1/tests/parity/test_manifolds.py +20 -0
  102. pdft-0.2.1/tests/parity/test_new_bases.py +53 -0
  103. pdft-0.2.1/tests/parity/test_qft.py +54 -0
  104. pdft-0.2.1/tests/parity/test_scale.py +28 -0
  105. pdft-0.2.1/tests/parity/test_serialize.py +50 -0
  106. pdft-0.2.1/tests/parity/test_training.py +41 -0
  107. pdft-0.2.1/tests/test_basis_seeded_init.py +118 -0
  108. pdft-0.2.1/tests/test_loss.py +45 -0
  109. pdft-0.2.1/tests/test_manifolds.py +129 -0
  110. pdft-0.2.1/tests/test_profiling.py +198 -0
  111. pdft-0.2.1/tests/test_smoke.py +11 -0
  112. pdft-0.2.1/tests/training/__init__.py +0 -0
  113. pdft-0.2.1/tests/training/conftest.py +16 -0
  114. pdft-0.2.1/tests/training/test_batched.py +275 -0
  115. pdft-0.2.1/tests/training/test_eval_loop.py +106 -0
  116. pdft-0.2.1/tests/training/test_integration.py +58 -0
  117. pdft-0.2.1/tests/training/test_single.py +60 -0
  118. pdft-0.2.1/tests/viz/__init__.py +0 -0
  119. pdft-0.2.1/tests/viz/test_loss.py +44 -0
@@ -0,0 +1,184 @@
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
+ - Design spec: `docs/superpowers/specs/2026-04-24-pdft-migration-design.md`
10
+ - Roadmap: GitHub issue #1
11
+ - Reliability hardening backlog: GitHub issue #2
12
+ - 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__`).
13
+
14
+ ## Critical conventions (do not break)
15
+
16
+ These are the non-obvious invariants that took the original port multiple iterations to find. Violating any of them silently breaks parity with Julia.
17
+
18
+ ### 1. Wirtinger gradient conjugation
19
+
20
+ **JAX returns `∂f/∂z̄` while Julia's Zygote returns `∂f/∂z`** for real-valued functions of complex inputs. They are complex conjugates. `optimize()` in `src/pdft/optimizers.py` conjugates `raw_grads` immediately after `grad_fn(...)`:
21
+
22
+ ```python
23
+ raw_grads = grad_fn(state.current_tensors)
24
+ raw_grads = [jnp.conj(g) for g in raw_grads] # MUST stay
25
+ ```
26
+
27
+ Without this line, GD trajectories drift ~10% over 50 steps and Adam outright diverges. Any new optimizer added to this module must apply the same conjugation, or define a custom JAX `vjp` that matches Julia's convention.
28
+
29
+ ### 2. Yao little-endian qubit ordering
30
+
31
+ `pic.reshape((2,)*(m+n))` gives axes in big-endian bit order, but Julia's Yao treats qubit 1 as the LSB. `_circuit.build_circuit_einsum` reverses qubit-to-axis mapping within each block:
32
+
33
+ ```python
34
+ row_pic = [input_labels[q - 1] for q in range(m, 0, -1)] # axis 0 ↔ qubit m
35
+ col_pic = [input_labels[q - 1] for q in range(m + n, m, -1)]
36
+ ```
37
+
38
+ Same reversal applies to `out_labels`. Don't undo this without first making `ft_mat` parity tests fail in a controlled way.
39
+
40
+ ### 3. Compact 2×2 CP tensor representation
41
+
42
+ 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**:
43
+
44
+ ```python
45
+ controlled_phase_diag(φ) = [[1, 1], [1, exp(iφ)]]
46
+ ```
47
+
48
+ This shares the wire labels of control and target qubits and does NOT introduce new labels (the gate is diagonal so each wire's value is unchanged; only a multiplicative phase is injected). All circuit modules use this representation; never substitute the 4×4 form even though it would be mathematically equivalent — Julia goldens won't match.
49
+
50
+ ### 4. Hadamard-first tensor sort
51
+
52
+ Julia's `qft_code` does `perm_vec = sortperm(tn.tensors, by=x -> !(x ≈ mat(H)))` — Hadamards come first, CPs after. Python's `_circuit.build_circuit_einsum` applies the same sort. The einsum is rebuilt with the permuted tensor list AND permuted subscripts so it stays valid. **JSON serialization assumes this order**; cross-language interop breaks if you change it.
53
+
54
+ ### 5. JAX x64 mode at import time
55
+
56
+ `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).
57
+
58
+ ### 6. Column-major iteration for cross-language hashes
59
+
60
+ `io_json.basis_hash` and `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.
61
+
62
+ ### 7. Julia-compatible float formatting
63
+
64
+ Python's `repr(5e-7)` gives `"5e-07"`; Julia's `string(5e-7)` gives `"5.0e-7"`. `io_json._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.
65
+
66
+ ### 8. L1-cusp horizon for trajectory parity
67
+
68
+ GD on L1 loss is bit-exact for ~50 steps 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/test_parity_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.
69
+
70
+ ### 9. Phase extractors classify by tensor shape, not gate-list metadata
71
+
72
+ `get_*_gate_indices` 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.
73
+
74
+ ## Repo layout
75
+
76
+ ```
77
+ src/pdft/
78
+ ├── manifolds.py Mathematical core (UnitaryManifold, PhaseManifold, batched ops)
79
+ ├── loss.py L1Norm, MSELoss, topk_truncate, loss_function (public)
80
+ ├── profiling.py Cross-cutting profiling helpers
81
+ ├── bases/
82
+ │ ├── base.py AbstractSparseBasis + bases_allclose + 4 basis dataclasses
83
+ │ ├── circuit/ QFT, EntangledQFT, TEBD, MERA (full circuit topologies)
84
+ │ └── block/ Blocked, Rich, RealRich (parameter-efficient)
85
+ ├── circuit/ Einsum builder (builder.py) + JIT closure cache (cache.py)
86
+ ├── optimizers/ core, gd (RiemannianGD + Armijo), adam (RiemannianAdam), loop
87
+ ├── training/ schedules, single (train_basis), batched, adam_step, eval_loop
88
+ ├── io/ serialize (JSON), compression
89
+ └── viz/ loss (matplotlib loss plots), circuit (schematic)
90
+
91
+ reference/julia/ Julia harness — needed only to regenerate goldens
92
+ reference/goldens/ Committed .npz + .json files (<200 KB total)
93
+ examples/ 3 runnable demos, each <10s
94
+ tests/ pytest; mirrors src/pdft/ layout (tests/bases/, tests/optimizers/, ...)
95
+ ```
96
+
97
+ 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`).
98
+
99
+ `DCTBasis` was removed during the modular-src refactor (PR #11). Use a parametric basis (`RichBasis` / `RealRichBasis`) for similar use cases.
100
+
101
+ `pdft/__init__.py` re-exports a small set of most-used names (basis classes,
102
+ trainer, optimizer, loss). Less-public names live in their subpackages —
103
+ e.g. `pdft.io.save_basis`, `pdft.manifolds.UnitaryManifold`,
104
+ `pdft.profiling.profile_training`, `pdft.bases.circuit.qft_code`.
105
+
106
+ `AbstractRiemannianOptimizer` is `RiemannianGD | RiemannianAdam` — a structural union, not a Protocol. Adding a third optimizer means extending that union *and* adding an `isinstance` branch in `optimize()`.
107
+
108
+ `train_basis` is generic over basis type via JAX pytree flatten/unflatten. The convention: each registered basis pytree must have leaves ordered as `tuple(tensors) + tuple(inv_tensors)`, with everything else (m, n, code, inv_code, counts) in aux data. New basis types must follow this convention.
109
+
110
+ ## Dev workflow
111
+
112
+ ```bash
113
+ # Install (Python 3.11+; conda env at /opt/conda/envs/pdft on the dev box)
114
+ pip install -e ".[dev]"
115
+
116
+ # Run tests
117
+ pytest # full suite
118
+ pytest --cov=pdft --cov-fail-under=90 # CI gate
119
+ pytest tests/test_parity_*.py # parity-only
120
+
121
+ # Lint (CI fails if this is dirty — check before pushing)
122
+ ruff check src tests
123
+ ruff format src tests
124
+
125
+ # Run examples
126
+ python examples/basis_demo.py
127
+ python examples/optimizer_benchmark.py
128
+ python examples/mera_demo.py
129
+
130
+ # Regenerate Julia goldens (requires Julia 1.10+)
131
+ make goldens
132
+ ```
133
+
134
+ After regenerating goldens: also update `__upstream_ref__` in `src/pdft/__init__.py` to match `manifest.json`'s `upstream_sha`.
135
+
136
+ ## Tests and parity
137
+
138
+ Three layers (per spec section 7):
139
+
140
+ 1. **Parity tests** (`tests/test_parity_*.py`) — load committed `.npz` / `.json` goldens from `reference/goldens/` and assert Python matches Julia. These are the load-bearing correctness tests.
141
+ 2. **Property tests** (`tests/test_<module>.py`) — math-invariant checks (unitarity preserved, round-trip identity, monotone descent, …). Don't depend on Julia.
142
+ 3. **Smoke / integration** (`tests/test_smoke.py`, `tests/test_training_integration.py`).
143
+
144
+ 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.
145
+
146
+ `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)`.
147
+
148
+ ## When to break parity
149
+
150
+ The default answer is "never". The only acceptable cases:
151
+
152
+ - **Float-precision noise**: test tolerances are documented per case; if you cross a tolerance boundary, investigate before relaxing.
153
+ - **Tie-breaking divergences** (e.g. argpartition vs partialsortperm): document and assert downstream behavior matches instead.
154
+
155
+ If you find another mismatch:
156
+ 1. Diagnose with `reference/julia/` (write a small Julia script to dump intermediate values).
157
+ 2. Compare to Python at the same intermediate point.
158
+ 3. **The bug is in Python**, almost always.
159
+ 4. If the bug is in upstream Julia, file an issue there and document the divergence in this file.
160
+
161
+ ## CI gotchas
162
+
163
+ - Ruff is strict (`F401`, `F841`, `E741`, `E731` all errors). Auto-`--fix` handles most.
164
+ - Coverage gate runs only on the actual test suite, not examples; don't rely on examples for coverage.
165
+ - The `verify-upstream-pin.yml` workflow runs only on PRs touching `reference/`. It uses the GitHub API to confirm the pinned sha exists in upstream — don't push a sha that's only in a fork.
166
+ - The matrix is 3.11 / 3.12 / 3.13. JAX dropped 3.10 in 0.10.0; do not lower the floor.
167
+
168
+ ## What NOT to do
169
+
170
+ - **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.
171
+ - **Don't switch from `jnp.einsum_path("greedy")` to `"optimal"`.** "Optimal" is exponential in tensor count and hangs on the 3×3 QFT (12 tensors).
172
+ - **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.
173
+ - **Don't introduce backwards-compat shims** for the JSON schema. We're at v0.1.0; if the schema changes, bump the version and regenerate goldens.
174
+ - **Don't add ML scaffolding** (no DataLoader, no Trainer-like classes, no Lightning). Upstream is one-target-image-at-a-time and we mirror that. Batched training is open work in #2.
175
+ - **Don't run examples in CI.** They write to `out/` (gitignored) and are not coverage-relevant.
176
+
177
+ ## When making changes
178
+
179
+ 1. Make the change.
180
+ 2. Run `ruff check src tests` and `ruff format src tests`.
181
+ 3. Run `pytest --cov=pdft --cov-fail-under=90`.
182
+ 4. If you touched a circuit, basis, optimizer, or io_json module: also run the relevant `tests/test_parity_*.py` to confirm Julia-parity hasn't drifted.
183
+ 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.
184
+ 6. Push and watch the GitHub Actions matrix — three Python versions must all be green before merging.
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ cache: pip
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Ruff lint
24
+ run: ruff check src tests
25
+ - name: Pytest
26
+ run: pytest --cov=pdft --cov-report=term --cov-fail-under=90
@@ -0,0 +1,63 @@
1
+ name: publish
2
+
3
+ # Builds and publishes pdft to PyPI on every v*.*.* tag push.
4
+ #
5
+ # Uses PyPI Trusted Publishers (OIDC) — NO API token stored in GitHub
6
+ # secrets. To activate: on https://pypi.org/manage/project/pdft/settings/publishing/
7
+ # add a Trusted Publisher with:
8
+ # PyPI Project Name: pdft
9
+ # Owner: zazabap
10
+ # Repository name: pdft
11
+ # Workflow name: publish.yml
12
+ # Environment name: pypi
13
+ #
14
+ # After the first successful publish, future releases are:
15
+ # git tag v0.2.1 && git push --tags
16
+ #
17
+ # Docs: https://docs.pypi.org/trusted-publishers/
18
+
19
+ on:
20
+ push:
21
+ tags:
22
+ - "v*.*.*"
23
+
24
+ jobs:
25
+ build:
26
+ name: Build sdist + wheel
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.12"
33
+ - name: Install build backend
34
+ run: python -m pip install --upgrade pip build
35
+ - name: Verify pyproject version matches the git tag
36
+ run: |
37
+ tag="${GITHUB_REF#refs/tags/v}"
38
+ file_ver=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
39
+ if [ "$tag" != "$file_ver" ]; then
40
+ echo "::error::Tag v$tag does not match pyproject.toml version $file_ver"
41
+ exit 1
42
+ fi
43
+ - name: Build
44
+ run: python -m build
45
+ - name: Upload artifacts
46
+ uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ publish:
52
+ name: Publish to PyPI
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ environment: pypi
56
+ permissions:
57
+ id-token: write # mandatory for Trusted Publisher OIDC
58
+ steps:
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ name: Verify upstream pin
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - "reference/**"
7
+ - ".github/workflows/verify-upstream-pin.yml"
8
+
9
+ jobs:
10
+ verify:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Extract pinned sha
15
+ id: pin
16
+ run: |
17
+ sha=$(grep -oP 'UPSTREAM_SHA = "\K[0-9a-f]+' reference/julia/generate_goldens.jl | head -n1)
18
+ echo "sha=$sha" >> "$GITHUB_OUTPUT"
19
+ - name: Assert sha exists upstream
20
+ env:
21
+ GH_TOKEN: ${{ github.token }}
22
+ run: |
23
+ sha="${{ steps.pin.outputs.sha }}"
24
+ if [ -z "$sha" ]; then
25
+ echo "Could not extract UPSTREAM_SHA from generate_goldens.jl"
26
+ exit 1
27
+ fi
28
+ gh api "repos/nzy1997/ParametricDFT.jl/commits/$sha" >/dev/null
pdft-0.2.1/.gitignore ADDED
@@ -0,0 +1,59 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ *.egg-info/
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Virtual environments
25
+ .venv/
26
+ venv/
27
+ env/
28
+ ENV/
29
+
30
+ # Tooling caches
31
+ .pytest_cache/
32
+ .ruff_cache/
33
+ .mypy_cache/
34
+ .tox/
35
+ .coverage
36
+ .coverage.*
37
+ htmlcov/
38
+ coverage.xml
39
+ *.cover
40
+
41
+ # Jupyter
42
+ .ipynb_checkpoints/
43
+
44
+ # Editors / OS
45
+ .idea/
46
+ .vscode/
47
+ .DS_Store
48
+ *.swp
49
+
50
+ # Claude Code — track CLAUDE.md, ignore everything else
51
+ .claude/*
52
+ !.claude/CLAUDE.md
53
+
54
+ # Example outputs
55
+ out/
56
+ /out/
57
+
58
+ # Benchmark outputs
59
+ benchmarks/results/
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.5.0
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
pdft-0.2.1/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zazabap
4
+
5
+ Portions of this project are a Python port of ParametricDFT.jl
6
+ (https://github.com/nzy1997/ParametricDFT.jl), Copyright (c) 2025 nzy1997,
7
+ originally released under the MIT License.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
pdft-0.2.1/Makefile ADDED
@@ -0,0 +1,8 @@
1
+ .PHONY: 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
pdft-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdft
3
+ Version: 0.2.1
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.5; 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
+ A Python port of [ParametricDFT.jl](https://github.com/nzy1997/ParametricDFT.jl):
63
+ learning parametric quantum Fourier transforms via manifold optimization. The
64
+ package implements a variational approach that approximates the Discrete
65
+ Fourier Transform (DFT) with parameterized quantum circuits.
66
+
67
+ > Status: early scaffold. The Julia package is the reference implementation;
68
+ > this repository will grow the Python equivalent incrementally.
69
+
70
+ ## Installation
71
+
72
+ Once published on PyPI:
73
+
74
+ ```bash
75
+ pip install pdft
76
+ ```
77
+
78
+ From source:
79
+
80
+ ```bash
81
+ git clone https://github.com/zazabap/pdft.git
82
+ cd pdft
83
+ pip install -e ".[dev]"
84
+ ```
85
+
86
+ ## Quick start
87
+
88
+ _(coming soon — mirrors the `make example` demo in the upstream Julia
89
+ package)_
90
+
91
+ ## Background
92
+
93
+ See the upstream notes for the theory:
94
+ - [`note/stepbystep.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/stepbystep.pdf)
95
+ - [`note/main.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/main.pdf)
96
+
97
+ ## License
98
+
99
+ MIT. See [LICENSE](LICENSE). This project is a derivative port of
100
+ ParametricDFT.jl (Copyright © 2025 nzy1997, MIT).
pdft-0.2.1/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # pdft
2
+
3
+ A Python port of [ParametricDFT.jl](https://github.com/nzy1997/ParametricDFT.jl):
4
+ learning parametric quantum Fourier transforms via manifold optimization. The
5
+ package implements a variational approach that approximates the Discrete
6
+ Fourier Transform (DFT) with parameterized quantum circuits.
7
+
8
+ > Status: early scaffold. The Julia package is the reference implementation;
9
+ > this repository will grow the Python equivalent incrementally.
10
+
11
+ ## Installation
12
+
13
+ Once published on PyPI:
14
+
15
+ ```bash
16
+ pip install pdft
17
+ ```
18
+
19
+ From source:
20
+
21
+ ```bash
22
+ git clone https://github.com/zazabap/pdft.git
23
+ cd pdft
24
+ pip install -e ".[dev]"
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ _(coming soon — mirrors the `make example` demo in the upstream Julia
30
+ package)_
31
+
32
+ ## Background
33
+
34
+ See the upstream notes for the theory:
35
+ - [`note/stepbystep.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/stepbystep.pdf)
36
+ - [`note/main.pdf`](https://github.com/nzy1997/ParametricDFT.jl/blob/main/note/main.pdf)
37
+
38
+ ## License
39
+
40
+ MIT. See [LICENSE](LICENSE). This project is a derivative port of
41
+ ParametricDFT.jl (Copyright © 2025 nzy1997, MIT).
@@ -0,0 +1,49 @@
1
+ """basis_demo.py — train a QFTBasis on a random 4x4 image and plot the loss.
2
+
3
+ Run: python examples/basis_demo.py
4
+ Requires: pdft + pdft[plot] extra.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from pathlib import Path
10
+
11
+ import jax
12
+ import jax.numpy as jnp
13
+
14
+ import pdft
15
+ from pdft.viz import TrainingHistory, plot_training_loss
16
+
17
+
18
+ def main(out_dir: str | Path = "out") -> None:
19
+ out = Path(out_dir)
20
+ out.mkdir(parents=True, exist_ok=True)
21
+
22
+ target = jax.random.normal(jax.random.PRNGKey(7), (4, 4)).astype(jnp.complex128)
23
+ basis = pdft.QFTBasis(m=2, n=2)
24
+
25
+ print(f"Training QFTBasis on a 4x4 target image for 50 steps with RiemannianGD...")
26
+ t0 = time.perf_counter()
27
+ result = pdft.train_basis(
28
+ basis,
29
+ target=target,
30
+ loss=pdft.L1Norm(),
31
+ optimizer=pdft.RiemannianGD(lr=0.01),
32
+ steps=50,
33
+ seed=0,
34
+ )
35
+ elapsed = time.perf_counter() - t0
36
+ print(f"done in {elapsed:.2f}s")
37
+ print(f" initial loss: {result.loss_history[0]:.4f}")
38
+ print(f" final loss: {result.loss_history[-1]:.4f}")
39
+ print(f" reduction : {result.loss_history[0] - result.loss_history[-1]:.4f}")
40
+
41
+ hist = TrainingHistory(losses=result.loss_history, label="RiemannianGD")
42
+ fig_path = out / "basis_demo_loss.png"
43
+ plot_training_loss(hist, output_path=fig_path, smooth_alpha=0.2,
44
+ title="QFTBasis training loss (L1, RiemannianGD, lr=0.01)")
45
+ print(f" wrote plot: {fig_path}")
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -0,0 +1,51 @@
1
+ """mera_demo.py — compare QFTBasis, EntangledQFTBasis, TEBDBasis, MERABasis on a single target.
2
+
3
+ Run: python examples/mera_demo.py
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import jax
11
+ import jax.numpy as jnp
12
+
13
+ import pdft
14
+ from pdft.viz import TrainingHistory, plot_training_comparison
15
+
16
+
17
+ def main(out_dir: str | Path = "out") -> None:
18
+ out = Path(out_dir)
19
+ out.mkdir(parents=True, exist_ok=True)
20
+
21
+ target = jax.random.normal(jax.random.PRNGKey(11), (4, 4)).astype(jnp.complex128)
22
+
23
+ histories: list[TrainingHistory] = []
24
+ for label, basis in [
25
+ ("QFTBasis", pdft.QFTBasis(m=2, n=2)),
26
+ ("EntangledQFTBasis", pdft.EntangledQFTBasis(m=2, n=2)),
27
+ ("TEBDBasis", pdft.TEBDBasis(m=2, n=2)),
28
+ ("MERABasis", pdft.MERABasis(m=2, n=2)),
29
+ ]:
30
+ print(f"Training {label}...")
31
+ t0 = time.perf_counter()
32
+ result = pdft.train_basis(
33
+ basis,
34
+ target=target,
35
+ loss=pdft.L1Norm(),
36
+ optimizer=pdft.RiemannianAdam(lr=0.01),
37
+ steps=40,
38
+ seed=0,
39
+ )
40
+ elapsed = time.perf_counter() - t0
41
+ print(f" {elapsed:.2f}s, initial {result.loss_history[0]:.4f}, final {result.loss_history[-1]:.4f}")
42
+ histories.append(TrainingHistory(losses=result.loss_history, label=label))
43
+
44
+ fig_path = out / "mera_demo.png"
45
+ plot_training_comparison(histories, output_path=fig_path,
46
+ title="Four bases on the same 4x4 target (RiemannianAdam, L1 loss)")
47
+ print(f"wrote: {fig_path}")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()