patchcraft 0.2.2__tar.gz → 0.5.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 (84) hide show
  1. {patchcraft-0.2.2 → patchcraft-0.5.1}/.gitignore +7 -0
  2. {patchcraft-0.2.2 → patchcraft-0.5.1}/CHANGELOG.md +282 -23
  3. patchcraft-0.5.1/CONTRIBUTING.md +270 -0
  4. patchcraft-0.5.1/MANIFEST.in +37 -0
  5. patchcraft-0.2.2/README.pypi.md → patchcraft-0.5.1/PKG-INFO +79 -6
  6. {patchcraft-0.2.2 → patchcraft-0.5.1}/README.md +37 -7
  7. {patchcraft-0.2.2 → patchcraft-0.5.1}/README.pt-BR.md +37 -7
  8. patchcraft-0.2.2/PKG-INFO → patchcraft-0.5.1/README.pypi.md +37 -46
  9. patchcraft-0.5.1/accel/Cargo.lock +232 -0
  10. patchcraft-0.5.1/accel/Cargo.toml +32 -0
  11. patchcraft-0.5.1/accel/README.md +48 -0
  12. patchcraft-0.5.1/accel/src/kernel.rs +252 -0
  13. patchcraft-0.5.1/accel/src/lib.rs +88 -0
  14. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/AUXILIARY.md +13 -8
  15. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/GUIDE.md +47 -33
  16. patchcraft-0.5.1/docs/PERFORMANCE.md +116 -0
  17. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/ROADMAP.md +2 -1
  18. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/SCOPE.md +17 -11
  19. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/STUDIES/2026-08-04-patch-techniques.md +2 -1
  20. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/THEORY.md +6 -5
  21. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/USAGE.md +16 -8
  22. patchcraft-0.5.1/docs/superpowers/plans/2026-09-01-fase1-bugs-performance.md +1179 -0
  23. patchcraft-0.5.1/docs/superpowers/plans/2026-09-01-fase2-acel.md +1643 -0
  24. patchcraft-0.5.1/docs/superpowers/plans/2026-09-01-fase3-g1-predicado.md +1705 -0
  25. patchcraft-0.5.1/docs/superpowers/specs/2026-09-01-fase1-bugs-performance-design.md +151 -0
  26. patchcraft-0.5.1/docs/superpowers/specs/2026-09-01-fase2-acel-design.md +168 -0
  27. patchcraft-0.5.1/docs/superpowers/specs/2026-09-01-fase3-g1-predicado-design.md +260 -0
  28. patchcraft-0.5.1/pyproject.toml +130 -0
  29. patchcraft-0.5.1/setup.cfg +4 -0
  30. patchcraft-0.5.1/setup.py +81 -0
  31. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/__init__.py +17 -1
  32. patchcraft-0.5.1/src/patchcraft/_accel.py +116 -0
  33. patchcraft-0.5.1/src/patchcraft/_foldgeom.py +87 -0
  34. patchcraft-0.5.1/src/patchcraft/_version.py +24 -0
  35. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/cache.py +7 -0
  36. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/extract.py +32 -1
  37. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/geometry.py +10 -2
  38. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/metrics.py +21 -13
  39. patchcraft-0.5.1/src/patchcraft/reconstruct.py +137 -0
  40. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/resize.py +6 -3
  41. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/stitch.py +102 -112
  42. patchcraft-0.5.1/src/patchcraft.egg-info/PKG-INFO +188 -0
  43. patchcraft-0.5.1/src/patchcraft.egg-info/SOURCES.txt +76 -0
  44. patchcraft-0.5.1/src/patchcraft.egg-info/dependency_links.txt +1 -0
  45. patchcraft-0.5.1/src/patchcraft.egg-info/requires.txt +13 -0
  46. patchcraft-0.5.1/src/patchcraft.egg-info/scm_file_list.json +77 -0
  47. patchcraft-0.5.1/src/patchcraft.egg-info/scm_version.json +8 -0
  48. patchcraft-0.5.1/src/patchcraft.egg-info/top_level.txt +1 -0
  49. patchcraft-0.5.1/tests/_rng.py +108 -0
  50. patchcraft-0.5.1/tests/test_accel.py +223 -0
  51. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_cache.py +22 -0
  52. patchcraft-0.5.1/tests/test_exactness.py +164 -0
  53. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_extract.py +62 -1
  54. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_geometry.py +39 -11
  55. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_import.py +6 -0
  56. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_metrics.py +47 -0
  57. patchcraft-0.5.1/tests/test_public_api.py +249 -0
  58. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_reconstruct.py +89 -30
  59. patchcraft-0.5.1/tests/test_reference.py +92 -0
  60. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_resize.py +11 -0
  61. patchcraft-0.5.1/tests/test_rng.py +109 -0
  62. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_stitch.py +91 -20
  63. patchcraft-0.5.1/tools/benchmark.py +165 -0
  64. patchcraft-0.5.1/tools/check_dist.py +209 -0
  65. patchcraft-0.5.1/uv.lock +1188 -0
  66. patchcraft-0.2.2/.github/workflows/release.yml +0 -128
  67. patchcraft-0.2.2/.github/workflows/test.yml +0 -51
  68. patchcraft-0.2.2/CONTRIBUTING.md +0 -98
  69. patchcraft-0.2.2/pyproject.toml +0 -100
  70. patchcraft-0.2.2/src/patchcraft/reconstruct.py +0 -155
  71. patchcraft-0.2.2/uv.lock +0 -1041
  72. {patchcraft-0.2.2 → patchcraft-0.5.1}/.python-version +0 -0
  73. {patchcraft-0.2.2 → patchcraft-0.5.1}/LICENSE +0 -0
  74. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/ADR/0001-patch-extraction-api.md +0 -0
  75. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/ADR/0002-patchify-transform.md +0 -0
  76. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/ADR/0003-reversibility-classes.md +0 -0
  77. {patchcraft-0.2.2 → patchcraft-0.5.1}/docs/FOCO-1.0.md +0 -0
  78. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/pair.py +0 -0
  79. {patchcraft-0.2.2 → patchcraft-0.5.1}/src/patchcraft/py.typed +0 -0
  80. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/__init__.py +0 -0
  81. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/_datasets.py +0 -0
  82. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/conftest.py +0 -0
  83. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_datasets_helper.py +0 -0
  84. {patchcraft-0.2.2 → patchcraft-0.5.1}/tests/test_pair.py +0 -0
