patchcraft 0.5.5__tar.gz → 0.5.7__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 (96) hide show
  1. {patchcraft-0.5.5 → patchcraft-0.5.7}/.pre-commit-config.yaml +2 -2
  2. {patchcraft-0.5.5 → patchcraft-0.5.7}/AGENTS.md +5 -3
  3. {patchcraft-0.5.5 → patchcraft-0.5.7}/CHANGELOG.md +194 -1
  4. {patchcraft-0.5.5 → patchcraft-0.5.7}/CITATION.cff +5 -6
  5. {patchcraft-0.5.5 → patchcraft-0.5.7}/CONTRIBUTING.md +7 -6
  6. {patchcraft-0.5.5 → patchcraft-0.5.7}/MANIFEST.in +4 -0
  7. {patchcraft-0.5.5 → patchcraft-0.5.7}/MAP.md +5 -1
  8. {patchcraft-0.5.5/src/patchcraft.egg-info → patchcraft-0.5.7}/PKG-INFO +7 -7
  9. {patchcraft-0.5.5 → patchcraft-0.5.7}/README.md +4 -4
  10. {patchcraft-0.5.5 → patchcraft-0.5.7}/README.pt-BR.md +5 -5
  11. {patchcraft-0.5.5 → patchcraft-0.5.7}/README.pypi.md +6 -6
  12. {patchcraft-0.5.5 → patchcraft-0.5.7}/STATUS.md +36 -14
  13. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/ADR/0003-reversibility-classes.md +7 -2
  14. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/ADR/0004-precision-and-effort-parameters.md +8 -4
  15. patchcraft-0.5.7/docs/ADR/0005-border-policy.md +94 -0
  16. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/FOCO-1.0.md +7 -7
  17. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/GUIDE.md +201 -30
  18. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/PERFORMANCE.md +1 -1
  19. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/ROADMAP.md +7 -1
  20. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/SCOPE.md +5 -6
  21. patchcraft-0.5.7/docs/STUDIES/2026-10-10-border-policy.md +113 -0
  22. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/THEORY.md +33 -12
  23. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/USAGE.md +181 -19
  24. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/_accel.py +12 -3
  25. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/_version.py +3 -3
  26. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/extract.py +25 -41
  27. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/geometry.py +10 -4
  28. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/reconstruct.py +99 -25
  29. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/stitch.py +6 -3
  30. {patchcraft-0.5.5 → patchcraft-0.5.7/src/patchcraft.egg-info}/PKG-INFO +7 -7
  31. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft.egg-info/SOURCES.txt +6 -0
  32. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft.egg-info/scm_file_list.json +18 -0
  33. patchcraft-0.5.7/src/patchcraft.egg-info/scm_version.json +8 -0
  34. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/_rng.py +5 -1
  35. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_accel.py +10 -5
  36. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_exactness.py +28 -1
  37. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_extract.py +15 -9
  38. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_geometry.py +21 -0
  39. patchcraft-0.5.7/tests/test_integer.py +191 -0
  40. patchcraft-0.5.7/tests/test_operator.py +144 -0
  41. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_reconstruct.py +19 -5
  42. patchcraft-0.5.7/tests/test_vmap.py +72 -0
  43. {patchcraft-0.5.5 → patchcraft-0.5.7}/tools/check_dist.py +3 -0
  44. patchcraft-0.5.7/tools/compare_border_strategies.py +116 -0
  45. {patchcraft-0.5.5 → patchcraft-0.5.7}/tools/compare_exactness_rules.py +31 -1
  46. {patchcraft-0.5.5 → patchcraft-0.5.7}/tools/make_outreach_figures.py +21 -9
  47. {patchcraft-0.5.5 → patchcraft-0.5.7}/uv.lock +101 -101
  48. patchcraft-0.5.5/src/patchcraft.egg-info/scm_version.json +0 -8
  49. {patchcraft-0.5.5 → patchcraft-0.5.7}/.gitignore +0 -0
  50. {patchcraft-0.5.5 → patchcraft-0.5.7}/.python-version +0 -0
  51. {patchcraft-0.5.5 → patchcraft-0.5.7}/CODE_OF_CONDUCT.md +0 -0
  52. {patchcraft-0.5.5 → patchcraft-0.5.7}/LICENSE +0 -0
  53. {patchcraft-0.5.5 → patchcraft-0.5.7}/SECURITY.md +0 -0
  54. {patchcraft-0.5.5 → patchcraft-0.5.7}/accel/Cargo.lock +0 -0
  55. {patchcraft-0.5.5 → patchcraft-0.5.7}/accel/Cargo.toml +0 -0
  56. {patchcraft-0.5.5 → patchcraft-0.5.7}/accel/README.md +0 -0
  57. {patchcraft-0.5.5 → patchcraft-0.5.7}/accel/src/kernel.rs +0 -0
  58. {patchcraft-0.5.5 → patchcraft-0.5.7}/accel/src/lib.rs +0 -0
  59. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/ADR/0001-patch-extraction-api.md +0 -0
  60. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/ADR/0002-patchify-transform.md +0 -0
  61. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/AUXILIARY.md +0 -0
  62. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
  63. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/STUDIES/2026-09-03-precision-measurements.md +0 -0
  64. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/STUDIES/2026-09-03-strata-conformity.md +0 -0
  65. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
  66. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/design/2026-09-01-fase2-acelerador.md +0 -0
  67. {patchcraft-0.5.5 → patchcraft-0.5.7}/docs/design/2026-09-01-fase3-predicado.md +0 -0
  68. {patchcraft-0.5.5 → patchcraft-0.5.7}/pyproject.toml +0 -0
  69. {patchcraft-0.5.5 → patchcraft-0.5.7}/setup.cfg +0 -0
  70. {patchcraft-0.5.5 → patchcraft-0.5.7}/setup.py +0 -0
  71. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/__init__.py +0 -0
  72. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/_foldgeom.py +0 -0
  73. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/cache.py +0 -0
  74. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/metrics.py +0 -0
  75. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/pair.py +0 -0
  76. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/py.typed +0 -0
  77. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft/resize.py +0 -0
  78. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft.egg-info/dependency_links.txt +0 -0
  79. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft.egg-info/requires.txt +0 -0
  80. {patchcraft-0.5.5 → patchcraft-0.5.7}/src/patchcraft.egg-info/top_level.txt +0 -0
  81. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/__init__.py +0 -0
  82. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/_datasets.py +0 -0
  83. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/conftest.py +0 -0
  84. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_cache.py +0 -0
  85. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_citation.py +0 -0
  86. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_datasets_helper.py +0 -0
  87. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_docs_usage.py +0 -0
  88. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_import.py +0 -0
  89. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_metrics.py +0 -0
  90. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_pair.py +0 -0
  91. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_public_api.py +0 -0
  92. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_reference.py +0 -0
  93. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_resize.py +0 -0
  94. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_rng.py +0 -0
  95. {patchcraft-0.5.5 → patchcraft-0.5.7}/tests/test_stitch.py +0 -0
  96. {patchcraft-0.5.5 → patchcraft-0.5.7}/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,197 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.7] - 2026-10-10
