patchcraft 0.5.5__tar.gz → 0.5.6__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 (95) hide show
  1. {patchcraft-0.5.5 → patchcraft-0.5.6}/.pre-commit-config.yaml +2 -2
  2. {patchcraft-0.5.5 → patchcraft-0.5.6}/AGENTS.md +5 -3
  3. {patchcraft-0.5.5 → patchcraft-0.5.6}/CHANGELOG.md +154 -1
  4. {patchcraft-0.5.5 → patchcraft-0.5.6}/CITATION.cff +5 -6
  5. {patchcraft-0.5.5 → patchcraft-0.5.6}/CONTRIBUTING.md +7 -6
  6. {patchcraft-0.5.5 → patchcraft-0.5.6}/MANIFEST.in +4 -0
  7. {patchcraft-0.5.5 → patchcraft-0.5.6}/MAP.md +5 -1
  8. {patchcraft-0.5.5 → patchcraft-0.5.6}/PKG-INFO +6 -6
  9. {patchcraft-0.5.5 → patchcraft-0.5.6}/README.md +3 -3
  10. {patchcraft-0.5.5 → patchcraft-0.5.6}/README.pt-BR.md +4 -4
  11. {patchcraft-0.5.5 → patchcraft-0.5.6}/README.pypi.md +5 -5
  12. {patchcraft-0.5.5 → patchcraft-0.5.6}/STATUS.md +26 -13
  13. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/ADR/0003-reversibility-classes.md +7 -2
  14. patchcraft-0.5.6/docs/ADR/0005-border-policy.md +94 -0
  15. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/FOCO-1.0.md +7 -7
  16. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/GUIDE.md +178 -13
  17. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/PERFORMANCE.md +1 -1
  18. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/ROADMAP.md +7 -1
  19. patchcraft-0.5.6/docs/STUDIES/2026-10-10-border-policy.md +113 -0
  20. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/THEORY.md +27 -8
  21. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/USAGE.md +163 -18
  22. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/_accel.py +12 -3
  23. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/_version.py +3 -3
  24. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/extract.py +13 -8
  25. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/geometry.py +10 -4
  26. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/reconstruct.py +25 -18
  27. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/stitch.py +6 -3
  28. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft.egg-info/PKG-INFO +6 -6
  29. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft.egg-info/SOURCES.txt +5 -0
  30. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft.egg-info/scm_file_list.json +17 -0
  31. patchcraft-0.5.6/src/patchcraft.egg-info/scm_version.json +8 -0
  32. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/_rng.py +1 -0
  33. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_accel.py +10 -5
  34. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_exactness.py +28 -1
  35. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_extract.py +4 -2
  36. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_geometry.py +21 -0
  37. patchcraft-0.5.6/tests/test_operator.py +144 -0
  38. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_reconstruct.py +14 -0
  39. patchcraft-0.5.6/tests/test_vmap.py +72 -0
  40. {patchcraft-0.5.5 → patchcraft-0.5.6}/tools/check_dist.py +3 -0
  41. patchcraft-0.5.6/tools/compare_border_strategies.py +116 -0
  42. {patchcraft-0.5.5 → patchcraft-0.5.6}/tools/compare_exactness_rules.py +31 -1
  43. {patchcraft-0.5.5 → patchcraft-0.5.6}/tools/make_outreach_figures.py +21 -9
  44. {patchcraft-0.5.5 → patchcraft-0.5.6}/uv.lock +101 -101
  45. patchcraft-0.5.5/src/patchcraft.egg-info/scm_version.json +0 -8
  46. {patchcraft-0.5.5 → patchcraft-0.5.6}/.gitignore +0 -0
  47. {patchcraft-0.5.5 → patchcraft-0.5.6}/.python-version +0 -0
  48. {patchcraft-0.5.5 → patchcraft-0.5.6}/CODE_OF_CONDUCT.md +0 -0
  49. {patchcraft-0.5.5 → patchcraft-0.5.6}/LICENSE +0 -0
  50. {patchcraft-0.5.5 → patchcraft-0.5.6}/SECURITY.md +0 -0
  51. {patchcraft-0.5.5 → patchcraft-0.5.6}/accel/Cargo.lock +0 -0
  52. {patchcraft-0.5.5 → patchcraft-0.5.6}/accel/Cargo.toml +0 -0
  53. {patchcraft-0.5.5 → patchcraft-0.5.6}/accel/README.md +0 -0
  54. {patchcraft-0.5.5 → patchcraft-0.5.6}/accel/src/kernel.rs +0 -0
  55. {patchcraft-0.5.5 → patchcraft-0.5.6}/accel/src/lib.rs +0 -0
  56. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/ADR/0001-patch-extraction-api.md +0 -0
  57. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/ADR/0002-patchify-transform.md +0 -0
  58. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/ADR/0004-precision-and-effort-parameters.md +0 -0
  59. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/AUXILIARY.md +0 -0
  60. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/SCOPE.md +0 -0
  61. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
  62. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/STUDIES/2026-09-03-precision-measurements.md +0 -0
  63. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/STUDIES/2026-09-03-strata-conformity.md +0 -0
  64. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
  65. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/design/2026-09-01-fase2-acelerador.md +0 -0
  66. {patchcraft-0.5.5 → patchcraft-0.5.6}/docs/design/2026-09-01-fase3-predicado.md +0 -0
  67. {patchcraft-0.5.5 → patchcraft-0.5.6}/pyproject.toml +0 -0
  68. {patchcraft-0.5.5 → patchcraft-0.5.6}/setup.cfg +0 -0
  69. {patchcraft-0.5.5 → patchcraft-0.5.6}/setup.py +0 -0
  70. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/__init__.py +0 -0
  71. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/_foldgeom.py +0 -0
  72. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/cache.py +0 -0
  73. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/metrics.py +0 -0
  74. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/pair.py +0 -0
  75. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/py.typed +0 -0
  76. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft/resize.py +0 -0
  77. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft.egg-info/dependency_links.txt +0 -0
  78. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft.egg-info/requires.txt +0 -0
  79. {patchcraft-0.5.5 → patchcraft-0.5.6}/src/patchcraft.egg-info/top_level.txt +0 -0
  80. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/__init__.py +0 -0
  81. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/_datasets.py +0 -0
  82. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/conftest.py +0 -0
  83. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_cache.py +0 -0
  84. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_citation.py +0 -0
  85. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_datasets_helper.py +0 -0
  86. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_docs_usage.py +0 -0
  87. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_import.py +0 -0
  88. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_metrics.py +0 -0
  89. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_pair.py +0 -0
  90. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_public_api.py +0 -0
  91. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_reference.py +0 -0
  92. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_resize.py +0 -0
  93. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_rng.py +0 -0
  94. {patchcraft-0.5.5 → patchcraft-0.5.6}/tests/test_stitch.py +0 -0
  95. {patchcraft-0.5.5 → patchcraft-0.5.6}/tools/benchmark.py +0 -0