@@ -3,6 +3,10 @@ __pycache__/
3
3
  *.py[cod]
4
4
  *$py.class
5
5
  *.so
6
+ # Built in place by an editable install (setuptools-rust).
7
+ src/patchcraft/_accel_native*
8
+ # Generated at build time by setuptools-scm.
9
+ src/patchcraft/_version.py
6
10
 
7
11
  # Distribution / packaging
8
12
  build/
@@ -53,3 +57,6 @@ runs/
53
57
 
54
58
  # Datasets and caches live on Z:
55
59
  data/
60
+
61
+ # Rust accelerator build artifacts (accel/Cargo.lock IS committed)
62
+ accel/target/
@@ -6,8 +6,280 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] - 2026-09-02
10
+
11
+ > Numbered `0.6.0` while it was being built. Corrected before release: the
12
+ > middle digit is the compatibility boundary, and this release changes no
13
+ > signature, no name and no output value. See the versioning policy and its
14
+ > errata in [`CONTRIBUTING.md`](CONTRIBUTING.md).
15
+
16
+ ### Changed
17
+
18
+ - **The native accelerator ships inside this wheel.** It was going to be a
19
+ second PyPI project, `patchcraft-accel`, reached through
20
+ `pip install patchcraft[accel]`. That project was never published and now
21
+ never will be. A release produces one sdist and six wheels for the single
22
+ `patchcraft` project: five tagged `cp312-abi3-<platform>` with the Rust
23
+ kernel compiled in as `patchcraft._accel_native`, and one `py3-none-any`
24
+ for every other platform. Installers prefer the most specific compatible
25
+ tag, so `pip install patchcraft` picks up the accelerator where one exists
26
+ and the pure wheel everywhere else, with no extra to remember. The pattern
27
+ and its universal fallback follow `coverage`, which publishes the same two
28
+ shapes for one version.
29
+ - The `accel` extra is removed. Nothing depended on it, because it never
30
+ reached the index.
31
+ - The accelerator is now on by default where a wheel carries it, rather than
32
+ opt-in. The two paths are bit-identical by test, and `PATCHCRAFT_ACCEL=0`
33
+ still forces the pure one at runtime.
34
+ - Build backend moves from hatchling to setuptools with setuptools-rust, which
35
+ is what makes one project able to emit both shapes of wheel. `setup.py`
36
+ holds the single decision, driven by `PATCHCRAFT_PURE_PYTHON` and
37
+ `PATCHCRAFT_REQUIRE_EXTENSION`; everything else stays declarative in
38
+ `pyproject.toml`. Installing from the sdist compiles the extension when a
39
+ Rust toolchain is present and degrades to a pure install when it is not.
40
+ - License metadata follows PEP 639: the SPDX expression `MIT` with
41
+ `license-files`, and the superseded `License :: OSI Approved` classifier is
42
+ gone. setuptools rejects carrying both.
43
+
44
+ ### Added
45
+
46
+ - **The version comes from the git tag.** `setuptools-scm` derives it at build
47
+ time, so no file in the source states a version, the release procedure loses
48
+ its bump step, and a tag and a release can no longer disagree.
49
+ `patchcraft.__version__` still works: the build writes a generated
50
+ `_version.py` that `__init__.py` imports, which costs nothing at import time,
51
+ with `importlib.metadata` as a fallback for a tree that was never built.
52
+ Measured before choosing: `importlib.metadata` alone was the wrong default,
53
+ because it took 56 ms on first call and reported a stale version in an
54
+ editable install.
55
+ - `docs/PERFORMANCE.md`, which is where the accelerator's measurements live.
56
+ It carries the test machine, the versions, the date, the reproduction
57
+ command and what the numbers do not say, and its table is pasted from
58
+ `tools/benchmark.py --markdown` rather than retyped.
59
+ - The Rust kernel is now always built in release mode, including in an
60
+ editable install. It was following the build command, so a development
61
+ checkout compiled a debug kernel: measured here, that turned a 14x speedup
62
+ into 2.1x, which made local benchmarks meaningless and made the accelerated
63
+ CI job exercise code no user runs.
64
+ - `patchcraft.__version__` is re-exported explicitly, so `mypy --strict` in a
65
+ consuming project still accepts it. Moving it out of a literal and into an
66
+ import had quietly made it an implicit re-export, which strict mode rejects.
67
+ - `tools/benchmark.py` measures the accelerated fold against the pure one and
68
+ refuses to report a timing unless the two results are bit-identical. The
69
+ numbers in the documentation come from it, and it is the reproduction
70
+ command they cite.
71
+ - Every build checkout uses `fetch-depth: 0`. The default shallow clone hides
72
+ the tags, and setuptools-scm's fallback for that is a silent
73
+ `0.1.dev1+g<sha>`, a version that walks backwards rather than an error.
74
+ - `tools/check_dist.py` grew `--expect-version` and `--require-universal`, and
75
+ it now rejects any artifact carrying a local version segment. PyPI refuses
76
+ those on upload, so this turns a late and confusing server-side rejection
77
+ into an early local one. It also fixes a bug in the job that checks the
78
+ native wheels, which demanded a universal wheel that job never has.
79
+ - Linux aarch64 wheels, which the previous four-target matrix did not cover.
80
+ - `tools/check_dist.py`, run before every upload. It fails the release if a
81
+ platform wheel lost its extension, which `optional=True` would otherwise
82
+ let through as a silently slow wheel, or if the universal wheel gained one.
83
+ - The versioning policy now names the resolver rule it follows, which is that
84
+ `0.y` is the compatibility boundary in Cargo, npm and PEP 440 alike, and it
85
+ carries an errata for the two releases numbered before the rule was written.
86
+ - Version numbers are gone from the documentation surface. The three cover
87
+ pages carry none at all, since the PyPI badge already shows the current
88
+ release, and the manual keeps only the one the citation entry needs. The
89
+ release archaeology those pages used to recite lives here, and the rest is
90
+ in the git history.
91
+ - `MANIFEST.in`, which puts `accel/` in the sdist so the source path can build
92
+ the extension, and keeps `lab/`, `.superpowers/` and cargo's `target/` out.
93
+
94
+ - **Corrected: the hand-rolled and library tile-and-blend results are not
95
+ bit-identical.** The three cover pages and GUIDE section 1 claimed they were,
96
+ and the GUIDE block asserted it. Re-measured: they agree to 1.17e-05 on a
97
+ value in [0, 1], differing on 24500 of 49152 elements. The cause is the 0.3.0
98
+ separable denominator, where `stitch` sums two 1-D window folds while the
99
+ hand-rolled twin folds a replicated 2-D kernel. `stitch(weight="uniform")`
100
+ is still bit-identical to `reconstruct`. Nothing about the round-trip
101
+ contract changed; the documentation was simply asserting more than the code
102
+ delivered.
103
+ - Documentation corrections found by auditing every page against the code:
104
+ the changelog's reference links did not define the two most recent versions;
105
+ `docs/AUXILIARY.md` listed wheel contents that predate three releases;
106
+ `docs/SCOPE.md` still promised "pure-Python single-threaded", which the
107
+ rayon-parallel kernel contradicts; `docs/ROADMAP.md` still named hatchling;
108
+ GUIDE said the library "is not faster", that the no-zstandard branch is
109
+ untested, and that CI is six cells; the cover pages pointed at `USAGE.md` as
110
+ a walkthrough of 20 symbols when it covers 18 and says so. `CONTRIBUTING.md`
111
+ told a contributor to run `pytest` after a `uv sync` that does not install it.
112
+
113
+ ### Notes
114
+
115
+ - No signature, no name and no output value changed. The public surface is the
116
+ same 20 names, still pinned by `tests/test_public_api.py`.
117
+ - Measured on torch 2.14, CPU, mean over the dev machine: `reconstruct` at
118
+ 3x512x512 p=32 s=16 goes 11.4 ms to 2.5 ms (4.5x), at 3x2048x2048 p=64 s=32
119
+ 477.9 ms to 31.8 ms (15.0x); `stitch[hann]` 21.6 ms to 7.0 ms (3.1x) and
120
+ 488.7 ms to 38.8 ms (12.6x). The gain is algorithmic rather than a thread
121
+ count: the pure `F.fold` measured between 365 and 465 ms on that largest
122
+ case at 4, 8, 16 and 36 torch threads.
123
+
124
+ ## [0.5.0] - 2026-09-02
125
+
126
+ ### Added
127
+
128
+ - Python 3.14 is supported and declared. The classifiers list it, the CI
129
+ matrix runs the full suite plus `ruff check` and `mypy --strict` against it
130
+ on Ubuntu and Windows, and all six cells are green. The floor stays at 3.12
131
+ because `cache.py` uses PEP 695 generic syntax, which 3.11 cannot parse.
132
+
133
+ ### Changed
134
+
135
+ - `tilings`/`paired_tilings` no longer emit degenerate single-patch overlap
136
+ specs: with one patch the stride is unobservable and the spec duplicated
137
+ the exact tile. `tilings((28, 28), allow_overlap=True)` now returns 73
138
+ specs instead of 100; `paired_tilings((14, 14), (28, 28), allow_overlap=True)`
139
+ returns 27 instead of 40.
140
+ - The development lock moved to torch 2.14.0 and torchvision 0.29.0, with the
141
+ linters and the rest of the toolchain along with it. The full
142
+ 126,736-geometry exactness sweep passes on torch 2.14, so the count-map
143
+ contract is unchanged by the upgrade. Runtime requirements are untouched and
144
+ stay at `torch>=2.6`.
145
+
9
146
  ### Fixed
