patchcraft 0.5.1__tar.gz → 0.5.2__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.
- {patchcraft-0.5.1 → patchcraft-0.5.2}/CHANGELOG.md +81 -1
- {patchcraft-0.5.1 → patchcraft-0.5.2}/CONTRIBUTING.md +59 -3
- {patchcraft-0.5.1 → patchcraft-0.5.2}/MANIFEST.in +3 -1
- {patchcraft-0.5.1 → patchcraft-0.5.2}/PKG-INFO +1 -1
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/ADR/0003-reversibility-classes.md +1 -0
- patchcraft-0.5.2/docs/ADR/0004-precision-and-effort-parameters.md +227 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/GUIDE.md +2 -2
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/THEORY.md +5 -4
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/_version.py +3 -3
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/reconstruct.py +8 -1
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/resize.py +12 -3
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/stitch.py +10 -4
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/PKG-INFO +1 -1
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/SOURCES.txt +4 -6
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/scm_file_list.json +12 -6
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/scm_version.json +2 -2
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_exactness.py +1 -1
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_reconstruct.py +21 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_resize.py +35 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_stitch.py +35 -0
- patchcraft-0.5.1/docs/superpowers/plans/2026-09-01-fase1-bugs-performance.md +0 -1179
- patchcraft-0.5.1/docs/superpowers/plans/2026-09-01-fase2-acel.md +0 -1643
- patchcraft-0.5.1/docs/superpowers/plans/2026-09-01-fase3-g1-predicado.md +0 -1705
- {patchcraft-0.5.1 → patchcraft-0.5.2}/.gitignore +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/.python-version +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/LICENSE +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/README.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/README.pt-BR.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/README.pypi.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/accel/Cargo.lock +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/accel/Cargo.toml +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/accel/README.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/accel/src/kernel.rs +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/accel/src/lib.rs +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/ADR/0001-patch-extraction-api.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/ADR/0002-patchify-transform.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/AUXILIARY.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/FOCO-1.0.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/PERFORMANCE.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/ROADMAP.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/SCOPE.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/docs/USAGE.md +0 -0
- /patchcraft-0.5.1/docs/superpowers/specs/2026-09-01-fase1-bugs-performance-design.md → /patchcraft-0.5.2/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
- /patchcraft-0.5.1/docs/superpowers/specs/2026-09-01-fase2-acel-design.md → /patchcraft-0.5.2/docs/design/2026-09-01-fase2-acelerador.md +0 -0
- /patchcraft-0.5.1/docs/superpowers/specs/2026-09-01-fase3-g1-predicado-design.md → /patchcraft-0.5.2/docs/design/2026-09-01-fase3-predicado.md +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/pyproject.toml +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/setup.cfg +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/setup.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/__init__.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/_accel.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/_foldgeom.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/cache.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/extract.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/geometry.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/metrics.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/pair.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft/py.typed +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/dependency_links.txt +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/requires.txt +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/src/patchcraft.egg-info/top_level.txt +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/__init__.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/_datasets.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/_rng.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/conftest.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_accel.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_cache.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_datasets_helper.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_extract.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_geometry.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_import.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_metrics.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_pair.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_public_api.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_reference.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tests/test_rng.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tools/benchmark.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/tools/check_dist.py +0 -0
- {patchcraft-0.5.1 → patchcraft-0.5.2}/uv.lock +0 -0
|
@@ -6,6 +6,85 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.2] - 2026-09-03
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`stitch(weight="hann")` corrupted the corner band of every patch of 99 or
|
|
14
|
+
more.** The denominator was floored by an absolute `clamp(min=1e-6)` whose
|
|
15
|
+
comment called it a defensive no-op. It was not: the 2-D hann corner weight
|
|
16
|
+
is `(pi/(n+1))**4`, which crosses below `1e-6` at patch 99, so the corner
|
|
17
|
+
band was divided by the floor rather than by the real weight. Measured at
|
|
18
|
+
640x640, patch 256, stride 128: maximum error 0.94 on data in [0, 1] and 960
|
|
19
|
+
wrong pixels, identical in float64 because the constant and not the precision
|
|
20
|
+
was the cause. The floor is now `finfo(dtype).tiny`, the model already used
|
|
21
|
+
in `metrics.py`, which never reaches a legitimate weight: the smallest 2-D
|
|
22
|
+
hann corner even at patch 1024 is 8.8e-11. THEORY 9.9 claimed no covered
|
|
23
|
+
pixel is zeroed by the window, which was true of the window and false of the
|
|
24
|
+
result.
|
|
25
|
+
- **`reconstruct` returned a view aliasing the caller's patches on a
|
|
26
|
+
single-patch grid.** Writing to the patches afterwards changed an image the
|
|
27
|
+
function had already returned, measured as 0.744 becoming 99.0. `extract`
|
|
28
|
+
guards the mirror image of this case and `reconstruct` did not.
|
|
29
|
+
- **`resize(..., backend="pil")` raised a raw `RuntimeError` on `int8`.** The
|
|
30
|
+
inbound `clamp(0, 255)` was applied in the input dtype, where the bound 255
|
|
31
|
+
is unrepresentable, so torch raised `value cannot be converted to type
|
|
32
|
+
int8_t without overflow` before clamping anything. Every integer dtype now
|
|
33
|
+
works on both backends, and THEORY 9.4 gained the row for integer tensors on
|
|
34
|
+
the PIL backend, which it had never specified.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- The agentic workflow's leftovers. `.superpowers/` at the repository root
|
|
40
|
+
held 62 files of review diffs and task-progress notes, hidden from
|
|
41
|
+
`git status` by a nested `.gitignore` containing `*`, so it sat in the tree
|
|
42
|
+
unseen and reached the 0.5.0 sdist. The three implementation plans under
|
|
43
|
+
`docs/superpowers/plans/` were 4527 lines of checkbox task lists addressed
|
|
44
|
+
"For agentic workers", and they shipped to PyPI inside every sdist.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `docs/ADR/0004-precision-and-effort-parameters.md`, in `Proposed` status: names the
|
|
49
|
+
precision policy the library already applies in five places, and designs an `effort=`
|
|
50
|
+
preset under the constraint that it may change speed and memory but never a returned
|
|
51
|
+
value. Nothing is implemented; the ADR records the design, the effort per step and the
|
|
52
|
+
order. It amends ADR 0003 on one measured point, that the exactness predicate is a
|
|
53
|
+
property of the geometry and the accumulator rather than of the geometry alone.
|
|
54
|
+
- `outreach/`, material for presenting the project publicly, in the shape the
|
|
55
|
+
sibling projects use: a dated news source at the root and one subfolder per
|
|
56
|
+
channel, with the rule that no channel text changes before the source does.
|
|
57
|
+
It publishes no new measurement, and it is pruned from the sdist.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- The three design specs that were under `docs/superpowers/specs/` moved to
|
|
62
|
+
`docs/design/`. They are the record of what was measured before each phase
|
|
63
|
+
was built, including the spike table that rejected `conv_transpose2d` and
|
|
64
|
+
`index_add_` before the Rust kernel was written, and Amendment A, which
|
|
65
|
+
`tests/test_exactness.py` cites for the per-pixel error bound.
|
|
66
|
+
- The sdist is 264 KiB where it was 316 KiB.
|
|
67
|
+
- Nothing ephemeral is kept in the project folder any more. `accel/target`
|
|
68
|
+
had reached 269 MiB of Rust build output, and `dist/`, `.pytest_cache` and
|
|
69
|
+
`src/patchcraft.egg-info` were accumulating alongside it. The caches are
|
|
70
|
+
now redirected by user-level environment variables, joining the ones this
|
|
71
|
+
machine already had for uv, ruff, mypy, pip and `__pycache__`, and
|
|
72
|
+
`CONTRIBUTING.md` records the convention and why it is not committed here.
|
|
73
|
+
- `.vscode/settings.json` stays tracked and is pruned from the sdist instead.
|
|
74
|
+
- **The versioning rule was self-contradictory and is corrected.** One row said
|
|
75
|
+
an output value change is a `0.Y.0`, the next said a fix is a `0.y.Z`, and a
|
|
76
|
+
bug fix satisfies both, so the table made every fix a minor and left patch
|
|
77
|
+
releases unable to fix anything. It also contradicted the project's own
|
|
78
|
+
history: `0.2.1` changed `stitch(weight="hann")` output for every geometry
|
|
79
|
+
and shipped as a patch. The rule now turns on whether the contract moved or
|
|
80
|
+
the implementation stopped violating it, which sorts `0.2.1`, `0.3.0`,
|
|
81
|
+
`0.5.0` and `0.5.2` correctly. `CONTRIBUTING.md` carries the rule with those
|
|
82
|
+
four worked, and GUIDE section 8 item 6 no longer cites a patch release as
|
|
83
|
+
an example of what only a minor may do.
|
|
84
|
+
Its `${workspaceFolder}/.venv` path is not broken: `.venv` is a directory
|
|
85
|
+
junction to the real environment outside the tree, so the setting resolves
|
|
86
|
+
and is portable to any checkout following the same convention.
|
|
87
|
+
|
|
9
88
|
## [0.5.1] - 2026-09-02
|
|
10
89
|
|
|
11
90
|
> Numbered `0.6.0` while it was being built. Corrected before release: the
|
|
@@ -587,7 +666,8 @@ First public release. Public API stable; signatures will only change in 1.x.
|
|
|
587
666
|
- [`README.md`](README.md) covers installation, the car-vs-track metaphor,
|
|
588
667
|
validation lab.
|
|
589
668
|
|
|
590
|
-
[Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.
|
|
669
|
+
[Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.2...HEAD
|
|
670
|
+
[0.5.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.2
|
|
591
671
|
[0.5.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.1
|
|
592
672
|
[0.5.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.0
|
|
593
673
|
|
|
@@ -68,6 +68,36 @@ CARGO_TARGET_DIR=/tmp/pc-target uv sync --extra cache --extra dev
|
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
71
|
+
## Nothing ephemeral lives in the project folder
|
|
72
|
+
|
|
73
|
+
Caches, build output and virtual environments are kept off the tree, because
|
|
74
|
+
they are large, regenerable and machine-specific. They are redirected by
|
|
75
|
+
user-level environment variables rather than by anything committed here: a
|
|
76
|
+
machine path in `pyproject.toml` would be wrong for every other machine and
|
|
77
|
+
for CI, which is why one was removed in 0.3.0.
|
|
78
|
+
|
|
79
|
+
| Variable | Sends |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `CARGO_TARGET_DIR` | the Rust build output, which reached 269 MiB inside `accel/target` before it was redirected |
|
|
82
|
+
| `PYTEST_ADDOPTS=-o cache_dir=...` | `.pytest_cache` |
|
|
83
|
+
| `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR` | the linter caches |
|
|
84
|
+
| `UV_CACHE_DIR`, `PIP_CACHE_DIR` | downloaded wheels |
|
|
85
|
+
| `PYTHONPYCACHEPREFIX` | `__pycache__` |
|
|
86
|
+
|
|
87
|
+
The virtual environment lives outside the tree too; see
|
|
88
|
+
[`docs/AUXILIARY.md`](docs/AUXILIARY.md).
|
|
89
|
+
|
|
90
|
+
Two directories still appear during a build and are gitignored: `build/` and
|
|
91
|
+
`src/patchcraft.egg-info/`, both setuptools working state. They exist only
|
|
92
|
+
while you build, and redirecting them would need a machine path in a tracked
|
|
93
|
+
file, which is the thing this section is avoiding.
|
|
94
|
+
|
|
95
|
+
Three files are written into `src/patchcraft/` by an editable install and
|
|
96
|
+
are gitignored: the compiled `_accel_native`, the generated `_version.py`, and
|
|
97
|
+
`__pycache__`. Those have to be there for the package to import.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
71
101
|
## Layout
|
|
72
102
|
|
|
73
103
|
```
|
|
@@ -137,7 +167,11 @@ PatchCraft/
|
|
|
137
167
|
│ ├── FOCO-1.0.md what 1.0 freezes and the blockers in the way
|
|
138
168
|
│ ├── ADR/ one file per decision that shaped the API
|
|
139
169
|
│ ├── STUDIES/ background reading behind those decisions
|
|
140
|
-
│ └──
|
|
170
|
+
│ └── design/ one spec per work phase: the alternatives measured and the decision
|
|
171
|
+
├── outreach/ material for presenting the project publicly, not shipped
|
|
172
|
+
│ ├── README.md how it is organized and what to check before publishing
|
|
173
|
+
│ ├── 2026-09-03-lancamento.md the dated news source every channel text derives from
|
|
174
|
+
│ └── linkedin/ post (short) and artigo (long), each in PT and EN
|
|
141
175
|
└── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
|
|
142
176
|
```
|
|
143
177
|
|
|
@@ -186,13 +220,35 @@ promise that nothing does.
|
|
|
186
220
|
|
|
187
221
|
That fixes what each digit is for here:
|
|
188
222
|
|
|
223
|
+
The question that decides it is not "did a returned value change". A bug fix
|
|
224
|
+
changes returned values by definition, and if that forced a `0.Y.0` then a
|
|
225
|
+
patch release could never fix anything. The question is **whether the contract
|
|
226
|
+
moved, or whether the implementation stopped violating it**:
|
|
227
|
+
|
|
189
228
|
| Change | Bump | Why |
|
|
190
229
|
|---|---|---|
|
|
191
|
-
|
|
|
192
|
-
|
|
|
230
|
+
| The documented contract moves: a signature, a name, or behaviour the documentation described as correct | `0.Y.0` | Code that was right against the old documentation can now be wrong |
|
|
231
|
+
| The implementation stops violating the contract, however visibly | `0.y.Z` | The documentation always promised this; only the code was wrong |
|
|
232
|
+
| New functionality that breaks nothing | `0.y.Z` | Anyone pinned to the y-series should get it |
|
|
193
233
|
| Documentation, tests, CI, packaging or tooling only | `0.y.Z` | Still a release if it is published, still no break |
|
|
194
234
|
| Nothing published | nothing | Accumulate under `[Unreleased]` |
|
|
195
235
|
|
|
236
|
+
The project's own history sorts cleanly under that rule, and does not sort
|
|
237
|
+
under any rule keyed on whether values changed:
|
|
238
|
+
|
|
239
|
+
- `0.2.1` rewrote the hann window, changing `stitch(weight="hann")` output for
|
|
240
|
+
**every** geometry. THEORY §9.9 had promised no covered pixel is zeroed by
|
|
241
|
+
the window; the old window zeroed 108 of 144 pixels in one measured case. A
|
|
242
|
+
violation, so a **z**, and it shipped as one.
|
|
243
|
+
- `0.5.2` floored the `stitch` denominator by the dtype's tiny instead of an
|
|
244
|
+
absolute `1e-6`, changing hann output at patch 99 and above. The same
|
|
245
|
+
promise, the same kind of violation, so also a **z**.
|
|
246
|
+
- `0.3.0` moved `stitch` output by ULPs through a different summation order.
|
|
247
|
+
Nothing was wrong before, so behaviour the documentation endorsed changed.
|
|
248
|
+
A **y**.
|
|
249
|
+
- `0.5.0` made `tilings` return 73 specs where it returned 100. The
|
|
250
|
+
enumeration's output set is the contract. A **y**.
|
|
251
|
+
|
|
196
252
|
After 1.0 the ordinary reading applies: breaking is major, additive is minor,
|
|
197
253
|
fixed is patch. What 1.0 freezes is written in
|
|
198
254
|
[`docs/FOCO-1.0.md`](docs/FOCO-1.0.md).
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
- **Status:** Proposed
|
|
4
4
|
- **Date:** 2026-08-04
|
|
5
5
|
- **Deciders:** Leonardo Marques de Souza
|
|
6
|
+
- **Amended by:** [ADR 0004](0004-precision-and-effort-parameters.md), which measures the exactness predicate below to be a property of the geometry **and the accumulator**, not of the geometry alone: with a `float64` accumulator, 76 of 76 sampled legal geometries are bit-exact where a `float32` accumulator gives 45, and those 45 are exactly the power-of-two ones.
|
|
6
7
|
- **Relates to:** [ADR 0001](0001-patch-extraction-api.md), [`THEORY.md`](../THEORY.md) §0, §9, [`STUDIES/2026-08-04-patch-techniques.md`](../STUDIES/2026-08-04-patch-techniques.md) §4, §5
|
|
7
8
|
|
|
8
9
|
## Context
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# ADR 0004: naming the precision policy, and an effort preset that may not change values
|
|
2
|
+
|
|
3
|
+
- **Status:** Proposed
|
|
4
|
+
- **Date:** 2026-09-03
|
|
5
|
+
- **Deciders:** Leonardo Marques de Souza
|
|
6
|
+
- **Relates to:** [ADR 0003](0003-reversibility-classes.md) (which this amends on one point), [`THEORY.md`](../THEORY.md) §9.1, §9.2, §9.9, [`FOCO-1.0.md`](../FOCO-1.0.md) §1, [`PERFORMANCE.md`](../PERFORMANCE.md)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
### The library already has a precision policy. It has no name.
|
|
11
|
+
|
|
12
|
+
Five places decide precision on the caller's behalf, none of them visible from a call site:
|
|
13
|
+
|
|
14
|
+
| Site | The choice made for you |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `reconstruct.py:55`, `stitch.py:189` | `float16` and `bfloat16` accumulate in `float32`, rounding once on return |
|
|
17
|
+
| `stitch.py:229` | the denominator is floored by `clamp(min=1e-6)`, an absolute constant |
|
|
18
|
+
| `metrics.py:67`, `:123` | accumulation promotes to `float64` whatever the input was |
|
|
19
|
+
| `metrics.py:164` | `clamp_min(finfo(float64).tiny)` before the PSNR logarithm |
|
|
20
|
+
| `resize.py:47`, `:148` | every cross-backend conversion passes through normalised `float32` in [0, 1] |
|
|
21
|
+
|
|
22
|
+
So the question is not whether to introduce a precision concept. The concept is already
|
|
23
|
+
here, applied by default and unnamed. The question is whether to give it a name.
|
|
24
|
+
|
|
25
|
+
### What forced the question
|
|
26
|
+
|
|
27
|
+
A user with a `uint8` image cannot use the library at all. `extract` raises
|
|
28
|
+
`NotImplementedError: "im2col_out_cpu" not implemented for 'Byte'`, and `reconstruct` and
|
|
29
|
+
`stitch` reject every non-floating-point dtype outright. The only route is `.float()`
|
|
30
|
+
first, which on an 8192x8192 tile at patch 512 stride 256 turns a 721 MiB patch stack into
|
|
31
|
+
2883 MiB.
|
|
32
|
+
|
|
33
|
+
The owner's framing of the problem is the right one and worth quoting, because it rules out
|
|
34
|
+
the easy answer: `input(byte) -> operation(???) -> output(byte)` is not the same thing as
|
|
35
|
+
`input(byte) -> operation(byte) -> output(byte)`. Returning the input dtype after widening
|
|
36
|
+
internally gives back neither the memory nor the time.
|
|
37
|
+
|
|
38
|
+
### Measurements that shaped the design
|
|
39
|
+
|
|
40
|
+
All taken on 2026-09-03, CPU, torch 2.14.0+cpu, on the machine named in
|
|
41
|
+
[`PERFORMANCE.md`](../PERFORMANCE.md).
|
|
42
|
+
|
|
43
|
+
**Byte-native averaging is exact, and it is the fastest option.** The identity
|
|
44
|
+
`sum(a_i)//k == sum(a_i//k) + sum(a_i mod k)//k` holds for every `k`, verified exhaustively
|
|
45
|
+
for k in 2..5 and over 200,000 random samples up to k=16. The quotient accumulator always
|
|
46
|
+
fits `uint8`; the remainder accumulator holds `k*(k-1)` and so fits `uint8` only for k <= 16.
|
|
47
|
+
At k=4 over 3.1M pixels: 2.51 ms byte-native, 4.24 ms widening to `int16`, 6.19 ms via
|
|
48
|
+
`float32`, with accumulators of 3.0, 6.0 and 12.0 MiB.
|
|
49
|
+
|
|
50
|
+
**A first measurement of this said the opposite, and the error is instructive.** Writing
|
|
51
|
+
the byte path as `(x // k).sum(0)` measured 9x slower than float. `torch`'s `uint8.sum()`
|
|
52
|
+
promotes to `int64` silently, so that benchmark was the very
|
|
53
|
+
`input(byte) -> operation(???)` shape the design exists to avoid. Only accumulators declared
|
|
54
|
+
`uint8` measure the thing.
|
|
55
|
+
|
|
56
|
+
**What decides speed is not integer versus float. It is whether the divisor is a power of
|
|
57
|
+
two.** The same arithmetic, the same k, written with `//` and `%` against `>>` and `&`:
|
|
58
|
+
|
|
59
|
+
| k | via `//` and `%` | via `>>` and `&` | ratio |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| 2 | 22.51 ms | 1.39 ms | 16.1x |
|
|
62
|
+
| 4 | 40.17 ms | 2.53 ms | 15.9x |
|
|
63
|
+
| 8 | 76.46 ms | 5.09 ms | 15.0x |
|
|
64
|
+
| 16 | 148.71 ms | 9.71 ms | 15.3x |
|
|
65
|
+
|
|
66
|
+
x86 has no SIMD integer division. A power-of-two divisor turns it into a shift, which
|
|
67
|
+
vectorises.
|
|
68
|
+
|
|
69
|
+
**The same idea generalises to floats, and fixes a defect the library papered over.**
|
|
70
|
+
THEORY §9.2 records that a constant `float16` image at 10000.0 with `ph=3, stride=1`
|
|
71
|
+
returned `inf` in 144 of 256 pixels, and that `float32` promotion was added to prevent it.
|
|
72
|
+
Reproduced exactly. Scaling each patch by `1/k` before the fold keeps the numerator at
|
|
73
|
+
1.0e+04 against a finite `float16` maximum of 6.55e+04, produces no `inf`, and returns the
|
|
74
|
+
image with zero error, in a 2 byte per pixel accumulator instead of 4. On ordinary [0, 1]
|
|
75
|
+
data it costs 5.96e-08. On this machine it is nonetheless 1.35x slower than the promotion,
|
|
76
|
+
because torch's CPU `float16` kernels are weak. The advantage is real but it is per-dtype
|
|
77
|
+
and must be measured, never assumed.
|
|
78
|
+
|
|
79
|
+
**`bfloat16` is in the wrong bucket.** THEORY §9.2 justifies promoting both half formats by
|
|
80
|
+
`float16` overflowing its finite maximum of 65504, then applies the rule to `bfloat16` too.
|
|
81
|
+
`bfloat16` carries `float32`'s exponent: on the same case its numerator reached 9.01e+04
|
|
82
|
+
with no `inf` and a finite maximum of 3.39e+38. Promoting it buys precision, since it has
|
|
83
|
+
7 mantissa bits against `float16`'s 10, not range. The stated reason covers one of the two.
|
|
84
|
+
|
|
85
|
+
## Decision
|
|
86
|
+
|
|
87
|
+
Three fine-grained knobs, each naming one existing policy, plus one preset under a hard
|
|
88
|
+
constraint. **Nothing here is implemented. This ADR records the design and the order.**
|
|
89
|
+
|
|
90
|
+
### The knobs
|
|
91
|
+
|
|
92
|
+
| Name | Type | Values | Default | Governs | Borrowed from |
|
|
93
|
+
|---|---|---|---|---|---|
|
|
94
|
+
| `accum_dtype` | `torch.dtype \| None`, keyword-only | any dtype at least as wide as the input, of the same kind | `None`, meaning today's rule | where the numerator and count map live in `reconstruct` and `stitch`. Does not change the return dtype. | `numpy.sum(dtype=)`, documented as "the accumulator in which the elements are summed", and `torch.sum(dtype=)` |
|
|
95
|
+
| `rounding_mode` | `Literal["trunc","floor","nearest_even"] \| None` | as listed | `None` | the single rounding event on `stitch`'s final divide, once `stitch` accepts integers. Not offered on `reconstruct`. | `torch.div(rounding_mode=)`, same name, two of three values; the tie-break vocabulary from stdlib `decimal` |
|
|
96
|
+
| `antialias` | `bool \| None` | `True`, `False`, `None` | `None`, meaning whatever the backend does today | whether the torch `resize` backend prefilters before downsampling | `torch.nn.functional.interpolate` and `torchvision.transforms.v2.functional.resize`, verbatim |
|
|
97
|
+
|
|
98
|
+
`None` is a sentinel on every one of them, never a spelled-out default. Without it a preset
|
|
99
|
+
cannot distinguish "unspecified" from "explicitly asked for the current value", and the
|
|
100
|
+
resolver has nothing to key on.
|
|
101
|
+
|
|
102
|
+
**Not `accumulate=`.** It is a verb where every precedent is a noun, it does not say the
|
|
103
|
+
value is a dtype, and `numpy` already owns the identifier for `ufunc.accumulate`, which is a
|
|
104
|
+
running scan and a different operation.
|
|
105
|
+
|
|
106
|
+
**No `rounding_mode` on `reconstruct`.** For unmodified patches every covering value is the
|
|
107
|
+
same `v`, so the sum is exactly `k*v` and `k*v // k == v` for every `k`, in every dtype.
|
|
108
|
+
`reconstruct` does not round. A knob there would advertise a decision the function does not
|
|
109
|
+
make.
|
|
110
|
+
|
|
111
|
+
### The preset
|
|
112
|
+
|
|
113
|
+
`effort: Literal["balanced", "fast"] | None = None`, keyword-only, on `reconstruct`,
|
|
114
|
+
`stitch`, `extract` and `Patchify.__init__`. `Patchify` is the reason it exists: inside a
|
|
115
|
+
`Compose` there is no per-call keyword, so a stored string is the only way a policy reaches
|
|
116
|
+
a data pipeline.
|
|
117
|
+
|
|
118
|
+
One axis, following x264 and x265, whose presets are all speed and whose quality is a
|
|
119
|
+
separate orthogonal knob. Not `fast`/`quality`/`precise`: that trio spans three axes and
|
|
120
|
+
admits no correct ordering, and it leaves the caller who wants exact-and-fast, which is the
|
|
121
|
+
common case here, nothing to pass. No superlative at either end, so a third rung can be
|
|
122
|
+
added later without a lie, which is the wall both `torch` ("highest") and JAX (`HIGHEST`)
|
|
123
|
+
hit.
|
|
124
|
+
|
|
125
|
+
**The constraint, and it is the whole decision:**
|
|
126
|
+
|
|
127
|
+
> `effort=` may change speed and memory. It may never change a returned value. Anything
|
|
128
|
+
> that changes values requires an explicit knob.
|
|
129
|
+
|
|
130
|
+
That constraint is what separates this from `-ffast-math`, which is structurally the same
|
|
131
|
+
proposal: a name promising speed whose effect changes results. Its failure modes are all
|
|
132
|
+
reachable here in miniature. It deletes the caller's own safety checks, a compiler update
|
|
133
|
+
silently reinterpreted it more aggressively and broke shipping software with no source
|
|
134
|
+
change, and it escapes the caller's scope entirely by setting process-wide floating point
|
|
135
|
+
mode. The constraint also makes the preset falsifiable, which is what this project's
|
|
136
|
+
contract style demands: "fast" is unprovable, but "`effort="fast"` is bit-identical to
|
|
137
|
+
`effort="balanced"`" is a test id.
|
|
138
|
+
|
|
139
|
+
`effort="fast"` may therefore expand only into: permitting the native accelerator without
|
|
140
|
+
an environment variable, which structurally replaces the `PATCHCRAFT_ACCEL` global that is
|
|
141
|
+
already an unnamed preset today; permitting the shift-and-mask integer path where the
|
|
142
|
+
coverage map makes it bit-identical to the divide; and returning views where aliasing is
|
|
143
|
+
provably safe, which is a memory choice and needs its own sub-knob rather than hiding here.
|
|
144
|
+
`effort="balanced"` expands to nothing at all: it is today's behaviour, exhaustively.
|
|
145
|
+
|
|
146
|
+
Ship `patchcraft.effort_options(name) -> dict` returning the literal expansion, after
|
|
147
|
+
`torch._inductor.list_mode_options`, so the preset is never a black box and the knobs stay
|
|
148
|
+
the source of truth.
|
|
149
|
+
|
|
150
|
+
**Precedence: the preset seeds defaults, explicit keyword arguments override, and argument
|
|
151
|
+
order is irrelevant.** After x265, which promises exactly that in prose. Not mutual
|
|
152
|
+
exclusion: `torch.compile` raises on preset-plus-options and the bill is visible, because
|
|
153
|
+
`max-autotune-no-cudagraphs` exists as a whole extra public name purely to work around it.
|
|
154
|
+
|
|
155
|
+
### Amendment to ADR 0003
|
|
156
|
+
|
|
157
|
+
**The exactness predicate is a property of the geometry *and the accumulator*, not of the
|
|
158
|
+
geometry alone.** ADR 0003 states the round trip is bit-exact iff every coverage count is a
|
|
159
|
+
power of two. Measured over 76 legal square geometries on `float32` input: with a `float32`
|
|
160
|
+
accumulator, 45 of 76 are exact, and those 45 are exactly the power-of-two geometries, so
|
|
161
|
+
the predicate is perfectly predictive. With a `float64` accumulator, **76 of 76 are exact**.
|
|
162
|
+
|
|
163
|
+
The predicate as written carries an unstated premise, which is the accumulator the library
|
|
164
|
+
happens to use today. ADR 0003 should say so. It also means `accum_dtype=torch.float64`
|
|
165
|
+
would offer unconditional bit-exactness for `float32` input at twice the accumulator memory,
|
|
166
|
+
which is a stronger guarantee than the library currently advertises.
|
|
167
|
+
|
|
168
|
+
**And the predicate does not govern integers at all.** A `uint8` round trip through an
|
|
169
|
+
`int32` accumulator was exact on all six geometries tested, including three whose count maps
|
|
170
|
+
are not powers of two, for the reason given above: the sum is exactly `k*v`. Gating an
|
|
171
|
+
integer path on the float predicate would be a category error.
|
|
172
|
+
|
|
173
|
+
## Consequences
|
|
174
|
+
|
|
175
|
+
### Effort, each step independently shippable
|
|
176
|
+
|
|
177
|
+
| Step | Size | Breaks frozen surface | When |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| 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 |
|
|
180
|
+
| A. Fix three defects as bugs, no knob | done, 2026-09-03 | no signature change | done before 1.0, as planned |
|
|
181
|
+
| 1. `extract` accepts the five integer dtypes, by adding them to the fast-path set | ~1 day | no | after 1.0, never alone |
|
|
182
|
+
| 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 |
|
|
183
|
+
| 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 |
|
|
184
|
+
| 4. `accum_dtype=` on `reconstruct` and `stitch` | 1 day, 2 alongside step 2 | yes, signature | after 1.0 |
|
|
185
|
+
| 5. `rounding_mode=` on `stitch` | 1 day | yes, signature | after 1.0, and only if a real caller appears |
|
|
186
|
+
| 6. `effort=` and `effort_options()` | 1 day of code, 3+ of contract | yes, signature, four sites | after 1.0, after 1 and 2 have shipped |
|
|
187
|
+
| 7. Bool support, where the inverse is a majority vote | 3 to 4 days | it is a 21st public name | 1.1 at the earliest, a function rather than a parameter |
|
|
188
|
+
|
|
189
|
+
### Order
|
|
190
|
+
|
|
191
|
+
Record the design. Fix the three defects as bugs on their own merits, because folding them
|
|
192
|
+
into a `precise` tier would sell the absence of a bug as a quality level. **Step A landed on
|
|
193
|
+
2026-09-03**, and the three were the `stitch` denominator floor, the `reconstruct`
|
|
194
|
+
single-patch aliasing and the `resize` inbound clamp on a dtype that cannot hold the bound.
|
|
195
|
+
The remaining items named in the audit, the `bfloat16` justification in THEORY 9.2 and the
|
|
196
|
+
resize antialias gap, are still open and are not defects of the same kind: the first is a
|
|
197
|
+
documentation correction and the second is a user-facing choice that needs its own release. Document, at zero
|
|
198
|
+
cost, the rounding policy already in force. Freeze 1.0 at 20 names with no new parameters.
|
|
199
|
+
Then 1.1 brings steps 1 and 2 together and unrestricted. Then stop and wait for a user:
|
|
200
|
+
only if one appears do the knobs follow, and the preset last of all.
|
|
201
|
+
|
|
202
|
+
### What not to do
|
|
203
|
+
|
|
204
|
+
- Do not gate the integer path on the power-of-two predicate. It is a float predicate.
|
|
205
|
+
- Do not let any preset change a returned value.
|
|
206
|
+
- Do not ship step 1 without step 2. `extract` accepting `uint8` while `reconstruct` rejects
|
|
207
|
+
it makes THEORY §9.1 and §9.2 false as the matched pair they are written as.
|
|
208
|
+
- Do not make any of this a module global, an environment variable, or a context manager
|
|
209
|
+
that mutates torch state.
|
|
210
|
+
- Do not offer `rounding="nearest"` without saying which tie-break.
|
|
211
|
+
- Do not add a knob to `accel_available()`. It is one of the 20 frozen names with a pinned
|
|
212
|
+
zero-argument signature.
|
|
213
|
+
|
|
214
|
+
### Open questions, for the decision this ADR defers
|
|
215
|
+
|
|
216
|
+
1. Does `accum_dtype` change only where the sum lives, or also what comes back?
|
|
217
|
+
`reconstruct` casts back to `patches.dtype`; `metrics` returns `float64` unconditionally.
|
|
218
|
+
The two are inconsistent today and the knob would expose it.
|
|
219
|
+
2. If `accum_dtype=float64` makes every geometry exact, does the headline contract become
|
|
220
|
+
"exact, at this accumulator width" rather than "exact on these geometries"? That reads
|
|
221
|
+
as a stronger promise and needs its own falsification suite before it is made.
|
|
222
|
+
3. Are steps 1 to 3 a `0.Y.0` or a `z` bump? The project shipped the mirror-image change
|
|
223
|
+
once: 0.2.1 added the `reconstruct` integer guard, turning a raw `NotImplementedError`
|
|
224
|
+
into a framed `ValueError`.
|
|
225
|
+
4. Integers with `dilation > 1`: a framed `ValueError`, or torch's raw error in that corner?
|
|
226
|
+
5. CUDA. Everything here was measured on CPU, and the accelerator refuses non-CPU tensors
|
|
227
|
+
outright. No accept text may say "integers work" without a device qualifier.
|
|
@@ -812,7 +812,7 @@ Five things this project does not claim.
|
|
|
812
812
|
|
|
813
813
|
**5. The accelerated and the pure paths are equal by test, on two platforms out of five.** Every geometry in the suite is checked with `torch.equal` against the pure path, but that job runs on Ubuntu and Windows x86_64 only. The macOS and aarch64 wheels are built and their contents are checked, and nothing has ever executed their kernel in CI.
|
|
814
814
|
|
|
815
|
-
**6. Pre-1.0 means output values can change
|
|
815
|
+
**6. Pre-1.0 means output values can change in either kind of release, for two different reasons.** A new `0.y` may move behaviour the documentation endorsed, which is what happened when `tilings` began returning 73 specs where it returned 100. A new `0.y.z` may change values too, but only by making the code match a promise the documentation was already making: twice now that has been the `stitch` hann denominator, once for a window that zeroed covered pixels and once for a floor that swallowed the corner band. [CHANGELOG.md](../CHANGELOG.md) records each one together with the measurement that motivated it, and [CONTRIBUTING.md](../CONTRIBUTING.md) carries the rule.
|
|
816
816
|
|
|
817
817
|
The backlog that closes these is [FOCO-1.0.md](FOCO-1.0.md).
|
|
818
818
|
|
|
@@ -855,7 +855,7 @@ Installing from the sdist compiles the accelerator when a Rust toolchain is pres
|
|
|
855
855
|
author = {Souza, Leonardo Marques de},
|
|
856
856
|
title = {PatchCraft: image patch extraction, reconstruction, pairing
|
|
857
857
|
and seam-aware stitching},
|
|
858
|
-
version = {0.5.
|
|
858
|
+
version = {0.5.2}, % check CHANGELOG.md for the release you used
|
|
859
859
|
year = {2026},
|
|
860
860
|
url = {https://github.com/LeoPR/PatchCraft}
|
|
861
861
|
}
|
|
@@ -111,9 +111,9 @@ Visualizing the count map along one axis (image cols 0..7, `ph=4`, `sh=2`):
|
|
|
111
111
|
|
|
112
112
|
**Dilation.** `F.fold` does not support dilation in the same way as `F.unfold`: for `d > 1` the patch footprint skips pixels, and `fold` would deposit the sparse contributions into a canvas that is not the image. Rather than building a custom scatter, we refuse `dilation != 1` at reconstruction time with a clear `ValueError`. Callers who extracted with dilation are expected to consume patches directly (e.g., as features) and not round-trip.
|
|
113
113
|
|
|
114
|
-
**Stride maior que patch (lacunas).** Quando `sh > ph` ou `sw > pw`, o grid pula pixels: a soma `fold(...)` tem zeros nessas posições, e qualquer divisão (incluindo pelo
|
|
114
|
+
**Stride maior que patch (lacunas).** Quando `sh > ph` ou `sw > pw`, o grid pula pixels: a soma `fold(...)` tem zeros nessas posições, e qualquer divisão (incluindo pelo piso do denominador) produz pixels com valor arbitrário, ou seja, síntese de dado, exatamente o que §1 proíbe ao escolher truncamento como única política de borda. Recusamos a condição com `ValueError` logo na entrada de `reconstruct`. Consumidores que querem features esparsas (kernels, classificadores) usam apenas `extract`, onde `stride > patch_size` é aceito sem objeções, e não tentam round-trip.
|
|
115
115
|
|
|
116
|
-
**Design decision.** `patchcraft.reconstruct(patches, image_shape, stride, dilation=1) -> Tensor[C, H, W]` uses `F.fold` followed by division by the overlap-count map (computed once, same-geometry fold of ones). Raises `ValueError` when `dilation != 1` **ou quando `sh > ph` ou `sw > pw`** (cobertura parcial é proibida). `image_shape` is `(C, H, W)` and must match the geometry implied by the patch grid; inconsistent shapes raise `ValueError`.
|
|
116
|
+
**Design decision.** `patchcraft.reconstruct(patches, image_shape, stride, dilation=1) -> Tensor[C, H, W]` uses `F.fold` followed by division by the overlap-count map (computed once, same-geometry fold of ones). Raises `ValueError` when `dilation != 1` **ou quando `sh > ph` ou `sw > pw`** (cobertura parcial é proibida). `image_shape` is `(C, H, W)` and must match the geometry implied by the patch grid; inconsistent shapes raise `ValueError`. O piso do denominador existe apenas para absorver ruído float em pixels totalmente cobertos, nunca para mascarar buracos de cobertura; desde 0.5.2 ele é `finfo(dtype).tiny`, que nunca alcança um peso legítimo (o menor canto hann 2-D, mesmo em patch 1024, é 8,8e-11). Output dtype matches input.
|
|
117
117
|
|
|
118
118
|
## 2.5 Stitching modified patches
|
|
119
119
|
|
|
@@ -129,7 +129,7 @@ The standard trick is to weight each patch's contribution by a 2-D window whose
|
|
|
129
129
|
|
|
130
130
|
For *unmodified* patches, every `patch_k(i_k, j_k)` equals `img(x, y)` (definition of `extract`), so the numerator factors as `img(x, y) · denominator(x, y)`, and the output is `img(x, y)` exactly, so round-trip is preserved for any kernel as long as the denominator at `(x, y)` is positive. For *modified* patches, contributions are weighted by how central `(x, y)` is to each contributing patch, so seams are attenuated.
|
|
131
131
|
|
|
132
|
-
**Implementation.** Two `F.fold` calls: one on the weighted patches (numerator), one on the kernel replicated across the `L` patch slots (denominator). Identical geometry to `reconstruct`'s count-map fold; same `
|
|
132
|
+
**Implementation.** Two `F.fold` calls: one on the weighted patches (numerator), one on the kernel replicated across the `L` patch slots (denominator). Identical geometry to `reconstruct`'s count-map fold; the denominator carries the same floor. Since 0.5.2 that floor is `torch.finfo(dtype).tiny` and no longer an absolute `1e-6`: the 2-D hann corner weight is `(π/(n+1))⁴`, which falls below `1e-6` at patch 99, so every larger hann stitch had its corner band divided by the floor instead of by the real weight. Medido a 640×640, patch 256, stride 128: erro máximo 0,94 em dado [0, 1] e 960 pixels errados, idêntico em float64 porque a causa era a constante e não a precisão.
|
|
133
133
|
|
|
134
134
|
**Window kernels.**
|
|
135
135
|
|
|
@@ -319,6 +319,7 @@ Retorna `PatchPair(lr_patches, hr_patches, metas)` (frozen dataclass com `__slot
|
|
|
319
319
|
- `resample=None` → default por backend (LANCZOS pra PIL, bilinear pra torch).
|
|
320
320
|
- Tensor inteiro com `backend == "torch"`: interpolação em float, cast de volta com **round + clamp** ao range do dtype (bicubic ultrapassa o range de entrada legitimamente, e um cast cru wrapava: `-9.0 → 247` em uint8; corrigido em 0.2.1).
|
|
321
321
|
- Tensor float com `backend == "pil"`: o hop uint8 pelo PIL **clampa a `[0, 1]`**; valores fora desse intervalo não sobrevivem nesse backend (comportamento documentado, não exceção).
|
|
322
|
+
- Tensor inteiro com `backend == "pil"`: o mesmo hop uint8 **clampa a `[0, 255]`** na ida e satura ao range do dtype na volta. Os dois backends recusam-se a dar a volta (`wrap`), que é a classe de defeito da 0.2.0, mas saturam em ranges diferentes de propósito: o torch preserva 300 e −9 num int32, o PIL não. Até 0.5.1 a ida fazia `clamp(0, 255)` no dtype de entrada, e num `int8` o próprio limite era irrepresentável, então o torch levantava `RuntimeError` cru antes de limitar coisa alguma.
|
|
322
323
|
|
|
323
324
|
**Rejeita (`ValueError`):**
|
|
324
325
|
- `Tensor` em CUDA com `backend == "pil"` (PIL não enxerga GPU, então exige `.cpu()` explícito do caller).
|
|
@@ -439,7 +440,7 @@ The blending counterpart to `reconstruct`. Same fold geometry, same rejections;
|
|
|
439
440
|
- `dtype` and `device` preserved on output.
|
|
440
441
|
|
|
441
442
|
**Sinaliza (não exceção):**
|
|
442
|
-
- (nenhuma condição sinalizada; as três janelas são estritamente positivas desde 0.2.1, então nenhum pixel coberto é zerado pela janela)
|
|
443
|
+
- (nenhuma condição sinalizada; as três janelas são estritamente positivas desde 0.2.1, então nenhum pixel coberto é zerado pela janela. Até 0.5.1 essa frase era verdadeira quanto à janela e falsa quanto ao resultado, porque o piso absoluto `1e-6` do denominador corrompia a banda de canto a partir de patch 99; corrigido em 0.5.2.)
|
|
443
444
|
|
|
444
445
|
**Rejeita (`ValueError` / `TypeError`):**
|
|
445
446
|
- `patches` not a tensor (`TypeError`).
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
21
|
+
__version__ = version = '0.5.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g6090abd8c'
|
|
@@ -85,7 +85,14 @@ def reconstruct(
|
|
|
85
85
|
# reconstruction is a pure rearrangement -- no fold, no count map, and
|
|
86
86
|
# no widening for half precision (nothing accumulates).
|
|
87
87
|
grid = patches.reshape(num_h, num_w, c, ph, pw)
|
|
88
|
-
|
|
88
|
+
out = grid.permute(2, 0, 3, 1, 4).reshape(c, h, w)
|
|
89
|
+
if out.untyped_storage().data_ptr() == patches.untyped_storage().data_ptr():
|
|
90
|
+
# Single-patch grid: the permuted view is already contiguous, so
|
|
91
|
+
# reshape aliased the caller's patches and a later write to them
|
|
92
|
+
# would change an image this function already returned. `extract`
|
|
93
|
+
# guards the mirror image of this case.
|
|
94
|
+
out = out.clone()
|
|
95
|
+
return out
|
|
89
96
|
|
|
90
97
|
# Half-precision inputs overflow inside F.fold, which accumulates the sum
|
|
91
98
|
# of all overlapping patches before the count-map division (fp16 max is
|
|
@@ -57,8 +57,11 @@ def _tensor_to_pil_u8(tensor: torch.Tensor) -> PILImage:
|
|
|
57
57
|
else:
|
|
58
58
|
# Integer tensors: clamp to the uint8 range before casting, otherwise
|
|
59
59
|
# values outside [0, 255] wrap (300 -> 44) -- same defect class as the
|
|
60
|
-
# 0.2.0 bicubic overshoot wrap on the torch backend.
|
|
61
|
-
|
|
60
|
+
# 0.2.0 bicubic overshoot wrap on the torch backend. Widen first,
|
|
61
|
+
# because the bound itself has to be representable: `clamp(0, 255)` on
|
|
62
|
+
# an int8 tensor raised `value cannot be converted to type int8_t
|
|
63
|
+
# without overflow` before clamping anything.
|
|
64
|
+
arr = tensor.to(torch.int32).clamp(0, 255).cpu().numpy().astype(np.uint8)
|
|
62
65
|
c = arr.shape[0]
|
|
63
66
|
if c == 1:
|
|
64
67
|
return Image.fromarray(arr[0], mode="L")
|
|
@@ -184,7 +187,13 @@ def resize(
|
|
|
184
187
|
pil_out = _resize_pil(pil_in, target_size, resample)
|
|
185
188
|
tensor_out = _pil_to_tensor_f32(pil_out)
|
|
186
189
|
if not original_dtype.is_floating_point:
|
|
187
|
-
|
|
190
|
+
# Round AND clamp, the same pair the torch branch applies.
|
|
191
|
+
# Without the clamp a narrow signed dtype cannot represent the
|
|
192
|
+
# 0..255 range the PIL hop produces, and the cast raised a raw
|
|
193
|
+
# `RuntimeError: value cannot be converted to type int8_t
|
|
194
|
+
# without overflow` instead of returning a saturated value.
|
|
195
|
+
info = torch.iinfo(original_dtype)
|
|
196
|
+
tensor_out = (tensor_out * 255).round().clamp(info.min, info.max)
|
|
188
197
|
return tensor_out.to(original_dtype)
|
|
189
198
|
return _resize_torch(image, target_size, resample)
|
|
190
199
|
|
|
@@ -223,7 +223,13 @@ def stitch(
|
|
|
223
223
|
den_w = _fold_window_1d(ww, w, num_w, sw)
|
|
224
224
|
den = den_h.unsqueeze(1) * den_w.unsqueeze(0)
|
|
225
225
|
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# positive
|
|
229
|
-
|
|
226
|
+
# The floor is the dtype's own smallest normal, not an absolute constant.
|
|
227
|
+
# Geometry validation guarantees coverage and all three windows are
|
|
228
|
+
# strictly positive, so the denominator is genuinely positive and this
|
|
229
|
+
# only guards a true zero. An absolute 1e-6 was not the no-op its comment
|
|
230
|
+
# claimed: the 2-D hann corner weight is (pi/(n+1))**4, which falls below
|
|
231
|
+
# 1e-6 at patch 99, so every larger hann stitch had its corner band
|
|
232
|
+
# divided by 1e-6 instead of by the real weight. Measured at 640x640,
|
|
233
|
+
# patch 256, stride 128: max error 0.94 on data in [0, 1], 960 pixels
|
|
234
|
+
# wrong, identical in float64 because it was never a precision problem.
|
|
235
|
+
return (numerator / den.clamp_min(torch.finfo(den.dtype).tiny)).to(patches.dtype)
|
|
@@ -26,13 +26,11 @@ docs/USAGE.md
|
|
|
26
26
|
docs/ADR/0001-patch-extraction-api.md
|
|
27
27
|
docs/ADR/0002-patchify-transform.md
|
|
28
28
|
docs/ADR/0003-reversibility-classes.md
|
|
29
|
+
docs/ADR/0004-precision-and-effort-parameters.md
|
|
29
30
|
docs/STUDIES/2026-08-04-patch-techniques.md
|
|
30
|
-
docs/
|
|
31
|
-
docs/
|
|
32
|
-
docs/
|
|
33
|
-
docs/superpowers/specs/2026-09-01-fase1-bugs-performance-design.md
|
|
34
|
-
docs/superpowers/specs/2026-09-01-fase2-acel-design.md
|
|
35
|
-
docs/superpowers/specs/2026-09-01-fase3-g1-predicado-design.md
|
|
31
|
+
docs/design/2026-09-01-fase1-bugs-performance.md
|
|
32
|
+
docs/design/2026-09-01-fase2-acelerador.md
|
|
33
|
+
docs/design/2026-09-01-fase3-predicado.md
|
|
36
34
|
src/patchcraft/__init__.py
|
|
37
35
|
src/patchcraft/_accel.py
|
|
38
36
|
src/patchcraft/_foldgeom.py
|