@@ -1,8 +1,8 @@
1
1
  # Runs the gates CI runs, before the commit rather than after it.
2
2
  #
3
3
  # Deliberately absent: `ruff-format`. This project has never used it and
4
- # adopting it here would reformat 36 of 65 files in a commit that changed no
5
- # behaviour. `ruff check` is the gate; formatting is not.
4
+ # adopting it here would reformat about half the tree (CONTRIBUTING.md has the
5
+ # count) in a commit that changed no behaviour. `ruff check` is the gate; formatting is not.
6
6
  #
7
7
  # `mypy` runs through the project environment rather than through a pre-commit
8
8
  # virtualenv, because it needs torch to resolve any annotation in `src/` and
@@ -128,9 +128,11 @@ connectives; one idea per paragraph. `README.md` is a call page, not a manual:
128
128
  the manual is [`docs/GUIDE.md`](docs/GUIDE.md), and complex examples belong
129
129
  there.
130
130
 
131
- Documents that state a version go stale. There is exactly one version string
132
- left in the documentation, the BibTeX entry in GUIDE section 9, plus
133
- [`CITATION.cff`](CITATION.cff). Do not add a third.
131
+ Documents that state a version go stale. A release bumps three: the BibTeX
132
+ entry in GUIDE section 9, [`CITATION.cff`](CITATION.cff), and the release
133
+ named at the top of [`STATUS.md`](STATUS.md). [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md)
134
+ names the version it measured, and changes only when it is re-measured. Do
135
+ not add another.
134
136
 
135
137
  ## 2. Before you touch anything
136
138
 