10
+
11
+ Integer images, end to end. `extract` takes every dtype at every dilation,
12
+ and `reconstruct` takes the five integer dtypes and returns them exactly in
13
+ every legal geometry, since integer sums do not round. Every float result is
14
+ unchanged: `extract`'s new single path is bit-identical to the one it
15
+ replaces. The full suite passes on both code paths at the declared floor and
16
+ at the newest releases, and the 126,736-geometry sweep, now with an integer
17
+ round trip, passes on both paths.
18
+
19
+ ### Added
20
+
21
+ - **Integer images through `extract` and `reconstruct`, exact in every legal
22
+ geometry.** These are steps 1 and 2 of ADR 0004, shipped together as the ADR
23
+ requires. `extract` takes every dtype at every dilation, because it is now
24
+ one strided window view and one copy for all of them: bit-identical to the
25
+ `F.unfold` path it replaced for floats, and 3.9 times faster at dilation 2
26
+ on 3x1024x1024. `reconstruct` takes `uint8`, `int8`, `int16`, `int32` and
27
+ `int64` and returns each pixel's exact mean rounded half to even, the
28
+ rounding `resize` already applies to integers. For patches from `extract`
29
+ that mean is the pixel itself in every legal geometry, with no count-map
30
+ rule, since integer sums do not round. Nothing overflows: up to `int32` the
31
+ sum is taken in `int64`, and `int64` values are split by their pixel's count
32
+ into a quotient and a remainder before they are added. `bool` and complex
33
+ patches are refused with a `ValueError`, `stitch` stays float-only, and the
34
+ accelerator leaves integers to torch. `tests/test_integer.py` checks the
35
+ round trip over the sampled geometry space, all 126,736 geometries under
36
+ `PATCHCRAFT_SWEEP_FULL=1`, for `uint8` and `int64`, and the mean of
37
+ disagreeing patches against Python integers in every dtype; deliberately
38
+ breaking the rounding or the accumulator fails 63 and 90 of its cases.
39
+
40
+ ### Changed
41
+
42
+ - THEORY §1 and SCOPE §2.1 described `extract` as built on `F.unfold`, which
43
+ had been true only at dilation above 1 since 0.3.0 and is not true at all
44
+ now. Both describe the strided view and the copy. THEORY §9.1 and §9.2,
45
+ GUIDE §3, USAGE §5, the three READMEs and ADR 0004 state the integer
46
+ contract.
47
+
48
+ ## [0.5.6] - 2026-10-10
49
+
50
+ A fix release. `torch.vmap` works again over `extract`, `reconstruct` and
51
+ `stitch`, which it had not since 0.5.0, and `tilings` returns `[]` for an
52
+ image smaller than the default minimum patch instead of raising. No other
53
+ returned value changes. The full suite passes on both code paths at the
54
+ declared floor (Python 3.12, torch 2.6.0, numpy 1.26.0, pillow 10.1.0, Rust
55
+ 1.82) and at the newest releases (Python 3.14, torch 2.14.1, numpy 2.5.4,
56
+ pillow 12.3.0), and the 126,736-geometry exactness sweep passes on both paths.
57
+ The rest is documentation, a CI job that now holds the floor to that claim,
58
+ and the CI actions moved to Node 24.
59
+
60
+ ### Added
61
+
62
+ - `docs/PAPER/`, the scaffold of an article on computing a mean whose every
63
+ intermediate stays in the input's domain, drawing on PatchCraft's
64
+ patch-grid mean: outline, formal statements with proofs, measurements with
65
+ the scripts that reproduce them, and checked references. It is not
66
+ documentation of the library and does not ship: `MANIFEST.in` prunes it and
67
+ `tools/check_dist.py` fails a release that carries it.
68
+ - **CI tests the declared floor, not only the newest versions.** A new job
69
+ builds the extension with Rust 1.82 and runs the whole suite on both code
70
+ paths at Python 3.12, torch 2.6.0, numpy 1.26.0 and pillow 10.1.0, the
71
+ oldest versions `pyproject.toml` and `Cargo.toml` accept that install on
72
+ 3.12; until now the floor was a claim no job exercised. The lock moves to
73
+ the newest runtime releases (torch 2.14.1, torchvision 0.29.1, numpy
74
+ 2.5.4), so the other jobs test the top of the range; the development tools
75
+ stay pinned. The first run at the floor found one claim that held only at
76
+ the top: `extract` on an integer tensor raises torch's own error, which is a
77
+ `RuntimeError` at torch 2.6 and its subclass `NotImplementedError` in newer
78
+ releases, while the test, THEORY §9.1 and the PyPI page named the subclass.
79
+ They now name what holds across the range.
80
+ - ADR 0005 is recorded in the ROADMAP as the 1.1 candidate, not scheduled.
81
+ - ADR 0005 (`Proposed`) records how a grid that does not cover the image
82
+ would be padded, borrowed rather than designed: kornia's auto-padding on the
83
+ way in and unpadding on the way back, MONAI's `padding_mode` with `F.pad`'s
84
+ values, the padding split between both sides, and `None` as the default
85
+ that changes no call. Nothing is implemented. The survey and the
86
+ measurements behind it are in `docs/STUDIES/2026-10-10-border-policy.md`,
87
+ reproduced by `tools/compare_border_strategies.py`.
88
+ - **THEORY §2.6 states the operator behind `reconstruct` and `stitch`.**
89
+ `extract` is a selection matrix `U`, the fold is `Uᵀ`, `UᵀU` is the count
90
+ map, `reconstruct` is the least-squares inverse `(UᵀU)⁻¹Uᵀ` and `stitch` the
91
+ window-weighted one, the same weighted overlap-add that inverts an STFT.
92
+ `tests/test_operator.py` pins each statement on both code paths. GUIDE
93
+ gains three recipes built on it, run against 0.5.5: what averaging does to a
94
+ per-patch quantization (§2), a filter applied per tile with a halo, which
95
+ comes back bit-identical to the whole-image filter (§5), and the padded
96
+ round trip for a frame no tiling covers (§6). Section 6 used to rule
97
+ padding out because it puts pixels the frame never had into the edge
98
+ patches; it now offers it with that cost stated.
99
+
100
+ ### Fixed
101
+
102
+ - **`torch.vmap` works again over `extract`, `reconstruct` and `stitch`.** The
103
+ documentation names vmap as one of the three ways to batch (README, SCOPE,
104
+ USAGE, THEORY §0 and §9.1), and since 0.5.0 it failed on most calls: `extract`
105
+ at every geometry without dilation and `reconstruct` at `stride ==
106
+ patch_size` asked the tensor for its storage to guard against aliasing, which
107
+ a vmap batched tensor does not have, and with the accelerator every
108
+ overlapping `reconstruct` and `stitch` failed reading a data pointer. Both
109
+ guards are now an unconditional clone, which is the one copy those paths
110
+ always made, and the accelerator treats a tensor without storage as
111
+ ineligible and hands it to the torch path, as its docstring already said.
112
+ Bit-identical to 0.5.5 on ordinary calls in 144 comparisons per path,
113
+ including the degenerate geometries the guard existed for, with no output
114
+ sharing memory with its input and no change in time. `tests/test_vmap.py`
115
+ pins vmap to a plain loop bit for bit, on both paths and in four dtypes; 70
116
+ of its 114 cases fail against 0.5.5. The test helper `bit_equal` gained
117
+ bfloat16.
118
+ - **`tilings` returns `[]` for an image smaller than the default minimum
119
+ patch.** A one-row image raised `min_patch_size (2) > max_patch_size (1)`,
120
+ blaming a bound the caller never passed, while THEORY §9.6 accepts any
121
+ positive shape and an explicit bound that excludes every patch already
122
+ returned `[]`. An explicit `max_patch_size` below `min_patch_size` still
123
+ raises. The docstring no longer promises at least one entry, which was false
124
+ for both cases.
125
+
126
+ ### Changed
127
+
128
+ - **CI actions moved off Node 20**, which GitHub has deprecated and was
129
+ forcing onto Node 24: `actions/checkout` v5, `astral-sh/setup-uv` v7,
130
+ `actions/upload-artifact` v6, `actions/download-artifact` v7 and
131
+ `softprops/action-gh-release` v3, each the first major on Node 24 rather
132
+ than the newest. Every input the workflows pass exists in those versions.
133
+ - **USAGE walks a 4×4 image through every call.** Its sections now show
134
+ the pixels, not only the shapes: the patches `extract` returns and their
135
+ order, where each patch lands in `reconstruct`, the per-pixel mean when
136
+ patches disagree, the step `uniform` leaves and the ramp `hann` and
137
+ `gaussian` make, an LR patch beside its HR patch in `pair`, a resize down
138
+ and back up, and the one patch `per_patch_mse` singles out. Every example
139
+ runs as a doctest. GUIDE §1 shows the uncovered band on a 5×5 count map, and
140
+ GUIDE §4 shows the count map beside the pixels that come back different.
141
+ - GUIDE §4 still gave the old reason for the exactness rule, that division by
142
+ a power of two is exact; it now gives the corrected one, as the other pages
143
+ do since the entry below.
144
+ - **The stated reason for the exactness rule is corrected.** The pages said the
145
+ round trip is exact because dividing a float by a power of two never rounds.
146
+ That leaves out the sum: k equal values added one at a time and divided by k
147
+ come back exact for k = 2 and 4, and for k = 8 they do not, since 3x, 5x, 6x
148
+ and 7x round (3,914,684 of the 8,388,608 float32 values in [1, 2) fail,
149
+ `python docs/PAPER/experiments/float_mean.py`). The rule itself stands,
150
+ because a coverage of three on either axis puts a 3 in the map, so over all
151
+ 126,736 legal geometries a map of powers of two holds only 1, 2 and 4. The
152
+ READMEs, THEORY §1, the `reconstruct` docstring, ADR 0003 (with a dated
153
+ correction) and the outreach sources now give that reason.
154
+ - Exact inside the rule means equal as values, which is what `torch.equal`
155
+ checks, and THEORY and the `reconstruct` docstring now say so: the fold sums
156
+ into `+0.0`, so on an overlapping grid a `-0.0` comes back `+0.0` and a NaN
157
+ may change its bits. Both paths do the same, and a test now pins it, which
158
+ `torch.equal` could not.
159
+ - THEORY §9.2 said promotion cut the bfloat16 error from 9.3e-03 to 1.9e-03.
160
+ Those two numbers measured the input's cast from float32, not the library:
161
+ on data drawn in bfloat16 the native fold misses by 7.8e-03 and the promoted
162
+ round trip returns the input bit for bit, at counts 3, 6 and 9. THEORY,
163
+ GUIDE and the code comment now say so, and a test pins it.
164
+ - THEORY §2 and §2.5 and USAGE §5 described the `reconstruct` and `stitch`
165
+ implementation from before 0.5.0 (a second fold of ones, a floor on every
166
+ denominator). They now describe the closed-form count map, the separable
167
+ window denominator and the non-overlapping rearrangement. USAGE §5 checks
168
+ the overlapping round trip with `torch.equal` instead of `torch.allclose`.
169
+ - THEORY §9.6 said `tilings` returns a sorted list. Its order is patch size
170
+ ascending with the exact tile first, which `sorted()` does not give, and a
171
+ test now pins it.
172
+ - The accelerator parity tests compare bits instead of `torch.equal`, and
173
+ include `stride == patch_size`, where hann and gaussian still fold now that
174
+ uniform takes the fast path.
175
+ - Errata for 0.5.5, whose entry says it "changes no bit": that holds for
176
+ returned values in eager mode. Under the uniform window at `stride ==
177
+ patch_size`, derivatives pass a `-0.0` or a signaling NaN through unchanged
178
+ and `torch.compile` folds the `+ 0.0` away; the code comment now says so.
179
+ - **Documentation that said more than the code does.** GUIDE called hann the
180
+ cheapest window, where on an overlapping grid the three cost the same; GUIDE
181
+ and the PyPI page said `extract` hands every tensor to `F.unfold`, where only
182
+ an integer one goes there; PERFORMANCE said 25 runs where the benchmark
183
+ takes 15; GUIDE's two bound figures now read as the measured misses inside
184
+ the `(k+1)·eps·|v|` bound, with the shape of the fp16 example stated.
185
+ FOCO-1.0 freezes values rather than bits, the bound rather than "1 ULP",
186
+ and 20 names. CONTRIBUTING and AGENTS name the three version strings a
187
+ release bumps, STATUS among them. The CITATION abstract no longer carries
188
+ development history, and the BibTeX author reads `Marques de Souza`.
189
+ - `tools/compare_exactness_rules.py` also reports the worst miss outside the
190
+ rule: 19.0 ULP in float32, at `(h, w, ph, pw, sh, sw) = (18, 23, 9, 9, 1,
191
+ 1)`, on a pixel covered 81 times. CI's ruff step checks `tools/` as well.
192
+ - Outreach: a news source dated 2026-10-10 carries the 0.5.5 table, and the
193
+ 2026-09-04 one stays as the record of 0.5.4. The articles follow it, scope
194
+ the 19 ULP figure to the geometry that produced it, and say where each path
195
+ was executed instead of calling the GPU path correct. The post no longer
196
+ says `extract` validates the geometry, the MNIST page states when each of
197
+ the three problems appears, and every reproduction block clones the
198
+ repository, since the wheel carries neither `tools/` nor `tests/`.
199
+
9
200
  ## [0.5.5] - 2026-09-26
