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.
- {patchcraft-0.2.0 → patchcraft-0.2.1}/.github/workflows/release.yml +19 -2
- {patchcraft-0.2.0 → patchcraft-0.2.1}/.github/workflows/test.yml +10 -2
- patchcraft-0.2.1/CHANGELOG.md +271 -0
- patchcraft-0.2.1/CONTRIBUTING.md +98 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/PKG-INFO +30 -88
- patchcraft-0.2.1/README.md +168 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/ADR/0001-patch-extraction-api.md +6 -6
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/ADR/0002-patchify-transform.md +9 -9
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/AUXILIARY.md +24 -22
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/ROADMAP.md +40 -23
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/SCOPE.md +51 -40
- patchcraft-0.2.1/docs/STUDIES/2026-08-04-patch-techniques.md +165 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/THEORY.md +82 -73
- {patchcraft-0.2.0 → patchcraft-0.2.1}/docs/USAGE.md +48 -45
- {patchcraft-0.2.0 → patchcraft-0.2.1}/pyproject.toml +11 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/__init__.py +4 -3
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/cache.py +247 -247
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/extract.py +116 -116
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/geometry.py +4 -4
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/metrics.py +13 -5
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/pair.py +2 -2
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/reconstruct.py +39 -9
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/resize.py +202 -189
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/stitch.py +52 -28
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/_datasets.py +1 -1
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_cache.py +2 -2
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_extract.py +1 -1
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_geometry.py +1 -1
- patchcraft-0.2.1/tests/test_import.py +14 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_metrics.py +45 -2
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_pair.py +1 -1
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_reconstruct.py +60 -3
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_resize.py +43 -1
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/test_stitch.py +78 -24
- patchcraft-0.2.1/uv.lock +1041 -0
- patchcraft-0.2.0/.vscode/settings.json +0 -3
- patchcraft-0.2.0/CHANGELOG.md +0 -175
- patchcraft-0.2.0/README.md +0 -226
- patchcraft-0.2.0/lab/.gitignore +0 -3
- patchcraft-0.2.0/lab/2026-05-16-roundtrip-mnist.py +0 -80
- patchcraft-0.2.0/lab/README.md +0 -34
- patchcraft-0.2.0/lab/usage_demo.out +0 -174
- patchcraft-0.2.0/lab/usage_demo.py +0 -239
- patchcraft-0.2.0/tests/test_import.py +0 -7
- patchcraft-0.2.0/uv.lock +0 -1010
- {patchcraft-0.2.0 → patchcraft-0.2.1}/.gitignore +0 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/.python-version +0 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/LICENSE +0 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/src/patchcraft/py.typed +0 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/__init__.py +0 -0
- {patchcraft-0.2.0 → patchcraft-0.2.1}/tests/conftest.py +0 -0
- {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
|
|
34
|
-
- run: uv sync --extra cache --extra dev
|
|
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
|
|
37
|
+
run: uv python install
|
|
32
38
|
|
|
33
39
|
- name: Install project with dev + cache extras
|
|
34
|
-
|
|
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.
|
|
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
|
|
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
|
|
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**
|
|
52
|
-
- **The track**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
134
|
+
(== reconstruct) centers > edges centers >> edges
|
|
135
|
+
(never 0) (never 0)
|
|
135
136
|
|
|
136
|
-
+ + + + .
|
|
137
|
-
+ + + +
|
|
138
|
-
+ + + +
|
|
139
|
-
+ + + + .
|
|
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
|
-
|
|
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.)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
172
|
-
- No neural network training
|
|
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 | [
|
|
257
|
-
| The line between "PatchCraft's job" and "your pipeline's job", plus the parallelization story | [
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
|
|
|
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
|
|