@@ -6,6 +6,158 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.6] - 2026-10-10
10
+
11
+ A fix release. `torch.vmap` works again over `extract`, `reconstruct` and
12
+ `stitch`, which it had not since 0.5.0, and `tilings` returns `[]` for an
13
+ image smaller than the default minimum patch instead of raising. No other
14
+ returned value changes. The full suite passes on both code paths at the
15
+ declared floor (Python 3.12, torch 2.6.0, numpy 1.26.0, pillow 10.1.0, Rust
16
+ 1.82) and at the newest releases (Python 3.14, torch 2.14.1, numpy 2.5.4,
17
+ pillow 12.3.0), and the 126,736-geometry exactness sweep passes on both paths.
18
+ The rest is documentation, a CI job that now holds the floor to that claim,
19
+ and the CI actions moved to Node 24.
20
+
21
+ ### Added
22
+
23
+ - `docs/PAPER/`, the scaffold of an article on computing a mean whose every
24
+ intermediate stays in the input's domain, drawing on PatchCraft's
25
+ patch-grid mean: outline, formal statements with proofs, measurements with
26
+ the scripts that reproduce them, and checked references. It is not
27
+ documentation of the library and does not ship: `MANIFEST.in` prunes it and
28
+ `tools/check_dist.py` fails a release that carries it.
29
+ - **CI tests the declared floor, not only the newest versions.** A new job
30
+ builds the extension with Rust 1.82 and runs the whole suite on both code
31
+ paths at Python 3.12, torch 2.6.0, numpy 1.26.0 and pillow 10.1.0, the
32
+ oldest versions `pyproject.toml` and `Cargo.toml` accept that install on
33
+ 3.12; until now the floor was a claim no job exercised. The lock moves to
34
+ the newest runtime releases (torch 2.14.1, torchvision 0.29.1, numpy
35
+ 2.5.4), so the other jobs test the top of the range; the development tools
36
+ stay pinned. The first run at the floor found one claim that held only at
37
+ the top: `extract` on an integer tensor raises torch's own error, which is a
38
+ `RuntimeError` at torch 2.6 and its subclass `NotImplementedError` in newer
39
+ releases, while the test, THEORY §9.1 and the PyPI page named the subclass.
40
+ They now name what holds across the range.
41
+ - ADR 0005 is recorded in the ROADMAP as the 1.1 candidate, not scheduled.
42
+ - ADR 0005 (`Proposed`) records how a grid that does not cover the image
43
+ would be padded, borrowed rather than designed: kornia's auto-padding on the
44
+ way in and unpadding on the way back, MONAI's `padding_mode` with `F.pad`'s
45
+ values, the padding split between both sides, and `None` as the default
46
+ that changes no call. Nothing is implemented. The survey and the
47
+ measurements behind it are in `docs/STUDIES/2026-10-10-border-policy.md`,
48
+ reproduced by `tools/compare_border_strategies.py`.
49
+ - **THEORY §2.6 states the operator behind `reconstruct` and `stitch`.**
50
+ `extract` is a selection matrix `U`, the fold is `Uᵀ`, `UᵀU` is the count
51
+ map, `reconstruct` is the least-squares inverse `(UᵀU)⁻¹Uᵀ` and `stitch` the
52
+ window-weighted one, the same weighted overlap-add that inverts an STFT.
53
+ `tests/test_operator.py` pins each statement on both code paths. GUIDE
54
+ gains three recipes built on it, run against 0.5.5: what averaging does to a
55
+ per-patch quantization (§2), a filter applied per tile with a halo, which
56
+ comes back bit-identical to the whole-image filter (§5), and the padded
57
+ round trip for a frame no tiling covers (§6). Section 6 used to rule
58
+ padding out because it puts pixels the frame never had into the edge
59
+ patches; it now offers it with that cost stated.
60
+
61
+ ### Fixed
62
+
63
+ - **`torch.vmap` works again over `extract`, `reconstruct` and `stitch`.** The
64
+ documentation names vmap as one of the three ways to batch (README, SCOPE,
65
+ USAGE, THEORY §0 and §9.1), and since 0.5.0 it failed on most calls: `extract`
66
+ at every geometry without dilation and `reconstruct` at `stride ==
67
+ patch_size` asked the tensor for its storage to guard against aliasing, which
68
+ a vmap batched tensor does not have, and with the accelerator every
69
+ overlapping `reconstruct` and `stitch` failed reading a data pointer. Both
70
+ guards are now an unconditional clone, which is the one copy those paths
71
+ always made, and the accelerator treats a tensor without storage as
72
+ ineligible and hands it to the torch path, as its docstring already said.
73
+ Bit-identical to 0.5.5 on ordinary calls in 144 comparisons per path,
74
+ including the degenerate geometries the guard existed for, with no output
75
+ sharing memory with its input and no change in time. `tests/test_vmap.py`
76
+ pins vmap to a plain loop bit for bit, on both paths and in four dtypes; 70
77
+ of its 114 cases fail against 0.5.5. The test helper `bit_equal` gained
78
+ bfloat16.
79
+ - **`tilings` returns `[]` for an image smaller than the default minimum
80
+ patch.** A one-row image raised `min_patch_size (2) > max_patch_size (1)`,
81
+ blaming a bound the caller never passed, while THEORY §9.6 accepts any
82
+ positive shape and an explicit bound that excludes every patch already
83
+ returned `[]`. An explicit `max_patch_size` below `min_patch_size` still
84
+ raises. The docstring no longer promises at least one entry, which was false
85
+ for both cases.
86
+
87
+ ### Changed
88
+
89
+ - **CI actions moved off Node 20**, which GitHub has deprecated and was
90
+ forcing onto Node 24: `actions/checkout` v5, `astral-sh/setup-uv` v7,
91
+ `actions/upload-artifact` v6, `actions/download-artifact` v7 and
92
+ `softprops/action-gh-release` v3, each the first major on Node 24 rather
93
+ than the newest. Every input the workflows pass exists in those versions.
94
+ - **USAGE walks a 4×4 image through every call.** Its sections now show
95
+ the pixels, not only the shapes: the patches `extract` returns and their
96
+ order, where each patch lands in `reconstruct`, the per-pixel mean when
97
+ patches disagree, the step `uniform` leaves and the ramp `hann` and
98
+ `gaussian` make, an LR patch beside its HR patch in `pair`, a resize down
99
+ and back up, and the one patch `per_patch_mse` singles out. Every example
100
+ runs as a doctest. GUIDE §1 shows the uncovered band on a 5×5 count map, and
101
+ GUIDE §4 shows the count map beside the pixels that come back different.
102
+ - GUIDE §4 still gave the old reason for the exactness rule, that division by
103
+ a power of two is exact; it now gives the corrected one, as the other pages
104
+ do since the entry below.
105
+ - **The stated reason for the exactness rule is corrected.** The pages said the
106
+ round trip is exact because dividing a float by a power of two never rounds.
107
+ That leaves out the sum: k equal values added one at a time and divided by k
108
+ come back exact for k = 2 and 4, and for k = 8 they do not, since 3x, 5x, 6x
109
+ and 7x round (3,914,684 of the 8,388,608 float32 values in [1, 2) fail,
110
+ `python docs/PAPER/experiments/float_mean.py`). The rule itself stands,
111
+ because a coverage of three on either axis puts a 3 in the map, so over all
112
+ 126,736 legal geometries a map of powers of two holds only 1, 2 and 4. The
113
+ READMEs, THEORY §1, the `reconstruct` docstring, ADR 0003 (with a dated
114
+ correction) and the outreach sources now give that reason.
115
+ - Exact inside the rule means equal as values, which is what `torch.equal`
116
+ checks, and THEORY and the `reconstruct` docstring now say so: the fold sums
117
+ into `+0.0`, so on an overlapping grid a `-0.0` comes back `+0.0` and a NaN
118
+ may change its bits. Both paths do the same, and a test now pins it, which
119
+ `torch.equal` could not.
120
+ - THEORY §9.2 said promotion cut the bfloat16 error from 9.3e-03 to 1.9e-03.
121
+ Those two numbers measured the input's cast from float32, not the library:
122
+ on data drawn in bfloat16 the native fold misses by 7.8e-03 and the promoted
123
+ round trip returns the input bit for bit, at counts 3, 6 and 9. THEORY,
124
+ GUIDE and the code comment now say so, and a test pins it.
125
+ - THEORY §2 and §2.5 and USAGE §5 described the `reconstruct` and `stitch`
126
+ implementation from before 0.5.0 (a second fold of ones, a floor on every
127
+ denominator). They now describe the closed-form count map, the separable
128
+ window denominator and the non-overlapping rearrangement. USAGE §5 checks
129
+ the overlapping round trip with `torch.equal` instead of `torch.allclose`.
130
+ - THEORY §9.6 said `tilings` returns a sorted list. Its order is patch size
131
+ ascending with the exact tile first, which `sorted()` does not give, and a
132
+ test now pins it.
133
+ - The accelerator parity tests compare bits instead of `torch.equal`, and
134
+ include `stride == patch_size`, where hann and gaussian still fold now that
135
+ uniform takes the fast path.
136
+ - Errata for 0.5.5, whose entry says it "changes no bit": that holds for
137
+ returned values in eager mode. Under the uniform window at `stride ==
138
+ patch_size`, derivatives pass a `-0.0` or a signaling NaN through unchanged
139
+ and `torch.compile` folds the `+ 0.0` away; the code comment now says so.
140
+ - **Documentation that said more than the code does.** GUIDE called hann the
141
+ cheapest window, where on an overlapping grid the three cost the same; GUIDE
142
+ and the PyPI page said `extract` hands every tensor to `F.unfold`, where only
143
+ an integer one goes there; PERFORMANCE said 25 runs where the benchmark
144
+ takes 15; GUIDE's two bound figures now read as the measured misses inside
145
+ the `(k+1)·eps·|v|` bound, with the shape of the fp16 example stated.
146
+ FOCO-1.0 freezes values rather than bits, the bound rather than "1 ULP",
147
+ and 20 names. CONTRIBUTING and AGENTS name the three version strings a
148
+ release bumps, STATUS among them. The CITATION abstract no longer carries
149
+ development history, and the BibTeX author reads `Marques de Souza`.
150
+ - `tools/compare_exactness_rules.py` also reports the worst miss outside the
151
+ rule: 19.0 ULP in float32, at `(h, w, ph, pw, sh, sw) = (18, 23, 9, 9, 1,
152
+ 1)`, on a pixel covered 81 times. CI's ruff step checks `tools/` as well.
153
+ - Outreach: a news source dated 2026-10-10 carries the 0.5.5 table, and the
154
+ 2026-09-04 one stays as the record of 0.5.4. The articles follow it, scope
155
+ the 19 ULP figure to the geometry that produced it, and say where each path
156
+ was executed instead of calling the GPU path correct. The post no longer
157
+ says `extract` validates the geometry, the MNIST page states when each of
158
+ the three problems appears, and every reproduction block clones the
159
+ repository, since the wheel carries neither `tools/` nor `tests/`.
160
+
9
161
  ## [0.5.5] - 2026-09-26