10
201
 
11
202
  A performance release that changes no bit. Every change to `src/` below was
@@ -1016,7 +1207,9 @@ First public release. Public API stable; signatures will only change in 1.x.
1016
1207
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
1017
1208
  validation lab.
1018
1209
 
1019
- [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.5...HEAD
1210
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.7...HEAD
1211
+ [0.5.7]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.7
1212
+ [0.5.6]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.6
1020
1213
  [0.5.5]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.5
1021
1214
  [0.5.4]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.4
1022
1215
  [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.7"
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.7
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
+ An 8-bit image goes in as it is. `extract` copies, so its patches hold the image's own bytes, and `reconstruct` returns them exactly in every legal geometry, since `k` copies of `v` sum to `k·v` and divide back to `v`; patches that disagree come back as their exact mean rounded half to even. `stitch` stays float-only, because its windows are float weights.
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,9 +162,9 @@ 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 of a float image 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
- 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.
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. An integer image comes back exact in every legal geometry, because integer sums do not round.
168
168
 
169
169
  The everyday shorthand is that `stride == patch_size` and `stride == patch_size / 2` always satisfy the rule. Both are sufficient conditions rather than necessary ones, so a geometry outside them can still be exact, and the [guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) carries the sweep that measures it.
170
170
 
@@ -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)
@@ -83,7 +83,7 @@ The two functions answer two different questions, so choosing between them is th
83
83
 
84
84
  | You want | Call | Because |
85
85
  |---|---|---|
86
- | The patches back as an image, untouched | `reconstruct` | Exact inverse of `extract` when every overlap count is a power of two, which is always true at `stride == patch_size` |
86
+ | The patches back as an image, untouched | `reconstruct` | Exact inverse of `extract`: always for an integer image, and for a float image when every overlap count is a power of two, which is always true at `stride == patch_size` |
87
87
  | A model's output back as an image | `stitch` | Overlapping patches disagree, and the window hides the grid |
88
88
 
89
89
  ## Why not unfold and fold directly
@@ -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 of a float image 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: among floats the axis that decides the answer is the geometry, and [the guide](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) has the sweep that measures it. An integer image comes back exact in every legal geometry, because integer sums do not round.
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) |
@@ -83,7 +83,7 @@ O `stitch` é para patches que um modelo reescreveu. Nesse caso os patches vizin
83
83
 