10
147
 
148
+ - **Public retraction: the documented exactness boundary was wrong.** The old
149
+ wording (`k_max <= 4`, "`stride == patch_size / 2`", "within ~1 ULP" outside
150
+ the rule) was measured false: the error grows with the pixel's coverage
151
+ count (up to 19 ULP at count 81 in float32). The correct contract
152
+ (ADR 0003): the `extract`/`reconstruct` round trip is bit-exact iff every
153
+ value of the overlap count map is a power of two, which is always true
154
+ at `stride == patch_size`, and outside the rule the per-pixel error is
155
+ bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count.
156
+ Docstrings and docs (SCOPE, THEORY, GUIDE, USAGE, READMEs) now state this
157
+ form; THEORY §9.1 records the extract-truncates / reconstruct-rejects
158
+ asymmetry.
159
+ - Packaging: the sdist no longer carries `.superpowers/`, an untracked
160
+ directory of agent tooling that hatchling swept in (62 files). The same
161
+ class of leak as the `lab/` files that reached PyPI in 0.2.0.
162
+ - Documentation: GUIDE section 6 still printed the pre-0.5.0 enumeration
163
+ output and described the single-patch duplicates as an open wart; section 7
164
+ asserted `__version__ == "0.4.0"`; section 8 and the BibTeX entry quoted
165
+ 0.4.0 and its test counts. All four now match the release.
166
+ - Prose: the em-dashes that entered the docstrings, docs and tests during
167
+ 0.3.0 to 0.5.0 are rewritten as ordinary sentences, per the project's
168
+ writing rule.
169
+ - Test suite: round-trip assertions now run on seeded full-mantissa noise
170
+ (integer ramps and widened-float32 data could mask ULP-level errors); a
171
+ falsification suite (`tests/test_exactness.py`) enumerates the 126,736
172
+ legal geometries, samples 256 (seeded; full sweep via
173
+ `PATCHCRAFT_SWEEP_FULL=1`), and pins both halves of the predicate; a naive
174
+ loop-based reference (`tests/test_reference.py`) cross-checks the fast
175
+ paths; the 20-name public surface is frozen by `tests/test_public_api.py`;
176
+ the no-`zstandard` cache branch is now covered.
177
+
178
+ ### Notes
179
+
180
+ - No signature changed. The public surface is exactly the 20 names of 0.4.0,
181
+ now pinned by test.
182
+
183
+ ## [0.4.0] - 2026-09-01
184
+
185
+ > Source milestone, never uploaded to PyPI. Its contents reached
186
+ > users inside 0.5.0. See the versioning policy in
187
+ > [`CONTRIBUTING.md`](CONTRIBUTING.md) for why the number was minted
188
+ > early and why that no longer happens.
189
+
190
+
191
+ ### Added
192
+
193
+ - Optional native accelerator `patchcraft-accel` (`pip install patchcraft[accel]`):
194
+ a Rust/pyo3 kernel for the overlap fold of `reconstruct`/`stitch`. Prebuilt
195
+ abi3 wheels (Python 3.12+) for Windows x64, Linux x86_64 (manylinux), macOS
196
+ arm64 and macOS x86_64; self-contained, no system dependencies, no torch
197
+ linkage. When it is absent, ABI-mismatched, ineligible (CUDA/integer
198
+ tensors), or disabled via `PATCHCRAFT_ACCEL=0`, every path falls back
199
+ silently to pure torch.
200
+ - `patchcraft.accel_available()` reports whether the accelerator is active.
201
+
202
+ ### Performance
203
+
204
+ - Overlap fold with the accelerator, mean of `lab/bench_phase2.py` on the
205
+ dev machine: reconstruct 3x512x512 p=32 s=16 17.727 ms -> 2.609 ms (6.80x);
206
+ stitch[hann] same geometry 23.714 ms -> 8.894 ms (2.67x); 3x2048x2048 p=64
207
+ s=32 reconstruct 469.814 ms -> 32.458 ms (14.47x). The accelerated `stitch`
208
+ also skips the full pre-multiply pass over the patches tensor.
209
+
210
+ ### Notes
211
+
212
+ - Numeric equivalence accel vs pure: bit-exact (`torch.equal`) across the
213
+ test grid (overlap x rectangular x stride∤patch x C in {1,3,4} x f32/f64).
214
+ - Linux validation: wheel
215
+ `patchcraft_accel-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`
216
+ installed and the test suite passed accelerated on Ubuntu 26.04 (WSL),
217
+ built via the manylinux maturin container.
218
+
219
+ ## [0.3.0] - 2026-09-01
220
+
221
+ > Source milestone, never uploaded to PyPI. Its contents reached
222
+ > users inside 0.5.0. See the versioning policy in
223
+ > [`CONTRIBUTING.md`](CONTRIBUTING.md) for why the number was minted
224
+ > early and why that no longer happens.
225
+
226
+
227
+ ### Performance
228
+
229
+ - `extract` uses a strided-window view (`Tensor.unfold`) for `dilation == 1`,
230
+ one copy instead of im2col + permute-contiguous: 13-21x faster on CPU,
231
+ bit-exact. `dilation > 1` still uses `F.unfold`.
232
+ - `reconstruct` skips `F.fold` entirely on non-overlapping grids (a pure
233
+ rearrangement, 27x faster on CPU) and computes the overlap count map in
234
+ closed form O(H+W) instead of folding a tensor of ones.
235
+ - `stitch` builds its denominator from two 1-D window folds (the kernels are
236
+ separable) instead of a second 2-D `F.fold`. `uniform` stays bit-exact vs
237
+ `reconstruct`; `hann`/`gaussian` may differ by ULPs (summation order).
238
+ - `patch_metrics`/`per_patch_mse` materialize one f64 tensor instead of
239
+ three and sync the device once instead of three times.
240
+
241
+ ### Fixed
242
+
243
+ - `resize` with `backend="pil"` clamped integer tensors to the uint8 range
244
+ before casting; values outside [0, 255] used to wrap (300 became 44).
245
+ - Removed the machine-local `cache_dir = "Z:\caches\pytest"` from
246
+ `pyproject.toml` (created a literal `Z:\caches\...` directory on
247
+ Linux/macOS).
248
+
249
+ ### Documentation
250
+
251
+ - Corrected stale 0.2.1 version strings on all cover pages and in GUIDE.
252
+ - Corrected the gaussian-kernel floor claim in `stitch` (exp(-4) at corners,
253
+ not exp(-2) everywhere).
254
+ - Corrected the bit-exactness rule in THEORY/USAGE (power-of-two count map).
255
+ - SCOPE §4.4 no longer claims `reconstruct` lacks a dtype guard.
256
+ - CHANGELOG: link targets for [Unreleased], [0.2.2], [0.2.1].
257
+
258
+ ### Internal
259
+
260
+ - `reconstruct`/`stitch` share one fold-geometry validator
261
+ (`patchcraft._foldgeom.check_fold_geometry`); error messages unchanged.
262
+
263
+ ## [0.2.2] - 2026-08-30
264
+
265
+ Documentation release. No behaviour changes, so every call that worked on 0.2.1
266
+ works identically here, and the only file under `src/` that changed is a
267
+ docstring.
268
+
269
+ ### Fixed
270
+
271
+ - **`reconstruct`'s docstring claimed the overlap round trip returns the
272
+ original image.** It said each pixel comes back as "the average of all patches
273
+ covering it, the same as the original when patches came from `extract`
274
+ unmodified". The second half is false. Measured on a 16x16 image with
275
+ `patch_size=4, stride=1`, unmodified patches straight from `extract` come back
276
+ with `torch.equal` False and a maximum absolute error of 2.4e-07 in float32.
277
+
278
+ The docstring now states the real rule: the round trip is bit-exact when every
279
+ value in the count map is a power of two, because dividing a float by a power
280
+ of two is the one division that never rounds. It also says that widening the
281
+ dtype does not rescue it, since the deciding axis is the geometry rather than
282
+ the precision.
11
283
  - **`pair()`'s docstring double-applied the stride, and could silently