10
162
 
11
163
  A performance release that changes no bit. Every change to `src/` below was
@@ -1016,7 +1168,8 @@ First public release. Public API stable; signatures will only change in 1.x.
1016
1168
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
1017
1169
  validation lab.
1018
1170
 
1019
- [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.5...HEAD
1171
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.6...HEAD
1172
+ [0.5.6]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.6
1020
1173
  [0.5.5]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.5
1021
1174
  [0.5.4]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.4
1022
1175
  [0.5.3]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.3
@@ -6,8 +6,8 @@ authors:
6
6
  - given-names: Leonardo
7
7
  family-names: Marques de Souza
8
8
  email: leonardo.marques.souza@gmail.com
9
- version: "0.5.5"
10
- date-released: "2026-09-26"
9
+ version: "0.5.6"
10
+ date-released: "2026-10-10"
11
11
  repository-code: "https://github.com/LeoPR/PatchCraft"
12
12
  license: MIT
13
13
  abstract: >
@@ -28,10 +28,9 @@ abstract: >
28
28
  every value of the overlap coverage map is a power of two, always true at
29
29
  stride equal to patch size, and outside that rule the per-pixel error is
30
30
  bounded by (k+1) times eps times the absolute value, with k the pixel's
31
- coverage count. An earlier and weaker form of that claim was published, then
32
- measured false and publicly retracted; a falsification suite now enumerates
33
- the 126,736 legal geometries of the space independently of the predicate and
34
- tries to break both halves of it.
31
+ coverage count. A falsification suite enumerates the 126,736 legal
32
+ geometries of the space independently of the predicate and tries to break
33
+ both halves of it.
35
34
 
36
35
  Five of the six published wheels carry a Rust kernel for the overlapping
37
36
  fold, compiled into the wheel itself rather than into a separate package, and
@@ -34,7 +34,7 @@ is silently inert here. Either make that global hook delegate, or set
34
34
  whatever else the global hooks were doing.
35
35
 
36
36
  `ruff-format` is deliberately not among the hooks. This project has never used
37
- it, and adopting it would reformat 37 of 82 files in a commit that changed no
37
+ it, and adopting it would reformat 37 files in a commit that changed no
38
38
  behaviour.
39
39
 
40
40
  ```
@@ -153,7 +153,7 @@ PatchCraft/
153
153
  │ ├── ISSUE_TEMPLATE/ four forms and a chooser; the required fields are what makes a report reproducible
154
154
  │ ├── PULL_REQUEST_TEMPLATE.md asks whether a returned value moved, which decides the version bump
155
155
  │ └── workflows/
156
- │ ├── test.yml matrix CI on PRs/main, plus the accelerated job
156
+ │ ├── test.yml matrix CI on PRs/main, plus the accelerated and floor jobs
157
157
  │ └── release.yml publishes to PyPI on vX.Y.Z tag push (Trusted Publishing)
158
158
  ├── src/patchcraft/ library core, one-image-at-a-time primitives
159
159
  │ ├── __init__.py re-exports the full public API and the generated version
@@ -207,11 +207,12 @@ PatchCraft/
207
207
  │ ├── FOCO-1.0.md what 1.0 freezes, and the six blockers, all closed
208
208
  │ ├── ADR/ one file per decision; the choice only, evidence in STUDIES/
209
209
  │ ├── STUDIES/ background reading behind those decisions
210
- │ └── design/ one spec per work phase: the alternatives measured and the decision
210
+ │ ├── design/ one spec per work phase: the alternatives measured and the decision
211
+ │ └── PAPER/ an article in preparation, with its experiments; not shipped
211
212
  ├── outreach/ material for presenting the project publicly, not shipped
212
213
  │ ├── README.md how it is organized and what to check before publishing
213
- │ ├── 2026-09-04-lancamento.md the current dated news source the channel texts derive from
214
- │ ├── 2026-09-03-lancamento.md the previous one, kept as the record of 0.5.1
214
+ │ ├── 2026-10-10-lancamento.md the current dated news source the channel texts derive from
215
+ │ ├── 2026-09-0*-lancamento.md the earlier ones, kept as the records of 0.5.4 and 0.5.1
215
216
  │ └── linkedin/ post, artigo, the illustrated page and the figures, PT and EN
216
217
  └── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
217
218
  ```
@@ -347,7 +348,7 @@ silent: a version that walks backwards rather than an error.
347
348
  ## Releasing (maintainer only)
348
349
 
349
350
  1. Close the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md) as `[X.Y.Z] YYYY-MM-DD`.
350
- 2. Update `version` and `date-released` in [`CITATION.cff`](CITATION.cff), and the `version` field of the BibTeX entry in [`docs/GUIDE.md`](docs/GUIDE.md) section 9. Those are the only version strings left in the repository, because a citation needs a concrete one. `uvx --from cffconvert cffconvert --validate` checks the file.
351
+ 2. Update `version` and `date-released` in [`CITATION.cff`](CITATION.cff), the `version` field of the BibTeX entry in [`docs/GUIDE.md`](docs/GUIDE.md) section 9, and the release named at the top of [`STATUS.md`](STATUS.md). Those are the version strings a release bumps, because a citation needs a concrete one and a snapshot names its release; [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md) names the version it measured and changes only when the table is re-measured. `uvx --from cffconvert cffconvert --validate` checks the file.
351
352
  3. Update [`docs/ROADMAP.md`](docs/ROADMAP.md) milestone checkboxes.
352
353
  4. Commit: `release: vX.Y.Z`.
353
354
  5. Tag + push: `git tag -a vX.Y.Z -m "..."` then `git push origin vX.Y.Z`.