84
84
  | Você quer | Chame | Porque |
85
85
  |---|---|---|
86
- | Os patches de volta como imagem, intocados | `reconstruct` | Inverso exato do `extract` quando toda contagem de sobreposição é potência de dois, o que sempre vale com `stride == patch_size` |
86
+ | Os patches de volta como imagem, intocados | `reconstruct` | Inverso exato do `extract`: sempre para uma imagem inteira, e para uma imagem float quando toda contagem de sobreposição é potência de dois, o que sempre vale com `stride == patch_size` |
87
87
  | A saída de um modelo de volta como imagem | `stitch` | Patches sobrepostos discordam, e a janela esconde a grade |
88
88
 
89
89
  ## Por que não usar `unfold` e `fold` direto
@@ -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 de uma imagem float é 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: entre os floats, quem decide a resposta é a geometria, e [o manual](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) traz a varredura que mede isso. Uma imagem inteira volta exata em toda geometria legal, porque soma de inteiros não arredonda.
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
+ An 8-bit image goes in as it is. `extract` copies, so its patches hold the image's own bytes, and `reconstruct` returns them exactly in every legal geometry, since `k` copies of `v` sum to `k·v` and divide back to `v`; patches that disagree come back as their exact mean rounded half to even. `stitch` stays float-only, because its windows are float weights.
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,9 +120,9 @@ 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 of a float image 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
- 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.
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. An integer image comes back exact in every legal geometry, because integer sums do not round.
126
126
 
127
127
  The everyday shorthand is that `stride == patch_size` and `stride == patch_size / 2` always satisfy the rule. Both are sufficient conditions rather than necessary ones, so a geometry outside them can still be exact, and the [guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) carries the sweep that measures it.
128
128
 
@@ -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)