patchcraft 0.5.0__tar.gz → 0.5.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 (82) hide show
  1. {patchcraft-0.5.0 → patchcraft-0.5.1}/.gitignore +4 -0
  2. {patchcraft-0.5.0 → patchcraft-0.5.1}/CHANGELOG.md +121 -3
  3. patchcraft-0.5.1/CONTRIBUTING.md +270 -0
  4. patchcraft-0.5.1/MANIFEST.in +37 -0
  5. {patchcraft-0.5.0 → patchcraft-0.5.1}/PKG-INFO +31 -30
  6. {patchcraft-0.5.0 → patchcraft-0.5.1}/README.md +14 -13
  7. {patchcraft-0.5.0 → patchcraft-0.5.1}/README.pt-BR.md +14 -13
  8. {patchcraft-0.5.0 → patchcraft-0.5.1}/README.pypi.md +15 -13
  9. patchcraft-0.5.1/accel/Cargo.lock +232 -0
  10. patchcraft-0.5.1/accel/Cargo.toml +32 -0
  11. patchcraft-0.5.1/accel/README.md +48 -0
  12. patchcraft-0.5.1/accel/src/kernel.rs +252 -0
  13. patchcraft-0.5.1/accel/src/lib.rs +88 -0
  14. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/AUXILIARY.md +11 -6
  15. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/GUIDE.md +34 -23
  16. patchcraft-0.5.1/docs/PERFORMANCE.md +116 -0
  17. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/ROADMAP.md +2 -1
  18. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/SCOPE.md +9 -6
  19. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/superpowers/plans/2026-09-01-fase3-g1-predicado.md +2 -2
  20. patchcraft-0.5.1/pyproject.toml +130 -0
  21. patchcraft-0.5.1/setup.cfg +4 -0
  22. patchcraft-0.5.1/setup.py +81 -0
  23. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/__init__.py +15 -1
  24. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/_accel.py +12 -11
  25. patchcraft-0.5.1/src/patchcraft/_version.py +24 -0
  26. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/reconstruct.py +1 -1
  27. patchcraft-0.5.1/src/patchcraft.egg-info/PKG-INFO +188 -0
  28. patchcraft-0.5.1/src/patchcraft.egg-info/SOURCES.txt +76 -0
  29. patchcraft-0.5.1/src/patchcraft.egg-info/dependency_links.txt +1 -0
  30. patchcraft-0.5.1/src/patchcraft.egg-info/requires.txt +13 -0
  31. patchcraft-0.5.1/src/patchcraft.egg-info/scm_file_list.json +77 -0
  32. patchcraft-0.5.1/src/patchcraft.egg-info/scm_version.json +8 -0
  33. patchcraft-0.5.1/src/patchcraft.egg-info/top_level.txt +1 -0
  34. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_accel.py +10 -10
  35. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_import.py +1 -1
  36. patchcraft-0.5.1/tools/benchmark.py +165 -0
  37. patchcraft-0.5.1/tools/check_dist.py +209 -0
  38. {patchcraft-0.5.0 → patchcraft-0.5.1}/uv.lock +26 -10
  39. patchcraft-0.5.0/.github/workflows/release.yml +0 -203
  40. patchcraft-0.5.0/.github/workflows/test.yml +0 -93
  41. patchcraft-0.5.0/CONTRIBUTING.md +0 -161
  42. patchcraft-0.5.0/pyproject.toml +0 -112
  43. {patchcraft-0.5.0 → patchcraft-0.5.1}/.python-version +0 -0
  44. {patchcraft-0.5.0 → patchcraft-0.5.1}/LICENSE +0 -0
  45. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/ADR/0001-patch-extraction-api.md +0 -0
  46. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/ADR/0002-patchify-transform.md +0 -0
  47. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/ADR/0003-reversibility-classes.md +0 -0
  48. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/FOCO-1.0.md +0 -0
  49. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
  50. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/THEORY.md +0 -0
  51. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/USAGE.md +0 -0
  52. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/superpowers/plans/2026-09-01-fase1-bugs-performance.md +0 -0
  53. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/superpowers/plans/2026-09-01-fase2-acel.md +0 -0
  54. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/superpowers/specs/2026-09-01-fase1-bugs-performance-design.md +0 -0
  55. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/superpowers/specs/2026-09-01-fase2-acel-design.md +0 -0
  56. {patchcraft-0.5.0 → patchcraft-0.5.1}/docs/superpowers/specs/2026-09-01-fase3-g1-predicado-design.md +0 -0
  57. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/_foldgeom.py +0 -0
  58. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/cache.py +0 -0
  59. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/extract.py +0 -0
  60. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/geometry.py +0 -0
  61. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/metrics.py +0 -0
  62. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/pair.py +0 -0
  63. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/py.typed +0 -0
  64. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/resize.py +0 -0
  65. {patchcraft-0.5.0 → patchcraft-0.5.1}/src/patchcraft/stitch.py +0 -0
  66. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/__init__.py +0 -0
  67. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/_datasets.py +0 -0
  68. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/_rng.py +0 -0
  69. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/conftest.py +0 -0
  70. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_cache.py +0 -0
  71. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_datasets_helper.py +0 -0
  72. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_exactness.py +0 -0
  73. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_extract.py +0 -0
  74. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_geometry.py +0 -0
  75. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_metrics.py +0 -0
  76. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_pair.py +0 -0
  77. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_public_api.py +0 -0
  78. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_reconstruct.py +0 -0
  79. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_reference.py +0 -0
  80. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_resize.py +0 -0
  81. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_rng.py +0 -0
  82. {patchcraft-0.5.0 → patchcraft-0.5.1}/tests/test_stitch.py +0 -0