@@ -42,3 +42,7 @@ prune .vscode
42
42
  # Presentation material, not source. It is tracked, so the file finder offers
43
43
  # it and only this line keeps it out.
44
44
  prune outreach
45
+ # An article in preparation, not documentation of the library. Without this
46
+ # line its experiment scripts would ship too, since the finder offers every
47
+ # tracked file.
48
+ prune docs/PAPER
@@ -44,6 +44,7 @@ PatchCraft/
44
44
  ├── tests/ .................... the contract, executable
45
45
  │ ├── test_exactness.py ..... enumerates 126,736 geometries and tries to FALSIFY the predicate
46
46
  │ ├── test_reference.py ..... a naive loop the fast paths must agree with
47
+ │ ├── test_operator.py ...... extract as a matrix U: the fold is U^T, reconstruct and stitch are least squares
47
48
  │ ├── test_public_api.py .... freezes the 20 names, their signatures and carrier fields
48
49
  │ └── _rng.py ............... audited data generation; integer ramps hid a real defect once
49
50
  │
@@ -58,12 +59,14 @@ PatchCraft/
58
59
  │ ├── FOCO-1.0.md ........... what 1.0 freezes, and the six blockers, all closed
59
60
  │ ├── ADR/ .................. one file per decision; the choice, not the research
60
61
  │ ├── design/ ............... one spec per work phase: alternatives measured, then chosen
61
- │ └── STUDIES/ .............. background reading, and the Strata conformity review
62
+ │ ├── STUDIES/ .............. background reading, and the Strata conformity review
63
+ │ └── PAPER/ ................ an article in preparation, on means that stay in their domain; not shipped
62
64
  │
63
65
  ├── tools/
64
66
  │ ├── benchmark.py .......... accelerated vs pure, and proves they agree before timing
65
67
  │ ├── check_dist.py ......... release gate: extension present, versions agree, tag respected
66
68
  │ ├── compare_exactness_rules.py the power-of-two rule against k_max <= 4, measured
69
+ │ ├── compare_border_strategies.py truncate, pad or move the last window, measured
67
70
  │ └── make_outreach_figures.py every outreach figure and generated page, computed
68
71
  │
69
72
  ├── outreach/ ................. material for presenting the project; not shipped
@@ -82,6 +85,7 @@ PatchCraft/
82
85
  | **Work on it as an agent**, or learn what this repo insists on | [AGENTS.md](AGENTS.md) |
83
86
  | **Use it**: every argument, measured, with its printed output | [docs/GUIDE.md](docs/GUIDE.md) |
84
87
  | Know **when the round trip is exact**, as a rule I can evaluate myself | [docs/GUIDE.md §4](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit), and [docs/ADR/0003](docs/ADR/0003-reversibility-classes.md) for the contract |
88
+ | Know **what `reconstruct` and `stitch` compute** for patches a model changed, and how to filter or pad around them | [docs/THEORY.md §2.6](docs/THEORY.md), then [docs/GUIDE.md](docs/GUIDE.md) §2, §5 and §6 for the measured recipes |
85
89
  | Know **what each function accepts and rejects**, precisely | [docs/THEORY.md §9](docs/THEORY.md), which is the arbiter where documents disagree |
86
90
  | Know **whether this library is the right tool** | [docs/SCOPE.md](docs/SCOPE.md) |
87
91
  | Know **what the native accelerator is worth**, and re-measure it | [docs/PERFORMANCE.md](docs/PERFORMANCE.md), then `python tools/benchmark.py` |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchcraft
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: Image patch extraction, reconstruction, pairing and seam-aware stitching for super-resolution and dataset pipelines.
5
5
  Author: Leonardo Marques de Souza
6
6
  License-Expression: MIT
@@ -111,7 +111,7 @@ blended = stitch(edited, image.shape, stride=16, weight="hann")
111
111
  assert blended.shape == image.shape # seams smoothed, geometry preserved
112
112
  ```
113
113
 
114
- PatchCraft accepts float tensors only. An 8-bit image has to become `image.float() / 255` before it reaches `extract`, because `extract` passes the tensor straight to `F.unfold`, and torch has no integer kernel there: it raises `NotImplementedError: "im2col_out_cpu" not implemented for 'Byte'`.
114
+ PatchCraft accepts float tensors only. An 8-bit image has to become `image.float() / 255` before it reaches `extract`, because `extract` routes an integer tensor to `F.unfold`, and torch has no integer kernel there: it raises its own error, `"im2col_out_cpu" not implemented for 'Byte'`.
115
115
 
116
116
  ## reconstruct or stitch
117
117
 
@@ -128,7 +128,7 @@ Uniform averaging is the default, and it is the option that reports what the mod
128
128
 
129
129
  ## Why not unfold and fold directly
130
130
 
131
- Nothing stops you, and PatchCraft is a thin contract over exactly those two calls. What the contract buys is the pixel order and the boundary checks, because the intuitive reshape after `F.unfold` returns a tensor of the right shape whose pixels are scrambled.
131
+ Nothing stops you. What PatchCraft adds is the pixel order and the boundary checks, done once with tests around them, and a faster fold where the wheel carries one, because the intuitive reshape after `F.unfold` returns a tensor of the right shape whose pixels are scrambled.
132
132
 
133
133
  ```python
134
134
  import torch
@@ -144,7 +144,7 @@ assert scrambled.shape == patches.shape # the right shape
144
144
  assert not torch.equal(scrambled, patches) # and the wrong pixels
