patchcraft 0.5.6__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 (95) hide show
  1. {patchcraft-0.5.6 → patchcraft-0.5.7}/CHANGELOG.md +41 -1
  2. {patchcraft-0.5.6 → patchcraft-0.5.7}/CITATION.cff +1 -1
  3. {patchcraft-0.5.6 → patchcraft-0.5.7}/PKG-INFO +4 -4
  4. {patchcraft-0.5.6 → patchcraft-0.5.7}/README.md +2 -2
  5. {patchcraft-0.5.6 → patchcraft-0.5.7}/README.pt-BR.md +2 -2
  6. {patchcraft-0.5.6 → patchcraft-0.5.7}/README.pypi.md +3 -3
  7. {patchcraft-0.5.6 → patchcraft-0.5.7}/STATUS.md +13 -4
  8. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/ADR/0004-precision-and-effort-parameters.md +8 -4
  9. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/GUIDE.md +26 -20
  10. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/SCOPE.md +5 -6
  11. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/THEORY.md +7 -5
  12. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/USAGE.md +18 -1
  13. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/_version.py +3 -3
  14. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/extract.py +25 -46
  15. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/reconstruct.py +74 -7
  16. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/PKG-INFO +4 -4
  17. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/SOURCES.txt +1 -0
  18. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/scm_file_list.json +1 -0
  19. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/scm_version.json +2 -2
  20. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/_rng.py +4 -1
  21. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_extract.py +15 -11
  22. patchcraft-0.5.7/tests/test_integer.py +191 -0
  23. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_reconstruct.py +5 -5
  24. {patchcraft-0.5.6 → patchcraft-0.5.7}/.gitignore +0 -0
  25. {patchcraft-0.5.6 → patchcraft-0.5.7}/.pre-commit-config.yaml +0 -0
  26. {patchcraft-0.5.6 → patchcraft-0.5.7}/.python-version +0 -0
  27. {patchcraft-0.5.6 → patchcraft-0.5.7}/AGENTS.md +0 -0
  28. {patchcraft-0.5.6 → patchcraft-0.5.7}/CODE_OF_CONDUCT.md +0 -0
  29. {patchcraft-0.5.6 → patchcraft-0.5.7}/CONTRIBUTING.md +0 -0
  30. {patchcraft-0.5.6 → patchcraft-0.5.7}/LICENSE +0 -0
  31. {patchcraft-0.5.6 → patchcraft-0.5.7}/MANIFEST.in +0 -0
  32. {patchcraft-0.5.6 → patchcraft-0.5.7}/MAP.md +0 -0
  33. {patchcraft-0.5.6 → patchcraft-0.5.7}/SECURITY.md +0 -0
  34. {patchcraft-0.5.6 → patchcraft-0.5.7}/accel/Cargo.lock +0 -0
  35. {patchcraft-0.5.6 → patchcraft-0.5.7}/accel/Cargo.toml +0 -0
  36. {patchcraft-0.5.6 → patchcraft-0.5.7}/accel/README.md +0 -0
  37. {patchcraft-0.5.6 → patchcraft-0.5.7}/accel/src/kernel.rs +0 -0
  38. {patchcraft-0.5.6 → patchcraft-0.5.7}/accel/src/lib.rs +0 -0
  39. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/ADR/0001-patch-extraction-api.md +0 -0
  40. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/ADR/0002-patchify-transform.md +0 -0
  41. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/ADR/0003-reversibility-classes.md +0 -0
  42. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/ADR/0005-border-policy.md +0 -0
  43. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/AUXILIARY.md +0 -0
  44. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/FOCO-1.0.md +0 -0
  45. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/PERFORMANCE.md +0 -0
  46. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/ROADMAP.md +0 -0
  47. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
  48. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/STUDIES/2026-09-03-precision-measurements.md +0 -0
  49. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/STUDIES/2026-09-03-strata-conformity.md +0 -0
  50. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/STUDIES/2026-10-10-border-policy.md +0 -0
  51. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
  52. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/design/2026-09-01-fase2-acelerador.md +0 -0
  53. {patchcraft-0.5.6 → patchcraft-0.5.7}/docs/design/2026-09-01-fase3-predicado.md +0 -0
  54. {patchcraft-0.5.6 → patchcraft-0.5.7}/pyproject.toml +0 -0
  55. {patchcraft-0.5.6 → patchcraft-0.5.7}/setup.cfg +0 -0
  56. {patchcraft-0.5.6 → patchcraft-0.5.7}/setup.py +0 -0
  57. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/__init__.py +0 -0
  58. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/_accel.py +0 -0
  59. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/_foldgeom.py +0 -0
  60. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/cache.py +0 -0
  61. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/geometry.py +0 -0
  62. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/metrics.py +0 -0
  63. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/pair.py +0 -0
  64. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/py.typed +0 -0
  65. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/resize.py +0 -0
  66. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft/stitch.py +0 -0
  67. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/dependency_links.txt +0 -0
  68. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/requires.txt +0 -0
  69. {patchcraft-0.5.6 → patchcraft-0.5.7}/src/patchcraft.egg-info/top_level.txt +0 -0
  70. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/__init__.py +0 -0
  71. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/_datasets.py +0 -0
  72. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/conftest.py +0 -0
  73. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_accel.py +0 -0
  74. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_cache.py +0 -0
  75. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_citation.py +0 -0
  76. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_datasets_helper.py +0 -0
  77. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_docs_usage.py +0 -0
  78. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_exactness.py +0 -0
  79. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_geometry.py +0 -0
  80. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_import.py +0 -0
  81. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_metrics.py +0 -0
  82. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_operator.py +0 -0
  83. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_pair.py +0 -0
  84. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_public_api.py +0 -0
  85. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_reference.py +0 -0
  86. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_resize.py +0 -0
  87. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_rng.py +0 -0
  88. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_stitch.py +0 -0
  89. {patchcraft-0.5.6 → patchcraft-0.5.7}/tests/test_vmap.py +0 -0
  90. {patchcraft-0.5.6 → patchcraft-0.5.7}/tools/benchmark.py +0 -0
  91. {patchcraft-0.5.6 → patchcraft-0.5.7}/tools/check_dist.py +0 -0
  92. {patchcraft-0.5.6 → patchcraft-0.5.7}/tools/compare_border_strategies.py +0 -0
  93. {patchcraft-0.5.6 → patchcraft-0.5.7}/tools/compare_exactness_rules.py +0 -0
  94. {patchcraft-0.5.6 → patchcraft-0.5.7}/tools/make_outreach_figures.py +0 -0
  95. {patchcraft-0.5.6 → patchcraft-0.5.7}/uv.lock +0 -0
