patchcraft 0.2.2__tar.gz → 0.5.0__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 (64) hide show
  1. {patchcraft-0.2.2 → patchcraft-0.5.0}/.github/workflows/release.yml +76 -1
  2. {patchcraft-0.2.2 → patchcraft-0.5.0}/.github/workflows/test.yml +43 -1
  3. {patchcraft-0.2.2 → patchcraft-0.5.0}/.gitignore +3 -0
  4. {patchcraft-0.2.2 → patchcraft-0.5.0}/CHANGELOG.md +164 -23
  5. patchcraft-0.5.0/CONTRIBUTING.md +161 -0
  6. {patchcraft-0.2.2 → patchcraft-0.5.0}/PKG-INFO +38 -6
  7. {patchcraft-0.2.2 → patchcraft-0.5.0}/README.md +34 -5
  8. {patchcraft-0.2.2 → patchcraft-0.5.0}/README.pt-BR.md +34 -5
  9. {patchcraft-0.2.2 → patchcraft-0.5.0}/README.pypi.md +34 -5
  10. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/AUXILIARY.md +2 -2
  11. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/GUIDE.md +23 -20
  12. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/SCOPE.md +10 -7
  13. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/STUDIES/2026-08-04-patch-techniques.md +2 -1
  14. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/THEORY.md +6 -5
  15. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/USAGE.md +16 -8
  16. patchcraft-0.5.0/docs/superpowers/plans/2026-09-01-fase1-bugs-performance.md +1179 -0
  17. patchcraft-0.5.0/docs/superpowers/plans/2026-09-01-fase2-acel.md +1643 -0
  18. patchcraft-0.5.0/docs/superpowers/plans/2026-09-01-fase3-g1-predicado.md +1705 -0
  19. patchcraft-0.5.0/docs/superpowers/specs/2026-09-01-fase1-bugs-performance-design.md +151 -0
  20. patchcraft-0.5.0/docs/superpowers/specs/2026-09-01-fase2-acel-design.md +168 -0
  21. patchcraft-0.5.0/docs/superpowers/specs/2026-09-01-fase3-g1-predicado-design.md +260 -0
  22. {patchcraft-0.2.2 → patchcraft-0.5.0}/pyproject.toml +14 -2
  23. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/__init__.py +3 -1
  24. patchcraft-0.5.0/src/patchcraft/_accel.py +115 -0
  25. patchcraft-0.5.0/src/patchcraft/_foldgeom.py +87 -0
  26. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/cache.py +7 -0
  27. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/extract.py +32 -1
  28. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/geometry.py +10 -2
  29. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/metrics.py +21 -13
  30. patchcraft-0.5.0/src/patchcraft/reconstruct.py +137 -0
  31. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/resize.py +6 -3
  32. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/stitch.py +102 -112
  33. patchcraft-0.5.0/tests/_rng.py +108 -0
  34. patchcraft-0.5.0/tests/test_accel.py +223 -0
  35. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_cache.py +22 -0
  36. patchcraft-0.5.0/tests/test_exactness.py +164 -0
  37. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_extract.py +62 -1
  38. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_geometry.py +39 -11
  39. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_import.py +6 -0
  40. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_metrics.py +47 -0
  41. patchcraft-0.5.0/tests/test_public_api.py +249 -0
  42. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_reconstruct.py +89 -30
  43. patchcraft-0.5.0/tests/test_reference.py +92 -0
  44. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_resize.py +11 -0
  45. patchcraft-0.5.0/tests/test_rng.py +109 -0
  46. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_stitch.py +91 -20
  47. patchcraft-0.5.0/uv.lock +1172 -0
  48. patchcraft-0.2.2/CONTRIBUTING.md +0 -98
  49. patchcraft-0.2.2/src/patchcraft/reconstruct.py +0 -155
  50. patchcraft-0.2.2/uv.lock +0 -1041
  51. {patchcraft-0.2.2 → patchcraft-0.5.0}/.python-version +0 -0
  52. {patchcraft-0.2.2 → patchcraft-0.5.0}/LICENSE +0 -0
  53. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/ADR/0001-patch-extraction-api.md +0 -0
  54. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/ADR/0002-patchify-transform.md +0 -0
  55. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/ADR/0003-reversibility-classes.md +0 -0
  56. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/FOCO-1.0.md +0 -0
  57. {patchcraft-0.2.2 → patchcraft-0.5.0}/docs/ROADMAP.md +0 -0
  58. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/pair.py +0 -0
  59. {patchcraft-0.2.2 → patchcraft-0.5.0}/src/patchcraft/py.typed +0 -0
  60. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/__init__.py +0 -0
  61. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/_datasets.py +0 -0
  62. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/conftest.py +0 -0
  63. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_datasets_helper.py +0 -0
  64. {patchcraft-0.2.2 → patchcraft-0.5.0}/tests/test_pair.py +0 -0
@@ -64,6 +64,46 @@ jobs:
64
64
  name: dist
65
65
  path: dist/
66
66
 