145
145
  ```
146
146
 
147
- The saving is real on the other side too. Tiling an image, running a per-patch model and blending the result back with a Hann window took 17 non-blank lines by hand against 3 with `extract` and `stitch`, and the two outputs agreed to about 1.2e-05 on a value in [0, 1], summation-order rounding more than 300 times smaller than one 8-bit step.
147
+ The saving is real on the other side too. Tiling an image, running a per-patch model and blending the result back with a Hann window costs 17 non-blank lines by hand against 3 with `extract` and `stitch`, and the two outputs agree to about 1.2e-05 on a value in [0, 1], summation-order rounding more than 300 times smaller than one 8-bit step.
148
148
 
149
149
  ## The geometry has to cover the image
150
150
 
@@ -162,7 +162,7 @@ It helps when you tile one image for an inference pass too large to run in a sin
162
162
 
163
163
  ## When the round trip is bit for bit
164
164
 
165
- The round trip is exact when every value in the count map is a power of two. The reason is that reconstruction divides each pixel by the number of patches that covered it, and dividing a float by a power of two is the one division that never rounds.
165
+ The round trip is exact when every value in the count map is a power of two. Reconstruction adds up the patches covering each pixel and divides by how many there were, and in a legal geometry a map of powers of two holds only the counts 1, 2 and 4, where both steps are exact: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4.
166
166
 
167
167
  That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule the per-pixel error is bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count. A wider float buys a smaller miss and never exactness.
168
168
 
@@ -178,7 +178,7 @@ Where it applies today: on GPU the functions accept CUDA tensors and keep the de
178
178
 
179
179
  - [Guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md), the manual, with every figure on this page shown as runnable code
180
180
  - [Performance](https://github.com/LeoPR/PatchCraft/blob/main/docs/PERFORMANCE.md), what the native accelerator is worth and how to re-measure it
181
- - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of each of the 20 public symbols, executed as a doctest
181
+ - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of the public surface, executed as a doctest
182
182
  - [Theory](https://github.com/LeoPR/PatchCraft/blob/main/docs/THEORY.md), the math and the per-function contract
183
183
  - [Scope](https://github.com/LeoPR/PatchCraft/blob/main/docs/SCOPE.md), the line between this library and your pipeline
184
184
  - [Repository](https://github.com/LeoPR/PatchCraft), [issues](https://github.com/LeoPR/PatchCraft/issues) and [contributing](https://github.com/LeoPR/PatchCraft/blob/main/CONTRIBUTING.md)
@@ -140,13 +140,13 @@ PatchCraft does not help when you want a batched op over N images, a dataset or
140
140
 
141
141
  ## When the round trip is bit for bit
142
142
 
143
- The round trip is exact when every value in the count map is a power of two, which happens because dividing a float by a power of two is the one division that never rounds. Outside that rule the per-pixel error is bounded by (k+1)·eps·|v|, with k the pixel's coverage count. A wider float buys a smaller miss and never exactness, so float64 is not a safe harbour. The axis that decides the answer is the geometry and not the dtype, and [the guide](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) has the sweep that measures it.
143
+ The round trip is exact when every value in the count map is a power of two, which in a legal geometry means every count is 1, 2 or 4: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4. Outside that rule the per-pixel error is bounded by (k+1)·eps·|v|, with k the pixel's coverage count. A wider float buys a smaller miss and never exactness, so float64 is not a safe harbour. The axis that decides the answer is the geometry and not the dtype, and [the guide](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) has the sweep that measures it.
144
144
 
145
145
  ## Status
146
146
 
147
147
  **Pre-1.0**, and the badge above is the current release. While the leading digit is zero the middle one is the compatibility boundary, so `0.y.z` is safe to take and a new `0.y` is where an output value or a signature is allowed to move. [CHANGELOG.md](CHANGELOG.md) records every such change with the measurement behind it.
148
148
 
149
- 1657 tests collected and CI is green on {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, with `ruff check` and `mypy --strict` in the same run. The package is typed and ships `py.typed`.
149
+ CI is green on {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14} with the newest torch, and on the declared floor, Python 3.12 with torch 2.6, numpy 1.26, pillow 10.1 and Rust 1.82, with `ruff check` and `mypy --strict` in the same run. The package is typed and ships `py.typed`.
150
150
 
151
151
  On GPU the functions keep the device you hand them, and the Rust kernel is CPU-only, so it does not accelerate there; every figure here is a CPU figure. No external project consumes the library yet, which is the gate for calling the API settled. [The guide](docs/GUIDE.md#8-where-this-applies-today) says where the rest applies.
152
152
 
@@ -156,7 +156,7 @@ On GPU the functions keep the device you hand them, and the Rust kernel is CPU-o
156
156
  |---|---|
157
157
  | The manual: every argument above, measured, with its output | [docs/GUIDE.md](docs/GUIDE.md) |
158
158
  | What the native accelerator is worth, and how to re-measure it | [docs/PERFORMANCE.md](docs/PERFORMANCE.md) |
159
- | A walkthrough of each of the 20 public symbols, executed as a doctest | [docs/USAGE.md](docs/USAGE.md) |
159
+ | A walkthrough of the public surface, executed as a doctest | [docs/USAGE.md](docs/USAGE.md) |
160
160
  | The line between this library and your pipeline | [docs/SCOPE.md](docs/SCOPE.md) |
161
161
  | The math, the decisions and the per-function contract | [docs/THEORY.md](docs/THEORY.md) |
162
162
  | Why the API looks like this | [docs/ADR/](docs/ADR) |
@@ -106,7 +106,7 @@ assert scrambled.shape == patches.shape # a forma certa
106
106
  assert not torch.equal(scrambled, patches) # e os pixels errados
107
107
  ```
108
108
 
109
- O segundo é um stride que não cobre a imagem. Numa imagem de 128 por 128 com `patch=32, stride=20`, a grade para no pixel 112, o que deixa 3840 dos 16384 pixels em zero, e um `fold` escrito à mão devolve essa imagem parcialmente preta sem reclamar.
109
+ O segundo é um stride, a distância de um patch para o próximo, que não cobre a imagem. Numa imagem de 128 por 128 com `patch=32, stride=20`, a grade para no pixel 112, o que deixa 3840 dos 16384 pixels em zero, e um `fold` escrito à mão devolve essa imagem parcialmente preta sem reclamar.
110
110
 