@@ -6,6 +6,45 @@ 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
+
9
48
  ## [0.5.6] - 2026-10-10
10
49
 
11
50
  A fix release. `torch.vmap` works again over `extract`, `reconstruct` and
@@ -1168,7 +1207,8 @@ First public release. Public API stable; signatures will only change in 1.x.
1168
1207
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
1169
1208
  validation lab.
1170
1209
 
1171
- [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.6...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
1172
1212
  [0.5.6]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.6
1173
1213
  [0.5.5]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.5
1174
1214
  [0.5.4]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.4
@@ -6,7 +6,7 @@ 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.6"
9
+ version: "0.5.7"
10
10
  date-released: "2026-10-10"
11
11
  repository-code: "https://github.com/LeoPR/PatchCraft"
12
12
  license: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchcraft
3
- Version: 0.5.6
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` 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'`.
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
 
@@ -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. 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.
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
 
@@ -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,7 +140,7 @@ 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 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.
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
 
@@ -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
@@ -140,7 +140,7 @@ 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, 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.
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
 
@@ -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` 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'`.
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
 
@@ -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. 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.
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
 
@@ -6,12 +6,12 @@
6
6
 
7
7
  ## Where the project is
8
8
 
9
- **0.5.6 is on PyPI**, published through Trusted Publishing on a tag push: one
9
+ **0.5.7 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
- **1818 tests pass** under `pytest -m "not gpu"`, with 32 skipped and 5
14
+ **1971 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
@@ -24,7 +24,14 @@ pillow 10.1, Rust 1.82) that runs it on both paths.
24
24
 
25
25
  ## What is in flight
26
26
 
27
- **Nothing unreleased.** 0.5.6 is a fix release: `torch.vmap` over `extract`,
27
+ **Nothing unreleased.** 0.5.7 brings integer images: `extract` takes every
28
+ dtype, and `reconstruct` takes the five integer dtypes and returns them exactly
29
+ in every legal geometry, with disagreeing patches rounded half to even and no
30
+ intermediate overflowing. They are ADR 0004's steps 1 and 2, shipped together.
31
+ The full suite and the 126,736-geometry sweep, which now includes an integer
32
+ round trip, pass on both paths.
33
+
34
+ 0.5.6, the release before it, was a fix release: `torch.vmap` over `extract`,
28
35
  `reconstruct` and `stitch`, which the documentation offers as a way to batch,
29
36
  failed on most calls since 0.5.0 and works again, pinned by
30
37
  `tests/test_vmap.py`; and `tilings` returns `[]` for an image smaller than the
@@ -57,7 +64,9 @@ vocabulary its first draft proposed is deferred until a transform needs it.
57
64
 
58
65
  **ADR 0004 is `Proposed`, and asks nothing.** It records the precision and
59
66
  effort parameters as a design that is not built; the status reflects that none
60
- of it is implemented, not that a decision is pending. The one question it
67
+ of the parameters is implemented, not that a decision is pending. Its steps 1
68
+ and 2, integers through `extract` and `reconstruct` without a knob, are on
69
+ `main` since 2026-10-10. The one question it
61
70
  appeared to hold, whether to add the knobs before the 1.0 freeze or after,
62
71
  rested on the premise that adding one afterwards is expensive. It is not: each
63
72
  is a keyword-only argument with a default, so adding one is additive and is a
