diff --git a/README.md b/README.md index 561b1be..fc7cee0 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ NumPy 2.5.4 pins cover lengths 1, 2, 4, 8, 64, 256 and 1024. The tests state the ```text // Package.swift dependencies: [ - .package(url: "https://github.com/mac4DSTEM/ImagoKit.git", exact: "0.8.0"), + .package(url: "https://github.com/mac4DSTEM/ImagoKit.git", from: "0.8.0"), ], targets: [ .target(name: "MyApp", dependencies: [ @@ -196,7 +196,7 @@ reference within its own tolerance; `docs/parity.md` lists each function, its st - iOS 18 builds but has never been run on a device or simulator; macOS 15 is declared but built and tested on macOS 27 only. - A first call of a vDSP 1D FFT or DFT size can differ by one Float32 ulp. - Float32 tolerances are claims on the planted and pinned cases, not proofs for every input. -- Zoom and rotate refuse the `wrap` mode at spline orders 2 to 5; the masked registration returns the shift only, and a symmetric pair with exactly tied maxima is not reproducible (`docs/open-items.md`). +- Zoom and rotate refuse the `wrap` mode at spline orders 2 to 5; the masked registration returns the shift only, and a symmetric pair with exactly tied maxima is not reproducible (`docs/parity.md`, known limits). - No number is validated on a microscope; the HRTEM filters have never been compared with DigitalMicrograph. - Not yet checked on a raw (counts) HRTEM image; the available TIFFs are 8-bit renders. diff --git a/ROADMAP.md b/ROADMAP.md index 3fb779a..c8d4d19 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # Roadmap -ImagoKit is pre-1.0: a minor version may change the API, and its CHANGELOG entry says how to migrate. The detailed list of -gaps is [`docs/open-items.md`](docs/open-items.md); the function-by-function state is [`docs/parity.md`](docs/parity.md). +ImagoKit is pre-1.0: a minor version may change the API, and its CHANGELOG entry says how to migrate. Open work is in the GitHub +issues; the function-by-function state and the known limits are in [`docs/parity.md`](docs/parity.md). ## Next diff --git a/docs/open-items.md b/docs/open-items.md deleted file mode 100644 index c7e270f..0000000 --- a/docs/open-items.md +++ /dev/null @@ -1,39 +0,0 @@ -# Open items - -A plain list. The first entries are functions slower than their Python counterpart at the typical size (512², `docs/benchmarks.md`), -each with its cause as far as it is known (diagnose before changing a number). - -## Slower than Python at the typical size - -1. **`label` is 2.1x slower** (4.4 ms against 2.1 ms at 512²). Not diagnosed: `Segmentation.grow` calls two closures per pixel - (`isForeground`, `same`) and keeps Int arrays; profile before changing it. -2. **`phase_cross_correlation` with `upsample_factor=10` Float32 is 1.15x (128²) and 1.27x (512²)**; the integer-pixel case is now 0.6x. The FFTs - are no longer the cost: at 512² the upsampled case takes 4.9 ms against 2.2 ms for the whole integer case, and the difference is the Hermitian - completion of P plus the matrix-multiply DFT (`upsampledDFT`, scalar loops); not profiled stage by stage. What would refute it: a profile showing - another stage. Fix: the two small complex matrix products on Accelerate (`cblas`/`vDSP_mmul`). -3. **`fft2` Float64 on the vDSP lengths (2^n x {3,5,15}) is up to 1.12x**, e.g. 480² 1.53 ms against 1.37 (NumPy). A batch of 480 lines takes the same time on - `vDSP_DFT_zop` and on the mixed-radix plan (0.49 ms), so the remainder is the transposes around the vDSP calls; the mixed-radix plan needs none for the columns. - The kit keeps the vDSP path by design. - -## Other open work - -- **1.0 item, naming:** the new surface takes `rows`/`columns` (decided 2026-10-22: `DistanceTransform`, `Segmentation.watershed`/`markers`, `RollingBall`, `Denoise`, `GeometricPhaseAnalysis`, `ImageResampling`); `LabelImage`, `Micrograph` and the older morphology and segmentation API still say `width`/`height`. Unify at 1.0 (a break). Four error enums carry a `.shape` case (`MorphologyError`, `RestorationError`, `DenoiseError`, `GeometricPhaseAnalysisError`); `RestorationError` and `DenoiseError` have no `LocalizedError` text yet. - -- 0.8.0 gaps, none blocking: mixed radix has radices 2, 3, 5, 7 only (11 and 13, a Float32 plan and a generic odd-radix pass are open: Float32 500² runs Bluestein at 1.53 ms, 0.8x NumPy); 1D `rfft`/`irfft` take power-of-two lengths only; `hfft`/`ihfft` missing; - `distance_transform_edt` is 2D; `zoom`/`rotate` are 2D; the denoisers are 2D single-channel, float only, and Bregman refuses an axis of length 1; - GPA is Float64 CPU, two reflections, `validation: "none"` until one dataset with truth passes (the apps should show the amplitude maps). - -- 0.7.0 (precipitation) gaps, none blocking: the thresholds, binary morphology, `ObjectStatistics`, `regionProperties` and `arealDensity` are CPU - (Double or Float32 as the reference is) and not yet benchmarked against the Python (rows for `docs/benchmarks.md` are open); - `BinaryMorphology` refuses even structures and an all-false structure (SciPy shifts the origin and answers) and runs a per-pixel loop - (fine at the 512² of a micrograph, slow for a 4096² mask: a bit-packed or separable path is the fix if an app asks); `Thresholding.yen` - can differ from scikit-image on an image of more than 2^24 pixels whose last histogram bin is nearly empty (Float32 `P1` reaches 1 and - NumPy's `argmax` returns the first NaN); `regionProperties` computes every property per object (use `objectMoments` alone for just the moments); - (`watershed`, `rolling_ball` and the Euclidean distance transform arrived in 0.8.0.) - -- Metal Float32 paths: FFT, then the separable filters; measure the break-even at 2048² to 8192² in the nightly review. - (`ndimage.zoom` and `rotate` and the TV and non-local-means denoisers arrived in 0.8.0.) -- `zoom` and `rotate` with `wrap` at spline orders 2 to 5 (refused by name). The shifts measured the rule (prefilter and - taps as `mirror`, the coordinate mapped with period n − 1); the resampling coordinate path has not been checked against it. -- Masked registration: scikit-image's `xcorr == xcorr.max()` tie rule is reproducible only for a unique maximum (FFT length - differs from SciPy's fast length); symmetric pairs decide on rounding noise (the kit's fast length is the next 7-smooth length, SciPy's also allows 11). diff --git a/docs/parity.md b/docs/parity.md index c1d8d84..a5a90cd 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -190,3 +190,25 @@ Numerics are never held stricter than that; readers, I/O, indices and metadata a | Function or option group | State | Kit symbol | Counterpart tolerance (source) | Priority | Notes | |---|---|---|---|---|---| | GPA strain from two reflections of a lattice image (Hytch, Snoeck and Kilaas 1998) | present, validation none (0.8.0) | `GeometricPhaseAnalysis.analyze`, `findReflections` | the planted field of `tools/gpa/planted.py` (no upstream package): 2x the plain-numpy selfcheck error per case (uniform 9.8e-4, bump 3.12e-3, bump_oblique 2.97e-3, perfect 1e-6), floor 1e-6, interior only | study | Float64 CPU only; measured errors equal the numpy GPA's (4.89e-4, 1.56e-3, 1.48e-3, 2.5e-11 at size 128); badged `validation: "none"` until one dataset with truth passes | + +## Known limits and decisions + +Open work is in the [issues](https://github.com/mac4DSTEM/ImagoKit/issues), including the ones closed as *not planned* and +parked for later (`label` speed #6, upsampled registration #7, FFT gaps #8, 1.0 naming #9, precipitation benchmarks #11). +Speed against Python is in `docs/benchmarks.md`. + +- **`fft2` Float64 on the vDSP lengths** (2^n x {3, 5, 15}) is up to 1.12x NumPy (480²: 1.53 ms against 1.37); the remainder + is the transposes around the vDSP calls. The kit keeps the vDSP path by design. +- **Naming before 1.0:** the newer surface takes `rows`/`columns`; `LabelImage`, `Micrograph` and the older morphology and + segmentation API still say `width`/`height` (unified at 1.0, a break). +- **`BinaryMorphology`** refuses even structures and an all-false structure (SciPy shifts the origin and answers) and runs a + per-pixel loop (fine at 512², slow for a 4096² mask). +- **`Thresholding.yen`** can differ from scikit-image on an image of more than 2^24 pixels whose last histogram bin is nearly + empty (Float32 `P1` reaches 1 and NumPy's `argmax` returns the first NaN). +- **`regionProperties`** computes every property per object; use `objectMoments` alone for just the moments. +- **`zoom` and `rotate` with `wrap`** at spline orders 2 to 5 are refused by name; the shifts measured the rule (prefilter + and taps as `mirror`, the coordinate mapped with period n − 1), the resampling coordinate path has not been checked against it. +- **Masked registration:** scikit-image's `xcorr == xcorr.max()` tie rule is reproducible only for a unique maximum; the + kit's fast length is the next 7-smooth length, SciPy's also allows 11, so symmetric pairs decide on rounding noise. +- **Metal Float32 paths** (FFT, then the separable filters) are not written; the break-even at 2048² to 8192² is measured in + the nightly review first. diff --git a/docs/status.md b/docs/status.md index 398b054..420520d 100644 --- a/docs/status.md +++ b/docs/status.md @@ -3,6 +3,6 @@ ImagoKit 0.8.0 (2026-10-22), against NumPy 2.5.4, SciPy 1.18.1 and scikit-image 0.26.0. Live: the FFT surface (`fft2`, `rfft2`, `fftshift`, axes and norm), power spectra and spot measurement, Gaussian, uniform, difference-of-Gaussians, Butterworth and window filters, Fourier and spline-order shifts, phase correlation (plain and masked), HRTEM filter steps, `peak_local_max`, profiles, colormaps, segmentation helpers (thresholds, binary morphology, labelled statistics, `regionprops` completion, areal density, distance transform, watershed, rolling ball), zoom and rotate, TV and non-local-means denoising, real-input 2D and 1D transforms and a mixed-radix Float64 plan, GPA strain (planted truth, validation none), CSV export. Parity: `docs/parity.md` (93 rows: 36 present, 12 partial, 45 missing); timings against Python: -`docs/benchmarks.md`; what is open (the upsampled registration, `label`, Metal paths): `docs/open-items.md`. +`docs/benchmarks.md`; what is open: the GitHub issues (the upsampled registration, `label`, Metal paths); known limits: `docs/parity.md`. Test split: the comparisons with the pinned NumPy, SciPy and scikit-image values and the benchmark harness are the `ImagoKitParityTests` target (fixtures stay in `Tests/Imago*Tests/Fixtures`, found through `ImagoKitTestSupport`); `swift test -c release --parallel --skip ImagoKitParityTests` runs the fast unit tests (what every pull request runs), the plain `swift test -c release --parallel` the full suite (before a release and nightly). On an Apple Silicon Mac, release, warm build, the fast target takes about 2 s and the full suite about 5 s.