patchcraft 0.2.0__tar.gz → 0.2.1__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 (52) hide show
  1. {patchcraft-0.2.0 → patchcraft-0.2.1}/.github/workflows/release.yml +19 -2
  2. {patchcraft-0.2.0 → patchcraft-0.2.1}/.github/workflows/test.yml +10 -2
  3. patchcraft-0.2.1/CHANGELOG.md +271 -0
  4. patchcraft-0.2.1/CONTRIBUTING.md +98 -0
  5. {patchcraft-0.2.0 → patchcraft-0.2.1}/PKG-INFO +30 -88
  6. patchcraft-0.2.1/README.md +168 -0
  7. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/ADR/0001-patch-extraction-api.md +6 -6
  8. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/ADR/0002-patchify-transform.md +9 -9
  9. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/AUXILIARY.md +24 -22
  10. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/ROADMAP.md +40 -23
  11. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/SCOPE.md +51 -40
  12. patchcraft-0.2.1/docs/STUDIES/2026-08-04-patch-techniques.md +165 -0
  13. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/THEORY.md +82 -73
  14. {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/USAGE.md +48 -45
  15. {patchcraft-0.2.0 → patchcraft-0.2.1}/pyproject.toml +11 -0
  16. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/__init__.py +4 -3
  17. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/cache.py +247 -247
  18. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/extract.py +116 -116
  19. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/geometry.py +4 -4
  20. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/metrics.py +13 -5
  21. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/pair.py +2 -2
  22. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/reconstruct.py +39 -9
  23. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/resize.py +202 -189
  24. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/stitch.py +52 -28
  25. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/_datasets.py +1 -1
  26. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_cache.py +2 -2
  27. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_extract.py +1 -1
  28. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_geometry.py +1 -1
  29. patchcraft-0.2.1/tests/test_import.py +14 -0
  30. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_metrics.py +45 -2
  31. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_pair.py +1 -1
  32. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_reconstruct.py +60 -3
  33. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_resize.py +43 -1
  34. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_stitch.py +78 -24
  35. patchcraft-0.2.1/uv.lock +1041 -0
  36. patchcraft-0.2.0/.vscode/settings.json +0 -3
  37. patchcraft-0.2.0/CHANGELOG.md +0 -175
  38. patchcraft-0.2.0/README.md +0 -226
  39. patchcraft-0.2.0/lab/.gitignore +0 -3
  40. patchcraft-0.2.0/lab/2026-05-16-roundtrip-mnist.py +0 -80
  41. patchcraft-0.2.0/lab/README.md +0 -34
  42. patchcraft-0.2.0/lab/usage_demo.out +0 -174
  43. patchcraft-0.2.0/lab/usage_demo.py +0 -239
  44. patchcraft-0.2.0/tests/test_import.py +0 -7
  45. patchcraft-0.2.0/uv.lock +0 -1010
  46. {patchcraft-0.2.0 → patchcraft-0.2.1}/.gitignore +0 -0
  47. {patchcraft-0.2.0 → patchcraft-0.2.1}/.python-version +0 -0
  48. {patchcraft-0.2.0 → patchcraft-0.2.1}/LICENSE +0 -0
  49. {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/py.typed +0 -0
  50. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/__init__.py +0 -0
  51. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/conftest.py +0 -0
  52. {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_datasets_helper.py +0 -0
@@ -25,13 +25,24 @@ jobs:
25
25
  validate:
26
26
  name: re-run tests at tag ref
27
27
  runs-on: ubuntu-latest
28
+ env:
29
+ UV_PYTHON: "3.13"
28
30
  steps:
29
31
  - uses: actions/checkout@v4
30
32
  - uses: astral-sh/setup-uv@v3
31
33
  with:
32
34
  enable-cache: true
33
- - run: uv python install 3.13
34
- - run: uv sync --extra cache --extra dev --python 3.13
35
+ - run: uv python install
36
+ - run: uv sync --locked --extra cache --extra dev
37
+ - name: Tag must match __version__
38
+ run: |
39
+ tag="${GITHUB_REF_NAME#v}"
40
+ pkg="$(uv run python -c 'import patchcraft; print(patchcraft.__version__)')"
41
+ if [ "$tag" != "$pkg" ]; then
42
+ echo "::error::tag ${GITHUB_REF_NAME} implies version '${tag}' but patchcraft.__version__ is '${pkg}'"
43
+ exit 1
44
+ fi
45
+ echo "tag ${GITHUB_REF_NAME} matches __version__ ${pkg}"
35
46
  - run: uv run ruff check src tests
36
47
  - run: uv run mypy --strict src
37
48
  - run: uv run pytest -m "not gpu"
@@ -69,6 +80,12 @@ jobs:
69
80
  path: dist/
70
81
  - name: Publish via Trusted Publishing
71
82
  uses: pypa/gh-action-pypi-publish@release/v1
83
+ with:
84
+ # A version already on PyPI is a no-op instead of a hard failure.
85
+ # Without this, re-running the pipeline (or tagging a version that
86
+ # was uploaded by hand) fails here, and `github-release` never runs
87
+ # because it is `needs: publish-pypi`.
88
+ skip-existing: true
72
89
  # No `password` field -- OIDC handles authentication.
73
90
  # The pypi.org "Pending publisher" config must match:
74
91
  # PyPI project name: patchcraft
@@ -19,6 +19,12 @@ jobs:
19
19
  matrix:
20
20
  os: [ubuntu-latest, windows-latest]
21
21
  python: ["3.12", "3.13"]
22
+ env:
23
+ # Force uv to use the matrix Python in every uv* invocation
24
+ # (otherwise `uv run` re-resolves to its default, which is the
25
+ # newest installed interpreter -- masking the matrix and leaking
26
+ # the dev deps installed under a different version).
27
+ UV_PYTHON: ${{ matrix.python }}
22
28
  steps:
23
29
  - uses: actions/checkout@v4
24
30
 
@@ -28,10 +34,12 @@ jobs:
28
34
  enable-cache: true
29
35
 
30
36
  - name: Set up Python ${{ matrix.python }}
31
- run: uv python install ${{ matrix.python }}
37
+ run: uv python install
32
38
 
33
39
  - name: Install project with dev + cache extras
34
- run: uv sync --extra cache --extra dev --python ${{ matrix.python }}
40
+ # --locked: fail if uv.lock is stale or absent rather than silently
41
+ # re-resolving, so the committed lock is what CI actually validates.
42
+ run: uv sync --locked --extra cache --extra dev
35
43
 
36
44
  - name: Ruff
37
45
  run: uv run ruff check src tests
@@ -0,0 +1,271 @@
1
+ # Changelog
2
+
3
+ All notable changes to PatchCraft will be documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.2.1] 2026-08-04
8
+
9
+ Bugfix release. Closes the full correctness backlog found by the 0.2.0 audit
10
+ (2026-08-03). Every fix below was first reproduced as a failing regression
11
+ test measuring the same quantities quoted from the audit, then fixed, then
12
+ verified by round-trip lab scripts under `lab/2026-08-04-*.py` (reports in
13
+ `Z:\outputs\patchcraft\2026-08-04-*\`). Test suite: 309 → 345 passed. No new
14
+ features; the only namespace change is `WeightKind` becoming reachable, which
15
+ was itself an audit defect.
16
+
17
+ ### Fixed: the four audit defects
18
+
19
+ - **`reconstruct` and `stitch` silently zeroed every pixel the patch grid did
20
+ not cover.** Both validated only the patch *count*
21
+ (`n_patches == num_h * num_w`) and never the *coverage*
22
+ (`(num_h - 1) * sh + ph == h`). A truncated grid returned a partly-black
23
+ image instead of raising, contradicting the bit-exact round-trip guarantee
24
+ in `docs/THEORY.md` §9.2. Measured on 0.2.0: `10×10` with `patch_size=4,
25
+ stride=4` returned 36 of 100 pixels zeroed; `13×13` with `patch_size=5,
26
+ stride=5` returned 69 of 169. Both functions now reject any grid whose last
27
+ patch does not end exactly on the image edge, with a `ValueError` naming the
28
+ covered vs. requested extents and pointing at `patchcraft.tilings`.
29
+ - **`stitch(..., weight="hann")` zeroed most of the image, not the four
30
+ corners.** The old `_hann_1d(n)` was the symmetric window
31
+ `0.5·(1 − cos(2πi/(n−1)))`, exactly zero at both endpoints. Measured on
32
+ 0.2.0: `12×12, patch 4, stride 4` came back with 108 of 144 pixels zeroed;
33
+ `patch_size=2` degenerated to an all-zero window and returned an all-black
34
+ image with no error. The window is now the interior of a longer symmetric
35
+ Hann window, `hann_window(n + 2, periodic=False)[1:-1]`, strictly positive
36
+ on every sample (THEORY §2.5). Output values of `stitch(hann)` change for
37
+ every geometry; round-trip of unmodified patches remains exact.
38
+ - **`resize` corrupted integer dtypes on the torch backend.** `_resize_torch`
39
+ cast back with a bare `out[0].to(original_dtype)`, with no clamp and no
40
+ round. Bicubic legitimately overshoots the input range, so the cast wrapped:
41
+ `-9.0 → 247`, `281.9 → 25`. Measured on 0.2.0: an 8×8 uint8 hard edge
42
+ resized to 32×32 had 256 of 1024 pixels wrong, black pixels becoming 254
43
+ and white pixels becoming 1. The cast back now rounds and clamps to the
44
+ `torch.iinfo` range of the integer dtype. Separately, the `pil` backend's
45
+ clamp of out-of-range floats to `[0, 1]` (the uint8 hop) is now documented
46
+ behavior in the docstring and THEORY §9.4 instead of a silent surprise.
47
+ - **`per_patch_mse` / `per_patch_psnr` did not promote to `float64`** the way
48
+ `patch_metrics` does. On 0.2.0, `uint8` input raised a raw torch
49
+ `RuntimeError` with no mention of PatchCraft, and `float16` input silently
50
+ returned `inf`. Both now compute in `float64` for any input dtype and always
51
+ return `float64` (contract change recorded in THEORY §9.8).
52
+
53
+ ### Fixed: secondary defects recorded by the audit
54
+
55
+ - **`reconstruct` and `stitch` overflowed to `inf` on `float16`/`bfloat16`.**
56
+ `F.fold` accumulates the sum of all overlapping patches before the count-map
57
+ division, and the numerator exceeds the fp16 finite max (65504) well before
58
+ the division happens. Measured on 0.2.0: fp16 constant image of `10000.0`,
59
+ `patch 3, stride 1`, returned `inf` in 144 of 256 pixels. Half-precision
60
+ inputs now accumulate internally in `float32` (fold, count map, division)
61
+ and are cast back to the original dtype on return.
62
+ - **`reconstruct` rejected integer dtypes with a raw torch
63
+ `NotImplementedError`** (`col2im_out_cpu` not implemented for
64
+ `Byte`/`Int`/`Long`). It now raises a clear `ValueError` up front, matching
65
+ the `stitch` guard.
66
+ - **`WeightKind` was unreachable from the public namespace** despite appearing
67
+ in `stitch`'s signature. It is now exported by `patchcraft.stitch` and
68
+ re-exported by `patchcraft` (public API: 18 → 19 symbols).
69
+
70
+ ### Fixed: CI and packaging (carried over from the audit pass)
71
+
72
+ - The sdist no longer ships `lab/` or `.vscode/`. The published
73
+ `patchcraft-0.2.0.tar.gz` contains `lab/usage_demo.py`,
74
+ `lab/usage_demo.out` and `lab/2026-05-16-roundtrip-mnist.py`, three files
75
+ that `lab/.gitignore` excludes from version control, which made that sdist
76
+ impossible to reproduce from any commit. The wheel was never affected.
77
+ - `.github/workflows/release.yml` passes `skip-existing: true` to
78
+ `gh-action-pypi-publish`, so re-running the pipeline on an already-published
79
+ version is a no-op instead of a hard failure that also skips the
80
+ GitHub Release job. The `validate` job now refuses to proceed when the tag
81
+ does not match `patchcraft.__version__`.
82
+ - Both workflows run `uv sync --locked`, so `uv.lock` is now actually enforced
83
+ in CI rather than being advisory.
84
+
85
+ ## [0.2.0] 2026-05-17
86
+
87
+ Second public release. Adds three feature groups motivated by the QPatchSR
88
+ super-resolution consumer plus internal ergonomics. No breaking changes vs
89
+ v0.1.0, so all v0.1.0 imports keep working.
90
+
91
+ ### Changed: package name (twice, both on 2026-05-17)
92
+
93
+ The project shipped v0.1.0 to GitHub under the name **PatchKit** and was
94
+ renamed twice in the run-up to the first PyPI upload, each time because the
95
+ name was already taken:
96
+
97
+ 1. `patchkit` → `patchforge` (commit `f761834`), because `pypi.org/project/patchkit/`
98
+ belongs to an unrelated model-patching utility.
99
+ 2. `patchforge` → `patchcraft` (commit `627a9c8`, final), because
100
+ `pypi.org/project/patchforge/` was also taken, by a llama-server CLI.
101
+
102
+ Both renames were done by string substitution across the tree, which rewrote
103
+ the historical entries below: the v0.1.0 section of this file says
104
+ `patchcraft`, but `git show v0.1.0:pyproject.toml` says `name = "patchkit"`
105
+ and `git ls-tree v0.1.0 src/` says `src/patchkit`. **v0.1.0 was never
106
+ published to PyPI under any name**: `patchcraft` 0.2.0 is the first and only
107
+ PyPI release, so no import path that ever existed on PyPI has changed, and no
108
+ migration shim is needed. Read the v0.1.0 section as a record of *what the API
109
+ was*, not of *what the package was called*.
110
+
111
+ ### Added: cross-resolution geometry (THEORY §1.5, §9.7)
112
+
113
+ Motivated by the QPatchSR consumer's question: "given two image shapes
114
+ (LR and HR of the same source), what `(patch_size, stride)` on each
115
+ side yields the same number of patches with corresponding regions?"
116
+ Three new helpers in `patchcraft.geometry`:
117
+
118
+ - **`scale_factor(lr_shape, hr_shape) -> int | None`** returns the
119
+ integer `k` such that `hr.shape[-2:] == (k * lr.shape[-2], k *
120
+ lr.shape[-1])`, or `None`. Accepts `(H, W)` or `(C, H, W)`. Pre-
121
+ flight check for `pair`.
122
+ - **`paired_tilings(lr_shape, hr_shape, *, allow_overlap=False, ...)`**
123
+ enumerates every `(lr_spec, hr_spec)` pair where both fully cover
124
+ their respective image and produce identical patch counts. Patch
125
+ `k` on each side covers the same image region.
126
+ Example: `paired_tilings((14, 14), (28, 28))` returns three pairs:
127
+ `(p_lr=2, p_hr=4, total=49)`, `(p_lr=7, p_hr=14, total=4)`,
128
+ `(p_lr=14, p_hr=28, total=1)`.
129
+ - **`PairedTilingSpec(lr, hr, scale_factor)`** is a `NamedTuple` carrying
130
+ both sides and the discovered scale factor.
131
+
132
+ ### Added: patch-level pixel metrics (THEORY §1.6, §9.8)
133
+
134
+ Canonical reductions so consumers don't reinvent slightly-divergent
135
+ versions in every project. New module `patchcraft.metrics`:
136
+
137
+ - **`patch_metrics(a, b, *, max_value=1.0) -> dict[str, float]`**:
138
+ scalar `mae`, `mse`, `max_abs`, `psnr_db` over the full tensor
139
+ (any matching shape works). Internal accumulation in `float64`
140
+ for stability; PSNR returns `+inf` for identical inputs.
141
+ - **`per_patch_mse(a, b) -> Tensor[L]`** gives one MSE per patch in a
142
+ `(L, C, h, w)` stack.
143
+ - **`per_patch_psnr(a, b, *, max_value=1.0) -> Tensor[L]`** gives one
144
+ PSNR per patch. Identical patches yield `+inf` via `torch.where`
145
+ (no clamp tricks).
146
+
147
+ Explicitly **not** included: SSIM, MS-SSIM, LPIPS, FID, any windowed
148
+ or learned metric. Use `pytorch-msssim`, `lpips`, `clean-fid` on the
149
+ caller side ([SCOPE.md](docs/SCOPE.md) §4.3 explains the boundary).
150
+
151
+ ### Added: patch stitching for modified patches (THEORY §2.5, §9.9)
152
+
153
+ `reconstruct` is the bit-exact inverse of `extract`. When patches have
154
+ been modified (model output, denoised, super-resolved), averaging them
155
+ back uniformly shows visible seams at patch boundaries. `stitch` is the
156
+ seam-aware counterpart: it folds patches weighted by a 2-D window
157
+ kernel so each pixel "trusts" patches closer to its center more.
158
+
159
+ - **`stitch(patches, image_shape, stride, *, weight="uniform"|"hann"|"gaussian", dilation=1)`**
160
+ keeps the same `F.fold` geometry and rejections as `reconstruct`; adds a
161
+ weighted-blend numerator over a weighted-sum denominator. With
162
+ `weight="uniform"` it is mathematically equivalent to `reconstruct`
163
+ (covered by a bit-exact equality test on no-overlap and `allclose`
164
+ on overlap). With `"hann"` it strongly suppresses seams at the
165
+ cost of zeroing image corners that fall on Hann's edge-weight-zero
166
+ region (documented artifact). With `"gaussian"`
167
+ (`sigma = max(1, min(ph, pw) / 4)`) it blends smoothly with no
168
+ corner artifact.
169
+
170
+ Floating-point patches only, because window kernels are float-valued and we
171
+ refuse to silently quantize or implicitly promote. Caller converts to
172
+ `float` first.
173
+
174
+ ### Changed
175
+
176
+ - Public API surface: 11 → 18 symbols.
177
+ - [`docs/SCOPE.md`](docs/SCOPE.md) gains rows for paired tilings,
178
+ pixel metrics, and stitch; §4.3 discusses why pixel metrics stayed
179
+ core while windowed/learned metrics did not, §4.4 explains why
180
+ `stitch` is a separate function from `reconstruct` rather than a
181
+ kwarg.
182
+ - [`docs/THEORY.md`](docs/THEORY.md) gains §1.5 expansion (cross-
183
+ resolution paragraphs), §1.6 (patch comparison metrics), §2.5
184
+ (stitch: math, kernels, why it is separate), §9.7
185
+ (paired tilings contract), §9.8 (metrics contract), §9.9 (stitch
186
+ contract).
187
+
188
+ ## [0.1.0] 2026-05-16
189
+
190
+ First public release. Public API stable; signatures will only change in 1.x.
191
+
192
+ ### Added: core (one image at a time)
193
+
194
+ - **`extract(image, patch_size, stride, dilation=1)`** gives patches from a
195
+ `(C, H, W)` tensor via `torch.nn.functional.unfold`. Truncation-only
196
+ boundary; returns `Tensor[0, C, ph, pw]` when geometry fits no patch.
197
+ Per [ADR 0001](docs/ADR/0001-patch-extraction-api.md).
198
+ - **`Patchify(patch_size, stride, dilation=1)`** is the callable wrapper for
199
+ `torchvision.transforms.Compose([...])`. Eager geometry validation in
200
+ `__init__`; `__slots__`-bound (no state beyond config). Per
201
+ [ADR 0002](docs/ADR/0002-patchify-transform.md).
202
+ - **`reconstruct(patches, image_shape, stride, dilation=1)`** is the inverse
203
+ of `extract` via `F.fold` plus a fold-of-ones count map. Bit-exact
204
+ round-trip for `stride == patch_size`; weighted-exact for overlap.
205
+ Rejects `dilation != 1` and `stride > patch_size` (partial coverage
206
+ is forbidden by design, because synthesizing pixel values is not PatchCraft's
207
+ job).
208
+ - **`pair(lr_image, hr_image, lr_patch_size, scale_factor, stride, *, image_id=None)`**
209
+ gives LR/HR patch correspondences. Returns a frozen `PatchPair`
210
+ dataclass with `lr_patches`, `hr_patches`, `metas`. Integer
211
+ `scale_factor` only. LR and HR must share `C`, dtype, and device.
212
+ - **`PatchPair`**, **`PatchMeta`** are frozen `@dataclass(slots=True)`.
213
+ `PatchMeta` carries `patch_index`, `row`, `col` (LR coords),
214
+ `lr_patch_size`, `hr_patch_size`, `image_id`. CPU-only metadata.
215
+ - **`resize(image, target_size, backend="pil", resample=None)`**:
216
+ single-image resize. Output type matches input
217
+ (PIL → PIL, Tensor → Tensor). Cross-backend conversions go through
218
+ a float32 [0, 1] / uint8 hop (numpy intermediate; no torchvision in
219
+ the core). CUDA tensors accepted only with `backend="torch"`.
220
+ - **`Cache(root, namespace, version=1)`** is a content-addressed disk
221
+ cache. `key_for(*parts) → str`, `put(key, bytes)`, `get(key) → bytes | None`.
222
+ Atomic write via `*.tmp` + `os.replace` with retry on transient
223
+ `PermissionError` (5 attempts on put with exponential backoff
224
+ `0.25/0.5/1/2/4` s, which handles OneDrive, antivirus, Windows Search
225
+ races). Optional zstandard compression at level 3 (`[cache]` extra);
226
+ uncompressed fallback when not installed. Sidecar JSON carries
227
+ SHA-256 checksum; corruption surfaces as `OSError`.
228
+ - **`num_patches(image_shape, patch_size, stride, dilation=1)`** is the
229
+ patch count formula, exposed as a function. No allocation, no
230
+ tensor. Accepts `(H, W)` or `(C, H, W)`.
231
+ - **`tilings(image_shape, *, allow_overlap=False, min_patch_size=2, max_patch_size=None)`**
232
+ enumerates every square, full-coverage `(patch_size, stride)`
233
+ geometry. Always emits `dilation=(1, 1)`. With default flags returns
234
+ exact tilings only (`patch_size == stride`, divisibility); with
235
+ `allow_overlap=True` adds clean-edge overlap geometries. Truncated
236
+ geometries are deliberately excluded, because the function answers "what is
237
+ sound by construction?", not "what will `extract` accept?".
238
+ - **`TilingSpec`** is a `NamedTuple(patch_size, stride, dilation,
239
+ num_patches, total_patches, overlap)`.
240
+
241
+ ### Added: packaging
242
+
243
+ - `py.typed` marker (PEP 561): downstream `mypy` now honors PatchCraft's
244
+ type hints.
245
+ - `[cache]` extra: pulls `zstandard>=0.22` for compressed cache
246
+ entries. Core works without it.
247
+
248
+ ### Out of scope (v0.1.x)
249
+
250
+ - Multi-image batched API: use a `for` loop, `torch.vmap`, or a
251
+ `DataLoader`. See `Patchify` for `transforms.Compose` integration.
252
+ - Dataset orchestration (download, batching, sampling): the auxiliary
253
+ framework in [`tests/_datasets.py`](tests/_datasets.py) handles this
254
+ for the test suite and `lab/` scripts; it is not shipped in the wheel.
255
+ - Channels-last layout, quantization, `nn.Module` integration:
256
+ documented in [`docs/THEORY.md`](docs/THEORY.md) §6 and §8 (open
257
+ questions).
258
+
259
+ ### Documentation
260
+
261
+ - [`docs/THEORY.md`](docs/THEORY.md) has §0 binding scope, §§1–6 design
262
+ decisions per primitive, §7 resolved questions, §8 open questions,
263
+ §9 the per-API condition contract (Accepts / Rejects / Out of scope)
264
+ that the test suite mirrors.
265
+ - [`docs/ADR/0001-patch-extraction-api.md`](docs/ADR/0001-patch-extraction-api.md)
266
+ and [`docs/ADR/0002-patchify-transform.md`](docs/ADR/0002-patchify-transform.md).
267
+ - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
268
+ validation lab.
269
+
270
+ [0.2.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.0
271
+ [0.1.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.1.0
@@ -0,0 +1,98 @@
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}`. 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 public-facing (also goes to PyPI as long_description)
30
+ ├── CONTRIBUTING.md this file
31
+ ├── CHANGELOG.md Keep-a-Changelog format
32
+ ├── LICENSE MIT
33
+ ├── .python-version 3.13
34
+ ├── .gitignore ignores archive/, venvs, caches, outputs
35
+ ├── .github/workflows/
36
+ │ ├── test.yml matrix CI on PRs/main
37
+ │ └── release.yml publishes to PyPI on vX.Y.Z tag push (Trusted Publishing)
38
+ ├── src/patchcraft/ library core, one-image-at-a-time primitives
39
+ │ ├── __init__.py re-exports the full public API
40
+ │ ├── extract.py patches via F.unfold; Patchify wrapper (ADR 0002)
41
+ │ ├── reconstruct.py inverse via F.fold + count map
42
+ │ ├── stitch.py weighted reassembly for modified patches
43
+ │ ├── geometry.py pre-flight: num_patches, tilings, scale_factor, paired_tilings
44
+ │ ├── metrics.py patch_metrics, per_patch_mse, per_patch_psnr
45
+ │ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
46
+ │ ├── resize.py resize with PIL or torch backends
47
+ │ └── cache.py content-addressed disk cache
48
+ ├── tests/ pytest suite (contract tests for src/)
49
+ │ ├── test_extract.py extract + Patchify
50
+ │ ├── test_reconstruct.py
51
+ │ ├── test_stitch.py
52
+ │ ├── test_geometry.py num_patches + tilings + scale_factor + paired_tilings
53
+ │ ├── test_metrics.py
54
+ │ ├── test_pair.py
55
+ │ ├── test_resize.py
56
+ │ ├── test_cache.py
57
+ │ ├── test_datasets_helper.py label_subset
58
+ │ ├── test_import.py
59
+ │ └── _datasets.py dev-only fixtures (MNIST, etc), NOT public API
60
+ ├── lab/ ephemeral experiments; see lab/README.md
61
+ │ ├── README.md bench rules (tracked)
62
+ │ ├── usage_demo.py regenerates the live REPL outputs in docs/USAGE.md
63
+ │ └── .gitignore ignores everything else (tracked)
64
+ ├── docs/
65
+ │ ├── USAGE.md live REPL walkthrough of every public API
66
+ │ ├── SCOPE.md responsibilities matrix + parallelization analysis
67
+ │ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
68
+ │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
69
+ │ ├── ROADMAP.md milestone plan
70
+ │ └── ADR/
71
+ │ ├── 0001-patch-extraction-api.md pure function `extract`
72
+ │ └── 0002-patchify-transform.md callable wrapper for Compose pipelines
73
+ └── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Validation lab
79
+
80
+ 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:
81
+
82
+ - [`tests/`](tests/) holds the formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
83
+ - [`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).
84
+
85
+ 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.
86
+
87
+ The dev framework that makes this all work (fixtures, dataset helpers, `Z:\` conventions) is documented in [`docs/AUXILIARY.md`](docs/AUXILIARY.md).
88
+
89
+ ---
90
+
91
+ ## Releasing (maintainer only)
92
+
93
+ 1. Bump `__version__` in [`src/patchcraft/__init__.py`](src/patchcraft/__init__.py).
94
+ 2. Close the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md) as `[X.Y.Z] YYYY-MM-DD`.
95
+ 3. Update [`docs/ROADMAP.md`](docs/ROADMAP.md) milestone checkboxes.
96
+ 4. Commit: `release: vX.Y.Z`.
97
+ 5. Tag + push: `git tag -a vX.Y.Z -m "..."` then `git push origin vX.Y.Z`.
98
+ 6. `release.yml` fires automatically: validates → builds → publishes to PyPI via Trusted Publishing → creates GitHub Release with `.whl` + `.tar.gz`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchcraft
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -40,18 +40,18 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  # PatchCraft
42
42
 
43
- A small library for **encoding an image into patches and decoding it back**. Built to slot into other people's `torch` pipelines as one transform among many like a `GaussianBlur` step in a `Compose([...])`.
43
+ A small library for **encoding an image into patches and decoding it back**. Built to slot into other people's `torch` pipelines as one transform among many, like a `GaussianBlur` step in a `Compose([...])`.
44
44
 
45
- > **Status (2026-05-17):** v0.1.0 released; v0.2.0-track is on `main` (not yet tagged). Public API: `extract`, `Patchify`, `reconstruct`, `stitch`, `pair`, `resize`, `Cache`, plus geometry helpers (`num_patches`, `tilings`, `TilingSpec`, `scale_factor`, `paired_tilings`, `PairedTilingSpec`), pixel metrics (`patch_metrics`, `per_patch_mse`, `per_patch_psnr`), and `PatchPair`/`PatchMeta`.
45
+ > **Status:** v0.2.1 on PyPI, installable with `pip install patchcraft`. Public API (19 symbols): `extract`, `Patchify`, `reconstruct`, `stitch` (+ its `WeightKind`), `pair`, `resize`, `Cache`, plus geometry helpers (`num_patches`, `tilings`, `TilingSpec`, `scale_factor`, `paired_tilings`, `PairedTilingSpec`), pixel metrics (`patch_metrics`, `per_patch_mse`, `per_patch_psnr`), and `PatchPair`/`PatchMeta`.
46
46
 
47
47
  ## The lib vs. this repo
48
48
 
49
49
  Think of the lib as a **car** and this repo as the **car plus its test track**.
50
50
 
51
- - **The car** [`src/patchcraft/`](src/patchcraft/) is what gets installed by `pip install patchcraft`. It is a single library with one job: take one image (`Tensor[C, H, W]`), encode it into patches, decode patches back into the image, optionally pair LR/HR, resize, cache. **One image at a time, every time.** No datasets, no training, no orchestration, no batching across images. Multi-image is the caller's `for` loop, or `torch.vmap`, or a `DataLoader`.
52
- - **The track** [`tests/`](tests/), [`lab/`](lab/), [`tests/_datasets.py`](tests/_datasets.py), and the dev extras (`torchvision`, etc.) is the pit crew, telemetry, driver and stopwatch that **prove the car works** on real images (MNIST today; more later). It downloads datasets, drives the lib through varied geometries, measures correctness. It never ships in the wheel.
51
+ - **The car** is the [`patchcraft`](https://github.com/LeoPR/PatchCraft/tree/main/src/patchcraft) package, what gets installed by `pip install patchcraft`. It is a single library with one job: take one image (`Tensor[C, H, W]`), encode it into patches, decode patches back into the image, optionally pair LR/HR, resize, cache. **One image at a time, every time.** No datasets, no training, no orchestration, no batching across images. Multi-image is the caller's `for` loop, or `torch.vmap`, or a `DataLoader`.
52
+ - **The track** is `tests/`, `lab/`, `tests/_datasets.py` and the dev extras (`torchvision`, etc.) in the repo. It is the pit crew, telemetry, driver and stopwatch that **prove the car works** on real images. It downloads datasets, drives the lib through varied geometries, measures correctness. It never ships in the wheel. See [CONTRIBUTING.md](https://github.com/LeoPR/PatchCraft/blob/main/CONTRIBUTING.md) if you're contributing.
53
53
 
54
- The car is also **acoplável** designed to drop into someone else's pipeline:
54
+ The car is also **acoplável**, designed to drop into someone else's pipeline:
55
55
 
56
56
  ```python
57
57
  from patchcraft import Patchify
@@ -70,7 +70,7 @@ transform = transforms.Compose([
70
70
 
71
71
  The five core operations, one diagram each. Letters mark which patch each cell came from / goes to.
72
72
 
73
- ### `extract` image → patch stack
73
+ ### `extract`: image → patch stack
74
74
 
75
75
  `patch_size=4`, `stride=4` (no overlap) on an 8×8 image:
76
76
 
@@ -89,7 +89,7 @@ The five core operations, one diagram each. Letters mark which patch each cell c
89
89
  +-----------------+
90
90
  ```
91
91
 
92
- ### `reconstruct` patch stack → image (bit-exact when `stride == patch_size`)
92
+ ### `reconstruct`: patch stack → image (bit-exact when `stride == patch_size`)
93
93
 
94
94
  Each output pixel = sum of patch contributions / `count` map (= how many patches covered it). When `stride == patch_size`, `count` is all-ones and the divide is a no-op.
95
95
 
@@ -105,7 +105,7 @@ Each output pixel = sum of patch contributions / `count` map (= how many patches
105
105
  count: 1 1 2 2 2 2 1 1 <- divide sum by this
106
106
  ```
107
107
 
108
- ### `pair` LR <-> HR, same image region, different resolution
108
+ ### `pair`: LR <-> HR, same image region, different resolution
109
109
 
110
110
  `scale_factor=2`: every k-th LR patch corresponds to the k-th HR patch; HR coords are LR coords times the integer scale.
111
111
 
@@ -125,21 +125,22 @@ Each output pixel = sum of patch contributions / `count` map (= how many patches
125
125
  LR patch at (row=1, col=1) <--> HR patch at (row=2, col=2)
126
126
  ```
127
127
 
128
- ### `stitch` same fold geometry as `reconstruct`, but each patch weighted by a window kernel
128
+ ### `stitch`: same fold geometry as `reconstruct`, with each patch weighted by a window kernel
129
129
 
130
130
  Use when patches were modified by a model and uniform averaging shows boundary seams. Window kernels for `patch_size=4`:
131
131
 
132
132
  ```
133
133
  weight="uniform" weight="hann" weight="gaussian"
134
- (== reconstruct) centers > edges centers >> edges (never 0)
134
+ (== reconstruct) centers > edges centers >> edges
135
+ (never 0) (never 0)
135
136
 
136
- + + + + . . . . . o o .
137
- + + + + . X X . o X X o
138
- + + + + . X X . o X X o
139
- + + + + . . . . . o o .
137
+ + + + + . X X . . o o .
138
+ + + + + X X X X o X X o
139
+ + + + + X X X X o X X o
140
+ + + + + . X X . . o o .
140
141
 
141
142
  no seam attenuation strong attenuation, smooth attenuation,
142
- image corners -> 0 corners preserved
143
+ corners preserved corners preserved
143
144
  ```
144
145
 
145
146
  ### Everything stays one-image-at-a-time
@@ -151,25 +152,25 @@ Use when patches were modified by a model and uniform averaging shows boundary s
151
152
  out = stitch(result, ...) # PatchCraft primitive
152
153
  ```
153
154
 
154
- Multi-image parallelism is the caller's pipeline (`torch.vmap`, `DataLoader` workers, etc.) see [`SCOPE.md`](docs/SCOPE.md) §2.
155
+ Multi-image parallelism is the caller's pipeline (`torch.vmap`, `DataLoader` workers, etc.). See [SCOPE.md](https://github.com/LeoPR/PatchCraft/blob/main/docs/SCOPE.md) §2.
155
156
 
156
157
  ## Scope (what the car does)
157
158
 
158
159
  - **Extract** patches from a single image with configurable size, stride and dilation (`extract`, `Patchify`).
159
- - **Reconstruct** an image from its patches exact and weighted-overlap (`reconstruct`).
160
+ - **Reconstruct** an image from its patches, exact and weighted-overlap (`reconstruct`).
160
161
  - **Stitch** *modified* patches (model output, denoised, super-resolved) back into one image with a window kernel that attenuates boundary seams (`stitch`, with `weight="uniform"|"hann"|"gaussian"`).
161
- - **Plan** the geometry ahead of time: `num_patches((H, W), ...)` for the count, `tilings((H, W), allow_overlap=...)` for every full-coverage `(patch_size, stride)` combo (no image, no allocation just arithmetic). For LR↔HR setups: `scale_factor(...)` and `paired_tilings(...)`.
162
+ - **Plan** the geometry ahead of time: `num_patches((H, W), ...)` for the count, `tilings((H, W), allow_overlap=...)` for every full-coverage `(patch_size, stride)` combo (no image, no allocation, just arithmetic). For LR↔HR setups: `scale_factor(...)` and `paired_tilings(...)`.
162
163
  - **Pair** LR and HR patches with metadata sufficient to reconstruct either (`pair`, `PatchPair`, `PatchMeta`).
163
164
  - **Measure** pixel-level error between two patch stacks: `patch_metrics`, `per_patch_mse`, `per_patch_psnr`.
164
- - **Resize** with pluggable backends PIL or torch (`resize`).
165
+ - **Resize** with pluggable backends, either PIL or torch (`resize`).
165
166
  - **Cache** results on disk with content-addressed keys, OneDrive-race retry, optional zstd (`Cache`).
166
167
 
167
168
  ## Scope (what the car does NOT do)
168
169
 
169
- - **Not a dataset manager.** PatchCraft does not load, download, batch, shuffle, or stream datasets. That's the track's job `tests/_datasets.py` has `mnist_subset(...)` for dev fixtures, and `torchvision` is in the `[dev]` extra (never a runtime dep of the car).
170
+ - **Not a dataset manager.** PatchCraft does not load, download, batch, shuffle, or stream datasets. That's the track's job: `tests/_datasets.py` has `mnist_subset(...)` for dev fixtures, and `torchvision` is in the `[dev]` extra (never a runtime dep of the car).
170
171
  - **Not a multi-image API.** Every primitive takes one image. Use `vmap` or a Python loop if you need to apply it to many.
171
- - No SVMs, no kernels, no quantum circuits those belong to other projects.
172
- - No neural network training PatchCraft is infrastructure, not a model.
172
+ - No SVMs, no kernels, no quantum circuits: those belong to other projects.
173
+ - No neural network training. PatchCraft is infrastructure, not a model.
173
174
 
174
175
  ## Install
175
176
 
@@ -191,75 +192,16 @@ pip install -e ".[dev,cache]"
191
192
  For GPU support, install a matching torch wheel before PatchCraft
192
193
  (e.g. `pip install torch --index-url https://download.pytorch.org/whl/cu124`).
193
194
 
194
- ## Run tests
195
-
196
- ```
197
- pytest
198
- pytest -m "not gpu" # skip GPU-requiring tests
199
- ```
200
-
201
- ## Layout
202
-
203
- ```
204
- PatchCraft/
205
- ├── pyproject.toml package metadata, build backend (hatchling)
206
- ├── README.md this file
207
- ├── LICENSE MIT
208
- ├── .python-version 3.13
209
- ├── .gitignore ignores archive/, venvs, caches, outputs
210
- ├── src/patchcraft/ library core — one-image-at-a-time primitives
211
- │ ├── __init__.py re-exports the full public API
212
- │ ├── extract.py patches via F.unfold; Patchify wrapper (ADR 0002)
213
- │ ├── reconstruct.py inverse via F.fold + count map
214
- │ ├── geometry.py pre-flight: num_patches, tilings, TilingSpec
215
- │ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
216
- │ ├── resize.py resize with PIL or torch backends
217
- │ └── cache.py content-addressed disk cache
218
- ├── tests/ pytest suite (contract tests for src/)
219
- │ ├── test_extract.py extract + Patchify
220
- │ ├── test_reconstruct.py
221
- │ ├── test_geometry.py num_patches + tilings
222
- │ ├── test_pair.py
223
- │ ├── test_resize.py
224
- │ ├── test_cache.py
225
- │ ├── test_datasets_helper.py label_subset
226
- │ ├── test_import.py
227
- │ └── _datasets.py dev-only fixtures (MNIST, etc) — NOT public API
228
- ├── lab/ ephemeral experiments; see lab/README.md
229
- │ ├── README.md bench rules (tracked)
230
- │ └── .gitignore ignores everything else (tracked)
231
- ├── docs/
232
- │ ├── USAGE.md live REPL walkthrough of every public API
233
- │ ├── SCOPE.md responsibilities matrix + parallelization analysis
234
- │ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
235
- │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
236
- │ ├── ROADMAP.md milestone plan
237
- │ └── ADR/
238
- │ ├── 0001-patch-extraction-api.md pure function `extract`
239
- │ └── 0002-patchify-transform.md callable wrapper for Compose pipelines
240
- └── archive/ reference-only; gitignored (pruned 2026-05-17 — only HISTORY.md kept)
241
- ```
242
-
243
- ## Validation lab
244
-
245
- 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:
246
-
247
- - [`tests/`](tests/) — formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
248
- - [`lab/`](lab/) — 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).
249
-
250
- 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.
251
-
252
195
  ## Where to read next
253
196
 
254
197
  | If you want… | Open |
255
198
  |---|---|
256
- | A hands-on tour with real REPL outputs for every public API | [`docs/USAGE.md`](docs/USAGE.md) |
257
- | The line between "PatchCraft's job" and "your pipeline's job", plus the parallelization story | [`docs/SCOPE.md`](docs/SCOPE.md) |
258
- | The auxiliary test fixtures and lab conventions (not shipped) | [`docs/AUXILIARY.md`](docs/AUXILIARY.md) |
259
- | Design decisions, math, the per-API contract | [`docs/THEORY.md`](docs/THEORY.md) |
260
- | Architecture Decision Records | [`docs/ADR/`](docs/ADR/) |
261
- | Milestone plan | [`docs/ROADMAP.md`](docs/ROADMAP.md) |
262
- | Per-release changes | [`CHANGELOG.md`](CHANGELOG.md) |
199
+ | A hands-on tour with real REPL outputs for every public API | [USAGE.md](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md) |
200
+ | The line between "PatchCraft's job" and "your pipeline's job", plus the parallelization story | [SCOPE.md](https://github.com/LeoPR/PatchCraft/blob/main/docs/SCOPE.md) |
201
+ | Design decisions, math, the per-API contract | [THEORY.md](https://github.com/LeoPR/PatchCraft/blob/main/docs/THEORY.md) |
202
+ | Architecture Decision Records | [ADR/](https://github.com/LeoPR/PatchCraft/tree/main/docs/ADR) |
203
+ | Per-release changes | [CHANGELOG.md](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) |
204
+ | Cloning and contributing (run tests, layout, validation conventions) | [CONTRIBUTING.md](https://github.com/LeoPR/PatchCraft/blob/main/CONTRIBUTING.md) |
263
205
 
264
206
  ## Author
265
207