@@ -41,9 +41,11 @@ external consumer, and Strata §9's default verdict all point the same way.
41
41
 
42
42
  **So this ADR asks nothing.** It records a design that is not built, the names precedent
43
43
  determines, the constraint that would make a preset safe, and the order. It stays `Proposed`
44
- because none of it is implemented, not because a decision is pending.
44
+ because none of its parameters is implemented, not because a decision is pending.
45
45
 
46
- **Nothing here is implemented.**
46
+ **What is implemented is the integer path without a knob:** steps 1 and 2 below shipped
47
+ together on 2026-10-10, ahead of the 1.0 the table assigned them to, because the owner moved
48
+ them forward. None of the parameters exists.
47
49
 
48
50
  ## 1. The names, determined by precedent
49
51
 
@@ -146,8 +148,8 @@ this heading, was posed on top of a table that had already answered it.
146
148
  |---|---|---|---|
147
149
  | 0. Record this ADR, and qualify the four copies of "widening the dtype does not help" | 0.5 to 1 day | no | before 1.0, and it is the only item that must be |
148
150
  | A. Fix three defects as bugs, no knob | done, 2026-09-03 | no signature change | done before 1.0, as planned |
149
- | 1. `extract` accepts the five integer dtypes, by adding them to the fast-path set | ~1 day | no | after 1.0, never alone |
150
- | 2. Exact integer `reconstruct`, unrestricted, `int32` accumulator, every legal geometry | 4 to 6 days | no signature change | after 1.0, shipped with step 1 as one 1.1 |
151
+ | 1. `extract` accepts the five integer dtypes, by adding them to the fast-path set | ~1 day | no | **done, 2026-10-10**, with step 2: every dtype now takes one strided-view path, at every dilation |
152
+ | 2. Exact integer `reconstruct`, unrestricted, `int32` accumulator, every legal geometry | 4 to 6 days | no signature change | **done, 2026-10-10**, with step 1: an `int64` sum up to `int32` and a quotient-and-remainder split for `int64`, so nothing overflows; disagreeing patches round half to even, as `resize` does |
151
153
  | 3. Byte-native `uint8` quotient-and-remainder path | 2 to 3 days | no | do not build yet: 1.7x over an `int32` accumulator, at the cost of a second, different predicate on the doc surface |
152
154
  | 4. `accum_dtype=` on `reconstruct` and `stitch` | 1 day, 2 alongside step 2 | yes, signature | after 1.0 |
153
155
  | 5. `rounding_mode=` on `stitch` | 1 day | yes, signature | after 1.0, and only if a real caller appears |
@@ -206,5 +208,7 @@ only if one appears do the knobs follow, and the preset last of all.
206
208
  once: 0.2.1 added the `reconstruct` integer guard, turning a raw `NotImplementedError`
207
209
  into a framed `ValueError`.
208
210
  4. Integers with `dilation > 1`: a framed `ValueError`, or torch's raw error in that corner?
211
+ *Answered by the implementation, 2026-10-10:* neither. `extract` no longer goes through
212
+ `F.unfold`, so a dilated integer extraction is the same strided copy as any other.
209
213
  5. CUDA. Everything here was measured on CPU, and the accelerator refuses non-CPU tensors
210
214
  outright. No accept text may say "integers work" without a device qualifier.
@@ -328,38 +328,44 @@ There are three ways out, depending on what you need. Decide once for the whole
328
328
 
329
329
  ## 3. Dtypes and files
330
330
 
331
- ### Float tensors only
331
+ ### Integer images
332
332
 
333
- `extract` routes an integer tensor to `F.unfold`, which has no integer kernel, so an 8-bit image fails with torch's own message rather than with one of ours. Convert on the way in, which costs you one division.
333
+ `extract` copies, so an 8-bit image goes in as it is and its patches are the image's own bytes. `reconstruct` takes them back in every legal geometry, and for integers there is no count-map rule: `k` copies of `v` sum to `k·v` exactly, and `k·v` divided by `k` is `v`. The example uses patch 3 at stride 1, whose counts of 3, 6 and 9 make the float round trip inexact ([section 4](#4-when-the-round-trip-is-bit-for-bit)).
334
334
 