12
284
  misplace every patch for a caller who trusted it.** It said patch `k` has
13
285
  its top-left at LR pixel `(row * sh_lr, col * sw_lr)`, but `PatchMeta.row`
@@ -37,32 +309,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
37
309
  Reported from an API review done for the QSR prototype, which uses patch
38
310
  position as a context feature.
39
311
 
40
- ## [0.2.2] 2026-08-30
41
-
42
- Documentation release. No behaviour changes, so every call that worked on 0.2.1
43
- works identically here, and the only file under `src/` that changed is a
44
- docstring.
45
-
46
- ### Fixed
47
-
48
- - **`reconstruct`'s docstring claimed the overlap round trip returns the
49
- original image.** It said each pixel comes back as "the average of all patches
50
- covering it, the same as the original when patches came from `extract`
51
- unmodified". The second half is false. Measured on a 16x16 image with
52
- `patch_size=4, stride=1`, unmodified patches straight from `extract` come back
53
- with `torch.equal` False and a maximum absolute error of 2.4e-07 in float32.
54
-
55
- The docstring now states the real rule: the round trip is bit-exact when every
56
- value in the count map is a power of two, because dividing a float by a power
57
- of two is the one division that never rounds. It also says that widening the
58
- dtype does not rescue it, since the deciding axis is the geometry rather than
59
- the precision.
60
-
61
312
  ### Changed