67
+ build-accel:
68
+ name: accel wheel (${{ matrix.target }})
69
+ runs-on: ${{ matrix.os }}
70
+ needs: validate
71
+ strategy:
72
+ fail-fast: false
73
+ matrix:
74
+ include:
75
+ # macos-13 is the Intel runner; macos-latest is arm64.
76
+ - os: windows-latest
77
+ target: x86_64-pc-windows-msvc
78
+ - os: ubuntu-latest
79
+ target: x86_64-unknown-linux-gnu
80
+ - os: macos-latest
81
+ target: aarch64-apple-darwin
82
+ - os: macos-13
83
+ target: x86_64-apple-darwin
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+
87
+ - name: Build accel wheel
88
+ uses: PyO3/maturin-action@v1
89
+ with:
90
+ target: ${{ matrix.target }}
91
+ args: --release --out dist-accel --manifest-path accel/Cargo.toml
92
+
93
+ - name: Build accel sdist (linux only)
94
+ # The sdist is OS-independent; one is enough. Building it from source
95
+ # requires a Rust toolchain -- documented in accel/README.md.
96
+ if: matrix.target == 'x86_64-unknown-linux-gnu'
97
+ run: pipx run maturin sdist --manifest-path accel/Cargo.toml --out dist-accel
98
+
99
+ - name: Check accel artifacts
100
+ run: pipx run twine check dist-accel/*
101
+
102
+ - uses: actions/upload-artifact@v4
103
+ with:
104
+ name: accel-${{ matrix.target }}
105
+ path: dist-accel/
106
+
67
107
  publish-pypi:
68
108
  name: publish to PyPI (Trusted Publishing)
69
109
  runs-on: ubuntu-latest
@@ -94,10 +134,38 @@ jobs:
94
134
  # Workflow: release.yml
95
135
  # Environment: pypi
96
136
 
137
+ publish-accel:
138
+ name: publish patchcraft-accel to PyPI (Trusted Publishing)
139
+ runs-on: ubuntu-latest
140
+ needs: build-accel
141
+ environment:
142
+ name: pypi
143
+ url: https://pypi.org/project/patchcraft-accel/
144
+ permissions:
145
+ id-token: write # required for OIDC token exchange with PyPI
146
+ steps:
147
+ - uses: actions/download-artifact@v4
148
+ with:
149
+ pattern: accel-*
150
+ path: dist-accel
151
+ merge-multiple: true
152
+ - name: Publish via Trusted Publishing
153
+ uses: pypa/gh-action-pypi-publish@release/v1
154
+ with:
155
+ skip-existing: true
156
+ packages-dir: dist-accel/
157
+ # FIRST RELEASE REQUIRES a one-time "pending publisher" config on
158
+ # pypi.org (user action, not this repo's):
159
+ # PyPI project name: patchcraft-accel
160
+ # Owner: LeoPR
161
+ # Repository: PatchCraft
162
+ # Workflow: release.yml
163
+ # Environment: pypi
164
+
97
165
  github-release:
98
166
  name: create GitHub Release with assets
99
167
  runs-on: ubuntu-latest
100
- needs: publish-pypi
168
+ needs: [publish-pypi, build-accel]
101
169
  permissions:
102
170
  contents: write # required to create a Release
103
171
  steps:
@@ -106,6 +174,11 @@ jobs:
106
174
  with:
107
175
  name: dist
108
176
  path: dist/
177
+ - uses: actions/download-artifact@v4
178
+ with:
179
+ pattern: accel-*
180
+ path: dist-accel
181
+ merge-multiple: true
109
182
  - name: Extract version from tag
110
183
  id: ver
111
184
  run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
@@ -123,6 +196,8 @@ jobs:
123
196
  files: |
124
197
  dist/*.whl
125
198
  dist/*.tar.gz
199
+ dist-accel/*.whl
200
+ dist-accel/*.tar.gz
126
201
  fail_on_unmatched_files: true
127
202
  draft: false
128
203
  prerelease: false
@@ -18,7 +18,7 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  os: [ubuntu-latest, windows-latest]
21
- python: ["3.12", "3.13"]
21
+ python: ["3.12", "3.13", "3.14"]
22
22
  env:
23
23
  # Force uv to use the matrix Python in every uv* invocation
24
24
  # (otherwise `uv run` re-resolves to its default, which is the
@@ -49,3 +49,45 @@ jobs:
49
49
 
50
50
  - name: Pytest (skip GPU markers)
51
51
  run: uv run pytest -m "not gpu"
52
+
53
+ test-accel:
54
+ name: ${{ matrix.os }} / py3.12 / accel
55
+ runs-on: ${{ matrix.os }}
56
+ env:
57
+ UV_PYTHON: "3.12"
58
+ strategy:
59
+ fail-fast: false
60
+ matrix:
61
+ os: [ubuntu-latest, windows-latest]
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+
65
+ - name: Install uv
66
+ uses: astral-sh/setup-uv@v3
67
+ with:
68
+ enable-cache: true
69
+
70
+ - name: Install Rust
71
+ uses: dtolnay/rust-toolchain@stable
72
+
73
+ - name: Set up Python
74
+ run: uv python install
75
+
76
+ - name: Install project with dev + cache extras
77
+ run: uv sync --locked --extra cache --extra dev
78
+
79
+ - name: Rust unit tests
80
+ # The venv's interpreter on PATH lets the pyo3 build script resolve
81
+ # libpython (extension-module is off outside maturin builds).
82
+ run: uv run cargo test --manifest-path accel/Cargo.toml
83
+
84
+ - name: Build + install the accelerator
85
+ run: |
86
+ uv pip install maturin
87
+ uv run maturin develop --manifest-path accel/Cargo.toml --release
88
+
89
+ - name: Accelerator is detected
90
+ run: uv run python -c "import patchcraft; assert patchcraft.accel_available()"
91
+
92
+ - name: Pytest (accelerated mode)
93
+ run: uv run pytest -m "not gpu"
@@ -53,3 +53,6 @@ runs/
53
53
 
54
54
  # Datasets and caches live on Z:
55
55
  data/
56
+
57
+ # Rust accelerator build artifacts (accel/Cargo.lock IS committed)
58
+ accel/target/
@@ -6,8 +6,165 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-09-02
10
+
11
+ ### Added
12
+
13
+ - Python 3.14 is supported and declared. The classifiers list it, the CI
14
+ matrix runs the full suite plus `ruff check` and `mypy --strict` against it
15
+ on Ubuntu and Windows, and all six cells are green. The floor stays at 3.12
16
+ because `cache.py` uses PEP 695 generic syntax, which 3.11 cannot parse.
17
+
18
+ ### Changed
19
+
20
+ - `tilings`/`paired_tilings` no longer emit degenerate single-patch overlap
21
+ specs: with one patch the stride is unobservable and the spec duplicated
22
+ the exact tile. `tilings((28, 28), allow_overlap=True)` now returns 73
23
+ specs instead of 100; `paired_tilings((14, 14), (28, 28), allow_overlap=True)`
24
+ returns 27 instead of 40.
25
+ - The development lock moved to torch 2.14.0 and torchvision 0.29.0, with the
26
+ linters and the rest of the toolchain along with it. The full
27
+ 126,736-geometry exactness sweep passes on torch 2.14, so the count-map
28
+ contract is unchanged by the upgrade. Runtime requirements are untouched and
29
+ stay at `torch>=2.6`.
30
+
31
+ ### Fixed
32
+
33
+ - **Public retraction: the documented exactness boundary was wrong.** The old
34
+ wording (`k_max <= 4`, "`stride == patch_size / 2`", "within ~1 ULP" outside
35
+ the rule) was measured false: the error grows with the pixel's coverage
36
+ count (up to 19 ULP at count 81 in float32). The correct contract
37
+ (ADR 0003): the `extract`/`reconstruct` round trip is bit-exact iff every
38
+ value of the overlap count map is a power of two, which is always true
39
+ at `stride == patch_size`, and outside the rule the per-pixel error is
40
+ bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count.
41
+ Docstrings and docs (SCOPE, THEORY, GUIDE, USAGE, READMEs) now state this
42
+ form; THEORY §9.1 records the extract-truncates / reconstruct-rejects
43
+ asymmetry.
44
+ - Packaging: the sdist no longer carries `.superpowers/`, an untracked
45
+ directory of agent tooling that hatchling swept in (62 files). The same
46
+ class of leak as the `lab/` files that reached PyPI in 0.2.0.
47
+ - Documentation: GUIDE section 6 still printed the pre-0.5.0 enumeration
48
+ output and described the single-patch duplicates as an open wart; section 7
49
+ asserted `__version__ == "0.4.0"`; section 8 and the BibTeX entry quoted
50
+ 0.4.0 and its test counts. All four now match the release.
51
+ - Prose: the em-dashes that entered the docstrings, docs and tests during
52
+ 0.3.0 to 0.5.0 are rewritten as ordinary sentences, per the project's
53
+ writing rule.
54
+ - Test suite: round-trip assertions now run on seeded full-mantissa noise
55
+ (integer ramps and widened-float32 data could mask ULP-level errors); a
56
+ falsification suite (`tests/test_exactness.py`) enumerates the 126,736
57
+ legal geometries, samples 256 (seeded; full sweep via
58
+ `PATCHCRAFT_SWEEP_FULL=1`), and pins both halves of the predicate; a naive
59
+ loop-based reference (`tests/test_reference.py`) cross-checks the fast
60
+ paths; the 20-name public surface is frozen by `tests/test_public_api.py`;
61
+ the no-`zstandard` cache branch is now covered.
62
+
63
+ ### Notes
64
+
65
+ - No signature changed. The public surface is exactly the 20 names of 0.4.0,
66
+ now pinned by test.
67
+
68
+ ## [0.4.0] - 2026-09-01
69
+
70
+ > Source milestone, never uploaded to PyPI. Its contents reached
71
+ > users inside 0.5.0. See the versioning policy in
72
+ > [`CONTRIBUTING.md`](CONTRIBUTING.md) for why the number was minted
73
+ > early and why that no longer happens.
74
+
75
+
76
+ ### Added
77
+
78
+ - Optional native accelerator `patchcraft-accel` (`pip install patchcraft[accel]`):
79
+ a Rust/pyo3 kernel for the overlap fold of `reconstruct`/`stitch`. Prebuilt
80
+ abi3 wheels (Python 3.12+) for Windows x64, Linux x86_64 (manylinux), macOS
81
+ arm64 and macOS x86_64; self-contained, no system dependencies, no torch
82
+ linkage. When it is absent, ABI-mismatched, ineligible (CUDA/integer
83
+ tensors), or disabled via `PATCHCRAFT_ACCEL=0`, every path falls back
84
+ silently to pure torch.
85
+ - `patchcraft.accel_available()` reports whether the accelerator is active.
86
+
87
+ ### Performance
88
+
89
+ - Overlap fold with the accelerator, mean of `lab/bench_phase2.py` on the
90
+ dev machine: reconstruct 3x512x512 p=32 s=16 17.727 ms -> 2.609 ms (6.80x);
91
+ stitch[hann] same geometry 23.714 ms -> 8.894 ms (2.67x); 3x2048x2048 p=64
92
+ s=32 reconstruct 469.814 ms -> 32.458 ms (14.47x). The accelerated `stitch`
93
+ also skips the full pre-multiply pass over the patches tensor.
94
+
95
+ ### Notes
96
+
97
+ - Numeric equivalence accel vs pure: bit-exact (`torch.equal`) across the
98
+ test grid (overlap x rectangular x stride∤patch x C in {1,3,4} x f32/f64).
99
+ - Linux validation: wheel
100
+ `patchcraft_accel-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`
101
+ installed and the test suite passed accelerated on Ubuntu 26.04 (WSL),
102
+ built via the manylinux maturin container.
103
+
104
+ ## [0.3.0] - 2026-09-01
105
+
106
+ > Source milestone, never uploaded to PyPI. Its contents reached
107
+ > users inside 0.5.0. See the versioning policy in
108
+ > [`CONTRIBUTING.md`](CONTRIBUTING.md) for why the number was minted
109
+ > early and why that no longer happens.
110
+
111
+
112
+ ### Performance
113
+
114
+ - `extract` uses a strided-window view (`Tensor.unfold`) for `dilation == 1`,
115
+ one copy instead of im2col + permute-contiguous: 13-21x faster on CPU,
116
+ bit-exact. `dilation > 1` still uses `F.unfold`.
117
+ - `reconstruct` skips `F.fold` entirely on non-overlapping grids (a pure
118
+ rearrangement, 27x faster on CPU) and computes the overlap count map in
119
+ closed form O(H+W) instead of folding a tensor of ones.
120
+ - `stitch` builds its denominator from two 1-D window folds (the kernels are
121
+ separable) instead of a second 2-D `F.fold`. `uniform` stays bit-exact vs
122
+ `reconstruct`; `hann`/`gaussian` may differ by ULPs (summation order).
123
+ - `patch_metrics`/`per_patch_mse` materialize one f64 tensor instead of
124
+ three and sync the device once instead of three times.
125
+
9
126
  ### Fixed
10
127
 
128
+ - `resize` with `backend="pil"` clamped integer tensors to the uint8 range
129
+ before casting; values outside [0, 255] used to wrap (300 became 44).
130
+ - Removed the machine-local `cache_dir = "Z:\caches\pytest"` from
131
+ `pyproject.toml` (created a literal `Z:\caches\...` directory on
132
+ Linux/macOS).
133
+
134
+ ### Documentation
135
+
136
+ - Corrected stale 0.2.1 version strings on all cover pages and in GUIDE.
137
+ - Corrected the gaussian-kernel floor claim in `stitch` (exp(-4) at corners,
138
+ not exp(-2) everywhere).
139
+ - Corrected the bit-exactness rule in THEORY/USAGE (power-of-two count map).
140
+ - SCOPE §4.4 no longer claims `reconstruct` lacks a dtype guard.
141
+ - CHANGELOG: link targets for [Unreleased], [0.2.2], [0.2.1].
142
+
143
+ ### Internal
144
+
145
+ - `reconstruct`/`stitch` share one fold-geometry validator
146
+ (`patchcraft._foldgeom.check_fold_geometry`); error messages unchanged.
147
+
148
+ ## [0.2.2] - 2026-08-30
149
+
150
+ Documentation release. No behaviour changes, so every call that worked on 0.2.1
151
+ works identically here, and the only file under `src/` that changed is a
152
+ docstring.
153
+
154
+ ### Fixed
155
+
156
+ - **`reconstruct`'s docstring claimed the overlap round trip returns the
157
+ original image.** It said each pixel comes back as "the average of all patches
158
+ covering it, the same as the original when patches came from `extract`
159
+ unmodified". The second half is false. Measured on a 16x16 image with
160
+ `patch_size=4, stride=1`, unmodified patches straight from `extract` come back
161
+ with `torch.equal` False and a maximum absolute error of 2.4e-07 in float32.
162
+
163
+ The docstring now states the real rule: the round trip is bit-exact when every
164
+ value in the count map is a power of two, because dividing a float by a power
165
+ of two is the one division that never rounds. It also says that widening the
166
+ dtype does not rescue it, since the deciding axis is the geometry rather than
167
+ the precision.
11
168
  - **`pair()`'s docstring double-applied the stride, and could silently
12
169
  misplace every patch for a caller who trusted it.** It said patch `k` has
13
170
  its top-left at LR pixel `(row * sh_lr, col * sw_lr)`, but `PatchMeta.row`
@@ -37,32 +194,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
37
194
  Reported from an API review done for the QSR prototype, which uses patch
38
195
  position as a context feature.
39
196
 
40
- ## [0.2.2] 2026-08-30
41
-
42
- Documentation release. No behaviour changes, so every call that worked on 0.2.1
43
- works identically here, and the only file under `src/` that changed is a
44
- docstring.
45
-
46
- ### Fixed
47
-
48
- - **`reconstruct`'s docstring claimed the overlap round trip returns the
49
- original image.** It said each pixel comes back as "the average of all patches
50
- covering it, the same as the original when patches came from `extract`
51
- unmodified". The second half is false. Measured on a 16x16 image with
52
- `patch_size=4, stride=1`, unmodified patches straight from `extract` come back
53
- with `torch.equal` False and a maximum absolute error of 2.4e-07 in float32.
54
-
55
- The docstring now states the real rule: the round trip is bit-exact when every
56
- value in the count map is a power of two, because dividing a float by a power
57
- of two is the one division that never rounds. It also says that widening the
58
- dtype does not rescue it, since the deciding axis is the geometry rather than
59
- the precision.
60
-
61
197
  ### Changed
62
198
 
63
199
  - **The README is now a call page and the manual moved to `docs/GUIDE.md`.** The
64
200
  README had reached 864 lines, which made it a manual printed on the front
65
- door. It is 137 lines now, and it answers what the library is, where you are
201
+ door. It is 156 lines now, and it answers what the library is, where you are
66
202
  getting into, and which of `reconstruct` or `stitch` you want. Nothing was
67
203
  discarded: every measurement, table and long example moved into the guide,
68
204
  which gained its own table of contents.
@@ -233,7 +369,7 @@ kernel so each pixel "trusts" patches closer to its center more.
233
369
  on overlap). With `"hann"` it strongly suppresses seams at the
234
370
  cost of zeroing image corners that fall on Hann's edge-weight-zero
235
371
  region (documented artifact). With `"gaussian"`
236
- (`sigma = max(1, min(ph, pw) / 4)`) it blends smoothly with no
372
+ (per-axis `sigma = max(1.0, n / 4.0)`) it blends smoothly with no
237
373
  corner artifact.
238
374
 
239
375
  Floating-point patches only, because window kernels are float-valued and we
@@ -336,5 +472,10 @@ First public release. Public API stable; signatures will only change in 1.x.
336
472
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
337
473
  validation lab.
338
474
 
475
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.4.0...HEAD
476
+ [0.4.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.4.0
477
+ [0.3.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.3.0
478
+ [0.2.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.2
479
+ [0.2.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.1
339
480
  [0.2.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.0
340
481
  [0.1.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.1.0
@@ -0,0 +1,161 @@
1
+ # Contributing to PatchCraft
2
+
3
+ If you've cloned this repo and want to run the test suite, understand how the project is laid out, or follow the validation conventions, this is the page. For installation and usage, see the [README](README.md).
4
+
5
+ ---
6
+
7
+ ## Run tests
8
+
9
+ ```
10
+ pytest
11
+ pytest -m "not gpu" # skip GPU-requiring tests
12
+ ```
13
+
14
+ CI runs the full suite on every push and PR. See [`.github/workflows/test.yml`](.github/workflows/test.yml). Matrix is `{ubuntu-latest, windows-latest} × {python 3.12, 3.13, 3.14}`. To run the same checks locally before pushing:
15
+
16
+ ```
17
+ ruff check src tests
18
+ mypy --strict src
19
+ pytest -m "not gpu"
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Layout
25
+
26
+ ```
27
+ PatchCraft/
28
+ ├── pyproject.toml package metadata, build backend (hatchling)
29
+ ├── README.md the call page, canonical, English
30
+ ├── README.pt-BR.md the same call page in Portuguese
31
+ ├── README.pypi.md the PyPI page (long_description), links absolute
32
+ ├── CONTRIBUTING.md this file
33
+ ├── CHANGELOG.md Keep-a-Changelog format
34
+ ├── LICENSE MIT
35
+ ├── .python-version 3.13
36
+ ├── .gitignore ignores archive/, venvs, caches, outputs
37
+ ├── .github/workflows/
38
+ │ ├── test.yml matrix CI on PRs/main, plus the accelerated job
39
+ │ └── release.yml publishes to PyPI on vX.Y.Z tag push (Trusted Publishing)
40
+ ├── src/patchcraft/ library core, one-image-at-a-time primitives
41
+ │ ├── __init__.py re-exports the full public API, holds __version__
42
+ │ ├── extract.py patches via strided view or F.unfold; Patchify (ADR 0002)
43
+ │ ├── reconstruct.py inverse via F.fold + count map, closed-form fast path
44
+ │ ├── stitch.py weighted reassembly for modified patches
45
+ │ ├── geometry.py pre-flight: num_patches, tilings, scale_factor, paired_tilings
46
+ │ ├── metrics.py patch_metrics, per_patch_mse, per_patch_psnr
47
+ │ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
48
+ │ ├── resize.py resize with PIL or torch backends
49
+ │ ├── cache.py content-addressed disk cache
50
+ │ ├── _accel.py bridge to the optional native accelerator, falls back silently
51
+ │ └── _foldgeom.py shared fold-geometry validation for reconstruct and stitch
52
+ ├── accel/ patchcraft-accel, a separate distribution (Rust, pyo3/maturin)
53
+ │ ├── Cargo.toml crate metadata, holds the accel version
54
+ │ ├── src/kernel.rs the gather-fold kernel, pure Rust, cargo-tested
55
+ │ ├── src/lib.rs pyo3 glue
56
+ │ └── README.md how to build it from source
57
+ ├── tests/ pytest suite (contract tests for src/)
58
+ │ ├── test_extract.py extract + Patchify
59
+ │ ├── test_reconstruct.py
60
+ │ ├── test_stitch.py
61
+ │ ├── test_geometry.py num_patches + tilings + scale_factor + paired_tilings
62
+ │ ├── test_metrics.py
63
+ │ ├── test_pair.py
64
+ │ ├── test_resize.py
65
+ │ ├── test_cache.py
66
+ │ ├── test_exactness.py falsifies the count-map predicate over the legal space
67
+ │ ├── test_reference.py naive loop-based reference for the fast paths
68
+ │ ├── test_public_api.py freezes the 20 names, their signatures and carrier fields
69
+ │ ├── test_accel.py native path, skipped unless the accelerator is installed
70
+ │ ├── test_datasets_helper.py label_subset
71
+ │ ├── test_import.py
72
+ │ ├── _rng.py audited round-trip helpers (data generation, bit equality)
73
+ │ ├── test_rng.py tests for those helpers
74
+ │ └── _datasets.py dev-only fixtures (MNIST, etc), NOT public API
75
+ ├── lab/ ephemeral experiments; see lab/README.md
76
+ │ ├── README.md bench rules (tracked)
77
+ │ ├── usage_demo.py regenerates the live REPL outputs in docs/USAGE.md
78
+ │ └── .gitignore ignores everything else (tracked)
79
+ ├── docs/
80
+ │ ├── GUIDE.md the manual, every README claim measured with its output
81
+ │ ├── USAGE.md REPL walkthrough of every public symbol (behind, see B4)
82
+ │ ├── SCOPE.md responsibilities matrix + parallelization analysis
83
+ │ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
84
+ │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
85
+ │ ├── ROADMAP.md milestone plan
86
+ │ ├── FOCO-1.0.md what 1.0 freezes and the blockers in the way
87
+ │ ├── ADR/ one file per decision that shaped the API
88
+ │ ├── STUDIES/ background reading behind those decisions
89
+ │ └── superpowers/ the design specs and implementation plans per work phase
90
+ └── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Validation lab
96
+
97
+ The library is "one image in, one tensor out" by design, but you only know it works once you run it end-to-end on real images. That happens in two places, neither of which is part of the shipped package:
98
+
99
+ - [`tests/`](tests/) holds the formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
100
+ - [`lab/`](lab/) holds ephemeral scripts and notebooks for fast hypothesis-checking. See [`lab/README.md`](lab/README.md) for the bench rules; outputs go to `Z:\outputs\patchcraft\` (off-tree).
101
+
102
+ Datasets used by tests/lab are downloaded lazily into `Z:\caches\datasets\<name>\` on first use; they do not ship with the package and are never bundled into the wheel.
103
+
104
+ The dev framework that makes this all work (fixtures, dataset helpers, `Z:\` conventions) is documented in [`docs/AUXILIARY.md`](docs/AUXILIARY.md).
105
+
106
+ ---
107
+
108
+ ## Versioning policy
109
+
110
+ The scheme is [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html),
111
+ expressed in the [PEP 440](https://peps.python.org/pep-0440/) form that PyPI
112
+ accepts. Two rules matter more than the arithmetic, because both were broken
113
+ once and both cost work to undo.
114
+
115
+ **The number names a published artifact, not a unit of work.** Finishing a
116
+ phase, merging a branch and landing a refactor are not releases, and none of
117
+ them touches `__version__`. Everything between releases accumulates under
118
+ `[Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md), and the number is assigned
119
+ in the release commit itself. Versions 0.3.0 and 0.4.0 came from the opposite
120
+ habit: each internal work phase minted a minor, so three numbers existed
121
+ before any of them reached PyPI.
122
+
123
+ **A published version is frozen.** PyPI refuses a filename it has already
124
+ seen, so a version that shipped can only be superseded, never edited. That is
125
+ also why a failed pipeline never costs a number: `skip-existing` makes the
126
+ upload a no-op and the same tag can be re-run.
127
+
128
+ Which component moves, while the project is pre-1.0 and SemVer leaves `0.y.z`
129
+ open:
130
+
131
+ | Change | Bump |
132
+ |---|---|
133
+ | Any output value, signature, name or documented behaviour changes | minor, `0.Y.0` |
134
+ | A fix that changes no documented behaviour and no signature | patch, `0.y.Z` |
135
+ | Documentation, tests, CI or tooling only | nothing |
136
+
137
+ After 1.0 the ordinary reading applies: breaking is major, additive is minor,
138
+ fixed is patch. What 1.0 freezes is written in
139
+ [`docs/FOCO-1.0.md`](docs/FOCO-1.0.md).
140
+
141
+ The accelerator in [`accel/`](accel/) is a separate distribution with its own
142
+ version in `accel/Cargo.toml`. It moves on its own schedule, and the two
143
+ numbers are not meant to track each other.
144
+
145
+ ---
146
+
147
+ ## Releasing (maintainer only)
148
+
149
+ 1. Bump `__version__` in [`src/patchcraft/__init__.py`](src/patchcraft/__init__.py).
150
+ 2. Close the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md) as `[X.Y.Z] YYYY-MM-DD`.
151
+ 3. Sweep the version strings the docs quote, which live in
152
+ [`docs/GUIDE.md`](docs/GUIDE.md) (the provenance note, the section 7
153
+ assertion, section 8 and the BibTeX entry) and in the Status block of the
154
+ three cover pages.
155
+ 4. Update [`docs/ROADMAP.md`](docs/ROADMAP.md) milestone checkboxes.
156
+ 5. Commit: `release: vX.Y.Z`.
157
+ 6. Tag + push: `git tag -a vX.Y.Z -m "..."` then `git push origin vX.Y.Z`.
158
+ 7. `release.yml` fires automatically: validates → builds → publishes to PyPI via Trusted Publishing → creates GitHub Release with `.whl` + `.tar.gz`.
159
+
160
+ The tag must match `__version__`, and the `validate` job fails the run if it
161
+ does not.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchcraft
3
- Version: 0.2.2
3
+ Version: 0.5.0
4
4
  Summary: Image patch extraction, reconstruction, pairing and seam-aware stitching for super-resolution and dataset pipelines.
5
5
  Project-URL: Homepage, https://github.com/LeoPR/PatchCraft
6
6
  Project-URL: Repository, https://github.com/LeoPR/PatchCraft
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3
20
20
  Classifier: Programming Language :: Python :: 3 :: Only
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
23
24
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
25
  Classifier: Topic :: Scientific/Engineering :: Image Processing
25
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -28,6 +29,8 @@ Requires-Python: >=3.12
28
29
  Requires-Dist: numpy>=1.26
29
30
  Requires-Dist: pillow>=10
30
31
  Requires-Dist: torch>=2.6
32
+ Provides-Extra: accel
33
+ Requires-Dist: patchcraft-accel>=0.1.0; extra == 'accel'
31
34
  Provides-Extra: cache
32
35
  Requires-Dist: zstandard>=0.22; extra == 'cache'
33
36
  Provides-Extra: dev
@@ -51,6 +54,24 @@ PatchCraft takes a single `(C, H, W)` float tensor, cuts it into a stack of patc
51
54
 
52
55
  The scope is one image at a time, and that is worth knowing before you install anything, because it is the constraint that decides whether PatchCraft fits your problem at all. There is no batching across images, no `Dataset`, no `DataLoader` and no training loop, so multi-image work stays in your own `for` loop, in your `torch.vmap`, or in your `DataLoader` calling this once per item.
53
56
 
57
+ ```
58
+ one image the patch stack one image again
59
+ (1, 4, 4) (4, 1, 2, 2), row-major (1, 4, 4)
60
+
61
+ +-----+-----+ +-----+ +-----+ +-----+-----+
62
+ | A A | B B | | A A | | B B | | A A | B B |
63
+ | A A | B B | extract | A A | | B B | reconstruct | A A | B B |
64
+ +-----+-----+ ----------> +--p0-+ +--p1-+ ----------> +-----+-----+
65
+ | C C | D D | patch_size=2 +-----+ +-----+ stride=2 | C C | D D |
66
+ | C C | D D | stride=2 | C C | | D D | | C C | D D |
67
+ +-----+-----+ | C C | | D D | +-----+-----+
68
+ +--p2-+ +--p3-+
69
+ ```
70
+
71
+ The image goes out as a stack of patches, you do your work on the stack, and it comes back as one
72
+ image. That last arrow has two doors: `reconstruct` when the patches are untouched, and `stitch`
73
+ when a model rewrote them and the seams need to fade.
74
+
54
75
  This page is the short one. The manual is [docs/GUIDE.md](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md), which carries the measurements, the tables and the long examples.
55
76
 
56
77
  ## Install
@@ -60,7 +81,18 @@ pip install patchcraft
60
81
  pip install "patchcraft[cache]" # adds zstandard, which compresses Cache payloads
61
82
  ```
62
83
 
63
- The distribution name and the import name are both `patchcraft`. The extra is optional, because `Cache` works without `zstandard` as well and simply stores its payload uncompressed.
84
+ Or with the optional native accelerator (prebuilt wheels for Windows x64,
85
+ Linux x86_64, macOS arm64 and macOS x86_64; the package stays pure-Python
86
+ and fully functional without it):
87
+
88
+ ```bash
89
+ pip install patchcraft[accel]
90
+ ```
91
+
92
+ `patchcraft.accel_available()` reports at runtime whether the accelerator is
93
+ active; `PATCHCRAFT_ACCEL=0` in the environment forces the pure path.
94
+
95
+ The distribution name and the import name are both `patchcraft`. The cache extra is optional, because `Cache` works without `zstandard` as well and simply stores its payload uncompressed.
64
96
 
65
97
  ## Sixty seconds
66
98
 
@@ -118,7 +150,7 @@ The saving is real on the other side too. Tiling an image, running a per-patch m
118
150
 
119
151
  `extract` follows whatever grid you hand it, but `reconstruct` and `stitch` refuse a grid that does not cover the image exactly, rather than returning a plausible tensor built on missing pixels. On a 128x128 image with `patch_size=32` and `stride=20` the grid reaches only 112x112, which leaves 3840 of the 16384 pixels at zero, and the error message names that covered extent instead of hiding it.
120
152
 
121
- The answer is to pick a legal geometry rather than to pad the image into one, because padding synthesizes pixels you never had. `tilings(image_shape)` enumerates the legal geometries from the shape alone and allocates nothing while it does so, so you can call it before you have committed to anything: a 28x28 image has 5 exact tilings, and 100 of them once `allow_overlap=True` lets the patches overlap.
153
+ The answer is to pick a legal geometry rather than to pad the image into one, because padding synthesizes pixels you never had. `tilings(image_shape)` enumerates the legal geometries from the shape alone and allocates nothing while it does so, so you can call it before you have committed to anything: a 28x28 image has 5 exact tilings, and 73 of them once `allow_overlap=True` lets the patches overlap.
122
154
 
123
155
  Two narrower questions have their own entry points. `num_patches` takes a geometry you already have in mind and returns the grid it implies, and `paired_tilings` is the one to reach for when a low-resolution image and a high-resolution image have to stay aligned patch for patch.
124
156
 
@@ -132,20 +164,20 @@ It helps when you tile one image for an inference pass too large to run in a sin
132
164
 
133
165
  The round trip is exact when every value in the count map is a power of two. The reason is that reconstruction divides each pixel by the number of patches that covered it, and dividing a float by a power of two is the one division that never rounds.
134
166
 
135
- That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule `float32` misses by roughly `1e-7`, and `float64` misses too, by roughly `1e-16`.
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.
136
168
 
137
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.
138
170
 
139
171
  ## Status
140
172
 
141
- This page documents 0.2.1, which is pre-1.0, so both the output values and the API shape can still change in a minor release, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) is where each of those changes is recorded with the measurement behind it. The suite runs 346 tests and they pass on Python 3.12 and 3.13, on Ubuntu and on Windows alike.
173
+ This page documents 0.5.0, which is pre-1.0, so both the output values and the API shape can still change in a minor release, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) is where each of those changes is recorded with the measurement behind it. The suite collects 1571 tests and passes on Python 3.12, 3.13 and 3.14, on Ubuntu and on Windows alike.
142
174
 
143
175
  Two limits are worth knowing before you depend on it. Every figure on this page was measured on CPU, and no CUDA path has ever executed in the test matrix, so the pipeline does preserve the device you hand it while the exactness numbers stay unverified on GPU. The other limit is that no external project has consumed the published API in real use yet, and that consumption is this project's own stated gate for calling the shape settled.
144
176
 
145
177
  ## Documentation
146
178
 
147
179
  - [Guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md), the manual, with every figure on this page shown as runnable code
148
- - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of each of the 19 public symbols
180
+ - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of each of the 20 public symbols
149
181
  - [Theory](https://github.com/LeoPR/PatchCraft/blob/main/docs/THEORY.md), the math and the per-function contract
150
182
  - [Scope](https://github.com/LeoPR/PatchCraft/blob/main/docs/SCOPE.md), the line between this library and your pipeline
151
183
  - [Repository](https://github.com/LeoPR/PatchCraft), [issues](https://github.com/LeoPR/PatchCraft/issues) and [contributing](https://github.com/LeoPR/PatchCraft/blob/main/CONTRIBUTING.md)