335
335
  ```python
336
336
  import torch
337
- from patchcraft import extract, reconstruct
337
+ from patchcraft import extract, reconstruct, stitch
338
338
 
339
- eight_bit = (torch.rand(1, 64, 64) * 255).to(torch.uint8) # what read_image or PIL gives you
340
- try:
341
- extract(eight_bit, patch_size=16, stride=16)
342
- except Exception as error:
343
- print("extract:", type(error).__name__ + ":", error)
339
+ torch.manual_seed(0)
340
+ eight_bit = torch.randint(0, 256, (1, 64, 64), dtype=torch.uint8) # what read_image or PIL gives you
341
+ patches = extract(eight_bit, patch_size=3, stride=1)
342
+ back = reconstruct(patches, eight_bit.shape, stride=1)
343
+ print(tuple(patches.shape), patches.dtype, "| round trip exact:", torch.equal(back, eight_bit))
344
344
 
345
- patches = extract(eight_bit.float() / 255, patch_size=16, stride=16)
346
- print(tuple(patches.shape), patches.dtype)
345
+ as_float = eight_bit.float() / 255
346
+ back_float = reconstruct(extract(as_float, patch_size=3, stride=1), as_float.shape, stride=1)
347
+ print("the same geometry in float32, exact:", torch.equal(back_float, as_float))
348
+
349
+ # Two 1x2 patches that disagree on the middle pixel of a 1x3 strip.
350
+ two = torch.tensor([[[[10, 11]]], [[[12, 20]]]], dtype=torch.uint8)
351
+ print("11 and 12 on the middle pixel:", reconstruct(two, (1, 1, 3), stride=1)[0, 0].tolist())
347
352
 
348
353
  try:
349
- reconstruct(patches.to(torch.uint8), (1, 64, 64), stride=16)
350
- except Exception as error:
351
- print("reconstruct:", type(error).__name__ + ":", error)
354
+ stitch(patches, eight_bit.shape, stride=1, weight="hann")
355
+ except ValueError as error:
356
+ print("stitch:", str(error).split(". ")[0])
352
357
  ```
353
358
 
354
359
  ```
355
- extract: NotImplementedError: "im2col_out_cpu" not implemented for 'Byte'
356
- (16, 1, 16, 16) torch.float32
357
- reconstruct: ValueError: reconstruct requires floating-point patches, got dtype=torch.uint8. F.fold is not implemented for integer dtypes; convert with patches.float() first.
360
+ (3844, 1, 3, 3) torch.uint8 | round trip exact: True
361
+ the same geometry in float32, exact: False
362
+ 11 and 12 on the middle pixel: [10, 12, 20]
363
+ stitch: stitch requires floating-point patches, got dtype=torch.uint8
358
364
  ```
359
365
 
360
- The two messages are not equally good, and the asymmetry is a rough edge rather than a design. `reconstruct` and `stitch` carry a dtype guard that raises a framed `ValueError` naming the conversion, while `extract` still lets torch's raw `NotImplementedError` through.
366
+ Patches that disagree, which is what a model returns, come back as the exact mean rounded half to even, the rounding `resize` already applies to integers: 11 and 12 give 12, and so would 12 and 13. Nothing overflows on the way, `int64` included, because up to `int32` the sum lives in `int64`, and `int64` values are divided by their pixel's count before they are added.
361
367
 
362
- The accepted dtypes are `float16`, `float32`, `float64` and `bfloat16`, and they are contract text in [THEORY.md](THEORY.md) §9.1 and §9.2.
368
+ `stitch` stays float-only, since its windows are float weights and a weighted mean of integers would have to round somewhere you should choose. `reconstruct` refuses `bool`, because a mean of booleans is a vote. The accepted dtypes are contract text in [THEORY.md](THEORY.md) §9.1 and §9.2, measured on CPU.
363
369
 
364
370
  ### End to end from a file
365
371
 