62
313
 
63
314
  - **The README is now a call page and the manual moved to `docs/GUIDE.md`.** The
64
315
  README had reached 864 lines, which made it a manual printed on the front
65
- door. It is 137 lines now, and it answers what the library is, where you are
316
+ door. It is 156 lines now, and it answers what the library is, where you are
66
317
  getting into, and which of `reconstruct` or `stitch` you want. Nothing was
67
318
  discarded: every measurement, table and long example moved into the guide,
68
319
  which gained its own table of contents.
@@ -233,7 +484,7 @@ kernel so each pixel "trusts" patches closer to its center more.
233
484
  on overlap). With `"hann"` it strongly suppresses seams at the
234
485
  cost of zeroing image corners that fall on Hann's edge-weight-zero
235
486
  region (documented artifact). With `"gaussian"`
236
- (`sigma = max(1, min(ph, pw) / 4)`) it blends smoothly with no
487
+ (per-axis `sigma = max(1.0, n / 4.0)`) it blends smoothly with no
237
488
  corner artifact.
238
489
 
239
490
  Floating-point patches only, because window kernels are float-valued and we
@@ -336,5 +587,13 @@ First public release. Public API stable; signatures will only change in 1.x.
336
587
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
337
588
  validation lab.
338
589
 
590
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.1...HEAD
591
+ [0.5.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.1
592
+ [0.5.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.0
593
+
594
+ <!-- 0.4.0 and 0.3.0 are deliberately unlinked: they are source milestones that
595
+ were never tagged and never released, as their own entries say. -->
596
+ [0.2.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.2
597
+ [0.2.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.1
339
598
  [0.2.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.0
340
599
  [0.1.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.1.0
@@ -0,0 +1,270 @@
1
+ # Contributing to PatchCraft
2
+
3
+ If you've cloned this repo and want to run the test suite, understand how the project is laid out, or follow the validation conventions, this is the page. For installation and usage, see the [README](README.md).
4
+
5
+ ---
6
+
7
+ ## Run tests
8
+
9
+ The test runner and the linters live in the `dev` extra, so a plain `uv sync`
10
+ does not install them:
11
+
12
+ ```
13
+ uv sync --locked --extra cache --extra dev
14
+ ```
15
+
16
+ ```
17
+ pytest
18
+ pytest -m "not gpu" # skip GPU-requiring tests
19
+ ```
20
+
21
+ CI runs the full suite on every push and PR. See [`.github/workflows/test.yml`](.github/workflows/test.yml). Matrix is `{ubuntu-latest, windows-latest} × {python 3.12, 3.13, 3.14}` for the pure path, plus a second job on both operating systems for the accelerated one. To run the same checks locally before pushing:
22
+
23
+ ```
24
+ ruff check src tests
25
+ mypy --strict src
26
+ pytest -m "not gpu"
27
+ ```
28
+
29
+ ---
30
+
31
+ ## The two paths
32
+
33
+ The overlapping fold in `reconstruct` and `stitch` has a Rust implementation
34
+ in [`accel/`](accel/), compiled into the wheel on the platforms CI builds for.
35
+ Everything else runs the same operation in torch. The two are bit-identical by
36
+ test, so a contribution has to keep both green rather than pick one.
37
+
38
+ Which one your checkout runs depends on whether the extension got built:
39
+
40
+ ```
41
+ python -c "import patchcraft; print(patchcraft.accel_available())"
42
+ ```
43
+
44
+ `uv sync --extra cache --extra dev` builds it when a Rust toolchain is on
45
+ PATH, always in release mode. Two environment variables override that, and CI
46
+ sets both explicitly rather than relying on what happens to be installed:
47
+
48
+ | Variable | Effect |
49
+ |---|---|
50
+ | `PATCHCRAFT_PURE_PYTHON=1` | Skip the extension. How the universal wheel is built. |
51
+ | `PATCHCRAFT_REQUIRE_EXTENSION=1` | Make a Rust build failure fatal instead of degrading to the pure path. |
52
+ | `PATCHCRAFT_ACCEL=0` | Runtime only: ignore an extension that is already built. |
53
+
54
+ The Rust kernel has its own tests, which need no Python at all:
55
+
56
+ ```
57
+ cargo test --manifest-path accel/Cargo.toml
58
+ ```
59
+
60
+ One local wrinkle worth knowing. If your checkout sits under a path with
61
+ non-ASCII characters, `setuptools-rust` mis-decodes the artifact path that
62
+ cargo reports and the copy step fails. Point the build at an ASCII directory
63
+ and it works:
64
+
65
+ ```
66
+ CARGO_TARGET_DIR=/tmp/pc-target uv sync --extra cache --extra dev
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Layout
72
+
73
+ ```
74
+ PatchCraft/
75
+ ├── pyproject.toml package metadata, build backend, cibuildwheel targets
76
+ ├── setup.py the one build decision: with or without the Rust extension
77
+ ├── tools/check_dist.py gate: extension present, versions agree, tag respected
78
+ ├── tools/benchmark.py accelerated vs pure, and proves they agree first
79
+ ├── MANIFEST.in what the sdist carries
80
+ ├── README.md the call page, canonical, English
81
+ ├── README.pt-BR.md the same call page in Portuguese
82
+ ├── README.pypi.md the PyPI page (long_description), links absolute
83
+ ├── CONTRIBUTING.md this file
84
+ ├── CHANGELOG.md Keep-a-Changelog format
85
+ ├── LICENSE MIT
86
+ ├── .python-version 3.13
87
+ ├── .gitignore ignores archive/, venvs, caches, outputs
88
+ ├── .github/workflows/
89
+ │ ├── test.yml matrix CI on PRs/main, plus the accelerated job
90
+ │ └── release.yml publishes to PyPI on vX.Y.Z tag push (Trusted Publishing)
91
+ ├── src/patchcraft/ library core, one-image-at-a-time primitives
92
+ │ ├── __init__.py re-exports the full public API and the generated version
93
+ │ ├── extract.py patches via strided view or F.unfold; Patchify (ADR 0002)
94
+ │ ├── reconstruct.py inverse via F.fold + count map, closed-form fast path
95
+ │ ├── stitch.py weighted reassembly for modified patches
96
+ │ ├── geometry.py pre-flight: num_patches, tilings, scale_factor, paired_tilings
97
+ │ ├── metrics.py patch_metrics, per_patch_mse, per_patch_psnr
98
+ │ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
99
+ │ ├── resize.py resize with PIL or torch backends
100
+ │ ├── cache.py content-addressed disk cache
101
+ │ ├── _accel.py bridge to the optional native accelerator, falls back silently
102
+ │ └── _foldgeom.py shared fold-geometry validation for reconstruct and stitch
103
+ ├── accel/ the Rust crate compiled into this wheel (pyo3, setuptools-rust)
104
+ │ ├── Cargo.toml crate metadata; the version there is internal
105
+ │ ├── src/kernel.rs the gather-fold kernel, pure Rust, cargo-tested
106
+ │ ├── src/lib.rs pyo3 glue, exports patchcraft._accel_native
107
+ │ └── README.md how to build it, and how to check it is active
108
+ ├── tests/ pytest suite (contract tests for src/)
109
+ │ ├── test_extract.py extract + Patchify
110
+ │ ├── test_reconstruct.py
111
+ │ ├── test_stitch.py
112
+ │ ├── test_geometry.py num_patches + tilings + scale_factor + paired_tilings
113
+ │ ├── test_metrics.py
114
+ │ ├── test_pair.py
115
+ │ ├── test_resize.py
116
+ │ ├── test_cache.py
117
+ │ ├── test_exactness.py falsifies the count-map predicate over the legal space
118
+ │ ├── test_reference.py naive loop-based reference for the fast paths
119
+ │ ├── test_public_api.py freezes the 20 names, their signatures and carrier fields
120
+ │ ├── test_accel.py native path, skipped unless the extension was built
121
+ │ ├── test_datasets_helper.py label_subset
122
+ │ ├── test_import.py
123
+ │ ├── _rng.py audited round-trip helpers (data generation, bit equality)
124
+ │ ├── test_rng.py tests for those helpers
125
+ │ └── _datasets.py dev-only fixtures (MNIST, etc), NOT public API
126
+ ├── lab/ ephemeral experiments; see lab/README.md
127
+ │ ├── README.md bench rules (tracked; the scripts themselves are not)
128
+ │ └── .gitignore ignores everything else (tracked)
129
+ ├── docs/
130
+ │ ├── GUIDE.md the manual, every README claim measured with its output
131
+ │ ├── PERFORMANCE.md what the accelerator is worth, and how to re-measure
132
+ │ ├── USAGE.md REPL walkthrough of every public symbol (behind, see B4)
133
+ │ ├── SCOPE.md responsibilities matrix + parallelization analysis
134
+ │ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
135
+ │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
136
+ │ ├── ROADMAP.md milestone plan
137
+ │ ├── FOCO-1.0.md what 1.0 freezes and the blockers in the way
138
+ │ ├── ADR/ one file per decision that shaped the API
139
+ │ ├── STUDIES/ background reading behind those decisions
140
+ │ └── superpowers/ the design specs and implementation plans per work phase
141
+ └── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Validation lab
147
+
148
+ The library is "one image in, one tensor out" by design, but you only know it works once you run it end-to-end on real images. That happens in two places, neither of which is part of the shipped package:
149
+
150
+ - [`tests/`](tests/) holds the formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
151
+ - [`lab/`](lab/) holds ephemeral scripts and notebooks for fast hypothesis-checking. See [`lab/README.md`](lab/README.md) for the bench rules; outputs go to `Z:\outputs\patchcraft\` (off-tree).
152
+
153
+ Datasets used by tests/lab are downloaded lazily into `Z:\caches\datasets\<name>\` on first use; they do not ship with the package and are never bundled into the wheel.
154
+
155
+ The dev framework that makes this all work (fixtures, dataset helpers, `Z:\` conventions) is documented in [`docs/AUXILIARY.md`](docs/AUXILIARY.md).
156
+
157
+ ---
158
+
159
+ ## Versioning policy
160
+
161
+ The scheme is [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html),
162
+ expressed in the [PEP 440](https://peps.python.org/pep-0440/) form that PyPI
163
+ accepts. Two rules matter more than the arithmetic, because both were broken
164
+ once and both cost work to undo.
165
+
166
+ **The number names a published artifact, not a unit of work.** Finishing a
167
+ phase, merging a branch and landing a refactor are not releases, and none of
168
+ them touches `__version__`. Everything between releases accumulates under
169
+ `[Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md), and the number is assigned
170
+ in the release commit itself. Versions 0.3.0 and 0.4.0 came from the opposite
171
+ habit: each internal work phase minted a minor, so three numbers existed
172
+ before any of them reached PyPI.
173
+
174
+ **A published version is frozen.** PyPI refuses a filename it has already
175
+ seen, so a version that shipped can only be superseded, never edited. That is
176
+ also why a failed pipeline never costs a number: `skip-existing` makes the
177
+ upload a no-op and the same tag can be re-run.
178
+
179
+ **While the project is pre-1.0, the middle digit is the compatibility
180
+ boundary.** SemVer itself leaves `0.y.z` open, saying only that "anything MAY
181
+ change at any time", so the meaning comes from the resolvers instead, and they
182
+ agree: Cargo expands `^0.2.3` to `>=0.2.3, <0.3.0`, npm does the same, and
183
+ PEP 440's `~=0.5.0` means `>=0.5.0, <0.6.0`. In every one of them a `0.y` bump
184
+ is the announcement that something may break, and a `0.y.z` bump is the
185
+ promise that nothing does.
186
+
187
+ That fixes what each digit is for here:
188
+
189
+ | Change | Bump | Why |
190
+ |---|---|---|
191
+ | An output value, a signature, a name or documented behaviour changes | `0.Y.0` | Resolvers stop at the y boundary, so this is where a break belongs |
192
+ | New functionality that breaks nothing, or a fix | `0.y.Z` | Anyone pinned to the y-series should get it |
193
+ | Documentation, tests, CI, packaging or tooling only | `0.y.Z` | Still a release if it is published, still no break |
194
+ | Nothing published | nothing | Accumulate under `[Unreleased]` |
195
+
196
+ After 1.0 the ordinary reading applies: breaking is major, additive is minor,
197
+ fixed is patch. What 1.0 freezes is written in
198
+ [`docs/FOCO-1.0.md`](docs/FOCO-1.0.md).
199
+
200
+ ### Errata
201
+
202
+ The rule above was written after the fact, and two releases predate it.
203
+
204
+ `0.4.0` should have been `0.3.1`. It added `accel_available()` and nothing
205
+ else: no signature changed, and the accelerated path was bit-exact against the
206
+ pure one. Additive and non-breaking is a z-bump under the boundary rule, and
207
+ minting a y announced a break that never happened.
208
+
209
+ `0.5.1` was briefly numbered `0.6.0` before release. It changes only how the
210
+ package is built and shipped, so it is a z-bump.
211
+
212
+ Two others were right and are worth recording as the shape to copy. `0.3.0`
213
+ moved `stitch` output by ULPs through a different summation order, which is an
214
+ output change and belongs at a y boundary. `0.5.0` made `tilings` return 73
215
+ specs where it returned 100, which is squarely a break.
216
+
217
+ The accelerator in [`accel/`](accel/) has no version of its own to manage. It
218
+ is compiled into this wheel, so it ships when `patchcraft` ships. The version
219
+ in `accel/Cargo.toml` is internal and nothing reads it; what the Python side
220
+ actually checks is `_ABI_VERSION` in `accel/src/lib.rs`, and a mismatch there
221
+ means a silent fall back to the pure path rather than a crash.
222
+
223
+ ---
224
+
225
+ ## Where the version comes from
226
+
227
+ The git tag, and nowhere else. `setuptools-scm` derives it at build time, so
228
+ no file in the source states a version and there is no literal to forget.
229
+ `v0.5.1` produces `0.5.1`; the leading `v` is stripped for you.
230
+
231
+ At build time setuptools-scm writes `src/patchcraft/_version.py`, which
232
+ `__init__.py` imports and re-exports as `patchcraft.__version__`. That file is
233
+ generated and gitignored, so a checkout that was never built falls back to
234
+ `importlib.metadata` and then to `0+unknown`. The fallback exists so an
235
+ un-built tree still imports; it is not a supported way to learn the version.
236
+
237
+ Anything other than a clean tree sitting exactly on a tag produces a version
238
+ with a local segment, like `0.5.2.dev0+gb4edfd81.d20260902` from a dirty tree
239
+ or `0.1.dev1+gb4edfd81` from a checkout whose tags were never fetched. PyPI
240
+ refuses local segments outright, so such a build can never be published by
241
+ accident, and `tools/check_dist.py` rejects it before the upload rather than
242
+ letting the upload fail.
243
+
244
+ That last point is why every `actions/checkout` that builds uses
245
+ `fetch-depth: 0`. The default shallow clone hides the tags, and the failure is
246
+ silent: a version that walks backwards rather than an error.
247
+
248
+ ---
249
+
250
+ ## Releasing (maintainer only)
251
+
252
+ 1. Close the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md) as `[X.Y.Z] YYYY-MM-DD`.
253
+ 2. Update the `version` field of the BibTeX entry in [`docs/GUIDE.md`](docs/GUIDE.md) section 9. It is the one version string left in the documentation, because a citation needs a concrete one.
254
+ 3. Update [`docs/ROADMAP.md`](docs/ROADMAP.md) milestone checkboxes.
255
+ 4. Commit: `release: vX.Y.Z`.
256
+ 5. Tag + push: `git tag -a vX.Y.Z -m "..."` then `git push origin vX.Y.Z`.
257
+ 6. `release.yml` fires automatically: validates, builds, publishes to PyPI via Trusted Publishing, then creates the GitHub Release with every artifact attached.
258
+
259
+ There is no version to bump in step 4, and no way for a tag and a release to
260
+ disagree. The `validate` job still cross-checks that setuptools-scm derived
261
+ exactly what the tag says, which catches a dirty or shallow checkout before
262
+ anything is built.
263
+
264
+ One release produces one sdist and six wheels: five `cp312-abi3-<platform>`
265
+ wheels with the Rust extension inside, and one `py3-none-any` wheel for
266
+ everywhere else. `tools/check_dist.py` runs before the upload and fails the
267
+ release if a platform wheel lost its extension, if the universal wheel gained
268
+ one, if the artifacts disagree about the version, or if any of them is not the
269
+ version the tag names. Publishing is a single project, so there is nothing to
270
+ register on PyPI beyond the publisher that already exists.
@@ -0,0 +1,37 @@
1
+ # The sdist is the reproducible source snapshot: the product (`src/`, `accel/`)
2
+ # plus the track that proves it works (`tests/`, `docs/`). `accel/` has to be
3
+ # here, because an sdist build compiles it when a Rust toolchain is present.
4
+ #
5
+ # The `prune` lines below are load-bearing rather than decorative, and the
6
+ # reason changed with setuptools-scm. Its file finder offers every git-tracked
7
+ # file to the sdist, so this file stopped being an allowlist and became a
8
+ # blocklist over everything git knows about. Measured: with no prune for it,
9
+ # `.vscode/settings.json` reached the sdist.
10
+ #
11
+ # Note that `lab/` is only *mostly* ignored: `lab/README.md` and
12
+ # `lab/.gitignore` are tracked, so the finder offers them and the prune is what
13
+ # stops them. The rest of `lab/` is scratch work that no commit can reproduce,
14
+ # and patchcraft-0.2.0.tar.gz shipped three such files to PyPI.
15
+ #
16
+ # Adding a tracked directory to this repository ships it to PyPI by default.
17
+ # `tools/check_dist.py` holds the matching allowlist and will fail the release
18
+ # for anything unexpected, which is the reminder to come back here.
19
+
20
+ include LICENSE
21
+ include CHANGELOG.md CONTRIBUTING.md
22
+ include README.md README.pt-BR.md README.pypi.md
23
+ include pyproject.toml setup.py uv.lock .python-version
24
+
25
+ recursive-include src *.py
26
+ include src/patchcraft/py.typed
27
+ recursive-include tests *.py
28
+ recursive-include docs *.md
29
+ recursive-include accel *.rs *.toml *.lock *.md
30
+ prune accel/target
31
+ recursive-include tools *.py
32
+
33
+ prune lab
34
+ prune archive
35
+ prune .superpowers
36
+ prune .github
37
+ prune .vscode