@@ -3,6 +3,10 @@ __pycache__/
3
3
  *.py[cod]
4
4
  *$py.class
5
5
  *.so
6
+ # Built in place by an editable install (setuptools-rust).
7
+ src/patchcraft/_accel_native*
8
+ # Generated at build time by setuptools-scm.
9
+ src/patchcraft/_version.py
6
10
 
7
11
  # Distribution / packaging
8
12
  build/
@@ -6,6 +6,121 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] - 2026-09-02
10
+
11
+ > Numbered `0.6.0` while it was being built. Corrected before release: the
12
+ > middle digit is the compatibility boundary, and this release changes no
13
+ > signature, no name and no output value. See the versioning policy and its
14
+ > errata in [`CONTRIBUTING.md`](CONTRIBUTING.md).
15
+
16
+ ### Changed
17
+
18
+ - **The native accelerator ships inside this wheel.** It was going to be a
19
+ second PyPI project, `patchcraft-accel`, reached through
20
+ `pip install patchcraft[accel]`. That project was never published and now
21
+ never will be. A release produces one sdist and six wheels for the single
22
+ `patchcraft` project: five tagged `cp312-abi3-<platform>` with the Rust
23
+ kernel compiled in as `patchcraft._accel_native`, and one `py3-none-any`
24
+ for every other platform. Installers prefer the most specific compatible
25
+ tag, so `pip install patchcraft` picks up the accelerator where one exists
26
+ and the pure wheel everywhere else, with no extra to remember. The pattern
27
+ and its universal fallback follow `coverage`, which publishes the same two
28
+ shapes for one version.
29
+ - The `accel` extra is removed. Nothing depended on it, because it never
30
+ reached the index.
31
+ - The accelerator is now on by default where a wheel carries it, rather than
32
+ opt-in. The two paths are bit-identical by test, and `PATCHCRAFT_ACCEL=0`
33
+ still forces the pure one at runtime.
34
+ - Build backend moves from hatchling to setuptools with setuptools-rust, which
35
+ is what makes one project able to emit both shapes of wheel. `setup.py`
36
+ holds the single decision, driven by `PATCHCRAFT_PURE_PYTHON` and
37
+ `PATCHCRAFT_REQUIRE_EXTENSION`; everything else stays declarative in
38
+ `pyproject.toml`. Installing from the sdist compiles the extension when a
39
+ Rust toolchain is present and degrades to a pure install when it is not.
40
+ - License metadata follows PEP 639: the SPDX expression `MIT` with
41
+ `license-files`, and the superseded `License :: OSI Approved` classifier is
42
+ gone. setuptools rejects carrying both.
43
+
44
+ ### Added
45
+
46
+ - **The version comes from the git tag.** `setuptools-scm` derives it at build
47
+ time, so no file in the source states a version, the release procedure loses
48
+ its bump step, and a tag and a release can no longer disagree.
49
+ `patchcraft.__version__` still works: the build writes a generated
50
+ `_version.py` that `__init__.py` imports, which costs nothing at import time,
51
+ with `importlib.metadata` as a fallback for a tree that was never built.
52
+ Measured before choosing: `importlib.metadata` alone was the wrong default,
53
+ because it took 56 ms on first call and reported a stale version in an
54
+ editable install.
55
+ - `docs/PERFORMANCE.md`, which is where the accelerator's measurements live.
56
+ It carries the test machine, the versions, the date, the reproduction
57
+ command and what the numbers do not say, and its table is pasted from
58
+ `tools/benchmark.py --markdown` rather than retyped.
59
+ - The Rust kernel is now always built in release mode, including in an
60
+ editable install. It was following the build command, so a development
61
+ checkout compiled a debug kernel: measured here, that turned a 14x speedup
62
+ into 2.1x, which made local benchmarks meaningless and made the accelerated
63
+ CI job exercise code no user runs.
64
+ - `patchcraft.__version__` is re-exported explicitly, so `mypy --strict` in a
65
+ consuming project still accepts it. Moving it out of a literal and into an
66
+ import had quietly made it an implicit re-export, which strict mode rejects.
67
+ - `tools/benchmark.py` measures the accelerated fold against the pure one and
68
+ refuses to report a timing unless the two results are bit-identical. The
69
+ numbers in the documentation come from it, and it is the reproduction
70
+ command they cite.
71
+ - Every build checkout uses `fetch-depth: 0`. The default shallow clone hides
72
+ the tags, and setuptools-scm's fallback for that is a silent
73
+ `0.1.dev1+g<sha>`, a version that walks backwards rather than an error.
74
+ - `tools/check_dist.py` grew `--expect-version` and `--require-universal`, and
75
+ it now rejects any artifact carrying a local version segment. PyPI refuses
76
+ those on upload, so this turns a late and confusing server-side rejection
77
+ into an early local one. It also fixes a bug in the job that checks the
78
+ native wheels, which demanded a universal wheel that job never has.
79
+ - Linux aarch64 wheels, which the previous four-target matrix did not cover.
80
+ - `tools/check_dist.py`, run before every upload. It fails the release if a
81
+ platform wheel lost its extension, which `optional=True` would otherwise
82
+ let through as a silently slow wheel, or if the universal wheel gained one.
83
+ - The versioning policy now names the resolver rule it follows, which is that
84
+ `0.y` is the compatibility boundary in Cargo, npm and PEP 440 alike, and it
85
+ carries an errata for the two releases numbered before the rule was written.
86
+ - Version numbers are gone from the documentation surface. The three cover
87
+ pages carry none at all, since the PyPI badge already shows the current
88
+ release, and the manual keeps only the one the citation entry needs. The
89
+ release archaeology those pages used to recite lives here, and the rest is
90
+ in the git history.
91
+ - `MANIFEST.in`, which puts `accel/` in the sdist so the source path can build
92
+ the extension, and keeps `lab/`, `.superpowers/` and cargo's `target/` out.
93
+
94
+ - **Corrected: the hand-rolled and library tile-and-blend results are not
95
+ bit-identical.** The three cover pages and GUIDE section 1 claimed they were,
96
+ and the GUIDE block asserted it. Re-measured: they agree to 1.17e-05 on a
97
+ value in [0, 1], differing on 24500 of 49152 elements. The cause is the 0.3.0
98
+ separable denominator, where `stitch` sums two 1-D window folds while the
99
+ hand-rolled twin folds a replicated 2-D kernel. `stitch(weight="uniform")`
100
+ is still bit-identical to `reconstruct`. Nothing about the round-trip
101
+ contract changed; the documentation was simply asserting more than the code
102
+ delivered.
103
+ - Documentation corrections found by auditing every page against the code:
104
+ the changelog's reference links did not define the two most recent versions;
105
+ `docs/AUXILIARY.md` listed wheel contents that predate three releases;
106
+ `docs/SCOPE.md` still promised "pure-Python single-threaded", which the
107
+ rayon-parallel kernel contradicts; `docs/ROADMAP.md` still named hatchling;
108
+ GUIDE said the library "is not faster", that the no-zstandard branch is
109
+ untested, and that CI is six cells; the cover pages pointed at `USAGE.md` as
110
+ a walkthrough of 20 symbols when it covers 18 and says so. `CONTRIBUTING.md`
111
+ told a contributor to run `pytest` after a `uv sync` that does not install it.
112
+
113
+ ### Notes
114
+
115
+ - No signature, no name and no output value changed. The public surface is the
116
+ same 20 names, still pinned by `tests/test_public_api.py`.
117
+ - Measured on torch 2.14, CPU, mean over the dev machine: `reconstruct` at
118
+ 3x512x512 p=32 s=16 goes 11.4 ms to 2.5 ms (4.5x), at 3x2048x2048 p=64 s=32
119
+ 477.9 ms to 31.8 ms (15.0x); `stitch[hann]` 21.6 ms to 7.0 ms (3.1x) and
120
+ 488.7 ms to 38.8 ms (12.6x). The gain is algorithmic rather than a thread
121
+ count: the pure `F.fold` measured between 365 and 465 ms on that largest
122
+ case at 4, 8, 16 and 36 torch threads.
123
+
9
124
  ## [0.5.0] - 2026-09-02