@@ -959,7 +965,7 @@ The LR and HR pairing symbols, which are `pair`, `paired_tilings` and `scale_fac
959
965
 
960
966
  **Before 1.0, and with no external consumer yet.** A real project consuming the published API is the gate [ROADMAP.md](ROADMAP.md) sets for calling the shape settled. Until then a new `0.y` may still change what comes out; pin `~=0.5.0` to take fixes only.
961
967
 
962
- What is verified is this. The full local run of `pytest -m "not gpu"` passes 1818 tests, skips 32 cases, and deselects 5 GPU tests, in under a minute on this machine, so run `pytest` yourself for the number in your environment. Of those skips, 30 are geometries that do not cover exactly and 2 are the full 126,736-geometry sweep, which is a local gate you arm with `PATCHCRAFT_SWEEP_FULL=1`. CI runs the same suite plus `ruff check` and `mypy --strict` on Ubuntu and Windows against Python 3.12, 3.13 and 3.14, and all six cells are green. Those six are forced onto the pure-torch path, so a separate two-cell job on Ubuntu and Windows builds the Rust kernel and runs the whole suite through it. Those eight run the newest torch, and one more job runs the whole suite on both paths at the declared floor: Python 3.12, torch 2.6, numpy 1.26 and pillow 10.1, with the kernel built by Rust 1.82. Releases reach PyPI through Trusted Publishing on a tag push. The package is typed and it ships `py.typed`.
968
+ What is verified is this. The full local run of `pytest -m "not gpu"` passes 1971 tests, skips 32 cases, and deselects 5 GPU tests, in under a minute on this machine, so run `pytest` yourself for the number in your environment. Of those skips, 30 are geometries that do not cover exactly and 2 are the full 126,736-geometry sweep, which is a local gate you arm with `PATCHCRAFT_SWEEP_FULL=1`. CI runs the same suite plus `ruff check` and `mypy --strict` on Ubuntu and Windows against Python 3.12, 3.13 and 3.14, and all six cells are green. Those six are forced onto the pure-torch path, so a separate two-cell job on Ubuntu and Windows builds the Rust kernel and runs the whole suite through it. Those eight run the newest torch, and one more job runs the whole suite on both paths at the declared floor: Python 3.12, torch 2.6, numpy 1.26 and pillow 10.1, with the kernel built by Rust 1.82. Releases reach PyPI through Trusted Publishing on a tag push. The package is typed and it ships `py.typed`.
963
969
 
964
970
  Where the evidence is thinner, this is what it means for you.
965
971
 
@@ -1012,7 +1018,7 @@ Installing from the sdist compiles the accelerator when a Rust toolchain is pres
1012
1018
  author = {Marques de Souza, Leonardo},
1013
1019
  title = {PatchCraft: image patch extraction, reconstruction, pairing
1014
1020
  and seam-aware stitching},
1015
- version = {0.5.6}, % check CHANGELOG.md for the release you used
1021
+ version = {0.5.7}, % check CHANGELOG.md for the release you used
1016
1022
  year = {2026},
1017
1023
  url = {https://github.com/LeoPR/PatchCraft}
1018
1024
  }
@@ -57,13 +57,12 @@ inside the primitive.
57
57
 
58
58
  ### 2.1 Inside `extract`: torch already does it
59
59
 
60
- `extract(image, ...)` reduces to a single `torch.nn.functional.unfold`
61
- call. There is no Python loop over patches. The work happens in:
60
+ `extract(image, ...)` reduces to one strided window view of the image and
61
+ one copy. There is no Python loop over patches. The view costs nothing, and
62
+ the copy is a single tensor operation over the whole `(C, H, W)` image:
62
63
 
63
- - **CPU**: `im2col` is a vectorized C++ implementation; uses SIMD where
64
- applicable. Operates on the whole `(C, H, W)` tensor at once.
65
- - **CUDA**: a single `im2col` kernel launch. Patches are produced in
66
- parallel across thousands of CUDA threads.
64
+ - **CPU**: one vectorized copy kernel, which torch runs on its own thread pool.
65
+ - **CUDA**: one copy kernel launch, parallel across the device.
67
66
 
68
67
  You get the parallelism for free; PatchCraft does not need to and does
69
68
  not try to.
@@ -28,9 +28,9 @@ Multi-image batching is **out of scope**, not under-specified: callers use a Pyt
28
28
 
29
29
  **Boundary conditions.** Truncation, never padding. Pixels in the trailing rows/columns that do not fit a full patch are dropped. PatchCraft does not synthesize data; callers who need full coverage should pad the image themselves before extracting.
30
30
 
31
- **Memory layout.** `F.unfold(x.unsqueeze(0), (ph,pw), dilation=d, stride=(sh,sw))` yields shape `(1, C·ph·pw, L)`. Reshape as `.view(C, ph, pw, L).permute(3, 0, 1, 2).contiguous()` to obtain `(L, C, ph, pw)`. Device and dtype are preserved from the input.
31
+ **Memory layout.** `F.unfold(x.unsqueeze(0), (ph,pw), dilation=d, stride=(sh,sw))` yields shape `(1, C·ph·pw, L)`, and the layout to get from there to `(L, C, ph, pw)` is `.view(C, ph, pw, L).permute(3, 0, 1, 2).contiguous()`; the intuitive `.view(L, C, ph, pw)` keeps the shape and scrambles the pixels. `extract` does not go through `F.unfold`: it takes the strided window view `x.unfold(1, eh, sh).unfold(2, ew, sw)` over the dilated footprint, keeps every `d`-th pixel of it, and copies once into `(L, C, ph, pw)`. Device and dtype are preserved from the input.
32
32
 
33
- **Design decision.** `patchcraft.extract(image, patch_size, stride, dilation=1) -> Tensor[L, C, ph, pw]` is a pure function built on `F.unfold`. `image` must be `(C, H, W)`; batching is explicit (call in a loop or `vmap`) and is not part of the v0.1 signature. `patch_size` and `stride` accept `int` (square) or `(int, int)`. Truncation is the only boundary policy. When no patch fits, we return an empty tensor `Tensor[0, C, ph, pw]` rather than raising, so callers decide. No caching inside the function; caching is a separate concern (see §4). Dilation is supported here even though reconstruction rejects it (see §2). See also [ADR 0001](ADR/0001-patch-extraction-api.md) for the API rationale.
33
+ **Design decision.** `patchcraft.extract(image, patch_size, stride, dilation=1) -> Tensor[L, C, ph, pw]` is a pure function built on a strided window view of the image and one copy, which has a kernel for every dtype. `image` must be `(C, H, W)`; batching is explicit (call in a loop or `vmap`) and is not part of the v0.1 signature. `patch_size` and `stride` accept `int` (square) or `(int, int)`. Truncation is the only boundary policy. When no patch fits, we return an empty tensor `Tensor[0, C, ph, pw]` rather than raising, so callers decide. No caching inside the function; caching is a separate concern (see §4). Dilation is supported here even though reconstruction rejects it (see §2). See also [ADR 0001](ADR/0001-patch-extraction-api.md) for the API rationale.
34
34
 
35
35
  **Composability with torch transforms (Patchify).** ADR 0002 adds a thin callable companion: `patchcraft.Patchify(patch_size, stride, dilation=1)` is a class with `__call__(image) -> Tensor[L, C, ph, pw]` that delegates to `extract`. It exists to slot into `torchvision.transforms.Compose([..., Patchify(4, 2), ...])` without forcing callers to write a lambda, because lambdas are not repr-friendly and they skip eager validation. `Patchify` validates the geometry in `__init__` (so a bad `patch_size` fails when the pipeline is built, not when the first batch arrives), and carries **only** the geometry ints (`__slots__`, no `__dict__`, no cache, no buffer). The function is the contract; the class is a convenience. Same shape contract, same dtype/device preservation, same truncation policy.
36
36
 
@@ -260,9 +260,9 @@ Esta seção consolida, por API, as condições que v0.1 deve **aceitar**, **rej
260
260
  `Patchify(...)(image)` is the callable form and delegates to `extract` with the same contract, plus eager geometry validation at `__init__`.
261
261
 
262
262
  **Aceita:**
263
- - `image` é `Tensor` 3D `(C, H, W)` com `C ≥ 1` arbitrário; dtype suportado por `torch.nn.functional.unfold` (todos os float: `float16/float32/float64/bfloat16`); CPU ou CUDA (preservados na saída).
263
+ - `image` é `Tensor` 3D `(C, H, W)` com `C ≥ 1` arbitrário, de **qualquer dtype**: os float, os cinco inteiros (`uint8`, `int8`, `int16`, `int32`, `int64`), `bool` e os complexos, em qualquer `dilation`. CPU ou CUDA (preservados na saída).
264
264
 
265
- Em CPU, **nenhum** dtype inteiro funciona: `im2col_out_cpu` não é implementado para `Byte`, `Int` nem `Long` (verificado em torch 2.13.0+cpu). Uma versão anterior desta seção dizia que só `uint8` era o problema; está errado. O caller converte pra float antes. `extract` não intercepta isso, então o erro que sobe é o do próprio torch, não um `ValueError` do PatchCraft: `NotImplementedError` nas versões recentes e `RuntimeError`, a classe-base dele, no torch 2.6.
265
+ `extract` só copia: cada patch é uma janela com stride sobre a imagem, materializada por uma cópia, então os valores saem bit a bit iguais aos da imagem em qualquer dtype. Até 0.5.6 os inteiros passavam pelo `F.unfold`, que não tem kernel inteiro em CPU, e falhavam com o erro do próprio torch; a view com stride que o substituiu tem kernel para todo dtype. Medido em CPU; nenhum caminho inteiro executou em GPU.
266
266
  - `patch_size`, `stride`, `dilation` como `int` (quadrado) ou `(int, int)` com valores positivos.
267
267
  - Patch maior que imagem → retorna `Tensor[0, C, ph, pw]` (não levanta).
268
268
  - `stride > patch_size` (grid esparso com lacunas, válido para features que não fazem round-trip; ver §9.2 para a contrapartida em `reconstruct`).
@@ -286,7 +286,8 @@ Esta seção consolida, por API, as condições que v0.1 deve **aceitar**, **rej
286
286
  **Aceita:**
287
287
  - `sh ≤ ph` e `sw ≤ pw` (cobertura total ou overlap).
288
288
  - `image_shape` igual a `(C, H, W)` consistente com a geometria implícita pelo `patches.shape[0]`.
289
- - Dtypes float (`float16/float32/float64/bfloat16`). Dtypes inteiros **não** funcionam: `col2im_out_cpu` não é implementado para `Byte`/`Int`/`Long`, simetricamente ao `extract` em §9.1. Desde 0.2.1, `reconstruct` tem guarda de dtype e rejeita inteiros com `ValueError` claro (em 0.2.0 o erro era um `NotImplementedError` cru do torch).
289
+ - Dtypes float (`float16/float32/float64/bfloat16`) e os cinco inteiros (`uint8`, `int8`, `int16`, `int32`, `int64`).
290
+ - Sobre os inteiros: o resultado é, em cada pixel, a média exata dos valores que os patches dão a ele, arredondada para o par mais próximo, o mesmo arredondamento que `resize` aplica a inteiros (§9.4). Para patches vindos do `extract` todos os valores de um pixel são o mesmo `v`, a soma é `k·v` e o resultado é `v`: o round trip é exato **em toda geometria legal**, sem a regra do mapa de cobertura, que é uma regra de ponto flutuante. Nenhum intermediário estoura: até `int32` a soma é feita em `int64`, e em `int64` cada valor é dividido pela contagem do seu pixel antes da soma, em quociente e resto, de modo que a soma dos quocientes não passa do maior valor de entrada e a dos restos fica abaixo de `k²`. O acelerador nativo não tem caminho inteiro e deixa esses casos com o torch. Medido em CPU, contra a mesma média calculada em inteiros do Python (`tests/test_integer.py`).
290
291
  - Sobre `float16`: o `F.fold` acumula a soma de todos os patches sobrepostos *antes* de dividir pelo count map, e o numerador estoura o máximo finito de fp16 (65504) bem antes de a divisão acontecer. Medido em 0.2.0: imagem fp16 `(1, 16, 16)` constante de valor `10000.0`, `ph=pw=3`, `stride=1`, devolveu `inf` em **144 de 256** pixels. Desde 0.2.1 a acumulação interna (fold, count map, divisão) é feita em `float32`, com cast de volta ao dtype original no retorno; `stitch` faz o mesmo (§9.9).
291
292
  - Sobre `bfloat16`: é promovido do mesmo jeito, **mas não pela mesma razão**, e até 0.5.2 este documento dizia que sim. O `bfloat16` carrega o expoente do `float32`, então não estoura: no caso acima o numerador chegou a `9.0112e+04` contra um máximo finito de `3.390e+38`, sem nenhum `inf`. O que a promoção compra nele é exatidão, já que tem 8 bits de mantissa contra os 11 do `float16`: em 1×16×16, `ph = pw = 3`, `stride = 1`, com dado `torch.rand` gerado em `bfloat16`, acumular nativamente erra até `7.812e-03`, dois passos do `bfloat16` perto de 1, e promovendo o round trip devolve a entrada bit a bit, embora o mapa tenha contagens 3, 6 e 9: o arredondamento final para 8 bits absorve o erro do acumulador em `float32`. É o menos preciso dos dois formatos justamente por trocar mantissa por expoente.
292
293
 
@@ -296,6 +297,7 @@ Esta seção consolida, por API, as condições que v0.1 deve **aceitar**, **rej
296
297
  - Grade truncada: `(num_h - 1) * sh + ph != H` ou `(num_w - 1) * sw + pw != W` (o último patch tem que terminar exatamente na borda da imagem; grades truncadas zeravam pixels silenciosamente em 0.2.0, medido `10×10` patch 4 stride 4 → 36 de 100 zerados, `13×13` patch 5 stride 5 → 69 de 169; guarda adicionada em 0.2.1).
297
298
  - `image_shape` inconsistente com `patches.shape[0]` / `stride`.
298
299
  - `patches.ndim != 4`.
300
+ - `bool` e complexos: a média de booleanos é uma votação, que o ADR 0004 reserva a uma função própria, e os complexos não têm regra de arredondamento aqui.
299
301
 
300
302
  **Fora de escopo v0.1:**
301
303
  - Output em PIL.
@@ -233,9 +233,26 @@ value; sum is `k * value`; division by the count map gives back
233
233
  `stride == patch_size` always qualifies and `stride == patch_size / 2`
234
234
  does too (counts 1, 2, 4, as above). Otherwise the error is bounded
235
235
  per pixel by `(k+1)·eps·|v|`,
236
- whatever the dtype: the deciding axis is the geometry, not the
236
+ whatever the float dtype: the deciding axis is the geometry, not the
237
237
  precision.
238
238
 
239
+ Integer images have no such rule. Their sums do not round, so they come
240
+ back exact in every legal geometry, here at patch 3 and stride 1, where
241
+ counts reach 9:
242
+
243
+ ```python
244
+ >>> as_int = img.to(torch.int16) # the 28x28 image above, as integers
245
+ >>> back = reconstruct(extract(as_int, patch_size=3, stride=1),
246
+ ... image_shape=as_int.shape, stride=1)
247
+ >>> back.dtype, torch.equal(back, as_int)
248
+ (torch.int16, True)
249
+
250
+ ```
251
+
252
+ Patches that disagree get their exact mean rounded half to even, and
253
+ `bool` patches are refused, since their mean is a vote
254
+ ([`THEORY.md`](THEORY.md) §9.2).
255
+
239
256
  When the patches disagree, which is what a model's output does, each pixel
240
257
  gets the mean of what its patches say. Below, nine overlapping patches cover
241
258
  the tiny image; the middle one says 1 and the other eight say 0:
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.5.6'
22
- __version_tuple__ = version_tuple = (0, 5, 6)
21
+ __version__ = version = '0.5.7'
22
+ __version_tuple__ = version_tuple = (0, 5, 7)
23
23
 
24
- __commit_id__ = commit_id = 'gc0bd52ca3'
24
+ __commit_id__ = commit_id = 'gd41e635eb'
@@ -1,4 +1,4 @@
1
- """Patch extraction: strided-window fast path, F.unfold for dilation > 1.
1
+ """Patch extraction: a strided window view and one copy, for every dtype.
2
2
 
3
3
  Contract: docs/THEORY.md §1 and §9.1, docs/ADR/0001-patch-extraction-api.md,
4
4
  docs/ADR/0002-patchify-transform.md.
@@ -6,29 +6,10 @@ docs/ADR/0002-patchify-transform.md.
6
6
  from __future__ import annotations
7
7
 
8
8
  import torch
9
- import torch.nn.functional as F # noqa: N812 (torch convention)
10
9
 
11
10
  __all__ = ["Patchify", "extract"]
12
11
 
13
12
 
14
- # Dtypes that F.unfold (im2col) supports on CPU in torch 2.x. The dilation == 1
15
- # fast path is restricted to these to keep the accept/reject contract unchanged:
16
- # Tensor.unfold views accept any dtype, but integer dtypes must still raise
17
- # from im2col (NotImplementedError, or RuntimeError on torch 2.6), so they fall
18
- # through to F.unfold as before.
19
- _FAST_PATH_DTYPES = frozenset(
20
- {
21
- torch.bool,
22
- torch.float16,
23
- torch.bfloat16,
24
- torch.float32,
25
- torch.float64,
26
- torch.complex64,
27
- torch.complex128,
28
- }
29
- )
30
-
31
-
32
13
  def _as_pair(value: int | tuple[int, int], name: str) -> tuple[int, int]:
33
14
  if isinstance(value, int) and not isinstance(value, bool):
34
15
  if value <= 0:
@@ -57,7 +38,9 @@ def extract(
57
38
  Returns `Tensor[L, C, ph, pw]` in row-major order. Patch `k` has its
58
39
  top-left at `(k // num_w * sh, k % num_w * sw)`. Truncation is the only
59
40
  boundary policy: if the geometry fits no patch, returns `Tensor[0, C, ph, pw]`.
60
- Dtype and device of `image` are preserved.
41
+ Dtype and device of `image` are preserved. Every dtype is accepted, the
42
+ integer ones and bool included, at every dilation: extraction only copies,
43
+ so each patch holds the image's own values bit for bit.
61
44
  """
62
45
  if not isinstance(image, torch.Tensor):
63
46
  raise TypeError(f"image must be torch.Tensor, got {type(image).__name__}")
@@ -75,32 +58,28 @@ def extract(
75
58
  if h < eff_h or w < eff_w:
76
59
  return torch.empty(0, c, ph, pw, dtype=image.dtype, device=image.device)
77
60
 
78
- if dh == 1 and dw == 1 and image.dtype in _FAST_PATH_DTYPES:
79
- # Fast path: strided window view + one copy. Measured 13-21x faster
80
- # than F.unfold (im2col) on CPU, bit-exact (same pixels, same order).
81
- nh = (h - ph) // sh + 1
82
- nw = (w - pw) // sw + 1
83
- windows = image.unfold(1, ph, sh).unfold(2, pw, sw) # (C, nh, nw, ph, pw)
84
- # Exactly one copy in every case. A reshape would copy too, except on
85
- # degenerate geometries (e.g. ph == pw == 1), where the permuted window
86
- # is already contiguous and reshape would return a view aliasing the
87
- # image, which the contract forbids. Cloning unconditionally covers
88
- # that case without asking the tensor for its storage, which a
89
- # torch.vmap batched tensor does not have.
90
- return (
91
- windows.permute(1, 2, 0, 3, 4)
92
- .clone(memory_format=torch.contiguous_format)
93
- .view(nh * nw, c, ph, pw)
94
- )
95
-
96
- unfolded = F.unfold(
97
- image.unsqueeze(0),
98
- kernel_size=(ph, pw),
99
- dilation=(dh, dw),
100
- stride=(sh, sw),
61
+ # A strided window view and exactly one copy, for every dtype and dilation.
62
+ # The window spans the dilated footprint and the slice keeps every d-th
63
+ # pixel of it. Against F.unfold (im2col), which this replaced: the same
64
+ # values bit for bit, 13 to 21 times faster at dilation 1 and 3.9 times at
65
+ # dilation 2 on 3x1024x1024, and a kernel for every dtype where im2col has
66
+ # none for integers.
67
+ nh = (h - eff_h) // sh + 1
68
+ nw = (w - eff_w) // sw + 1
69
+ windows = image.unfold(1, eff_h, sh).unfold(2, eff_w, sw) # (C, nh, nw, eh, ew)
70
+ if dh > 1 or dw > 1:
71
+ windows = windows[..., ::dh, ::dw] # (C, nh, nw, ph, pw)
72
+ # Exactly one copy in every case. A reshape would copy too, except on
73
+ # degenerate geometries (e.g. ph == pw == 1), where the permuted window
74
+ # is already contiguous and reshape would return a view aliasing the
75
+ # image, which the contract forbids. Cloning unconditionally covers
76
+ # that case without asking the tensor for its storage, which a
77
+ # torch.vmap batched tensor does not have.
78
+ return (
79
+ windows.permute(1, 2, 0, 3, 4)
80
+ .clone(memory_format=torch.contiguous_format)
81
+ .view(nh * nw, c, ph, pw)
101
82
  )
102
- return unfolded[0].view(c, ph, pw, -1).permute(3, 0, 1, 2).contiguous()
103
-
104
83
 
105
84
  class Patchify:
106
85
  """Callable that extracts patches with a frozen geometry.