111
111
  Escrever o laço de recortar e remontar à mão custa 17 linhas não vazias contra 3 aqui, e os dois resultados concordam em cerca de 1,2e-05 num valor em [0, 1], mais de 300 vezes menos que um degrau de 8 bits, com as duas rotas diferindo só na ordem em que somam uma janela separável. [O manual](docs/GUIDE.md#1-why-not-unfold-and-fold-directly) roda as duas versões lado a lado.
112
112
 
@@ -140,13 +140,13 @@ O PatchCraft não ajuda quando você quer uma operação em lote sobre N imagens
140
140
 
141
141
  ## Quando o round-trip é bit a bit
142
142
 
143
- O round-trip é exato quando todo valor do mapa de cobertura é potência de dois, e isso acontece porque dividir um float por uma potência de dois é a única divisão que nunca arredonda. Fora dessa regra, o erro por pixel é limitado por (k+1)·eps·|v|, com k a contagem de cobertura do pixel. Um float mais largo compra um erro menor e nunca a exatidão, então o float64 não é um porto seguro. Quem decide a resposta é a geometria, e não o dtype. [O manual](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) traz a varredura que mede isso.
143
+ O round-trip é exato quando todo valor do mapa de cobertura é potência de dois, o que numa geometria legal quer dizer que toda contagem é 1, 2 ou 4: somar duas ou quatro cópias de um valor nunca arredonda, e dividir por 2 ou por 4 também não. Fora dessa regra, o erro por pixel é limitado por (k+1)·eps·|v|, com k a contagem de cobertura do pixel. Um float mais largo compra um erro menor e nunca a exatidão, então o float64 não é um porto seguro. Quem decide a resposta é a geometria, e não o dtype. [O manual](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) traz a varredura que mede isso.
144
144
 
145
145
  ## Estado
146
146
 
147
147
  **Pré-1.0**, e o badge acima mostra a versão atual. Enquanto o primeiro dígito for zero, quem marca a fronteira de compatibilidade é o do meio: um `0.y.z` novo é seguro de pegar, e é num `0.y` novo que um valor de saída ou uma assinatura pode mudar. O [CHANGELOG.md](CHANGELOG.md) registra cada uma dessas mudanças com a medição por trás dela.
148
148
 
149
- São 1657 testes coletados, com CI verde em {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, e com `ruff check` e `mypy --strict` na mesma execução. O pacote é tipado e distribui o `py.typed`.
149
+ A CI está verde em {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14} com o torch mais novo, e no piso declarado, Python 3.12 com torch 2.6, numpy 1.26, pillow 10.1 e Rust 1.82, com `ruff check` e `mypy --strict` na mesma execução. O pacote é tipado e distribui o `py.typed`.
150
150
 
151
151
  Em GPU as funções preservam o dispositivo que você passa, e o kernel Rust é só de CPU, então não acelera ali; todo número desta página é de CPU. Nenhum projeto externo consome a biblioteca ainda, que é o critério para dizer a API assentada. [O manual](docs/GUIDE.md#8-where-this-applies-today) diz onde o resto se aplica.
152
152
 
@@ -156,7 +156,7 @@ Em GPU as funções preservam o dispositivo que você passa, e o kernel Rust é
156
156
  |---|---|
157
157
  | O manual: cada argumento acima, medido, com a saída | [docs/GUIDE.md](docs/GUIDE.md) |
158
158
  | Quanto vale o acelerador nativo, e como remedir | [docs/PERFORMANCE.md](docs/PERFORMANCE.md) |
159
- | Um passeio por cada um dos 20 símbolos públicos, executado como doctest | [docs/USAGE.md](docs/USAGE.md) |
159
+ | Um passeio pela superfície pública, executado como doctest | [docs/USAGE.md](docs/USAGE.md) |
160
160
  | A linha entre esta biblioteca e o seu pipeline | [docs/SCOPE.md](docs/SCOPE.md) |
161
161
  | A matemática, as decisões e o contrato por função | [docs/THEORY.md](docs/THEORY.md) |
162
162
  | Por que a API tem essa cara | [docs/ADR/](docs/ADR) |
@@ -69,7 +69,7 @@ blended = stitch(edited, image.shape, stride=16, weight="hann")
69
69
  assert blended.shape == image.shape # seams smoothed, geometry preserved
70
70
  ```
71
71
 
72
- PatchCraft accepts float tensors only. An 8-bit image has to become `image.float() / 255` before it reaches `extract`, because `extract` passes the tensor straight to `F.unfold`, and torch has no integer kernel there: it raises `NotImplementedError: "im2col_out_cpu" not implemented for 'Byte'`.
72
+ PatchCraft accepts float tensors only. An 8-bit image has to become `image.float() / 255` before it reaches `extract`, because `extract` routes an integer tensor to `F.unfold`, and torch has no integer kernel there: it raises its own error, `"im2col_out_cpu" not implemented for 'Byte'`.
73
73
 
74
74
  ## reconstruct or stitch
75
75
 
@@ -86,7 +86,7 @@ Uniform averaging is the default, and it is the option that reports what the mod
86
86
 
87
87
  ## Why not unfold and fold directly
88
88
 
89
- Nothing stops you, and PatchCraft is a thin contract over exactly those two calls. What the contract buys is the pixel order and the boundary checks, because the intuitive reshape after `F.unfold` returns a tensor of the right shape whose pixels are scrambled.
89
+ Nothing stops you. What PatchCraft adds is the pixel order and the boundary checks, done once with tests around them, and a faster fold where the wheel carries one, because the intuitive reshape after `F.unfold` returns a tensor of the right shape whose pixels are scrambled.
90
90
 
91
91
  ```python
92
92
  import torch
@@ -102,7 +102,7 @@ assert scrambled.shape == patches.shape # the right shape
102
102
  assert not torch.equal(scrambled, patches) # and the wrong pixels
103
103
  ```
104
104
 
105
- The saving is real on the other side too. Tiling an image, running a per-patch model and blending the result back with a Hann window took 17 non-blank lines by hand against 3 with `extract` and `stitch`, and the two outputs agreed to about 1.2e-05 on a value in [0, 1], summation-order rounding more than 300 times smaller than one 8-bit step.
105
+ The saving is real on the other side too. Tiling an image, running a per-patch model and blending the result back with a Hann window costs 17 non-blank lines by hand against 3 with `extract` and `stitch`, and the two outputs agree to about 1.2e-05 on a value in [0, 1], summation-order rounding more than 300 times smaller than one 8-bit step.
106
106
 
107
107
  ## The geometry has to cover the image
108
108
 
@@ -120,7 +120,7 @@ It helps when you tile one image for an inference pass too large to run in a sin
120
120
 
121
121
  ## When the round trip is bit for bit
122
122
 
123
- The round trip is exact when every value in the count map is a power of two. The reason is that reconstruction divides each pixel by the number of patches that covered it, and dividing a float by a power of two is the one division that never rounds.
123
+ The round trip is exact when every value in the count map is a power of two. Reconstruction adds up the patches covering each pixel and divides by how many there were, and in a legal geometry a map of powers of two holds only the counts 1, 2 and 4, where both steps are exact: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4.
124
124
 
125
125
  That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule the per-pixel error is bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count. A wider float buys a smaller miss and never exactness.
126
126
 
@@ -136,7 +136,7 @@ Where it applies today: on GPU the functions accept CUDA tensors and keep the de
136
136
 
137
137
  - [Guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md), the manual, with every figure on this page shown as runnable code
138
138
  - [Performance](https://github.com/LeoPR/PatchCraft/blob/main/docs/PERFORMANCE.md), what the native accelerator is worth and how to re-measure it
139
- - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of each of the 20 public symbols, executed as a doctest
139
+ - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of the public surface, executed as a doctest
140
140
  - [Theory](https://github.com/LeoPR/PatchCraft/blob/main/docs/THEORY.md), the math and the per-function contract
141
141
  - [Scope](https://github.com/LeoPR/PatchCraft/blob/main/docs/SCOPE.md), the line between this library and your pipeline
142
142
  - [Repository](https://github.com/LeoPR/PatchCraft), [issues](https://github.com/LeoPR/PatchCraft/issues) and [contributing](https://github.com/LeoPR/PatchCraft/blob/main/CONTRIBUTING.md)
@@ -1,4 +1,4 @@
1
- # STATUS: 2026-09-26
1
+ # STATUS: 2026-10-10
2
2
 
3
3
  > The snapshot. Where things are *right now*, and who each open item waits on.
4
4
  > Where things **are** is [MAP.md](MAP.md); the rules are [AGENTS.md](AGENTS.md);
@@ -6,31 +6,44 @@
6
6
 
7
7
  ## Where the project is
8
8
 
9
- **0.5.5 is on PyPI**, published through Trusted Publishing on a tag push: one
9
+ **0.5.6 is on PyPI**, published through Trusted Publishing on a tag push: one
10
10
  sdist, five `cp312-abi3` platform wheels carrying the Rust accelerator, and one
11
11
  universal `py3-none-any` wheel. There is no extra to enable and no second
12
12
  package.
13
13
 
14
- **1620 tests pass** under `pytest -m "not gpu"`, with 32 skipped and 5
14
+ **1818 tests pass** under `pytest -m "not gpu"`, with 32 skipped and 5
15
15
  deselected, plus two full-sweep gates behind `PATCHCRAFT_SWEEP_FULL=1`
16
16
  that enumerate all 126,736 legal geometries. CI is green on
17
17
  {Ubuntu, Windows} x {3.12, 3.13, 3.14} on the pure path, plus a two-OS job that
18
- builds the Rust kernel and runs the whole suite through it.
18
+ builds the Rust kernel and runs the whole suite through it, all at the newest
19
+ torch, and a job at the declared floor (Python 3.12, torch 2.6, numpy 1.26,
20
+ pillow 10.1, Rust 1.82) that runs it on both paths.
19
21
 
20
22
  **The surface is 20 names**, pinned by `tests/test_public_api.py` with
21
23
  `inspect.signature`.
22
24
 
23
25
  ## What is in flight
24
26
 
25
- **Nothing unreleased.** 0.5.5 is a performance release that changes no bit:
26
- `stitch` folds its window without a Python loop and skips the fold for the
27
- uniform window on non-overlapping grids, `reconstruct` builds its count map
28
- with fewer operations, both divide in place, and `tilings` visits only the
29
- strides that can work. Each was compared against the 0.5.4 code through
30
- integer views, 53,716 cases with 0 differences, and the full 126,736-geometry
31
- sweep passes on both paths. Alongside it, the surface pages lost their
32
- development history, GUIDE section 8 became "Where this applies today", and
33
- docs/PERFORMANCE.md was re-measured on the code that ships.
27
+ **Nothing unreleased.** 0.5.6 is a fix release: `torch.vmap` over `extract`,
28
+ `reconstruct` and `stitch`, which the documentation offers as a way to batch,
29
+ failed on most calls since 0.5.0 and works again, pinned by
30
+ `tests/test_vmap.py`; and `tilings` returns `[]` for an image smaller than the
31
+ default minimum patch instead of raising. With them came the corrected reason
32
+ for the exactness rule, THEORY §2.6 on the operator, the USAGE walk through a
33
+ 4×4 image, and a CI job at the declared floor, whose first run found a claim
34
+ that held only at the top. The full 126,736-geometry sweep passes on both
35
+ paths.
36
+
37
+ **ADR 0005 is `Proposed`**: padding a grid that does not cover the image, with
38
+ the design taken from kornia's auto-padding and MONAI's `padding_mode`, one
39
+ keyword with a default that changes no call. Nothing is implemented. The owner
40
+ recorded it in the ROADMAP as the 1.1 candidate rather than scheduling it, so it
41
+ **waits on a consumer**.
42
+
43
+ **`docs/PAPER/` is a scaffold** for an article on means that never leave
44
+ their domain: outline, formal statements, measurements and references, with
45
+ the prose not yet written. It does not ship. Its first open item, a search for
46
+ prior art on the integer carry, **waits on the owner**, as does the writing.
34
47
 
35
48
  ## What is blocked, and on whom
36
49
 
@@ -58,8 +58,13 @@ be false over part of their accepted input.
58
58
  **Geometry.** The `extract`/`reconstruct` round trip is bit-exact when every
59
59
  value of the overlap coverage map is a power of two, and not otherwise. The
60
60
  reason is arithmetic rather than statistical: the fold sums `k` copies of a value
61
- and divides by `k`, which is exact in binary floating point exactly when `k` is a
62
- power of two. Every exact tiling qualifies, since all its counts are 1.
61
+ and divides by `k`. Dividing by a power of two never rounds, and for `k` = 2 and 4
62
+ the sum of equal values is exact too, short of overflow. For `k` = 8 it is not,
63
+ since the partial sums 3x, 5x, 6x and 7x round; the rule holds anyway because a
64
+ coverage of three or more on either axis puts a 3 in the map, so a map of powers
65
+ of two only ever holds 1, 2 and 4, which is what all 126,736 legal geometries
66
+ show. (Corrected 2026-10-10: the earlier wording credited every power of two.)
67
+ Every exact tiling qualifies, since all its counts are 1.
63
68
 
64
69
  **Accumulator.** ADR 0004 measured the sentence above to carry an unstated
65
70
  premise, which is the accumulator the library happens to use. Over 76 legal