10
125
 
11
126
  ### Added
@@ -472,9 +587,12 @@ First public release. Public API stable; signatures will only change in 1.x.
472
587
  - [`README.md`](README.md) covers installation, the car-vs-track metaphor,
473
588
  validation lab.
474
589
 
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
590
+ [Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.5.1...HEAD
591
+ [0.5.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.1
592
+ [0.5.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.0
593
+
594
+ <!-- 0.4.0 and 0.3.0 are deliberately unlinked: they are source milestones that
595
+ were never tagged and never released, as their own entries say. -->
478
596
  [0.2.2]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.2
479
597
  [0.2.1]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.1
480
598
  [0.2.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.0
@@ -0,0 +1,270 @@
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
+ The test runner and the linters live in the `dev` extra, so a plain `uv sync`
10
+ does not install them:
11
+
12
+ ```
13
+ uv sync --locked --extra cache --extra dev
14
+ ```
15
+
16
+ ```
17
+ pytest
18
+ pytest -m "not gpu" # skip GPU-requiring tests
19
+ ```
20
+
21
+ 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}` for the pure path, plus a second job on both operating systems for the accelerated one. To run the same checks locally before pushing:
22
+
23
+ ```
24
+ ruff check src tests
25
+ mypy --strict src
26
+ pytest -m "not gpu"
27
+ ```
28
+
29
+ ---
30
+
31
+ ## The two paths
32
+
33
+ The overlapping fold in `reconstruct` and `stitch` has a Rust implementation
34
+ in [`accel/`](accel/), compiled into the wheel on the platforms CI builds for.
35
+ Everything else runs the same operation in torch. The two are bit-identical by
36
+ test, so a contribution has to keep both green rather than pick one.
37
+
38
+ Which one your checkout runs depends on whether the extension got built:
39
+
40
+ ```
41
+ python -c "import patchcraft; print(patchcraft.accel_available())"
42
+ ```
43
+
44
+ `uv sync --extra cache --extra dev` builds it when a Rust toolchain is on
45
+ PATH, always in release mode. Two environment variables override that, and CI
46
+ sets both explicitly rather than relying on what happens to be installed:
47
+
48
+ | Variable | Effect |
49
+ |---|---|
50
+ | `PATCHCRAFT_PURE_PYTHON=1` | Skip the extension. How the universal wheel is built. |
51
+ | `PATCHCRAFT_REQUIRE_EXTENSION=1` | Make a Rust build failure fatal instead of degrading to the pure path. |
52
+ | `PATCHCRAFT_ACCEL=0` | Runtime only: ignore an extension that is already built. |
53
+
54
+ The Rust kernel has its own tests, which need no Python at all:
55
+
56
+ ```
57
+ cargo test --manifest-path accel/Cargo.toml
58
+ ```
59
+
60
+ One local wrinkle worth knowing. If your checkout sits under a path with
61
+ non-ASCII characters, `setuptools-rust` mis-decodes the artifact path that
62
+ cargo reports and the copy step fails. Point the build at an ASCII directory
63
+ and it works:
64
+
65
+ ```
66
+ CARGO_TARGET_DIR=/tmp/pc-target uv sync --extra cache --extra dev
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Layout
72
+
73
+ ```
74
+ PatchCraft/
75
+ ├── pyproject.toml package metadata, build backend, cibuildwheel targets
76
+ ├── setup.py the one build decision: with or without the Rust extension
77
+ ├── tools/check_dist.py gate: extension present, versions agree, tag respected
78
+ ├── tools/benchmark.py accelerated vs pure, and proves they agree first
79
+ ├── MANIFEST.in what the sdist carries
80
+ ├── README.md the call page, canonical, English
81
+ ├── README.pt-BR.md the same call page in Portuguese
82
+ ├── README.pypi.md the PyPI page (long_description), links absolute
83
+ ├── CONTRIBUTING.md this file
84
+ ├── CHANGELOG.md Keep-a-Changelog format
85
+ ├── LICENSE MIT
86
+ ├── .python-version 3.13
87
+ ├── .gitignore ignores archive/, venvs, caches, outputs
88
+ ├── .github/workflows/
89
+ │ ├── test.yml matrix CI on PRs/main, plus the accelerated job
90
+ │ └── release.yml publishes to PyPI on vX.Y.Z tag push (Trusted Publishing)
91
+ ├── src/patchcraft/ library core, one-image-at-a-time primitives
92
+ │ ├── __init__.py re-exports the full public API and the generated version
93
+ │ ├── extract.py patches via strided view or F.unfold; Patchify (ADR 0002)
94
+ │ ├── reconstruct.py inverse via F.fold + count map, closed-form fast path
95
+ │ ├── stitch.py weighted reassembly for modified patches
96
+ │ ├── geometry.py pre-flight: num_patches, tilings, scale_factor, paired_tilings
97
+ │ ├── metrics.py patch_metrics, per_patch_mse, per_patch_psnr
98
+ │ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
99
+ │ ├── resize.py resize with PIL or torch backends
100
+ │ ├── cache.py content-addressed disk cache
101
+ │ ├── _accel.py bridge to the optional native accelerator, falls back silently
102
+ │ └── _foldgeom.py shared fold-geometry validation for reconstruct and stitch
103
+ ├── accel/ the Rust crate compiled into this wheel (pyo3, setuptools-rust)
104
+ │ ├── Cargo.toml crate metadata; the version there is internal
105
+ │ ├── src/kernel.rs the gather-fold kernel, pure Rust, cargo-tested
106
+ │ ├── src/lib.rs pyo3 glue, exports patchcraft._accel_native
107
+ │ └── README.md how to build it, and how to check it is active
108
+ ├── tests/ pytest suite (contract tests for src/)
109
+ │ ├── test_extract.py extract + Patchify
110
+ │ ├── test_reconstruct.py
111
+ │ ├── test_stitch.py
112
+ │ ├── test_geometry.py num_patches + tilings + scale_factor + paired_tilings
113
+ │ ├── test_metrics.py
114
+ │ ├── test_pair.py
115
+ │ ├── test_resize.py
116
+ │ ├── test_cache.py
117
+ │ ├── test_exactness.py falsifies the count-map predicate over the legal space
118
+ │ ├── test_reference.py naive loop-based reference for the fast paths
119
+ │ ├── test_public_api.py freezes the 20 names, their signatures and carrier fields
120
+ │ ├── test_accel.py native path, skipped unless the extension was built
121
+ │ ├── test_datasets_helper.py label_subset
122
+ │ ├── test_import.py
123
+ │ ├── _rng.py audited round-trip helpers (data generation, bit equality)
124
+ │ ├── test_rng.py tests for those helpers
125
+ │ └── _datasets.py dev-only fixtures (MNIST, etc), NOT public API
126
+ ├── lab/ ephemeral experiments; see lab/README.md
127
+ │ ├── README.md bench rules (tracked; the scripts themselves are not)
128
+ │ └── .gitignore ignores everything else (tracked)
129
+ ├── docs/
130
+ │ ├── GUIDE.md the manual, every README claim measured with its output
131
+ │ ├── PERFORMANCE.md what the accelerator is worth, and how to re-measure
132
+ │ ├── USAGE.md REPL walkthrough of every public symbol (behind, see B4)
133
+ │ ├── SCOPE.md responsibilities matrix + parallelization analysis
134
+ │ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
135
+ │ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
136
+ │ ├── ROADMAP.md milestone plan
137
+ │ ├── FOCO-1.0.md what 1.0 freezes and the blockers in the way
138
+ │ ├── ADR/ one file per decision that shaped the API
139
+ │ ├── STUDIES/ background reading behind those decisions
140
+ │ └── superpowers/ the design specs and implementation plans per work phase
141
+ └── archive/ reference-only; gitignored (pruned 2026-05-17, only HISTORY.md kept)
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Validation lab
147
+
148
+ 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:
149
+
150
+ - [`tests/`](tests/) holds the formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
151
+ - [`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).
152
+
153
+ 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.
154
+
155
+ The dev framework that makes this all work (fixtures, dataset helpers, `Z:\` conventions) is documented in [`docs/AUXILIARY.md`](docs/AUXILIARY.md).
156
+
157
+ ---
158
+
159
+ ## Versioning policy
160
+
161
+ The scheme is [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html),
162
+ expressed in the [PEP 440](https://peps.python.org/pep-0440/) form that PyPI
163
+ accepts. Two rules matter more than the arithmetic, because both were broken
164
+ once and both cost work to undo.
165
+
166
+ **The number names a published artifact, not a unit of work.** Finishing a
167
+ phase, merging a branch and landing a refactor are not releases, and none of
168
+ them touches `__version__`. Everything between releases accumulates under
169
+ `[Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md), and the number is assigned
170
+ in the release commit itself. Versions 0.3.0 and 0.4.0 came from the opposite
171
+ habit: each internal work phase minted a minor, so three numbers existed
172
+ before any of them reached PyPI.
173
+
174
+ **A published version is frozen.** PyPI refuses a filename it has already
175
+ seen, so a version that shipped can only be superseded, never edited. That is
176
+ also why a failed pipeline never costs a number: `skip-existing` makes the
177
+ upload a no-op and the same tag can be re-run.
178
+
179
+ **While the project is pre-1.0, the middle digit is the compatibility
180
+ boundary.** SemVer itself leaves `0.y.z` open, saying only that "anything MAY
181
+ change at any time", so the meaning comes from the resolvers instead, and they
182
+ agree: Cargo expands `^0.2.3` to `>=0.2.3, <0.3.0`, npm does the same, and
183
+ PEP 440's `~=0.5.0` means `>=0.5.0, <0.6.0`. In every one of them a `0.y` bump
184
+ is the announcement that something may break, and a `0.y.z` bump is the
185
+ promise that nothing does.
186
+
187
+ That fixes what each digit is for here:
188
+
189
+ | Change | Bump | Why |
190
+ |---|---|---|
191
+ | An output value, a signature, a name or documented behaviour changes | `0.Y.0` | Resolvers stop at the y boundary, so this is where a break belongs |
192
+ | New functionality that breaks nothing, or a fix | `0.y.Z` | Anyone pinned to the y-series should get it |
193
+ | Documentation, tests, CI, packaging or tooling only | `0.y.Z` | Still a release if it is published, still no break |
194
+ | Nothing published | nothing | Accumulate under `[Unreleased]` |
195
+
196
+ After 1.0 the ordinary reading applies: breaking is major, additive is minor,
197
+ fixed is patch. What 1.0 freezes is written in
198
+ [`docs/FOCO-1.0.md`](docs/FOCO-1.0.md).
199
+
200
+ ### Errata
201
+
202
+ The rule above was written after the fact, and two releases predate it.
203
+
204
+ `0.4.0` should have been `0.3.1`. It added `accel_available()` and nothing
205
+ else: no signature changed, and the accelerated path was bit-exact against the
206
+ pure one. Additive and non-breaking is a z-bump under the boundary rule, and
207
+ minting a y announced a break that never happened.
208
+
209
+ `0.5.1` was briefly numbered `0.6.0` before release. It changes only how the
210
+ package is built and shipped, so it is a z-bump.
211
+
212
+ Two others were right and are worth recording as the shape to copy. `0.3.0`
213
+ moved `stitch` output by ULPs through a different summation order, which is an
214
+ output change and belongs at a y boundary. `0.5.0` made `tilings` return 73
215
+ specs where it returned 100, which is squarely a break.
216
+
217
+ The accelerator in [`accel/`](accel/) has no version of its own to manage. It
218
+ is compiled into this wheel, so it ships when `patchcraft` ships. The version
219
+ in `accel/Cargo.toml` is internal and nothing reads it; what the Python side
220
+ actually checks is `_ABI_VERSION` in `accel/src/lib.rs`, and a mismatch there
221
+ means a silent fall back to the pure path rather than a crash.
222
+
223
+ ---
224
+
225
+ ## Where the version comes from
226
+
227
+ The git tag, and nowhere else. `setuptools-scm` derives it at build time, so
228
+ no file in the source states a version and there is no literal to forget.
229
+ `v0.5.1` produces `0.5.1`; the leading `v` is stripped for you.
230
+
231
+ At build time setuptools-scm writes `src/patchcraft/_version.py`, which
232
+ `__init__.py` imports and re-exports as `patchcraft.__version__`. That file is
233
+ generated and gitignored, so a checkout that was never built falls back to
234
+ `importlib.metadata` and then to `0+unknown`. The fallback exists so an
235
+ un-built tree still imports; it is not a supported way to learn the version.
236
+
237
+ Anything other than a clean tree sitting exactly on a tag produces a version
238
+ with a local segment, like `0.5.2.dev0+gb4edfd81.d20260902` from a dirty tree
239
+ or `0.1.dev1+gb4edfd81` from a checkout whose tags were never fetched. PyPI
240
+ refuses local segments outright, so such a build can never be published by
241
+ accident, and `tools/check_dist.py` rejects it before the upload rather than
242
+ letting the upload fail.
243
+
244
+ That last point is why every `actions/checkout` that builds uses
245
+ `fetch-depth: 0`. The default shallow clone hides the tags, and the failure is
246
+ silent: a version that walks backwards rather than an error.
247
+
248
+ ---
249
+
250
+ ## Releasing (maintainer only)
251
+
252
+ 1. Close the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md) as `[X.Y.Z] YYYY-MM-DD`.
253
+ 2. Update the `version` field of the BibTeX entry in [`docs/GUIDE.md`](docs/GUIDE.md) section 9. It is the one version string left in the documentation, because a citation needs a concrete one.
254
+ 3. Update [`docs/ROADMAP.md`](docs/ROADMAP.md) milestone checkboxes.
255
+ 4. Commit: `release: vX.Y.Z`.
256
+ 5. Tag + push: `git tag -a vX.Y.Z -m "..."` then `git push origin vX.Y.Z`.
257
+ 6. `release.yml` fires automatically: validates, builds, publishes to PyPI via Trusted Publishing, then creates the GitHub Release with every artifact attached.
258
+
259
+ There is no version to bump in step 4, and no way for a tag and a release to
260
+ disagree. The `validate` job still cross-checks that setuptools-scm derived
261
+ exactly what the tag says, which catches a dirty or shallow checkout before
262
+ anything is built.
263
+
264
+ One release produces one sdist and six wheels: five `cp312-abi3-<platform>`
265
+ wheels with the Rust extension inside, and one `py3-none-any` wheel for
266
+ everywhere else. `tools/check_dist.py` runs before the upload and fails the
267
+ release if a platform wheel lost its extension, if the universal wheel gained
268
+ one, if the artifacts disagree about the version, or if any of them is not the
269
+ version the tag names. Publishing is a single project, so there is nothing to
270
+ register on PyPI beyond the publisher that already exists.
@@ -0,0 +1,37 @@
1
+ # The sdist is the reproducible source snapshot: the product (`src/`, `accel/`)
2
+ # plus the track that proves it works (`tests/`, `docs/`). `accel/` has to be
3
+ # here, because an sdist build compiles it when a Rust toolchain is present.
4
+ #
5
+ # The `prune` lines below are load-bearing rather than decorative, and the
6
+ # reason changed with setuptools-scm. Its file finder offers every git-tracked
7
+ # file to the sdist, so this file stopped being an allowlist and became a
8
+ # blocklist over everything git knows about. Measured: with no prune for it,
9
+ # `.vscode/settings.json` reached the sdist.
10
+ #
11
+ # Note that `lab/` is only *mostly* ignored: `lab/README.md` and
12
+ # `lab/.gitignore` are tracked, so the finder offers them and the prune is what
13
+ # stops them. The rest of `lab/` is scratch work that no commit can reproduce,
14
+ # and patchcraft-0.2.0.tar.gz shipped three such files to PyPI.
15
+ #
16
+ # Adding a tracked directory to this repository ships it to PyPI by default.
17
+ # `tools/check_dist.py` holds the matching allowlist and will fail the release
18
+ # for anything unexpected, which is the reminder to come back here.
19
+
20
+ include LICENSE
21
+ include CHANGELOG.md CONTRIBUTING.md
22
+ include README.md README.pt-BR.md README.pypi.md
23
+ include pyproject.toml setup.py uv.lock .python-version
24
+
25
+ recursive-include src *.py
26
+ include src/patchcraft/py.typed
27
+ recursive-include tests *.py
28
+ recursive-include docs *.md
29
+ recursive-include accel *.rs *.toml *.lock *.md
30
+ prune accel/target
31
+ recursive-include tools *.py
32
+
33
+ prune lab
34
+ prune archive
35
+ prune .superpowers
36
+ prune .github
37
+ prune .vscode
@@ -1,45 +1,44 @@
1
- Metadata-Version: 2.5
1
+ Metadata-Version: 2.4
2
2
  Name: patchcraft
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Image patch extraction, reconstruction, pairing and seam-aware stitching for super-resolution and dataset pipelines.
5
+ Author: Leonardo Marques de Souza
6
+ License-Expression: MIT
5
7
  Project-URL: Homepage, https://github.com/LeoPR/PatchCraft
6
8
  Project-URL: Repository, https://github.com/LeoPR/PatchCraft
7
9
  Project-URL: Issues, https://github.com/LeoPR/PatchCraft/issues
8
10
  Project-URL: Changelog, https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md
9
11
  Project-URL: Documentation, https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md
10
- Author: Leonardo Marques de Souza
11
- License-Expression: MIT
12
- License-File: LICENSE
13
- Keywords: dataset,image,patches,super-resolution,torch
12
+ Keywords: image,patches,super-resolution,dataset,torch
14
13
  Classifier: Development Status :: 4 - Beta
15
14
  Classifier: Intended Audience :: Developers
16
15
  Classifier: Intended Audience :: Science/Research
17
- Classifier: License :: OSI Approved :: MIT License
18
16
  Classifier: Operating System :: OS Independent
19
17
  Classifier: Programming Language :: Python :: 3
20
18
  Classifier: Programming Language :: Python :: 3 :: Only
21
19
  Classifier: Programming Language :: Python :: 3.12
22
20
  Classifier: Programming Language :: Python :: 3.13
23
21
  Classifier: Programming Language :: Python :: 3.14
24
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Programming Language :: Rust
25
23
  Classifier: Topic :: Scientific/Engineering :: Image Processing
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
26
  Classifier: Typing :: Typed
28
27
  Requires-Python: >=3.12
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
29
30
  Requires-Dist: numpy>=1.26
30
- Requires-Dist: pillow>=10
31
31
  Requires-Dist: torch>=2.6
32
- Provides-Extra: accel
33
- Requires-Dist: patchcraft-accel>=0.1.0; extra == 'accel'
32
+ Requires-Dist: pillow>=10
34
33
  Provides-Extra: cache
35
- Requires-Dist: zstandard>=0.22; extra == 'cache'
34
+ Requires-Dist: zstandard>=0.22; extra == "cache"
36
35
  Provides-Extra: dev
37
- Requires-Dist: mypy>=1.10; extra == 'dev'
38
- Requires-Dist: pytest-cov; extra == 'dev'
39
- Requires-Dist: pytest>=8; extra == 'dev'
40
- Requires-Dist: ruff>=0.5; extra == 'dev'
41
- Requires-Dist: torchvision>=0.20; extra == 'dev'
42
- Description-Content-Type: text/markdown
36
+ Requires-Dist: pytest>=8; extra == "dev"
37
+ Requires-Dist: pytest-cov; extra == "dev"
38
+ Requires-Dist: ruff>=0.5; extra == "dev"
39
+ Requires-Dist: mypy>=1.10; extra == "dev"
40
+ Requires-Dist: torchvision>=0.20; extra == "dev"
41
+ Dynamic: license-file
43
42
 
44
43
  # PatchCraft
45
44
 
@@ -81,16 +80,17 @@ pip install patchcraft
81
80
  pip install "patchcraft[cache]" # adds zstandard, which compresses Cache payloads
82
81
  ```
83
82
 
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
- ```
83
+ There is nothing else to install for speed. On Windows x64, Linux x86_64 and
84
+ aarch64, and both macOS architectures, the wheel carries a Rust accelerator for
85
+ the overlapping fold, which is where `reconstruct` and `stitch` spend their
86
+ time. Every other platform gets the universal wheel and runs the pure-torch
87
+ paths, which return the same values.
91
88
 
92
- `patchcraft.accel_available()` reports at runtime whether the accelerator is
93
- active; `PATCHCRAFT_ACCEL=0` in the environment forces the pure path.
89
+ `patchcraft.accel_available()` reports at runtime which one you got, and
90
+ `PATCHCRAFT_ACCEL=0` in the environment forces the pure path. On the overlapping
91
+ fold it is worth between 2.6x and 14x on the machine it was measured on, which
92
+ the [performance page](https://github.com/LeoPR/PatchCraft/blob/main/docs/PERFORMANCE.md)
93
+ reports in full.
94
94
 
95
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.
96
96
 
@@ -144,7 +144,7 @@ assert scrambled.shape == patches.shape # the right shape
144
144
  assert not torch.equal(scrambled, patches) # and the wrong pixels
145
145
  ```
146
146
 
147
- The saving is real on the other side too. Tiling an image, running a per-patch model and blending the result back with a Hann window took 17 non-blank lines by hand against 3 with `extract` and `stitch`, and the two outputs were bit-identical.
147
+ The saving is real on the other side too. Tiling an image, running a per-patch model and blending the result back with a Hann window took 17 non-blank lines by hand against 3 with `extract` and `stitch`, and the two outputs agreed to about 1.2e-05 on a value in [0, 1].
148
148
 
149
149
  ## The geometry has to cover the image
150
150
 
@@ -170,14 +170,15 @@ The everyday shorthand is that `stride == patch_size` and `stride == patch_size
170
170
 
171
171
  ## Status
172
172
 
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.
173
+ This is pre-1.0, so both the output values and the API shape can still move. While the leading digit is zero the middle one is the compatibility boundary, which makes a new `0.y.z` safe to take and a new `0.y` the place where a change is allowed to land, and the [changelog](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) records each one 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.
174
174
 
175
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.
176
176
 
177
177
  ## Documentation
178
178
 
179
179
  - [Guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md), the manual, with every figure on this page shown as runnable code
180
- - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of each of the 20 public symbols
180
+ - [Performance](https://github.com/LeoPR/PatchCraft/blob/main/docs/PERFORMANCE.md), what the native accelerator is worth and how to re-measure it
181
+ - [Usage](https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md), a walkthrough of the public surface, captured against an older release
181
182
  - [Theory](https://github.com/LeoPR/PatchCraft/blob/main/docs/THEORY.md), the math and the per-function contract
182
183
  - [Scope](https://github.com/LeoPR/PatchCraft/blob/main/docs/SCOPE.md), the line between this library and your pipeline
183
184
  - [Repository](https://github.com/LeoPR/PatchCraft), [issues](https://github.com/LeoPR/PatchCraft/issues) and [contributing](https://github.com/LeoPR/PatchCraft/blob/main/CONTRIBUTING.md)
@@ -39,16 +39,16 @@ pip install patchcraft
39
39
  pip install "patchcraft[cache]" # adds zstandard, which compresses Cache payloads
40
40
  ```
41
41
 
42
- Or with the optional native accelerator (prebuilt wheels for Windows x64,
43
- Linux x86_64, macOS arm64 and macOS x86_64; the package stays pure-Python
44
- and fully functional without it):
42
+ There is nothing else to install for speed. On Windows x64, Linux x86_64 and
43
+ aarch64, and both macOS architectures, the wheel carries a Rust accelerator for
44
+ the overlapping fold, which is where `reconstruct` and `stitch` spend their
45
+ time. Every other platform gets the universal wheel and runs the pure-torch
46
+ paths, which return the same values.
45
47
 
46
- ```bash
47
- pip install patchcraft[accel]
48
- ```
49
-
50
- `patchcraft.accel_available()` reports at runtime whether the accelerator is
51
- active; `PATCHCRAFT_ACCEL=0` in the environment forces the pure path.
48
+ `patchcraft.accel_available()` reports at runtime which one you got, and
49
+ `PATCHCRAFT_ACCEL=0` in the environment forces the pure path. On the overlapping
50
+ fold it is worth between 2.6x and 14x here, measured in
51
+ [docs/PERFORMANCE.md](docs/PERFORMANCE.md).
52
52
 
53
53
  The distribution name and the import name are both `patchcraft`. The runtime dependencies are `torch>=2.6`, `numpy>=1.26` and `pillow>=10`, and the supported Python versions are in [the guide](docs/GUIDE.md#9-install-details-and-citation) together with the note you need before you install a GPU wheel.
54
54
 
@@ -77,7 +77,7 @@ That is the whole loop. You extract, you do your work per patch, and then you co
77
77
 
78
78
  The two functions answer two different questions, so choosing between them is the first decision you make.
79
79
 
80
- `reconstruct` is the inverse. It assumes the patches still hold the pixels `extract` gave you, it divides by the count map, and on a covering geometry it hands the image back unchanged.
80
+ `reconstruct` is the inverse. It assumes the patches still hold the pixels `extract` gave you, it divides by the count map, and on the geometries described further down it hands the image back unchanged.
81
81
 
82
82
  `stitch` is for patches a model rewrote. Neighbouring patches now disagree about the pixels they share, and uniform averaging leaves that disagreement visible as a grid of seams, so `stitch` weights each patch by a window that fades toward its border.
83
83
 
@@ -108,7 +108,7 @@ assert not torch.equal(scrambled, patches) # and the wrong pix
108
108
 
109
109
  The second one is a stride that does not cover the image. On a 128 by 128 image with `patch=32, stride=20` the grid stops at pixel 112, which leaves 3840 of the 16384 pixels at zero, and a hand-rolled `fold` returns that partly black image without complaining.
110
110
 
111
- Writing the tile-and-blend loop by hand costs 17 non-blank lines against 3 here, and the two results are bit-identical. [The guide](docs/GUIDE.md#1-why-not-unfold-and-fold-directly) runs both versions side by side.
111
+ Writing the tile-and-blend loop by hand costs 17 non-blank lines against 3 here, and the two results agree to about 1.2e-05 on a value in [0, 1], the two routes differing only in the order they sum a separable window. [The guide](docs/GUIDE.md#1-why-not-unfold-and-fold-directly) runs both versions side by side.
112
112
 
113
113
  ## The geometry has to cover the image
114
114
 
@@ -144,7 +144,7 @@ The round trip is exact when every value in the count map is a power of two, whi
144
144
 
145
145
  ## Status
146
146
 
147
- **0.5.0, pre-1.0.** Output values can still change in a minor release, and [CHANGELOG.md](CHANGELOG.md) records each change with the measurement behind it.
147
+ **Pre-1.0**, and the badge above is the current release. While the leading digit is zero the middle one is the compatibility boundary, so `0.y.z` is safe to take and a new `0.y` is where an output value or a signature is allowed to move. [CHANGELOG.md](CHANGELOG.md) records every such change with the measurement behind it.
148
148
 
149
149
  1571 tests collected and CI is green on {Ubuntu, Windows} x {Python 3.12, 3.13, 3.14}, with `ruff check` and `mypy --strict` in the same run. The package is typed and ships `py.typed`.
150
150
 
@@ -155,7 +155,8 @@ No external project has consumed it yet, and no CUDA path in this library has ev
155
155
  | If you want | Open |
156
156
  |---|---|
157
157
  | The manual: every argument above, measured, with its output | [docs/GUIDE.md](docs/GUIDE.md) |
158
- | A walkthrough of each of the 20 public symbols | [docs/USAGE.md](docs/USAGE.md) |
158
+ | What the native accelerator is worth, and how to re-measure it | [docs/PERFORMANCE.md](docs/PERFORMANCE.md) |
159
+ | A walkthrough of the public surface, captured against an older release | [docs/USAGE.md](docs/USAGE.md) |
159
160
  | The line between this library and your pipeline | [docs/SCOPE.md](docs/SCOPE.md) |
160
161
  | The math, the decisions and the per-function contract | [docs/THEORY.md](docs/THEORY.md) |
161
162
  | Why the API looks like this | [docs/ADR/](docs/ADR) |