diff --git a/docs/api.md b/docs/api.md index b503f50..72ac101 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,7 +14,7 @@ instrument variant. ```bash python -m pip install spmkit # PyPI 0.1.2 -python -m pip install "spmkit[gwy,hdf5,grains]" # selected optional features +python -m pip install "spmkit[gwy,hdf5]" # selected optional features ``` The current source and GitHub-release options are listed in the @@ -88,6 +88,306 @@ Roughness expects a previously levelled spatial image. The result fields use the ISO-style capitalization shown above. The current implementation excludes non-finite values and centres the finite height population before calculating the metrics. +## Arc-revolution background + +SPM-Kit exposes physical arc-revolution background estimation through the +public Python API: + +```python +from spmkit.core.analysis import ( + estimate_arc_revolution_background, + remove_arc_revolution_background, +) + +background = estimate_arc_revolution_background( + height, + radius=2e-6, + direction="both", + side="below", + border="nearest", +) + +corrected = remove_arc_revolution_background( + height, + radius=2e-6, + direction="both", + side="below", + border="nearest", +) +``` + +`radius` is expressed in metres. Channel heights must use a supported +geometric Z unit. Heights are converted internally to metres and returned in +the original unit while preserving the channel context. + +`direction="horizontal"` processes rows, `"vertical"` processes columns, and +`"both"` applies horizontal followed by vertical. `side="above"` is defined +as the inversion dual of `"below"`. + +The current contract accepts finite data and the `"nearest"` and `"reflect"` +border policies. Masks, CLI and Fathom exposure are not available. The +estimated background remains separately inspectable and satisfies +`corrected + background == original` within floating-point tolerance. + +This implementation is LEVEL 1 — SOFTWARE_VERIFIED through synthetic tests +and an independent test-local one-dimensional oracle. Numerical equivalence +with Gwyddion has not been established. + +## Sphere-revolution background + +Sphere Revolution uses a true two-dimensional spherical cap in physical XY +coordinates: + +```python +from spmkit.core.analysis import ( + estimate_sphere_revolution_background, + remove_sphere_revolution_background, +) + +background = estimate_sphere_revolution_background( + height, + radius=2e-6, + side="below", + border="nearest", +) + +corrected = remove_sphere_revolution_background( + height, + radius=2e-6, + side="below", + border="nearest", +) +``` + +`radius` is expressed in metres. Geometric Z values are converted internally +to metres and returned in the channel's original unit. + +The spherical footprint is circular in physical coordinates. With anisotropic +pixel spacing it can therefore appear elliptical in array-index coordinates. +This operation is genuinely two-dimensional and is not equivalent to applying +horizontal and vertical arc openings sequentially. + +`side="above"` is the exact inversion dual of `"below"`. The supported border +policies are `"nearest"` and `"reflect"`. Finite data are required; masks, CLI +and Fathom exposure are not available. + +The background remains separately inspectable and satisfies +`corrected + background == original` within floating-point tolerance. + +This implementation is LEVEL 1 — SOFTWARE_VERIFIED through synthetic tests +and independent test-local two-dimensional oracles for both supported border +policies. Numerical equivalence with Gwyddion has not been established. + +## Gwyddion-compatible Sphere-revolution background + +SPM-Kit provides data-adaptive background estimation compatible with Gwyddion 2.71's +Revolve Sphere module: + +```python +from spmkit.core.analysis import ( + analyze_gwyddion_sphere_revolution_background, + estimate_gwyddion_sphere_revolution_background, + remove_gwyddion_sphere_revolution_background, +) + +background = estimate_gwyddion_sphere_revolution_background( + channel, + radius_px=20.0, + inverted=False, +) + +corrected = remove_gwyddion_sphere_revolution_background( + channel, + radius_px=20.0, + inverted=False, +) + +result = analyze_gwyddion_sphere_revolution_background( + channel, + radius_px=20.0, + inverted=False, +) +``` + +`radius_px` is expressed in samples (array-index units). The public Gwyddion-compatible +range is inclusive from 1.0 through 1000.0. `channel` must be a real, finite, non-empty +`SPMChannel`. + +`inverted=False` executes Gwyddion 2.71's normal Sphere Revolution route. `inverted=True` +applies the exact dual `-B(-data)` for background estimation. To avoid the internal crash +occurring in Gwyddion 2.71's C module when `inverted=True`, the corrected channel uses +the safe deliberate divergence `corrected = original - background`, guaranteeing exact +reconstruction of the original channel data. + +`analyze_gwyddion_sphere_revolution_background` returns a `BackgroundResult` with +`method="gwyddion_sphere_revolution"` and `parameters={"radius_px": float(radius_px), "inverted": bool(inverted)}`. + +This estimator is distinct from SPMKit's physical sphere-revolution model +(`estimate_sphere_revolution_background`), which operates with physical metric radii in metres, +circular footprints in physical coordinates, and explicit physical border policies. + +## Gwyddion 2.71 Median Background + +SPM-Kit provides the frozen Gwyddion 2.71 Median Background semantics through three public +operations: + +- `estimate_gwyddion_median_background(channel, radius_px=20) -> SPMChannel` +- `remove_gwyddion_median_background(channel, radius_px=20) -> SPMChannel` +- `analyze_gwyddion_median_background(channel, radius_px=20) -> BackgroundResult` + +```python +from spmkit.core.analysis import ( + analyze_gwyddion_median_background, + estimate_gwyddion_median_background, + remove_gwyddion_median_background, +) + +background = estimate_gwyddion_median_background( + channel, + radius_px=20, +) + +corrected = remove_gwyddion_median_background( + channel, + radius_px=20, +) + +result = analyze_gwyddion_median_background( + channel, + radius_px=20, +) +print(result.method, result.parameters) +``` + +`radius_px` is an integer pixel radius with default `20` and inclusive range `1..1024`. +The kernel is Gwyddion's fixed inclusive digital ellipse and exterior samples use fixed +nearest-edge `gwyddion_border_extend`; the public API intentionally exposes no border, shape, +rank, or backend option. `estimate_gwyddion_median_background()` returns the background as an +`SPMChannel`, `remove_gwyddion_median_background()` returns `input - background` as an +`SPMChannel`, and `analyze_gwyddion_median_background()` returns a `BackgroundResult`. + +The result method is `"gwyddion_median_background"`. Its metadata records `radius_px`, +`kernel_resolution`, `kernel_active_count`, `rank_index`, `rank_backend_reference`, +`border_policy="gwyddion_border_extend"`, and +`kernel_geometry="gwyddion_digital_ellipse"`. `rank_backend_reference` identifies the +observed Gwyddion reference route, not an SPM-Kit backend. + +Inputs must be finite two-dimensional data; NaN and infinite values are rejected. The source +channel is not mutated, and output channels preserve its shape, units, ranges, direction, group, +and copied metadata according to the `SPMChannel` contract. The public implementation does not +require Gwyddion at runtime. + +This capability is CROSS_VALIDATED only within its frozen 36-case Gwyddion 2.71 campaign. Its +scope, frozen evidence, semantics, and non-claims are specified in the +[Gwyddion Median Background compatibility specification](design/GWYDDION_MEDIAN_BACKGROUND_COMPATIBILITY.md). + +## Gwyddion 2.71 Filter flat-disc morphology + +SPM-Kit exposes the frozen Gwyddion 2.71 Filter-tool flat-disc Opening and Closing: + +```python +from spmkit.core.analysis import ( + gwyddion_flat_disc_closing, + gwyddion_flat_disc_opening, +) + +opened = gwyddion_flat_disc_opening(channel, size_px=5) +closed = gwyddion_flat_disc_closing(channel, size_px=5) +``` + +Both functions accept a pixel-based `size_px` in the inclusive range `2..31`, defaulting to +`5`, and return a new `SPMChannel`. The K×K digital ellipse, nearest-edge extension, and +Gwyddion executable even-size anchoring are fixed; erosion, dilation, masks, ROI, ASF, and +physical-radius options are not public parameters. Inputs must be finite, non-empty, and 2D; +the source channel is not mutated, and shape, Z/XY units, ranges, direction, group, and copied +metadata are preserved. + +This capability is `CROSS_VALIDATED` only within the frozen 12-field campaign and six sizes +`2, 3, 4, 5, 30, 31` (72 Opening and 72 Closing cases). The complete scope, executable tie +semantics, evidence identities, and non-claims are recorded in the +[Gwyddion flat-disc morphology compatibility specification](design/GWYDDION_FLAT_DISC_MORPHOLOGY_COMPATIBILITY.md). + +## Gwyddion 2.71 Path Level + +SPM-Kit exposes the frozen Gwyddion Path Level operation as a non-mutating channel transform: + +```python +from spmkit.core.analysis import gwyddion_path_level + +lines = [(0.0, 0.0, 4.0e-6, 3.0e-6)] +levelled = gwyddion_path_level(channel, lines, thickness_px=1) +``` + +`gwyddion_path_level(channel, lines, *, thickness_px=1) -> SPMChannel` accepts an ordered +collection of straight physical-coordinate selections `(x0, y0, x1, y1)`. Duplicates and order +are meaningful. `thickness_px` is an integer in the inclusive range `1..128`, defaulting to +`1`. The operation has fixed endpoint conversion, no interpolation, horizontal-line exclusion, +and cumulative row-level correction semantics. It requires finite, non-empty 2D data and finite +positive channel ranges. + +The result is a new `SPMChannel`: shape, Z/XY units, ranges, name, direction, group, and copied +metadata are preserved, while the input remains unchanged. Masks, ROI, `GwySelectionPath`, +splines, polylines, profiles, and GUI publication parameters are not part of this API. The scope, +executable evidence, and non-claims are defined in the +[Gwyddion Path Level compatibility specification](design/GWYDDION_PATH_LEVEL_COMPATIBILITY.md). + +## Gwyddion 2.71 Align Rows statistics + +SPM-Kit exposes four explicit, non-mutating Gwyddion Align Rows statistics transforms. They are +separate from the existing generic `align_rows`, whose semantics are not described as +Gwyddion-compatible. + +```python +from spmkit.core.analysis import ( + gwyddion_align_rows_median, + gwyddion_align_rows_median_of_differences, + gwyddion_align_rows_trimmed_mean, + gwyddion_align_rows_trimmed_mean_of_differences, +) + +median = gwyddion_align_rows_median(channel, mask=mask, mask_mode="include") +differences = gwyddion_align_rows_median_of_differences(channel, direction="vertical") +trimmed = gwyddion_align_rows_trimmed_mean(channel, trim_fraction=0.05) +trimmed_differences = gwyddion_align_rows_trimmed_mean_of_differences( + channel, trim_fraction=0.05 +) +``` + +The public signatures are +`gwyddion_align_rows_median(channel, *, mask=None, mask_mode="ignore", direction="horizontal")` +and `gwyddion_align_rows_median_of_differences(channel, *, mask=None, mask_mode="ignore", +direction="horizontal")`; the two trimmed variants add the keyword-only +`trim_fraction=0.05`. `GwyddionAlignRowsMaskMode` is the typed literal +`"exclude" | "include" | "ignore"`; `GwyddionAlignRowsDirection` is +`"horizontal" | "vertical"`. A mask is optional, finite, numeric, and exactly channel-shaped. +Without a mask, every stored mode selects all samples. Outputs are independent C-contiguous +`float64` fields in a new `SPMChannel`, preserving name, units, physical ranges, direction, +group, and copied metadata. + +The fixed source semantics are: `Exclude = 0`, `Include = 1`, and `Ignore = 2`; vertical +processing is transpose/restore. For absolute methods Include selects mask values `> 0.0`, +Exclude selects values `< 1.0`, and undersampled rows use the global masked upper-median fallback +before mean centring all row shifts. Difference methods require both adjacent mask values `> 1.0` +(Include) or `< 1.0` (Exclude), use `+0.0` for undersampled pairs, accumulate from row zero, and +remove an unweighted least-squares row-index slope. Median is upper median. Trimmed methods use +`floor(fraction*n + 0.5)` and use upper median when trimming would leave no retained value. +The current public boundary returns only the corrected channel; private correction/background +diagnostics are intentionally not a new public result architecture. + +`portable_source_semantics` is the production contract. Public end-to-end tests are +`CROSS_VALIDATED` only within the frozen finite 64-case campaign: all `64/64` corrected arrays and +`3888/3888` elements are bitwise exact to the independent portable V2 oracle. The secondary +`installed_gwyddion_2_71_fast_math_profile` is bitwise exact in `61/64` arrays and `3757/3888` +elements. Its only recorded differences are three signed-zero elements in +`median__plateaus_signed_zero__10` and 64 finite elements in each of +`median_of_differences__irregular__11` and +`trimmed_mean_of_differences__irregular__11`, bounded by absolute difference +`5.329070518200751e-15`. The installed `process.so` +(`c21d52375807ae096e34a3469c2f20c4c66ea3197479e13215a6d7b9d465b451`) was built with GCC 16.1.1 +`-ffast-math`, associative reassociation, and LTO. SPM-Kit does not emulate that local build; +no V3 was justified. The complete evidence, profile policy, and non-claims are in the +[Gwyddion Align Rows statistics compatibility specification](design/GWYDDION_ALIGN_ROWS_STATISTICS_COMPATIBILITY.md). + ## KPFM statistics ```python @@ -122,7 +422,7 @@ print(segmentation.n_grains, segmentation.mean_diameter) print(segmentation.coverage, segmentation.density) ``` -`radial_psd()` returns `q` in `1/m`. Grain detection requires the `grains` extra, +`radial_psd()` returns `q` in `1/m`. Grain detection uses SciPy, a required SPMKit dependency, uses eight-connected components, and reports density in grains per µm². Automatic thresholding is an algorithmic default, not a scientifically universal segmentation rule; record or override it for a campaign. @@ -234,7 +534,7 @@ The plugin contract is versioned, but the surrounding package remains alpha. | capability inspection | `inspect_any(path)` | `DatasetInfo` | | capability loading | `load_any(path, kind)` | `(payload, kind)` | | force loading | `load_force(path)` | `ForceVolume` | -| image preprocessing | `analysis.leveling.*` | new `SPMChannel` | +| image preprocessing | `analysis.leveling.*`, `analysis.background.*` | new `SPMChannel` | | numerical results | `analysis.*` | immutable result dataclasses or arrays | | open exports | `core.export.*`, `save_gwy()` | file path/output artifact | | extension discovery | `spmkit.plugins.v1` | registered `Reader`/`Domain` | diff --git a/docs/design/GWYCOMPAT_SOURCE_AUDIT_FOUNDATION.md b/docs/design/GWYCOMPAT_SOURCE_AUDIT_FOUNDATION.md new file mode 100644 index 0000000..a4a7959 --- /dev/null +++ b/docs/design/GWYCOMPAT_SOURCE_AUDIT_FOUNDATION.md @@ -0,0 +1,43 @@ +# GwyCompat Source Audit Foundation + +## Status and boundary + +`spmkit.compat.gwyddion` is a conservative source-compatible migration and audit layer. This +foundation inventories supplied Gwyddion C source text statically; it does not compile, execute, +translate, import, or load a Gwyddion module or shared object. It provides no binary +compatibility and does not claim that a complete module is portable because a registration or +symbol is recognized. + +The initial profile is limited to frozen Gwyddion 2.71 source. It recognizes module-query macros, +the registered process/tool families, Gwyddion prefixes, and GTK/GLib dependencies. The only +explicit current mappings are data-model facts already represented by `SPMChannel`: x/y +resolution and x/y physical ranges. All other symbols remain `adapter-required`, `unsupported`, +or `unknown` as reported; similar names never establish support. + +## Static audit contract + +The lexical scanner preserves line/column locations, local and system includes, multiline calls, +registration-looking calls, Gwyddion symbols, and GTK/GLib dependencies. It masks comments and +string/character literal contents before detecting symbols and distinguishes function-like calls +from plain references. It deduplicates each symbol while retaining all ordered occurrences. + +This is not a complete C parser. It does not preprocess macros, resolve types, evaluate control +flow, prove mutation, or infer scientific semantics. UI, selection, parameter, publication, and +mutation results are named conservative audit hints. The report is deterministic JSON-compatible +data with a source content SHA-256 and a schema version; the core auditor performs no filesystem +writes. + +## Migration and licensing rules + +GwyCompat does not copy GPL implementation bodies and does not automatically translate scientific +algorithms. License compatibility must be reviewed for every proposed migrated module. Numerical +equivalence remains subject to the established workflow: + +```text +source → external probe → independent oracle → SPMKit implementation → validation +``` + +The closed Flatten Base, Arc, Sphere, Median Background, Flat-Disc, and Path Level specifications +remain scientific evidence for their individual capabilities. They are not a general source +migration authorization. Future data-field, selection, parameter, and publication adapters must +be designed, tested, and licensed independently before a report can move beyond static inventory. diff --git a/docs/design/GWYDDION_ALIGN_ROWS_STATISTICS_COMPATIBILITY.md b/docs/design/GWYDDION_ALIGN_ROWS_STATISTICS_COMPATIBILITY.md new file mode 100644 index 0000000..1d51d10 --- /dev/null +++ b/docs/design/GWYDDION_ALIGN_ROWS_STATISTICS_COMPATIBILITY.md @@ -0,0 +1,74 @@ +# Gwyddion 2.71 Align Rows statistics compatibility boundary + +## Scope and evidence status + +This private SPMKit kernel covers only four Gwyddion Align Rows row-shift statistics methods: + +1. Median (`1`) +2. Median of differences (`2`) +3. Trimmed mean (`5`) +4. Trimmed mean of differences (`6`) + +The frozen campaign contains 64 finite `float64` cases, sixteen per method. It is evidence for this bounded domain, not a claim of universal, non-finite, other-version, performance, or adapter equivalence. The production contract is `portable_source_semantics`, represented by the frozen independent V2 oracle. Four explicit public `SPMChannel` wrappers delegate once to this private implementation. The generic SPMKit `align_rows` dispatcher remains a separate backward-compatible extension for historical median/mean calls and is not described as Gwyddion-compatible. + +The repository fixture freezes a secondary profile, `installed_gwyddion_2_71_fast_math_profile`. It was executed by the installed Gwyddion 2.71 module and is retained as external executable evidence, not as the production arithmetic contract. + +## Source call graph + +The source basis is Gwyddion 2.71 `modules/process/linematch.c` (SHA-256 `79b951a161431ba9822d8d0faba2b512107a5e4822569f78c42201f289e06604`) dispatching Align Rows to `libprocess/correct.c` (SHA-256 `bdac3ea8fcc3555f33644c84d739818c12a8cb9c104cac06ac642c77d2ddaabb`). The final difference-method line fit uses `gwy_data_line_get_line_coeffs()` in `libprocess/dataline.c` (SHA-256 `359f8fed916eb9216441e3afea4238c7128c587b0877100e0a42f2737e8edbf4`). + +The source-driven chain is: + +```text +module dispatch → mask routing → oriented rows / adjacent pairs +→ estimator → row correction sequence → mean or slope normalization +→ correction application → optional input-minus-corrected background +``` + +## Shared input transport + +`Exclude = 0`, `Include = 1`, and `Ignore = 2`. A null mask selects every sample or pair regardless of the stored mode. Ignore likewise discards a present mask before row orientation. Direction `0` works on rows directly; direction `1` transposes field and mask into source-equivalent working rows, then restores the result. The transpose is logical orientation only: the returned field preserves original shape and C-contiguous `float64` storage. + +All inputs, masks, and trim fractions are finite in the frozen production domain. The private kernel rejects invalid geometry, non-real/non-finite fields, mask shape mismatch, invalid enum values, unsupported directions, and trim fractions outside `[0.0, 0.5]`. It never mutates its input field or mask. + +## Absolute methods: Median and Trimmed mean + +For an oriented row `z[r,c]` with mask `m[r,c]`: + +- Include selects `m[r,c] > 0.0`. +- Exclude selects `m[r,c] < 1.0`. +- Ignore selects every `c`. + +Consequently exact `0.5` is selected by both per-row Include and Exclude predicates. The automatic minimum sample count is `floor(log(width) + 1 + 0.5)`. Below that count, including zero and one selected samples, the estimate falls back to the global masked upper median. The source-confirmed global Exclude fallback population uses `m <= 0.0`; this intentional distinction from the per-row Exclude predicate is retained in the portable implementation. + +The median is the upper median: rank `floor(n/2)` after ordering. Trimmed mean computes `trim = floor(fraction*n + 0.5)`, retains `[trim, n-trim)`, and falls back to upper median if trimming would leave no retained sample. The frozen portable reduction explicitly preserves the confirmed sample sorting and binary64 accumulation order. Every absolute row shift is then mean-centred over all oriented rows before subtraction. + +## Difference methods: Median of differences and Trimmed mean of differences + +For adjacent oriented rows, the candidate difference is `z[r+1,c] - z[r,c]`. + +- Include requires **both** masks `> 1.0`. +- Exclude requires **both** masks `< 1.0`. +- Ignore selects every adjacent pair. + +Exact `0.5` participates in Exclude pairs; exact `1.0` participates in neither joint predicate. Below the same automatic count, including zero or one pair, an adjacent increment is `+0.0`. Increments are cumulatively added from row zero. The complete cumulative sequence is then levelled by the source-derived unweighted index-space least-squares line fit using every oriented row. The corrected field is the original oriented sample minus the final correction sequence. + +## Background and representation + +When requested, background is computed as `input - corrected` in the confirmed float64 loop order. The fixture requires the portable and installed background arrays to be bitwise identical for all eight requests (`504/504` elements), and it records both reconstruction relations separately. The result record is frozen; corrected field, optional background, and correction sequence are C-contiguous `float64` arrays. + +## Dual-profile divergence policy + +The frozen V2 portable profile and the installed external profile agree bitwise for `61/64` corrected arrays and `3757/3888` elements. No mismatch is silently normalized. + +| Exception | Frozen classification | Policy | +| --- | --- | --- | +| `median__plateaus_signed_zero__10` | 3 signed-zero-only elements; numerical equality | No output-specific zero-sign patch. | +| `median_of_differences__irregular__11` | 64 finite nonzero elements; max abs `5.329070518200751e-15` | Preserve portable source arithmetic. | +| `trimmed_mean_of_differences__irregular__11` | Same 64-element finite build-profile scope | Preserve portable source arithmetic. | + +The installed package was built with GCC 16.1.1, `-ffast-math`, associative floating-point reassociation, LTO, and package optimization flags. The frozen installed-build diagnosis is `INSTALLED_BUILD_ROOT_CAUSE_CONFIRMED` and `V3_NOT_JUSTIFIED`: disabling associative math in an isolated source build returns the portable result, while disabling LTO or vectorization does not. Emulating this local compiler transformation in SPMKit would overfit a build profile rather than implement portable source semantics. + +## Evidence maturity and non-claims + +This design records `SOURCE_CONFIRMED`, frozen external-probe evidence, and a bounded V2-oracle production contract. Public end-to-end tests are `CROSS_VALIDATED` only for the listed finite 64-case campaign: the portable profile is bitwise exact, while the installed fast-math profile retains its explicit bounded exceptions. It does not claim universal Gwyddion parity, non-finite equivalence, other-version/build or performance equivalence, adapter support, or correctness for any other Align Rows method family. GwyCompat is unchanged in this batch. diff --git a/docs/design/GWYDDION_ARC_REVOLUTION_COMPATIBILITY.md b/docs/design/GWYDDION_ARC_REVOLUTION_COMPATIBILITY.md new file mode 100644 index 0000000..3e6c6b3 --- /dev/null +++ b/docs/design/GWYDDION_ARC_REVOLUTION_COMPATIBILITY.md @@ -0,0 +1,443 @@ +# Gwyddion 2.71 Revolve Arc Compatibility Specification + +**Specification ID:** `spmkit-gwyddion-arc-revolution-v1`
+**Status:** Normative pre-implementation contract
+**Reference:** Gwyddion 2.71
+**Base branch:** `feat/gwyddion-leveling-parity`
+**Base commit:** `c0e4fc1d3ed24d9970b2e4f6781fb2552d9527c8`
+**Frozen fixture:** `gwyddion-2.71-arc-revolution-directional`
+**Scoped maturity:** `LEVEL 3 — CROSS_VALIDATED` + +This claim applies only to the source, probes, fixtures, routes, parameter +cases and tolerances declared here. It is not a universal-equivalence claim. + +## 1. Purpose + +This specification binds together the mathematics, exact reference control +flow, external probes, numerical fixtures, public API, tests, provenance and +scientific claims for SPMKit's Gwyddion-compatible Revolve Arc operation. + +The implementation is judged against the evidence and this specification. +Neither the algorithm, tests nor specification may be silently altered merely +to obtain passing tests. + +Every discrepancy must first be classified as an implementation defect, test +defect, oracle defect, reference defect, specification defect, +unsupported-domain case, or floating-point/platform effect. + +## 2. Scientific identity and scope + +The operation is classified as a: + +> **Gwyddion 2.71-compatible, data-adaptive arc-envelope background +> estimator.** + +It is not represented as: + +- SPMKit's physical arc-revolution estimator; +- a classical morphological opening; +- an exact frequency cutoff; +- probe deconvolution; +- tip estimation; +- specimen-surface reconstruction; +- metrologically certified correction. + +SPMKit's existing physical Arc Revolution remains an independent algorithm. +It uses a physical radius in metres, lateral pixel spacing and explicit border +policies. The compatibility operation uses radius in samples and reproduces +the declared Gwyddion 2.71 semantics. + +## 3. Evidence hierarchy + +Conflicts are resolved in this order: + +1. Exact Gwyddion 2.71 source. +2. C probes compiled against Gwyddion 2.71. +3. Frozen JSON/NPZ numerical fixtures. +4. Official Gwyddion user documentation. +5. Mathematical and SPM-domain literature. +6. Bibliographic and general software context. + +Literature defines terminology and conceptual boundaries. It does not +override observed behaviour of the frozen executable reference. + +### 3.1 Executable evidence + +| Artifact | SHA-256 | +|---|---| +| Gwyddion 2.71 `arc-revolve.c` | `afb19a2382b0abb46595fa3dabc126ade50ec31c91ec9c96ea2284f42d0a67ac` | +| Behaviour-probe source | `27e92376d7955f134a6d76091775dc28fe2e1ba8246936b27e2e924d3ba765f4` | +| Behaviour-probe output | `1f8ee0535ac3b0d93e3b330ec4f96b39436e4da1853f5d3ce9ba45e1f2d0eca3` | +| Directional fixture metadata | `5e037b33e04d2c95420c3e71acbf7b4bc46b8723163bfc11363e6ac083005cd2` | +| Directional fixture NPZ | `50b263b8add97950ba1ef882f96d5ee3bc35001908c47a1dbb20b9428bc3bc5e` | + +### 3.2 Literature evidence + +| Source | Role | SHA-256 | +|---|---|---| +| Gwyddion levelling guide | Declared user semantics | `002b1af784a1f5c441c21bbf55b670c335d5ed53eb0f6e02ab894d4911178ade` | +| Heijmans, 1995 | Mathematical morphology context | `085cede6c5cce62e214d14eb9ef624db5902f8ac512ec9626761afeedafa41eb` | +| Villarrubia, 1997 | SPM geometry and reconstruction boundary | `d50c845edf53bb6713dc8c3d72fdded1db6ba44906bbac0ee9d830eaad0dbae9` | +| Nečas–Klapetek DOI CSL | Bibliographic identity | `5f6ec95fd7eb68ec66aa8eeeaeee4284d1232e8a85cfbccb48e5d11ca20f448e` | + +The Nečas–Klapetek full-text PDF is contextual and optional. Its absence does +not weaken executable numerical provenance. Dynamic publisher HTML is +explicitly non-normative. + +## 4. Mathematical boundary + +Mathematical morphology provides an algebraic and geometric framework for +non-linear image transformations. Villarrubia applies dilation and erosion to +SPM image simulation, surface reconstruction and tip estimation. + +These sources explain why geometric envelopes matter in SPM. They do not +prove that Gwyddion Revolve Arc is a classical morphological opening. + +The reference operator is data-adaptive because: + +- arc amplitude depends on the global RMS; +- local mean-minus-RMS clipping modifies the working profile; +- moving sums contain historical reference-specific control flow. + +Idempotence, anti-extensivity, increasingness and other morphology axioms +must not be claimed without separate proof for this exact adaptive operator. + +## 5. Public input contract + +The public operation accepts a real, finite, non-empty, two-dimensional +channel. + +- `radius_px` is a finite real scalar. +- Boolean, complex, NaN and infinite radii are rejected. +- `1.0 <= radius_px <= 1000.0`. +- Default `radius_px` is `20.0`. +- Direction is `horizontal`, `vertical` or `both`. +- `inverted` is strictly boolean. +- Masks and non-finite field values are unsupported. +- Z units are preserved and need not be geometric lengths. +- Physical lateral ranges do not affect the numerical output. + +## 6. Global scale + +For all N field samples in C-order, + + mu = (1/N) sum_i f_i + +and + + sigma = sqrt((1/N) sum_i (f_i - mu)^2). + +This is population RMS. The arc scale is + + q = sigma / sqrt(2/3 - pi/16). + +Accumulation order is part of numerical compatibility. + +## 7. Discrete arc + +For radius r and processing-axis resolution n, + + s = floor(min(r, n) + 1/2). + +This is Gwyddion positive half-up rounding, not bankers' rounding. + +For k from -s through s, with u = abs(k)/r, + + phi_r(k) = + u^2/2 * (1 + u^2/4 * (1 + u^2/2)) when r/8 > n + 1 when u > 1 + 1 - sqrt(1 - u^2) otherwise + +and + + a_r(k) = q * phi_r(k). + +Branch order and floating-point operation order are normative. + +## 8. Historical moving sums + +Local statistics reproduce Gwyddion 2.71 `moving_sums()` control flow. + +Ordinary regimes agree with asymmetric truncated windows. When window size +becomes comparable to the profile, the historical `Moving a whale` branch is +normative even where it differs from a conventional window oracle. + +The oversized shortcut is retained in the private primitive for source +fidelity, although it is unreachable through normal valid arc geometry. + +## 9. Local clipping and horizontal envelope + +At each position j, let m_j and s_j be the local mean and population RMS +produced by the historical moving-sum route. + + g_j = max(f_j, m_j - 2.5*s_j). + +No undocumented variance clamp is introduced. + +The horizontal background is + + H_r(f)_j = min_k (g_(j+k) + a_r(k)), + +using only offsets that remain within the profile. Edges use truncated +support. There is no padding mode and no public border parameter. + +## 10. Directional composition + +For a field F: + + B_horizontal(F) = H_r(F) + + B_vertical(F) = transpose(H_r(transpose(F))) + + B_both(F) = + transpose(H_r(transpose(H_r(F)))) + +`both` is horizontal followed by vertical. It is ordered and is not +represented as a commutative isotropic two-dimensional operator. + +## 11. Inversion and corrected field + +The inverted background is + + B_inverted(F) = -B(-F). + +The corrected field is always + + C(F) = F - B(F). + +This reconstruction identity applies to all six direction/inversion routes. + +## 12. Known reference defects + +### 12.1 Horizontal plus inverted corrected result + +Gwyddion 2.71 computes and restores the background correctly, then returns +before writing the corrected field. + +Classification: `KNOWN_REFERENCE_DEFECT`. + +SPMKit policy: + +- preserve the externally validated background; +- return `input - background`; +- test reconstruction explicitly; +- disclose the repaired divergence; +- never claim reproduction of the defective corrected output. + +### 12.2 One-sample processing axis + +The historical moving-sum branch can read before its output buffer. + +Classification: `KNOWN_REFERENCE_DEFECT`. + +SPMKit policy: + +- do not freeze process-memory-dependent output; +- define a one-sample processed axis as identity; +- test the safe definition explicitly. + +### 12.3 Dynamic publisher HTML + +Immediate downloads produced different generated byte streams. + +Classification: `NON_NORMATIVE_DYNAMIC_CAPTURE`. + +SPMKit policy: + +- use DOI CSL as canonical bibliographic identity; +- treat local full text as optional context; +- retain dynamic HTML only as provenance; +- never make its byte hash an implementation requirement. + +## 13. Public API + + estimate_gwyddion_arc_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, + ) -> SPMChannel + + remove_gwyddion_arc_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, + ) -> SPMChannel + + analyze_gwyddion_arc_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, + ) -> BackgroundResult + +The existing physical functions remain unchanged. + +## 14. Single authoritative numerical route + +The adapter exposes one internal route: + + _gwyddion_arc_result( + data: np.ndarray, + radius: object, + *, + direction: GwyddionArcDirection, + inverted: bool, + ) -> tuple[FloatArray, FloatArray] + +It computes the background once and derives corrected data from that exact +background. + +- Estimate selects background. +- Remove selects corrected. +- Analyze wraps both. +- No public route recomputes the kernel. +- No alternative correction path can drift. + +## 15. Channel and structured-result contract + +Returned channels preserve: + +- name; +- unit; +- x and y ranges; +- acquisition direction; +- group; +- an independent metadata copy. + +Kernel arrays are: + +- `float64`; +- C-contiguous; +- independent of the input buffer; +- read-only; +- non-mutating. + +Structured results use: + + method = "gwyddion_arc_revolution" + +and record effective runtime parameters only: + + { + "radius_px": 20.0, + "direction": "horizontal", + "inverted": False, + } + +Reference versions, hashes, defects, tolerances and maturity remain in +validation provenance rather than runtime parameters. + +## 16. Verification model + +### 16.1 Source-semantic tests + +- half-up rounding; +- exact arc branch boundaries; +- population RMS; +- ordinary moving windows; +- `Moving a whale`; +- oversized shortcut; +- constant fields; +- single rows; +- large radii; +- one-sample safe definition; +- validation; +- non-mutation; +- immutability. + +### 16.2 External validation + +Frozen fixture: `gwyddion-2.71-arc-revolution-directional`. + +The campaign validates: + +- one asymmetric 5 by 7 field; +- radius 2.5; +- six background routes; +- five valid corrected routes; +- horizontal-inverted background; +- untouched defect sentinel; +- repaired SPMKit reconstruction; +- directional composition; +- artifact and array hashes. + +### 16.3 Metamorphic properties + +Where mathematically supported, test: + + B(F + c) = B(F) + c + C(F + c) = C(F) + B(alpha*F) = alpha*B(F), alpha > 0 + B_inverted(F) = -B(-F) + B_vertical(F) = transpose(B_horizontal(transpose(F))) + C(F) + B(F) = F + +Classical morphology axioms are not requirements without independent proof +for this adaptive operator. + +## 17. Scientific claim + +Supported claim: + +> SPMKit's declared Gwyddion-compatible Revolve Arc path is +> `LEVEL 3 — CROSS_VALIDATED` against Gwyddion 2.71 for the frozen kernels, +> fixture, six background routes, five valid corrected routes, focal cases +> and declared tolerances. + +This does not establish: + +- universal equivalence; +- equivalence with other Gwyddion versions; +- physical truth of the estimated background; +- specimen-surface recovery; +- tip deconvolution or reconstruction; +- metrological traceability; +- performance equivalence; +- support for masks or non-finite values. + +## 18. Change control + +Explicit specification review is required for changes to: + +- arithmetic or accumulation order; +- radius semantics; +- edge handling; +- local clipping; +- direction order; +- inversion; +- one-sample policy; +- public defaults or limits; +- runtime metadata; +- fixtures; +- tolerances; +- scientific claims. + +Tests may reveal a specification defect. They may not silently redefine the +specification. + +Source, probes, implementation, tests and specification must be reconciled +and classified before changing the algorithm. + +## 19. References + +1. Gwyddion developers, Data Levelling and Background Subtraction, frozen + official user documentation. +2. H. J. A. M. Heijmans, Mathematical Morphology: A Modern Approach in Image + Processing Based on Algebra and Geometry, SIAM Review 37(1), 1–36, 1995. + DOI: 10.1137/1037001. +3. J. S. Villarrubia, Algorithms for Scanned Probe Microscope Image + Simulation, Surface Reconstruction, and Tip Estimation, Journal of + Research of NIST 102(4), 425–454, 1997. + DOI: 10.6028/jres.102.030. +4. D. Nečas and P. Klapetek, Gwyddion: an open-source software for SPM data + analysis, Central European Journal of Physics 10(1), 181–188, 2012. + DOI: 10.2478/s11534-011-0096-2. +5. Masaryk University institutional publication record: + https://www.muni.cz/en/research/publications/966983 +6. Gwyddion project publication record: + https://gwyddion.net/publications/ diff --git a/docs/design/GWYDDION_FLAT_DISC_MORPHOLOGY_COMPATIBILITY.md b/docs/design/GWYDDION_FLAT_DISC_MORPHOLOGY_COMPATIBILITY.md new file mode 100644 index 0000000..4dccdf7 --- /dev/null +++ b/docs/design/GWYDDION_FLAT_DISC_MORPHOLOGY_COMPATIBILITY.md @@ -0,0 +1,54 @@ +# Gwyddion Flat-Disc Morphology Compatibility + +## Status and scope + +`gwyddion_filter_flat_disc_morphology` records Gwyddion 2.71 Filter-tool +flat-disc Opening and Closing for finite, non-empty, full-field 2D data with +mask policy fixed to ignore. It is limited to the frozen 12-field campaign +and sizes 2, 3, 4, 5, 30 and 31. + +## Reference and parameters + +The reference is the audited installed Gwyddion 2.71 executable path through +`gwy_data_field_area_filter_min_max`. `size_px` is an integer in `2..31`; +the planned public default is 5. The kernel is a K by K digital ellipse, +where K equals `size_px`. + +## Numerical semantics + +The exterior policy is nearest valid edge pixel. Minimum/erosion uses the +unreflected RLE mask and anchor `(K-1)//2`; maximum/dilation uses the rotated, +row-sorted RLE mask and anchor `K//2`. Opening is dilation after erosion; +Closing is erosion after dilation. + +The executable hierarchy is not generic C-language tie semantics. The +audited Gwyddion 2.71 library (`libgwyprocess2.so.0.51.1`, SHA-256 +`5f5b53cb544068638d1a3be8d6703345e49d5626d3fa4791106ce11bc051d3d7`, +Build ID `04187a41d4102c827e2705bb867292ba77ae37f4`) recursively constructs +Each/Even row reductions. Its compiled MINSD/MAXSD composition sites select +the second operand on equal values, preserving signed zero. The later RLE +aggregation uses strict comparison and retains the earlier row-major segment. + +## Evidence and fixture + +The external canonical reference is SHA-256 +`907bd347cc8c213d1061b786b6efe5692c87ffd8be62db1f6de2bd9bc78acdbd` and +its provenance is `c3777cdcfdd868a705ef09c63a7548a5b1c0eb0b79d053e02ac2f45e9c97e5af`. +The independent oracle V2 is `bf4129fe4fd871dda3132d5457d45d0833d69e9acd5cf3bb765fc0f3a8d9792e`; +its executable reduction model is `43089668a7fe0c699093be440402c8b1b11b42dfea4eb720785241788306c543`. + +The fixture stores 12 inputs, 30 masks for sizes 2..31, 72 Opening outputs, +and 72 Closing outputs. It records kernels 30/30 and both operations 72/72 +bitwise exact, max absolute difference 0, max ULP 0, signed-zero mismatches 0, +and input mutation 0. + +The rejected uninitialised-kernel microprobe is invalid evidence: elliptic +fill writes active pixels only. Approved probes zero-initialize the kernel +before filling it. + +## Non-claims + +This is not universal equivalence. It excludes NaN, infinities, ROI, masks, +ASF, tip morphology, other Gwyddion versions/builds, and performance parity. +It records the audited executable path, not a claim that all C compilers lower +the source identically. diff --git a/docs/design/GWYDDION_MEDIAN_BACKGROUND_COMPATIBILITY.md b/docs/design/GWYDDION_MEDIAN_BACKGROUND_COMPATIBILITY.md new file mode 100644 index 0000000..94e321b --- /dev/null +++ b/docs/design/GWYDDION_MEDIAN_BACKGROUND_COMPATIBILITY.md @@ -0,0 +1,156 @@ +# Gwyddion Median Background Compatibility + +## 0. Status and normative scope + +**FREEZE_AUDIT_APPROVED** applies to frozen Gwyddion 2.71 Median Background evidence. +This is a normative design specification for the next mini-batch; no production code is +delivered here. Evidence is limited to the frozen 36-case campaign, represents both rank +filter backends, does not validate an SPMKit implementation, and does not exclude errors +outside the frozen domain. + +## 1. Reference identity + +**SOURCE_CONFIRMED** reference software is Gwyddion 2.71. The frozen module is +Gwyddion 2.71 source `modules/process/median-bg.c`, SHA-256 +`5021fff407531459ed47aff7a47e4f5b2ce2ea7df13d04ca4405f05581258729`. +The manifest records the probe, runner, oracle, campaign, and fixture identities. + +## 2. User-visible operation + +`median_bg` estimates a local rank-filter background and returns corrected data by +subtracting that background from input. Its radius is a pixel-sample quantity, not a +physical lateral-unit quantity. + +## 3. Parameter contract + +**SOURCE_CONFIRMED** radius is an integer from 1 through 1024, with default 20. Future +compatibility APIs shall use `radius_px=20` and expose no configurable border, shape, or +rank parameter. Fixture-domain inputs are finite two-dimensional `float64`; mutation is +forbidden. + +## 4. Digital elliptical kernel + +**SOURCE_CONFIRMED** kernel resolution is `2*radius + 1`. The active region is an +inclusive digital ellipse over pixel centres: `kernel_index + 0.5` with squared ellipse +condition `<= radius_squared`. Offsets subtract `radius` and enumerate row-major. +Cardinalities for radii 1, 2, 3, 4, 20, and 1024 are 9, 21, 37, 69, 1313, and 3297401. + +## 5. Border extension + +**SOURCE_CONFIRMED** exterior handling is `GWY_EXTERIOR_BORDER_EXTEND`: an exterior +sample maps to the nearest valid edge pixel. No alternate border policy belongs here. + +## 6. Rank selection + +The rank is `kernel_active_count//2`. The direct reference path applies when active +count is at most 25; the radixtree reference path applies when active count exceeds 25. + +## 7. Background and corrected fields + +For input `F` and background `B`, corrected data are `C = F - B`. Frozen outputs are +finite, C-contiguous `float64` arrays with shapes equal to the corresponding input. + +## 8. Direct and radix-tree reference paths + +**EXECUTABLE_EXTERNAL_REFERENCE** covers direct radii 1 and 2, and radixtree radii 3, +4, 20, and 1024. A future implementation shall match observed fields without reproducing +Gwyddion's internal radixtree structure. + +## 9. External probe campaign + +The campaign contains 36 logical cases and 72 executions: 36 normal and 36 ASan. All +exit codes are zero; timeouts, GLib detections, and ASan detections are zero. Normal and +ASan stdout are byte-identical in all 36 pairs. Coverage includes wide, tall, constants, +signed fields, impulses, monotonic fields, singleton dimensions, oversized radii, edges, +and corners. + +## 10. Independent oracle + +**INDEPENDENT_ORACLE_CONFIRMED** is a Python and NumPy oracle with no SPMKit or SciPy +import, subprocess, or Gwyddion execution. It uses `numpy.partition` rather than the +internal selection path, calculates from metadata and `input_*` before loading reference +arrays, and selects no acceptance tolerance. All frozen background and corrected arrays +are bitwise equal to their reference counterparts. + +## 11. Frozen fixture + +The fixture has exactly 108 arrays: `input__`, `background__`, and +`corrected__` for each case. Background and corrected arrays are copied from the +approved external-reference arrays. Canonical array hashes use SHA-256 of `dtype.str`, a +NUL byte, comma-separated shape, a NUL byte, and C-order bytes. + +## 12. Future SPMKit API contract + +**FUTURE_IMPLEMENTATION_REQUIREMENT** reserves `estimate_gwyddion_median_background`, +`remove_gwyddion_median_background`, and `analyze_gwyddion_median_background`. Planned +parameters are `channel`, `radius_px=20`, and keyword-only parameters where applicable. +`BackgroundResult` shall use `gwyddion_median_background` and metadata `radius_px`, +`kernel_resolution`, `kernel_active_count`, `rank_index`, `rank_backend_reference`, +`border_policy="gwyddion_border_extend"`, and +`kernel_geometry="gwyddion_digital_ellipse"`. These are future requirements, not APIs +already present. + +## 13. Acceptance contract + +Background and corrected comparisons require bitwise exact `float64` equality. Output +shape must equal input; C-contiguity and finiteness are required for fixture inputs; input +mutation is forbidden. Reconstruction requires `input == background + corrected` with +absolute tolerance `1e-15` and relative tolerance `0`. No acceptance relaxation may be +introduced merely to satisfy tests. Any discrepancy must first adjudicate source, probe, +oracle, fixture, and implementation evidence. + +## 14. Evidence classification + +| Classification | Meaning | +|---|---| +| **SOURCE_CONFIRMED** | Frozen source establishes parameter, mask, border, rank, and subtraction semantics. | +| **EXECUTABLE_EXTERNAL_REFERENCE** | Normal and ASan Gwyddion 2.71 probe outputs. | +| **INDEPENDENT_ORACLE_CONFIRMED** | Independent NumPy oracle matches external arrays. | +| **FREEZE_AUDIT_APPROVED** | Audit approval within the frozen domain. | +| **FUTURE_IMPLEMENTATION_REQUIREMENT** | Requirement for later work, not delivered code. | +| **NON_CLAIM** | Boundary that must not be inferred from evidence. | +| **TOOLING_LIMITATION** | Non-blocking campaign tooling observation. | + +## 15. Explicit non-claims + +**NON_CLAIM:** the fixture does not establish behavior for matrices, radii, input +families, or Gwyddion paths outside the frozen campaign. It does not by itself establish +the behavior of a future SPMKit implementation or every Gwyddion feature. + +## 16. Known tooling limitations + +**TOOLING_LIMITATION:** runner compilation commands use `|| true`, so their stored status +does not preserve the original compiler exit. Its auxiliary parser also recognizes broad +`background_` and `corrected_` prefixes. These do not invalidate the campaign: both +binaries executed, all 72 processes returned, outputs were valid, stderr was empty, +normal and ASan stdout were byte-identical, and arrays and metrics were recalculated +independently. + +## 17. Scientific-integrity rule + +Tests judge algorithms against valid evidence; algorithms must not be deformed +merely to make tests pass. + +Any discrepancy shall be adjudicated before production changes. Acceptance shall not be +relaxed for convenience. Deliberate divergences and reference defects, if evidenced, +shall be preserved explicitly. Existing generic or physical median operations shall +remain separate from this compatibility operation. + +## 18. Required implementation workflow + +1. Preserve this fixture and manifest unchanged before implementation. +2. Compare candidate background and corrected fields against the fixture bitwise. +3. Check dtype, shape, C-order, finiteness, input immutability, and reconstruction. +4. Adjudicate a mismatch against source, probe, oracle, and fixture evidence before + modifying production behavior. + +## 19. Artifact inventory + +The manifest records the frozen module, probe source, runner, campaign summary, oracle +script, summary, source NPZ, provenance, report, log, and permanent NPZ fixture. `/tmp` +paths are ephemeral source artifacts whose identity is frozen by SHA-256. + +## 20. Freeze decision + +**FREEZE_AUDIT_APPROVED:** artifacts are suitable as external evidence and an independent +oracle within the explicit domain of this campaign. diff --git a/docs/design/GWYDDION_PATH_LEVEL_COMPATIBILITY.md b/docs/design/GWYDDION_PATH_LEVEL_COMPATIBILITY.md new file mode 100644 index 0000000..7d9a78e --- /dev/null +++ b/docs/design/GWYDDION_PATH_LEVEL_COMPATIBILITY.md @@ -0,0 +1,66 @@ +# Gwyddion Path Level Compatibility + +## Status and scope + +`gwyddion_path_level` records the registered Gwyddion `pathlevel` **Path +Level** tool within the frozen, finite, non-empty, full-field campaign. The +reference is the installed `tools.so` module, SHA-256 +`4711c360dd42e3e16257bf0e86d8bd41852b43d1d34540bf097736a603146237`, +Build ID `600b16d9857946609b567704b406abcc74aea698`, whose debug source matches +the frozen `pathlevel.c` SHA-256 +`4c0411c73f7ca883d4d03f35b38ef81a02ea3c7688620754992cb58e8825326f`. + +The evidence consists of 18 field/selection families, four thicknesses, 72 +logical cases, 144 fresh external executions, and an independent Python oracle +with 72/72 bitwise-exact arrays. This is not a universal-equivalence claim. + +## Selection identity and coordinates + +Path Level consumes an ordered collection of straight `GwySelectionLine` +objects, not `GwySelectionPath`. `GwySelectionPath` belongs to unrelated +path/spline tools. Every line is `(x0, y0, x1, y1)` in physical data-field +coordinates. The tool maps horizontal coordinates as `x*xres/xreal` and +vertical coordinates as `y*yres/yreal`; field origin offsets do not participate. + +Each endpoint is floored. If the first Y is greater than the second Y, both +endpoints are swapped. X endpoints and Y bounds are clamped to the field. +The active transition domain is `y0 < row <= y1`; consequently horizontal +lines are excluded. + +## Numerical contract + +For each ordered line object, the tool creates a start and an end change point. +They are ordered by row, then starts before ends, then object ID. This makes +line IDs and user-supplied object order scientifically relevant. Duplicates +and overlap retain multiplicity. + +For an active line, the column at each row transition uses the source integer +formula with C signed integer division truncated toward zero. A thickness +window is inclusive and asymmetric: `(thickness - 1)//2` samples on the lower +column side and `thickness//2` on the upper side, clamped to valid columns. +Its range is 1..128; the future public default is 1. + +Row differences are accumulated as explicit scalar `current - previous` +samples in line-object and increasing-column order, then divided once by the +sample count. The per-row differences are cumulatively summed left to right. +That correction is subtracted from every column of its row. There is no +interpolation, mask, ROI, path, spline, or profile operation in this contract. + +## Publication semantics + +Gwyddion mutates and publishes the selected data field in place, with undo and +tool logging. The future SPMKit kernel returns a new corrected array and does +not mutate its input. This intentionally follows SPMKit immutable-return +convention; it does not claim GUI, undo, logging, or publication parity. + +## Evidence and non-claims + +The frozen fixture preserves external input/output records, independently +regenerated inputs/cases, oracle comparison records, physical ranges, ordered +lines, normalized endpoints, and bitwise uint64 hashes. It covers signed zero, +line-order sensitivity, overlap, clamping, fractional coordinates, and all +four frozen thicknesses. + +No claim is made for NaN or infinity, masks or ROI, spline/polyline paths, +profile extraction, volume line-leveling, `align_rows` equivalence, performance +parity, other Gwyddion builds or versions, or universal equivalence. diff --git a/docs/design/GWYDDION_SPHERE_REVOLUTION_COMPATIBILITY.md b/docs/design/GWYDDION_SPHERE_REVOLUTION_COMPATIBILITY.md new file mode 100644 index 0000000..08d4ed6 --- /dev/null +++ b/docs/design/GWYDDION_SPHERE_REVOLUTION_COMPATIBILITY.md @@ -0,0 +1,486 @@ +# Gwyddion 2.71 Sphere Revolution Compatibility Specification + +## 1. Status and scope + +This document establishes the normative design specification for SPMKit's compatibility with Gwyddion 2.71's 2D Revolve Sphere background leveling operation. + +Current Status: Normative Design Specification (Implementation Pending). + +The primary objective is to reproduce the exact, observable numerical semantics of Gwyddion 2.71's ``sphere-revolve`` module within SPMKit. + +Scope boundaries: +- Direct external reference scope (direct external reference): The normal route (`inverted=False`). +- Derived external reference scope (derived external reference): Inverted background evaluated via the exact mathematical dual `-B(-F)` using normal reference executions on negated inputs. +- Safe deliberate divergence scope (safe deliberate divergence): Inverted corrected field evaluated as `F - B_inv(F)` to guarantee complete reconstruction identity without inheriting upstream memory corruption bugs. +- Universal equivalence is explicitly excluded. Parity claims apply strictly to the frozen test suite and external validation fixtures. + +| Capability | Evidence class | Planned SPMKit behavior | +|---|---|---| +| Normal background | Direct external reference | Reproduce Gwyddion 2.71 numerical outputs | +| Normal corrected | Direct external reference | Reproduce Gwyddion 2.71 numerical outputs | +| Inverted reference wrapper | Frozen reference defect | Excluded due to upstream memory corruption crash | +| Inverted background | Derived external reference | Reproduce dual `-B_normal(-F)` using negated inputs | +| Inverted corrected | Safe deliberate divergence | Evaluate `F - B_inv(F)` to ensure input reconstruction | +| Physical Sphere Revolution | Independent physical model | Preserved completely intact without modification | + +## 2. Separation from physical Sphere Revolution + +SPMKit currently provides `estimate_sphere_revolution_background`, which models a physical 2D spherical contact tip over real surface topographies using physical SI units (metres), anisotropic lateral pixel dimensions (`dx`, `dy`), morphological opening operations, and physical border modes (`nearest`, `reflect`). + +The Gwyddion 2.71 compatibility operation defined herein uses dimensionless pixel-sample radii (`radius_px`), data-adaptive RMS scaling (`q`), and historical C-array index truncation. + +> The two operations remain separate because their parameter semantics, geometry, scaling, and evidence contracts are not equivalent. + +The existing physical Sphere Revolution implementation (`estimate_sphere_revolution_background`, `remove_sphere_revolution_background`, `analyze_sphere_revolution_background`) and its test suite shall remain completely intact and unmodified. + +## 3. Proposed public API + +The public API for Gwyddion Sphere Revolution compatibility shall provide three functions in `spmkit.core.analysis.background`: + +```python +estimate_gwyddion_sphere_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + inverted: bool = False, +) -> SPMChannel +``` + +```python +remove_gwyddion_sphere_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + inverted: bool = False, +) -> SPMChannel +``` + +```python +analyze_gwyddion_sphere_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + inverted: bool = False, +) -> BackgroundResult +``` + +Method string string: + +`gwyddion_sphere_revolution` + +Parameters dictionary: + +```python +{ + "radius_px": float(radius_px), + "inverted": bool(inverted), +} +``` + +Public imports will be exported in `spmkit.core.analysis` and `spmkit` upon completion of Block D of the implementation sequence. + +## 4. Input and parameter contract + +The input channel and parameters must adhere to the following contract: +- Input channel data must be a two-dimensional, finite, real, non-empty `float64` array. +- All internal numerical calculations must use IEEE-754 `numpy.float64` double precision. +- `radius_px` must be a real, finite scalar in the inclusive range `1.0 <= radius_px <= 1000.0`. +- Boolean values passed as `radius_px` must be rejected with `TypeError`, matching SPMKit's `_validated_gwyddion_radius_px` contract. +- `inverted` must be a boolean. Non-boolean values (e.g. integers or strings) must be rejected with `TypeError`. +- `radius_px` represents a sample count along pixel grid axes and is completely independent of physical lateral metadata (`xreal`, `yreal`, `dx`, `dy`). +- Input channels and arrays must never be mutated in place. +- Private array outputs must be C-contiguous `numpy.float64` arrays with `flags.writeable = False`. +- Public SPMChannel metadata, Z units, and spatial context must be preserved using `channel.with_data(...)`. + +## 5. Global normalization + +Let $F$ be a 2D data field of dimensions $y_{\mathrm{res}} \times x_{\mathrm{res}}$ containing $N = y_{\mathrm{res}} \cdot x_{\mathrm{res}}$ samples. + +Global mean $\bar F$ is calculated using a serial C-order sum over double-precision values: + +$$ +\bar F = \frac{1}{N} \sum_{p=0}^{N-1} F_p +$$ + +Global population RMS is calculated in a second serial C-order pass: + +$$ +\operatorname{RMS}(F) = \sqrt{\frac{1}{N} \sum_{p=0}^{N-1} (F_p - \bar F)^2} +$$ + +The global scaling parameter $q$ is defined as: + +$$ +q = \frac{\operatorname{RMS}(F)}{\sqrt{5/6}} +$$ + +Key requirements: +- The divisor $N$ uses the full population count. +- Calculation requires two explicit serial passes to preserve exact accumulation order. +- A constant input field yields $\operatorname{RMS}(F) = 0.0$ and $q = 0.0$. +- `np.std()` must not be used as the normative definition due to variance in accumulation order. + +## 6. Discrete sphere construction + +The integer sphere radius $s$ in samples, kernel size $n$, and local filter half-width $k$ are derived via `GWY_ROUND` (`floor(val + 0.5)`): + +$$ +s = \left\lfloor \min(r, x_{\mathrm{res}}) + 0.5 \right\rfloor +$$ + +$$ +n = 2s + 1 +$$ + +$$ +k = \left\lfloor \frac{s}{2} \right\rfloor +$$ + +For indices $i, j \in [0, s]$, normalized coordinate offsets are defined as: + +$$ +u = \frac{i}{r}, \qquad v = \frac{j}{r}, \qquad \rho^2 = u^2 + v^2 +$$ + +The dimensionless sphere height $z$ is evaluated via the normal branch when $r / 8 \le x_{\mathrm{res}}$: + +$$ +z = \begin{cases} 1 - \sqrt{1 - \rho^2}, & \rho^2 \le 1 \\ 2, & \rho^2 > 1 \end{cases} +$$ + +When $r / 8 > x_{\mathrm{res}}$, the very-flat branch polynomial is evaluated: + +$$ +z = \frac{\rho^2}{2} \left[ 1 + \frac{\rho^2}{4} \left( 1 + \frac{\rho^2}{2} \right) \right] +$$ + +The scaled sphere kernel $S$ is obtained by quadrant-symmetric assignment and scaling: + +$$ +S = -q z +$$ + +Key requirements: +- Quadrant symmetry assigns identical $z$ to $(s-i, s-j)$, $(s-i, s+j)$, $(s+i, s-j)$, and $(s+i, s+j)$. +- The parameter $x_{\mathrm{res}}$ is the sole dimension passed to Gwyddion's `make_sphere`. +- On non-square rectangular grids ($x_{\mathrm{res}} \ne y_{\mathrm{res}}$), this creates an intentional asymmetry under matrix transposition. +- This historical asymmetry is required for exact Gwyddion 2.71 compatibility and must not be "corrected" by using $\min(x_{\mathrm{res}}, y_{\mathrm{res}})$. + +## 7. Local mean and RMS semantics + +For filter size $k = s // 2 > 0$, local moving windows use asymmetric negative and positive extensions: + +$$ +k_- = (k - 1) // 2, \qquad k_+ = k // 2 +$$ + +For each pixel $(r, c)$, the window bounds are truncated at image borders: + +$$ +r_{\mathrm{start}} = \max(0, r - k_-), \qquad r_{\mathrm{stop}} = \min(y_{\mathrm{res}} - 1, r + k_+) +$$ + +$$ +c_{\mathrm{start}} = \max(0, c - k_-), \qquad c_{\mathrm{stop}} = \min(x_{\mathrm{res}} - 1, c + k_+) +$$ + +Properties: +- Truncated window support at boundaries without zero-padding. +- Window sums are normalized by the effective pixel count in the window. +- Odd $k$ yields a symmetric centered window; even $k$ places one extra sample to the right and bottom. + +Local mean $\mu_{\mathrm{local}}$ is the window arithmetic mean. + +Local RMS $\sigma_{\mathrm{local}}$ for $k > 1$ is calculated as: + +$$ +\sigma_{\mathrm{local}} = \sqrt{\max\left(E[F^2] - E[F]^2, 0\right)} +$$ + +Special filter size semantics: +- When $k = 0$: $\mu_{\mathrm{local}} = F$ (unfiltered copy) and $\sigma_{\mathrm{local}} = F$ (unfiltered copy). +- When $k = 1$: $\mu_{\mathrm{local}} = F$ (unfiltered copy) and $\sigma_{\mathrm{local}} = 0.0$. +- SPMKit reproduces these exact numerical outputs. +- SPMKit does not emit Gwyddion's `GwyProcess-CRITICAL` GLib diagnostic warnings. + +Note: The independent Python oracle uses direct window summation loops, while Gwyddion uses 1D rolling sums. Both yield identical floating-point results within sub-ULP rounding tolerances. + +## 8. Outlier-trimmed field + +The outlier-trimmed field $T$ is computed element-by-element as: + +$$ +T = \max\left(F, \mu_{\mathrm{local}} - 2.5 \sigma_{\mathrm{local}}\right) +$$ + +For $k = 0$, where $\mu_{\mathrm{local}} = F$ and $\sigma_{\mathrm{local}} = F$, this simplifies to: + +$$ +T = \max(F, -1.5 F) +$$ + +$T$ represents an intermediate trimmed field and is not the final background. + +## 9. Two-dimensional envelope + +The background $B_{ij}$ at pixel $(i, j)$ is extracted as the lower envelope of $T$ relative to the scaled sphere kernel $S$: + +$$ +B_{ij} = \min_{\substack{a \in [-s, s], b \in [-s, s] \\ (i+a, j+b) \text{ valid} \\ S_{s+a, s+b} \ge -q}} \left[ T_{i+a, j+b} - S_{s+a, s+b} \right] +$$ + +Since $S = -q z$, this is equivalent to: + +$$ +B_{ij} = \min \left[ T_{i+a, j+b} + q z_{s+a, s+b} \right] +$$ + +Key requirements: +- Full 2D minimization loop over valid kernel offsets. +- Support points with $S < -q$ are excluded from minimization. +- Truncated boundary support without padding. +- Normal corrected field: $C = F - B$. + +## 10. Radius-one historical semantics + +Executable probe evidence confirms: +- For $r = 1.0$, $s = 1$, $n = 3$, and $k = 0$. +- Gwyddion 2.71 emits two GLib critical diagnostic warnings (`size > 0` assertion failures) because $k = 0$. +- The Gwyddion execution continues normally, returning exit code 0, finite outputs, and exact reconstruction. +- SPMKit preserves the exact numerical output ($T = \max(F, -1.5F)$ passed through the 2D envelope) while executing cleanly without diagnostics. + +## 11. Constant-field q=0 semantics + +For constant input fields ($F_{ij} = c$): +- Global RMS is $0.0$, yielding $q = 0.0$. +- The scaled sphere kernel $S$ consists entirely of zeros. +- The condition $S \ge -q$ ($0.0 \ge 0.0$) holds for all kernel positions. +- Background $B_{ij} = c$ and corrected $C_{ij} = 0.0$. +- All outputs are finite and exhibit exact zero error. + +## 12. Inverted reference execution defect + +### EXECUTABLE_CONFIRMED_REFERENCE_DEFECT + +Executable campaign evidence across 15 inverted reference cases confirms: +- In Gwyddion 2.71's `sphere-revolve.c`, when `inverted=TRUE`, line 320 executes `gwy_object_unref(field); field = invfield;`. +- At line 328, `gwy_data_field_subtract_fields(args->field, field)` uses `args->field` which was not reassigned to `invfield`. +- 15 out of 15 inverted normal cases crashed with exit code 139 (SIGSEGV). +- 15 out of 15 inverted ASan cases crashed with exit code 134 (SIGABRT). +- Probe output recorded `execute_started=1` but `execute_returned=0`. +- The crash occurs inside `gwy_data_field_check_compatibility()` due to a read of unallocated memory during final subtraction. +- ASan output did not emit the literal string `heap-use-after-free`, so that specific string is not used as an exact diagnostic tag. +- Gwyddion 2.71 provides no valid reference output for `inverted=TRUE`. +- The reference `inverted=TRUE` C wrapper cannot serve as a valid numerical oracle. + +## 13. Safe inverted semantics in SPMKit + +To provide mathematically sound inversion without inheriting reference crashes, SPMKit defines safe inverted semantics: + +Inverted Background Dual: + +$$ +B_{\mathrm{inv}}(F) = -B_{\mathrm{normal}}(-F) +$$ + +Inverted Corrected Field: + +$$ +C_{\mathrm{inv}}(F) = F - B_{\mathrm{inv}}(F) +$$ + +Evidence hierarchy: +- Direct external reference: Normal route (`inverted=False`). +- Derived external reference: Inverted background $B_{\mathrm{inv}}(F) = -B_{\mathrm{normal}}(-F)$, verified by running Gwyddion's normal C kernel on 10 explicitly negated inputs (`input_negation_max_abs = 0.0`, $q$ difference $= 0.0$, dual reconstruction max abs error $= 8.88 \times 10^{-16}$). +- Safe deliberate divergence: Inverted corrected field $C_{\mathrm{inv}}(F) = F - B_{\mathrm{inv}}(F)$, ensuring exact reconstruction identity without undefined behavior. + +## 14. Independent oracle evidence + +An independent Python oracle (`sphere_revolution_oracle.py`, frozen by its recorded SHA-256) evaluated 20 valid cases (10 original normal, 10 negated normal): +- Implemented in pure Python 3 and NumPy without SciPy or SPMKit imports. +- Evaluated using direct 2D window loops. +- Max $q$ absolute error: `0.0`. +- Max background absolute error: `4.4408920985006262e-16`. +- Max background ULP error: 2 ULP. +- Max corrected absolute error: `8.8817841970012523e-16`. +- Max reconstruction error: `8.8817841970012523e-16`. +- All outputs 100% finite. +- Large raw corrected ULP (`4377498837804122113`) resulted from comparing `-4.44e-16` against positive zero `0.0`. +- Raw ULP near zero is not an acceptance criterion. + +## 15. Acceptance criteria + +Numerical acceptance criteria for external validation fixtures: + +```text +background_max_abs_error = 5e-14 +corrected_max_abs_error = 5e-14 +reconstruction_max_abs_error = 5e-14 +rtol = 0 +``` + +Explanation: +- Provides a safety margin above the observed maximum numerical discrepancy (`8.88e-16`). +- Matches the tolerance scale established for Gwyddion Revolve Arc compatibility. +- Applies absolute comparison (`atol = 5e-14`, `rtol = 0`). +- ULP distance is logged as a diagnostic metric only. +- Applies to frozen external validation fixtures and does not constitute universal equivalence. + +## 16. Planned implementation architecture + +Implementation will add one private module and modify two existing files: +- New file: `src/spmkit/core/analysis/_gwyddion_sphere_revolution.py` +- Modify: `src/spmkit/core/analysis/background.py` +- Modify: `src/spmkit/core/analysis/__init__.py` + +Private API signatures in `_gwyddion_sphere_revolution.py`: + +```python +_gwyddion_sphere_background( + data: FloatArray, + radius: float, +) -> FloatArray +``` + +```python +_gwyddion_sphere_result( + data: FloatArray, + radius: float, + *, + inverted: bool = False, +) -> tuple[FloatArray, FloatArray] +``` + +```python +_gwyddion_sphere_corrected( + data: FloatArray, + radius: float, + *, + inverted: bool = False, +) -> FloatArray +``` + +Architecture rules: +- `_gwyddion_sphere_background` computes authoritative normal background. +- `_gwyddion_sphere_result` centralizes inversion dual and corrected calculation. +- `_gwyddion_sphere_corrected` delegates to `_gwyddion_sphere_result`. +- Public adapters delegate to `_gwyddion_sphere_result`. +- No duplication of algorithm logic. +- No sharing of private helper functions with Arc Revolution in this phase. +- Existing physical Sphere Revolution code remains completely untouched. + +## 17. Required tests + +Required test matrix: + +### Private/core tests (`tests/core/test_gwyddion_sphere_revolution_background.py`) +- `test_default_radius_is_20` +- `test_radius_boundary_1_accepted` +- `test_radius_boundary_1000_accepted` +- `test_invalid_nonfinite_radius_rejected` +- `test_invalid_range_radius_rejected` +- `test_bool_radius_rejected` +- `test_float64_conversion` +- `test_c_contiguous_output` +- `test_readonly_array_output` +- `test_input_array_not_mutated` +- `test_constant_field_zero_corrected` +- `test_radius_one_semantics` +- `test_very_flat_branch_execution` +- `test_rectangular_asymmetry` +- `test_safe_inversion_dual_identity` +- `test_reconstruction_identity` +- `test_private_result_agreement` + +### Public adapter tests (`tests/core/test_gwyddion_sphere_revolution_background.py`) +- `test_estimate_delegates_correctly` +- `test_remove_delegates_correctly` +- `test_analyze_returns_background_result` +- `test_method_string_is_gwyddion_sphere_revolution` +- `test_parameters_dict_contents` +- `test_channel_context_preservation` +- `test_physical_sphere_api_unchanged` +- `test_public_private_numerical_agreement` + +### External validation tests (`tests/validation/test_sphere_revolution_vs_gwyddion.py`) +- `test_gwyddion_sphere_direct_normal_background_matches_gwyddion_2_71` +- `test_gwyddion_sphere_direct_normal_corrected_matches_gwyddion_2_71` +- `test_gwyddion_sphere_negated_normal_background_matches_gwyddion_2_71` +- `test_gwyddion_sphere_derived_inverted_background_matches_gwyddion_2_71` +- `test_gwyddion_sphere_safe_inverted_corrected_matches_gwyddion_2_71` +- `test_gwyddion_sphere_reconstruction_identity` +- `test_gwyddion_sphere_reference_inverted_failure_evidence_is_documented` +- `test_gwyddion_sphere_public_result_matches_gwyddion_2_71` +- `test_gwyddion_sphere_fixture_hashes_are_stable` + +## 18. Fixture and provenance requirements + +Fixture location: +`tests/validation/fixtures/gwyddion/sphere_revolution/` + +Artifacts: +- `gwyddion_2_71_sphere.npz` (uncompressed NPZ containing input, reference background, reference corrected, and derived arrays). +- `gwyddion_2_71_sphere.json` (JSON metadata with SHA-256 hashes, canonical array hashes, source/probe/runner/oracle hashes, case metadata, acceptance tolerances, and reference execution status). + +Requirements: +- Distinguish direct normal cases, negated-normal cases, derived inverted arrays, and failed original inverted executions. +- Do not store dummy or synthetic arrays for crashing reference cases. + +## 19. Scientific claim boundaries + +Claim status before implementation: +- Design specified and normative contract established. +- External reference and independent oracle characterized. +- SPMKit implementation pending. + +Claim status after implementation and validation: +- Normal route: Level 3 CROSS_VALIDATED within external fixture. +- Inverted background: Derived external cross-validation (`-B_normal(-F)`). +- Inverted corrected: Safe deliberate divergence (`F - B_inv(F)`). +- Universal equivalence across arbitrary inputs or platforms is not claimed. +- Physical Sphere Revolution maintains its independent maturity and physical claims. + +## 20. Explicit non-goals + +The following non-goals are explicitly established: +- Do not replace or modify physical Sphere Revolution. +- Do not modify or patch upstream Gwyddion 2.71 C source code. +- Do not reproduce upstream C memory corruption crashes. +- Do not reproduce GLib warning messages. +- Do not use physical units (metres, nanometres) in Gwyddion compatibility APIs. +- Do not add mask support in this phase. +- Do not add border padding or extension policies. +- Do not add direction parameters (`direction` is for Arc, not Sphere). +- Do not expand radius range beyond `1.0 <= radius_px <= 1000.0`. +- Do not perform premature performance optimization before closing parity. +- Do not parallelize execution loops. +- Do not refactor Arc Revolution implementation. +- Do not claim universal numerical equivalence beyond frozen fixtures. + +## 21. Provenance ledger + +| Artifact | SHA-256 | Role | +|---|---|---| +| `sphere-revolve.c` | `4218cd4e303634c610e9be5f18656d12715c68df95a9b30930b33232b3d8cbe9` | Gwyddion 2.71 reference C module source | +| `sphere_revolve_behavior_probe.c` | `97248b51df742937ed5dc0a975b8b1ca08b1b6eeb5add95eda4526118337b188` | C probe source (schema_version 2, 35 cases) | +| `run_sphere_probe_campaign.sh` | `d673393126833277bda41c77403f1dbaf5dc965d6d8b63ee73994238bec8f7a7` | Campaign runner script v2 | +| `sphere_oracle.py` | `f1598e5f7cd0e173ec72ea928e270038ac8ab5f4c61d57b31a60373e50d40e4b` | Independent Python oracle script | +| `precision_audit.py` | `58f1acd0d3c3d644c93adcc7c754889e883726976341695e974d4c09a34f72e4` | Floating-point precision audit script | +| `implementation_dossier.md` | `fd9e83b28027a130025528130f96d3e4f4f03bdc792830666861ec335efef3a2` | Implementation dossier report | + +## 22. Implementation sequence + +Execution order: +1. Block A: Create normative specification `docs/design/GWYDDION_SPHERE_REVOLUTION_COMPATIBILITY.md` (completed in this step). +2. Block B: Implement private numerical kernel `src/spmkit/core/analysis/_gwyddion_sphere_revolution.py`. +3. Block C: Implement private unit tests `tests/core/test_gwyddion_sphere_revolution_background.py`. +4. Block D: Implement public adapters in `src/spmkit/core/analysis/background.py` and `__init__.py`. +5. Block E: Implement public API unit tests in `tests/core/test_gwyddion_sphere_revolution_background.py`. +6. Block F: Create frozen external validation fixtures in `tests/validation/fixtures/gwyddion/sphere_revolution/`. +7. Block G: Implement external validation tests in `tests/validation/test_sphere_revolution_vs_gwyddion.py`. +8. Block H: Update documentation files (`docs/api.md`, `docs/scientific-status.md`, `docs/validation/index.md`). +9. Block I: Run focal regression test suite and static type checking. +10. Block J: Run global regression test suite (`pytest`). +11. Block K: Atomic git commit and push. + +Each block must be verified before proceeding to the next block. diff --git a/docs/manual/artifacts-manifest.json b/docs/manual/artifacts-manifest.json index 7400d4b..96ae5e7 100644 --- a/docs/manual/artifacts-manifest.json +++ b/docs/manual/artifacts-manifest.json @@ -1,14 +1,14 @@ { "artifacts": [ { - "bytes": 56415, + "bytes": 56482, "path": "docs/user-guide.md", - "sha256": "dbbfa1fddffd5f25b3b880c80b33f6b8b1e37b0c15dd85af25c7aacef9f8fe08" + "sha256": "2af676b1a962202ec7c5424a95d7477a6b99621c88ad433720cff46b2e117a43" }, { - "bytes": 39873, + "bytes": 39958, "path": "docs/user-guide.tex", - "sha256": "d11a27d1889510c6beaed2f1055216b54366e737cbfbfc4bbbaaa5e6617bd6db" + "sha256": "9a8de11a62874301eee6a7797149146f5fe8057374b7c8008c1463da1e1e0314" }, { "bytes": 117821, diff --git a/docs/scientific-status.md b/docs/scientific-status.md index e401076..e6694bc 100644 --- a/docs/scientific-status.md +++ b/docs/scientific-status.md @@ -35,6 +35,15 @@ and tolerance. It never transfers automatically to an adjacent feature. | Sa, Sq, Sz on public experimental GWY matrices | `core.analysis.roughness` | 12 cases, 36/36 shared-matrix comparisons within tolerance | CROSS_VALIDATED for the shared-matrix algorithm track | Gwyddion 2.71 | Parser/end-to-end observations are separate: 10 equivalences and 2 preserved channel-count differences | | Limited Nanoscope III `.spm` images | `core.io.bruker_spm` | Six demonstrated files; 18/18 Sa/Sq/Sz comparisons within tolerance and zero reported pixel delta | NUMERICALLY_VERIFIED | Gwyddion 2.71 | `ACCIDENTAL_PRE_FREEZE_UNBLINDING`; partial variants only, no blind holdout or general Bruker support | | NanoSurf `.nid` mapping and orientation | `core.io.nid`, `core.verify` | Synthetic byte-budget/orientation tests and selected lab-context comparisons | SOFTWARE_VERIFIED; selected comparisons do not establish universal format coverage | Gwyddion exports for selected files | Private instrument corpus is not distributed; additional redistributable multi-instrument fixtures are needed | +| Gwyddion Flatten Base end-to-end trajectory | `core.analysis._flatten_base` | Focal LM and packed-Cholesky verification plus a frozen Gwyddion 2.71 end-to-end fixture; matching facet/polynomial control flow and corrected-field maximum absolute difference `1.465494e-14` | CROSS_VALIDATED | Gwyddion 2.71 executable | Internal Core path and one frozen end-to-end trajectory plus focused numerical cases; no universal equivalence claim across datasets, parameter regimes, platforms or Gwyddion versions | +| Physical arc-revolution background | `core.analysis.background` | 55 unit and synthetic tests, including a test-local brute-force 1D oracle, inversion duality, physical-unit equivalence, anisotropic spacing, border policies and reconstruction identity | SOFTWARE_VERIFIED | None | Python API only; finite geometric Z data; no masks, Gwyddion equivalence or physical-reference campaign | +| Gwyddion-compatible Revolve Arc background | `core.analysis.background`, `core.analysis._gwyddion_arc_revolution` | Frozen Gwyddion 2.71 source semantics, focal kernel probes, one asymmetric 5×7 directional fixture, 6/6 background routes and 5/5 valid corrected routes within `5e-14`; repaired reconstruction for the defective horizontal-inverted wrapper | CROSS_VALIDATED for the frozen campaign | Gwyddion 2.71 source, compiled probes and frozen JSON/NPZ fixture | Radius is in samples; no masks or non-finite data; one-sample processing axes use a documented safe definition; no physical validation, tip reconstruction, performance equivalence or universal-equivalence claim | +| Physical sphere-revolution background | `core.analysis.background` | 51 unit and synthetic tests, including independent brute-force 2D oracles for nearest and reflect borders, physical anisotropy, non-separability, unit equivalence and reconstruction identity | SOFTWARE_VERIFIED | None | Python API only; finite geometric Z data; no masks, Gwyddion equivalence, performance campaign or physical-reference campaign | +| Gwyddion-compatible Sphere-revolution background | `core.analysis.background`, `core.analysis._gwyddion_sphere_revolution` | Frozen Gwyddion 2.71 source semantics, focal probes, 10 original surfaces, 10 normal executions on negated inputs (20 valid external runs per build), 15/15 inverted runs failing in normal build and under ASan; direct external reference for normal, derived external cross-validation for inverted background, safe deliberate divergence for inverted corrected (`atol=5e-14`, `rtol=0.0`); independent Python oracle | CROSS_VALIDATED for the frozen campaign | Gwyddion 2.71 source, compiled probes, independent Python oracle and frozen JSON/NPZ fixture | Radius is in samples; no non-finite data or masks; inverted corrected does not claim equivalence with Gwyddion's crashing wrapper; no physical validation, tip deconvolution or universal-equivalence claim; physical sphere-revolution maintains its independent software verification | +| Gwyddion 2.71 Median Background | `core.analysis.background`, `core.analysis._median_background` | Frozen executable reference campaign: 36 logical cases, 72 executions (36 normal, 36 ASan), radii 1/2/3/4/20/1024, direct and radixtree reference paths; public background and corrected fields 36/36 bitwise exact, maximum absolute difference 0 and maximum ULP 0; input mutation maximum 0 and reconstruction maximum `4.4408920985006262e-16` | CROSS_VALIDATED within the frozen 36-case campaign | Gwyddion 2.71 source, executable probe, independent Python oracle, frozen NPZ/JSON fixture | Finite two-dimensional inputs only; no universal equivalence, performance-equivalence, future-Gwyddion, all-radii, or all-matrices claim; `rank_backend_reference` describes Gwyddion, not an SPM-Kit backend | +| Gwyddion 2.71 Filter flat-disc morphology | `core.analysis.background`, `core.analysis._gwyddion_flat_disc_morphology` | Frozen executable reference campaign: 12 fields, six sizes 2/3/4/5/30/31, 72 Opening and 72 Closing cases; kernels 30/30, Opening 72/72 and Closing 72/72 bitwise exact; maximum absolute difference 0, maximum ULP 0, signed-zero mismatches 0, input mutation 0 | CROSS_VALIDATED within the frozen campaign | Audited Gwyddion 2.71 executable, corrected external probe V3, executable reduction trace, independent oracle V2, frozen NPZ/JSON fixture | Finite full-field data with masks ignored; no universal equivalence, NaN/Inf, ROI, masks, ASF, tip morphology, physical rolling-ball, performance, other builds/versions, public erosion/dilation, or source-only tie claim | +| Gwyddion 2.71 Path Level | `core.analysis.leveling`, `core.analysis._gwyddion_path_level` | Audited executable campaign: 18 base families, thicknesses 1/2/3/128, 72 logical cases, 144 fresh external executions and 72 deterministic repeat pairs; private and public arrays 72/72 bitwise exact, 4,652/4,652 elements exact, max absolute/ULP 0, signed-zero mismatches 0, normalized endpoints and mutation/no-op classifications 72/72 | CROSS_VALIDATED within the frozen campaign | Audited Gwyddion 2.71 Path Level tool, external probe, independent oracle V1, frozen NPZ/JSON fixture | Finite non-empty full fields and ordered straight selections only; no universal equivalence, NaN/Inf, masks/ROI, paths/splines, profiles, align-rows, volume, GUI, performance, or other-build/version claim | +| Gwyddion 2.71 Align Rows statistics | `core.analysis.leveling`, `core.analysis._gwyddion_align_rows_statistics` | Public 64-case finite campaign: portable source semantics 64/64 arrays and 3,888/3,888 elements bitwise exact; installed fast-math profile 61/64 arrays and 3,757/3,888 elements exact, with only three signed-zero and 128 independently explained reassociation differences | CROSS_VALIDATED within the frozen dual-profile campaign | Gwyddion 2.71 source, external executable probe, independent portable V2 oracle, frozen NPZ/JSON fixture, installed-build diagnosis | Four methods only; finite full fields, frozen masks/directions/trims; no universal, non-finite, performance, other-version/build, GUI, or generic-`align_rows` compatibility claim | | Hertz / conical contact and DMT paths | `core.analysis.forcecurve` | Unit and synthetic-recovery tests; Hertz/conical modulus recovery gates | NUMERICALLY_VERIFIED within synthetic test scope | Analytical construction | No certified cantilever/tip calibration or broad experimental campaign | | Adhesive JKR | `core.analysis.experimental` | Synthetic recovery of reduced modulus and work of adhesion; Hertz-limit test | NUMERICALLY_VERIFIED within synthetic scope | Analytical construction | Experimental module; no physical-reference campaign | | WLC and FJC chain models | `core.analysis.chain` | Analytical synthetic-recovery tests | NUMERICALLY_VERIFIED within synthetic scope | Analytical construction | No cross-software or experimental population campaign | @@ -52,6 +61,196 @@ and tolerance. It never transfers automatically to an adjacent feature. - [Public experimental GWY pilot summary](https://github.com/kegouro/spmkit-validation/blob/main/evidence/campaigns/real_data_roughness_pilot_v0.1_summary.json) - [Nanoscope `.spm` pilot summary](https://github.com/kegouro/spmkit-validation/blob/main/evidence/campaigns/nanoscope_spm_parser_pilot_v0.1_summary.json) - [Nanoscope incident and final audit](https://github.com/kegouro/spmkit-validation/blob/main/docs/campaigns/nanoscope_spm_parser_pilot_v0.1_audit.md) +- [Flatten Base Gwyddion 2.71 frozen end-to-end fixture](https://github.com/kegouro/spmkit/blob/flatten-base-gwyddion-parity-v1/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.json) +- [Sphere Revolution Gwyddion 2.71 frozen fixture](https://github.com/kegouro/spmkit/blob/feat/gwyddion-leveling-parity/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.json) +- [Median Background Gwyddion 2.71 frozen manifest](https://github.com/kegouro/spmkit/blob/main/tests/validation/fixtures/gwyddion/median_background/median_background_reference.json) + +### Gwyddion Sphere Revolution + +SPM-Kit's `gwyddion_sphere_revolution` implementation is maintained separately from physical sphere revolution. It reproduces Gwyddion 2.71's Revolve Sphere numerical semantics: +- **Campaign Scope:** 10 original surface matrices across WIDE_ASYMMETRIC, TALL_ASYMMETRIC, CONSTANT_ZERO_RMS, and SIGNED_MICRO_GRID families, plus 10 normal executions on explicitly negated inputs (20 valid external runs per build). +- **Inverted Route Failure Evidence:** 15/15 executions of `inverted=True` crash in Gwyddion 2.71's C module (exit code 139 in normal build, exit code 134 under ASan at `sphere-revolve.c:328 gwy_data_field_subtract_fields`). +- **Evidence Classes:** + - `normal` route: Direct external reference from Gwyddion 2.71 stdout. + - `inverted` background: Derived external cross-validation from `-B(-data)`. + - `inverted` corrected: Safe deliberate divergence (`original - background`) reconstructing original data without invoking Gwyddion's crashing subtract wrapper. +- **Independent Oracle & Tolerances:** Verified against an independent Python oracle (`atol=5e-14`, `rtol=0.0`). The maximum observed numerical discrepancy across all comparisons is `8.881784e-16` (well below `5e-14`). +- **Claim:** LEVEL 3 CROSS_VALIDATED within the frozen fixture scope. No universal equivalence or physical validation is claimed. Physical sphere revolution (`estimate_sphere_revolution_background`) maintains its independent software verification. + +### Gwyddion 2.71 Median Background + +**Claim:** `CROSS_VALIDATED` only for the frozen campaign. Gwyddion 2.71 is the executable +external reference; the campaign contains 36 logical cases and 72 executions (36 normal and +36 ASan) over radii 1, 2, 3, 4, 20, and 1024. Both Gwyddion reference paths are represented: +`direct` and `radixtree`. Public `estimate_gwyddion_median_background`, +`remove_gwyddion_median_background`, and `analyze_gwyddion_median_background` reproduce the +frozen background and corrected arrays bitwise in 36/36 cases: maximum absolute difference 0, +maximum ULP 0, input mutation maximum 0, and reconstruction maximum +`4.4408920985006262e-16`. + +The fixed semantics are the inclusive digital ellipse, `gwyddion_border_extend`, middle rank +`kernel_active_count//2`, and `corrected = input - background`. The public API preserves the +`SPMChannel` context and requires finite two-dimensional data. The private kernel is independent +of Gwyddion at runtime. The fixture and independent Python oracle remain frozen evidence outside +production. + +**Traceability:** + +```text +Gwyddion 2.71 source: modules/process/median-bg.c + → frozen external probe: median_background_behavior_probe.c + → frozen campaign runner: run_median_background_probe_campaign.sh + → independent Python oracle recorded by docs/design/GWYDDION_MEDIAN_BACKGROUND_COMPATIBILITY.md + → tests/validation/fixtures/gwyddion/median_background/median_background_reference.npz + → tests/validation/fixtures/gwyddion/median_background/median_background_reference.json + → src/spmkit/core/analysis/_median_background.py + → src/spmkit/core/analysis/background.py + → tests/core/test_gwyddion_median_background_private.py + → tests/core/test_gwyddion_median_background.py + → tests/validation/test_median_background_fixture_integrity.py + → docs/scientific-status.md +``` + +The evidence was frozen in `818dbd3` (freeze evidence), the private kernel in `a53c3bb`, and +the public API in `ed5c837`. The focal inventory is 20 fixture-integrity, 67 private Median +Background, and 72 public Median Background tests; the preceding combined focal run collected +442 tests. These are focal-campaign counts, not a project-wide total. + +**Non-claims:** no universal equivalence; no guarantee outside the 36 cases; no NaN or infinity +coverage and no reproduction of Gwyddion's internal radixtree; no performance-equivalence claim; +no claim for future Gwyddion versions; no claim for every radius or matrix; and no validation of +configurable border, shape, or rank parameters because the API exposes none. + +**Non-blocking tooling limitations:** the probe runner uses `|| true` during compilation and does +not retain the original compiler exit code; its auxiliary parser recognizes broad `background_` +and `corrected_` prefixes. The campaign remains valid because both binaries executed, 72 +processes returned, stderr was empty, normal and ASan stdout were byte-identical, outputs were +parsed and recalculated independently, oracle/reference results were bitwise exact, and the +fixture stores canonical hashes. + +### Gwyddion 2.71 Filter flat-disc morphology + +**Claim:** `CROSS_VALIDATED` only within the frozen executable campaign. The audited Gwyddion +2.71 Filter path is represented by 12 deterministic finite fields at sizes 2, 3, 4, 5, 30, +and 31. Opening and Closing each match the frozen external outputs bitwise in 72/72 cases; +kernels match 30/30, maximum absolute difference and ULP distance are both 0, signed-zero +mismatches are 0, and input mutation is 0. + +The fixed semantics are the K×K inclusive digital ellipse, nearest-edge extension, asymmetric +even-size anchors, and the audited executable Each/Even plus RLE reduction hierarchy. The +source strict ternaries and executable MINSD/MAXSD equality behavior are distinct; SPM-Kit +reproduces the audited executable path. The rejected uninitialised-kernel microprobe is not +evidence; the corrected zero-initialised probe is the valid external record. + +**Traceability:** + +```text +Gwyddion 2.71 source: libprocess/filters-minmax.c + → frozen external probe: flat_disc_probe_v3 + → frozen reduction trace: flat_disc_reduction_trace_v1 + → independent oracle: flat_disc_morphology_oracle.py + → tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.npz + → tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.json + → src/spmkit/core/analysis/_gwyddion_flat_disc_morphology.py + → src/spmkit/core/analysis/background.py + → tests/core/test_gwyddion_flat_disc_morphology_private.py + → tests/core/test_gwyddion_flat_disc_morphology.py + → docs/scientific-status.md +``` + +Evidence was frozen in `2ba366e`; the private kernel is `05c5ae4`; the public API and +documentation are recorded in `1b2d081` and `c0de811`. **Non-claims:** no universal equivalence; +no NaN or infinity coverage; +no ROI, masks, ASF, tip morphology, physical rolling-ball equivalence, performance parity, +other Gwyddion builds or versions, public erosion or dilation, or claim that source-level C +tie semantics alone reproduce the audited binary. + +### Gwyddion 2.71 Path Level + +**Claim:** `CROSS_VALIDATED` only within the frozen Path Level campaign against the audited +Gwyddion 2.71 tool module `tools.so` +(`4711c360dd42e3e16257bf0e86d8bd41852b43d1d34540bf097736a603146237`, Build ID +`600b16d9857946609b567704b406abcc74aea698`). The campaign contains 18 finite, non-empty, +full-field base families, thicknesses 1, 2, 3, and 128, 72 logical cases, 144 fresh external +executions, and 72/72 deterministic repeat pairs. Private and public `gwyddion_path_level` +arrays are bitwise exact in 72/72 cases and 4,652/4,652 elements: maximum absolute difference +0, maximum ULP 0, signed-zero mismatches 0, normalized endpoints 72/72, mutation/no-op +classification 72/72, and input mutation 0. + +The operation consumes ordered GwySelectionLine-equivalent straight physical-coordinate segments; +duplicates and object order are significant. Its fixed executable semantics include endpoint +conversion, horizontal-line exclusion, C truncating division, inclusive thickness windows, and +left-to-right cumulative row correction. Gwyddion mutates the selected data field in place and +performs GUI publication, undo, and logging; SPM-Kit returns a new `SPMChannel` and claims no +GUI-publication parity. + +**Traceability:** + +```text +Gwyddion 2.71 source: modules/tools/pathlevel.c + → installed Gwyddion 2.71 Path Level tool execution + → frozen external probe: path_level_probe_v1 + → independent oracle: path_level_oracle_v1 + → tests/validation/fixtures/gwyddion/path_level/path_level_reference.npz + → tests/validation/fixtures/gwyddion/path_level/path_level_reference.json + → src/spmkit/core/analysis/_gwyddion_path_level.py + → src/spmkit/core/analysis/leveling.py + → tests/core/test_gwyddion_path_level_private.py + → tests/core/test_gwyddion_path_level.py + → tests/validation/test_path_level_fixture_integrity.py + → docs/scientific-status.md +``` + +The evidence commit is `d3566ce`; the private-kernel commit is `4ead95b`. No future public or +documentation commit hash is claimed. **Non-claims:** no universal equivalence; no NaN or +infinity coverage; no masks or ROI; no GwySelectionPath, splines, or polylines; no profile +extraction, align-rows equivalence, volume line-leveling, GUI/undo/logging/selection-widget +parity, performance parity, or guarantee for other Gwyddion versions or builds. + +### Gwyddion 2.71 Align Rows statistics + +**Claim:** `CROSS_VALIDATED` only within the frozen finite 64-case public campaign, with sixteen +cases each for Median, Median of differences, Trimmed mean, and Trimmed mean of differences. +The production contract is `portable_source_semantics`: the public wrappers are bitwise exact to +the independent V2 oracle in `64/64` corrected arrays and `3888/3888` elements, retaining all +frozen mask modes, absent-mask routes, horizontal/vertical orientations, trim fractions `0.0`, +`0.05`, and `0.5`, mutation/no-op classifications, and deterministic output. The wrappers return +new context-preserving `SPMChannel` instances and do not claim Gwyddion GUI, publication, undo, +or mutation behavior. + +The secondary `installed_gwyddion_2_71_fast_math_profile` is external executable evidence from +`process.so` (Gwyddion 2.71 installed module) +(`c21d52375807ae096e34a3469c2f20c4c66ea3197479e13215a6d7b9d465b451`). It is bitwise exact for +`61/64` arrays and `3757/3888` elements. The complete and bounded exception set is three +signed-zero-only Median elements in `median__plateaus_signed_zero__10` plus 64 finite elements in +each of `median_of_differences__irregular__11` and +`trimmed_mean_of_differences__irregular__11`; their maximum absolute difference is +`5.329070518200751e-15`, with no NaN or infinity discrepancy. All eight requested backgrounds +(`504/504` elements) are bitwise exact and mutation/no-op classifications agree `64/64`. + +The installed package build diagnosis is `INSTALLED_BUILD_ROOT_CAUSE_CONFIRMED` and +`V3_NOT_JUSTIFIED`: GCC 16.1.1 `-ffast-math`, associative floating-point reassociation, and LTO +produce the two irregular difference-method residuals. The portable source-semantic arithmetic +is deliberate; SPM-Kit does not emulate that package-specific transformation and introduces no +named-case or signed-zero patch. The public functions are explicit alternatives to, not a +compatibility claim for, the existing generic `align_rows`. + +**Traceability:** + +```text +Gwyddion 2.71 source: modules/process/linematch.c + → frozen external probe: align_rows_probe_v1 + → independent oracle: align_rows_oracle_stats_v2 + → tests/validation/fixtures/gwyddion/align_rows_statistics/ + → src/spmkit/core/analysis/_gwyddion_align_rows_statistics.py + → src/spmkit/core/analysis/leveling.py + → tests/core/test_gwyddion_align_rows_statistics.py + → docs/design/GWYDDION_ALIGN_ROWS_STATISTICS_COMPATIBILITY.md +``` + +**Non-claims:** no universal equivalence; no NaN/Inf, other Gwyddion version or build, untested +matrix, performance, ROI/GUI, adapter, or other Align Rows method-family claim. This finite +campaign does not establish physical validation or general SPMKit parity. ## Test-count policy diff --git a/docs/theory/spmkit-workflows.md b/docs/theory/spmkit-workflows.md index 4c9dd76..c074228 100644 --- a/docs/theory/spmkit-workflows.md +++ b/docs/theory/spmkit-workflows.md @@ -11,6 +11,8 @@ |---|---|---|---|---|---| | file inspection/routing | `core.io.load_any`, `core.plugins`, built-in readers | automatic open route | `spmkit info` | format-specific Level 1/2 | support is variant-specific | | leveling | `core.analysis.leveling` | Imagen (`image`) | `roughness --level`, `analyze --level` | Level 1 + synthetic cases | changes the reference surface | +| arc-revolution background | `core.analysis.background` | not exposed | Python API | Level 1 with test-local 1D oracle and synthetic physical tests | finite geometric Z only; no masks, CLI, Fathom or external-equivalence claim | +| sphere-revolution background | `core.analysis.background` | not exposed | Python API | Level 1 with independent 2D nearest/reflect oracles and synthetic physical tests | true 2D spherical cap; no masks, CLI, Fathom, external-equivalence or performance claim | | Sa/Sq/Sz/Ssk/Sku | `core.analysis.roughness.statistics` | Imagen (`image`) | `spmkit roughness`, `analyze` | scoped Level 3 for Sa/Sq/Sz | external campaigns do not cover Ssk/Sku or every preprocessing route | | line profile | `core.analysis.profiles.line` | Imagen (`image`) | Python API | Level 1 | interpolation and coordinate choice matter | | grain segmentation | `core.analysis.grains.detect` | Granos (`grains`) | `spmkit grains` | Level 1 + synthetic tests | threshold/overlap/tip effects | diff --git a/docs/user-guide.md b/docs/user-guide.md index 86e4f4e..282424d 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -410,7 +410,7 @@ KPFM work‑function calculation uses `Φ_sample = Φ_tip - e·CPD`. ### Grains (`grains`) **Purpose:** Detect particles/grains on a levelled topography channel. -Requires the `grains` extra (scipy). +Uses SciPy, which is a required SPMKit dependency. | Feature | Description | |---------|-------------| @@ -1264,7 +1264,7 @@ ValueError: unsupported extension: .xxx ``` ModuleNotFoundError: No module named 'scipy' ``` -**Fix:** Install the relevant extra (e.g. `pip install "spmkit[grains]"`). +**Fix:** Reinstall SPMKit so its required dependencies are restored (for a development checkout: `python -m pip install -e .`). ### Empty or unexpected channels ``` diff --git a/docs/user-guide.tex b/docs/user-guide.tex index aedeefb..9409d44 100644 --- a/docs/user-guide.tex +++ b/docs/user-guide.tex @@ -421,7 +421,7 @@ \subsection{Image perspectives} \multicolumn{2}{c}{\textbf{Module: image}}\\ \midrule image & Channel viewer, leveling (plane/poly/align-rows), colormap, line profile ROI, roughness + KPFM analysis, profile export.\\ -grains & Particle/grain detection on levelled topography; colour overlay, size statistics. Requires \texttt{grains} extra.\\ +grains & Particle/grain detection on levelled topography; colour overlay, size statistics. Uses SciPy, which is installed as a required SPMKit dependency.\\ spectral & Radial PSD (log-log), fractal dimension~D, Hurst exponent~H, correlation length.\\ resonance & Thermal tune: SHO fit $\rightarrow$ f$_0$, Q, spring constant by equipartition.\\ evaporation & Mass sensing: load series of tuning .nid files $\rightarrow$ f(t), mass, d\textsuperscript{2} law fit.\\ @@ -812,7 +812,7 @@ \section{Troubleshooting} \item[Missing GUI extra] \hfill \\ \texttt{pip install "spmkit[gui]"} \item[Qt platform plugin error] \hfill \\ Install Qt dependencies or set \texttt{QT\_QPA\_PLATFORM=offscreen}. \item[Unsupported format] \hfill \\ Check extension matches \texttt{.nid/.nhf/.gwy/.jpk-force}. For experimental readers, install \texttt{afm} or \texttt{jpk}. - \item[Missing optional dependency] \hfill \\ Install the relevant extra (e.g. \texttt{pip install "spmkit[grains]"}). + \item[Missing optional dependency] \hfill \\ Reinstall SPMKit so its required dependencies are restored (for a development checkout: \texttt{python -m pip install -e .}). \item[Large force-volume memory] \hfill \\ Use lazy loading or the \texttt{--fast} CPU vectorised path. \item[GPU fallback] \hfill \\ CuPy is not bundled. Install separately: \texttt{pip install cupy-cuda12x}. \item[Export failure] \hfill \\ Check write permissions. Use \texttt{--output} with a writable path. diff --git a/docs/validation/index.md b/docs/validation/index.md index 09f1190..3c419d9 100644 --- a/docs/validation/index.md +++ b/docs/validation/index.md @@ -36,6 +36,12 @@ references, tolerances, outputs, hashes, and limitations. |---|---|---:|---|---| | Gwyddion roughness 48 v0.1 | Sa, Sq, Sz on 48 canonical synthetic matrices | 144/144 within tolerance | CROSS_VALIDATED | No preprocessing; shared matrices; not physical validation | | Real-data roughness pilot v0.1 | Sa, Sq, Sz on 12 public GWY matrices | 36/36 shared-matrix comparisons within tolerance | CROSS_VALIDATED for the algorithm track | Parser/end-to-end observations are separate; real data are not ground truth | +| Gwyddion Revolve Arc 2.71 v1 | Data-adaptive arc-envelope background on a frozen asymmetric 5×7 field, six direction/inversion routes and focal kernel cases | 6/6 backgrounds and 5/5 valid corrected outputs within `5e-14`; horizontal-inverted reference defect preserved as evidence and repaired by reconstruction | CROSS_VALIDATED for the frozen campaign | Gwyddion 2.71 only; radius in samples; known wrapper and one-sample reference defects documented; not physical validation or universal equivalence | +| Gwyddion Revolve Sphere 2.71 v1 | Data-adaptive sphere-envelope background on 10 logical pairs (20 normal runs per build) and 15 failing inverted runs; direct normal external reference and derived inverted background within 5e-14; safe inverted corrected reconstruction | 20/20 valid external runs and 10/10 derived inverted backgrounds within 5e-14 | CROSS_VALIDATED for the frozen campaign | Gwyddion 2.71 only; radius in samples; 15/15 inverted wrapper crashes documented as reference failures; not physical validation or universal equivalence | +| Gwyddion Median Background 2.71 v1 | Local rank background on 36 frozen logical cases, 72 executions (36 normal, 36 ASan), radii 1/2/3/4/20/1024, and both direct/radixtree reference paths | Public background and corrected fields 36/36 bitwise exact; maximum absolute difference 0, maximum ULP 0, input mutation maximum 0, reconstruction maximum `4.4408920985006262e-16` | CROSS_VALIDATED within the frozen campaign | Gwyddion 2.71 only; finite inputs; no universal, performance, future-version, all-radii, or all-matrices claim; no public border/shape/rank configuration | +| Gwyddion Filter flat-disc morphology 2.71 v1 | 12 frozen fields, six sizes 2/3/4/5/30/31, full-field mask-ignore Opening and Closing | Kernels 30/30; Opening 72/72 and Closing 72/72 bitwise exact; max absolute difference 0, max ULP 0, signed-zero mismatches 0, input mutation 0 | CROSS_VALIDATED within the frozen campaign | Gwyddion 2.71 executable only; finite full-field data; no universal, NaN/Inf, ROI, mask, ASF, tip, physical rolling-ball, performance, other-build, public erosion/dilation, or source-only tie claim | +| Gwyddion Path Level 2.71 v1 | 18 frozen finite full-field families, ordered straight physical selections, thicknesses 1/2/3/128, 72 logical cases and 144 fresh external executions | Public arrays 72/72 bitwise exact, 4,652/4,652 elements exact; max absolute/ULP 0, signed-zero mismatches 0, 72/72 repeat pairs, normalized endpoints, and mutation/no-op classifications | CROSS_VALIDATED within the frozen campaign | Audited Gwyddion 2.71 Path Level executable only; no universal, NaN/Inf, ROI/mask, path/spline, profile, align-rows, volume, GUI, performance, other-build/version claim | +| Gwyddion Align Rows statistics 2.71 v1 | 64 finite cases, 16 each for Median, Median of differences, Trimmed mean, and Trimmed mean of differences; numeric masks, absent masks, both directions, and trims 0/0.05/0.5 | Portable source semantics: public 64/64 arrays and 3,888/3,888 elements bitwise exact. Installed fast-math profile: 61/64 arrays and 3,757/3,888 elements exact; only 3 signed-zero and 128 explained reassociation differences | CROSS_VALIDATED within the frozen dual-profile campaign | Finite frozen domain only; no universal, NaN/Inf, performance, other-version/build, GUI, adapter, or generic-`align_rows` compatibility claim | | Nanoscope `.spm` pilot v0.1 | Six demonstrated files | 18/18 metric comparisons within tolerance | NUMERICALLY_VERIFIED limited parser claim | Partial support and `ACCIDENTAL_PRE_FREEZE_UNBLINDING` | See [Scientific status](../scientific-status.md) for the complete mapping and @@ -48,6 +54,171 @@ The `.nid` path also provides byte-level inspection through `spmkit verify` and conversion, finiteness, and orientation rules. Integrity and parser traceability do not establish physical correctness. +### Gwyddion 2.71 Median Background + +The frozen campaign trace is: + +```text +Gwyddion source +→ external probe +→ independent Python oracle +→ frozen fixture +→ private SPMKit kernel +→ public API +→ public bitwise tests +→ scientific status +``` + +The concrete records are Gwyddion 2.71 source `modules/process/median-bg.c`, +the frozen `median_background_behavior_probe.c`, +and the frozen `run_median_background_probe_campaign.sh`, +`docs/design/GWYDDION_MEDIAN_BACKGROUND_COMPATIBILITY.md`, +`tests/validation/fixtures/gwyddion/median_background/median_background_reference.npz`, +`tests/validation/fixtures/gwyddion/median_background/median_background_reference.json`, +`src/spmkit/core/analysis/_median_background.py`, +`src/spmkit/core/analysis/background.py`, +`tests/core/test_gwyddion_median_background_private.py`, +`tests/core/test_gwyddion_median_background.py`, +`tests/validation/test_median_background_fixture_integrity.py`, and +`docs/scientific-status.md`. + +The chain was frozen by `818dbd3` (evidence), `a53c3bb` (private kernel), and `ed5c837` +(public API). The permanent fixture contains canonical hashes; the original oracle and its +ephemeral source artifacts are identified in the fixture manifest. The campaign's runner +limitations are non-blocking: `|| true` does not preserve an original compiler exit, and the +auxiliary parser accepts broad `background_` and `corrected_` prefixes. Both binaries still +executed; 72 processes returned with empty stderr; normal/ASan stdout was byte-identical; and +the parsed outputs, oracle/reference equality, and canonical hashes were independently checked. + +#### Focal test inventory + +- Fixture integrity: 20 tests. +- Private Median Background: 67 tests. +- Public Median Background: 72 tests. +- Combined focal campaign: 442 tests. + +These counts describe the frozen focal validation campaign for this capability. They are not +the global test total of the SPMKit project. + +### Gwyddion 2.71 Filter flat-disc morphology + +The frozen trace is: + +```text +Gwyddion source +→ corrected external probe V3 +→ executable reduction trace +→ independent oracle V2 +→ frozen fixture +→ private SPMKit kernel +→ public bitwise tests +→ CROSS_VALIDATED status +``` + +The records are Gwyddion 2.71 source `libprocess/filters-minmax.c`, +the frozen `flat_disc_probe_v3` and `flat_disc_reduction_trace_v1`, +`docs/design/GWYDDION_FLAT_DISC_MORPHOLOGY_COMPATIBILITY.md`, +`tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.npz`, +`tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.json`, +`src/spmkit/core/analysis/_gwyddion_flat_disc_morphology.py`, +`src/spmkit/core/analysis/background.py`, +`tests/core/test_gwyddion_flat_disc_morphology_private.py`, and +`tests/core/test_gwyddion_flat_disc_morphology.py`. + +The evidence commit is `2ba366e`; the private-kernel commit is `05c5ae4`. The claim is limited +to the 12 frozen fields and six sizes. Source strict ternaries and executable MINSD/MAXSD +equality behavior are distinguished; the rejected uninitialised-kernel probe is excluded, and +the corrected zero-initialised probe is the valid external evidence. No claim is made for +universal equivalence, non-finite data, ROI/masks, ASF, tip morphology, physical rolling-ball, +performance, other Gwyddion builds, public erosion/dilation, or source-only tie semantics. + +### Gwyddion 2.71 Path Level + +The frozen trace is: + +```text +Gwyddion source +→ installed Path Level tool execution +→ frozen 72-case external probe +→ independent oracle V1 +→ frozen repository fixture +→ private SPMKit kernel +→ public SPMChannel API +→ public bitwise tests +→ CROSS_VALIDATED status +``` + +The records are Gwyddion 2.71 source `modules/tools/pathlevel.c`, +the frozen `path_level_probe_v1` and independent `path_level_oracle_v1`, +`docs/design/GWYDDION_PATH_LEVEL_COMPATIBILITY.md`, +`tests/validation/fixtures/gwyddion/path_level/path_level_reference.npz`, +`tests/validation/fixtures/gwyddion/path_level/path_level_reference.json`, +`src/spmkit/core/analysis/_gwyddion_path_level.py`, `src/spmkit/core/analysis/leveling.py`, +`tests/core/test_gwyddion_path_level_private.py`, and +`tests/core/test_gwyddion_path_level.py`, and +`tests/validation/test_path_level_fixture_integrity.py`. + +The evidence commit is `d3566ce`; the private-kernel commit is `4ead95b`. The claim is limited +to 18 finite, non-empty, full-field families, ordered straight selections, thicknesses 1, 2, 3, +and 128, 72 logical cases, 144 fresh executions, and 72/72 deterministic repeat pairs. Public +arrays are bitwise exact in 72/72 cases and 4,652/4,652 elements, with maximum absolute +difference 0, maximum ULP 0, signed-zero mismatches 0, normalized endpoints and mutation/no-op +classification 72/72, and input mutation 0. The audited module is Gwyddion 2.71 `tools.so` +with SHA-256 `4711c360dd42e3e16257bf0e86d8bd41852b43d1d34540bf097736a603146237` and Build ID +`600b16d9857946609b567704b406abcc74aea698`. + +Gwyddion mutates its selected data field in place and publishes GUI undo/logging state; SPM-Kit +returns a new `SPMChannel`. No claim is made for universal equivalence, NaN/Inf, masks/ROI, +GwySelectionPath, splines/polylines, profiles, align-rows, volume line-leveling, GUI/undo/logging +or selection-widget parity, performance, or other Gwyddion versions or builds. + +### Gwyddion 2.71 Align Rows statistics + +The public validation trace is: + +```text +Gwyddion source +→ installed external probe +→ independent portable V2 oracle +→ frozen dual-profile repository fixture +→ private SPMKit kernel +→ public SPMChannel wrappers +→ public bitwise tests +→ CROSS_VALIDATED status +``` + +The records are Gwyddion 2.71 source `modules/process/linematch.c`, +the frozen `align_rows_probe_v1` and independent `align_rows_oracle_stats_v2`, +`tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.npz`, +`tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.json`, +`docs/design/GWYDDION_ALIGN_ROWS_STATISTICS_COMPATIBILITY.md`, +`src/spmkit/core/analysis/_gwyddion_align_rows_statistics.py`, +`src/spmkit/core/analysis/leveling.py`, +`tests/core/test_gwyddion_align_rows_statistics_private.py`, +`tests/core/test_gwyddion_align_rows_statistics.py`, and +`tests/validation/test_gwyddion_align_rows_statistics_fixture_integrity.py`. + +`portable_source_semantics` is the production contract. Public output is bitwise exact to the +frozen V2 oracle for all `64/64` corrected arrays and `3888/3888` elements. This is +`CROSS_VALIDATED` only in the frozen finite campaign: 16 cases per supported method, numeric and +absent masks, Exclude/Include/Ignore routing, horizontal/vertical orientation, and trim fractions +`0.0`, `0.05`, and `0.5`. All eight requested background arrays (`504/504` elements) are bitwise +exact across profiles, and mutation/no-op classifications agree `64/64`. + +The installed Gwyddion 2.71 `process.so` +(`c21d52375807ae096e34a3469c2f20c4c66ea3197479e13215a6d7b9d465b451`) is a secondary profile, +`installed_gwyddion_2_71_fast_math_profile`: public corrected arrays are bitwise exact in `61/64` +arrays and `3757/3888` elements. The exact exception set is three signed-zero-only elements in +`median__plateaus_signed_zero__10`, and 64 finite elements each in +`median_of_differences__irregular__11` and +`trimmed_mean_of_differences__irregular__11`, bounded by maximum absolute difference +`5.329070518200751e-15`, with no NaN/Inf mismatch. The installed-build diagnosis confirms GCC +16.1.1 `-ffast-math` associative reassociation with LTO; SPM-Kit deliberately preserves portable +source arithmetic rather than emulate that local build. Therefore V3 is not justified. + +No claim is made for non-finite fields, universal or performance equivalence, another Gwyddion +version/build, ROI/GUI/adapters, other Align Rows families, or the existing generic `align_rows`. + ## What remains open - redistributable multi-instrument fixtures for built-in and adapter readers; diff --git a/pyproject.toml b/pyproject.toml index c5cc5f8..416a7da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ ] dependencies = [ "numpy>=1.24", + "scipy>=1.14", "typer>=0.12", "rich>=13.0", "pyyaml>=6.0", # serialización de Recipe (pipeline reproducible) @@ -36,7 +37,6 @@ gwy = ["gwyfile>=0.3"] # interop con Gwyddion (.gwy) nanosurf = ["NSFopen>=2.2"] # lector .nhf validado (NanoSurf) afm = ["afmformats>=0.18"] # lectores de la cola larga (JPK QI, .ibw, HDF5, NT-MDT…) jpk = ["tifffile>=2023.7"] # curvas/mapas de fuerza JPK en formato TIFF -grains = ["scipy>=1.10"] # detección de granos/partículas parallel = ["joblib>=1.3"] # backend paralelo opcional para force-volumes grandes pandas = ["pandas>=2.0"] # export a DataFrame (batch, resultados) viz = [ # figuras de publicación @@ -56,7 +56,7 @@ dev = [ ] test-gui = ["pytest-qt>=4.4"] # tests de GUI (requiere también el extra 'gui') docs = ["mkdocs-material>=9.5"] -all = ["spmkit[hdf5,gwy,nanosurf,afm,jpk,grains,viz,report,gui]"] +all = ["spmkit[hdf5,gwy,nanosurf,afm,jpk,viz,report,gui]"] [project.urls] Homepage = "https://kegouro.github.io/spmkit/" diff --git a/src/spmkit/compat/__init__.py b/src/spmkit/compat/__init__.py new file mode 100644 index 0000000..d998418 --- /dev/null +++ b/src/spmkit/compat/__init__.py @@ -0,0 +1,5 @@ +"""Conservative source-compatibility utilities isolated from scientific core code.""" + +from spmkit.compat import gwyddion + +__all__ = ["gwyddion"] diff --git a/src/spmkit/compat/gwyddion/__init__.py b/src/spmkit/compat/gwyddion/__init__.py new file mode 100644 index 0000000..53c7fca --- /dev/null +++ b/src/spmkit/compat/gwyddion/__init__.py @@ -0,0 +1,25 @@ +"""Static audit primitives for conservative Gwyddion source migration work.""" + +from spmkit.compat.gwyddion.profiles import ( + GwyddionCompatibilityProfile, + GwyddionVersion, + gwyddion_2_71_profile, +) +from spmkit.compat.gwyddion.reports import ( + GwyddionModuleAuditReport, + canonical_report_json, + report_from_dict, + report_to_dict, +) +from spmkit.compat.gwyddion.source_audit import audit_gwyddion_source + +__all__ = [ + "GwyddionCompatibilityProfile", + "GwyddionModuleAuditReport", + "GwyddionVersion", + "audit_gwyddion_source", + "canonical_report_json", + "gwyddion_2_71_profile", + "report_from_dict", + "report_to_dict", +] diff --git a/src/spmkit/compat/gwyddion/errors.py b/src/spmkit/compat/gwyddion/errors.py new file mode 100644 index 0000000..51af789 --- /dev/null +++ b/src/spmkit/compat/gwyddion/errors.py @@ -0,0 +1,15 @@ +"""Explicit errors for the static Gwyddion source-audit boundary.""" + +from __future__ import annotations + + +class GwyddionCompatibilityError(Exception): + """Base error for conservative Gwyddion compatibility operations.""" + + +class InvalidGwyddionSourceError(GwyddionCompatibilityError, TypeError): + """Raised when a source audit does not receive source text.""" + + +class UnsupportedGwyddionProfileError(GwyddionCompatibilityError, ValueError): + """Raised when no explicit compatibility profile exists for a version.""" diff --git a/src/spmkit/compat/gwyddion/profiles.py b/src/spmkit/compat/gwyddion/profiles.py new file mode 100644 index 0000000..91ff9ab --- /dev/null +++ b/src/spmkit/compat/gwyddion/profiles.py @@ -0,0 +1,109 @@ +"""Version-scoped, conservative Gwyddion source-audit profiles.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from spmkit.compat.gwyddion.errors import UnsupportedGwyddionProfileError + + +@dataclass(frozen=True, order=True) +class GwyddionVersion: + """A concrete Gwyddion version identity, without compatibility extrapolation.""" + + major: int + minor: int + patch: int = 0 + + def __post_init__(self) -> None: + if any(value < 0 for value in (self.major, self.minor, self.patch)): + raise ValueError("Gwyddion version components must be non-negative") + + def __str__(self) -> str: + if self.patch == 0: + return f"{self.major}.{self.minor}" + return f"{self.major}.{self.minor}.{self.patch}" + + +@dataclass(frozen=True) +class GwyddionCompatibilityProfile: + """Known static-audit facts for one Gwyddion version. + + Recognition does not establish source portability. Exact mappings are + deliberately restricted to current SPMKit data-model facts. + """ + + name: str + version: GwyddionVersion + registration_calls: tuple[str, ...] + gwyddion_symbol_prefixes: tuple[str, ...] + gtk_symbol_prefixes: tuple[str, ...] + glib_symbol_prefixes: tuple[str, ...] + exact_symbol_mappings: tuple[tuple[str, str], ...] + + @property + def mapping_dict(self) -> dict[str, str]: + """Return a fresh lookup for the profile's explicitly supported mappings.""" + return dict(self.exact_symbol_mappings) + + +_GWYDDION_2_71_PROFILE = GwyddionCompatibilityProfile( + name="gwyddion-2.71-source-audit", + version=GwyddionVersion(2, 71), + registration_calls=( + "GWY_MODULE_QUERY", + "GWY_MODULE_QUERY2", + "GWY_MODULE_QUERY3", + "gwy_curve_map_func_register", + "gwy_file_func_register", + "gwy_graph_func_register", + "gwy_layer_func_register", + "gwy_process_func_register", + "gwy_tool_func_register", + "gwy_volume_func_register", + "gwy_xyz_func_register", + ), + gwyddion_symbol_prefixes=("gwy_", "GWY_"), + gtk_symbol_prefixes=( + "gtk_", + "gdk_", + "pango_", + "GTK_", + "GDK_", + "PANGO_", + "Gtk", + "Gdk", + "Pango", + ), + glib_symbol_prefixes=( + "g_", + "G_", + "GLIB_", + "GIO_", + "GObject", + "GType", + "GQuark", + "GList", + "GSList", + ), + exact_symbol_mappings=( + ("gwy_data_field_get_xres", "SPMChannel.shape[1]"), + ("gwy_data_field_get_yres", "SPMChannel.shape[0]"), + ("gwy_data_field_get_xreal", "SPMChannel.x_range"), + ("gwy_data_field_get_yreal", "SPMChannel.y_range"), + ), +) + + +def gwyddion_2_71_profile() -> GwyddionCompatibilityProfile: + """Return the immutable conservative profile for frozen Gwyddion 2.71 source.""" + return _GWYDDION_2_71_PROFILE + + +def profile_for_version(version: GwyddionVersion) -> GwyddionCompatibilityProfile: + """Return the explicitly supported profile or fail without approximation.""" + if not isinstance(version, GwyddionVersion): + raise TypeError("version must be a GwyddionVersion") + if version == _GWYDDION_2_71_PROFILE.version: + return _GWYDDION_2_71_PROFILE + raise UnsupportedGwyddionProfileError(f"no Gwyddion source-audit profile for {version}") diff --git a/src/spmkit/compat/gwyddion/reports.py b/src/spmkit/compat/gwyddion/reports.py new file mode 100644 index 0000000..a4cbe87 --- /dev/null +++ b/src/spmkit/compat/gwyddion/reports.py @@ -0,0 +1,259 @@ +"""Deterministic JSON-compatible models for static Gwyddion source audits.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any + +from spmkit.compat.gwyddion.profiles import GwyddionCompatibilityProfile, GwyddionVersion +from spmkit.compat.gwyddion.symbols import ( + DependencyReference, + IncludeReference, + ModuleRegistration, + RegistrationKind, + SourceLocation, + SourceSpan, + SymbolClassification, + SymbolReference, + SymbolSupportStatus, +) + +REPORT_SCHEMA_VERSION = 1 + + +@dataclass(frozen=True) +class AuditEvidence: + """Static-audit provenance and limitations, without an execution claim.""" + + source_sha256: str + scanner: str + scanner_version: int + limitations: tuple[str, ...] + + +@dataclass(frozen=True) +class GwyddionModuleAuditReport: + """Immutable report from a lexical Gwyddion C source inventory.""" + + schema_version: int + module_path: str | None + content_sha256: str + profile: GwyddionCompatibilityProfile + evidence: AuditEvidence + registrations: tuple[ModuleRegistration, ...] + includes: tuple[IncludeReference, ...] + gwyddion_symbols: tuple[SymbolReference, ...] + gtk_glib_dependencies: tuple[DependencyReference, ...] + mapped_total: int + adapter_required_total: int + unsupported_total: int + unknown_total: int + has_ui_dependency: bool + likely_selection_dependencies: tuple[str, ...] + likely_parameter_system_dependencies: tuple[str, ...] + likely_publication_logging_dependencies: tuple[str, ...] + conservative_mutation_hints: tuple[str, ...] + migration_blockers: tuple[str, ...] + migration_warnings: tuple[str, ...] + evidence_limitations: tuple[str, ...] + + +def _location_to_dict(location: SourceLocation) -> dict[str, object]: + return {"column": location.column, "line": location.line, "source_path": location.source_path} + + +def _span_to_dict(span: SourceSpan) -> dict[str, object]: + return {"end": _location_to_dict(span.end), "start": _location_to_dict(span.start)} + + +def _profile_to_dict(profile: GwyddionCompatibilityProfile) -> dict[str, object]: + return { + "exact_symbol_mappings": [list(item) for item in profile.exact_symbol_mappings], + "glib_symbol_prefixes": list(profile.glib_symbol_prefixes), + "gwyddion_symbol_prefixes": list(profile.gwyddion_symbol_prefixes), + "gtk_symbol_prefixes": list(profile.gtk_symbol_prefixes), + "name": profile.name, + "registration_calls": list(profile.registration_calls), + "version": { + "major": profile.version.major, + "minor": profile.version.minor, + "patch": profile.version.patch, + }, + } + + +def report_to_dict(report: GwyddionModuleAuditReport) -> dict[str, object]: + """Return a JSON-compatible, order-preserving representation of an audit report.""" + return { + "adapter_required_total": report.adapter_required_total, + "content_sha256": report.content_sha256, + "conservative_mutation_hints": list(report.conservative_mutation_hints), + "evidence": { + "limitations": list(report.evidence.limitations), + "scanner": report.evidence.scanner, + "scanner_version": report.evidence.scanner_version, + "source_sha256": report.evidence.source_sha256, + }, + "evidence_limitations": list(report.evidence_limitations), + "gtk_glib_dependencies": [ + { + "classification": dependency.classification.value, + "name": dependency.name, + "occurrences": [_span_to_dict(span) for span in dependency.occurrences], + "support_status": dependency.support_status.value, + } + for dependency in report.gtk_glib_dependencies + ], + "gwyddion_symbols": [ + { + "call_occurrences": [_span_to_dict(span) for span in symbol.call_occurrences], + "classification": symbol.classification.value, + "occurrences": [_span_to_dict(span) for span in symbol.occurrences], + "support_status": symbol.support_status.value, + "symbol": symbol.symbol, + } + for symbol in report.gwyddion_symbols + ], + "has_ui_dependency": report.has_ui_dependency, + "includes": [ + { + "is_local": include.is_local, + "name": include.name, + "span": _span_to_dict(include.span), + } + for include in report.includes + ], + "likely_parameter_system_dependencies": list(report.likely_parameter_system_dependencies), + "likely_publication_logging_dependencies": list( + report.likely_publication_logging_dependencies + ), + "likely_selection_dependencies": list(report.likely_selection_dependencies), + "mapped_total": report.mapped_total, + "migration_blockers": list(report.migration_blockers), + "migration_warnings": list(report.migration_warnings), + "module_path": report.module_path, + "registrations": [ + { + "callee": registration.callee, + "declared_name": registration.declared_name, + "kind": registration.kind.value, + "span": _span_to_dict(registration.span), + } + for registration in report.registrations + ], + "profile": _profile_to_dict(report.profile), + "schema_version": report.schema_version, + "unknown_total": report.unknown_total, + "unsupported_total": report.unsupported_total, + } + + +def canonical_report_json(report: GwyddionModuleAuditReport) -> str: + """Serialize a report deterministically without writing a file.""" + return json.dumps( + report_to_dict(report), + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + + +def _location_from_dict(value: dict[str, Any]) -> SourceLocation: + return SourceLocation(value["source_path"], int(value["line"]), int(value["column"])) + + +def _span_from_dict(value: dict[str, Any]) -> SourceSpan: + return SourceSpan(_location_from_dict(value["start"]), _location_from_dict(value["end"])) + + +def _profile_from_dict(value: dict[str, Any]) -> GwyddionCompatibilityProfile: + version = value["version"] + return GwyddionCompatibilityProfile( + name=str(value["name"]), + version=GwyddionVersion( + int(version["major"]), + int(version["minor"]), + int(version["patch"]), + ), + registration_calls=tuple(str(item) for item in value["registration_calls"]), + gwyddion_symbol_prefixes=tuple(str(item) for item in value["gwyddion_symbol_prefixes"]), + gtk_symbol_prefixes=tuple(str(item) for item in value["gtk_symbol_prefixes"]), + glib_symbol_prefixes=tuple(str(item) for item in value["glib_symbol_prefixes"]), + exact_symbol_mappings=tuple( + (str(item[0]), str(item[1])) for item in value["exact_symbol_mappings"] + ), + ) + + +def report_from_dict(value: dict[str, Any]) -> GwyddionModuleAuditReport: + """Reconstruct a report from :func:`report_to_dict` output.""" + evidence_value = value["evidence"] + return GwyddionModuleAuditReport( + schema_version=int(value["schema_version"]), + module_path=value["module_path"], + content_sha256=str(value["content_sha256"]), + profile=_profile_from_dict(value["profile"]), + evidence=AuditEvidence( + source_sha256=str(evidence_value["source_sha256"]), + scanner=str(evidence_value["scanner"]), + scanner_version=int(evidence_value["scanner_version"]), + limitations=tuple(str(item) for item in evidence_value["limitations"]), + ), + registrations=tuple( + ModuleRegistration( + kind=RegistrationKind(item["kind"]), + callee=str(item["callee"]), + declared_name=item["declared_name"], + span=_span_from_dict(item["span"]), + ) + for item in value["registrations"] + ), + includes=tuple( + IncludeReference( + name=str(item["name"]), + is_local=bool(item["is_local"]), + span=_span_from_dict(item["span"]), + ) + for item in value["includes"] + ), + gwyddion_symbols=tuple( + SymbolReference( + symbol=str(item["symbol"]), + classification=SymbolClassification(item["classification"]), + support_status=SymbolSupportStatus(item["support_status"]), + occurrences=tuple(_span_from_dict(span) for span in item["occurrences"]), + call_occurrences=tuple(_span_from_dict(span) for span in item["call_occurrences"]), + ) + for item in value["gwyddion_symbols"] + ), + gtk_glib_dependencies=tuple( + DependencyReference( + name=str(item["name"]), + classification=SymbolClassification(item["classification"]), + support_status=SymbolSupportStatus(item["support_status"]), + occurrences=tuple(_span_from_dict(span) for span in item["occurrences"]), + ) + for item in value["gtk_glib_dependencies"] + ), + mapped_total=int(value["mapped_total"]), + adapter_required_total=int(value["adapter_required_total"]), + unsupported_total=int(value["unsupported_total"]), + unknown_total=int(value["unknown_total"]), + has_ui_dependency=bool(value["has_ui_dependency"]), + likely_selection_dependencies=tuple( + str(item) for item in value["likely_selection_dependencies"] + ), + likely_parameter_system_dependencies=tuple( + str(item) for item in value["likely_parameter_system_dependencies"] + ), + likely_publication_logging_dependencies=tuple( + str(item) for item in value["likely_publication_logging_dependencies"] + ), + conservative_mutation_hints=tuple( + str(item) for item in value["conservative_mutation_hints"] + ), + migration_blockers=tuple(str(item) for item in value["migration_blockers"]), + migration_warnings=tuple(str(item) for item in value["migration_warnings"]), + evidence_limitations=tuple(str(item) for item in value["evidence_limitations"]), + ) diff --git a/src/spmkit/compat/gwyddion/source_audit.py b/src/spmkit/compat/gwyddion/source_audit.py new file mode 100644 index 0000000..3a8914a --- /dev/null +++ b/src/spmkit/compat/gwyddion/source_audit.py @@ -0,0 +1,398 @@ +"""Dependency-free lexical inventory for conservative Gwyddion C source audits. + +This module is intentionally not a complete C parser. It never preprocesses, +compiles, executes, or loads the audited source. +""" + +from __future__ import annotations + +import hashlib +import re +from bisect import bisect_right +from collections import OrderedDict +from collections.abc import Iterable + +from spmkit.compat.gwyddion.errors import InvalidGwyddionSourceError +from spmkit.compat.gwyddion.profiles import ( + GwyddionCompatibilityProfile, + gwyddion_2_71_profile, +) +from spmkit.compat.gwyddion.reports import ( + REPORT_SCHEMA_VERSION, + AuditEvidence, + GwyddionModuleAuditReport, +) +from spmkit.compat.gwyddion.symbols import ( + DependencyReference, + IncludeReference, + ModuleRegistration, + RegistrationKind, + SourceLocation, + SourceSpan, + SymbolClassification, + SymbolReference, + SymbolSupportStatus, +) + +_IDENTIFIER = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") +_INCLUDE = re.compile(r'(?m)^[ \t]*#[ \t]*include[ \t]*([<"])([^>"]+)[>"]') +_KNOWN_REGISTRATION_KINDS = { + "gwy_curve_map_func_register": RegistrationKind.CURVE_MAP, + "gwy_file_func_register": RegistrationKind.FILE, + "gwy_graph_func_register": RegistrationKind.GRAPH, + "gwy_layer_func_register": RegistrationKind.LAYER, + "gwy_process_func_register": RegistrationKind.PROCESS, + "gwy_tool_func_register": RegistrationKind.TOOL, + "gwy_volume_func_register": RegistrationKind.VOLUME, + "gwy_xyz_func_register": RegistrationKind.XYZ, +} +_PROCESS_NUMERICAL_PREFIXES = ( + "gwy_data_field_area_", + "gwy_data_field_filter_", + "gwy_data_field_elliptic_", + "gwy_data_field_grains_", +) +_MUTATING_DATA_FIELD_MARKERS = ("_set_", "_add_", "_subtract_", "_fill", "_filter_") +_LIMITATIONS = ( + "Lexical inventory only; this is not a complete C parser.", + "No preprocessing, macro expansion, type resolution, or control-flow analysis occurs.", + "Mutation, UI, selection, parameter, and publication findings are audit hints," + " not semantic proof.", + "Recognized symbols and registrations do not establish full-module source portability.", +) + + +def _mask_comments(text: str) -> str: + """Replace comments by spaces while preserving strings and every newline.""" + result = list(text) + index = 0 + state = "normal" + while index < len(text): + current = text[index] + following = text[index + 1] if index + 1 < len(text) else "" + if state == "normal" and current == "/" and following == "/": + result[index] = result[index + 1] = " " + index += 2 + while index < len(text) and text[index] != "\n": + result[index] = " " + index += 1 + continue + if state == "normal" and current == "/" and following == "*": + result[index] = result[index + 1] = " " + index += 2 + while index < len(text): + if text[index] == "*" and index + 1 < len(text) and text[index + 1] == "/": + result[index] = result[index + 1] = " " + index += 2 + break + if text[index] != "\n": + result[index] = " " + index += 1 + continue + if state == "normal" and current in ('"', "'"): + state = current + elif state != "normal" and current == "\\": + index += 2 + continue + elif state != "normal" and current == state: + state = "normal" + index += 1 + return "".join(result) + + +def _mask_literals(text: str) -> str: + """Replace C string and character literal contents while preserving locations.""" + result = list(text) + index = 0 + delimiter: str | None = None + while index < len(text): + current = text[index] + if delimiter is None and current in ('"', "'"): + delimiter = current + result[index] = " " + elif delimiter is not None: + if current != "\n": + result[index] = " " + if current == "\\" and index + 1 < len(text): + index += 1 + if text[index] != "\n": + result[index] = " " + elif current == delimiter: + delimiter = None + index += 1 + return "".join(result) + + +def _line_offsets(text: str) -> list[int]: + return [0, *(match.end() for match in re.finditer("\n", text))] + + +def _location(offset: int, offsets: list[int], source_path: str | None) -> SourceLocation: + line_index = bisect_right(offsets, offset) - 1 + return SourceLocation(source_path, line_index + 1, offset - offsets[line_index] + 1) + + +def _span( + start: int, + end: int, + offsets: list[int], + source_path: str | None, +) -> SourceSpan: + return SourceSpan(_location(start, offsets, source_path), _location(end, offsets, source_path)) + + +def _next_nonspace(text: str, index: int) -> int: + while index < len(text) and text[index].isspace(): + index += 1 + return index + + +def _closing_parenthesis(text: str, open_index: int) -> int | None: + depth = 0 + for index in range(open_index, len(text)): + if text[index] == "(": + depth += 1 + elif text[index] == ")": + depth -= 1 + if depth == 0: + return index + return None + + +def _is_gwyddion_symbol(symbol: str, profile: GwyddionCompatibilityProfile) -> bool: + return symbol.startswith(profile.gwyddion_symbol_prefixes) + + +def _is_gtk_symbol(symbol: str, profile: GwyddionCompatibilityProfile) -> bool: + return symbol.startswith(profile.gtk_symbol_prefixes) + + +def _is_glib_symbol(symbol: str, profile: GwyddionCompatibilityProfile) -> bool: + return symbol.startswith(profile.glib_symbol_prefixes) or symbol in { + "gboolean", + "gchar", + "gdouble", + "gint", + "gpointer", + "guint", + "gulong", + } + + +def _classification(symbol: str, profile: GwyddionCompatibilityProfile) -> SymbolClassification: + if symbol in profile.registration_calls or symbol.startswith("GWY_MODULE_QUERY"): + return SymbolClassification.MODULE_REGISTRATION + if _is_gtk_symbol(symbol, profile): + return SymbolClassification.GUI_GTK + if _is_glib_symbol(symbol, profile): + return SymbolClassification.GLIB_RUNTIME + if symbol.startswith(_PROCESS_NUMERICAL_PREFIXES): + return SymbolClassification.PROCESS_NUMERICAL + if symbol.startswith(("gwy_data_field_", "gwy_data_line_", "gwy_brick_", "gwy_surface_")): + return SymbolClassification.DATA_MODEL + if symbol.startswith(("gwy_selection_", "gwy_layer_", "gwy_vector_layer_", "gwy_plain_tool_")): + return SymbolClassification.SELECTION_LAYER + if symbol.startswith(("gwy_params_", "gwy_param_", "gwy_app_settings_")): + return SymbolClassification.PARAMETERS_SETTINGS + if symbol.startswith(("gwy_app_undo_", "gwy_app_channel_log_", "gwy_container_set_")): + return SymbolClassification.PUBLICATION_LOGGING + if symbol.startswith(("gwy_container_", "gwy_app_")): + return SymbolClassification.CONTAINER_APPLICATION + return SymbolClassification.UNKNOWN + + +def _support_status( + symbol: str, + classification: SymbolClassification, + profile: GwyddionCompatibilityProfile, +) -> SymbolSupportStatus: + if symbol in profile.mapping_dict: + return SymbolSupportStatus.MAPPED + if classification is SymbolClassification.GUI_GTK: + return SymbolSupportStatus.UNSUPPORTED + if classification is SymbolClassification.UNKNOWN: + return SymbolSupportStatus.UNKNOWN + return SymbolSupportStatus.ADAPTER_REQUIRED + + +def _registration_kind(symbol: str) -> RegistrationKind | None: + if symbol in _KNOWN_REGISTRATION_KINDS: + return _KNOWN_REGISTRATION_KINDS[symbol] + if symbol.startswith("GWY_MODULE_QUERY") or symbol.endswith("_func_register"): + return RegistrationKind.UNKNOWN + return None + + +def _registration_name(symbol: str, raw_call: str) -> str | None: + if symbol.startswith("GWY_MODULE_QUERY"): + match = re.search(r",\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)$", raw_call, re.DOTALL) + return match.group(1) if match else None + if symbol in {"gwy_process_func_register", "gwy_file_func_register"}: + match = re.search(r'\(\s*"((?:[^"\\]|\\.)*)"', raw_call, re.DOTALL) + return match.group(1) if match else None + return None + + +def _deduplicated(values: Iterable[str]) -> tuple[str, ...]: + return tuple(OrderedDict.fromkeys(values)) + + +def audit_gwyddion_source( + source_text: str, + *, + source_path: str | None = None, + profile: GwyddionCompatibilityProfile | None = None, +) -> GwyddionModuleAuditReport: + """Audit C source text lexically without executing, compiling, or writing files.""" + if not isinstance(source_text, str): + raise InvalidGwyddionSourceError("source_text must be a str") + if source_path is not None and not isinstance(source_path, str): + raise TypeError("source_path must be a str or None") + if profile is None: + profile = gwyddion_2_71_profile() + if not isinstance(profile, GwyddionCompatibilityProfile): + raise TypeError("profile must be a GwyddionCompatibilityProfile") + + comment_masked = _mask_comments(source_text) + lexical_text = _mask_literals(comment_masked) + offsets = _line_offsets(source_text) + includes = tuple( + IncludeReference( + name=match.group(2), + is_local=match.group(1) == '"', + span=_span(match.start(2), match.end(2), offsets, source_path), + ) + for match in _INCLUDE.finditer(comment_masked) + ) + symbol_occurrences: OrderedDict[str, list[SourceSpan]] = OrderedDict() + call_occurrences: OrderedDict[str, list[SourceSpan]] = OrderedDict() + dependency_occurrences: OrderedDict[str, list[SourceSpan]] = OrderedDict() + registrations: list[ModuleRegistration] = [] + + for match in _IDENTIFIER.finditer(lexical_text): + symbol = match.group(0) + symbol_span = _span(match.start(), match.end(), offsets, source_path) + following = _next_nonspace(lexical_text, match.end()) + is_call = following < len(lexical_text) and lexical_text[following] == "(" + if _is_gwyddion_symbol(symbol, profile): + symbol_occurrences.setdefault(symbol, []).append(symbol_span) + if is_call: + call_occurrences.setdefault(symbol, []).append(symbol_span) + kind = _registration_kind(symbol) + if kind is not None: + closing = _closing_parenthesis(lexical_text, following) + call_end = closing + 1 if closing is not None else match.end() + raw_call = source_text[match.start() : call_end] + registrations.append( + ModuleRegistration( + kind=kind, + callee=symbol, + declared_name=_registration_name(symbol, raw_call), + span=_span(match.start(), call_end, offsets, source_path), + ) + ) + elif _is_gtk_symbol(symbol, profile) or _is_glib_symbol(symbol, profile): + dependency_occurrences.setdefault(symbol, []).append(symbol_span) + + symbols = tuple( + SymbolReference( + symbol=symbol, + classification=_classification(symbol, profile), + support_status=_support_status(symbol, _classification(symbol, profile), profile), + occurrences=tuple(occurrences), + call_occurrences=tuple(call_occurrences.get(symbol, [])), + ) + for symbol, occurrences in symbol_occurrences.items() + ) + dependencies = tuple( + DependencyReference( + name=name, + classification=_classification(name, profile), + support_status=_support_status(name, _classification(name, profile), profile), + occurrences=tuple(occurrences), + ) + for name, occurrences in dependency_occurrences.items() + ) + classifications = {symbol.symbol: symbol.classification for symbol in symbols} + selection = _deduplicated( + symbol.symbol + for symbol in symbols + if classifications[symbol.symbol] is SymbolClassification.SELECTION_LAYER + ) + parameters = _deduplicated( + symbol.symbol + for symbol in symbols + if classifications[symbol.symbol] is SymbolClassification.PARAMETERS_SETTINGS + ) + publication = _deduplicated( + symbol.symbol + for symbol in symbols + if classifications[symbol.symbol] is SymbolClassification.PUBLICATION_LOGGING + ) + mutation_hints = _deduplicated( + f"possible data-field mutation: {symbol.symbol}" + for symbol in symbols + if symbol.symbol.startswith("gwy_data_field_") + and any(marker in symbol.symbol for marker in _MUTATING_DATA_FIELD_MARKERS) + ) + blockers: list[str] = [] + if any( + dependency.classification is SymbolClassification.GUI_GTK for dependency in dependencies + ): + blockers.append("GUI/GTK dependency requires an explicit adapter and remains unsupported.") + if selection: + blockers.append("Selection/layer dependency requires an explicit adapter.") + if parameters: + blockers.append("Parameter/settings dependency requires an explicit adapter.") + if publication: + blockers.append("Publication/logging dependency requires an explicit adapter.") + blockers.extend( + f"Unknown support status: {symbol.symbol}" + for symbol in symbols + if symbol.support_status is SymbolSupportStatus.UNKNOWN + ) + warnings = _deduplicated( + [ + "Static source inventory does not establish semantic equivalence.", + "No binary compatibility, dynamic module loading, or automatic translation" + " is provided.", + "License compatibility must be reviewed per migrated module.", + ] + ) + totals = dict.fromkeys(SymbolSupportStatus, 0) + for symbol_reference in symbols: + totals[symbol_reference.support_status] += 1 + for dependency_reference in dependencies: + totals[dependency_reference.support_status] += 1 + digest = hashlib.sha256(source_text.encode("utf-8")).hexdigest() + evidence = AuditEvidence( + source_sha256=digest, + scanner="spmkit.compat.gwyddion.lexical-source-audit", + scanner_version=1, + limitations=_LIMITATIONS, + ) + return GwyddionModuleAuditReport( + schema_version=REPORT_SCHEMA_VERSION, + module_path=source_path, + content_sha256=digest, + profile=profile, + evidence=evidence, + registrations=tuple(registrations), + includes=includes, + gwyddion_symbols=symbols, + gtk_glib_dependencies=dependencies, + mapped_total=totals[SymbolSupportStatus.MAPPED], + adapter_required_total=totals[SymbolSupportStatus.ADAPTER_REQUIRED], + unsupported_total=totals[SymbolSupportStatus.UNSUPPORTED], + unknown_total=totals[SymbolSupportStatus.UNKNOWN], + has_ui_dependency=any( + dependency.classification is SymbolClassification.GUI_GTK for dependency in dependencies + ), + likely_selection_dependencies=selection, + likely_parameter_system_dependencies=parameters, + likely_publication_logging_dependencies=publication, + conservative_mutation_hints=mutation_hints, + migration_blockers=tuple(blockers), + migration_warnings=warnings, + evidence_limitations=_LIMITATIONS, + ) diff --git a/src/spmkit/compat/gwyddion/symbols.py b/src/spmkit/compat/gwyddion/symbols.py new file mode 100644 index 0000000..5a3b937 --- /dev/null +++ b/src/spmkit/compat/gwyddion/symbols.py @@ -0,0 +1,105 @@ +"""Immutable source-location and symbol-inventory models for static auditing.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + + +class RegistrationKind(StrEnum): + """Gwyddion module registration families recognized by the source auditor.""" + + PROCESS = "process" + TOOL = "tool" + FILE = "file" + GRAPH = "graph" + LAYER = "layer" + VOLUME = "volume" + XYZ = "xyz" + CURVE_MAP = "curve-map" + UNKNOWN = "unknown" + + +class SymbolClassification(StrEnum): + """Conservative ownership-oriented symbol classes, not semantic proof.""" + + DATA_MODEL = "data/model" + PROCESS_NUMERICAL = "process/numerical" + CONTAINER_APPLICATION = "container/application" + SELECTION_LAYER = "selection/layer" + PARAMETERS_SETTINGS = "parameters/settings" + PUBLICATION_LOGGING = "publication/logging" + GUI_GTK = "GUI/GTK" + GLIB_RUNTIME = "GLib/runtime" + MODULE_REGISTRATION = "module/registration" + UNKNOWN = "unknown" + + +class SymbolSupportStatus(StrEnum): + """Current migration support state; unproven names stay unknown.""" + + MAPPED = "mapped" + ADAPTER_REQUIRED = "adapter-required" + UNSUPPORTED = "unsupported" + UNKNOWN = "unknown" + + +@dataclass(frozen=True) +class SourceLocation: + """One-based source position controlled entirely by the audit caller.""" + + source_path: str | None + line: int + column: int + + def __post_init__(self) -> None: + if self.line < 1 or self.column < 1: + raise ValueError("source locations are one-based") + + +@dataclass(frozen=True) +class SourceSpan: + """Half-open source range represented by stable start and end locations.""" + + start: SourceLocation + end: SourceLocation + + +@dataclass(frozen=True) +class IncludeReference: + """One literal preprocessor include, retained without preprocessing it.""" + + name: str + is_local: bool + span: SourceSpan + + +@dataclass(frozen=True) +class ModuleRegistration: + """A registration-looking function or macro call detected lexically.""" + + kind: RegistrationKind + callee: str + declared_name: str | None + span: SourceSpan + + +@dataclass(frozen=True) +class SymbolReference: + """A deduplicated symbol with every lexical occurrence retained in order.""" + + symbol: str + classification: SymbolClassification + support_status: SymbolSupportStatus + occurrences: tuple[SourceSpan, ...] + call_occurrences: tuple[SourceSpan, ...] + + +@dataclass(frozen=True) +class DependencyReference: + """A GTK/GLib dependency inventory entry with ordered occurrences.""" + + name: str + classification: SymbolClassification + support_status: SymbolSupportStatus + occurrences: tuple[SourceSpan, ...] diff --git a/src/spmkit/core/analysis/__init__.py b/src/spmkit/core/analysis/__init__.py index 10ed367..0cff264 100644 --- a/src/spmkit/core/analysis/__init__.py +++ b/src/spmkit/core/analysis/__init__.py @@ -1,6 +1,7 @@ """Análisis numérico de datos SPM.""" from spmkit.core.analysis import ( + background, calibration, forcecurve, forcevolume, @@ -14,10 +15,52 @@ simulation, spectral, ) +from spmkit.core.analysis.background import ( + BackgroundResult, + GwyddionArcDirection, + analyze_arc_revolution_background, + analyze_gwyddion_arc_revolution_background, + analyze_gwyddion_median_background, + analyze_gwyddion_sphere_revolution_background, + analyze_median_background, + analyze_polynomial_background, + analyze_rolling_ball_background, + analyze_sphere_revolution_background, + analyze_spline_background, + estimate_arc_revolution_background, + estimate_gwyddion_arc_revolution_background, + estimate_gwyddion_median_background, + estimate_gwyddion_sphere_revolution_background, + estimate_median_background, + estimate_polynomial_background, + estimate_rolling_ball_background, + estimate_sphere_revolution_background, + estimate_spline_background, + gwyddion_flat_disc_closing, + gwyddion_flat_disc_opening, + remove_arc_revolution_background, + remove_gwyddion_arc_revolution_background, + remove_gwyddion_median_background, + remove_gwyddion_sphere_revolution_background, + remove_median_background, + remove_polynomial_background, + remove_rolling_ball_background, + remove_sphere_revolution_background, + remove_spline_background, +) from spmkit.core.analysis.forcecurve import ForceCurveFit from spmkit.core.analysis.forcevolume import VolumeResult, analyze_volume from spmkit.core.analysis.grains import GrainResult from spmkit.core.analysis.kpfm import CPDResult +from spmkit.core.analysis.leveling import ( + GwyddionAlignRowsDirection, + GwyddionAlignRowsMaskMode, + gwyddion_align_rows_median, + gwyddion_align_rows_median_of_differences, + gwyddion_align_rows_trimmed_mean, + gwyddion_align_rows_trimmed_mean_of_differences, + gwyddion_path_level, +) from spmkit.core.analysis.mechanics import ( ForceCurve, IndentationResult, @@ -36,6 +79,45 @@ from spmkit.core.analysis.spectral import FractalResult, RadialPSD __all__ = [ + "background", + "BackgroundResult", + "GwyddionArcDirection", + "analyze_arc_revolution_background", + "analyze_gwyddion_arc_revolution_background", + "analyze_gwyddion_median_background", + "analyze_gwyddion_sphere_revolution_background", + "analyze_median_background", + "analyze_polynomial_background", + "analyze_rolling_ball_background", + "analyze_sphere_revolution_background", + "analyze_spline_background", + "estimate_arc_revolution_background", + "estimate_gwyddion_arc_revolution_background", + "estimate_gwyddion_median_background", + "estimate_gwyddion_sphere_revolution_background", + "gwyddion_flat_disc_closing", + "gwyddion_flat_disc_opening", + "GwyddionAlignRowsDirection", + "GwyddionAlignRowsMaskMode", + "gwyddion_align_rows_median", + "gwyddion_align_rows_median_of_differences", + "gwyddion_align_rows_trimmed_mean", + "gwyddion_align_rows_trimmed_mean_of_differences", + "gwyddion_path_level", + "estimate_median_background", + "estimate_polynomial_background", + "estimate_rolling_ball_background", + "estimate_sphere_revolution_background", + "estimate_spline_background", + "remove_arc_revolution_background", + "remove_gwyddion_arc_revolution_background", + "remove_gwyddion_median_background", + "remove_gwyddion_sphere_revolution_background", + "remove_median_background", + "remove_polynomial_background", + "remove_rolling_ball_background", + "remove_sphere_revolution_background", + "remove_spline_background", "calibration", "leveling", "roughness", diff --git a/src/spmkit/core/analysis/_flatten_base.py b/src/spmkit/core/analysis/_flatten_base.py new file mode 100644 index 0000000..e827105 --- /dev/null +++ b/src/spmkit/core/analysis/_flatten_base.py @@ -0,0 +1,1658 @@ +"""Pure numerical building blocks for automated flat-base levelling.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass(frozen=True) +class HeightDistribution: + """Height-distribution data compatible with Gwyddion's convention.""" + + centers: np.ndarray + density: np.ndarray + bin_width: float + minimum: float + maximum: float + sample_count: int + + +def _gwyddion_height_distribution(data: np.ndarray) -> HeightDistribution: + """Return a Gwyddion-compatible automatic height distribution.""" + array = np.asarray(data) + + if np.iscomplexobj(array): + raise TypeError("height distribution requires real-valued data") + if array.ndim != 2: + raise ValueError("height distribution requires a two-dimensional array") + if array.size == 0: + raise ValueError("height distribution requires at least one value") + + try: + values = np.asarray(array, dtype=float) + except (TypeError, ValueError) as exc: + raise TypeError("height distribution requires numeric data") from exc + + if not np.all(np.isfinite(values)): + raise ValueError("height distribution requires finite data") + + sample_count = int(values.size) + bin_count = max( + 2, + int(np.floor(3.49 * np.cbrt(sample_count) + 0.5)), + ) + + minimum = float(np.min(values)) + maximum = float(np.max(values)) + density = np.zeros(bin_count, dtype=float) + + if minimum == maximum: + histogram_range = abs(maximum) if minimum != 0.0 else 1.0 + bin_width = histogram_range / bin_count + centers = (np.arange(bin_count, dtype=float) + 0.5) * bin_width + density[0] = bin_count / histogram_range + else: + histogram_range = maximum - minimum + bin_width = histogram_range / bin_count + centers = minimum + (np.arange(bin_count, dtype=float) + 0.5) * bin_width + + flat_values = values.ravel() + indices = np.floor((flat_values - minimum) * bin_count / histogram_range).astype(np.intp) + + indices[flat_values == maximum] = bin_count - 1 + valid = (indices >= 0) & (indices < bin_count) + + counts = np.bincount( + indices[valid], + minlength=bin_count, + ).astype(float) + + counted = int(np.count_nonzero(valid)) + density = counts * bin_count / (histogram_range * max(counted, 1)) + + centers.setflags(write=False) + density.setflags(write=False) + + return HeightDistribution( + centers=centers, + density=density, + bin_width=float(bin_width), + minimum=minimum, + maximum=maximum, + sample_count=sample_count, + ) + + +@dataclass(frozen=True) +class BasePeakWindow: + """Histogram window and initial parameters for base-peak fitting.""" + + centers: np.ndarray + density: np.ndarray + peak_index: int + start_index: int + stop_index: int + initial_mean: float + initial_offset: float + initial_amplitude: float + initial_width: float + + +def _select_base_peak_window( + distribution: HeightDistribution, +) -> BasePeakWindow: + """Select Gwyddion's local histogram window around the dominant peak.""" + centers = np.asarray(distribution.centers, dtype=float) + density = np.asarray(distribution.density, dtype=float) + + if centers.ndim != 1 or density.ndim != 1: + raise ValueError("base peak estimation requires one-dimensional histogram data") + if centers.size != density.size: + raise ValueError("base peak estimation requires matching centers and density") + if centers.size < 7: + raise ValueError("base peak estimation requires at least seven histogram bins") + if not np.all(np.isfinite(centers)) or not np.all(np.isfinite(density)): + raise ValueError("base peak estimation requires finite histogram data") + if not np.isfinite(distribution.bin_width) or distribution.bin_width <= 0.0: + raise ValueError("base peak estimation requires a positive bin width") + + peak_index = int(np.argmax(density)) + peak_height = float(density[peak_index]) + + if peak_height <= 0.0: + raise ValueError("base peak estimation requires a positive histogram peak") + + threshold = 0.3 * peak_height + + start_index = peak_index + while start_index > 0: + if density[start_index] < threshold: + break + start_index -= 1 + + end_index = peak_index + last_index = density.size - 1 + while end_index < last_index: + if density[end_index] < threshold: + break + end_index += 1 + + sample_count = end_index + 1 - start_index + while sample_count < 7: + if start_index > 0: + start_index -= 1 + if end_index < last_index: + end_index += 1 + sample_count = end_index + 1 - start_index + + stop_index = end_index + 1 + selected_centers = np.array( + centers[start_index:stop_index], + dtype=float, + copy=True, + ) + selected_density = np.array( + density[start_index:stop_index], + dtype=float, + copy=True, + ) + + selected_centers.setflags(write=False) + selected_density.setflags(write=False) + + return BasePeakWindow( + centers=selected_centers, + density=selected_density, + peak_index=peak_index, + start_index=start_index, + stop_index=stop_index, + initial_mean=float(centers[peak_index]), + initial_offset=0.0, + initial_amplitude=peak_height, + initial_width=0.3 * sample_count * float(distribution.bin_width), + ) + + +@dataclass(frozen=True) +class BasePeakFit: + """Result and identifiability diagnostics for a Gaussian base peak.""" + + mean: float + rms: float + offset: float + amplitude: float + width: float + residual_norm: float + solver_success: bool + covariance_available: bool + evaluations: int + jacobian_rank: int + condition_estimate: float + + @property + def success(self) -> bool: + """Whether the fitted peak is both converged and identifiable.""" + return self.solver_success and self.covariance_available + + +def _packed_lower_index(row: int, column: int) -> int: + """Index a row-packed lower-triangular symmetric matrix.""" + return row * (row + 1) // 2 + column + + +def _gwyddion_cholesky_decompose( + dimension: int, + packed: np.ndarray, +) -> bool: + """Decompose a packed SPD matrix using Gwyddion's loop order.""" + for diagonal in range(dimension): + value = float(packed[_packed_lower_index(diagonal, diagonal)]) + + for index in range(diagonal): + factor = float(packed[_packed_lower_index(diagonal, index)]) + value -= factor * factor + + if value <= 0.0: + return False + + root = float(np.sqrt(value)) + packed[_packed_lower_index(diagonal, diagonal)] = root + + for row in range(diagonal + 1, dimension): + value = float(packed[_packed_lower_index(row, diagonal)]) + + for index in range(diagonal): + value -= float(packed[_packed_lower_index(diagonal, index)]) * float( + packed[_packed_lower_index(row, index)] + ) + + packed[_packed_lower_index(row, diagonal)] = value / root + + return True + + +def _gwyddion_cholesky_solve( + dimension: int, + decomposition: np.ndarray, + right_hand_side: np.ndarray, +) -> None: + """Solve an SPD system using Gwyddion's substitution order.""" + for row in range(dimension): + for column in range(row): + right_hand_side[row] -= ( + decomposition[_packed_lower_index(row, column)] * right_hand_side[column] + ) + + right_hand_side[row] /= decomposition[_packed_lower_index(row, row)] + + for row in range(dimension - 1, -1, -1): + for column in range(row + 1, dimension): + right_hand_side[row] -= ( + decomposition[_packed_lower_index(column, row)] * right_hand_side[column] + ) + + right_hand_side[row] /= decomposition[_packed_lower_index(row, row)] + + +def _gwyddion_cholesky_invert( + dimension: int, + packed: np.ndarray, +) -> bool: + """Invert a packed SPD matrix using Gwyddion's algorithm.""" + temporary = np.empty(dimension, dtype=float) + packed_offset = 0 + + for pivot in range(dimension - 1, -1, -1): + scale = float(packed[0]) + + if scale <= 0.0: + return False + + row_end = 0 + + for row in range(dimension - 1): + packed_offset = row_end + 1 + row_end += row + 2 + element = float(packed[packed_offset]) + + temporary[row] = -element / scale + + if row >= pivot: + temporary[row] = -temporary[row] + + for index in range(packed_offset, row_end): + packed[index - (row + 1)] = ( + packed[index + 1] + element * temporary[index - packed_offset] + ) + + packed[row_end] = 1.0 / scale + + for row in range(dimension - 1): + packed[packed_offset + row] = temporary[row] + + return True + + +def _fit_base_peak_gwyddion_lm( + window: BasePeakWindow, +) -> BasePeakFit: + """Fit a Gaussian using Gwyddion's nonlinear-fit semantics.""" + centers = np.asarray(window.centers, dtype=float) + density = np.asarray(window.density, dtype=float) + + if centers.ndim != 1 or density.ndim != 1: + raise ValueError("base peak fitting requires one-dimensional data") + if centers.size != density.size: + raise ValueError("base peak fitting requires matching centers and density") + if centers.size < 4: + raise ValueError("base peak fitting requires at least four samples") + if not np.all(np.isfinite(centers)) or not np.all(np.isfinite(density)): + raise ValueError("base peak fitting requires finite data") + + parameters = np.array( + [ + window.initial_mean, + window.initial_offset, + window.initial_amplitude, + window.initial_width, + ], + dtype=float, + ) + + if not np.all(np.isfinite(parameters)): + raise ValueError("base peak fitting requires finite initial parameters") + if parameters[3] == 0.0: + raise ValueError("base peak fitting requires a non-zero initial width") + + parameter_count = 4 + packed_size = parameter_count * (parameter_count + 1) // 2 + finite_limit = np.finfo(float).max + + damping = 1.0e-4 + damping_decrease = 0.4 + damping_increase = 10.0 + damping_zero_replacement = 1.0e-6 + convergence_tolerance = 1.0e-16 + derivative_scale = 1.0e-5 + maximum_iterations = 100 + maximum_unimproved = 12 + + evaluations = 0 + + def gaussian_value( + coordinate: float, + current: np.ndarray, + ) -> tuple[float, bool]: + nonlocal evaluations + evaluations += 1 + + width = float(current[3]) + + if width == 0.0: + return 0.0, False + + scaled = (float(coordinate) - float(current[0])) / width + + with np.errstate( + over="ignore", + invalid="ignore", + ): + value = float(current[2]) * float(np.exp(-(scaled * scaled))) + float(current[1]) + + return value, True + + def calculate_residuals( + current: np.ndarray, + ) -> tuple[np.ndarray, float, bool]: + residuals = np.empty(centers.size, dtype=float) + residual_sum = 0.0 + + for index in range(centers.size): + value, valid = gaussian_value( + float(centers[index]), + current, + ) + + if not valid: + return residuals, -1.0, False + + residual = value - float(density[index]) + residuals[index] = residual + residual_sum += residual * residual + + if not np.isfinite(residual_sum): + return residuals, -1.0, False + + return residuals, residual_sum, True + + def calculate_derivatives( + coordinate: float, + current: np.ndarray, + ) -> tuple[np.ndarray, bool]: + derivatives = np.empty(parameter_count, dtype=float) + perturbed = current.copy() + + for parameter_index in range(parameter_count): + step = abs(float(perturbed[parameter_index])) * derivative_scale + + if step == 0.0: + step = derivative_scale + + perturbed[parameter_index] -= step + left, valid = gaussian_value( + coordinate, + perturbed, + ) + + if not valid: + return derivatives, False + + perturbed[parameter_index] += 2.0 * step + right, valid = gaussian_value( + coordinate, + perturbed, + ) + + if not valid: + return derivatives, False + + derivatives[parameter_index] = (right - left) / (2.0 * step) + perturbed[parameter_index] = current[parameter_index] + + return derivatives, True + + def rank_and_condition( + current: np.ndarray, + ) -> tuple[int, float]: + jacobian = np.empty( + (centers.size, parameter_count), + dtype=float, + ) + + for index in range(centers.size): + derivatives, valid = calculate_derivatives( + float(centers[index]), + current, + ) + + if not valid: + return 0, float("inf") + + jacobian[index, :] = derivatives + + singular_values = np.linalg.svd( + jacobian, + compute_uv=False, + ) + + if singular_values.size == 0 or singular_values[0] == 0.0: + return 0, float("inf") + + tolerance = np.finfo(float).eps * max(jacobian.shape) * singular_values[0] + rank = int(np.count_nonzero(singular_values > tolerance)) + + if rank < parameter_count or singular_values[-1] <= tolerance: + return rank, float("inf") + + condition = float(singular_values[0] / singular_values[-1]) + return rank, condition + + residuals, residual_sum_new, evaluation_valid = calculate_residuals(parameters) + + if not evaluation_valid: + width = abs(float(parameters[3])) + + return BasePeakFit( + mean=float(parameters[0]), + rms=width / np.sqrt(2.0), + offset=float(parameters[1]), + amplitude=float(parameters[2]), + width=width, + residual_norm=float("inf"), + solver_success=False, + covariance_available=False, + evaluations=evaluations, + jacobian_rank=0, + condition_estimate=float("inf"), + ) + + best_parameters = parameters.copy() + residual_sum_best: float = finite_limit + + gradient = np.empty(parameter_count, dtype=float) + normal = np.empty(packed_size, dtype=float) + saved_normal: np.ndarray | None = None + saved_parameters: np.ndarray | None = None + + iteration = 0 + unimproved = 0 + finished = False + + while True: + if unimproved == 0: + damping *= damping_decrease + residual_sum_best = residual_sum_new + best_parameters = parameters.copy() + + gradient.fill(0.0) + normal.fill(0.0) + + for sample_index in range(centers.size): + derivatives, valid = calculate_derivatives( + float(centers[sample_index]), + parameters, + ) + + if not valid: + evaluation_valid = False + residual_sum_best = -1.0 + break + + for row in range(parameter_count): + gradient[row] += derivatives[row] * residuals[sample_index] + + packed_row = row * (row + 1) // 2 + + for column in range(row + 1): + normal[packed_row + column] += derivatives[row] * derivatives[column] + + if not evaluation_valid: + break + + saved_normal = normal.copy() + saved_parameters = parameters.copy() + + if saved_normal is None or saved_parameters is None: + evaluation_valid = False + residual_sum_best = -1.0 + break + + positive_definite = False + first_pass = True + + while not positive_definite and np.isfinite(damping): + if not first_pass: + normal[:] = saved_normal + else: + first_pass = False + + step = -gradient.copy() + + for parameter_index in range(parameter_count): + diagonal = parameter_index * (parameter_index + 3) // 2 + + if saved_normal[diagonal] == 0.0: + normal[diagonal] = damping + else: + normal[diagonal] = saved_normal[diagonal] * (1.0 + damping) + + positive_definite = _gwyddion_cholesky_decompose( + parameter_count, + normal, + ) + + if not positive_definite: + damping *= damping_increase + + if damping == 0.0: + damping = damping_zero_replacement + + if not np.isfinite(damping): + evaluation_valid = False + residual_sum_best = -1.0 + break + + _gwyddion_cholesky_solve( + parameter_count, + normal, + step, + ) + + parameters = saved_parameters + step + + unchanged = 0 + + for parameter_index in range(parameter_count): + if ( + abs(float(parameters[parameter_index]) - float(saved_parameters[parameter_index])) + == 0.0 + ): + unchanged += 1 + + if unchanged == parameter_count: + break + + ( + residuals, + residual_sum_new, + evaluation_valid, + ) = calculate_residuals(parameters) + + if not evaluation_valid: + residual_sum_best = -1.0 + break + + if residual_sum_new == 0.0 or ( + iteration > 2 + and abs((residual_sum_best - residual_sum_new) / residual_sum_best) + < convergence_tolerance + ): + finished = True + + if residual_sum_new >= residual_sum_best: + damping *= damping_increase + + if damping == 0.0: + damping = damping_zero_replacement + + unimproved += 1 + else: + unimproved = 0 + + if unimproved >= maximum_unimproved: + break + + iteration += 1 + + if iteration >= maximum_iterations: + break + + if finished: + break + + parameters = best_parameters.copy() + solver_evaluations = evaluations + + covariance_available = False + + if evaluation_valid and saved_normal is not None: + original_normal = saved_normal.copy() + covariance = saved_normal.copy() + + for parameter_index in range(parameter_count): + diagonal = parameter_index * (parameter_index + 3) // 2 + + if original_normal[diagonal] == 0.0: + covariance[diagonal] = 1.0 + + covariance_available = _gwyddion_cholesky_invert( + parameter_count, + covariance, + ) + + if not covariance_available: + covariance = original_normal.copy() + + for parameter_index in range(parameter_count): + diagonal = parameter_index * (parameter_index + 3) // 2 + + if original_normal[diagonal] == 0.0: + covariance[diagonal] = 1.0 + + covariance[diagonal] *= 1.0001 + + covariance_available = _gwyddion_cholesky_invert( + parameter_count, + covariance, + ) + + covariance_available = bool(covariance_available and np.all(np.isfinite(covariance))) + + finite_parameters = bool(np.all(np.isfinite(parameters))) + + if not finite_parameters: + covariance_available = False + + jacobian_rank, condition_estimate = rank_and_condition(parameters) + + width = abs(float(parameters[3])) + solver_success = bool(covariance_available and finite_parameters and residual_sum_best >= 0.0) + + residual_norm = float(np.sqrt(residual_sum_best)) if residual_sum_best >= 0.0 else float("inf") + + return BasePeakFit( + mean=float(parameters[0]), + rms=width / np.sqrt(2.0), + offset=float(parameters[1]), + amplitude=float(parameters[2]), + width=width, + residual_norm=residual_norm, + solver_success=solver_success, + covariance_available=covariance_available, + evaluations=solver_evaluations, + jacobian_rank=jacobian_rank, + condition_estimate=condition_estimate, + ) + + +def _fit_base_peak(window: BasePeakWindow) -> BasePeakFit: + """Fit Gwyddion's Gaussian parameterization to a selected peak window.""" + centers = np.asarray(window.centers, dtype=float) + density = np.asarray(window.density, dtype=float) + + if centers.ndim != 1 or density.ndim != 1: + raise ValueError("base peak fitting requires one-dimensional data") + if centers.size != density.size: + raise ValueError("base peak fitting requires matching centers and density") + if centers.size < 4: + raise ValueError("base peak fitting requires at least four samples") + if not np.all(np.isfinite(centers)) or not np.all(np.isfinite(density)): + raise ValueError("base peak fitting requires finite data") + + initial_width = abs(float(window.initial_width)) + initial = np.array( + [ + window.initial_mean, + window.initial_offset, + window.initial_amplitude, + initial_width, + ], + dtype=float, + ) + + if not np.all(np.isfinite(initial)): + raise ValueError("base peak fitting requires finite initial parameters") + if initial_width == 0.0: + raise ValueError("base peak fitting requires a non-zero initial width") + + coordinate_span = max(float(np.ptp(centers)), 1.0) + width_floor = np.finfo(float).eps * coordinate_span + + def gaussian_components( + parameters: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray, float]: + mean, _, _, width = parameters + safe_width = float(width) + + if abs(safe_width) < width_floor: + safe_width = np.copysign( + width_floor, + safe_width if safe_width != 0.0 else 1.0, + ) + + delta = centers - mean + scaled = delta / safe_width + exponential = np.exp(-np.square(scaled)) + return exponential, scaled, safe_width + + def residuals(parameters: np.ndarray) -> np.ndarray: + _, offset, amplitude, _ = parameters + exponential, _, _ = gaussian_components(parameters) + return offset + amplitude * exponential - density + + def jacobian(parameters: np.ndarray) -> np.ndarray: + _, _, amplitude, _ = parameters + exponential, scaled, safe_width = gaussian_components(parameters) + + return np.column_stack( + ( + 2.0 * amplitude * exponential * scaled / safe_width, + np.ones_like(centers), + exponential, + 2.0 * amplitude * exponential * np.square(scaled) / safe_width, + ) + ) + + def rank_and_condition(matrix: np.ndarray) -> tuple[int, float]: + singular_values = np.linalg.svd(matrix, compute_uv=False) + + if singular_values.size == 0 or singular_values[0] == 0.0: + return 0, float("inf") + + tolerance = np.finfo(float).eps * max(matrix.shape) * singular_values[0] + rank = int(np.count_nonzero(singular_values > tolerance)) + + if rank < 4 or singular_values[-1] <= tolerance: + return rank, float("inf") + + return rank, float(singular_values[0] / singular_values[-1]) + + density_scale = max(float(np.max(np.abs(density))), 1.0) + constant_tolerance = 32.0 * np.finfo(float).eps * density_scale + + if float(np.ptp(density)) <= constant_tolerance: + parameters = np.array( + [ + window.initial_mean, + float(np.mean(density)), + 0.0, + initial_width, + ], + dtype=float, + ) + jacobian_rank, condition_estimate = rank_and_condition(jacobian(parameters)) + + return BasePeakFit( + mean=float(parameters[0]), + rms=initial_width / np.sqrt(2.0), + offset=float(parameters[1]), + amplitude=0.0, + width=initial_width, + residual_norm=float(np.linalg.norm(residuals(parameters))), + solver_success=False, + covariance_available=False, + evaluations=0, + jacobian_rank=jacobian_rank, + condition_estimate=condition_estimate, + ) + + normalized_window = BasePeakWindow( + centers=window.centers, + density=window.density, + peak_index=window.peak_index, + start_index=window.start_index, + stop_index=window.stop_index, + initial_mean=window.initial_mean, + initial_offset=window.initial_offset, + initial_amplitude=window.initial_amplitude, + initial_width=initial_width, + ) + + return _fit_base_peak_gwyddion_lm(normalized_window) + + +@dataclass(frozen=True) +class BasePeakEstimate: + """Complete base-peak estimate with intermediate numerical evidence.""" + + distribution: HeightDistribution + window: BasePeakWindow + fit: BasePeakFit + + @property + def success(self) -> bool: + """Whether the Gaussian base peak is identifiable.""" + return self.fit.success + + @property + def mean(self) -> float: + """Fitted base-peak position.""" + return self.fit.mean + + @property + def rms(self) -> float: + """Fitted base-peak RMS width.""" + return self.fit.rms + + +def _estimate_base_peak(data: np.ndarray) -> BasePeakEstimate: + """Estimate the dominant base peak from a two-dimensional field.""" + distribution = _gwyddion_height_distribution(data) + window = _select_base_peak_window(distribution) + fit = _fit_base_peak(window) + + return BasePeakEstimate( + distribution=distribution, + window=window, + fit=fit, + ) + + +@dataclass(frozen=True) +class FacetPlaneEstimate: + """Dominant-plane estimate using Gwyddion's facet weighting.""" + + intercept: float + x_coefficient: float + y_coefficient: float + physical_slope_x: float + physical_slope_y: float + slope_scale_squared: float + cell_count: int + weight_sum: float + degenerate: bool + + +def _estimate_gwyddion_facet_plane( + data: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, +) -> FacetPlaneEstimate: + """Estimate one dominant-plane correction without modifying the field.""" + array = np.asarray(data) + + if np.issubdtype(array.dtype, np.bool_): + raise TypeError("facet-plane estimation requires real-valued data") + if np.iscomplexobj(array): + raise TypeError("facet-plane estimation requires real-valued data") + if array.ndim != 2: + raise ValueError("facet-plane estimation requires a two-dimensional array") + if array.shape[0] < 2 or array.shape[1] < 2: + raise ValueError("facet-plane estimation requires at least one pixel cell") + + try: + values = np.asarray(array, dtype=float) + except (TypeError, ValueError) as exc: + raise TypeError("facet-plane estimation requires numeric data") from exc + + if not np.all(np.isfinite(values)): + raise ValueError("facet-plane estimation requires finite data") + + def positive_pixel_size(value: float, *, name: str) -> float: + if isinstance(value, (bool, np.bool_)) or np.iscomplexobj(value): + raise TypeError(f"facet-plane estimation requires {name} to be real") + + try: + scalar = float(value) + except (TypeError, ValueError) as exc: + raise TypeError(f"facet-plane estimation requires {name} to be real") from exc + + if not np.isfinite(scalar) or scalar <= 0.0: + raise ValueError(f"facet-plane estimation requires {name} to be positive") + + return scalar + + dx = positive_pixel_size(pixel_size_x, name="pixel_size_x") + dy = positive_pixel_size(pixel_size_y, name="pixel_size_y") + + x_slopes = (values[1:, 1:] + values[:-1, 1:] - values[1:, :-1] - values[:-1, :-1]) / (2.0 * dx) + + y_slopes = (values[1:, :-1] + values[1:, 1:] - values[:-1, :-1] - values[:-1, 1:]) / (2.0 * dy) + + if not np.all(np.isfinite(x_slopes)) or not np.all(np.isfinite(y_slopes)): + raise ValueError("facet-plane estimation produced non-finite slopes") + + squared_slopes = np.square(x_slopes) + np.square(y_slopes) + cell_count = int(squared_slopes.size) + slope_scale_squared = float(np.mean(squared_slopes) / 20.0) + + if slope_scale_squared == 0.0: + return FacetPlaneEstimate( + intercept=0.0, + x_coefficient=0.0, + y_coefficient=0.0, + physical_slope_x=0.0, + physical_slope_y=0.0, + slope_scale_squared=0.0, + cell_count=cell_count, + weight_sum=float(cell_count), + degenerate=True, + ) + + weights = np.exp(-squared_slopes / slope_scale_squared) + weight_sum = float(np.sum(weights)) + + if not np.isfinite(weight_sum) or weight_sum <= 0.0: + raise ValueError("facet-plane estimation produced invalid weights") + + physical_slope_x = float(np.sum(x_slopes * weights) / weight_sum) + physical_slope_y = float(np.sum(y_slopes * weights) / weight_sum) + + x_coefficient = physical_slope_x * dx + y_coefficient = physical_slope_y * dy + rows, columns = values.shape + intercept = -0.5 * (x_coefficient * columns + y_coefficient * rows) + + return FacetPlaneEstimate( + intercept=float(intercept), + x_coefficient=float(x_coefficient), + y_coefficient=float(y_coefficient), + physical_slope_x=physical_slope_x, + physical_slope_y=physical_slope_y, + slope_scale_squared=slope_scale_squared, + cell_count=cell_count, + weight_sum=weight_sum, + degenerate=False, + ) + + +@dataclass(frozen=True) +class FacetStageIteration: + """One facet correction and the base peak estimated afterwards.""" + + index: int + plane: FacetPlaneEstimate + peak: BasePeakEstimate + + +@dataclass(frozen=True) +class FacetStageResult: + """Result and evidence from the five-step Flatten Base facet stage.""" + + corrected: np.ndarray + background: np.ndarray + initial_peak: BasePeakEstimate + iterations: tuple[FacetStageIteration, ...] + termination: str + + @property + def completed_iterations(self) -> int: + """Number of facet planes actually subtracted.""" + return len(self.iterations) + + +def _run_flatten_base_facet_stage( + data: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, +) -> FacetStageResult: + """Run the facet-levelling stage used by Gwyddion Flatten Base.""" + array = np.asarray(data) + + if np.issubdtype(array.dtype, np.bool_) or np.iscomplexobj(array): + raise TypeError("flatten-base facet stage requires real-valued data") + if array.ndim != 2: + raise ValueError("flatten-base facet stage requires a two-dimensional array") + if array.shape[0] < 2 or array.shape[1] < 2: + raise ValueError("flatten-base facet stage requires at least one pixel cell") + + try: + working = np.array(array, dtype=float, copy=True) + except (TypeError, ValueError) as exc: + raise TypeError("flatten-base facet stage requires numeric data") from exc + + if not np.all(np.isfinite(working)): + raise ValueError("flatten-base facet stage requires finite data") + + background = np.zeros_like(working) + initial_peak = _estimate_base_peak(working) + + rows, columns = working.shape + column_indices = np.arange(columns, dtype=float) + row_indices = np.arange(rows, dtype=float) + xx, yy = np.meshgrid(column_indices, row_indices) + + iterations: list[FacetStageIteration] = [] + termination = "maximum_iterations" + + for index in range(5): + plane = _estimate_gwyddion_facet_plane( + working, + pixel_size_x=pixel_size_x, + pixel_size_y=pixel_size_y, + ) + + if plane.degenerate: + termination = "degenerate_plane" + break + + plane_surface = plane.intercept + plane.x_coefficient * xx + plane.y_coefficient * yy + + if not np.all(np.isfinite(plane_surface)): + raise ValueError("flatten-base facet stage produced a non-finite plane") + + working -= plane_surface + background += plane_surface + + peak = _estimate_base_peak(working) + iterations.append( + FacetStageIteration( + index=index, + plane=plane, + peak=peak, + ) + ) + + if not peak.success: + termination = "peak_failure" + break + + corrected = np.array(working, dtype=float, copy=True) + accumulated_background = np.array( + background, + dtype=float, + copy=True, + ) + corrected.setflags(write=False) + accumulated_background.setflags(write=False) + + return FacetStageResult( + corrected=corrected, + background=accumulated_background, + initial_peak=initial_peak, + iterations=tuple(iterations), + termination=termination, + ) + + +def _grow_mask_conn4( + mask: np.ndarray, + *, + radius: int, +) -> np.ndarray: + """Reproduce Gwyddion 2.71 CONN4 mask growth. + + This intentionally follows ``gwy_data_field_grains_grow()`` with + ``from_border=FALSE``, including its special image-border behaviour. + It is therefore not equivalent to ordinary city-block dilation when + grains are absent from, or touch, the field boundary. + """ + values = np.asarray(mask) + + if values.ndim != 2: + raise ValueError("conn4 mask growth requires a two-dimensional mask") + if not np.issubdtype(values.dtype, np.bool_): + raise TypeError("conn4 mask growth requires a boolean mask") + if isinstance(radius, (bool, np.bool_)) or not isinstance( + radius, + (int, np.integer), + ): + raise TypeError("conn4 mask growth requires an integer radius") + + radius_value = int(radius) + + if radius_value < 0: + raise ValueError("conn4 mask growth requires a non-negative radius") + if values.shape[0] == 0 or values.shape[1] == 0: + raise ValueError("conn4 mask growth requires a non-empty mask") + + seeds = np.array(values, dtype=bool, copy=True) + + # Gwyddion returns immediately for growth amounts below 0.5. + if radius_value == 0: + return seeds + + rows, columns = seeds.shape + unreachable = int(np.iinfo(np.uint32).max) + + # grains_grow() duplicates and inverts the original mask before the + # distance transform. Consequently original mask pixels are zeros, + # while the surrounding region starts as G_MAXUINT. + distances = np.where( + seeds, + 0, + unreachable, + ).astype(np.int64, copy=False) + + queue: list[tuple[int, int]] = [] + + # init_erosion_4(..., from_border=FALSE) scans only interior pixels. + for row in range(1, rows - 1): + for column in range(1, columns - 1): + if distances[row, column] != unreachable: + continue + + if ( + distances[row - 1, column] == 0 + or distances[row, column - 1] == 0 + or distances[row, column + 1] == 0 + or distances[row + 1, column] == 0 + ): + distances[row, column] = 1 + queue.append((row, column)) + + distance = 1 + + while queue: + next_queue: list[tuple[int, int]] = [] + next_distance = distance + 1 + + for row, column in queue: + neighbours = ( + (row - 1, column), + (row, column - 1), + (row, column + 1), + (row + 1, column), + ) + + for neighbour_row, neighbour_column in neighbours: + if not (0 <= neighbour_row < rows and 0 <= neighbour_column < columns): + continue + + if distances[neighbour_row, neighbour_column] != unreachable: + continue + + distances[neighbour_row, neighbour_column] = next_distance + next_queue.append((neighbour_row, neighbour_column)) + + if not next_queue: + break + + queue = next_queue + distance = next_distance + + # Gwyddion's post-pass gives distance 1 to border pixels that were + # never reached by the interior erosion queues. + top_unreached = distances[0, :] == unreachable + distances[0, top_unreached] = 1 + + bottom_unreached = distances[-1, :] == unreachable + distances[-1, bottom_unreached] = 1 + + left_unreached = distances[:, 0] == unreachable + distances[left_unreached, 0] = 1 + + right_unreached = distances[:, -1] == unreachable + distances[right_unreached, -1] = 1 + + grown = seeds.copy() + grown[distances <= radius_value] = True + + return grown + + +@dataclass(frozen=True) +class FlattenBaseMask: + """Automatic positive-feature mask for one polynomial stage.""" + + degree: int + threshold: float + growth_radius: int + raw: np.ndarray + grown: np.ndarray + raw_count: int + grown_count: int + + +def _build_flatten_base_mask( + data: np.ndarray, + *, + peak: BasePeakEstimate, + degree: int, +) -> FlattenBaseMask: + """Build the automatic exclusion mask used by Flatten Base.""" + values = np.asarray(data) + + if np.issubdtype(values.dtype, np.bool_) or np.iscomplexobj(values): + raise TypeError("Flatten Base masking requires real-valued data") + if values.ndim != 2: + raise ValueError("Flatten Base masking requires a two-dimensional array") + if isinstance(degree, (bool, np.bool_)) or not isinstance( + degree, + (int, np.integer), + ): + raise TypeError("Flatten Base masking requires an integer degree") + + degree_value = int(degree) + + if degree_value < 0: + raise ValueError("Flatten Base masking requires a non-negative degree") + + try: + numeric = np.asarray(values, dtype=float) + except (TypeError, ValueError) as exc: + raise TypeError("Flatten Base masking requires numeric data") from exc + + if not np.all(np.isfinite(numeric)): + raise ValueError("Flatten Base masking requires finite data") + + mean = float(peak.mean) + rms = float(peak.rms) + + if not np.isfinite(mean) or not np.isfinite(rms): + raise ValueError("Flatten Base masking requires finite peak parameters") + if rms < 0.0: + raise ValueError("Flatten Base masking requires non-negative peak RMS") + + threshold = mean + 3.0 * rms + growth_radius = 1 + degree_value // 2 + + raw = np.array( + numeric > threshold, + dtype=bool, + copy=True, + ) + grown = np.array( + _grow_mask_conn4( + raw, + radius=growth_radius, + ), + dtype=bool, + copy=True, + ) + + raw_count = int(np.count_nonzero(raw)) + grown_count = int(np.count_nonzero(grown)) + + raw.setflags(write=False) + grown.setflags(write=False) + + return FlattenBaseMask( + degree=degree_value, + threshold=float(threshold), + growth_radius=growth_radius, + raw=raw, + grown=grown, + raw_count=raw_count, + grown_count=grown_count, + ) + + +@dataclass(frozen=True) +class FlattenBasePolynomialIteration: + """Evidence from one masked polynomial correction.""" + + degree: int + powers: tuple[tuple[int, int], ...] + mask: FlattenBaseMask | None + selected_count: int + coefficients: np.ndarray + rank: int + singular_values: np.ndarray + background: np.ndarray + corrected: np.ndarray + peak: BasePeakEstimate + applied: bool = True + + +def _run_flatten_base_polynomial_iteration( + data: np.ndarray, + *, + peak: BasePeakEstimate, + degree: int, +) -> FlattenBasePolynomialIteration: + """Run one masked polynomial correction used by Flatten Base.""" + values = np.asarray(data) + + if np.issubdtype(values.dtype, np.bool_) or np.iscomplexobj(values): + raise TypeError("Flatten Base polynomial iteration requires real-valued data") + if values.ndim != 2: + raise ValueError("Flatten Base polynomial iteration requires " "a two-dimensional array") + if values.size == 0: + raise ValueError("Flatten Base polynomial iteration requires non-empty data") + if isinstance(degree, (bool, np.bool_)) or not isinstance( + degree, + (int, np.integer), + ): + raise TypeError("Flatten Base polynomial iteration requires an integer degree") + + degree_value = int(degree) + + if degree_value < 0: + raise ValueError("Flatten Base polynomial iteration requires " "a non-negative degree") + + try: + numeric = np.asarray(values, dtype=float) + except (TypeError, ValueError) as exc: + raise TypeError("Flatten Base polynomial iteration requires numeric data") from exc + + if not np.all(np.isfinite(numeric)): + raise ValueError("Flatten Base polynomial iteration requires finite data") + + if float(np.max(numeric)) <= float(np.min(numeric)): + background = np.zeros_like(numeric) + corrected = np.array(numeric, dtype=float, copy=True) + coefficients = np.empty(0, dtype=float) + singular_values = np.empty(0, dtype=float) + + updated_peak = _estimate_base_peak(corrected) + + background.setflags(write=False) + corrected.setflags(write=False) + coefficients.setflags(write=False) + singular_values.setflags(write=False) + + return FlattenBasePolynomialIteration( + degree=degree_value, + powers=(), + mask=None, + selected_count=0, + coefficients=coefficients, + rank=0, + singular_values=singular_values, + background=background, + corrected=corrected, + peak=updated_peak, + applied=False, + ) + + automatic_mask = _build_flatten_base_mask( + numeric, + peak=peak, + degree=degree_value, + ) + degree_value = automatic_mask.degree + + powers = tuple( + (x_power, y_power) + for x_power in range(degree_value + 1) + for y_power in range(degree_value + 1 - x_power) + ) + + selection = np.logical_not(automatic_mask.grown) + selected_count = int(np.count_nonzero(selection)) + + from spmkit.core.analysis.leveling import ( + _fit_polynomial_surface_data, + ) + + ( + fitted_background, + fitted_coefficients, + rank, + fitted_singular_values, + ) = _fit_polynomial_surface_data( + data, + powers=powers, + selection=selection, + operation=f"Flatten Base degree {degree_value}", + ) + + values = np.asarray(data, dtype=float) + background = np.array( + fitted_background, + dtype=float, + copy=True, + ) + coefficients = np.array( + fitted_coefficients, + dtype=float, + copy=True, + ) + singular_values = np.array( + fitted_singular_values, + dtype=float, + copy=True, + ) + + if background.shape != values.shape: + raise ValueError("Flatten Base polynomial fit returned an invalid background shape") + if coefficients.ndim != 1 or coefficients.size != len(powers): + raise ValueError("Flatten Base polynomial fit returned invalid coefficients") + if singular_values.ndim != 1: + raise ValueError("Flatten Base polynomial fit returned invalid singular values") + if not np.all(np.isfinite(background)): + raise ValueError("Flatten Base polynomial fit returned a non-finite background") + if not np.all(np.isfinite(coefficients)): + raise ValueError("Flatten Base polynomial fit returned non-finite coefficients") + if not np.all(np.isfinite(singular_values)): + raise ValueError("Flatten Base polynomial fit returned non-finite singular values") + + corrected = np.array( + values - background, + dtype=float, + copy=True, + ) + updated_peak = _estimate_base_peak(corrected) + + coefficients.setflags(write=False) + singular_values.setflags(write=False) + background.setflags(write=False) + corrected.setflags(write=False) + + return FlattenBasePolynomialIteration( + degree=degree_value, + powers=powers, + mask=automatic_mask, + selected_count=selected_count, + coefficients=coefficients, + rank=int(rank), + singular_values=singular_values, + background=background, + corrected=corrected, + peak=updated_peak, + ) + + +@dataclass(frozen=True) +class FlattenBasePolynomialStage: + """Evidence from the complete degree 2–5 polynomial stage.""" + + corrected: np.ndarray + background: np.ndarray + initial_peak: BasePeakEstimate + iterations: tuple[FlattenBasePolynomialIteration, ...] + termination: str + + @property + def attempted_degrees(self) -> tuple[int, ...]: + """Polynomial degrees attempted by the stage.""" + return tuple(iteration.degree for iteration in self.iterations) + + @property + def completed_degrees(self) -> tuple[int, ...]: + """Polynomial degrees that actually subtracted a background.""" + return tuple( + iteration.degree for iteration in self.iterations if getattr(iteration, "applied", True) + ) + + +def _run_flatten_base_polynomial_stage( + data: np.ndarray, + *, + peak: BasePeakEstimate, +) -> FlattenBasePolynomialStage: + """Run the degree 2, 3, 4 and 5 Flatten Base corrections.""" + values = np.asarray(data) + + if np.issubdtype(values.dtype, np.bool_) or np.iscomplexobj(values): + raise TypeError("Flatten Base polynomial stage requires real-valued data") + if values.ndim != 2: + raise ValueError("Flatten Base polynomial stage requires a two-dimensional array") + + try: + working = np.array(values, dtype=float, copy=True) + except (TypeError, ValueError) as exc: + raise TypeError("Flatten Base polynomial stage requires numeric data") from exc + + if not np.all(np.isfinite(working)): + raise ValueError("Flatten Base polynomial stage requires finite data") + + accumulated_background = np.zeros_like(working) + iterations: list[FlattenBasePolynomialIteration] = [] + current_peak = peak + termination = "completed" + + for degree in (2, 3, 4, 5): + iteration = _run_flatten_base_polynomial_iteration( + working, + peak=current_peak, + degree=degree, + ) + + iteration_background = np.asarray( + iteration.background, + dtype=float, + ) + iteration_corrected = np.asarray( + iteration.corrected, + dtype=float, + ) + + if iteration_background.shape != working.shape: + raise ValueError( + "Flatten Base polynomial iteration returned " "an invalid background shape" + ) + if iteration_corrected.shape != working.shape: + raise ValueError( + "Flatten Base polynomial iteration returned " "an invalid corrected shape" + ) + if not np.all(np.isfinite(iteration_background)): + raise ValueError( + "Flatten Base polynomial iteration returned " "a non-finite background" + ) + if not np.all(np.isfinite(iteration_corrected)): + raise ValueError( + "Flatten Base polynomial iteration returned " "non-finite corrected data" + ) + + accumulated_background += iteration_background + working = np.array( + iteration_corrected, + dtype=float, + copy=True, + ) + iterations.append(iteration) + current_peak = iteration.peak + + if not current_peak.success: + termination = "peak_failure" + break + + corrected = np.array(working, dtype=float, copy=True) + background = np.array( + accumulated_background, + dtype=float, + copy=True, + ) + + corrected.setflags(write=False) + background.setflags(write=False) + + return FlattenBasePolynomialStage( + corrected=corrected, + background=background, + initial_peak=peak, + iterations=tuple(iterations), + termination=termination, + ) + + +@dataclass(frozen=True) +class FlattenBaseResult: + """Complete Flatten Base result and stage-level evidence.""" + + corrected: np.ndarray + background: np.ndarray + facet_stage: FacetStageResult + polynomial_stage: FlattenBasePolynomialStage + final_peak: BasePeakEstimate + mean_offset: float + minimum_offset: float + mean_centered: bool + + @property + def total_offset(self) -> float: + """Total constant offset subtracted after background leveling.""" + return self.mean_offset + self.minimum_offset + + +def _run_flatten_base( + data: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, +) -> FlattenBaseResult: + """Run the complete Gwyddion-compatible Flatten Base pipeline.""" + facet_stage = _run_flatten_base_facet_stage( + data, + pixel_size_x=pixel_size_x, + pixel_size_y=pixel_size_y, + ) + + if facet_stage.iterations: + polynomial_input_peak = facet_stage.iterations[-1].peak + else: + polynomial_input_peak = facet_stage.initial_peak + + polynomial_stage = _run_flatten_base_polynomial_stage( + facet_stage.corrected, + peak=polynomial_input_peak, + ) + + if polynomial_stage.iterations: + final_peak = polynomial_stage.iterations[-1].peak + else: + final_peak = polynomial_stage.initial_peak + + corrected = np.array( + polynomial_stage.corrected, + dtype=float, + copy=True, + ) + background = np.array( + facet_stage.background, + dtype=float, + copy=True, + ) + polynomial_background = np.asarray( + polynomial_stage.background, + dtype=float, + ) + + if background.shape != corrected.shape: + raise ValueError("Flatten Base facet stage returned incompatible shapes") + if polynomial_background.shape != corrected.shape: + raise ValueError("Flatten Base polynomial stage returned " "incompatible shapes") + if corrected.size == 0: + raise ValueError("Flatten Base requires non-empty corrected data") + if not np.all(np.isfinite(corrected)): + raise ValueError("Flatten Base polynomial stage returned " "non-finite corrected data") + if not np.all(np.isfinite(background)): + raise ValueError("Flatten Base facet stage returned " "a non-finite background") + if not np.all(np.isfinite(polynomial_background)): + raise ValueError("Flatten Base polynomial stage returned " "a non-finite background") + + background += polynomial_background + + mean_centered = bool(final_peak.success) + mean_offset = float(final_peak.mean) if mean_centered else 0.0 + + if mean_centered: + corrected -= mean_offset + background += mean_offset + + remaining_minimum = float(np.min(corrected)) + minimum_offset = remaining_minimum if remaining_minimum > 0.0 else 0.0 + + if minimum_offset > 0.0: + corrected -= minimum_offset + background += minimum_offset + + corrected.setflags(write=False) + background.setflags(write=False) + + return FlattenBaseResult( + corrected=corrected, + background=background, + facet_stage=facet_stage, + polynomial_stage=polynomial_stage, + final_peak=final_peak, + mean_offset=mean_offset, + minimum_offset=minimum_offset, + mean_centered=mean_centered, + ) diff --git a/src/spmkit/core/analysis/_gwyddion_align_rows_statistics.py b/src/spmkit/core/analysis/_gwyddion_align_rows_statistics.py new file mode 100644 index 0000000..6d9aef5 --- /dev/null +++ b/src/spmkit/core/analysis/_gwyddion_align_rows_statistics.py @@ -0,0 +1,373 @@ +"""Private portable Gwyddion 2.71 Align Rows statistics kernel. + +This module intentionally implements only the four source-confirmed row-shift +statistics methods. It is not a public API and does not emulate the installed +package's compiler-specific reassociation profile. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from enum import IntEnum +from typing import cast + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +FloatArray = NDArray[np.float64] + + +class _GwyddionAlignRowsMethod(IntEnum): + """The four source-confirmed Align Rows row-shift methods.""" + + MEDIAN = 1 + MEDIAN_OF_DIFFERENCES = 2 + TRIMMED_MEAN = 5 + TRIMMED_MEAN_OF_DIFFERENCES = 6 + + +class _GwyddionMaskMode(IntEnum): + """Gwyddion's stored mask enum, including its source value order.""" + + EXCLUDE = 0 + INCLUDE = 1 + IGNORE = 2 + + +class _GwyddionAlignRowsDirection(IntEnum): + """Source row orientation before optional transpose/restore.""" + + HORIZONTAL = 0 + VERTICAL = 1 + + +@dataclass(frozen=True) +class _GwyddionAlignRowsStatisticsResult: + """Corrected private result with optional extracted background diagnostics.""" + + corrected: FloatArray + background: FloatArray | None + correction_sequence: FloatArray + + +def _validated_field(value: ArrayLike, *, label: str) -> FloatArray: + try: + source = np.asarray(value) + except (TypeError, ValueError) as exc: + raise TypeError(f"Gwyddion Align Rows {label} must be array-compatible") from exc + if source.ndim != 2: + raise ValueError(f"Gwyddion Align Rows {label} must be two-dimensional") + if 0 in source.shape: + raise ValueError(f"Gwyddion Align Rows {label} must have non-empty dimensions") + if not np.issubdtype(source.dtype, np.number) or np.iscomplexobj(source): + raise TypeError(f"Gwyddion Align Rows {label} must contain real numeric values") + values = np.array(source, dtype=np.float64, order="C", copy=True) + if not np.isfinite(values).all(): + raise ValueError(f"Gwyddion Align Rows {label} must be finite") + return values + + +def _validated_mask(value: ArrayLike | None, shape: tuple[int, int]) -> FloatArray | None: + if value is None: + return None + mask = _validated_field(value, label="mask") + if mask.shape != shape: + raise ValueError("Gwyddion Align Rows mask shape must match data") + return mask + + +def _validated_enum(value: object, enum_type: type[IntEnum], label: str) -> IntEnum: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer, IntEnum)): + raise TypeError(f"Gwyddion Align Rows {label} must be an integer enum value") + try: + return enum_type(int(value)) + except ValueError as exc: + allowed = ", ".join(str(int(member)) for member in enum_type) + raise ValueError(f"Gwyddion Align Rows {label} must be one of {allowed}") from exc + + +def _validated_trim_fraction(value: object) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, (int, float, np.integer, np.floating) + ): + raise TypeError("Gwyddion Align Rows trim_fraction must be a real scalar") + fraction = float(value) + if not math.isfinite(fraction): + raise ValueError("Gwyddion Align Rows trim_fraction must be finite") + if not 0.0 <= fraction <= 0.5: + raise ValueError( + "Gwyddion Align Rows trim_fraction must be in the inclusive range 0.0..0.5" + ) + return fraction + + +def _round_nonnegative(value: float) -> int: + """Return the source's non-negative ``floor(x + 0.5)`` conversion.""" + return math.floor(value + 0.5) + + +def _mean_in_order(values: list[float]) -> float: + if not values: + raise ValueError("Gwyddion Align Rows mean requires samples") + total = 0.0 + for value in values: + total = total + value + return total / len(values) + + +def _upper_median(values: list[float]) -> float: + if not values: + raise ValueError("Gwyddion Align Rows median requires samples") + ordered = sorted(values) + return ordered[len(ordered) // 2] + + +def _move_min_to_front(values: list[float]) -> None: + smallest = values[0] + for index in range(1, len(values)): + candidate = values[index] + if candidate < smallest: + values[index] = smallest + smallest = candidate + values[0] = smallest + + +def _move_max_to_back(values: list[float]) -> None: + largest = values[-1] + final = len(values) - 1 + for index in range(final): + candidate = values[index] + if candidate > largest: + values[index] = largest + largest = candidate + values[final] = largest + + +def _trimmed_mean_or_median(values: list[float], trim_fraction: float) -> float: + """Reduce selected samples in the frozen portable binary64 operation order.""" + count = len(values) + if not count: + raise ValueError("Gwyddion Align Rows reduction requires samples") + trim_count = _round_nonnegative(trim_fraction * count) + if 2 * trim_count + 1 >= count: + return _upper_median(values) + work = list(values) + if trim_count == 0: + return _mean_in_order(work) + if trim_count == 1: + if count % 2: + _move_min_to_front(work) + tail = work[1:] + _move_max_to_back(tail) + work[1:] = tail + else: + _move_max_to_back(work) + head = work[:-1] + _move_min_to_front(head) + work[:-1] = head + return _mean_in_order(work[1:-1]) + ordered = sorted(work) + return _mean_in_order(ordered[trim_count : count - trim_count]) + + +def _minimum_sample_count(width: int) -> int: + return _round_nonnegative(math.log(width) + 1.0) + + +def _selected_row_values( + row: FloatArray, mask_row: FloatArray | None, mode: _GwyddionMaskMode +) -> list[float]: + if mask_row is None or mode is _GwyddionMaskMode.IGNORE: + return [float(value) for value in row] + if mode is _GwyddionMaskMode.INCLUDE: + return [ + float(value) + for value, mask_value in zip(row, mask_row, strict=True) + if mask_value > 0.0 + ] + return [ + float(value) for value, mask_value in zip(row, mask_row, strict=True) if mask_value < 1.0 + ] + + +def _selected_global_values( + data: FloatArray, mask: FloatArray | None, mode: _GwyddionMaskMode +) -> list[float]: + """Select the source-confirmed absolute-method fallback population. + + The global Exclude fallback uses ``mask <= 0`` rather than the per-row + ``mask < 1`` predicate. This source distinction is retained deliberately. + """ + if mask is None or mode is _GwyddionMaskMode.IGNORE: + return [float(value) for value in data.ravel(order="C")] + values: list[float] = [] + for row, mask_row in zip(data, mask, strict=True): + for value, mask_value in zip(row, mask_row, strict=True): + if ( + mode is _GwyddionMaskMode.INCLUDE + and mask_value > 0.0 + or mode is _GwyddionMaskMode.EXCLUDE + and mask_value <= 0.0 + ): + values.append(float(value)) + return values + + +def _paired_differences( + data: FloatArray, mask: FloatArray | None, mode: _GwyddionMaskMode, row: int +) -> list[float]: + first = data[row] + second = data[row + 1] + if mask is None or mode is _GwyddionMaskMode.IGNORE: + return [float(second[column] - first[column]) for column in range(first.size)] + first_mask = mask[row] + second_mask = mask[row + 1] + differences: list[float] = [] + for column in range(first.size): + if mode is _GwyddionMaskMode.INCLUDE: + keep = first_mask[column] > 1.0 and second_mask[column] > 1.0 + else: + keep = first_mask[column] < 1.0 and second_mask[column] < 1.0 + if keep: + differences.append(float(second[column] - first[column])) + return differences + + +def _absolute_corrections( + data: FloatArray, mask: FloatArray | None, mode: _GwyddionMaskMode, trim_fraction: float +) -> FloatArray: + threshold = _minimum_sample_count(data.shape[1]) + global_values = _selected_global_values(data, mask, mode) + fallback = _upper_median(global_values) if global_values else 0.0 + shifts: list[float] = [] + for row in range(data.shape[0]): + selected = _selected_row_values(data[row], None if mask is None else mask[row], mode) + shifts.append( + _trimmed_mean_or_median(selected, trim_fraction) + if len(selected) >= threshold + else fallback + ) + offset = _mean_in_order(shifts) + return np.array([shift - offset for shift in shifts], dtype=np.float64, order="C") + + +def _slope_level(shifts: FloatArray) -> FloatArray: + count = float(shifts.size) + mean_index = (count - 1.0) / 2.0 + mean_index_square = (2.0 * count - 1.0) * (count - 1.0) / 6.0 + shift_values = [float(value) for value in shifts] + mean_shifts = _mean_in_order(shift_values) + index_weighted = 0.0 + for index, shift in enumerate(shift_values): + index_weighted = index_weighted + shift * index + index_weighted = index_weighted / count + denominator = mean_index_square - mean_index * mean_index + slope = (index_weighted - mean_shifts * mean_index) / denominator + intercept = (mean_shifts * mean_index_square - mean_index * index_weighted) / denominator + return np.array( + [shift - (intercept + slope * index) for index, shift in enumerate(shift_values)], + dtype=np.float64, + order="C", + ) + + +def _difference_corrections( + data: FloatArray, mask: FloatArray | None, mode: _GwyddionMaskMode, trim_fraction: float +) -> FloatArray: + threshold = _minimum_sample_count(data.shape[1]) + shifts = np.zeros(data.shape[0], dtype=np.float64) + for row in range(data.shape[0] - 1): + selected = _paired_differences(data, mask, mode, row) + shifts[row + 1] = ( + _trimmed_mean_or_median(selected, trim_fraction) if len(selected) >= threshold else 0.0 + ) + for row in range(1, shifts.size): + shifts[row] = shifts[row] + shifts[row - 1] + return _slope_level(shifts) + + +def _apply_corrections(data: FloatArray, corrections: FloatArray) -> FloatArray: + corrected = data.copy(order="C") + for row in range(corrected.shape[0]): + for column in range(corrected.shape[1]): + corrected[row, column] = corrected[row, column] - corrections[row] + return corrected + + +def _background_in_order(input_data: FloatArray, corrected: FloatArray) -> FloatArray: + background = np.empty_like(input_data, order="C") + for row in range(input_data.shape[0]): + for column in range(input_data.shape[1]): + background[row, column] = input_data[row, column] - corrected[row, column] + return background + + +def _gwyddion_align_rows_statistics_result( + data: ArrayLike, + *, + method: object, + masking_mode: object, + direction: object, + trim_fraction: object, + mask: ArrayLike | None = None, + extract_background: object = False, +) -> _GwyddionAlignRowsStatisticsResult: + """Compute one private portable Align Rows statistics result without input mutation.""" + values = _validated_field(data, label="data") + validated_mask = _validated_mask(mask, values.shape) + selected_method = cast( + _GwyddionAlignRowsMethod, + _validated_enum(method, _GwyddionAlignRowsMethod, "method"), + ) + selected_mode = cast( + _GwyddionMaskMode, + _validated_enum(masking_mode, _GwyddionMaskMode, "masking_mode"), + ) + selected_direction = cast( + _GwyddionAlignRowsDirection, + _validated_enum(direction, _GwyddionAlignRowsDirection, "direction"), + ) + fraction = _validated_trim_fraction(trim_fraction) + if not isinstance(extract_background, (bool, np.bool_)): + raise TypeError("Gwyddion Align Rows extract_background must be boolean") + + effective_mask = ( + None + if validated_mask is None or selected_mode is _GwyddionMaskMode.IGNORE + else validated_mask + ) + if selected_direction is _GwyddionAlignRowsDirection.HORIZONTAL: + working = values + working_mask = effective_mask + else: + working = np.ascontiguousarray(values.T, dtype=np.float64) + working_mask = ( + None + if effective_mask is None + else np.ascontiguousarray(effective_mask.T, dtype=np.float64) + ) + + if selected_method in (_GwyddionAlignRowsMethod.MEDIAN, _GwyddionAlignRowsMethod.TRIMMED_MEAN): + reduction_fraction = 0.5 if selected_method is _GwyddionAlignRowsMethod.MEDIAN else fraction + corrections = _absolute_corrections( + working, working_mask, selected_mode, reduction_fraction + ) + else: + reduction_fraction = ( + 0.5 if selected_method is _GwyddionAlignRowsMethod.MEDIAN_OF_DIFFERENCES else fraction + ) + corrections = _difference_corrections( + working, working_mask, selected_mode, reduction_fraction + ) + + corrected_working = _apply_corrections(working, corrections) + corrected = ( + corrected_working + if selected_direction is _GwyddionAlignRowsDirection.HORIZONTAL + else np.ascontiguousarray(corrected_working.T) + ) + background = _background_in_order(values, corrected) if extract_background else None + return _GwyddionAlignRowsStatisticsResult( + corrected=corrected, background=background, correction_sequence=corrections + ) diff --git a/src/spmkit/core/analysis/_gwyddion_arc_revolution.py b/src/spmkit/core/analysis/_gwyddion_arc_revolution.py new file mode 100644 index 0000000..acd6fc6 --- /dev/null +++ b/src/spmkit/core/analysis/_gwyddion_arc_revolution.py @@ -0,0 +1,519 @@ +"""Numerical kernels compatible with Gwyddion's Revolve Arc operation. + +This module implements the numerical semantics of the Gwyddion 2.71 +``arc-revolve`` process independently in NumPy. It intentionally remains +separate from SPMKit's physical arc-revolution estimator because the two +operations use different radius, scaling, and boundary conventions. +""" + +from __future__ import annotations + +import math +from typing import Literal + +import numpy as np +from numpy.typing import NDArray + +FloatArray = NDArray[np.float64] +GwyddionArcDirection = Literal["horizontal", "vertical", "both"] + + +def _gwyddion_round_positive(value: object) -> int: + """Round a finite non-negative scalar using ``floor(value + 0.5)``. + + Gwyddion's ``GWY_ROUND`` macro does not use bankers' rounding. In + particular, ``2.5`` becomes ``3`` and ``4.5`` becomes ``5``. + """ + value_data = np.asarray(value) + + if ( + value_data.ndim != 0 + or not np.issubdtype(value_data.dtype, np.number) + or np.iscomplexobj(value_data) + or isinstance(value, (bool, np.bool_)) + ): + raise TypeError("Gwyddion rounding requires a real scalar") + + numeric_value = float(value_data.item()) + + if not math.isfinite(numeric_value): + raise ValueError("Gwyddion rounding requires a finite scalar") + + if numeric_value < 0.0: + raise ValueError("Gwyddion rounding requires a non-negative scalar") + + return math.floor(numeric_value + 0.5) + + +def _make_gwyddion_arc( + radius: object, + maxres: object, +) -> FloatArray: + """Return the dimensionless arc generated by Gwyddion 2.71. + + Parameters + ---------- + radius: + Arc radius in samples, following Gwyddion's pixel-based convention. + maxres: + Maximum available profile resolution. The generated half-width is + ``GWY_ROUND(min(radius, maxres))``. + + Returns + ------- + numpy.ndarray + Read-only, symmetric ``float64`` arc with an odd number of samples. + + Notes + ----- + The very-flat-arc polynomial is reproduced with the same operation order + as the reference C implementation. Values whose normalized offset + exceeds one are clipped to one, matching Gwyddion's explicit branch. + """ + radius_data = np.asarray(radius) + + if ( + radius_data.ndim != 0 + or not np.issubdtype(radius_data.dtype, np.number) + or np.iscomplexobj(radius_data) + or isinstance(radius, (bool, np.bool_)) + ): + raise TypeError("Gwyddion arc radius must be a positive real scalar") + + radius_value = float(radius_data.item()) + + if not math.isfinite(radius_value): + raise ValueError("Gwyddion arc radius must be finite") + + if radius_value <= 0.0: + raise ValueError("Gwyddion arc radius must be positive") + + maxres_data = np.asarray(maxres) + + if ( + maxres_data.ndim != 0 + or not np.issubdtype(maxres_data.dtype, np.integer) + or isinstance(maxres, (bool, np.bool_)) + ): + raise TypeError("Gwyddion arc maxres must be a positive integer") + + maxres_value = int(maxres_data.item()) + + if maxres_value <= 0: + raise ValueError("Gwyddion arc maxres must be positive") + + size = _gwyddion_round_positive(min(radius_value, maxres_value)) + arc = np.empty(2 * size + 1, dtype=np.float64) + use_flat_arc_expansion = radius_value / 8.0 > maxres_value + + for offset in range(size + 1): + normalized_offset = offset / radius_value + + if use_flat_arc_expansion: + squared_offset = normalized_offset * normalized_offset + height = ( + squared_offset / 2.0 * (1.0 + squared_offset / 4.0 * (1.0 + squared_offset / 2.0)) + ) + elif normalized_offset > 1.0: + height = 1.0 + else: + height = 1.0 - math.sqrt(1.0 - normalized_offset * normalized_offset) + + arc[size + offset] = height + arc[size - offset] = height + + arc.setflags(write=False) + return arc + + +def _gwyddion_population_rms(data: np.ndarray) -> float: + """Return Gwyddion's population RMS with respect to the global mean. + + The mean and squared deviations are accumulated sequentially in C order, + matching ``gwy_data_field_get_rms()``. The divisor is the complete sample + count, equivalent to ``ddof=0``. + """ + data_array = np.asarray(data) + + if data_array.size == 0: + raise ValueError("Gwyddion RMS requires non-empty data") + + if not np.issubdtype(data_array.dtype, np.number) or np.iscomplexobj(data_array): + raise TypeError("Gwyddion RMS requires real numeric data") + + if not np.all(np.isfinite(data_array)): + raise ValueError("Gwyddion RMS requires finite data") + + flattened = np.asarray( + data_array, + dtype=np.float64, + ).ravel(order="C") + + total = 0.0 + for value in flattened: + total += float(value) + + mean = total / flattened.size + + squared_deviation_sum = 0.0 + for value in flattened: + deviation = float(value) - mean + squared_deviation_sum += deviation * deviation + + return math.sqrt(squared_deviation_sum / flattened.size) + + +def _moving_sums( + row: np.ndarray, + size: object, +) -> tuple[FloatArray, FloatArray]: + """Return Gwyddion-compatible moving sums and squared sums. + + ``size`` is Gwyddion's historical moving-window parameter. For ordinary + sizes the operation is equivalent to an asymmetric truncated window. + When the window becomes comparable to the complete row, the reference + enters its distinct ``Moving a whale`` control-flow branch; this behaviour + is reproduced explicitly rather than replaced by a conventional window. + + Notes + ----- + Gwyddion 2.71 accesses memory before the output buffer for certain + one-sample and out-of-domain combinations. Such undefined combinations + are rejected here. The horizontal kernel handles a one-sample processing + axis explicitly as identity. + """ + row_array = np.asarray(row) + + if row_array.ndim != 1: + raise ValueError("Gwyddion moving sums require a one-dimensional row") + + if row_array.size == 0: + raise ValueError("Gwyddion moving sums require a non-empty row") + + if not np.issubdtype(row_array.dtype, np.number) or np.iscomplexobj(row_array): + raise TypeError("Gwyddion moving sums require real numeric data") + + if not np.all(np.isfinite(row_array)): + raise ValueError("Gwyddion moving sums require finite data") + + size_data = np.asarray(size) + + if ( + size_data.ndim != 0 + or not np.issubdtype(size_data.dtype, np.integer) + or isinstance(size, (bool, np.bool_)) + ): + raise TypeError("Gwyddion moving-sum size must be a non-negative integer") + + size_value = int(size_data.item()) + + if size_value < 0: + raise ValueError("Gwyddion moving-sum size must be non-negative") + + values = np.asarray( + row_array, + dtype=np.float64, + ) + resolution = values.size + + sums = np.zeros(resolution, dtype=np.float64) + squared_sums = np.zeros(resolution, dtype=np.float64) + + left_half = size_value // 2 + right_half = 0 if size_value == 0 else (size_value - 1) // 2 + + # Exact historical shortcut. It is unreachable from make_arc() because + # the generated half-width never exceeds the processed resolution. + if right_half >= resolution: + first_value = float(values[0]) + sums.fill(first_value) + squared_sums.fill(first_value * first_value) + return sums, squared_sums + + phase_3b_start = resolution - 1 - right_half + if phase_3b_start <= 0 and phase_3b_start <= left_half: + raise ValueError( + "Gwyddion 2.71 moving sums are undefined for this " "resolution and window size" + ) + + # Phase 1: fill the first output element. + for index in range(right_half + 1): + value = float(values[index]) + sums[0] += value + squared_sums[0] += value * value + + # Phase 2: gather new values without dropping old ones. + phase_2_end = min( + left_half, + resolution - 1 - right_half, + ) + for index in range(1, phase_2_end + 1): + value = float(values[index + right_half]) + sums[index] = sums[index - 1] + value + squared_sums[index] = squared_sums[index - 1] + value * value + + # Phase 3a: move a complete window. + for index in range( + left_half + 1, + resolution - right_half, + ): + entering = float(values[index + right_half]) + leaving = float(values[index - left_half - 1]) + + sums[index] = sums[index - 1] + entering - leaving + squared_sums[index] = squared_sums[index - 1] + entering * entering - leaving * leaving + + # Phase 3b: a window larger than the available interior remains fixed. + for index in range( + phase_3b_start, + left_half + 1, + ): + sums[index] = sums[index - 1] + squared_sums[index] = squared_sums[index - 1] + + # Phase 4: lose values without gathering new ones. + for index in range( + max(left_half + 1, resolution - right_half), + resolution, + ): + leaving = float(values[index - left_half - 1]) + sums[index] = sums[index - 1] - leaving + squared_sums[index] = squared_sums[index - 1] - leaving * leaving + + return sums, squared_sums + + +def _gwyddion_arc_horizontal( + data: np.ndarray, + radius: object, +) -> FloatArray: + """Estimate a horizontal background using Gwyddion 2.71 semantics. + + The algorithm uses a global population RMS to scale the dimensionless arc, + clips downward protrusions against a local ``mean - 2.5*rms`` envelope, + and finds the minimum touching position using truncated edge support. + + A processing axis containing one sample is defined as identity. Gwyddion + 2.71 performs an out-of-bounds read for this degenerate geometry, so no + stable external numerical result exists to reproduce. + """ + data_array = np.asarray(data) + + if data_array.ndim != 2: + raise ValueError("Gwyddion horizontal arc requires two-dimensional data") + + if data_array.size == 0: + raise ValueError("Gwyddion horizontal arc requires non-empty data") + + if not np.issubdtype(data_array.dtype, np.number) or np.iscomplexobj(data_array): + raise TypeError("Gwyddion horizontal arc requires real numeric data") + + if not np.all(np.isfinite(data_array)): + raise ValueError("Gwyddion horizontal arc requires finite data") + + values = np.array( + data_array, + dtype=np.float64, + copy=True, + order="C", + ) + row_count, column_count = values.shape + + # Defined SPMKit behaviour for a reference implementation defect. + if column_count == 1: + values.setflags(write=False) + return values + + rms = _gwyddion_population_rms(values) + scale = rms / math.sqrt(2.0 / 3.0 - math.pi / 16.0) + + arc = _make_gwyddion_arc( + radius, + column_count, + ) + scaled_arc = np.asarray( + arc * -scale, + dtype=np.float64, + ) + half_width = scaled_arc.size // 2 + + weights, _ = _moving_sums( + np.ones(column_count, dtype=np.float64), + half_width, + ) + + background = np.empty_like(values) + clipped_row = np.empty(column_count, dtype=np.float64) + + for row_index in range(row_count): + source_row = values[row_index] + local_sums, local_squared_sums = _moving_sums( + source_row, + half_width, + ) + + for column_index in range(column_count): + local_mean = local_sums[column_index] / weights[column_index] + local_variance = ( + local_squared_sums[column_index] / weights[column_index] - local_mean * local_mean + ) + + local_rms = float("nan") if local_variance < 0.0 else math.sqrt(local_variance) + + lower_envelope = local_mean - 2.5 * local_rms + source_value = float(source_row[column_index]) + + # Preserve the argument ordering of GLib's MAX(a, b) macro. + clipped_row[column_index] = ( + source_value if source_value > lower_envelope else lower_envelope + ) + + for column_index in range(column_count): + first_offset = ( + max( + 0, + column_index - half_width, + ) + - column_index + ) + final_offset = ( + min( + column_index + half_width, + column_count - 1, + ) + - column_index + ) + + minimum = math.inf + + for offset in range( + first_offset, + final_offset + 1, + ): + candidate = -scaled_arc[half_width + offset] + clipped_row[column_index + offset] + + if candidate < minimum: + minimum = float(candidate) + + background[row_index, column_index] = minimum + + background.setflags(write=False) + return background + + +def _readonly_float_array(values: np.ndarray) -> FloatArray: + """Return an independent C-contiguous read-only ``float64`` array.""" + result = np.array( + values, + dtype=np.float64, + copy=True, + order="C", + ) + result.setflags(write=False) + return result + + +def _gwyddion_arc_background( + data: np.ndarray, + radius: object, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, +) -> FloatArray: + """Compose a complete Gwyddion-compatible Revolve Arc background. + + ``"vertical"`` is implemented by transposing the field, applying the + horizontal kernel, and transposing the result back. ``"both"`` applies + horizontal first and vertical second, matching Gwyddion 2.71. + + Inversion follows the mathematically consistent dual ``-B(-data)``. + Gwyddion 2.71 computes this background correctly for all directions, + including its defective horizontal-inverted wrapper route. + """ + if not isinstance(direction, str): + raise TypeError("Gwyddion arc direction must be a string") + + if direction not in ("horizontal", "vertical", "both"): + raise ValueError( + "Gwyddion arc direction must be one of " "'horizontal', 'vertical', or 'both'" + ) + + if not isinstance(inverted, (bool, np.bool_)): + raise TypeError("Gwyddion arc inverted must be a boolean") + + inverted_value = bool(inverted) + source = np.asarray(data) + + working = -np.asarray(source, dtype=np.float64) if inverted_value else source + + if direction == "horizontal": + background = _gwyddion_arc_horizontal( + working, + radius, + ) + elif direction == "vertical": + background = _gwyddion_arc_horizontal( + np.asarray(working).T, + radius, + ).T + else: + horizontal = _gwyddion_arc_horizontal( + working, + radius, + ) + background = _gwyddion_arc_horizontal( + horizontal.T, + radius, + ).T + + if inverted_value: + background = -background + + return _readonly_float_array(background) + + +def _gwyddion_arc_result( + data: np.ndarray, + radius: object, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, +) -> tuple[FloatArray, FloatArray]: + """Return background and corrected fields through one numerical route. + + The background is computed exactly once. The corrected field is then + defined by the reconstruction identity ``corrected = input - background``. + Both arrays are independent, C-contiguous, ``float64`` and read-only. + """ + background = _gwyddion_arc_background( + data, + radius, + direction=direction, + inverted=inverted, + ) + corrected = _readonly_float_array(np.asarray(data, dtype=np.float64) - background) + + return background, corrected + + +def _gwyddion_arc_corrected( + data: np.ndarray, + radius: object, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, +) -> FloatArray: + """Return the corrected field from the authoritative result route. + + Unlike the defective Gwyddion 2.71 horizontal-inverted wrapper, this + function always returns the scientifically consistent ``input-background`` + result while preserving the externally validated background. + """ + _, corrected = _gwyddion_arc_result( + data, + radius, + direction=direction, + inverted=inverted, + ) + + return corrected diff --git a/src/spmkit/core/analysis/_gwyddion_flat_disc_morphology.py b/src/spmkit/core/analysis/_gwyddion_flat_disc_morphology.py new file mode 100644 index 0000000..b882315 --- /dev/null +++ b/src/spmkit/core/analysis/_gwyddion_flat_disc_morphology.py @@ -0,0 +1,355 @@ +"""Diagnostic model of the Gwyddion min/max RLE reduction hierarchy. + +This is an executable-evidence model, not an oracle. It preserves the +precomputed ``Each``/``Even`` construction and every comparison site. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import lru_cache +from typing import Literal + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +Kind = Literal["each", "even"] + + +@dataclass(frozen=True) +class _GwyddionFlatDiscKernelSpec: + size_px: int + kernel_resolution: int + kernel_active_count: int + + +@dataclass(frozen=True) +class _GwyddionFlatDiscMorphologyResult: + opening: NDArray[np.float64] + closing: NDArray[np.float64] + kernel: _GwyddionFlatDiscKernelSpec + + +def _validated_gwyddion_flat_disc_size(size_px: object) -> int: + if isinstance(size_px, (bool, np.bool_)) or not isinstance(size_px, (int, np.integer)): + raise TypeError("size_px must be a Python or NumPy integer scalar; booleans are invalid") + value = int(size_px) + if not 2 <= value <= 31: + raise ValueError("size_px must be in the inclusive range 2..31") + return value + + +def _validated_gwyddion_flat_disc_data(data: ArrayLike) -> NDArray[np.float64]: + field = np.asarray(data, dtype=np.float64) + if field.ndim != 2: + raise ValueError("data must be exactly two-dimensional") + if 0 in field.shape: + raise ValueError("data dimensions must be non-empty") + if not np.isfinite(field).all(): + raise ValueError("data values must all be finite") + return np.array(field, dtype=np.float64, order="C", copy=True) + + +@dataclass +class Requirement: + needed: bool = False + even_even: bool = False + even_odd: bool = False + sublen1: int = 0 + sublen2: int = 0 + + +@dataclass +class Plan: + each: dict[int, Requirement] = field(default_factory=dict) + even: dict[int, Requirement] = field(default_factory=dict) + + def requirement(self, kind: Kind, length: int) -> Requirement: + mapping = self.even if kind == "even" else self.each + return mapping.setdefault(length, Requirement()) + + +def _set_requirement( + plan: Plan, + kind: Kind, + length: int, + sublen1: int, + sublen2: int, + even_odd: bool, + even_even: bool, +) -> None: + item = plan.requirement(kind, length) + item.sublen1 = sublen1 + item.sublen2 = sublen2 + item.even_odd = even_odd + item.even_even = even_even + + +def _need(plan: Plan, kind: Kind, length: int) -> bool: + item = plan.requirement(kind, length) + if item.needed: + return True + item.needed = True + return False + + +def _build_requirement(plan: Plan, length: int, even: bool) -> None: + kind: Kind = "even" if even else "each" + if _need(plan, kind, length): + return + if even: + if length == 2: + _set_requirement(plan, kind, length, 1, 1, False, False) + _build_requirement(plan, 1, False) + elif length % 4 == 0: + _set_requirement(plan, kind, length, length // 2, length // 2, False, True) + _build_requirement(plan, length // 2, True) + else: + _set_requirement(plan, kind, length, length // 2 - 1, length // 2 + 1, False, True) + _build_requirement(plan, length // 2 - 1, True) + _build_requirement(plan, length // 2 + 1, True) + return + if length == 1: + return + if length % 2 == 0: + for left in range(1, length // 2 + 1): + right = length - left + if plan.requirement("each", left).needed and plan.requirement("each", right).needed: + _set_requirement(plan, kind, length, left, right, False, False) + return + _set_requirement(plan, kind, length, length // 2, length // 2, False, False) + _build_requirement(plan, length // 2, False) + return + possible = 0 + for left in range(1, length // 2 + 1): + right = length - left + if plan.requirement("each", left).needed and plan.requirement("each", right).needed: + _set_requirement(plan, kind, length, left, right, False, False) + return + if plan.requirement("even", left).needed and plan.requirement("each", right).needed: + _set_requirement(plan, kind, length, left, right, True, False) + return + if plan.requirement("each", left).needed and plan.requirement("even", right).needed: + _set_requirement(plan, kind, length, right, left, True, False) + return + if plan.requirement("each", left).needed: + possible = left + if possible: + _set_requirement(plan, kind, length, possible, length - possible, False, False) + _build_requirement(plan, length - possible, False) + elif length % 4 == 1: + _set_requirement(plan, kind, length, length // 2, length // 2 + 1, True, False) + _build_requirement(plan, length // 2, True) + _build_requirement(plan, length // 2 + 1, False) + else: + _set_requirement(plan, kind, length, length // 2 + 1, length // 2, True, False) + _build_requirement(plan, length // 2 + 1, True) + _build_requirement(plan, length // 2, False) + + +def _second_on_equal(left: np.uint64, right: np.uint64, maximum: bool) -> np.uint64: + left_float = left.view(np.float64) + right_float = right.view(np.float64) + if maximum: + return right if right_float >= left_float else left + return right if right_float <= left_float else left + + +def _first_on_equal(left: np.uint64, right: np.uint64, maximum: bool) -> np.uint64: + left_float = left.view(np.float64) + right_float = right.view(np.float64) + if maximum: + return right if right_float > left_float else left + return right if right_float < left_float else left + + +def _compose_each(left: np.ndarray, right: np.ndarray, a: int, b: int, maximum: bool) -> np.ndarray: + target = np.zeros_like(left) + for index in range(left.size - (a + b) + 1): + target[index] = _second_on_equal(left[index], right[index + a], maximum) + return target + + +def _compose_even(left: np.ndarray, right: np.ndarray, a: int, b: int, maximum: bool) -> np.ndarray: + target = np.zeros_like(left) + for index in range(0, left.size - (a + b) + 1, 2): + target[index] = _second_on_equal(left[index], right[index + a], maximum) + return target + + +def _compose_even_odd( + even: np.ndarray, odd: np.ndarray, even_len: int, odd_len: int, maximum: bool +) -> np.ndarray: + target = np.zeros_like(odd) + count = odd.size - (even_len + odd_len) + even_one, odd_one = 0, even_len + even_two, odd_two = odd_len + 1, 1 + index = 0 + while index + 1 <= count: + target[index] = _second_on_equal(even[even_one], odd[odd_one], maximum) + index += 1 + even_one += 2 + odd_one += 2 + target[index] = _second_on_equal(even[even_two], odd[odd_two], maximum) + index += 1 + even_two += 2 + odd_two += 2 + if index <= count: + target[index] = _second_on_equal(even[even_one], odd[odd_one], maximum) + index += 1 + if index <= count: + target[index] = _second_on_equal(even[even_two], odd[odd_two], maximum) + return target + + +def _row_precomputations( + values: np.ndarray, lengths: tuple[int, ...], maximum: bool +) -> dict[int, np.ndarray]: + plan = Plan() + for length in sorted(set(lengths)): + _build_requirement(plan, length, False) + each: dict[int, np.ndarray] = {1: values.copy()} + even: dict[int, np.ndarray] = {} + max_each = max(plan.each, default=1) + max_even = max(plan.even, default=0) + for length in range(2, max_each + 1): + requirement = plan.requirement("each", length) + if requirement.needed: + if requirement.even_odd: + each[length] = _compose_even_odd( + even[requirement.sublen1], + each[requirement.sublen2], + requirement.sublen1, + requirement.sublen2, + maximum, + ) + else: + each[length] = _compose_each( + each[requirement.sublen1], + each[requirement.sublen2], + requirement.sublen1, + requirement.sublen2, + maximum, + ) + if length <= max_even: + requirement = plan.requirement("even", length) + if requirement.needed: + if requirement.even_even: + even[length] = _compose_even( + even[requirement.sublen1], + even[requirement.sublen2], + requirement.sublen1, + requirement.sublen2, + maximum, + ) + else: + even[length] = _compose_even(each[1], each[1], 1, 1, maximum) + return each + + +@lru_cache(maxsize=30) +def _mask(size_px: int) -> np.ndarray: + mask = np.zeros((size_px, size_px), dtype=np.uint8) + half = size_px / 2.0 + for row in range(size_px): + factor = ((row + 0.5) / half) * (2.0 - ((row + 0.5) / half)) + if factor > 0.0: + first = max(0, int(np.ceil(half * (1.0 - np.sqrt(factor)) - 0.5))) + last = min(size_px - 1, int(np.floor(half * (1.0 + np.sqrt(factor)) - 0.5))) + mask[row, first : last + 1] = 1 + return mask + + +def _segments(size_px: int, maximum: bool) -> tuple[tuple[int, int, int], ...]: + mask = _mask(size_px) + if maximum: + mask = mask[::-1, ::-1] + result = [] + for row in range(size_px): + columns = np.flatnonzero(mask[row]) + if columns.size: + result.append((row, int(columns[0]), int(columns.size))) + return tuple(result) + + +def filter_field(field: np.ndarray, size_px: int, maximum: bool) -> np.ndarray: + field = np.ascontiguousarray(field, dtype=np.float64) + rows, columns = field.shape + segments = _segments(size_px, maximum) + lengths = tuple(segment[2] for segment in segments) + up = size_px // 2 if maximum else (size_px - 1) // 2 + left = size_px // 2 if maximum else (size_px - 1) // 2 + right = size_px - 1 - left + result = np.empty(field.shape, dtype=np.uint64) + field_bits = field.view(np.uint64) + for output_row in range(rows): + per_row = [] + for kernel_row in range(size_px): + source_row = min(max(output_row + kernel_row - up, 0), rows - 1) + extended = np.concatenate( + ( + np.repeat(field_bits[source_row, 0], left), + field_bits[source_row], + np.repeat(field_bits[source_row, -1], right), + ) + ) + per_row.append(_row_precomputations(extended, lengths, maximum)) + for output_column in range(columns): + value: np.uint64 | None = None + for kernel_row, kernel_column, length in segments: + candidate = per_row[kernel_row][length][output_column + kernel_column] + value = candidate if value is None else _first_on_equal(value, candidate, maximum) + assert value is not None + result[output_row, output_column] = value + return result.view(np.float64) + + +def opening(field: np.ndarray, size_px: int) -> np.ndarray: + return filter_field(filter_field(field, size_px, False), size_px, True) + + +def closing(field: np.ndarray, size_px: int) -> np.ndarray: + return filter_field(filter_field(field, size_px, True), size_px, False) + + +def _gwyddion_flat_disc_kernel(size_px: object) -> _GwyddionFlatDiscKernelSpec: + value = _validated_gwyddion_flat_disc_size(size_px) + return _GwyddionFlatDiscKernelSpec( + size_px=value, + kernel_resolution=value, + kernel_active_count=int(_mask(value).sum()), + ) + + +def _gwyddion_flat_disc_extremum( + data: ArrayLike, + size_px: object, + *, + maximum: bool, +) -> NDArray[np.float64]: + field = _validated_gwyddion_flat_disc_data(data) + value = _validated_gwyddion_flat_disc_size(size_px) + return np.array(filter_field(field, value, maximum), dtype=np.float64, order="C") + + +def _gwyddion_flat_disc_morphology_result( + data: ArrayLike, + size_px: object, +) -> _GwyddionFlatDiscMorphologyResult: + field = _validated_gwyddion_flat_disc_data(data) + kernel = _gwyddion_flat_disc_kernel(size_px) + opening = np.array( + filter_field(filter_field(field, kernel.size_px, False), kernel.size_px, True), + dtype=np.float64, + order="C", + ) + closing = np.array( + filter_field(filter_field(field, kernel.size_px, True), kernel.size_px, False), + dtype=np.float64, + order="C", + ) + return _GwyddionFlatDiscMorphologyResult( + opening=opening, + closing=closing, + kernel=kernel, + ) diff --git a/src/spmkit/core/analysis/_gwyddion_path_level.py b/src/spmkit/core/analysis/_gwyddion_path_level.py new file mode 100644 index 0000000..be96f1d --- /dev/null +++ b/src/spmkit/core/analysis/_gwyddion_path_level.py @@ -0,0 +1,227 @@ +"""Private numerical kernel for the frozen Gwyddion 2.71 Path Level domain.""" + +from __future__ import annotations + +import math +from collections.abc import Sequence +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +FloatArray = NDArray[np.float64] +_NormalizedLine = tuple[int, int, int, int] + + +@dataclass(frozen=True) +class _GwyddionPathLevelLine: + """One ordered straight selection line in physical field coordinates.""" + + x0: float + y0: float + x1: float + y1: float + + +@dataclass(frozen=True) +class _GwyddionPathLevelResult: + """Independent Path Level output and source-visible numerical diagnostics.""" + + corrected: FloatArray + normalized_lines: tuple[_NormalizedLine, ...] + row_differences: FloatArray + cumulative_row_correction: FloatArray + thickness_px: int + + +def _validated_gwyddion_path_level_data(data: ArrayLike) -> FloatArray: + """Return a finite, non-empty C-contiguous float64 copy of a field.""" + try: + source = np.asarray(data) + except (TypeError, ValueError) as exc: + raise TypeError("Gwyddion Path Level requires array-compatible data") from exc + if source.ndim != 2: + raise ValueError("Gwyddion Path Level requires two-dimensional data") + if 0 in source.shape: + raise ValueError("Gwyddion Path Level requires non-empty dimensions") + if not np.issubdtype(source.dtype, np.number) or np.iscomplexobj(source): + raise TypeError("Gwyddion Path Level requires real numeric data") + values = np.array(source, dtype=np.float64, order="C", copy=True) + if not np.isfinite(values).all(): + raise ValueError("Gwyddion Path Level requires finite data") + return values + + +def _validated_gwyddion_path_level_lines( + lines: object, +) -> tuple[_GwyddionPathLevelLine, ...]: + """Validate an ordered, duplicate-preserving sequence of physical lines.""" + if isinstance(lines, (str, bytes)): + raise TypeError("Gwyddion Path Level lines must be numeric coordinate rows") + try: + source = np.asarray(lines) + except (TypeError, ValueError) as exc: + raise TypeError("Gwyddion Path Level lines must be array-compatible") from exc + if source.size == 0: + if source.ndim not in (1, 2): + raise ValueError("empty Gwyddion Path Level lines must be one- or two-dimensional") + return () + if source.ndim != 2 or source.shape[1] != 4: + raise ValueError("Gwyddion Path Level lines must have shape (n, 4)") + if not np.issubdtype(source.dtype, np.number) or np.iscomplexobj(source): + raise TypeError("Gwyddion Path Level lines must contain real numeric coordinates") + values = np.array(source, dtype=np.float64, order="C", copy=True) + if not np.isfinite(values).all(): + raise ValueError("Gwyddion Path Level lines must be finite") + return tuple(_GwyddionPathLevelLine(*(float(value) for value in row)) for row in values) + + +def _validated_gwyddion_path_level_thickness(thickness_px: object) -> int: + """Validate the source-supported Path Level thickness range.""" + if isinstance(thickness_px, (bool, np.bool_)) or not isinstance( + thickness_px, (int, np.integer) + ): + raise TypeError( + "Gwyddion Path Level thickness_px must be a Python or NumPy integer scalar; " + "booleans are invalid" + ) + value = int(thickness_px) + if not 1 <= value <= 128: + raise ValueError("Gwyddion Path Level thickness_px must be in the inclusive range 1..128") + return value + + +def _gwyddion_physical_to_pixel(value: float, resolution: int, real_extent: float) -> float: + """Map a physical coordinate to the Gwyddion data-field pixel coordinate.""" + return value * resolution / real_extent + + +def _gwyddion_c_trunc_div(numerator: int, denominator: int) -> int: + """Return C signed-integer division truncated toward zero.""" + if denominator == 0: + raise ZeroDivisionError("Gwyddion Path Level line division by zero") + magnitude = abs(numerator) // abs(denominator) + return -magnitude if (numerator < 0) != (denominator < 0) else magnitude + + +def _gwyddion_normalized_path_level_lines( + lines: Sequence[_GwyddionPathLevelLine], + *, + xres: int, + yres: int, + xreal: float, + yreal: float, +) -> tuple[_NormalizedLine, ...]: + """Convert ordered physical selections to source-equivalent integer endpoints.""" + result: list[_NormalizedLine] = [] + for line in lines: + x0 = math.floor(_gwyddion_physical_to_pixel(line.x0, xres, xreal)) + y0 = math.floor(_gwyddion_physical_to_pixel(line.y0, yres, yreal)) + x1 = math.floor(_gwyddion_physical_to_pixel(line.x1, xres, xreal)) + y1 = math.floor(_gwyddion_physical_to_pixel(line.y1, yres, yreal)) + if y0 > y1: + x0, x1 = x1, x0 + y0, y1 = y1, y0 + result.append( + ( + min(max(int(x0), 0), xres - 1), + min(max(math.floor(y0), 0), yres - 1), + min(max(int(x1), 0), xres - 1), + min(max(math.ceil(y1), 0), yres - 1), + ) + ) + return tuple(result) + + +def _validated_extent(value: object, name: str) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, (int, float, np.integer, np.floating) + ): + raise TypeError(f"Gwyddion Path Level {name} must be a real scalar") + try: + extent = float(value) + except (TypeError, ValueError) as exc: + raise TypeError(f"Gwyddion Path Level {name} must be a real scalar") from exc + if not math.isfinite(extent) or extent <= 0.0: + raise ValueError(f"Gwyddion Path Level {name} must be finite and positive") + return extent + + +def _line_column(line: _NormalizedLine, row: int) -> int: + x0, y0, x1, y1 = line + horizontal_span = x1 - x0 + vertical_span = y1 - y0 + orientation = 1 if vertical_span > 0 else -1 + numerator = (2 * (row - y0) + 1) * horizontal_span + orientation * vertical_span + denominator = 2 * orientation * vertical_span + return _gwyddion_c_trunc_div(numerator, denominator) + x0 + + +def _gwyddion_path_level_result( + data: ArrayLike, + lines: object, + *, + xreal: object, + yreal: object, + thickness_px: object, +) -> _GwyddionPathLevelResult: + """Compute the frozen Gwyddion 2.71 Path Level corrected field privately.""" + values = _validated_gwyddion_path_level_data(data) + physical_lines = _validated_gwyddion_path_level_lines(lines) + thickness = _validated_gwyddion_path_level_thickness(thickness_px) + horizontal_extent = _validated_extent(xreal, "xreal") + vertical_extent = _validated_extent(yreal, "yreal") + yres, xres = values.shape + normalized = _gwyddion_normalized_path_level_lines( + physical_lines, + xres=xres, + yres=yres, + xreal=horizontal_extent, + yreal=vertical_extent, + ) + changes = sorted( + [(line[1], False, identifier) for identifier, line in enumerate(normalized)] + + [(line[3], True, identifier) for identifier, line in enumerate(normalized)] + ) + active = [False] * len(normalized) + row_differences = np.zeros(yres, dtype=np.float64) + lower_reach = (thickness - 1) // 2 + upper_reach = thickness // 2 + change_index = 0 + + for row in range(yres): + if row: + total = np.float64(0.0) + count = 0 + for identifier, line in enumerate(normalized): + if active[identifier]: + column = _line_column(line, row) + first = max(0, column - lower_reach) + last = min(xres - 1, column + upper_reach) + for sample_column in range(first, last + 1): + difference = values[row, sample_column] - values[row - 1, sample_column] + total = total + difference + count += 1 + if count: + row_differences[row] = total / np.float64(count) + while change_index < len(changes) and changes[change_index][0] == row: + _, is_end, identifier = changes[change_index] + active[identifier] = not is_end + change_index += 1 + + cumulative = np.zeros(yres, dtype=np.float64) + running = np.float64(0.0) + for row in range(yres): + running = running + row_differences[row] + cumulative[row] = running + corrected = values.copy(order="C") + for row in range(yres): + for column in range(xres): + corrected[row, column] = corrected[row, column] - cumulative[row] + return _GwyddionPathLevelResult( + corrected=corrected, + normalized_lines=normalized, + row_differences=row_differences, + cumulative_row_correction=cumulative, + thickness_px=thickness, + ) diff --git a/src/spmkit/core/analysis/_gwyddion_sphere_revolution.py b/src/spmkit/core/analysis/_gwyddion_sphere_revolution.py new file mode 100644 index 0000000..88570df --- /dev/null +++ b/src/spmkit/core/analysis/_gwyddion_sphere_revolution.py @@ -0,0 +1,298 @@ +"""Numerical kernels compatible with Gwyddion's Revolve Sphere operation. + +This module implements the numerical semantics of the Gwyddion 2.71 +``sphere-revolve`` process independently in NumPy. It intentionally remains +separate from SPMKit's physical sphere-revolution estimator because the two +operations use different radius, scaling, and boundary conventions. +""" + +from __future__ import annotations + +import math +import sys + +import numpy as np +from numpy.typing import NDArray + +FloatArray = NDArray[np.float64] + + +def _validated_data_array(data: object, operation: str) -> FloatArray: + """Validate that input data is a real, finite, non-empty 2D array.""" + data_arr = np.asarray(data) + + if ( + data_arr.ndim != 2 + or data_arr.size == 0 + or not np.issubdtype(data_arr.dtype, np.number) + or np.iscomplexobj(data_arr) + or isinstance(data, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires data to be a real 2D array") + + float_arr = np.array(data_arr, dtype=np.float64, order="C", copy=True) + + if not np.all(np.isfinite(float_arr)): + raise ValueError(f"{operation} requires data to be finite") + + return float_arr + + +def _validated_radius(radius: object, operation: str) -> float: + """Validate that radius is a real scalar finite number between 1.0 and 1000.0.""" + radius_arr = np.asarray(radius) + + if ( + radius_arr.ndim != 0 + or not np.issubdtype(radius_arr.dtype, np.number) + or np.iscomplexobj(radius_arr) + or isinstance(radius, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires radius to be a real scalar") + + val = float(radius_arr.item()) + + if not math.isfinite(val): + raise ValueError(f"{operation} requires radius to be finite") + + if not 1.0 <= val <= 1000.0: + raise ValueError(f"{operation} requires radius to be between 1.0 and 1000.0 samples") + + return val + + +def _validated_inverted(inverted: object, operation: str) -> bool: + """Validate that inverted option is a boolean.""" + if not isinstance(inverted, (bool, np.bool_)): + raise TypeError(f"{operation} requires inverted to be a boolean") + return bool(inverted) + + +def _readonly_float_array(values: NDArray[np.float64]) -> FloatArray: + """Return a C-contiguous, read-only float64 copy of the input array.""" + array = np.array(values, dtype=np.float64, order="C", copy=True) + array.setflags(write=False) + return array + + +def _gwyddion_sphere_background( + data: FloatArray, + radius: float, +) -> FloatArray: + """Calculate Gwyddion 2.71 Sphere Revolution background on 2D float64 data. + + Parameters + ---------- + data: + Two-dimensional real finite float64 array. + radius: + Sphere radius in samples (1.0 through 1000.0). + + Returns + ------- + numpy.ndarray + Read-only C-contiguous float64 background matrix. + """ + array = _validated_data_array(data, "_gwyddion_sphere_background") + radius_val = _validated_radius(radius, "_gwyddion_sphere_background") + + yres, xres = array.shape + + # 1. Serial global mean (C-order) + total = 0.0 + for value in array.ravel(order="C"): + total += float(value) + mean = total / float(array.size) + + # 2. Serial global population RMS (C-order) + sum2 = 0.0 + for value in array.ravel(order="C"): + delta = float(value) - mean + sum2 += delta * delta + rms = math.sqrt(sum2 / float(array.size)) + + # 3. Global scaling parameter q + q = rms / math.sqrt(5.0 / 6.0) + + # 4. Discrete sphere dimensions + sphere_size = math.floor(min(radius_val, float(xres)) + 0.5) + sphere_resolution = 2 * sphere_size + 1 + local_filter_size = sphere_size // 2 + very_flat = (radius_val / 8.0) > float(xres) + + center = sphere_size + sphere_z = np.zeros((sphere_resolution, sphere_resolution), dtype=np.float64, order="C") + + # 5. Discrete sphere construction (quadrant loop) + for i in range(sphere_size + 1): + u = i / radius_val + for j in range(sphere_size + 1): + v = j / radius_val + r2 = u * u + v * v + if very_flat: + z = (r2 / 2.0) * (1.0 + (r2 / 4.0) * (1.0 + r2 / 2.0)) + else: + z = 2.0 if r2 > 1.0 else 1.0 - math.sqrt(1.0 - r2) + sphere_z[center - i, center - j] = z + sphere_z[center - i, center + j] = z + sphere_z[center + i, center - j] = z + sphere_z[center + i, center + j] = z + + # Correction 1: explicit scalar loop scaling + sphere_scaled = np.zeros( + (sphere_resolution, sphere_resolution), + dtype=np.float64, + order="C", + ) + for row in range(sphere_resolution): + for column in range(sphere_resolution): + sphere_scaled[row, column] = -q * float(sphere_z[row, column]) + + # 6. Direct local mean field + if local_filter_size == 0: + local_mean = np.array(array, dtype=np.float64, order="C", copy=True) + else: + neg_ext = (local_filter_size - 1) // 2 + pos_ext = local_filter_size // 2 + local_mean = np.zeros((yres, xres), dtype=np.float64, order="C") + for r in range(yres): + r_start = max(0, r - neg_ext) + r_stop = min(yres - 1, r + pos_ext) + for c in range(xres): + c_start = max(0, c - neg_ext) + c_stop = min(xres - 1, c + pos_ext) + sum_val = 0.0 + count = 0 + for rr in range(r_start, r_stop + 1): + for cc in range(c_start, c_stop + 1): + sum_val += float(array[rr, cc]) + count += 1 + local_mean[r, c] = sum_val / float(count) + + # 7. Direct local RMS field + if local_filter_size == 0: + local_rms = np.array(array, dtype=np.float64, order="C", copy=True) + elif local_filter_size == 1: + local_rms = np.zeros((yres, xres), dtype=np.float64, order="C") + else: + neg_ext = (local_filter_size - 1) // 2 + pos_ext = local_filter_size // 2 + local_rms = np.zeros((yres, xres), dtype=np.float64, order="C") + for r in range(yres): + r_start = max(0, r - neg_ext) + r_stop = min(yres - 1, r + pos_ext) + for c in range(xres): + c_start = max(0, c - neg_ext) + c_stop = min(xres - 1, c + pos_ext) + sum_val = 0.0 + sum_sq = 0.0 + count = 0 + for rr in range(r_start, r_stop + 1): + for cc in range(c_start, c_stop + 1): + val = float(array[rr, cc]) + sum_val += val + sum_sq += val * val + count += 1 + m = sum_val / float(count) + m_sq = sum_sq / float(count) + var = m_sq - m * m + if var < 0.0: + var = 0.0 + local_rms[r, c] = math.sqrt(var) + + # 8. Outlier-trimmed field T + trimmed = np.zeros((yres, xres), dtype=np.float64, order="C") + for r in range(yres): + for c in range(xres): + thresh = local_mean[r, c] - 2.5 * local_rms[r, c] + val = float(array[r, c]) + trimmed[r, c] = thresh if thresh > val else val + + # 9. Two-dimensional lower envelope minimization + bg = np.zeros((yres, xres), dtype=np.float64, order="C") + for r in range(yres): + for c in range(xres): + ifrom = max(0, r - sphere_size) - r + ito = min(r + sphere_size, yres - 1) - r + jfrom = max(0, c - sphere_size) - c + jto = min(c + sphere_size, xres - 1) - c + minimum = sys.float_info.max + for ii in range(ifrom, ito + 1): + for jj in range(jfrom, jto + 1): + sph_val = float(sphere_scaled[center + ii, center + jj]) + if sph_val >= -q: + data_val = float(trimmed[r + ii, c + jj]) + cand = data_val - sph_val + if cand < minimum: + minimum = cand + bg[r, c] = minimum + + return _readonly_float_array(bg) + + +def _gwyddion_sphere_result( + data: FloatArray, + radius: float, + *, + inverted: bool = False, +) -> tuple[FloatArray, FloatArray]: + """Calculate Gwyddion 2.71 Sphere Revolution (background, corrected) tuple. + + Parameters + ---------- + data: + Two-dimensional real finite float64 array. + radius: + Sphere radius in samples (1.0 through 1000.0). + inverted: + Apply safe dual inversion ``-B(-data)``. + + Returns + ------- + tuple[numpy.ndarray, numpy.ndarray] + Read-only float64 (background, corrected) array pair. + """ + array = _validated_data_array(data, "_gwyddion_sphere_result") + _validated_radius(radius, "_gwyddion_sphere_result") + inv_bool = _validated_inverted(inverted, "_gwyddion_sphere_result") + + if not inv_bool: + bg_arr = _gwyddion_sphere_background(array, radius) + bg_raw = np.asarray(bg_arr) + else: + negated = -array + neg_bg = _gwyddion_sphere_background(negated, radius) + bg_raw = -np.asarray(neg_bg) + + corr_raw = np.zeros(array.shape, dtype=np.float64, order="C") + yres, xres = array.shape + for r in range(yres): + for c in range(xres): + corr_raw[r, c] = float(array[r, c]) - float(bg_raw[r, c]) + + return _readonly_float_array(bg_raw), _readonly_float_array(corr_raw) + + +def _gwyddion_sphere_corrected( + data: FloatArray, + radius: float, + *, + inverted: bool = False, +) -> FloatArray: + """Calculate Gwyddion 2.71 Sphere Revolution corrected field. + + Parameters + ---------- + data: + Two-dimensional real finite float64 array. + radius: + Sphere radius in samples (1.0 through 1000.0). + inverted: + Apply safe dual inversion ``-B(-data)``. + + Returns + ------- + numpy.ndarray + Read-only C-contiguous float64 corrected matrix. + """ + return _gwyddion_sphere_result(data, radius, inverted=inverted)[1] diff --git a/src/spmkit/core/analysis/_median_background.py b/src/spmkit/core/analysis/_median_background.py new file mode 100644 index 0000000..a150033 --- /dev/null +++ b/src/spmkit/core/analysis/_median_background.py @@ -0,0 +1,170 @@ +"""Private numerical kernel for Gwyddion 2.71 Median Background. + +This module reproduces the frozen pixel-domain semantics independently with +NumPy. It deliberately has no public adapter: the future public API owns +channels, metadata, and result objects. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from functools import lru_cache +from typing import Literal + +import numpy as np +from numpy.typing import NDArray + +FloatArray = NDArray[np.float64] +IntArray = NDArray[np.int_] +_GwyddionMedianBackgroundBackend = Literal["direct", "radixtree"] + + +@dataclass(frozen=True) +class _MedianBackgroundKernelSpec: + """Immutable discrete-kernel specification for Median Background.""" + + radius_px: int + kernel_resolution: int + kernel_active_count: int + rank_index: int + rank_backend_reference: _GwyddionMedianBackgroundBackend + + +def _validated_median_background_radius(radius_px: object) -> int: + """Return a Gwyddion Median Background radius in the frozen range.""" + if isinstance(radius_px, (bool, np.bool_)): + raise TypeError( + "Gwyddion Median Background radius_px must be a Python or NumPy " + "integer scalar; booleans are not valid" + ) + if not isinstance(radius_px, (int, np.integer)): + raise TypeError( + "Gwyddion Median Background radius_px must be a Python or NumPy " + "integer scalar; booleans are not valid" + ) + + radius = int(radius_px) + if not 1 <= radius <= 1024: + raise ValueError( + "Gwyddion Median Background radius_px must be in the inclusive " "range 1..1024" + ) + + return radius + + +def _validated_median_background_data(data: object) -> FloatArray: + """Return a finite, non-empty, C-contiguous float64 copy of 2D data.""" + try: + source = np.asarray(data) + except (TypeError, ValueError) as exc: + raise TypeError("Gwyddion Median Background requires array-compatible data") from exc + + if source.ndim != 2: + raise ValueError("Gwyddion Median Background requires two-dimensional data") + if 0 in source.shape: + raise ValueError("Gwyddion Median Background requires non-empty dimensions") + if ( + not np.issubdtype(source.dtype, np.number) + or np.iscomplexobj(source) + or isinstance(data, (bool, np.bool_)) + ): + raise TypeError("Gwyddion Median Background requires real numeric data") + + values = np.array(source, dtype=np.float64, order="C", copy=True) + if not np.all(np.isfinite(values)): + raise ValueError("Gwyddion Median Background requires finite data") + + return values + + +@lru_cache(maxsize=8) +def _cached_median_background_active_offsets(radius_px: int) -> IntArray: + """Build read-only active offsets in the frozen row-major order.""" + diameter = 2 * radius_px + 1 + radius_square = diameter * diameter + + active_count = 0 + max_columns_by_row: list[int] = [] + for dr in range(-radius_px, radius_px + 1): + remaining = radius_square - 4 * dr * dr + max_abs_dc = math.isqrt(remaining // 4) + max_columns_by_row.append(max_abs_dc) + active_count += 2 * max_abs_dc + 1 + + offsets = np.empty((active_count, 2), dtype=np.int_, order="C") + position = 0 + for dr, max_abs_dc in zip(range(-radius_px, radius_px + 1), max_columns_by_row, strict=True): + row_count = 2 * max_abs_dc + 1 + stop = position + row_count + offsets[position:stop, 0] = dr + offsets[position:stop, 1] = np.arange(-max_abs_dc, max_abs_dc + 1, dtype=np.int_) + position = stop + + if position != active_count: + raise RuntimeError("Gwyddion Median Background offset count is inconsistent") + if active_count % 2 != 1: + raise RuntimeError("Gwyddion Median Background active count must be odd") + if not np.array_equal(offsets[active_count // 2], np.array([0, 0], dtype=np.int_)): + raise RuntimeError("Gwyddion Median Background offsets must contain the centre") + if not offsets.flags.c_contiguous: + raise RuntimeError("Gwyddion Median Background offsets must be C-contiguous") + + offsets.setflags(write=False) + return offsets + + +def _median_background_active_offsets(radius_px: object) -> IntArray: + """Return cached active digital-ellipse offsets for ``radius_px``.""" + return _cached_median_background_active_offsets(_validated_median_background_radius(radius_px)) + + +def _median_background_kernel_spec(radius_px: object) -> _MedianBackgroundKernelSpec: + """Construct the immutable Gwyddion Median Background kernel specification.""" + radius = _validated_median_background_radius(radius_px) + active_count = _cached_median_background_active_offsets(radius).shape[0] + backend: _GwyddionMedianBackgroundBackend = "direct" if active_count <= 25 else "radixtree" + + return _MedianBackgroundKernelSpec( + radius_px=radius, + kernel_resolution=2 * radius + 1, + kernel_active_count=active_count, + rank_index=active_count // 2, + rank_backend_reference=backend, + ) + + +def _gwyddion_median_background_result( + data: object, + radius_px: object, +) -> tuple[FloatArray, FloatArray, _MedianBackgroundKernelSpec]: + """Calculate frozen Gwyddion 2.71 Median Background fields. + + Exterior samples are clamped to the nearest valid edge pixel. The + selected rank is found independently with :func:`numpy.partition`; no + Gwyddion selection data structure is reproduced. + """ + values = _validated_median_background_data(data) + spec = _median_background_kernel_spec(radius_px) + offsets = _cached_median_background_active_offsets(spec.radius_px) + yres, xres = values.shape + + row_indices = np.clip( + np.arange(yres, dtype=np.int_)[:, np.newaxis] + offsets[np.newaxis, :, 0], + 0, + yres - 1, + ) + column_indices = np.clip( + np.arange(xres, dtype=np.int_)[:, np.newaxis] + offsets[np.newaxis, :, 1], + 0, + xres - 1, + ) + + background = np.empty(values.shape, dtype=np.float64, order="C") + for row in range(yres): + for column in range(xres): + samples = values[row_indices[row], column_indices[column]] + background[row, column] = np.partition(samples, spec.rank_index)[spec.rank_index] + + corrected = np.array(values - background, dtype=np.float64, order="C", copy=True) + return background, corrected, spec diff --git a/src/spmkit/core/analysis/_pspline.py b/src/spmkit/core/analysis/_pspline.py new file mode 100644 index 0000000..0ab09e4 --- /dev/null +++ b/src/spmkit/core/analysis/_pspline.py @@ -0,0 +1,679 @@ +"""Tensor-product penalized B-spline surface fitting. + +This module implements the P-spline construction introduced by Eilers and +Marx, Statistical Science 11 (1996), DOI: 10.1214/ss/1038425655. + +For coefficient matrix C, data Z, marginal B-spline bases Bx and By, and +difference operators Dx and Dy, the fitted surface minimizes + + ||W**0.5 (Z - By C Bx.T)||**2 + + smoothing_x ||C Dx.T||**2 + + smoothing_y ||Dy C||**2. + +The augmented least-squares system is exposed to LSMR as a LinearOperator. +The full tensor-product design matrix is never materialized. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from math import comb +from typing import Final + +import numpy as np +from numpy.typing import ArrayLike, NDArray +from scipy.interpolate import BSpline +from scipy.linalg import null_space +from scipy.sparse import csr_array, diags +from scipy.sparse.linalg import LinearOperator, lsmr + +FloatArray = NDArray[np.float64] + +_ACCEPTABLE_LSMR_STOPS: Final[frozenset[int]] = frozenset( + { + 1, + 2, + 4, + 5, + } +) + + +@dataclass(frozen=True) +class PSplineSurfaceFit: + """Result and diagnostics from a tensor-product P-spline fit.""" + + model: FloatArray + coefficients: FloatArray + knots_x: FloatArray + knots_y: FloatArray + degree_x: int + degree_y: int + penalty_order_x: int + penalty_order_y: int + smoothing_x: float + smoothing_y: float + selected_points: int + total_points: int + solver_stop_code: int + solver_iterations: int + augmented_residual_norm: float + normal_residual_norm: float + operator_norm: float + condition_estimate: float + coefficient_norm: float + weighted_data_residual_norm: float + penalty_x_norm: float + penalty_y_norm: float + x_min: float + x_max: float + y_min: float + y_max: float + + +def _as_real_numeric_array( + values: ArrayLike, + *, + name: str, +) -> FloatArray: + """Convert real numeric input without discarding complex components.""" + raw = np.asarray(values) + + if not np.issubdtype(raw.dtype, np.number) or np.iscomplexobj(raw): + raise TypeError(f"{name} must be real numeric") + + return np.asarray( + raw, + dtype=float, + ) + + +def _readonly_float_array(values: ArrayLike) -> FloatArray: + result = np.array( + values, + dtype=float, + copy=True, + order="C", + ) + result.setflags(write=False) + return result + + +def _open_uniform_knots( + n_basis: int, + degree: int, +) -> FloatArray: + if degree < 0: + raise ValueError("spline degree must be non-negative") + + if n_basis < degree + 1: + raise ValueError("n_basis must be at least degree + 1") + + n_internal = n_basis - degree - 1 + + if n_internal: + interior = np.linspace( + 0.0, + 1.0, + n_internal + 2, + dtype=float, + )[1:-1] + else: + interior = np.empty(0, dtype=float) + + return np.concatenate( + ( + np.zeros(degree + 1, dtype=float), + interior, + np.ones(degree + 1, dtype=float), + ) + ) + + +def _difference_matrix( + size: int, + order: int, +) -> csr_array: + if order < 1: + raise ValueError("penalty order must be at least 1") + + if order >= size: + raise ValueError("penalty order must be smaller than n_basis") + + coefficients = np.array( + [(-1.0) ** (order - index) * comb(order, index) for index in range(order + 1)], + dtype=float, + ) + + return csr_array( + diags( + coefficients, + offsets=np.arange(order + 1), + shape=(size - order, size), + format="csr", + ) + ) + + +def _normalized_axis( + length: int, + values: ArrayLike | None, + *, + name: str, +) -> tuple[FloatArray, float, float]: + if length < 2: + raise ValueError(f"{name} axis must contain at least two points") + + if values is None: + original = np.arange( + length, + dtype=float, + ) + else: + original = _as_real_numeric_array( + values, + name=f"{name} coordinates", + ) + + if original.ndim != 1: + raise ValueError(f"{name} coordinates must be one-dimensional") + + if original.size != length: + raise ValueError(f"{name} coordinate count does not match data") + + if not np.all(np.isfinite(original)): + raise ValueError(f"{name} coordinates must be finite") + + if not np.all(np.diff(original) > 0.0): + raise ValueError(f"{name} coordinates must be strictly increasing") + + lower = float(original[0]) + upper = float(original[-1]) + + normalized = (original - lower) / (upper - lower) + + return ( + np.asarray(normalized, dtype=float), + lower, + upper, + ) + + +def _validate_solver_parameter( + value: float, + *, + name: str, +) -> float: + raw = np.asarray(value) + + if ( + raw.ndim != 0 + or not np.issubdtype(raw.dtype, np.number) + or np.iscomplexobj(raw) + or raw.dtype == np.bool_ + ): + raise TypeError(f"{name} must be a real numeric scalar") + + validated = float(raw.item()) + + if not np.isfinite(validated) or validated <= 0.0: + raise ValueError(f"{name} must be finite and strictly positive") + + return validated + + +def _check_penalty_null_space_identifiability( + *, + basis_x: csr_array, + basis_y: csr_array, + difference_x: csr_array, + difference_y: csr_array, + selected: NDArray[np.intp], + sqrt_weights: FloatArray, + data_shape: tuple[int, int], +) -> None: + null_x = null_space(difference_x.toarray()) + null_y = null_space(difference_y.toarray()) + + null_surfaces: list[FloatArray] = [] + + for y_index in range(null_y.shape[1]): + for x_index in range(null_x.shape[1]): + coefficients = np.outer( + null_y[:, y_index], + null_x[:, x_index], + ) + + surface = np.asarray( + basis_y @ coefficients @ basis_x.T, + dtype=float, + order="C", + ) + + null_surfaces.append( + sqrt_weights + * surface.reshape( + data_shape, + order="C", + ).ravel( + order="C" + )[selected] + ) + + null_design = np.column_stack(null_surfaces) + + rank = int(np.linalg.matrix_rank(null_design)) + + if rank != null_design.shape[1]: + raise ValueError("selected data do not identify the P-spline penalty null space") + + +def fit_pspline_surface( + data: ArrayLike, + *, + x: ArrayLike | None = None, + y: ArrayLike | None = None, + mask: ArrayLike | None = None, + weights: ArrayLike | None = None, + n_basis_x: int = 12, + n_basis_y: int = 12, + degree_x: int = 3, + degree_y: int = 3, + penalty_order_x: int = 2, + penalty_order_y: int = 2, + smoothing_x: float = 1.0, + smoothing_y: float = 1.0, + atol: float = 1e-12, + btol: float = 1e-12, + conlim: float = 1e12, + maxiter: int | None = None, +) -> PSplineSurfaceFit: + """Fit an anisotropic tensor-product P-spline surface. + + Coordinates are normalized independently to ``[0, 1]``. Consequently, + smoothing parameters are not silently rescaled when physical scan ranges + change. Physical anisotropy is represented explicitly through separate + X and Y basis counts and smoothing parameters. + + ``mask`` is a strict Boolean selection. Non-selected data may contain + non-finite values. ``weights`` must be finite and non-negative; zero + weight excludes a selected observation. + """ + + values = _as_real_numeric_array( + data, + name="P-spline surface data", + ) + + if values.ndim != 2: + raise ValueError("P-spline surface data must be two-dimensional") + + rows, columns = values.shape + + normalized_x, x_min, x_max = _normalized_axis( + columns, + x, + name="x", + ) + normalized_y, y_min, y_max = _normalized_axis( + rows, + y, + name="y", + ) + + if not isinstance(n_basis_x, int) or isinstance( + n_basis_x, + bool, + ): + raise TypeError("n_basis_x must be an integer") + + if not isinstance(n_basis_y, int) or isinstance( + n_basis_y, + bool, + ): + raise TypeError("n_basis_y must be an integer") + + knots_x = _open_uniform_knots( + n_basis_x, + degree_x, + ) + knots_y = _open_uniform_knots( + n_basis_y, + degree_y, + ) + + difference_x = _difference_matrix( + n_basis_x, + penalty_order_x, + ) + difference_y = _difference_matrix( + n_basis_y, + penalty_order_y, + ) + + validated_smoothing_x = _validate_solver_parameter( + smoothing_x, + name="smoothing_x", + ) + validated_smoothing_y = _validate_solver_parameter( + smoothing_y, + name="smoothing_y", + ) + validated_atol = _validate_solver_parameter( + atol, + name="atol", + ) + validated_btol = _validate_solver_parameter( + btol, + name="btol", + ) + validated_conlim = _validate_solver_parameter( + conlim, + name="conlim", + ) + + if maxiter is not None: + if not isinstance(maxiter, int) or isinstance( + maxiter, + bool, + ): + raise TypeError("maxiter must be an integer") + + if maxiter < 1: + raise ValueError("maxiter must be strictly positive") + + if mask is None: + selected_mask = np.ones( + values.shape, + dtype=bool, + ) + else: + raw_mask = np.asarray(mask) + + if raw_mask.dtype != np.bool_: + raise TypeError("P-spline mask must be Boolean") + + if raw_mask.shape != values.shape: + raise ValueError("P-spline mask shape must match data") + + selected_mask = np.array( + raw_mask, + dtype=bool, + copy=True, + order="C", + ) + + if weights is None: + weight_values = np.ones( + values.shape, + dtype=float, + ) + else: + weight_values = _as_real_numeric_array( + weights, + name="P-spline weights", + ) + + if weight_values.shape != values.shape: + raise ValueError("P-spline weights shape must match data") + + if not np.all(np.isfinite(weight_values)): + raise ValueError("P-spline weights must be finite") + + if np.any(weight_values < 0.0): + raise ValueError("P-spline weights must be non-negative") + + active = selected_mask & (weight_values > 0.0) + selected = np.flatnonzero(active.ravel(order="C")) + + if selected.size == 0: + raise ValueError("P-spline fit requires selected observations") + + flat_values = values.ravel(order="C") + selected_values = flat_values[selected] + + if not np.all(np.isfinite(selected_values)): + raise ValueError("selected P-spline data must be finite") + + flat_weights = weight_values.ravel(order="C") + sqrt_weights = np.sqrt(flat_weights[selected]) + + basis_x = csr_array( + BSpline.design_matrix( + normalized_x, + knots_x, + degree_x, + ) + ) + basis_y = csr_array( + BSpline.design_matrix( + normalized_y, + knots_y, + degree_y, + ) + ) + + _check_penalty_null_space_identifiability( + basis_x=basis_x, + basis_y=basis_y, + difference_x=difference_x, + difference_y=difference_y, + selected=selected, + sqrt_weights=sqrt_weights, + data_shape=values.shape, + ) + + x_penalty_rows = n_basis_y * difference_x.shape[0] + y_penalty_rows = difference_y.shape[0] * n_basis_x + coefficient_count = n_basis_y * n_basis_x + + operator_rows = selected.size + x_penalty_rows + y_penalty_rows + + sqrt_smoothing_x = np.sqrt(validated_smoothing_x) + sqrt_smoothing_y = np.sqrt(validated_smoothing_y) + + def forward( + coefficient_vector: NDArray[np.float64], + ) -> FloatArray: + coefficients = np.asarray( + coefficient_vector, + dtype=float, + ).reshape( + n_basis_y, + n_basis_x, + order="C", + ) + + model = np.asarray( + basis_y @ coefficients @ basis_x.T, + dtype=float, + order="C", + ) + penalty_x = np.asarray( + coefficients @ difference_x.T, + dtype=float, + order="C", + ) + penalty_y = np.asarray( + difference_y @ coefficients, + dtype=float, + order="C", + ) + + return np.concatenate( + ( + sqrt_weights * model.ravel(order="C")[selected], + sqrt_smoothing_x * penalty_x.ravel(order="C"), + sqrt_smoothing_y * penalty_y.ravel(order="C"), + ) + ) + + def adjoint( + residual_vector: NDArray[np.float64], + ) -> FloatArray: + residuals = np.asarray( + residual_vector, + dtype=float, + ) + + position = 0 + + data_residual = sqrt_weights * residuals[position : position + selected.size] + position += selected.size + + penalty_x_residual = residuals[position : position + x_penalty_rows].reshape( + n_basis_y, + difference_x.shape[0], + order="C", + ) + position += x_penalty_rows + + penalty_y_residual = residuals[position:].reshape( + difference_y.shape[0], + n_basis_x, + order="C", + ) + + residual_grid = np.zeros( + values.shape, + dtype=float, + order="C", + ) + residual_grid.flat[selected] = data_residual + + gradient = np.asarray( + basis_y.T @ residual_grid @ basis_x, + dtype=float, + order="C", + ) + + gradient += sqrt_smoothing_x * np.asarray( + penalty_x_residual @ difference_x, + dtype=float, + ) + gradient += sqrt_smoothing_y * np.asarray( + difference_y.T @ penalty_y_residual, + dtype=float, + ) + + return np.asarray( + gradient, + dtype=float, + order="C", + ).ravel(order="C") + + operator = LinearOperator( + shape=( + operator_rows, + coefficient_count, + ), + matvec=forward, + rmatvec=adjoint, + dtype=float, + ) + + right_hand_side = np.concatenate( + ( + sqrt_weights * selected_values, + np.zeros( + operator_rows - selected.size, + dtype=float, + ), + ) + ) + + iteration_limit = ( + maxiter + if maxiter is not None + else max( + 1000, + 4 * coefficient_count, + ) + ) + + solution = lsmr( + operator, + right_hand_side, + atol=validated_atol, + btol=validated_btol, + conlim=validated_conlim, + maxiter=iteration_limit, + ) + + ( + coefficient_vector, + stop_code, + iterations, + augmented_residual_norm, + normal_residual_norm, + operator_norm, + condition_estimate, + coefficient_norm, + ) = solution + + zero_right_hand_side = bool(np.linalg.norm(right_hand_side) == 0.0) + + converged = stop_code in _ACCEPTABLE_LSMR_STOPS or (stop_code == 0 and zero_right_hand_side) + + if not converged: + raise RuntimeError( + "P-spline LSMR did not converge: " + f"stop_code={stop_code}, " + f"iterations={iterations}, " + f"condition_estimate={condition_estimate:.6g}" + ) + + coefficients = np.asarray( + coefficient_vector, + dtype=float, + ).reshape( + n_basis_y, + n_basis_x, + order="C", + ) + + model = np.asarray( + basis_y @ coefficients @ basis_x.T, + dtype=float, + order="C", + ) + + penalty_x = np.asarray( + coefficients @ difference_x.T, + dtype=float, + ) + penalty_y = np.asarray( + difference_y @ coefficients, + dtype=float, + ) + + weighted_data_residual = sqrt_weights * (model.ravel(order="C")[selected] - selected_values) + + return PSplineSurfaceFit( + model=_readonly_float_array(model), + coefficients=_readonly_float_array(coefficients), + knots_x=_readonly_float_array(knots_x), + knots_y=_readonly_float_array(knots_y), + degree_x=degree_x, + degree_y=degree_y, + penalty_order_x=penalty_order_x, + penalty_order_y=penalty_order_y, + smoothing_x=validated_smoothing_x, + smoothing_y=validated_smoothing_y, + selected_points=int(selected.size), + total_points=int(values.size), + solver_stop_code=int(stop_code), + solver_iterations=int(iterations), + augmented_residual_norm=float(augmented_residual_norm), + normal_residual_norm=float(normal_residual_norm), + operator_norm=float(operator_norm), + condition_estimate=float(condition_estimate), + coefficient_norm=float(coefficient_norm), + weighted_data_residual_norm=float(np.linalg.norm(weighted_data_residual)), + penalty_x_norm=float(np.linalg.norm(penalty_x)), + penalty_y_norm=float(np.linalg.norm(penalty_y)), + x_min=x_min, + x_max=x_max, + y_min=y_min, + y_max=y_max, + ) diff --git a/src/spmkit/core/analysis/background.py b/src/spmkit/core/analysis/background.py new file mode 100644 index 0000000..8207950 --- /dev/null +++ b/src/spmkit/core/analysis/background.py @@ -0,0 +1,1983 @@ +"""Background estimation and removal for SPM images. + +Local geometric estimators use explicit physical or pixel-based scales. +Global polynomial and spline estimators fit models over the complete image +using explicitly documented coordinate and mask conventions. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal, cast + +import numpy as np +from scipy.ndimage import generic_filter, grey_erosion, grey_opening + +from spmkit.core.analysis._gwyddion_arc_revolution import ( + GwyddionArcDirection, + _gwyddion_arc_result, +) +from spmkit.core.analysis._gwyddion_flat_disc_morphology import ( + _gwyddion_flat_disc_morphology_result, +) +from spmkit.core.analysis._gwyddion_sphere_revolution import ( + _gwyddion_sphere_result, +) +from spmkit.core.analysis._median_background import ( + _gwyddion_median_background_result, + _MedianBackgroundKernelSpec, +) +from spmkit.core.analysis._pspline import ( + PSplineSurfaceFit, + fit_pspline_surface, +) +from spmkit.core.geometry import ( + length_values_from_metres, + length_values_to_metres, +) +from spmkit.core.models import SPMChannel + +ArcDirection = Literal["horizontal", "vertical", "both"] +ArcSide = Literal["below", "above"] +ArcBorder = Literal["nearest", "reflect"] +BackgroundMethod = Literal[ + "arc_revolution", + "gwyddion_arc_revolution", + "gwyddion_median_background", + "gwyddion_sphere_revolution", + "sphere_revolution", + "rolling_ball", + "median", + "polynomial", + "spline", +] + + +@dataclass(frozen=True) +class BackgroundResult: + """Structured result of a background-removal operation. + + The complete background and corrected channels are retained for inspection. + ``parameters`` records the effective public algorithm configuration. + """ + + background: SPMChannel + corrected: SPMChannel + method: BackgroundMethod + parameters: dict[str, object] + + def to_dict(self) -> dict[str, object]: + """Return a serializable representation of the numerical result.""" + + def channel_payload(channel: SPMChannel) -> dict[str, object]: + data = np.asarray(channel.data) + + return { + "name": channel.name, + "unit": channel.unit, + "shape": list(channel.shape), + "x_range": float(channel.x_range), + "y_range": float(channel.y_range), + "direction": channel.direction, + "group": channel.group, + "data": data.tolist(), + } + + return { + "method": self.method, + "parameters": dict(self.parameters), + "background": channel_payload(self.background), + "corrected": channel_payload(self.corrected), + } + + +def _validated_channel_data( + channel: SPMChannel, + *, + operation: str, +) -> np.ndarray: + """Return valid, finite, real, two-dimensional channel data.""" + data = np.asarray(channel.data) + + if data.ndim != 2: + raise ValueError(f"{operation} requires a 2D channel") + + if data.size == 0: + raise ValueError(f"{operation} requires non-empty data") + + if not np.issubdtype(data.dtype, np.number) or np.iscomplexobj(data): + raise TypeError(f"{operation} requires real numeric data") + + if not np.all(np.isfinite(data)): + raise ValueError(f"{operation} requires finite data") + + return data + + +def _validated_gwyddion_radius_px( + radius_px: object, + *, + operation: str, +) -> float: + """Validate the public Gwyddion radius measured in samples.""" + radius_data = np.asarray(radius_px) + + if ( + radius_data.ndim != 0 + or not np.issubdtype(radius_data.dtype, np.number) + or np.iscomplexobj(radius_data) + or isinstance(radius_px, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires radius_px to be a real scalar") + + value = float(radius_data.item()) + + if not np.isfinite(value): + raise ValueError(f"{operation} requires radius_px to be finite") + + if not 1.0 <= value <= 1000.0: + raise ValueError( + f"{operation} requires radius_px to be between " "1.0 and 1000.0 inclusive" + ) + + return value + + +def _positive_radius( + radius: object, + *, + operation: str, +) -> float: + """Validate a physical radius expressed in metres.""" + radius_data = np.asarray(radius) + + if ( + radius_data.ndim != 0 + or not np.issubdtype(radius_data.dtype, np.number) + or np.iscomplexobj(radius_data) + or isinstance(radius, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires radius to be a positive real scalar") + + value = float(radius_data.item()) + + if not np.isfinite(value): + raise ValueError(f"{operation} requires radius to be finite") + + if value <= 0.0: + raise ValueError(f"{operation} requires radius to be positive") + + return value + + +def _positive_vertical_radius( + vertical_radius: object, + *, + operation: str, +) -> float: + """Validate a vertical rolling-ball semiaxis in channel units.""" + radius_data = np.asarray(vertical_radius) + + if ( + radius_data.ndim != 0 + or not np.issubdtype(radius_data.dtype, np.number) + or np.iscomplexobj(radius_data) + or isinstance(vertical_radius, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires vertical_radius to be a positive real scalar") + + value = float(radius_data.item()) + + if not np.isfinite(value): + raise ValueError(f"{operation} requires vertical_radius to be finite") + + if value <= 0.0: + raise ValueError(f"{operation} requires vertical_radius to be positive") + + return value + + +def _validated_choice( + value: object, + *, + name: str, + allowed: tuple[str, ...], + operation: str, +) -> str: + """Validate a string-valued public option.""" + if not isinstance(value, str): + raise TypeError(f"{operation} requires {name} to be a string") + + if value not in allowed: + choices = ", ".join(repr(item) for item in allowed) + raise ValueError(f"{operation} {name} must be one of {choices}") + + return value + + +def _axis_spacing( + channel: SPMChannel, + *, + axis: int, + operation: str, +) -> float: + """Return the canonical physical pixel spacing for one image axis.""" + spacing = channel.pixel_size_x if axis == 1 else channel.pixel_size_y + spacing_data = np.asarray(spacing) + + if ( + spacing_data.ndim != 0 + or not np.issubdtype(spacing_data.dtype, np.number) + or np.iscomplexobj(spacing_data) + ): + raise TypeError(f"{operation} requires real numeric lateral pixel spacing") + + spacing_value = float(spacing_data.item()) + + if not np.isfinite(spacing_value): + raise ValueError(f"{operation} requires finite lateral pixel spacing") + + if spacing_value <= 0.0: + raise ValueError(f"{operation} requires positive lateral pixel spacing") + + return spacing_value + + +def _arc_structure( + *, + radius: float, + spacing: float, + sample_count: int, +) -> np.ndarray: + """Return the non-flat structure for an arc rolling below a profile.""" + if sample_count <= 1: + return np.array([0.0]) + + maximum_supported_offset = sample_count - 1 + radius_in_pixels = radius / spacing + + if not np.isfinite(radius_in_pixels) or radius_in_pixels >= maximum_supported_offset: + maximum_offset = maximum_supported_offset + else: + maximum_offset = int(np.floor(radius_in_pixels)) + + if maximum_offset == 0: + return np.array([0.0]) + + offsets = np.arange( + -maximum_offset, + maximum_offset + 1, + dtype=float, + ) + distances = offsets * spacing + normalized_distance = distances / radius + squared_ratio = np.square(normalized_distance) + + root = np.sqrt( + np.maximum( + 1.0 - squared_ratio, + 0.0, + ) + ) + + # Algebraically equivalent to + # radius - sqrt(radius**2 - distance**2), but numerically stable when + # radius is much larger than the lateral pixel spacing. + sagitta = radius * squared_ratio / (1.0 + root) + + # SciPy grey erosion subtracts the structure and grey dilation adds it. + # A negative sagitta represents the upper arc of a circle rolling beneath + # the measured surface. + return -sagitta + + +def _opening_along_axis( + data: np.ndarray, + *, + radius: float, + spacing: float, + axis: int, + border: ArcBorder, +) -> np.ndarray: + """Apply one separable physical arc-opening stage.""" + sample_count = data.shape[axis] + structure_1d = _arc_structure( + radius=radius, + spacing=spacing, + sample_count=sample_count, + ) + + if structure_1d.size == 1: + return data.copy() + + structure = structure_1d[np.newaxis, :] if axis == 1 else structure_1d[:, np.newaxis] + + return np.asarray( + grey_opening( + data, + structure=structure, + mode=border, + ), + dtype=float, + ) + + +def _estimate_below_metres( + data_metres: np.ndarray, + channel: SPMChannel, + *, + radius: float, + direction: ArcDirection, + border: ArcBorder, + operation: str, +) -> np.ndarray: + """Estimate the sequential arc envelope below the surface.""" + x_spacing = _axis_spacing( + channel, + axis=1, + operation=operation, + ) + y_spacing = _axis_spacing( + channel, + axis=0, + operation=operation, + ) + + if direction == "horizontal": + return _opening_along_axis( + data_metres, + radius=radius, + spacing=x_spacing, + axis=1, + border=border, + ) + + if direction == "vertical": + return _opening_along_axis( + data_metres, + radius=radius, + spacing=y_spacing, + axis=0, + border=border, + ) + + horizontal = _opening_along_axis( + data_metres, + radius=radius, + spacing=x_spacing, + axis=1, + border=border, + ) + + return _opening_along_axis( + horizontal, + radius=radius, + spacing=y_spacing, + axis=0, + border=border, + ) + + +def estimate_arc_revolution_background( + channel: SPMChannel, + radius: float, + *, + direction: ArcDirection = "both", + side: ArcSide = "below", + border: ArcBorder = "nearest", +) -> SPMChannel: + """Estimate a physical arc-revolution background. + + Parameters + ---------- + channel: + Two-dimensional channel whose Z unit must be a geometric length. + radius: + Physical arc radius in metres. + direction: + ``"horizontal"`` processes rows, ``"vertical"`` processes columns, + and ``"both"`` applies horizontal followed by vertical. + side: + ``"below"`` rolls the arc beneath the surface. ``"above"`` is the + exact dual obtained by inversion. + border: + SciPy boundary mode. Only ``"nearest"`` and ``"reflect"`` are + supported. + + Notes + ----- + Non-finite data and masks are not supported. A radius smaller than the + relevant pixel spacing produces a one-element structure and therefore + leaves that stage unchanged. + """ + operation = "estimate_arc_revolution_background" + + data = _validated_channel_data( + channel, + operation=operation, + ) + radius_value = _positive_radius( + radius, + operation=operation, + ) + + direction_value = cast( + ArcDirection, + _validated_choice( + direction, + name="direction", + allowed=("horizontal", "vertical", "both"), + operation=operation, + ), + ) + side_value = _validated_choice( + side, + name="side", + allowed=("below", "above"), + operation=operation, + ) + border_value = cast( + ArcBorder, + _validated_choice( + border, + name="border", + allowed=("nearest", "reflect"), + operation=operation, + ), + ) + + data_metres = length_values_to_metres( + data, + unit=channel.unit, + ) + + if side_value == "below": + background_metres = _estimate_below_metres( + data_metres, + channel, + radius=radius_value, + direction=direction_value, + border=border_value, + operation=operation, + ) + else: + background_metres = -_estimate_below_metres( + -data_metres, + channel, + radius=radius_value, + direction=direction_value, + border=border_value, + operation=operation, + ) + + background = length_values_from_metres( + background_metres, + unit=channel.unit, + ) + + return channel.with_data(background) + + +def remove_arc_revolution_background( + channel: SPMChannel, + radius: float, + *, + direction: ArcDirection = "both", + side: ArcSide = "below", + border: ArcBorder = "nearest", +) -> SPMChannel: + """Subtract the estimated arc-revolution background from a channel.""" + background = estimate_arc_revolution_background( + channel, + radius, + direction=direction, + side=side, + border=border, + ) + + corrected = np.asarray(channel.data, dtype=float) - np.asarray( + background.data, + dtype=float, + ) + + return channel.with_data(corrected) + + +def _gwyddion_arc_channels( + channel: SPMChannel, + radius_px: object, + *, + direction: object, + inverted: object, + operation: str, +) -> tuple[ + SPMChannel, + SPMChannel, + float, + GwyddionArcDirection, + bool, +]: + """Validate one request and return background and corrected channels.""" + data = _validated_channel_data( + channel, + operation=operation, + ) + radius_value = _validated_gwyddion_radius_px( + radius_px, + operation=operation, + ) + direction_value = cast( + GwyddionArcDirection, + _validated_choice( + direction, + name="direction", + allowed=("horizontal", "vertical", "both"), + operation=operation, + ), + ) + + if not isinstance(inverted, (bool, np.bool_)): + raise TypeError(f"{operation} requires inverted to be a boolean") + + inverted_value = bool(inverted) + + background_data, corrected_data = _gwyddion_arc_result( + data, + radius_value, + direction=direction_value, + inverted=inverted_value, + ) + + return ( + channel.with_data(background_data), + channel.with_data(corrected_data), + radius_value, + direction_value, + inverted_value, + ) + + +def estimate_gwyddion_arc_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, +) -> SPMChannel: + """Estimate a Gwyddion 2.71-compatible Revolve Arc background. + + Parameters + ---------- + channel: + Real, finite, non-empty two-dimensional channel. The Z unit is + preserved and need not represent geometric length. + radius_px: + Arc radius in samples. The public Gwyddion-compatible range is + inclusive from 1.0 through 1000.0. + direction: + ``"horizontal"`` processes rows, ``"vertical"`` processes columns, + and ``"both"`` applies horizontal followed by vertical. + inverted: + Apply the exact dual ``-B(-data)``. + + Notes + ----- + This is a data-adaptive compatibility estimator, not SPMKit's physical + arc-revolution model, tip deconvolution, or surface reconstruction. + Edges use the truncated support of Gwyddion 2.71; there is no border mode. + """ + background, _, _, _, _ = _gwyddion_arc_channels( + channel, + radius_px, + direction=direction, + inverted=inverted, + operation="estimate_gwyddion_arc_revolution_background", + ) + return background + + +def remove_gwyddion_arc_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, +) -> SPMChannel: + """Subtract a Gwyddion 2.71-compatible Revolve Arc background.""" + _, corrected, _, _, _ = _gwyddion_arc_channels( + channel, + radius_px, + direction=direction, + inverted=inverted, + operation="remove_gwyddion_arc_revolution_background", + ) + return corrected + + +def _gwyddion_sphere_channels( + channel: SPMChannel, + radius_px: object, + *, + inverted: object, + operation: str, +) -> tuple[ + SPMChannel, + SPMChannel, + float, + bool, +]: + """Validate one request and return background and corrected channels.""" + data = _validated_channel_data( + channel, + operation=operation, + ) + radius_value = _validated_gwyddion_radius_px( + radius_px, + operation=operation, + ) + + if not isinstance(inverted, (bool, np.bool_)): + raise TypeError(f"{operation} requires inverted to be a boolean") + + inverted_value = bool(inverted) + + background_data, corrected_data = _gwyddion_sphere_result( + data, + radius_value, + inverted=inverted_value, + ) + + return ( + channel.with_data(background_data), + channel.with_data(corrected_data), + radius_value, + inverted_value, + ) + + +def estimate_gwyddion_sphere_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + inverted: bool = False, +) -> SPMChannel: + """Estimate a Gwyddion 2.71-compatible Sphere Revolution background. + + Parameters + ---------- + channel: + Real, finite, non-empty two-dimensional channel. The Z unit is + preserved and need not represent geometric length. + radius_px: + Sphere radius in samples. The public Gwyddion-compatible range is + inclusive from 1.0 through 1000.0. + inverted: + Apply the exact dual ``-B(-data)``. + + Notes + ----- + This is a data-adaptive compatibility estimator, not SPMKit's physical + sphere-revolution model, tip deconvolution, or surface reconstruction. + """ + background, _, _, _ = _gwyddion_sphere_channels( + channel, + radius_px, + inverted=inverted, + operation="estimate_gwyddion_sphere_revolution_background", + ) + return background + + +def remove_gwyddion_sphere_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + inverted: bool = False, +) -> SPMChannel: + """Subtract a Gwyddion 2.71-compatible Sphere Revolution background.""" + _, corrected, _, _ = _gwyddion_sphere_channels( + channel, + radius_px, + inverted=inverted, + operation="remove_gwyddion_sphere_revolution_background", + ) + return corrected + + +def _gwyddion_median_background_channels( + channel: SPMChannel, + radius_px: object, +) -> tuple[ + SPMChannel, + SPMChannel, + _MedianBackgroundKernelSpec, +]: + """Calculate one Median Background result and preserve channel context.""" + background_data, corrected_data, kernel_spec = _gwyddion_median_background_result( + channel.data, + radius_px, + ) + + return ( + channel.with_data(background_data), + channel.with_data(corrected_data), + kernel_spec, + ) + + +def estimate_gwyddion_median_background( + channel: SPMChannel, + radius_px: object = 20, +) -> SPMChannel: + """Estimate a Gwyddion 2.71-compatible Median Background. + + ``radius_px`` is an integer pixel radius from 1 through 1024, with a + default of 20. The digital ellipse and nearest-edge border extension + are fixed by Gwyddion semantics. The input channel is not mutated; + finite two-dimensional input is required. + + Returns + ------- + SPMChannel + The estimated background with the input channel context preserved. + """ + background, _, _ = _gwyddion_median_background_channels( + channel, + radius_px, + ) + return background + + +def remove_gwyddion_median_background( + channel: SPMChannel, + radius_px: object = 20, +) -> SPMChannel: + """Return the corrected Gwyddion 2.71-compatible Median Background field. + + ``radius_px`` is an integer pixel radius from 1 through 1024, with a + default of 20. The digital ellipse and nearest-edge border extension + are fixed by Gwyddion semantics. The input channel is not mutated; + finite two-dimensional input is required. + + Returns + ------- + SPMChannel + The corrected field with the input channel context preserved. + """ + _, corrected, _ = _gwyddion_median_background_channels( + channel, + radius_px, + ) + return corrected + + +def _gwyddion_flat_disc_channels( + channel: SPMChannel, + size_px: object, +) -> tuple[SPMChannel, SPMChannel]: + """Calculate flat-disc opening and closing while preserving context.""" + result = _gwyddion_flat_disc_morphology_result(channel.data, size_px) + return channel.with_data(result.opening), channel.with_data(result.closing) + + +def gwyddion_flat_disc_opening( + channel: SPMChannel, + *, + size_px: object = 5, +) -> SPMChannel: + """Apply Gwyddion 2.71 Filter flat-disc Opening. + + ``size_px`` is the pixel width of the fixed digital-ellipse kernel, from + 2 through 31 inclusive, with default ``5``. Nearest-edge extension and + the executable Gwyddion even-size anchor are fixed. Finite, non-empty + two-dimensional data are required and the input channel is not mutated. + + Returns + ------- + SPMChannel + The opening field with the input channel context preserved. + """ + opening, _ = _gwyddion_flat_disc_channels(channel, size_px) + return opening + + +def gwyddion_flat_disc_closing( + channel: SPMChannel, + *, + size_px: object = 5, +) -> SPMChannel: + """Apply Gwyddion 2.71 Filter flat-disc Closing. + + ``size_px`` is the pixel width of the fixed digital-ellipse kernel, from + 2 through 31 inclusive, with default ``5``. Nearest-edge extension and + the executable Gwyddion even-size anchor are fixed. Finite, non-empty + two-dimensional data are required and the input channel is not mutated. + + Returns + ------- + SPMChannel + The closing field with the input channel context preserved. + """ + _, closing = _gwyddion_flat_disc_channels(channel, size_px) + return closing + + +def _sphere_structure( + *, + radius: float, + x_spacing: float, + y_spacing: float, + shape: tuple[int, int], +) -> tuple[np.ndarray, np.ndarray]: + """Return a physical spherical-cap structure and its circular footprint.""" + rows, columns = shape + + maximum_x_offset = columns - 1 + radius_in_x_pixels = radius / x_spacing + + if not np.isfinite(radius_in_x_pixels) or radius_in_x_pixels >= maximum_x_offset: + x_offset = maximum_x_offset + else: + x_offset = int(np.floor(radius_in_x_pixels)) + + maximum_y_offset = rows - 1 + radius_in_y_pixels = radius / y_spacing + + if not np.isfinite(radius_in_y_pixels) or radius_in_y_pixels >= maximum_y_offset: + y_offset = maximum_y_offset + else: + y_offset = int(np.floor(radius_in_y_pixels)) + + x_offsets = np.arange( + -x_offset, + x_offset + 1, + dtype=float, + ) + y_offsets = np.arange( + -y_offset, + y_offset + 1, + dtype=float, + ) + + normalized_x = x_offsets * x_spacing / radius + normalized_y = y_offsets * y_spacing / radius + + squared_ratio = np.square(normalized_y)[:, np.newaxis] + np.square(normalized_x)[np.newaxis, :] + + tolerance = 8.0 * np.finfo(float).eps + footprint = squared_ratio <= 1.0 + tolerance + + clipped_ratio = np.minimum( + squared_ratio, + 1.0, + ) + root = np.sqrt( + np.maximum( + 1.0 - clipped_ratio, + 0.0, + ) + ) + + # Stable form of radius - sqrt(radius**2 - distance**2). + sagitta = radius * clipped_ratio / (1.0 + root) + + # Values outside the footprint are ignored by SciPy. Keeping them at zero + # prevents irrelevant invalid or extreme structure values. + structure = np.where( + footprint, + -sagitta, + 0.0, + ) + + return structure, footprint + + +def _opening_with_sphere( + data: np.ndarray, + *, + radius: float, + x_spacing: float, + y_spacing: float, + border: ArcBorder, +) -> np.ndarray: + """Apply one physical two-dimensional spherical opening.""" + structure, footprint = _sphere_structure( + radius=radius, + x_spacing=x_spacing, + y_spacing=y_spacing, + shape=data.shape, + ) + + if footprint.size == 1: + return data.copy() + + return np.asarray( + grey_opening( + data, + footprint=footprint, + structure=structure, + mode=border, + ), + dtype=float, + ) + + +def _estimate_sphere_below_metres( + data_metres: np.ndarray, + channel: SPMChannel, + *, + radius: float, + border: ArcBorder, + operation: str, +) -> np.ndarray: + """Estimate the spherical envelope rolling below a surface.""" + x_spacing = _axis_spacing( + channel, + axis=1, + operation=operation, + ) + y_spacing = _axis_spacing( + channel, + axis=0, + operation=operation, + ) + + return _opening_with_sphere( + data_metres, + radius=radius, + x_spacing=x_spacing, + y_spacing=y_spacing, + border=border, + ) + + +def estimate_sphere_revolution_background( + channel: SPMChannel, + radius: float, + *, + side: ArcSide = "below", + border: ArcBorder = "nearest", +) -> SPMChannel: + """Estimate a physical spherical-revolution background. + + The structuring surface is a true spherical cap in physical XY + coordinates. With anisotropic pixels its footprint can therefore appear + elliptical in index space while remaining circular in physical space. + """ + operation = "estimate_sphere_revolution_background" + + data = _validated_channel_data( + channel, + operation=operation, + ) + radius_value = _positive_radius( + radius, + operation=operation, + ) + side_value = _validated_choice( + side, + name="side", + allowed=("below", "above"), + operation=operation, + ) + border_value = cast( + ArcBorder, + _validated_choice( + border, + name="border", + allowed=("nearest", "reflect"), + operation=operation, + ), + ) + + data_metres = length_values_to_metres( + data, + unit=channel.unit, + ) + + if side_value == "below": + background_metres = _estimate_sphere_below_metres( + data_metres, + channel, + radius=radius_value, + border=border_value, + operation=operation, + ) + else: + background_metres = -_estimate_sphere_below_metres( + -data_metres, + channel, + radius=radius_value, + border=border_value, + operation=operation, + ) + + background = length_values_from_metres( + background_metres, + unit=channel.unit, + ) + + return channel.with_data(background) + + +def remove_sphere_revolution_background( + channel: SPMChannel, + radius: float, + *, + side: ArcSide = "below", + border: ArcBorder = "nearest", +) -> SPMChannel: + """Subtract the estimated spherical-revolution background.""" + background = estimate_sphere_revolution_background( + channel, + radius, + side=side, + border=border, + ) + + corrected = np.asarray(channel.data, dtype=float) - np.asarray( + background.data, + dtype=float, + ) + + return channel.with_data(corrected) + + +def _rolling_ball_structure( + *, + radius: float, + vertical_radius: float, + x_spacing: float, + y_spacing: float, + shape: tuple[int, int], + spherical: bool, +) -> tuple[np.ndarray, np.ndarray]: + """Return a physical rolling-ball structure and footprint.""" + rows, columns = shape + + maximum_x_offset = columns - 1 + radius_in_x_pixels = radius / x_spacing + + if not np.isfinite(radius_in_x_pixels) or radius_in_x_pixels >= maximum_x_offset: + x_offset = maximum_x_offset + else: + x_offset = int(np.floor(radius_in_x_pixels)) + + maximum_y_offset = rows - 1 + radius_in_y_pixels = radius / y_spacing + + if not np.isfinite(radius_in_y_pixels) or radius_in_y_pixels >= maximum_y_offset: + y_offset = maximum_y_offset + else: + y_offset = int(np.floor(radius_in_y_pixels)) + + x_offsets = np.arange( + -x_offset, + x_offset + 1, + dtype=float, + ) + y_offsets = np.arange( + -y_offset, + y_offset + 1, + dtype=float, + ) + + x_distances = x_offsets * x_spacing + y_distances = y_offsets * y_spacing + + squared_distance = np.square(y_distances)[:, np.newaxis] + np.square(x_distances)[np.newaxis, :] + squared_radius = radius * radius + squared_ratio = squared_distance / squared_radius + + tolerance = 8.0 * np.finfo(float).eps + footprint = squared_ratio <= 1.0 + tolerance + + if spherical: + # Physical sphere and scikit-image ball_kernel arithmetic: + # height = sqrt(radius**2 - distance**2). + clipped_distance = np.minimum( + squared_distance, + squared_radius, + ) + height = np.sqrt( + np.maximum( + squared_radius - clipped_distance, + 0.0, + ) + ) + sagitta = radius - height + else: + # General ellipsoid and scikit-image ellipsoid_kernel arithmetic: + # height = vertical_radius * sqrt(1 - normalized distance**2). + clipped_ratio = np.minimum( + squared_ratio, + 1.0, + ) + root = np.sqrt( + np.maximum( + 1.0 - clipped_ratio, + 0.0, + ) + ) + height = vertical_radius * root + sagitta = vertical_radius - height + + # scipy.ndimage.grey_erosion calculates min(image - structure). + # A negative sagitta therefore evaluates min(image + sagitta). + structure = np.where( + footprint, + -sagitta, + 0.0, + ) + + return structure, footprint + + +def _estimate_rolling_ball_below( + data: np.ndarray, + channel: SPMChannel, + *, + radius: float, + vertical_radius: float, + spherical: bool, + operation: str, +) -> np.ndarray: + """Evaluate the rolling-ball apex field below a surface.""" + x_spacing = _axis_spacing( + channel, + axis=1, + operation=operation, + ) + y_spacing = _axis_spacing( + channel, + axis=0, + operation=operation, + ) + + structure, footprint = _rolling_ball_structure( + radius=radius, + vertical_radius=vertical_radius, + x_spacing=x_spacing, + y_spacing=y_spacing, + shape=data.shape, + spherical=spherical, + ) + + if footprint.size == 1: + return data.copy() + + return np.asarray( + grey_erosion( + data, + footprint=footprint, + structure=structure, + mode="constant", + cval=np.inf, + ), + dtype=float, + ) + + +def estimate_rolling_ball_background( + channel: SPMChannel, + radius: float, + *, + vertical_radius: float | None = None, + side: ArcSide = "below", +) -> SPMChannel: + """Estimate a background using a physical rolling ball. + + The lateral semiaxis ``radius`` is expressed in metres. When + ``vertical_radius`` is omitted, channel Z values must represent length; + they are converted to metres and a physical sphere with equal lateral + and vertical radii is used. + + An explicit ``vertical_radius`` is interpreted in the native channel + unit, permitting ellipsoidal kernels for voltage, phase, current and + other non-geometric channels. + + The estimator is the apex-height rolling-ball operation described by + Sternberg (1983), equivalent to non-flat grey erosion. Samples outside + the image are ignored by assigning them positive infinity. + + References + ---------- + S. R. Sternberg, "Biomedical Image Processing", Computer 16(1), + 22-34 (1983), doi:10.1109/MC.1983.1654163. + """ + operation = "estimate_rolling_ball_background" + + data = _validated_channel_data( + channel, + operation=operation, + ) + radius_value = _positive_radius( + radius, + operation=operation, + ) + side_value = _validated_choice( + side, + name="side", + allowed=("below", "above"), + operation=operation, + ) + + use_geometric_z = vertical_radius is None + + if use_geometric_z: + working_data = length_values_to_metres( + data, + unit=channel.unit, + ) + vertical_radius_value = radius_value + else: + working_data = np.asarray( + data, + dtype=float, + ) + vertical_radius_value = _positive_vertical_radius( + vertical_radius, + operation=operation, + ) + + if side_value == "below": + working_background = _estimate_rolling_ball_below( + working_data, + channel, + radius=radius_value, + vertical_radius=vertical_radius_value, + spherical=use_geometric_z, + operation=operation, + ) + else: + working_background = -_estimate_rolling_ball_below( + -working_data, + channel, + radius=radius_value, + vertical_radius=vertical_radius_value, + spherical=use_geometric_z, + operation=operation, + ) + + if use_geometric_z: + background = length_values_from_metres( + working_background, + unit=channel.unit, + ) + else: + background = working_background + + return channel.with_data(background) + + +def remove_rolling_ball_background( + channel: SPMChannel, + radius: float, + *, + vertical_radius: float | None = None, + side: ArcSide = "below", +) -> SPMChannel: + """Subtract a rolling-ball background from a channel.""" + background = estimate_rolling_ball_background( + channel, + radius, + vertical_radius=vertical_radius, + side=side, + ) + + corrected = np.asarray( + channel.data, + dtype=float, + ) - np.asarray( + background.data, + dtype=float, + ) + + return channel.with_data(corrected) + + +def _positive_pixel_radius( + radius_pixels: object, + *, + operation: str, +) -> int: + """Validate a strictly positive integer radius expressed in pixels.""" + radius_data = np.asarray(radius_pixels) + + if ( + radius_data.ndim != 0 + or not np.issubdtype(radius_data.dtype, np.integer) + or isinstance(radius_pixels, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires radius_pixels to be a positive integer") + + value = int(radius_data.item()) + + if value <= 0: + raise ValueError(f"{operation} requires radius_pixels to be positive") + + return value + + +def _validated_median_radius( + radius_pixels: int, + *, + operation: str, +) -> int: + """Validate the Gwyddion Median Level integer-radius contract.""" + maximum_radius = 1024 + + if radius_pixels > maximum_radius: + raise ValueError(f"{operation}: radius_pixels must be in the range [1, {maximum_radius}]") + + return radius_pixels + + +def _median_disk_footprint( + radius_pixels: int, +) -> np.ndarray: + """Return Gwyddion's pixel-centre elliptic kernel rasterization. + + Gwyddion creates a square bounding box of side ``2*r + 1`` and + includes pixel centres inside the corresponding ellipse. For a + circular odd-sized kernel this is equivalent to + + ``(2*x)**2 + (2*y)**2 <= (2*r + 1)**2``. + + The integer form avoids floating-point boundary ambiguity. + """ + coordinates = 2 * np.arange( + -radius_pixels, + radius_pixels + 1, + dtype=np.int64, + ) + diameter = 2 * radius_pixels + 1 + + squared_distance = coordinates[:, np.newaxis] ** 2 + coordinates[np.newaxis, :] ** 2 + + return squared_distance <= diameter**2 + + +def _median_background_border_extend( + data: np.ndarray, + *, + radius_pixels: int, +) -> np.ndarray: + """Calculate a circular local median using nearest border extension.""" + footprint = _median_disk_footprint(radius_pixels) + + return np.asarray( + generic_filter( + np.asarray(data, dtype=float), + function=np.median, + footprint=footprint, + mode="nearest", + ), + dtype=float, + ) + + +def estimate_median_background( + channel: SPMChannel, + radius_pixels: int, +) -> SPMChannel: + """Estimate a local median background with a circular pixel kernel. + + The neighbourhood radius is an integer number of pixels. At image edges + values are extended using the nearest boundary sample. + The operation is rank-based and therefore does not require a geometric Z + unit. + """ + operation = "estimate_median_background" + + data = _validated_channel_data( + channel, + operation=operation, + ) + radius_value = _positive_pixel_radius( + radius_pixels, + operation=operation, + ) + radius_value = _validated_median_radius( + radius_value, + operation=operation, + ) + + background = _median_background_border_extend( + data, + radius_pixels=radius_value, + ) + + return channel.with_data(background) + + +def remove_median_background( + channel: SPMChannel, + radius_pixels: int, +) -> SPMChannel: + """Subtract a circular local-median background from a channel.""" + background = estimate_median_background( + channel, + radius_pixels, + ) + + corrected = np.asarray(channel.data, dtype=float) - np.asarray( + background.data, + dtype=float, + ) + + return channel.with_data(corrected) + + +def estimate_polynomial_background( + channel: SPMChannel, + *, + degree_mode: Literal["total", "independent"] = "total", + degree: int = 2, + x_degree: int | None = None, + y_degree: int | None = None, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", +) -> SPMChannel: + """Estimate a global two-dimensional polynomial background. + + Pixel-centre coordinates are normalized independently to ``[-1, 1]``. + ``degree_mode="total"`` includes terms with ``x_power + y_power <= degree``. + ``degree_mode="independent"`` includes the full tensor-product basis up to + ``x_degree`` and ``y_degree``. + + A mask controls only the points used for fitting. The fitted model is + evaluated over the complete image. + """ + from spmkit.core.analysis.leveling import ( + _estimate_polynomial_background_data, + ) + + background = _estimate_polynomial_background_data( + channel, + degree_mode=degree_mode, + degree=degree, + x_degree=x_degree, + y_degree=y_degree, + mask=mask, + mask_mode=mask_mode, + ) + + return channel.with_data(background) + + +def remove_polynomial_background( + channel: SPMChannel, + *, + degree_mode: Literal["total", "independent"] = "total", + degree: int = 2, + x_degree: int | None = None, + y_degree: int | None = None, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", +) -> SPMChannel: + """Subtract a global two-dimensional polynomial background.""" + background = estimate_polynomial_background( + channel, + degree_mode=degree_mode, + degree=degree, + x_degree=x_degree, + y_degree=y_degree, + mask=mask, + mask_mode=mask_mode, + ) + + data = np.asarray(channel.data, dtype=float) + background_data = np.asarray(background.data, dtype=float) + + return channel.with_data(data - background_data) + + +def _fit_spline_background( + channel: SPMChannel, + *, + n_basis_x: int = 12, + n_basis_y: int = 12, + degree_x: int = 3, + degree_y: int = 3, + penalty_order_x: int = 2, + penalty_order_y: int = 2, + smoothing_x: float = 1.0, + smoothing_y: float = 1.0, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + weights: np.ndarray | None = None, + atol: float = 1e-12, + btol: float = 1e-12, + conlim: float = 1e12, + maxiter: int | None = None, +) -> PSplineSurfaceFit: + """Fit a P-spline background while preserving complete diagnostics.""" + from spmkit.core.analysis.leveling import _fit_selection + + data = np.asarray(channel.data) + + if data.ndim != 2: + raise ValueError("spline_background requires a 2D channel") + + rows, columns = data.shape + + if rows < 2 or columns < 2: + raise ValueError("spline_background requires at least two rows and two columns") + + selection = _fit_selection( + data, + mask=mask, + mask_mode=mask_mode, + operation="spline_background", + minimum_points=1, + ) + + x_coordinates = (np.arange(columns, dtype=float) + 0.5) * float(channel.pixel_size_x) + y_coordinates = (np.arange(rows, dtype=float) + 0.5) * float(channel.pixel_size_y) + + return fit_pspline_surface( + data, + x=x_coordinates, + y=y_coordinates, + mask=selection, + weights=weights, + n_basis_x=n_basis_x, + n_basis_y=n_basis_y, + degree_x=degree_x, + degree_y=degree_y, + penalty_order_x=penalty_order_x, + penalty_order_y=penalty_order_y, + smoothing_x=smoothing_x, + smoothing_y=smoothing_y, + atol=atol, + btol=btol, + conlim=conlim, + maxiter=maxiter, + ) + + +def estimate_spline_background( + channel: SPMChannel, + *, + n_basis_x: int = 12, + n_basis_y: int = 12, + degree_x: int = 3, + degree_y: int = 3, + penalty_order_x: int = 2, + penalty_order_y: int = 2, + smoothing_x: float = 1.0, + smoothing_y: float = 1.0, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + weights: np.ndarray | None = None, +) -> SPMChannel: + """Estimate a global anisotropic tensor-product P-spline background. + + The basis is evaluated at physical pixel-centre coordinates. Each axis is + normalized independently inside the P-spline solver. A mask controls only + observations used for fitting; the model is evaluated over the full image. + """ + fit = _fit_spline_background( + channel, + n_basis_x=n_basis_x, + n_basis_y=n_basis_y, + degree_x=degree_x, + degree_y=degree_y, + penalty_order_x=penalty_order_x, + penalty_order_y=penalty_order_y, + smoothing_x=smoothing_x, + smoothing_y=smoothing_y, + mask=mask, + mask_mode=mask_mode, + weights=weights, + ) + + background = np.array( + fit.model, + dtype=float, + copy=True, + order="C", + ) + + return channel.with_data(background) + + +def remove_spline_background( + channel: SPMChannel, + *, + n_basis_x: int = 12, + n_basis_y: int = 12, + degree_x: int = 3, + degree_y: int = 3, + penalty_order_x: int = 2, + penalty_order_y: int = 2, + smoothing_x: float = 1.0, + smoothing_y: float = 1.0, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + weights: np.ndarray | None = None, +) -> SPMChannel: + """Subtract a global anisotropic tensor-product P-spline background.""" + background = estimate_spline_background( + channel, + n_basis_x=n_basis_x, + n_basis_y=n_basis_y, + degree_x=degree_x, + degree_y=degree_y, + penalty_order_x=penalty_order_x, + penalty_order_y=penalty_order_y, + smoothing_x=smoothing_x, + smoothing_y=smoothing_y, + mask=mask, + mask_mode=mask_mode, + weights=weights, + ) + + data = np.asarray(channel.data, dtype=float) + background_data = np.asarray(background.data, dtype=float) + + return channel.with_data(data - background_data) + + +def _build_background_result( + channel: SPMChannel, + background: SPMChannel, + *, + method: BackgroundMethod, + parameters: dict[str, object], +) -> BackgroundResult: + """Build a structured result without recalculating the background.""" + corrected = channel.with_data( + np.asarray(channel.data, dtype=float) - np.asarray(background.data, dtype=float) + ) + + return BackgroundResult( + background=background, + corrected=corrected, + method=method, + parameters=dict(parameters), + ) + + +def analyze_arc_revolution_background( + channel: SPMChannel, + radius: float, + *, + direction: ArcDirection = "both", + side: ArcSide = "below", + border: ArcBorder = "nearest", +) -> BackgroundResult: + """Estimate and subtract an arc-revolution background in one pass.""" + background = estimate_arc_revolution_background( + channel, + radius, + direction=direction, + side=side, + border=border, + ) + + return _build_background_result( + channel, + background, + method="arc_revolution", + parameters={ + "radius": float(radius), + "direction": direction, + "side": side, + "border": border, + }, + ) + + +def analyze_gwyddion_arc_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + direction: GwyddionArcDirection = "horizontal", + inverted: bool = False, +) -> BackgroundResult: + """Estimate and subtract a compatible Revolve Arc background once.""" + ( + background, + corrected, + radius_value, + direction_value, + inverted_value, + ) = _gwyddion_arc_channels( + channel, + radius_px, + direction=direction, + inverted=inverted, + operation="analyze_gwyddion_arc_revolution_background", + ) + + return BackgroundResult( + background=background, + corrected=corrected, + method="gwyddion_arc_revolution", + parameters={ + "radius_px": radius_value, + "direction": direction_value, + "inverted": inverted_value, + }, + ) + + +def analyze_gwyddion_sphere_revolution_background( + channel: SPMChannel, + radius_px: float = 20.0, + *, + inverted: bool = False, +) -> BackgroundResult: + """Estimate and subtract a compatible Sphere Revolution background once.""" + ( + background, + corrected, + radius_value, + inverted_value, + ) = _gwyddion_sphere_channels( + channel, + radius_px, + inverted=inverted, + operation="analyze_gwyddion_sphere_revolution_background", + ) + + return BackgroundResult( + background=background, + corrected=corrected, + method="gwyddion_sphere_revolution", + parameters={ + "radius_px": radius_value, + "inverted": inverted_value, + }, + ) + + +def analyze_gwyddion_median_background( + channel: SPMChannel, + radius_px: object = 20, +) -> BackgroundResult: + """Estimate and remove a Gwyddion 2.71-compatible Median Background. + + ``radius_px`` is an integer pixel radius from 1 through 1024, with a + default of 20. The digital ellipse and nearest-edge border extension + are fixed; border, shape, rank, and backend are not public options. The + input channel is not mutated and must contain finite two-dimensional data. + + Returns + ------- + BackgroundResult + The background, corrected field, method, and fixed kernel metadata. + """ + background, corrected, kernel_spec = _gwyddion_median_background_channels( + channel, + radius_px, + ) + + return BackgroundResult( + background=background, + corrected=corrected, + method="gwyddion_median_background", + parameters={ + "radius_px": kernel_spec.radius_px, + "kernel_resolution": kernel_spec.kernel_resolution, + "kernel_active_count": kernel_spec.kernel_active_count, + "rank_index": kernel_spec.rank_index, + "rank_backend_reference": kernel_spec.rank_backend_reference, + "border_policy": "gwyddion_border_extend", + "kernel_geometry": "gwyddion_digital_ellipse", + }, + ) + + +def analyze_sphere_revolution_background( + channel: SPMChannel, + radius: float, + *, + side: ArcSide = "below", + border: ArcBorder = "nearest", +) -> BackgroundResult: + """Estimate and subtract a spherical background in one pass.""" + background = estimate_sphere_revolution_background( + channel, + radius, + side=side, + border=border, + ) + + return _build_background_result( + channel, + background, + method="sphere_revolution", + parameters={ + "radius": float(radius), + "side": side, + "border": border, + }, + ) + + +def analyze_rolling_ball_background( + channel: SPMChannel, + radius: float, + *, + vertical_radius: float | None = None, + side: ArcSide = "below", +) -> BackgroundResult: + """Estimate and subtract a rolling-ball background in one pass.""" + background = estimate_rolling_ball_background( + channel, + radius, + vertical_radius=vertical_radius, + side=side, + ) + + return _build_background_result( + channel, + background, + method="rolling_ball", + parameters={ + "radius": float(radius), + "vertical_radius": (None if vertical_radius is None else float(vertical_radius)), + "side": side, + "boundary": "ignore", + }, + ) + + +def analyze_polynomial_background( + channel: SPMChannel, + *, + degree_mode: Literal["total", "independent"] = "total", + degree: int = 2, + x_degree: int | None = None, + y_degree: int | None = None, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", +) -> BackgroundResult: + """Estimate and subtract a polynomial background in one fit.""" + background = estimate_polynomial_background( + channel, + degree_mode=degree_mode, + degree=degree, + x_degree=x_degree, + y_degree=y_degree, + mask=mask, + mask_mode=mask_mode, + ) + + return _build_background_result( + channel, + background, + method="polynomial", + parameters={ + "degree_mode": degree_mode, + "degree": (int(degree) if degree_mode == "total" else None), + "x_degree": (int(x_degree) if x_degree is not None else None), + "y_degree": (int(y_degree) if y_degree is not None else None), + "mask_mode": mask_mode, + "mask_provided": mask is not None, + "coordinates": "normalized_-1_1", + }, + ) + + +def analyze_spline_background( + channel: SPMChannel, + *, + n_basis_x: int = 12, + n_basis_y: int = 12, + degree_x: int = 3, + degree_y: int = 3, + penalty_order_x: int = 2, + penalty_order_y: int = 2, + smoothing_x: float = 1.0, + smoothing_y: float = 1.0, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + weights: np.ndarray | None = None, +) -> BackgroundResult: + """Estimate and subtract a P-spline background using one fit.""" + fit = _fit_spline_background( + channel, + n_basis_x=n_basis_x, + n_basis_y=n_basis_y, + degree_x=degree_x, + degree_y=degree_y, + penalty_order_x=penalty_order_x, + penalty_order_y=penalty_order_y, + smoothing_x=smoothing_x, + smoothing_y=smoothing_y, + mask=mask, + mask_mode=mask_mode, + weights=weights, + ) + + background = channel.with_data( + np.array( + fit.model, + dtype=float, + copy=True, + order="C", + ) + ) + + return _build_background_result( + channel, + background, + method="spline", + parameters={ + "n_basis_x": int(fit.coefficients.shape[1]), + "n_basis_y": int(fit.coefficients.shape[0]), + "degree_x": int(fit.degree_x), + "degree_y": int(fit.degree_y), + "penalty_order_x": int(fit.penalty_order_x), + "penalty_order_y": int(fit.penalty_order_y), + "smoothing_x": float(fit.smoothing_x), + "smoothing_y": float(fit.smoothing_y), + "mask_mode": mask_mode, + "mask_provided": mask is not None, + "weights_provided": weights is not None, + "coordinates": "physical_pixel_centres_normalized_0_1", + "diagnostics": { + "selected_points": int(fit.selected_points), + "total_points": int(fit.total_points), + "solver_stop_code": int(fit.solver_stop_code), + "solver_iterations": int(fit.solver_iterations), + "augmented_residual_norm": float(fit.augmented_residual_norm), + "normal_residual_norm": float(fit.normal_residual_norm), + "operator_norm": float(fit.operator_norm), + "condition_estimate": float(fit.condition_estimate), + "coefficient_norm": float(fit.coefficient_norm), + "weighted_data_residual_norm": float(fit.weighted_data_residual_norm), + "penalty_x_norm": float(fit.penalty_x_norm), + "penalty_y_norm": float(fit.penalty_y_norm), + "x_min": float(fit.x_min), + "x_max": float(fit.x_max), + "y_min": float(fit.y_min), + "y_max": float(fit.y_max), + }, + }, + ) + + +def analyze_median_background( + channel: SPMChannel, + radius_pixels: int, +) -> BackgroundResult: + """Estimate and subtract a local-median background in one pass.""" + background = estimate_median_background( + channel, + radius_pixels, + ) + + return _build_background_result( + channel, + background, + method="median", + parameters={ + "radius_pixels": int(radius_pixels), + "border": "nearest", + }, + ) diff --git a/src/spmkit/core/analysis/grains.py b/src/spmkit/core/analysis/grains.py index 4864f3c..63fc58f 100644 --- a/src/spmkit/core/analysis/grains.py +++ b/src/spmkit/core/analysis/grains.py @@ -111,7 +111,8 @@ def detect( from scipy.ndimage import label as ndlabel except ImportError as exc: # pragma: no cover raise ImportError( - "La detección de granos requiere scipy. " "Instala con: pip install 'spmkit[grains]'" + "La detección de granos requiere SciPy, una dependencia obligatoria " + "de SPMKit. Reinstala el entorno con: python -m pip install -e ." ) from exc if not (0.0 < relative_height <= 1.0): diff --git a/src/spmkit/core/analysis/leveling.py b/src/spmkit/core/analysis/leveling.py index 695ce93..e954a32 100644 --- a/src/spmkit/core/analysis/leveling.py +++ b/src/spmkit/core/analysis/leveling.py @@ -7,55 +7,1397 @@ from __future__ import annotations +from typing import Literal + import numpy as np +from spmkit.core.analysis._gwyddion_align_rows_statistics import ( + _gwyddion_align_rows_statistics_result, + _GwyddionAlignRowsDirection, + _GwyddionAlignRowsMethod, + _GwyddionMaskMode, +) +from spmkit.core.analysis._gwyddion_path_level import _gwyddion_path_level_result +from spmkit.core.geometry import ( + bilinear_sample, + length_values_from_metres, + length_values_to_metres, + physical_to_pixel_indices, + pixel_center_axes, +) from spmkit.core.models import SPMChannel +GwyddionAlignRowsMaskMode = Literal["exclude", "include", "ignore"] +GwyddionAlignRowsDirection = Literal["horizontal", "vertical"] + +_GWYDDION_ALIGN_ROWS_MASK_MODES: dict[str, _GwyddionMaskMode] = { + "exclude": _GwyddionMaskMode.EXCLUDE, + "include": _GwyddionMaskMode.INCLUDE, + "ignore": _GwyddionMaskMode.IGNORE, +} +_GWYDDION_ALIGN_ROWS_DIRECTIONS: dict[str, _GwyddionAlignRowsDirection] = { + "horizontal": _GwyddionAlignRowsDirection.HORIZONTAL, + "vertical": _GwyddionAlignRowsDirection.VERTICAL, +} + + +def _validated_data(channel: SPMChannel, *, operation: str) -> np.ndarray: + """Return valid 2D, numeric, finite channel data.""" + data = np.asarray(channel.data) + + if data.ndim != 2: + raise ValueError(f"{operation} requires a 2D channel") + if data.size == 0: + raise ValueError(f"{operation} requires non-empty data") + if not np.issubdtype(data.dtype, np.number): + raise TypeError(f"{operation} requires numeric data") + if not np.all(np.isfinite(data)): + raise ValueError(f"{operation} requires finite data") + + return data + + +def _fit_selection( + data: np.ndarray, + *, + mask: np.ndarray | None, + mask_mode: Literal["ignore", "include", "exclude"], + operation: str, + minimum_points: int, +) -> np.ndarray: + """Return pixels selected for a background fit.""" + allowed_modes = {"ignore", "include", "exclude"} + + if mask_mode not in allowed_modes: + raise ValueError(f"{operation} mask_mode must be 'ignore', 'include', or 'exclude'") + + if mask_mode == "ignore": + return np.ones(data.shape, dtype=bool) + + if mask is None: + raise ValueError(f"{operation} requires a mask when mask_mode is '{mask_mode}'") + + mask_data = np.asarray(mask) + + if mask_data.shape != data.shape: + raise ValueError(f"{operation} requires mask shape to match channel data") + + if mask_data.dtype != np.bool_: + raise TypeError(f"{operation} requires a boolean mask") + + selection = mask_data if mask_mode == "include" else ~mask_data + + if np.count_nonzero(selection) < minimum_points: + raise ValueError(f"{operation} requires at least {minimum_points} selected points") + + return selection + + +def _nonnegative_integer( + value: object, + *, + name: str, + operation: str, +) -> int: + """Validate and return a non-negative integer parameter.""" + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, + (int, np.integer), + ): + raise TypeError(f"{operation} requires {name} to be a non-negative integer") + + integer_value = int(value) + + if integer_value < 0: + raise ValueError(f"{operation} requires {name} to be non-negative") + + return integer_value + + +def _trim_fraction(value: object, *, operation: str) -> float: + """Validate a trimming fraction in the closed interval [0, 0.5].""" + fraction_data = np.asarray(value) + + if ( + fraction_data.ndim != 0 + or not np.issubdtype(fraction_data.dtype, np.number) + or np.iscomplexobj(fraction_data) + or isinstance(value, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires trim_fraction to be a real scalar") + + fraction = float(fraction_data.item()) + + if not np.isfinite(fraction): + raise ValueError(f"{operation} requires trim_fraction to be finite") + + if fraction < 0.0 or fraction > 0.5: + raise ValueError(f"{operation} requires trim_fraction between 0 and 0.5") + + return fraction + + +def _trimmed_mean(values: np.ndarray, fraction: float) -> float: + """Return the symmetrically trimmed mean of one-dimensional values.""" + if fraction == 0.5: + return float(np.median(values)) + + ordered = np.sort(values) + trim_count = int(np.floor(fraction * ordered.size)) + + if trim_count == 0: + return float(np.mean(ordered)) + + return float(np.mean(ordered[trim_count:-trim_count])) + + +def _positive_integer( + value: object, + *, + name: str, + operation: str, +) -> int: + """Validate and return a strictly positive integer.""" + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, + (int, np.integer), + ): + raise TypeError(f"{operation} requires {name} to be a positive integer") + + integer_value = int(value) -def plane_fit(channel: SPMChannel) -> SPMChannel: - """Resta un plano de mínimos cuadrados ``z = a*x + b*y + c``. + if integer_value <= 0: + raise ValueError(f"{operation} requires {name} to be positive") - Es la corrección de inclinación más común para topografía AFM. + return integer_value + + +def _positive_real_scalar( + value: object, + *, + name: str, + operation: str, +) -> float: + """Validate and return a finite strictly positive real scalar.""" + scalar_data = np.asarray(value) + + if ( + scalar_data.ndim != 0 + or not np.issubdtype(scalar_data.dtype, np.number) + or np.iscomplexobj(scalar_data) + or isinstance(value, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires {name} to be a positive real scalar") + + scalar = float(scalar_data.item()) + + if not np.isfinite(scalar): + raise ValueError(f"{operation} requires {name} to be finite") + + if scalar <= 0.0: + raise ValueError(f"{operation} requires {name} to be positive") + + return scalar + + +def zero_mean(channel: SPMChannel) -> SPMChannel: + """Shift the vertical reference so the arithmetic mean is zero.""" + data = _validated_data(channel, operation="zero_mean") + mean_height = np.mean(data) + return channel.with_data(data - mean_height) + + +def zero_minimum(channel: SPMChannel) -> SPMChannel: + """Shift the vertical reference so the minimum height is zero.""" + data = _validated_data(channel, operation="zero_minimum") + minimum_height = np.min(data) + return channel.with_data(data - minimum_height) + + +def gwyddion_path_level( + channel: SPMChannel, + lines: object, + *, + thickness_px: object = 1, +) -> SPMChannel: + """Apply the frozen Gwyddion 2.71 Path Level operation. + + ``lines`` is an ordered collection of straight physical-coordinate + selections ``(x0, y0, x1, y1)``. Duplicates and ordering are meaningful. + ``thickness_px`` is an integer from 1 through 128, with default ``1``. + The operation has fixed Gwyddion Path Level semantics: no interpolation, + horizontal-line exclusion, and a cumulative row correction. Finite, + non-empty two-dimensional data and finite positive channel ranges are + required. The input channel is not mutated. + + Returns + ------- + SPMChannel + A corrected channel with the input context preserved. + """ + result = _gwyddion_path_level_result( + channel.data, + lines, + xreal=channel.x_range, + yreal=channel.y_range, + thickness_px=thickness_px, + ) + return channel.with_data(result.corrected) + + +def _gwyddion_align_rows_statistics_channel( + channel: SPMChannel, + *, + method: _GwyddionAlignRowsMethod, + mask: np.ndarray | None, + mask_mode: GwyddionAlignRowsMaskMode, + direction: GwyddionAlignRowsDirection, + trim_fraction: float, +) -> SPMChannel: + """Apply one fixed private Align Rows method and preserve channel context.""" + if not isinstance(channel, SPMChannel): + raise TypeError("Gwyddion Align Rows requires an SPMChannel") + if not isinstance(mask_mode, str) or mask_mode not in _GWYDDION_ALIGN_ROWS_MASK_MODES: + raise ValueError("Gwyddion Align Rows mask_mode must be 'exclude', 'include', or 'ignore'") + if not isinstance(direction, str) or direction not in _GWYDDION_ALIGN_ROWS_DIRECTIONS: + raise ValueError("Gwyddion Align Rows direction must be 'horizontal' or 'vertical'") + + result = _gwyddion_align_rows_statistics_result( + channel.data, + method=method, + masking_mode=_GWYDDION_ALIGN_ROWS_MASK_MODES[mask_mode], + direction=_GWYDDION_ALIGN_ROWS_DIRECTIONS[direction], + trim_fraction=trim_fraction, + mask=mask, + ) + return channel.with_data(result.corrected) + + +def gwyddion_align_rows_median( + channel: SPMChannel, + *, + mask: np.ndarray | None = None, + mask_mode: GwyddionAlignRowsMaskMode = "ignore", + direction: GwyddionAlignRowsDirection = "horizontal", +) -> SPMChannel: + """Apply Gwyddion 2.71 Align Rows Median with portable source semantics. + + ``mask`` is an optional finite numeric array matching the channel shape. + ``mask_mode`` is ``"exclude"``, ``"include"``, or ``"ignore"``; an absent + mask always selects all values. ``direction`` selects horizontal rows or + source-equivalent vertical transpose/restore processing. The result is a + new ``SPMChannel`` with the input context preserved. + """ + return _gwyddion_align_rows_statistics_channel( + channel, + method=_GwyddionAlignRowsMethod.MEDIAN, + mask=mask, + mask_mode=mask_mode, + direction=direction, + trim_fraction=0.05, + ) + + +def gwyddion_align_rows_median_of_differences( + channel: SPMChannel, + *, + mask: np.ndarray | None = None, + mask_mode: GwyddionAlignRowsMaskMode = "ignore", + direction: GwyddionAlignRowsDirection = "horizontal", +) -> SPMChannel: + """Apply Gwyddion 2.71 Align Rows Median of differences. + + The optional numeric mask and orientation use the same public contract as + :func:`gwyddion_align_rows_median`. This wrapper retains the private + engine's portable source-semantic cumulative correction and slope removal. + """ + return _gwyddion_align_rows_statistics_channel( + channel, + method=_GwyddionAlignRowsMethod.MEDIAN_OF_DIFFERENCES, + mask=mask, + mask_mode=mask_mode, + direction=direction, + trim_fraction=0.05, + ) + + +def gwyddion_align_rows_trimmed_mean( + channel: SPMChannel, + *, + trim_fraction: float = 0.05, + mask: np.ndarray | None = None, + mask_mode: GwyddionAlignRowsMaskMode = "ignore", + direction: GwyddionAlignRowsDirection = "horizontal", +) -> SPMChannel: + """Apply Gwyddion 2.71 Align Rows Trimmed mean. + + ``trim_fraction`` is a finite real value in the inclusive range ``0.0`` to + ``0.5``. The optional numeric mask and orientation use the public Median + contract. The result is a new context-preserving ``SPMChannel``. """ - z = channel.data - rows, cols = z.shape + return _gwyddion_align_rows_statistics_channel( + channel, + method=_GwyddionAlignRowsMethod.TRIMMED_MEAN, + mask=mask, + mask_mode=mask_mode, + direction=direction, + trim_fraction=trim_fraction, + ) + + +def gwyddion_align_rows_trimmed_mean_of_differences( + channel: SPMChannel, + *, + trim_fraction: float = 0.05, + mask: np.ndarray | None = None, + mask_mode: GwyddionAlignRowsMaskMode = "ignore", + direction: GwyddionAlignRowsDirection = "horizontal", +) -> SPMChannel: + """Apply Gwyddion 2.71 Align Rows Trimmed mean of differences. + + ``trim_fraction`` is a finite real value in the inclusive range ``0.0`` to + ``0.5``. The optional numeric mask and orientation use the public Median + contract. Portable source semantics, rather than an installed + package-specific reassociation profile, define the returned channel. + """ + return _gwyddion_align_rows_statistics_channel( + channel, + method=_GwyddionAlignRowsMethod.TRIMMED_MEAN_OF_DIFFERENCES, + mask=mask, + mask_mode=mask_mode, + direction=direction, + trim_fraction=trim_fraction, + ) + + +def shift_vertical(channel: SPMChannel, *, offset: float) -> SPMChannel: + """Add a finite scalar offset to every height value.""" + data = _validated_data(channel, operation="shift_vertical") + offset_array = np.asarray(offset) + + if ( + offset_array.ndim != 0 + or not np.issubdtype(offset_array.dtype, np.number) + or np.iscomplexobj(offset_array) + ): + raise TypeError("shift_vertical requires a real numeric scalar offset") + + offset_value = float(offset_array.item()) + + if not np.isfinite(offset_value): + raise ValueError("shift_vertical requires a finite offset") + + return channel.with_data(data + offset_value) + + +def plane_fit( + channel: SPMChannel, + *, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", +) -> SPMChannel: + """Subtract a least-squares plane from a two-dimensional channel.""" + data = _validated_data(channel, operation="plane_fit") + selection = _fit_selection( + data, + mask=mask, + mask_mode=mask_mode, + operation="plane_fit", + minimum_points=3, + ) + + rows, cols = data.shape yy, xx = np.mgrid[0:rows, 0:cols] - a_mat = np.column_stack([xx.ravel(), yy.ravel(), np.ones(z.size)]) - coeffs, *_ = np.linalg.lstsq(a_mat, z.ravel(), rcond=None) - plane = (a_mat @ coeffs).reshape(z.shape) - return channel.with_data(z - plane) + + design = np.column_stack( + ( + xx.ravel(), + yy.ravel(), + np.ones(data.size), + ) + ) + selected = selection.ravel() + + coefficients, _, rank, _ = np.linalg.lstsq( + design[selected], + data.ravel()[selected], + rcond=None, + ) + + if rank < 3: + raise ValueError("plane_fit selected points do not define a unique plane") + + plane = coefficients[0] * xx + coefficients[1] * yy + coefficients[2] + + return channel.with_data(data - plane) + + +def _rotation_matrix_to_horizontal( + x_slope: float, + y_slope: float, +) -> np.ndarray: + """Return the minimal 3D rotation mapping a plane normal to +Z.""" + normal = np.array( + [-x_slope, -y_slope, 1.0], + dtype=float, + ) + normal /= np.linalg.norm(normal) + + target = np.array([0.0, 0.0, 1.0]) + cross = np.cross(normal, target) + sine = float(np.linalg.norm(cross)) + cosine = float(np.dot(normal, target)) + + if sine <= np.finfo(float).eps: + return np.eye(3) + + cross_matrix = np.array( + [ + [0.0, -cross[2], cross[1]], + [cross[2], 0.0, -cross[0]], + [-cross[1], cross[0], 0.0], + ] + ) + + return np.eye(3) + cross_matrix + cross_matrix @ cross_matrix * ((1.0 - cosine) / sine**2) + + +def rotate_level( + channel: SPMChannel, + *, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + interpolation: Literal["linear"] = "linear", + fill_mode: Literal["nearest", "constant"] = "nearest", + fill_value: float = 0.0, + preserve_mean: bool = False, +) -> SPMChannel: + """Flatten a fitted plane by approximate physical 3D image rotation. + + The fitted plane defines an inverse lateral mapping from the output grid + to the source grid. Heights are interpolated in the source field and then + rotated in physical XYZ coordinates. Shape and lateral ranges are kept. + """ + data = _validated_data( + channel, + operation="rotate_level", + ) + + if interpolation != "linear": + raise ValueError("rotate_level interpolation must be 'linear'") + + if fill_mode not in {"nearest", "constant"}: + raise ValueError("rotate_level fill_mode must be 'nearest' or 'constant'") + + if not isinstance(preserve_mean, (bool, np.bool_)): + raise TypeError("rotate_level requires preserve_mean to be boolean") + + fill_data = np.asarray(fill_value) + + if ( + fill_data.ndim != 0 + or not np.issubdtype(fill_data.dtype, np.number) + or np.iscomplexobj(fill_data) + or isinstance(fill_value, (bool, np.bool_)) + ): + raise TypeError("rotate_level requires fill_value to be a real scalar") + + fill_scalar = float(fill_data.item()) + + if not np.isfinite(fill_scalar): + raise ValueError("rotate_level requires fill_value to be finite") + + data_metres = length_values_to_metres( + data, + unit=channel.unit, + ) + fill_metres = float( + length_values_to_metres( + np.asarray(fill_scalar), + unit=channel.unit, + ) + ) + + selection = _fit_selection( + data, + mask=mask, + mask_mode=mask_mode, + operation="rotate_level", + minimum_points=3, + ) + + x_coordinates, y_coordinates = pixel_center_axes( + data.shape, + x_range=channel.x_range, + y_range=channel.y_range, + ) + xx, yy = np.meshgrid( + x_coordinates, + y_coordinates, + ) + + design = np.column_stack( + ( + xx.ravel(), + yy.ravel(), + np.ones(data.size), + ) + ) + selected = selection.ravel() + + coefficients, _, rank, _ = np.linalg.lstsq( + design[selected], + data_metres.ravel()[selected], + rcond=None, + ) + + if rank < 3: + raise ValueError("rotate_level selected points do not define a unique plane") + + x_slope = float(coefficients[0]) + y_slope = float(coefficients[1]) + intercept = float(coefficients[2]) + + rotation = _rotation_matrix_to_horizontal( + x_slope, + y_slope, + ) + + output_plane_points = np.stack( + ( + xx.ravel(), + yy.ravel(), + np.zeros(data.size), + ) + ) + + source_plane_points = rotation.T @ output_plane_points + + source_x = source_plane_points[0].reshape(data.shape) + source_y = source_plane_points[1].reshape(data.shape) + + x_indices, y_indices = physical_to_pixel_indices( + source_x, + source_y, + shape=data.shape, + x_range=channel.x_range, + y_range=channel.y_range, + ) + + rows, columns = data.shape + + outside = ( + (x_indices < 0.0) | (x_indices > columns - 1) | (y_indices < 0.0) | (y_indices > rows - 1) + ) + + sampled_x_indices = np.clip( + x_indices, + 0.0, + columns - 1, + ) + sampled_y_indices = np.clip( + y_indices, + 0.0, + rows - 1, + ) + + sampled_heights = bilinear_sample( + data_metres, + x_index=sampled_x_indices, + y_index=sampled_y_indices, + fill_mode="nearest", + ) + + x_step = channel.x_range / columns + y_step = channel.y_range / rows + + effective_source_x = (sampled_x_indices + 0.5 - 0.5 * columns) * x_step + + effective_source_y = (sampled_y_indices + 0.5 - 0.5 * rows) * y_step + + actual_source_points = np.stack( + ( + effective_source_x.ravel(), + effective_source_y.ravel(), + (sampled_heights - intercept).ravel(), + ) + ) + + rotated_points = rotation @ actual_source_points + rotated_height_metres = rotated_points[2].reshape(data.shape) + + if fill_mode == "constant": + rotated_height_metres = np.where( + outside, + fill_metres, + rotated_height_metres, + ) + + rotated_data = length_values_from_metres( + rotated_height_metres, + unit=channel.unit, + ) + + if preserve_mean: + rotated_data = rotated_data + np.mean(data) - np.mean(rotated_data) + + return channel.with_data(rotated_data) + + +def _selected_local_facet_slopes( + data: np.ndarray, + selection: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + """Return local x/y facet slopes for fully selected pixel cells.""" + rows, columns = data.shape + + if rows < 2 or columns < 2: + raise ValueError("facet_level requires selected neighbouring pixel cells") + + selected_cells = ( + selection[:-1, :-1] & selection[:-1, 1:] & selection[1:, :-1] & selection[1:, 1:] + ) + + if not np.any(selected_cells): + raise ValueError("facet_level requires selected neighbouring pixel cells") + + x_coordinates = np.linspace(-1.0, 1.0, columns) + y_coordinates = np.linspace(-1.0, 1.0, rows) + + x_step = float(x_coordinates[1] - x_coordinates[0]) + y_step = float(y_coordinates[1] - y_coordinates[0]) + + x_slopes = (data[:-1, 1:] - data[:-1, :-1] + data[1:, 1:] - data[1:, :-1]) / (2.0 * x_step) + + y_slopes = (data[1:, :-1] - data[:-1, :-1] + data[1:, 1:] - data[:-1, 1:]) / (2.0 * y_step) + + return ( + x_slopes[selected_cells], + y_slopes[selected_cells], + ) + + +def _dominant_facet_slopes( + x_slopes: np.ndarray, + y_slopes: np.ndarray, +) -> tuple[float, float]: + """Estimate the dominant local facet slope using Gaussian reweighting.""" + seed_x = _half_sample_mode(x_slopes) + seed_y = _half_sample_mode(y_slopes) + + squared_distances = np.square(x_slopes - seed_x) + np.square(y_slopes - seed_y) + + epsilon = np.finfo(float).eps + positive_distances = squared_distances[squared_distances > epsilon] + + if positive_distances.size == 0: + return seed_x, seed_y + + scale = float(np.median(positive_distances)) + gaussian_constant = 1.0 / 20.0 + + weights = np.exp(-0.5 * squared_distances / (gaussian_constant * scale)) + + if float(np.sum(weights)) <= np.finfo(float).tiny: + return seed_x, seed_y + + return ( + float(np.average(x_slopes, weights=weights)), + float(np.average(y_slopes, weights=weights)), + ) + + +def facet_level( + channel: SPMChannel, + *, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + max_iterations: int = 20, + tolerance: float = 1e-12, + preserve_mean: bool = False, +) -> SPMChannel: + """Level a surface using the prevalent orientation of local facets.""" + data = _validated_data( + channel, + operation="facet_level", + ) + + iterations = _positive_integer( + max_iterations, + name="max_iterations", + operation="facet_level", + ) + convergence_tolerance = _positive_real_scalar( + tolerance, + name="tolerance", + operation="facet_level", + ) + + if not isinstance(preserve_mean, (bool, np.bool_)): + raise TypeError("facet_level requires preserve_mean to be boolean") + + selection = _fit_selection( + data, + mask=mask, + mask_mode=mask_mode, + operation="facet_level", + minimum_points=4, + ) + + rows, columns = data.shape + x_coordinates = np.linspace(-1.0, 1.0, columns) + y_coordinates = np.linspace(-1.0, 1.0, rows) + xx, yy = np.meshgrid(x_coordinates, y_coordinates) + + corrections = np.zeros(data.shape, dtype=float) + working = data.astype(float, copy=True) + + for _ in range(iterations): + x_slopes, y_slopes = _selected_local_facet_slopes( + working, + selection, + ) + dominant_x, dominant_y = _dominant_facet_slopes( + x_slopes, + y_slopes, + ) + + if np.hypot(dominant_x, dominant_y) <= convergence_tolerance: + break + + plane_tilt = dominant_x * xx + dominant_y * yy + + corrections += plane_tilt + working -= plane_tilt + + if preserve_mean: + corrections -= np.mean(corrections) + else: + corrections += float(np.mean(working[selection])) + + return channel.with_data(data - corrections) + + +def three_point_level( + channel: SPMChannel, + *, + points: tuple[ + tuple[int, int], + tuple[int, int], + tuple[int, int], + ], +) -> SPMChannel: + """Subtract the plane defined by three non-collinear reference pixels.""" + data = _validated_data(channel, operation="three_point_level") + point_data = np.asarray(points) + + if point_data.shape != (3, 2): + raise ValueError("three_point_level requires exactly three (row, column) points") + + if not np.issubdtype(point_data.dtype, np.integer): + raise TypeError("three_point_level requires integer pixel coordinates") + + point_rows = point_data[:, 0] + point_columns = point_data[:, 1] + + rows, columns = data.shape + out_of_bounds = ( + np.any(point_rows < 0) + or np.any(point_rows >= rows) + or np.any(point_columns < 0) + or np.any(point_columns >= columns) + ) + + if out_of_bounds: + raise ValueError("three_point_level requires points within channel bounds") + + design = np.column_stack( + ( + point_columns.astype(float), + point_rows.astype(float), + np.ones(3), + ) + ) + + if np.linalg.matrix_rank(design) < 3: + raise ValueError("three_point_level requires three non-collinear points") + + heights = data[point_rows, point_columns] + coefficients = np.linalg.solve(design, heights) + + yy, xx = np.mgrid[0:rows, 0:columns] + plane = coefficients[0] * xx + coefficients[1] * yy + coefficients[2] + + return channel.with_data(data - plane) + + +def _fit_polynomial_surface_data( + data: np.ndarray, + *, + powers: tuple[tuple[int, int], ...], + selection: np.ndarray, + operation: str, +) -> tuple[np.ndarray, np.ndarray, int, np.ndarray]: + """Fit and evaluate polynomial terms on normalized pixel coordinates.""" + values = np.asarray(data, dtype=float) + selected_points = np.asarray(selection, dtype=bool) + + if values.ndim != 2: + raise ValueError(f"{operation} requires a two-dimensional array") + if selected_points.shape != values.shape: + raise ValueError(f"{operation} requires selection to match data shape") + if not powers: + raise ValueError(f"{operation} requires at least one polynomial term") + + selected_count = int(np.count_nonzero(selected_points)) + if selected_count < len(powers): + raise ValueError(f"{operation} requires at least {len(powers)} selected points") + + rows, columns = values.shape + x_coordinates = np.linspace(-1.0, 1.0, columns) if columns > 1 else np.zeros(columns) + y_coordinates = np.linspace(-1.0, 1.0, rows) if rows > 1 else np.zeros(rows) + xx, yy = np.meshgrid(x_coordinates, y_coordinates) + + terms = [(xx**x_power) * (yy**y_power) for x_power, y_power in powers] + design = np.column_stack([term.ravel() for term in terms]) + selected = selected_points.ravel() + + coefficients, _, rank, singular_values = np.linalg.lstsq( + design[selected], + values.ravel()[selected], + rcond=None, + ) + + if rank < len(powers): + raise ValueError( + f"{operation} selected points do not define " "a unique polynomial background" + ) + + background = (design @ coefficients).reshape(values.shape) + + return ( + background, + coefficients, + int(rank), + singular_values, + ) + + +def _estimate_polynomial_background_data( + channel: SPMChannel, + *, + degree_mode: Literal["total", "independent"] = "total", + degree: int = 2, + x_degree: int | None = None, + y_degree: int | None = None, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", +) -> np.ndarray: + """Estimate a fitted two-dimensional polynomial background array.""" + data = _validated_data( + channel, + operation="polynomial_background", + ) + + if degree_mode not in {"total", "independent"}: + raise ValueError("polynomial_background degree_mode must be 'total' or 'independent'") + + if degree_mode == "total": + if x_degree is not None or y_degree is not None: + raise ValueError( + "polynomial_background total degree mode does not accept x_degree or y_degree" + ) + + total_degree = _nonnegative_integer( + degree, + name="degree", + operation="polynomial_background", + ) + powers = [ + (x_power, y_power) + for x_power in range(total_degree + 1) + for y_power in range(total_degree + 1 - x_power) + ] + + else: + if x_degree is None or y_degree is None: + raise ValueError( + "polynomial_background independent degree mode requires x_degree and y_degree" + ) + + horizontal_degree = _nonnegative_integer( + x_degree, + name="x_degree", + operation="polynomial_background", + ) + vertical_degree = _nonnegative_integer( + y_degree, + name="y_degree", + operation="polynomial_background", + ) + + powers = [ + (x_power, y_power) + for x_power in range(horizontal_degree + 1) + for y_power in range(vertical_degree + 1) + ] + + selection = _fit_selection( + data, + mask=mask, + mask_mode=mask_mode, + operation="polynomial_background", + minimum_points=len(powers), + ) + + background, _, _, _ = _fit_polynomial_surface_data( + data, + powers=tuple(powers), + selection=selection, + operation="polynomial_background", + ) + + return background + + +def polynomial_background( + channel: SPMChannel, + *, + degree_mode: Literal["total", "independent"] = "total", + degree: int = 2, + x_degree: int | None = None, + y_degree: int | None = None, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", +) -> SPMChannel: + """Subtract a fitted two-dimensional polynomial background.""" + background = _estimate_polynomial_background_data( + channel, + degree_mode=degree_mode, + degree=degree, + x_degree=x_degree, + y_degree=y_degree, + mask=mask, + mask_mode=mask_mode, + ) + data = np.asarray(channel.data, dtype=float) + return channel.with_data(data - background) def polynomial(channel: SPMChannel, order: int = 2) -> SPMChannel: - """Resta una superficie polinómica 2D de grado ``order``. + """Subtract a limited-total-degree polynomial background. - Útil cuando hay curvatura (bow) además de inclinación. + This function preserves the original SPMKit API. New code should use + :func:`polynomial_background`. """ + if isinstance(order, (bool, np.bool_)) or not isinstance( + order, + (int, np.integer), + ): + raise TypeError("order debe ser un entero") + if order < 1: raise ValueError("order debe ser >= 1") - z = channel.data - rows, cols = z.shape - yy, xx = np.mgrid[0:rows, 0:cols] - x = xx.ravel().astype(np.float64) - y = yy.ravel().astype(np.float64) - terms = [(x**i) * (y**j) for i in range(order + 1) for j in range(order + 1 - i)] - a_mat = np.column_stack(terms) - coeffs, *_ = np.linalg.lstsq(a_mat, z.ravel(), rcond=None) - surface = (a_mat @ coeffs).reshape(z.shape) - return channel.with_data(z - surface) + return polynomial_background( + channel, + degree_mode="total", + degree=int(order), + ) -def align_rows(channel: SPMChannel, method: str = "median") -> SPMChannel: - """Alinea filas restando su estadístico (corrige saltos línea a línea). - Args: - method: ``"median"`` (robusto) o ``"mean"``. - """ - z = channel.data +def _half_sample_mode(values: np.ndarray) -> float: + """Estimate the mode using the deterministic half-sample method.""" + ordered = np.sort(np.asarray(values, dtype=float)) + + while True: + count = ordered.size + + if count == 1: + return float(ordered[0]) + + if count == 2: + return float(np.mean(ordered)) + + if count == 3: + left_width = ordered[1] - ordered[0] + right_width = ordered[2] - ordered[1] + + if left_width < right_width: + return float(np.mean(ordered[:2])) + + if right_width < left_width: + return float(np.mean(ordered[1:])) + + return float(ordered[1]) + + interval_size = (count + 1) // 2 + widths = ordered[interval_size - 1 :] - ordered[: count - interval_size + 1] + start = int(np.argmin(widths)) + + ordered = ordered[start : start + interval_size] + + +def _facet_tilt_row_corrections( + data: np.ndarray, + selection: np.ndarray, +) -> np.ndarray: + """Estimate per-row tilt from the prevalent local slope.""" + row_count, column_count = data.shape + + if column_count < 2: + raise ValueError("align_rows facet_tilt requires selected neighbouring pixels in every row") + + x_coordinates = np.linspace( + -1.0, + 1.0, + column_count, + ) + x_coordinates = x_coordinates - np.mean(x_coordinates) + x_steps = np.diff(x_coordinates) + + corrections = np.empty(data.shape, dtype=float) + + for row_index in range(row_count): + selected_edges = selection[row_index, :-1] & selection[row_index, 1:] + + if not np.any(selected_edges): + raise ValueError( + "align_rows facet_tilt requires selected neighbouring pixels in every row" + ) + + local_slopes = np.diff(data[row_index]) / x_steps + + prevalent_slope = _half_sample_mode(local_slopes[selected_edges]) + + corrections[row_index] = prevalent_slope * x_coordinates + + return corrections + + +def _without_linear_row_component( + corrections: np.ndarray, +) -> np.ndarray: + """Remove the least-squares linear component from row corrections.""" + row_count = corrections.size + + if row_count <= 1: + return corrections.copy() + + row_coordinates = np.arange(row_count, dtype=float) + centered_rows = row_coordinates - np.mean(row_coordinates) + centered_corrections = corrections - np.mean(corrections) + + denominator = float(np.dot(centered_rows, centered_rows)) + + if denominator == 0.0: + return corrections.copy() + + correction_slope = float(np.dot(centered_rows, centered_corrections) / denominator) + + return corrections - correction_slope * centered_rows + + +def _matching_row_corrections( + data: np.ndarray, + selection: np.ndarray, + *, + preserve_tilt: bool, +) -> np.ndarray: + """Estimate row offsets by matching locally flat neighbouring segments.""" + row_count = data.shape[0] + corrections = np.zeros(row_count, dtype=float) + + for row_index in range(1, row_count): + shared_selection = selection[row_index - 1] & selection[row_index] + shared_edges = shared_selection[:-1] & shared_selection[1:] + + if not np.any(shared_edges): + raise ValueError( + "align_rows matching requires adjacent rows to share selected neighbouring pixels" + ) + + previous_row = data[row_index - 1] + current_row = data[row_index] + + vertical_differences = 0.5 * ( + current_row[:-1] - previous_row[:-1] + current_row[1:] - previous_row[1:] + ) + + previous_slopes = np.diff(previous_row) + current_slopes = np.diff(current_row) + + local_flatness = np.abs(previous_slopes) + np.abs(current_slopes) + selected_flatness = local_flatness[shared_edges] + + scale = float(np.median(selected_flatness)) + epsilon = np.finfo(float).eps + + if scale <= epsilon: + positive_flatness = selected_flatness[selected_flatness > epsilon] + scale = float(np.median(positive_flatness)) if positive_flatness.size else 1.0 + + normalized_flatness = selected_flatness / scale + weights = 1.0 / np.square(np.hypot(1.0, normalized_flatness)) + + increment = float( + np.average( + vertical_differences[shared_edges], + weights=weights, + ) + ) + + corrections[row_index] = corrections[row_index - 1] + increment + + if preserve_tilt: + corrections = _without_linear_row_component(corrections) + + return corrections + + +def _legacy_align_rows( + channel: SPMChannel, + method: Literal["median", "mean"], +) -> SPMChannel: + """Preserve the origin/main default median/mean call semantics.""" + data = channel.data if method == "median": - baseline = np.median(z, axis=1, keepdims=True) - elif method == "mean": - baseline = np.mean(z, axis=1, keepdims=True) + baseline = np.median(data, axis=1, keepdims=True) + else: + baseline = np.mean(data, axis=1, keepdims=True) + return channel.with_data(data - baseline) + + +def _difference_row_corrections( + data: np.ndarray, + selection: np.ndarray, + *, + statistic: Literal["median", "trimmed_mean"], + trim_fraction: float, + preserve_tilt: bool, +) -> np.ndarray: + """Estimate cumulative row offsets from vertical neighbour differences.""" + + row_count = data.shape[0] + + corrections = np.zeros(row_count, dtype=float) + + for row_index in range(1, row_count): + shared_selection = selection[row_index - 1] & selection[row_index] + + if not np.any(shared_selection): + raise ValueError("align_rows requires adjacent rows to share selected points") + + differences = data[row_index, shared_selection] - data[row_index - 1, shared_selection] + + if statistic == "median": + increment = float(np.median(differences)) + + else: + increment = _trimmed_mean( + differences, + trim_fraction, + ) + + corrections[row_index] = corrections[row_index - 1] + increment + + if preserve_tilt: + corrections = _without_linear_row_component(corrections) + + return corrections + + +def align_rows( + channel: SPMChannel, + method: Literal[ + "median", + "mean", + "mode", + "trimmed_mean", + "polynomial", + "median_difference", + "trimmed_mean_difference", + "matching", + "facet_tilt", + ] = "median", + *, + trim_fraction: float = 0.0, + polynomial_degree: int = 1, + mask: np.ndarray | None = None, + mask_mode: Literal["ignore", "include", "exclude"] = "ignore", + preserve_mean: bool = False, + preserve_tilt: bool = True, +) -> SPMChannel: + """Align rows by subtracting a fitted or representative row background. + + Historical ``method="median"``/``"mean"`` calls, including positional + method arguments, retain their SPMKit behavior. The additional methods + and keyword options are a backward-compatible SPMKit extension; this + dispatcher is not the Gwyddion compatibility contract. Use the four + explicit ``gwyddion_align_rows_*`` functions for that contract. + + ``preserve_mean=False`` retains the historical SPMKit behaviour. + ``preserve_mean=True`` keeps the mean correction at zero, matching + the absolute-level convention used by Gwyddion. + """ + legacy_defaults = ( + method in {"median", "mean"} + and mask is None + and mask_mode == "ignore" + and preserve_mean is False + and preserve_tilt is True + and isinstance(trim_fraction, (int, float, np.integer, np.floating)) + and float(trim_fraction) == 0.0 + and isinstance(polynomial_degree, (int, np.integer)) + and not isinstance(polynomial_degree, (bool, np.bool_)) + and int(polynomial_degree) == 1 + ) + if legacy_defaults and method == "median": + return _legacy_align_rows(channel, "median") + if legacy_defaults and method == "mean": + return _legacy_align_rows(channel, "mean") + + data = _validated_data(channel, operation="align_rows") + + allowed_methods = { + "median", + "mean", + "mode", + "trimmed_mean", + "polynomial", + "median_difference", + "trimmed_mean_difference", + "matching", + "facet_tilt", + } + + if method not in allowed_methods: + raise ValueError( + "align_rows method must be 'median', 'mean', 'mode', " + "'trimmed_mean', 'polynomial', 'median_difference', " + "'trimmed_mean_difference', 'matching', or 'facet_tilt'" + ) + + if not isinstance(preserve_mean, (bool, np.bool_)): + raise TypeError("align_rows requires preserve_mean to be boolean") + + if not isinstance(preserve_tilt, (bool, np.bool_)): + raise TypeError("align_rows requires preserve_tilt to be boolean") + + fraction = _trim_fraction( + trim_fraction, + operation="align_rows", + ) + + selection = _fit_selection( + data, + mask=mask, + mask_mode=mask_mode, + operation="align_rows", + minimum_points=1, + ) + + if method == "polynomial": + degree = _nonnegative_integer( + polynomial_degree, + name="polynomial_degree", + operation="align_rows", + ) + required_points = degree + 1 + else: + degree = 0 + required_points = 1 + + selected_per_row = np.count_nonzero(selection, axis=1) + + if np.any(selected_per_row < required_points): + point_word = "point" if required_points == 1 else "points" + raise ValueError( + f"align_rows requires at least {required_points} selected {point_word} in every row" + ) + + difference_methods = { + "median_difference", + "trimmed_mean_difference", + } + + if method == "facet_tilt": + corrections = _facet_tilt_row_corrections( + data, + selection, + ) + + elif method == "matching": + row_corrections = _matching_row_corrections( + data, + selection, + preserve_tilt=bool(preserve_tilt), + ) + corrections = row_corrections[:, np.newaxis] + + elif method in difference_methods: + statistic: Literal["median", "trimmed_mean"] = ( + "median" if method == "median_difference" else "trimmed_mean" + ) + + row_corrections = _difference_row_corrections( + data, + selection, + statistic=statistic, + trim_fraction=fraction, + preserve_tilt=bool(preserve_tilt), + ) + corrections = row_corrections[:, np.newaxis] + + elif method == "polynomial": + columns = data.shape[1] + x_coordinates = np.linspace(-1.0, 1.0, columns) if columns > 1 else np.zeros(columns) + + design = np.vander( + x_coordinates, + N=degree + 1, + increasing=True, + ) + + corrections = np.empty(data.shape, dtype=float) + + for row_index in range(data.shape[0]): + selected = selection[row_index] + + coefficients, _, rank, _ = np.linalg.lstsq( + design[selected], + data[row_index, selected], + rcond=None, + ) + + if rank < degree + 1: + raise ValueError( + "align_rows selected points do not define a unique polynomial in every row" + ) + + corrections[row_index] = design @ coefficients + else: - raise ValueError("method debe ser 'median' o 'mean'") - return channel.with_data(z - baseline) + baselines = np.empty(data.shape[0], dtype=float) + + for row_index in range(data.shape[0]): + row_values = data[row_index, selection[row_index]] + + if method == "median": + baselines[row_index] = np.median(row_values) + elif method == "mean": + baselines[row_index] = np.mean(row_values) + elif method == "mode": + baselines[row_index] = _half_sample_mode(row_values) + else: + baselines[row_index] = _trimmed_mean( + row_values, + fraction, + ) + + corrections = baselines[:, np.newaxis] + + if preserve_mean: + corrections = corrections - np.mean(corrections) + + return channel.with_data(data - corrections) diff --git a/src/spmkit/core/geometry.py b/src/spmkit/core/geometry.py new file mode 100644 index 0000000..f144d57 --- /dev/null +++ b/src/spmkit/core/geometry.py @@ -0,0 +1,287 @@ +"""Physical geometry primitives for SPM image transformations. + +Coordinates use physical pixel centres. Lateral ranges are expressed in +metres, while channel heights can be converted from supported length units. +The module depends only on NumPy so geometric Core operations remain available +in the minimal SPMKit installation. +""" + +from __future__ import annotations + +from typing import Literal + +import numpy as np + +FillMode = Literal["nearest", "constant"] + + +_LENGTH_SCALES_TO_METRES: dict[str, float] = { + "m": 1.0, + "metre": 1.0, + "meter": 1.0, + "mm": 1e-3, + "millimetre": 1e-3, + "millimeter": 1e-3, + "µm": 1e-6, + "um": 1e-6, + "micrometre": 1e-6, + "micrometer": 1e-6, + "nm": 1e-9, + "nanometre": 1e-9, + "nanometer": 1e-9, + "pm": 1e-12, + "picometre": 1e-12, + "picometer": 1e-12, + "å": 1e-10, + "ångström": 1e-10, + "angstrom": 1e-10, +} + + +def _positive_finite_scalar( + value: object, + *, + name: str, + operation: str, +) -> float: + """Validate a finite strictly positive real scalar.""" + scalar_data = np.asarray(value) + + if ( + scalar_data.ndim != 0 + or not np.issubdtype(scalar_data.dtype, np.number) + or np.iscomplexobj(scalar_data) + or isinstance(value, (bool, np.bool_)) + ): + raise TypeError(f"{operation} requires {name} to be a positive real scalar") + + scalar = float(scalar_data.item()) + + if not np.isfinite(scalar): + raise ValueError(f"{operation} requires {name} to be finite") + + if scalar <= 0.0: + raise ValueError(f"{operation} requires {name} to be positive") + + return scalar + + +def _validated_shape( + shape: object, + *, + operation: str, +) -> tuple[int, int]: + """Validate a non-empty two-dimensional array shape.""" + if ( + not isinstance(shape, tuple) + or len(shape) != 2 + or any( + isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)) + for value in shape + ) + ): + raise TypeError(f"{operation} requires shape to be a two-integer tuple") + + rows, columns = (int(shape[0]), int(shape[1])) + + if rows <= 0 or columns <= 0: + raise ValueError(f"{operation} requires a non-empty two-dimensional shape") + + return rows, columns + + +def length_scale_to_metres(unit: str) -> float: + """Return the multiplicative factor converting a length unit to metres.""" + if not isinstance(unit, str): + raise TypeError("length_scale_to_metres requires unit to be a string") + + normalized = unit.strip().replace("μ", "µ").lower() + + try: + return _LENGTH_SCALES_TO_METRES[normalized] + except KeyError as exc: + raise ValueError(f"unsupported geometric length unit: {unit!r}") from exc + + +def length_values_to_metres( + values: np.ndarray, + *, + unit: str, +) -> np.ndarray: + """Convert finite length values to metres.""" + data = np.asarray(values) + + if not np.issubdtype(data.dtype, np.number): + raise TypeError("length_values_to_metres requires numeric values") + + if np.iscomplexobj(data): + raise TypeError("length_values_to_metres requires real values") + + if not np.all(np.isfinite(data)): + raise ValueError("length_values_to_metres requires finite values") + + return data.astype(float, copy=False) * length_scale_to_metres(unit) + + +def length_values_from_metres( + values: np.ndarray, + *, + unit: str, +) -> np.ndarray: + """Convert finite metre values to the requested length unit.""" + data = np.asarray(values, dtype=float) + + if not np.all(np.isfinite(data)): + raise ValueError("length_values_from_metres requires finite values") + + return data / length_scale_to_metres(unit) + + +def pixel_center_axes( + shape: tuple[int, int], + *, + x_range: float, + y_range: float, +) -> tuple[np.ndarray, np.ndarray]: + """Return centred physical X and Y pixel-centre coordinates in metres.""" + rows, columns = _validated_shape( + shape, + operation="pixel_center_axes", + ) + physical_x_range = _positive_finite_scalar( + x_range, + name="x_range", + operation="pixel_center_axes", + ) + physical_y_range = _positive_finite_scalar( + y_range, + name="y_range", + operation="pixel_center_axes", + ) + + x_step = physical_x_range / columns + y_step = physical_y_range / rows + + x_coordinates = (np.arange(columns, dtype=float) + 0.5) * x_step - 0.5 * physical_x_range + + y_coordinates = (np.arange(rows, dtype=float) + 0.5) * y_step - 0.5 * physical_y_range + + return x_coordinates, y_coordinates + + +def physical_to_pixel_indices( + x_coordinates: np.ndarray, + y_coordinates: np.ndarray, + *, + shape: tuple[int, int], + x_range: float, + y_range: float, +) -> tuple[np.ndarray, np.ndarray]: + """Map centred physical coordinates to fractional pixel indices.""" + rows, columns = _validated_shape( + shape, + operation="physical_to_pixel_indices", + ) + physical_x_range = _positive_finite_scalar( + x_range, + name="x_range", + operation="physical_to_pixel_indices", + ) + physical_y_range = _positive_finite_scalar( + y_range, + name="y_range", + operation="physical_to_pixel_indices", + ) + + x_data, y_data = np.broadcast_arrays( + np.asarray(x_coordinates, dtype=float), + np.asarray(y_coordinates, dtype=float), + ) + + if not (np.all(np.isfinite(x_data)) and np.all(np.isfinite(y_data))): + raise ValueError("physical_to_pixel_indices requires finite coordinates") + + x_step = physical_x_range / columns + y_step = physical_y_range / rows + + x_indices = x_data / x_step + 0.5 * columns - 0.5 + y_indices = y_data / y_step + 0.5 * rows - 0.5 + + return x_indices, y_indices + + +def bilinear_sample( + data: np.ndarray, + *, + x_index: np.ndarray, + y_index: np.ndarray, + fill_mode: FillMode = "nearest", + fill_value: float = 0.0, +) -> np.ndarray: + """Sample a regular 2D grid at fractional pixel indices.""" + values = np.asarray(data) + + if values.ndim != 2 or values.size == 0: + raise ValueError("bilinear_sample requires non-empty two-dimensional data") + + if not np.issubdtype(values.dtype, np.number) or np.iscomplexobj(values): + raise TypeError("bilinear_sample requires real numeric data") + + if not np.all(np.isfinite(values)): + raise ValueError("bilinear_sample requires finite data") + + if fill_mode not in {"nearest", "constant"}: + raise ValueError("bilinear_sample fill_mode must be 'nearest' or 'constant'") + + x_data, y_data = np.broadcast_arrays( + np.asarray(x_index, dtype=float), + np.asarray(y_index, dtype=float), + ) + + if not (np.all(np.isfinite(x_data)) and np.all(np.isfinite(y_data))): + raise ValueError("bilinear_sample requires finite sample coordinates") + + rows, columns = values.shape + + outside = (x_data < 0.0) | (x_data > columns - 1) | (y_data < 0.0) | (y_data > rows - 1) + + sampled_x = np.clip(x_data, 0.0, columns - 1) + sampled_y = np.clip(y_data, 0.0, rows - 1) + + x0 = np.floor(sampled_x).astype(int) + y0 = np.floor(sampled_y).astype(int) + x1 = np.minimum(x0 + 1, columns - 1) + y1 = np.minimum(y0 + 1, rows - 1) + + x_weight = sampled_x - x0 + y_weight = sampled_y - y0 + + top = values[y0, x0] * (1.0 - x_weight) + values[y0, x1] * x_weight + bottom = values[y1, x0] * (1.0 - x_weight) + values[y1, x1] * x_weight + sampled = top * (1.0 - y_weight) + bottom * y_weight + + if fill_mode == "constant": + fill = _positive_or_zero_finite_scalar(fill_value) + sampled = np.where(outside, fill, sampled) + + return np.asarray(sampled, dtype=float) + + +def _positive_or_zero_finite_scalar(value: object) -> float: + """Validate a finite real scalar used as a fill value.""" + scalar_data = np.asarray(value) + + if ( + scalar_data.ndim != 0 + or not np.issubdtype(scalar_data.dtype, np.number) + or np.iscomplexobj(scalar_data) + or isinstance(value, (bool, np.bool_)) + ): + raise TypeError("bilinear_sample requires fill_value to be a real scalar") + + scalar = float(scalar_data.item()) + + if not np.isfinite(scalar): + raise ValueError("bilinear_sample requires fill_value to be finite") + + return scalar diff --git a/src/spmkit/gui/viewmodels/grains_vm.py b/src/spmkit/gui/viewmodels/grains_vm.py index c6382bc..25e3d53 100644 --- a/src/spmkit/gui/viewmodels/grains_vm.py +++ b/src/spmkit/gui/viewmodels/grains_vm.py @@ -3,7 +3,7 @@ Corre ``core.analysis.grains.detect`` sobre el canal **nivelado** del hub de imagen (:class:`ImageViewModel`) con parámetros ajustables (tamaño mínimo, altura relativa). Paridad con JPK/ANA (conteo, tamaño, cobertura, densidad de granos). Requiere scipy -(extra ``grains``); si falta, avisa por ``statusChanged`` sin tumbar la app. +como dependencia obligatoria; si falta, avisa por ``statusChanged`` sin tumbar la app. """ from __future__ import annotations @@ -78,7 +78,9 @@ def detect(self) -> None: relative_height=self._relative_height, ) except ImportError: - self.statusChanged.emit("la detección de granos requiere scipy (extra 'grains')") + self.statusChanged.emit( + "la detección de granos requiere SciPy, una dependencia obligatoria de SPMKit" + ) return except Exception as exc: # noqa: BLE001 - parámetros/imagen inválidos: se informa self.statusChanged.emit(f"detección falló: {exc}") diff --git a/src/spmkit/gui/viewmodels/image_vm.py b/src/spmkit/gui/viewmodels/image_vm.py index 1e99007..7e8e69d 100644 --- a/src/spmkit/gui/viewmodels/image_vm.py +++ b/src/spmkit/gui/viewmodels/image_vm.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Literal, cast from PyQt6.QtCore import QObject, pyqtSignal @@ -17,6 +17,8 @@ from spmkit.core.analysis.profiles import line as profile_line from spmkit.core.models import SPMChannel, SPMData +RowStatistic = Literal["median", "mean"] + def channel_labels(data: SPMData | None) -> list[str]: """Etiquetas **únicas** por canal, para los selectores de la GUI. @@ -52,7 +54,7 @@ def __init__(self, parent: QObject | None = None) -> None: self._channel_index = 0 # identidad del canal activo (por posición, no por nombre) self._leveling = "plane" self._poly_order = 2 # grado del nivelado polinómico - self._row_stat = "median" # estadístico del alineado por filas + self._row_stat: RowStatistic = "median" # estadístico del alineado por filas self._tip_work_function: float | None = None # eV; para phi de la muestra (KPFM) self._last_profile: Profile | None = None @@ -131,7 +133,7 @@ def poly_order(self) -> int: return self._poly_order @property - def row_stat(self) -> str: + def row_stat(self) -> RowStatistic: return self._row_stat def set_poly_order(self, order: int) -> None: @@ -144,7 +146,7 @@ def set_poly_order(self, order: int) -> None: def set_row_stat(self, stat: str) -> None: """Estadístico del alineado por filas (``"median"``/``"mean"``).""" if stat != self._row_stat and stat in ("median", "mean"): - self._row_stat = stat + self._row_stat = cast(RowStatistic, stat) if self._leveling == "rows": self.channelChanged.emit(self.channel) diff --git a/tests/compat/test_gwyddion_profiles.py b/tests/compat/test_gwyddion_profiles.py new file mode 100644 index 0000000..5bee21c --- /dev/null +++ b/tests/compat/test_gwyddion_profiles.py @@ -0,0 +1,33 @@ +"""Tests for conservative, version-scoped Gwyddion source-audit profiles.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from spmkit.compat.gwyddion.errors import UnsupportedGwyddionProfileError +from spmkit.compat.gwyddion.profiles import ( + GwyddionVersion, + gwyddion_2_71_profile, + profile_for_version, +) + + +def test_gwyddion_2_71_profile_is_immutable_and_conservative() -> None: + profile = gwyddion_2_71_profile() + assert str(profile.version) == "2.71" + assert "GWY_MODULE_QUERY2" in profile.registration_calls + assert "gwy_tool_func_register" in profile.registration_calls + assert profile.mapping_dict["gwy_data_field_get_xres"] == "SPMChannel.shape[1]" + assert "gwy_data_field_area_filter_min_max" not in profile.mapping_dict + with pytest.raises(FrozenInstanceError): + profile.name = "other" # type: ignore[misc] + + +def test_profile_lookup_never_approximates_an_unsupported_version() -> None: + assert profile_for_version(GwyddionVersion(2, 71)) == gwyddion_2_71_profile() + with pytest.raises(UnsupportedGwyddionProfileError): + profile_for_version(GwyddionVersion(2, 72)) + with pytest.raises(TypeError): + profile_for_version("2.71") # type: ignore[arg-type] diff --git a/tests/compat/test_gwyddion_reports.py b/tests/compat/test_gwyddion_reports.py new file mode 100644 index 0000000..29c58b8 --- /dev/null +++ b/tests/compat/test_gwyddion_reports.py @@ -0,0 +1,39 @@ +"""Deterministic serialization tests for static Gwyddion audit reports.""" + +from __future__ import annotations + +import json + +import pytest + +from spmkit.compat.gwyddion.errors import InvalidGwyddionSourceError +from spmkit.compat.gwyddion.reports import canonical_report_json, report_from_dict, report_to_dict +from spmkit.compat.gwyddion.source_audit import audit_gwyddion_source + + +def _report(): + return audit_gwyddion_source( + """ +#include +GWY_MODULE_QUERY2(module_info, report_sample) +gwy_process_func_register("report-sample", callback); +gwy_data_field_get_yreal(field); +""", + source_path="modules/process/report-sample.c", + ) + + +def test_canonical_json_is_stable_and_round_trips() -> None: + first = _report() + second = _report() + first_json = canonical_report_json(first) + assert first_json == canonical_report_json(second) + reconstructed = report_from_dict(json.loads(first_json)) + assert report_to_dict(reconstructed) == report_to_dict(first) + assert canonical_report_json(reconstructed) == first_json + assert "modules/process/report-sample.c" in first_json + + +def test_audit_rejects_non_text_source_without_writing() -> None: + with pytest.raises(InvalidGwyddionSourceError): + audit_gwyddion_source(b"gwy_process_func_register") # type: ignore[arg-type] diff --git a/tests/compat/test_gwyddion_source_audit.py b/tests/compat/test_gwyddion_source_audit.py new file mode 100644 index 0000000..8b54063 --- /dev/null +++ b/tests/compat/test_gwyddion_source_audit.py @@ -0,0 +1,115 @@ +"""Source-fact tests for the lexical Gwyddion migration auditor.""" + +from __future__ import annotations + +from spmkit.compat.gwyddion.source_audit import audit_gwyddion_source +from spmkit.compat.gwyddion.symbols import ( + RegistrationKind, + SymbolClassification, + SymbolSupportStatus, +) + +_SOURCE_SNIPPETS = { + "modules/tools/pathlevel.c": "\n" * 110 + + ' gwy_tool_func_register("pathlevel", callback);\n' + + "GWY_MODULE_QUERY2(module_info, pathlevel)\n" + + "#include \n" + + "gtk_widget_show(widget);\n" + + "gwy_plain_tool_connect_selection(tool);\n" + + "gwy_params_new_from_settings();\n", + "modules/tools/filter.c": ( + 'gwy_tool_func_register("filter", callback);\n' + "gwy_data_field_area_filter_min_max(field);\n" + ), + "modules/process/median-bg.c": ( + 'gwy_process_func_register("median-bg", callback);\n' "gwy_app_channel_log_add_proc();\n" + ), +} + + +def _audit(relative: str): + return audit_gwyddion_source( + _SOURCE_SNIPPETS[relative], + source_path=relative, + ) + + +def test_lexical_scanner_ignores_comments_and_literals_and_retains_calls() -> None: + source = """ +#include "local-header.h" +/* gwy_process_func_register("fake", nope); GWY_MODULE_QUERY2(fake, wrong) */ +const char *message = "gwy_tool_func_register(GWY_FAKE)"; +const char quoted = 'g'; +GWY_MODULE_QUERY2(module_info, synthetic) +gwy_process_func_register( + "real-process", + callback, + 0 +); +gwy_data_field_get_xres(field); +gwy_data_field_get_xres(field); +gwy_future_symbol(); +gwy_custom_func_register(); +gtk_widget_show(widget); +gwyish_data_field_get_xres(field); +""" + report = audit_gwyddion_source(source, source_path="synthetic.c") + assert [(item.kind, item.declared_name) for item in report.registrations] == [ + (RegistrationKind.UNKNOWN, "synthetic"), + (RegistrationKind.PROCESS, "real-process"), + (RegistrationKind.UNKNOWN, None), + ] + symbols = {item.symbol: item for item in report.gwyddion_symbols} + assert "gwy_tool_func_register" not in symbols + assert "gwyish_data_field_get_xres" not in symbols + assert len(symbols["gwy_data_field_get_xres"].occurrences) == 2 + assert len(symbols["gwy_data_field_get_xres"].call_occurrences) == 2 + assert symbols["gwy_data_field_get_xres"].support_status is SymbolSupportStatus.MAPPED + assert symbols["gwy_future_symbol"].classification is SymbolClassification.UNKNOWN + assert symbols["gwy_future_symbol"].support_status is SymbolSupportStatus.UNKNOWN + assert report.includes[0].name == "local-header.h" + assert report.includes[0].is_local is True + assert report.has_ui_dependency is True + assert report.unsupported_total == 1 + + +def test_incomplete_source_never_crashes_the_lexical_inventory() -> None: + report = audit_gwyddion_source("gwy_data_field_get_xres(field; /* unfinished") + assert report.gwyddion_symbols[0].symbol == "gwy_data_field_get_xres" + assert len(report.gwyddion_symbols[0].call_occurrences) == 1 + + +def test_representative_path_level_source_facts_are_extracted_with_locations() -> None: + report = _audit("modules/tools/pathlevel.c") + assert report.module_path == "modules/tools/pathlevel.c" + assert any(item.kind is RegistrationKind.TOOL for item in report.registrations) + assert any(item.declared_name == "pathlevel" for item in report.registrations) + assert any(include.name == "gtk/gtk.h" for include in report.includes) + assert report.has_ui_dependency is True + assert "gwy_plain_tool_connect_selection" in report.likely_selection_dependencies + assert "gwy_params_new_from_settings" in report.likely_parameter_system_dependencies + tool_registration = next( + item for item in report.registrations if item.kind is RegistrationKind.TOOL + ) + assert tool_registration.span.start.line == 111 + assert tool_registration.span.start.column == 5 + + +def test_representative_filter_and_median_sources_remain_audit_inventory_only() -> None: + filter_report = _audit("modules/tools/filter.c") + median_report = _audit("modules/process/median-bg.c") + assert any(item.kind is RegistrationKind.TOOL for item in filter_report.registrations) + filter_symbols = {item.symbol: item for item in filter_report.gwyddion_symbols} + assert filter_symbols["gwy_data_field_area_filter_min_max"].classification is ( + SymbolClassification.PROCESS_NUMERICAL + ) + assert filter_symbols["gwy_data_field_area_filter_min_max"].support_status is ( + SymbolSupportStatus.ADAPTER_REQUIRED + ) + process_registration = next( + item for item in median_report.registrations if item.kind is RegistrationKind.PROCESS + ) + assert process_registration.declared_name == "median-bg" + assert "gwy_app_channel_log_add_proc" in median_report.likely_publication_logging_dependencies + assert median_report.migration_warnings + assert any("does not establish" in warning for warning in median_report.migration_warnings) diff --git a/tests/core/test_arc_revolution_background.py b/tests/core/test_arc_revolution_background.py new file mode 100644 index 0000000..625b8c1 --- /dev/null +++ b/tests/core/test_arc_revolution_background.py @@ -0,0 +1,901 @@ +"""Tests for physical arc-revolution background estimation.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from spmkit.core.analysis.background import ( + _arc_structure, + estimate_arc_revolution_background, + remove_arc_revolution_background, +) +from spmkit.core.models import SPMChannel + + +def _channel( + data: np.ndarray, + *, + unit: str = "m", + x_range: float | None = None, + y_range: float | None = None, +) -> SPMChannel: + rows, columns = data.shape + + return SPMChannel( + name="Z-Axis", + data=np.asarray(data), + unit=unit, + x_range=float(columns) if x_range is None else x_range, + y_range=float(rows) if y_range is None else y_range, + direction="backward", + group="Synthetic", + metadata={"source": "arc-test"}, + ) + + +def _nearest_index(index: int, size: int) -> int: + return min(max(index, 0), size - 1) + + +def _reflect_index(index: int, size: int) -> int: + """Map an integer index using SciPy's half-sample reflect convention.""" + period = 2 * size + position = index % period + + if position < size: + return position + + return period - 1 - position + + +def _brute_force_below_nearest( + profile: np.ndarray, + *, + radius: float, + spacing: float, +) -> np.ndarray: + """Independent one-dimensional rolling-circle oracle.""" + values = np.asarray(profile, dtype=float) + maximum_offset = min( + int(np.floor(radius / spacing)), + values.size - 1, + ) + + offsets = np.arange( + -maximum_offset, + maximum_offset + 1, + dtype=int, + ) + distances = offsets.astype(float) * spacing + sagitta = radius - np.sqrt(np.maximum(radius**2 - distances**2, 0.0)) + + eroded = np.empty_like(values) + + for center in range(values.size): + candidates = [ + values[_nearest_index(center + offset, values.size)] + sag + for offset, sag in zip(offsets, sagitta, strict=True) + ] + eroded[center] = min(candidates) + + opened = np.empty_like(values) + + for position in range(values.size): + candidates = [ + eroded[_nearest_index(position - offset, values.size)] - sag + for offset, sag in zip(offsets, sagitta, strict=True) + ] + opened[position] = max(candidates) + + return opened + + +def _brute_force_below_reflect( + profile: np.ndarray, + *, + radius: float, + spacing: float, +) -> np.ndarray: + """Independent rolling-circle oracle with reflected boundaries.""" + values = np.asarray(profile, dtype=float) + maximum_offset = min( + int(np.floor(radius / spacing)), + values.size - 1, + ) + + offsets = np.arange( + -maximum_offset, + maximum_offset + 1, + dtype=int, + ) + distances = offsets.astype(float) * spacing + ratio = distances / radius + squared_ratio = np.square(ratio) + sagitta = radius * squared_ratio / (1.0 + np.sqrt(np.maximum(1.0 - squared_ratio, 0.0))) + + eroded = np.empty_like(values) + + for center in range(values.size): + candidates = [ + values[_reflect_index(center + offset, values.size)] + sag + for offset, sag in zip(offsets, sagitta, strict=True) + ] + eroded[center] = min(candidates) + + opened = np.empty_like(values) + + for position in range(values.size): + candidates = [ + eroded[_reflect_index(position - offset, values.size)] - sag + for offset, sag in zip(offsets, sagitta, strict=True) + ] + opened[position] = max(candidates) + + return opened + + +def test_flat_surface_is_preserved() -> None: + data = np.full((5, 7), 3.25) + channel = _channel(data) + + background = estimate_arc_revolution_background( + channel, + radius=3.0, + ) + corrected = remove_arc_revolution_background( + channel, + radius=3.0, + ) + + assert np.allclose(background.data, data) + assert np.allclose(corrected.data, 0.0) + + +def test_horizontal_matches_independent_one_dimensional_oracle() -> None: + profile = np.array([0.0, 0.2, 1.8, 0.5, 0.1, 0.0]) + data = np.vstack([profile, profile + 2.0]) + channel = _channel( + data, + x_range=float(profile.size), + y_range=2.0, + ) + + result = estimate_arc_revolution_background( + channel, + radius=2.5, + direction="horizontal", + border="nearest", + ) + + expected_first = _brute_force_below_nearest( + profile, + radius=2.5, + spacing=1.0, + ) + + assert np.allclose(result.data[0], expected_first) + assert np.allclose(result.data[1], expected_first + 2.0) + + +def test_above_is_exact_inversion_dual() -> None: + data = np.array( + [ + [0.0, -0.2, -1.5, -0.3, 0.0], + [0.1, -0.1, -1.0, -0.2, 0.2], + ] + ) + channel = _channel(data) + inverted = channel.with_data(-data) + + above = estimate_arc_revolution_background( + channel, + radius=2.0, + direction="horizontal", + side="above", + ) + below_inverted = estimate_arc_revolution_background( + inverted, + radius=2.0, + direction="horizontal", + side="below", + ) + + assert np.allclose(above.data, -below_inverted.data) + + +def test_reconstruction_identity() -> None: + yy, xx = np.mgrid[0:7, 0:9] + data = 0.02 * xx + 0.03 * yy + 2.0 * np.exp(-((xx - 4) ** 2 + (yy - 3) ** 2) / 2.0) + channel = _channel( + data, + x_range=9e-6, + y_range=14e-6, + ) + + background = estimate_arc_revolution_background( + channel, + radius=4e-6, + direction="both", + ) + corrected = remove_arc_revolution_background( + channel, + radius=4e-6, + direction="both", + ) + + assert np.allclose( + corrected.data + background.data, + data, + rtol=1e-12, + atol=1e-12, + ) + + +def test_radius_smaller_than_pixel_spacing_is_identity() -> None: + data = np.arange(12.0).reshape(3, 4) + channel = _channel( + data, + x_range=4.0, + y_range=3.0, + ) + + background = estimate_arc_revolution_background( + channel, + radius=0.5, + direction="both", + ) + + assert np.array_equal(background.data, data) + + +@pytest.mark.parametrize("radius", [0.0, -1.0, np.nan, np.inf]) +def test_invalid_radius_is_rejected(radius: float) -> None: + channel = _channel(np.ones((2, 2))) + + with pytest.raises((TypeError, ValueError)): + estimate_arc_revolution_background( + channel, + radius=radius, + ) + + +def test_vertical_matches_independent_one_dimensional_oracle() -> None: + profile = np.array([0.0, 0.3, 1.7, 0.4, 0.1, 0.0]) + data = np.column_stack((profile, profile + 1.5)) + channel = _channel( + data, + x_range=2.0, + y_range=float(profile.size), + ) + + result = estimate_arc_revolution_background( + channel, + radius=2.5, + direction="vertical", + border="nearest", + ) + + expected_first = _brute_force_below_nearest( + profile, + radius=2.5, + spacing=1.0, + ) + + assert np.allclose(result.data[:, 0], expected_first) + assert np.allclose(result.data[:, 1], expected_first + 1.5) + + +def test_both_is_horizontal_followed_by_vertical() -> None: + data = np.array( + [ + [0.0, 0.2, 0.0, 0.1, 0.0], + [0.3, 1.0, 2.5, 0.8, 0.2], + [0.0, 0.5, 4.0, 0.4, 0.0], + [0.2, 0.9, 2.0, 0.7, 0.1], + [0.0, 0.1, 0.0, 0.2, 0.0], + ] + ) + channel = _channel( + data, + x_range=5.0, + y_range=7.5, + ) + + horizontal = estimate_arc_revolution_background( + channel, + radius=2.5, + direction="horizontal", + ) + sequential = estimate_arc_revolution_background( + horizontal, + radius=2.5, + direction="vertical", + ) + combined = estimate_arc_revolution_background( + channel, + radius=2.5, + direction="both", + ) + + assert np.allclose(combined.data, sequential.data) + + +def test_positive_protrusion_is_retained_in_residual() -> None: + yy, xx = np.mgrid[0:9, 0:11] + data = 0.05 * xx + 0.03 * yy + data = data + 3.0 * np.exp(-((xx - 5) ** 2 + (yy - 4) ** 2) / 1.5) + channel = _channel( + data, + x_range=11.0, + y_range=9.0, + ) + + background = estimate_arc_revolution_background( + channel, + radius=3.0, + direction="both", + side="below", + ) + corrected = remove_arc_revolution_background( + channel, + radius=3.0, + direction="both", + side="below", + ) + + assert np.all(background.data <= data + 1e-12) + assert corrected.data[4, 5] > corrected.data[0, 0] + assert np.allclose(corrected.data + background.data, data) + + +def test_above_background_does_not_fall_below_surface() -> None: + yy, xx = np.mgrid[0:7, 0:9] + data = -2.0 * np.exp(-((xx - 4) ** 2 + (yy - 3) ** 2) / 1.5) + channel = _channel( + data, + x_range=9.0, + y_range=7.0, + ) + + background = estimate_arc_revolution_background( + channel, + radius=3.0, + side="above", + ) + corrected = remove_arc_revolution_background( + channel, + radius=3.0, + side="above", + ) + + assert np.all(background.data >= data - 1e-12) + assert np.allclose(corrected.data + background.data, data) + + +def test_anisotropic_pixel_spacing_changes_axis_response() -> None: + data = np.zeros((5, 5)) + data[2, 2] = 4.0 + channel = _channel( + data, + x_range=5.0, + y_range=10.0, + ) + + horizontal = estimate_arc_revolution_background( + channel, + radius=1.5, + direction="horizontal", + ) + vertical = estimate_arc_revolution_background( + channel, + radius=1.5, + direction="vertical", + ) + + # dx = 1 while dy = 2. The horizontal structure spans neighbours, + # whereas the vertical structure contains only its central sample. + assert horizontal.data[2, 2] < data[2, 2] + assert np.array_equal(vertical.data, data) + + +def test_lateral_range_controls_discrete_physical_structure() -> None: + data = np.array([[0.0, 0.0, 4.0, 0.0, 0.0]]) + + fine = _channel( + data, + x_range=5.0, + y_range=1.0, + ) + coarse = _channel( + data, + x_range=10.0, + y_range=1.0, + ) + + fine_background = estimate_arc_revolution_background( + fine, + radius=1.5, + direction="horizontal", + ) + coarse_background = estimate_arc_revolution_background( + coarse, + radius=1.5, + direction="horizontal", + ) + + assert fine_background.data[0, 2] < data[0, 2] + assert np.array_equal(coarse_background.data, data) + + +def test_equivalent_metres_and_nanometres_agree_physically() -> None: + data_metres = ( + np.array( + [ + [0.0, 0.2, 1.5, 0.2, 0.0], + [0.1, 0.4, 2.0, 0.3, 0.1], + [0.0, 0.2, 1.2, 0.2, 0.0], + ] + ) + * 1e-9 + ) + + channel_metres = _channel( + data_metres, + unit="m", + x_range=5e-6, + y_range=3e-6, + ) + channel_nanometres = _channel( + data_metres * 1e9, + unit="nm", + x_range=5e-6, + y_range=3e-6, + ) + + background_metres = estimate_arc_revolution_background( + channel_metres, + radius=2.5e-6, + ) + background_nanometres = estimate_arc_revolution_background( + channel_nanometres, + radius=2.5e-6, + ) + + assert np.allclose( + background_metres.data, + background_nanometres.data * 1e-9, + rtol=1e-12, + atol=1e-18, + ) + + +@pytest.mark.parametrize("border", ["nearest", "reflect"]) +def test_supported_borders_preserve_reconstruction(border: str) -> None: + data = np.array( + [ + [0.0, 0.5, 2.0, 0.2], + [0.1, 1.0, 3.0, 0.4], + [0.0, 0.3, 1.5, 0.1], + ] + ) + channel = _channel(data) + + background = estimate_arc_revolution_background( + channel, + radius=2.0, + border=border, + ) + corrected = remove_arc_revolution_background( + channel, + radius=2.0, + border=border, + ) + + assert np.all(np.isfinite(background.data)) + assert np.allclose(corrected.data + background.data, data) + + +@pytest.mark.parametrize( + "data", + [ + np.array([[7.0]]), + np.array([[0.0, 1.0, 3.0, 1.0, 0.0]]), + np.array([[0.0], [1.0], [3.0], [1.0], [0.0]]), + ], + ids=["one-by-one", "one-by-n", "n-by-one"], +) +def test_degenerate_dimensions_are_defined(data: np.ndarray) -> None: + channel = _channel(data) + + background = estimate_arc_revolution_background( + channel, + radius=2.0, + ) + corrected = remove_arc_revolution_background( + channel, + radius=2.0, + ) + + assert background.shape == data.shape + assert corrected.shape == data.shape + assert np.all(np.isfinite(background.data)) + assert np.allclose(corrected.data + background.data, data) + + if data.shape == (1, 1): + assert np.array_equal(background.data, data) + assert np.array_equal(corrected.data, np.zeros_like(data)) + + +def test_radius_larger_than_domain_is_supported() -> None: + data = np.array( + [ + [0.0, 1.0, 3.0, 1.0], + [0.2, 1.5, 4.0, 0.5], + [0.0, 0.8, 2.0, 0.0], + ] + ) + channel = _channel( + data, + x_range=4.0, + y_range=3.0, + ) + + background = estimate_arc_revolution_background( + channel, + radius=100.0, + border="reflect", + ) + corrected = remove_arc_revolution_background( + channel, + radius=100.0, + border="reflect", + ) + + assert background.shape == data.shape + assert np.all(np.isfinite(background.data)) + assert np.allclose(corrected.data + background.data, data) + + +@pytest.mark.parametrize( + ("parameter", "value"), + [ + ("direction", "diagonal"), + ("side", "inside"), + ("border", "constant"), + ], +) +def test_invalid_public_options_are_rejected( + parameter: str, + value: str, +) -> None: + channel = _channel(np.ones((2, 3))) + kwargs = {parameter: value} + + with pytest.raises(ValueError): + estimate_arc_revolution_background( + channel, + radius=1.0, + **kwargs, + ) + + +@pytest.mark.parametrize( + "data", + [ + np.array([[0.0, np.nan], [1.0, 2.0]]), + np.array([[0.0, np.inf], [1.0, 2.0]]), + ], + ids=["nan", "infinity"], +) +def test_nonfinite_data_are_rejected(data: np.ndarray) -> None: + channel = _channel(data) + + with pytest.raises( + ValueError, + match="requires finite data", + ): + estimate_arc_revolution_background( + channel, + radius=1.0, + ) + + +def test_non_geometric_z_unit_is_rejected() -> None: + channel = _channel( + np.ones((3, 4)), + unit="V", + ) + + with pytest.raises( + ValueError, + match="unsupported geometric length unit", + ): + estimate_arc_revolution_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize( + "radius", + [ + True, + "1.0", + [1.0], + 1.0 + 2.0j, + ], + ids=["boolean", "string", "array", "complex"], +) +def test_non_real_scalar_radius_is_rejected(radius: object) -> None: + channel = _channel(np.ones((2, 2))) + + with pytest.raises(TypeError): + estimate_arc_revolution_background( + channel, + radius=radius, + ) + + +def test_input_is_not_mutated_and_context_is_preserved() -> None: + data = np.array( + [ + [0.0, 0.5, 2.0], + [0.2, 1.0, 3.0], + ] + ) + channel = _channel( + data, + unit="nm", + x_range=3e-6, + y_range=2e-6, + ) + original_data = channel.data.copy() + original_metadata = dict(channel.metadata) + + background = estimate_arc_revolution_background( + channel, + radius=2e-6, + ) + corrected = remove_arc_revolution_background( + channel, + radius=2e-6, + ) + + assert np.array_equal(channel.data, original_data) + assert channel.metadata == original_metadata + + for result in (background, corrected): + assert result is not channel + assert result.data is not channel.data + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +@pytest.mark.parametrize( + ("data", "error_type", "message"), + [ + ( + np.array([0.0, 1.0, 2.0]), + ValueError, + "requires a 2D channel", + ), + ( + np.empty((0, 3)), + ValueError, + "requires non-empty data", + ), + ( + np.array([["a", "b"], ["c", "d"]]), + TypeError, + "requires real numeric data", + ), + ( + np.array( + [ + [1.0 + 0.0j, 2.0 + 1.0j], + [3.0 + 0.0j, 4.0 + 0.0j], + ] + ), + TypeError, + "requires real numeric data", + ), + ], + ids=[ + "one-dimensional", + "empty", + "non-numeric", + "complex", + ], +) +def test_invalid_channel_data_are_rejected( + data: np.ndarray, + error_type: type[Exception], + message: str, +) -> None: + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="m", + x_range=2.0, + y_range=2.0, + ) + + with pytest.raises(error_type, match=message): + estimate_arc_revolution_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize( + ("x_range", "y_range", "message"), + [ + (0.0, 2.0, "positive lateral pixel spacing"), + (-1.0, 2.0, "positive lateral pixel spacing"), + (np.inf, 2.0, "finite lateral pixel spacing"), + (2.0, 0.0, "positive lateral pixel spacing"), + (2.0, -1.0, "positive lateral pixel spacing"), + (2.0, np.inf, "finite lateral pixel spacing"), + ], +) +def test_invalid_lateral_geometry_is_rejected( + x_range: float, + y_range: float, + message: str, +) -> None: + channel = _channel( + np.ones((2, 3)), + x_range=x_range, + y_range=y_range, + ) + + with pytest.raises(ValueError, match=message): + estimate_arc_revolution_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize( + "direction", + ["horizontal", "vertical", "both"], +) +@pytest.mark.parametrize( + "side", + ["below", "above"], +) +def test_reconstruction_identity_for_every_mode( + direction: str, + side: str, +) -> None: + data = np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.4, 3.0, 0.4], + [0.1, 0.5, 1.8, 0.2], + ] + ) + channel = _channel( + data, + x_range=4.0, + y_range=3.0, + ) + + background = estimate_arc_revolution_background( + channel, + radius=2.0, + direction=direction, + side=side, + ) + corrected = remove_arc_revolution_background( + channel, + radius=2.0, + direction=direction, + side=side, + ) + + assert np.allclose( + corrected.data + background.data, + data, + rtol=1e-13, + atol=1e-13, + ) + + +def test_functions_are_available_from_public_analysis_api() -> None: + from spmkit.core.analysis import ( + estimate_arc_revolution_background as public_estimate, + ) + from spmkit.core.analysis import ( + remove_arc_revolution_background as public_remove, + ) + + assert public_estimate is estimate_arc_revolution_background + assert public_remove is remove_arc_revolution_background + + +def test_arc_structure_preserves_small_sagitta_for_large_radius() -> None: + structure = _arc_structure( + radius=1e12, + spacing=1.0, + sample_count=3, + ) + + expected = np.array( + [ + -2e-12, + -5e-13, + 0.0, + -5e-13, + -2e-12, + ] + ) + + assert structure.shape == (5,) + assert np.allclose( + structure, + expected, + rtol=1e-12, + atol=0.0, + ) + + +def test_horizontal_reflect_matches_independent_oracle() -> None: + profile = np.array([1.5, 0.1, 0.4, 2.5, 0.3, 1.2]) + channel = _channel( + profile[np.newaxis, :], + x_range=float(profile.size), + y_range=1.0, + ) + + result = estimate_arc_revolution_background( + channel, + radius=2.5, + direction="horizontal", + border="reflect", + ) + expected = _brute_force_below_reflect( + profile, + radius=2.5, + spacing=1.0, + ) + + assert np.allclose( + result.data[0], + expected, + rtol=1e-13, + atol=1e-13, + ) + + +@pytest.mark.parametrize( + ("parameter", "value"), + [ + ("direction", None), + ("side", 1), + ("border", ["nearest"]), + ], +) +def test_non_string_public_options_are_rejected( + parameter: str, + value: object, +) -> None: + channel = _channel(np.ones((2, 3))) + kwargs = {parameter: value} + + with pytest.raises( + TypeError, + match=rf"requires {parameter} to be a string", + ): + estimate_arc_revolution_background( + channel, + radius=1.0, + **kwargs, + ) diff --git a/tests/core/test_background_result.py b/tests/core/test_background_result.py new file mode 100644 index 0000000..d09970e --- /dev/null +++ b/tests/core/test_background_result.py @@ -0,0 +1,260 @@ +"""Structured background-result contracts.""" + +from __future__ import annotations + +import json +from dataclasses import FrozenInstanceError + +import numpy as np +import pytest + +from spmkit.core.analysis import ( + BackgroundResult, + analyze_arc_revolution_background, + analyze_median_background, + analyze_polynomial_background, + analyze_rolling_ball_background, + analyze_sphere_revolution_background, +) +from spmkit.core.analysis.background import ( + estimate_arc_revolution_background, + estimate_median_background, + estimate_sphere_revolution_background, + remove_arc_revolution_background, + remove_median_background, + remove_sphere_revolution_background, +) +from spmkit.core.models import SPMChannel + + +def _channel() -> SPMChannel: + data = ( + np.array( + [ + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0], + [1.0, 2.0, 5.0, 4.0, 5.0, 6.0], + [2.0, 4.0, 8.0, 7.0, 6.0, 7.0], + [3.0, 4.0, 7.0, 6.0, 5.0, 8.0], + [4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + ], + dtype=float, + ) + * 1e-9 + ) + + return SPMChannel( + name="Topography", + data=data, + unit="m", + x_range=6e-6, + y_range=5e-6, + direction="forward", + group="Scan", + metadata={"source": "synthetic"}, + ) + + +def test_arc_result_matches_existing_public_functions() -> None: + channel = _channel() + radius = 5e-6 + + result = analyze_arc_revolution_background( + channel, + radius, + direction="both", + side="below", + border="nearest", + ) + + expected_background = estimate_arc_revolution_background( + channel, + radius, + direction="both", + side="below", + border="nearest", + ) + expected_corrected = remove_arc_revolution_background( + channel, + radius, + direction="both", + side="below", + border="nearest", + ) + + assert isinstance(result, BackgroundResult) + assert result.method == "arc_revolution" + assert result.parameters == { + "radius": radius, + "direction": "both", + "side": "below", + "border": "nearest", + } + assert np.array_equal( + result.background.data, + expected_background.data, + ) + assert np.array_equal( + result.corrected.data, + expected_corrected.data, + ) + + +def test_sphere_result_matches_existing_public_functions() -> None: + channel = _channel() + radius = 5e-6 + + result = analyze_sphere_revolution_background( + channel, + radius, + side="below", + border="nearest", + ) + + expected_background = estimate_sphere_revolution_background( + channel, + radius, + side="below", + border="nearest", + ) + expected_corrected = remove_sphere_revolution_background( + channel, + radius, + side="below", + border="nearest", + ) + + assert result.method == "sphere_revolution" + assert result.parameters == { + "radius": radius, + "side": "below", + "border": "nearest", + } + assert np.array_equal( + result.background.data, + expected_background.data, + ) + assert np.array_equal( + result.corrected.data, + expected_corrected.data, + ) + + +def test_median_result_matches_existing_public_functions() -> None: + channel = _channel() + + result = analyze_median_background( + channel, + radius_pixels=2, + ) + + expected_background = estimate_median_background( + channel, + radius_pixels=2, + ) + expected_corrected = remove_median_background( + channel, + radius_pixels=2, + ) + + assert result.method == "median" + assert result.parameters == { + "radius_pixels": 2, + "border": "nearest", + } + assert np.array_equal( + result.background.data, + expected_background.data, + ) + assert np.array_equal( + result.corrected.data, + expected_corrected.data, + ) + + +@pytest.mark.parametrize( + "analyzer", + [ + lambda channel: analyze_arc_revolution_background( + channel, + 5e-6, + ), + lambda channel: analyze_sphere_revolution_background( + channel, + 5e-6, + ), + lambda channel: analyze_rolling_ball_background( + channel, + 5e-6, + ), + lambda channel: analyze_polynomial_background( + channel, + degree_mode="total", + degree=2, + ), + lambda channel: analyze_median_background( + channel, + 2, + ), + ], +) +def test_result_channels_preserve_context(analyzer) -> None: + channel = _channel() + result = analyzer(channel) + + for output in (result.background, result.corrected): + assert output.name == channel.name + assert output.unit == channel.unit + assert output.x_range == channel.x_range + assert output.y_range == channel.y_range + assert output.direction == channel.direction + assert output.group == channel.group + assert output.metadata == channel.metadata + assert output.metadata is not channel.metadata + + +def test_background_result_is_frozen() -> None: + result = analyze_median_background( + _channel(), + radius_pixels=1, + ) + + with pytest.raises(FrozenInstanceError): + result.method = "arc_revolution" # type: ignore[misc] + + +def test_to_dict_is_json_serializable() -> None: + result = analyze_median_background( + _channel(), + radius_pixels=1, + ) + + payload = result.to_dict() + + assert payload["method"] == "median" + assert payload["parameters"] == { + "radius_pixels": 1, + "border": "nearest", + } + + background = payload["background"] + corrected = payload["corrected"] + + assert isinstance(background, dict) + assert isinstance(corrected, dict) + assert background["shape"] == [5, 6] + assert corrected["shape"] == [5, 6] + assert isinstance(background["data"], list) + assert isinstance(corrected["data"], list) + + json.dumps(payload) + + +def test_structured_background_api_is_public() -> None: + from spmkit.core import analysis + + assert analysis.BackgroundResult is BackgroundResult + assert analysis.analyze_arc_revolution_background is analyze_arc_revolution_background + assert analysis.analyze_sphere_revolution_background is analyze_sphere_revolution_background + assert analysis.analyze_rolling_ball_background is analyze_rolling_ball_background + assert analysis.analyze_polynomial_background is analyze_polynomial_background + assert analysis.analyze_median_background is analyze_median_background diff --git a/tests/core/test_bruker_spm.py b/tests/core/test_bruker_spm.py index 6501fef..893ae63 100644 --- a/tests/core/test_bruker_spm.py +++ b/tests/core/test_bruker_spm.py @@ -188,6 +188,13 @@ def test_bruker_spm_uses_versioned_32bit_scale(tmp_path) -> None: # type: ignor def test_bruker_spm_does_not_import_gui(tmp_path) -> None: # type: ignore[no-untyped-def] p = tmp_path / "nogui.spm" _write_spm(p, np.ones((2, 2), dtype=np.int16), hard=1.0, sens=1.0, scan_um=1.0) + before_modules = set(sys.modules) + with pytest.warns(UserWarning): load_bruker_spm(p) - assert not any(name.startswith(("PyQt", "pyqtgraph")) for name in sys.modules) + + imported_modules = set(sys.modules) - before_modules + unexpected_gui_modules = sorted( + name for name in imported_modules if name.startswith(("PyQt", "pyqtgraph")) + ) + assert unexpected_gui_modules == [] diff --git a/tests/core/test_flatten_base_core.py b/tests/core/test_flatten_base_core.py new file mode 100644 index 0000000..3761dea --- /dev/null +++ b/tests/core/test_flatten_base_core.py @@ -0,0 +1,2251 @@ +from __future__ import annotations + +import numpy as np +import pytest + +import spmkit.core.analysis._flatten_base as flatten_base_core +from spmkit.core.analysis._flatten_base import ( + BasePeakFit, + BasePeakWindow, + HeightDistribution, + _fit_base_peak, + _gwyddion_height_distribution, + _select_base_peak_window, +) + + +def test_height_distribution_matches_nonconstant_gwyddion_contract() -> None: + data = np.array( + [ + [0.0, 0.2, 0.8, 1.5], + [2.1, 3.1, 3.7, 4.0], + ], + dtype=float, + ) + original = data.copy() + + result = _gwyddion_height_distribution(data) + + expected_counts = np.array([2, 1, 1, 1, 0, 1, 2]) + expected_width = 4.0 / 7.0 + expected_centers = (np.arange(7, dtype=float) + 0.5) * expected_width + expected_density = expected_counts * 7.0 / (4.0 * data.size) + + np.testing.assert_allclose(result.centers, expected_centers) + np.testing.assert_allclose(result.density, expected_density) + assert result.bin_width == pytest.approx(expected_width) + assert result.minimum == 0.0 + assert result.maximum == 4.0 + assert result.sample_count == data.size + assert np.sum(result.density) * result.bin_width == pytest.approx(1.0) + + np.testing.assert_array_equal(data, original) + assert not result.centers.flags.writeable + assert not result.density.flags.writeable + + +def test_height_distribution_preserves_gwyddion_constant_field_convention() -> None: + data = np.full((3, 3), 5.0) + + result = _gwyddion_height_distribution(data) + + expected_width = 5.0 / 7.0 + expected_centers = (np.arange(7, dtype=float) + 0.5) * expected_width + expected_density = np.zeros(7) + expected_density[0] = 7.0 / 5.0 + + np.testing.assert_allclose(result.centers, expected_centers) + np.testing.assert_allclose(result.density, expected_density) + assert result.bin_width == pytest.approx(expected_width) + assert result.minimum == 5.0 + assert result.maximum == 5.0 + assert result.sample_count == 9 + assert np.sum(result.density) * result.bin_width == pytest.approx(1.0) + + +def test_base_peak_window_matches_gwyddion_selection_rules() -> None: + centers = np.arange(9, dtype=float) + 0.5 + density = np.array( + [0.1, 0.2, 1.0, 1.0, 0.29, 0.1, 0.0, 0.0, 0.0], + dtype=float, + ) + distribution = HeightDistribution( + centers=centers, + density=density, + bin_width=1.0, + minimum=0.0, + maximum=9.0, + sample_count=100, + ) + + result = _select_base_peak_window(distribution) + + assert result.peak_index == 2 + assert result.start_index == 0 + assert result.stop_index == 7 + np.testing.assert_array_equal(result.centers, centers[:7]) + np.testing.assert_array_equal(result.density, density[:7]) + assert result.initial_mean == pytest.approx(2.5) + assert result.initial_offset == 0.0 + assert result.initial_amplitude == pytest.approx(1.0) + assert result.initial_width == pytest.approx(2.1) + assert not result.centers.flags.writeable + assert not result.density.flags.writeable + + +def test_base_peak_window_rejects_fewer_than_seven_bins() -> None: + distribution = HeightDistribution( + centers=np.arange(6, dtype=float) + 0.5, + density=np.ones(6), + bin_width=1.0, + minimum=0.0, + maximum=6.0, + sample_count=8, + ) + + with pytest.raises( + ValueError, + match="base peak estimation requires at least seven histogram bins", + ): + _select_base_peak_window(distribution) + + +def test_base_peak_fit_recovers_exact_gaussian() -> None: + centers = np.linspace(-3.0, 3.0, 17) + expected_mean = 0.35 + expected_offset = 0.18 + expected_amplitude = 2.4 + expected_width = 1.1 + + density = expected_offset + expected_amplitude * np.exp( + -np.square((centers - expected_mean) / expected_width) + ) + peak_index = int(np.argmax(density)) + + window = BasePeakWindow( + centers=centers, + density=density, + peak_index=peak_index, + start_index=0, + stop_index=centers.size, + initial_mean=float(centers[peak_index]), + initial_offset=0.0, + initial_amplitude=float(density[peak_index]), + initial_width=1.8, + ) + + result = _fit_base_peak(window) + + assert result.solver_success + assert result.covariance_available + assert result.success + assert result.mean == pytest.approx(expected_mean, abs=1e-8) + assert result.offset == pytest.approx(expected_offset, abs=1e-8) + assert result.amplitude == pytest.approx(expected_amplitude, abs=1e-8) + assert result.width == pytest.approx(expected_width, abs=1e-8) + assert result.rms == pytest.approx(expected_width / np.sqrt(2.0), abs=1e-8) + assert result.residual_norm < 1e-9 + assert result.evaluations > 0 + assert result.jacobian_rank == 4 + assert np.isfinite(result.condition_estimate) + + +def test_base_peak_fit_marks_constant_density_as_unidentifiable() -> None: + centers = np.linspace(-3.0, 3.0, 7) + density = np.ones_like(centers) + + window = BasePeakWindow( + centers=centers, + density=density, + peak_index=0, + start_index=0, + stop_index=centers.size, + initial_mean=float(centers[0]), + initial_offset=0.0, + initial_amplitude=1.0, + initial_width=2.1, + ) + + result = _fit_base_peak(window) + + assert not result.covariance_available + assert not result.success + assert result.jacobian_rank < 4 + + +def test_base_peak_fit_matches_gwyddion_271_reference() -> None: + """Cross-check a perturbed Gaussian against a direct Gwyddion 2.71 probe.""" + centers = -3.0 + 0.375 * np.arange(17, dtype=float) + density = ( + 0.18 + 2.4 * np.exp(-np.square((centers - 0.35) / 1.1)) + 0.015 * np.sin(1.7 * centers) + ) + peak_index = int(np.argmax(density)) + + window = BasePeakWindow( + centers=centers, + density=density, + peak_index=peak_index, + start_index=0, + stop_index=centers.size, + initial_mean=float(centers[peak_index]), + initial_offset=0.0, + initial_amplitude=float(density[peak_index]), + initial_width=1.8, + ) + + result = _fit_base_peak(window) + + # Frozen from a direct libgwyddion 2.71 C reference probe. + assert result.success + assert result.mean == pytest.approx( + 0.35624774459072917, + abs=5e-10, + ) + assert result.rms == pytest.approx( + 0.77337116119210381, + abs=5e-10, + ) + assert result.offset == pytest.approx( + 0.18099383510469755, + abs=5e-10, + ) + assert result.amplitude == pytest.approx( + 2.4105144489572057, + abs=5e-10, + ) + assert result.width == pytest.approx( + 1.0937119849061023, + abs=5e-10, + ) + + +def test_estimate_base_peak_composes_verified_stages( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.arange(64, dtype=float).reshape(8, 8) + original = data.copy() + + distribution = HeightDistribution( + centers=np.arange(7, dtype=float) + 0.5, + density=np.array([0.1, 0.3, 1.0, 0.4, 0.2, 0.1, 0.0]), + bin_width=1.0, + minimum=0.0, + maximum=7.0, + sample_count=data.size, + ) + window = BasePeakWindow( + centers=distribution.centers, + density=distribution.density, + peak_index=2, + start_index=0, + stop_index=7, + initial_mean=2.5, + initial_offset=0.0, + initial_amplitude=1.0, + initial_width=2.1, + ) + fit = BasePeakFit( + mean=2.45, + rms=0.4, + offset=0.01, + amplitude=0.99, + width=0.4 * np.sqrt(2.0), + residual_norm=1e-8, + solver_success=True, + covariance_available=True, + evaluations=12, + jacobian_rank=4, + condition_estimate=8.0, + ) + + calls: list[str] = [] + + def fake_distribution(received: np.ndarray) -> HeightDistribution: + assert received is data + calls.append("distribution") + return distribution + + def fake_window(received: HeightDistribution) -> BasePeakWindow: + assert received is distribution + calls.append("window") + return window + + def fake_fit(received: BasePeakWindow) -> BasePeakFit: + assert received is window + calls.append("fit") + return fit + + monkeypatch.setattr( + flatten_base_core, + "_gwyddion_height_distribution", + fake_distribution, + ) + monkeypatch.setattr( + flatten_base_core, + "_select_base_peak_window", + fake_window, + ) + monkeypatch.setattr( + flatten_base_core, + "_fit_base_peak", + fake_fit, + ) + + result = flatten_base_core._estimate_base_peak(data) + + assert calls == ["distribution", "window", "fit"] + assert result.distribution is distribution + assert result.window is window + assert result.fit is fit + assert result.success + assert result.mean == fit.mean + assert result.rms == fit.rms + np.testing.assert_array_equal(data, original) + + +def test_gwyddion_facet_plane_recovers_exact_physical_tilt() -> None: + rows = 4 + columns = 5 + pixel_size_x = 2.0 + pixel_size_y = 0.5 + expected_physical_x = 0.3 + expected_physical_y = -0.2 + + x = np.arange(columns, dtype=float) * pixel_size_x + y = np.arange(rows, dtype=float) * pixel_size_y + xx, yy = np.meshgrid(x, y) + + data = 7.0 + expected_physical_x * xx + expected_physical_y * yy + original = data.copy() + + result = flatten_base_core._estimate_gwyddion_facet_plane( + data, + pixel_size_x=pixel_size_x, + pixel_size_y=pixel_size_y, + ) + + expected_x_coefficient = expected_physical_x * pixel_size_x + expected_y_coefficient = expected_physical_y * pixel_size_y + expected_scale_squared = (expected_physical_x**2 + expected_physical_y**2) / 20.0 + expected_intercept = -0.5 * (expected_x_coefficient * columns + expected_y_coefficient * rows) + expected_cells = (rows - 1) * (columns - 1) + + assert not result.degenerate + assert result.cell_count == expected_cells + assert result.physical_slope_x == pytest.approx(expected_physical_x) + assert result.physical_slope_y == pytest.approx(expected_physical_y) + assert result.x_coefficient == pytest.approx(expected_x_coefficient) + assert result.y_coefficient == pytest.approx(expected_y_coefficient) + assert result.intercept == pytest.approx(expected_intercept) + assert result.slope_scale_squared == pytest.approx(expected_scale_squared) + assert result.weight_sum == pytest.approx(expected_cells * np.exp(-20.0)) + + np.testing.assert_array_equal(data, original) + + +def test_gwyddion_facet_plane_handles_flat_field_without_nan() -> None: + data = np.full((4, 5), 3.2) + + result = flatten_base_core._estimate_gwyddion_facet_plane( + data, + pixel_size_x=0.4, + pixel_size_y=0.7, + ) + + assert result.degenerate + assert result.cell_count == 12 + assert result.intercept == 0.0 + assert result.x_coefficient == 0.0 + assert result.y_coefficient == 0.0 + assert result.physical_slope_x == 0.0 + assert result.physical_slope_y == 0.0 + assert result.slope_scale_squared == 0.0 + assert result.weight_sum == pytest.approx(12.0) + + +def test_gwyddion_facet_plane_matches_gwyddion_271_reference() -> None: + """Cross-check the facet estimator against direct libgwyddion 2.71.""" + rows = 6 + columns = 7 + pixel_size_x = 2.0 + pixel_size_y = 0.5 + + data = np.empty((rows, columns), dtype=float) + + for row in range(rows): + for column in range(columns): + x = column * pixel_size_x + y = row * pixel_size_y + value = 7.0 + 0.3 * x - 0.2 * y + 0.04 * np.sin(0.7 * column + 0.3 * row) + + if row == 1 and column == 2: + value += 4.0 + if row == 3 and column == 5: + value += 2.5 + + data[row, column] = value + + result = flatten_base_core._estimate_gwyddion_facet_plane( + data, + pixel_size_x=pixel_size_x, + pixel_size_y=pixel_size_y, + ) + + # Frozen from a direct gwy_data_field_fit_facet_plane() probe. + assert not result.degenerate + assert result.intercept == pytest.approx( + -1.7454698947303242, + abs=5e-13, + ) + assert result.x_coefficient == pytest.approx( + 0.58850087792355377, + abs=5e-13, + ) + assert result.y_coefficient == pytest.approx( + -0.10476105933403794, + abs=5e-13, + ) + assert result.physical_slope_x == pytest.approx( + 0.29425043896177688, + abs=5e-13, + ) + assert result.physical_slope_y == pytest.approx( + -0.20952211866807588, + abs=5e-13, + ) + + assert result.cell_count == 30 + assert result.slope_scale_squared == pytest.approx( + 0.16422579481110028, + abs=5e-14, + ) + assert result.weight_sum == pytest.approx( + 9.9247467971063745, + abs=5e-13, + ) + + +def test_flatten_base_facet_stage_runs_exactly_five_iterations( + monkeypatch: pytest.MonkeyPatch, +) -> None: + rows = 4 + columns = 5 + data = np.arange(rows * columns, dtype=float).reshape(rows, columns) + original = data.copy() + + plane = flatten_base_core.FacetPlaneEstimate( + intercept=0.4, + x_coefficient=0.2, + y_coefficient=-0.1, + physical_slope_x=0.1, + physical_slope_y=-0.2, + slope_scale_squared=0.03, + cell_count=(rows - 1) * (columns - 1), + weight_sum=6.0, + degenerate=False, + ) + + class SuccessfulPeak: + success = True + + peak = SuccessfulPeak() + facet_inputs: list[np.ndarray] = [] + peak_inputs: list[np.ndarray] = [] + + def fake_facet( + received: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, + ) -> flatten_base_core.FacetPlaneEstimate: + assert pixel_size_x == 2.0 + assert pixel_size_y == 0.5 + facet_inputs.append(received.copy()) + return plane + + def fake_peak(received: np.ndarray) -> SuccessfulPeak: + peak_inputs.append(received.copy()) + return peak + + monkeypatch.setattr( + flatten_base_core, + "_estimate_gwyddion_facet_plane", + fake_facet, + ) + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + fake_peak, + ) + + result = flatten_base_core._run_flatten_base_facet_stage( + data, + pixel_size_x=2.0, + pixel_size_y=0.5, + ) + + column_indices = np.arange(columns, dtype=float) + row_indices = np.arange(rows, dtype=float) + xx, yy = np.meshgrid(column_indices, row_indices) + single_plane = plane.intercept + plane.x_coefficient * xx + plane.y_coefficient * yy + + np.testing.assert_allclose( + result.background, + 5.0 * single_plane, + ) + np.testing.assert_allclose( + result.corrected, + data - 5.0 * single_plane, + ) + + assert len(facet_inputs) == 5 + assert len(peak_inputs) == 6 + assert result.initial_peak is peak + assert result.completed_iterations == 5 + assert len(result.iterations) == 5 + assert result.termination == "maximum_iterations" + + for index, iteration in enumerate(result.iterations): + assert iteration.index == index + assert iteration.plane is plane + assert iteration.peak is peak + + np.testing.assert_array_equal(data, original) + assert not result.corrected.flags.writeable + assert not result.background.flags.writeable + + +def test_flatten_base_facet_stage_stops_before_degenerate_plane( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.arange(20, dtype=float).reshape(4, 5) + original = data.copy() + + class SuccessfulPeak: + success = True + + peak = SuccessfulPeak() + degenerate_plane = flatten_base_core.FacetPlaneEstimate( + intercept=0.0, + x_coefficient=0.0, + y_coefficient=0.0, + physical_slope_x=0.0, + physical_slope_y=0.0, + slope_scale_squared=0.0, + cell_count=12, + weight_sum=12.0, + degenerate=True, + ) + + peak_calls: list[np.ndarray] = [] + facet_calls: list[np.ndarray] = [] + + def fake_peak(received: np.ndarray) -> SuccessfulPeak: + peak_calls.append(received.copy()) + return peak + + def fake_facet( + received: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, + ) -> flatten_base_core.FacetPlaneEstimate: + assert pixel_size_x == 1.0 + assert pixel_size_y == 1.0 + facet_calls.append(received.copy()) + return degenerate_plane + + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + fake_peak, + ) + monkeypatch.setattr( + flatten_base_core, + "_estimate_gwyddion_facet_plane", + fake_facet, + ) + + result = flatten_base_core._run_flatten_base_facet_stage( + data, + pixel_size_x=1.0, + pixel_size_y=1.0, + ) + + assert result.initial_peak is peak + assert result.termination == "degenerate_plane" + assert result.completed_iterations == 0 + assert result.iterations == () + assert len(peak_calls) == 1 + assert len(facet_calls) == 1 + + np.testing.assert_array_equal(result.corrected, data) + np.testing.assert_array_equal( + result.background, + np.zeros_like(data), + ) + np.testing.assert_array_equal(data, original) + assert not result.corrected.flags.writeable + assert not result.background.flags.writeable + + +def test_flatten_base_facet_stage_keeps_correction_before_peak_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + rows = 4 + columns = 5 + data = np.arange(rows * columns, dtype=float).reshape(rows, columns) + + class Peak: + def __init__(self, success: bool) -> None: + self.success = success + + initial_peak = Peak(success=True) + failed_peak = Peak(success=False) + peak_results = [initial_peak, failed_peak] + + plane = flatten_base_core.FacetPlaneEstimate( + intercept=0.3, + x_coefficient=0.15, + y_coefficient=-0.05, + physical_slope_x=0.075, + physical_slope_y=-0.1, + slope_scale_squared=0.02, + cell_count=12, + weight_sum=7.0, + degenerate=False, + ) + + facet_calls = 0 + + def fake_peak(received: np.ndarray) -> Peak: + del received + return peak_results.pop(0) + + def fake_facet( + received: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, + ) -> flatten_base_core.FacetPlaneEstimate: + nonlocal facet_calls + del received, pixel_size_x, pixel_size_y + facet_calls += 1 + return plane + + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + fake_peak, + ) + monkeypatch.setattr( + flatten_base_core, + "_estimate_gwyddion_facet_plane", + fake_facet, + ) + + result = flatten_base_core._run_flatten_base_facet_stage( + data, + pixel_size_x=2.0, + pixel_size_y=0.5, + ) + + xx, yy = np.meshgrid( + np.arange(columns, dtype=float), + np.arange(rows, dtype=float), + ) + expected_plane = plane.intercept + plane.x_coefficient * xx + plane.y_coefficient * yy + + assert facet_calls == 1 + assert peak_results == [] + assert result.initial_peak is initial_peak + assert result.termination == "peak_failure" + assert result.completed_iterations == 1 + assert result.iterations[0].index == 0 + assert result.iterations[0].plane is plane + assert result.iterations[0].peak is failed_peak + + np.testing.assert_allclose(result.background, expected_plane) + np.testing.assert_allclose(result.corrected, data - expected_plane) + + +def test_grow_mask_conn4_forms_inclusive_city_block_diamond() -> None: + mask = np.zeros((5, 5), dtype=bool) + mask[2, 2] = True + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=2, + ) + + expected = np.array( + [ + [False, False, True, False, False], + [False, True, True, True, False], + [True, True, True, True, True], + [False, True, True, True, False], + [False, False, True, False, False], + ], + dtype=bool, + ) + + np.testing.assert_array_equal(observed, expected) + + +def test_grow_mask_conn4_matches_gwyddion_corner_handling() -> None: + mask = np.zeros((5, 5), dtype=bool) + mask[0, 0] = True + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=2, + ) + + expected = np.array( + [ + [True, True, True, True, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, True, True, True, True], + ], + dtype=bool, + ) + + np.testing.assert_array_equal(observed, expected) + assert np.count_nonzero(observed) == 16 + + +def test_grow_mask_conn4_matches_gwyddion_interior_merge_reference() -> None: + mask = np.zeros((5, 5), dtype=bool) + mask[2, 1] = True + mask[2, 3] = True + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=1, + ) + + expected = np.array( + [ + [False, False, False, False, False], + [False, True, False, True, False], + [False, True, True, True, False], + [False, True, False, True, False], + [False, False, False, False, False], + ], + dtype=bool, + ) + + np.testing.assert_array_equal(observed, expected) + assert np.count_nonzero(observed) == 7 + + +def test_grow_mask_conn4_zero_radius_returns_independent_copy() -> None: + mask = np.zeros((4, 5), dtype=bool) + mask[1, 3] = True + original = mask.copy() + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=0, + ) + + np.testing.assert_array_equal(observed, original) + np.testing.assert_array_equal(mask, original) + assert not np.shares_memory(observed, mask) + + observed[0, 0] = True + assert not mask[0, 0] + + +def test_grow_mask_conn4_matches_gwyddion_empty_mask_handling() -> None: + mask = np.zeros((4, 5), dtype=bool) + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=3, + ) + + expected = np.array( + [ + [True, True, True, True, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, True, True, True, True], + ], + dtype=bool, + ) + + np.testing.assert_array_equal(observed, expected) + assert np.count_nonzero(observed) == 14 + assert not np.shares_memory(observed, mask) + + +def test_grow_mask_conn4_does_not_mutate_seed_mask() -> None: + mask = np.zeros((5, 5), dtype=bool) + mask[2, 2] = True + original = mask.copy() + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=2, + ) + + np.testing.assert_array_equal(mask, original) + assert np.count_nonzero(observed) == 13 + assert not np.shares_memory(observed, mask) + + +def test_flatten_base_mask_uses_strict_threshold_and_degree_radius( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.array( + [ + [6.0, 7.0, 8.0, 2.0], + [9.0, 1.0, 7.0, 10.0], + [0.0, 5.0, 3.0, 7.0], + ], + dtype=float, + ) + original = data.copy() + + class Peak: + success = True + mean = 1.0 + rms = 2.0 + + captured: dict[str, object] = {} + + def fake_grow( + mask: np.ndarray, + *, + radius: int, + ) -> np.ndarray: + captured["mask"] = mask.copy() + captured["radius"] = radius + + grown = mask.copy() + grown[0, 0] = True + return grown + + monkeypatch.setattr( + flatten_base_core, + "_grow_mask_conn4", + fake_grow, + ) + + result = flatten_base_core._build_flatten_base_mask( + data, + peak=Peak(), + degree=2, + ) + + expected_raw = data > 7.0 + expected_grown = expected_raw.copy() + expected_grown[0, 0] = True + + assert result.degree == 2 + assert result.threshold == 7.0 + assert result.growth_radius == 2 + assert captured["radius"] == 2 + + np.testing.assert_array_equal(captured["mask"], expected_raw) + np.testing.assert_array_equal(result.raw, expected_raw) + np.testing.assert_array_equal(result.grown, expected_grown) + + assert result.raw_count == 3 + assert result.grown_count == 4 + assert not result.raw.flags.writeable + assert not result.grown.flags.writeable + + np.testing.assert_array_equal(data, original) + + +def test_flatten_base_mask_integrates_threshold_and_conn4_growth() -> None: + data = np.zeros((7, 7), dtype=float) + data[3, 3] = 4.0 + data[0, 0] = 3.0 + + class Peak: + success = True + mean = 0.0 + rms = 1.0 + + result = flatten_base_core._build_flatten_base_mask( + data, + peak=Peak(), + degree=5, + ) + + expected_raw = np.zeros((7, 7), dtype=bool) + expected_raw[3, 3] = True + + yy, xx = np.mgrid[0:7, 0:7] + expected_grown = (np.abs(yy - 3) + np.abs(xx - 3)) <= 3 + + assert result.threshold == 3.0 + assert result.growth_radius == 3 + assert result.raw_count == 1 + assert result.grown_count == 25 + + np.testing.assert_array_equal(result.raw, expected_raw) + np.testing.assert_array_equal(result.grown, expected_grown) + + +def test_flatten_base_polynomial_iteration_composes_mask_fit_and_peak( + monkeypatch: pytest.MonkeyPatch, +) -> None: + rows = 4 + columns = 5 + data = np.arange(rows * columns, dtype=float).reshape(rows, columns) + original = data.copy() + + raw_mask = np.zeros_like(data, dtype=bool) + raw_mask[1, 2] = True + + grown_mask = raw_mask.copy() + grown_mask[1, 1:4] = True + grown_mask[0, 2] = True + grown_mask[2, 2] = True + + raw_mask.setflags(write=False) + grown_mask.setflags(write=False) + + automatic_mask = flatten_base_core.FlattenBaseMask( + degree=2, + threshold=7.0, + growth_radius=2, + raw=raw_mask, + grown=grown_mask, + raw_count=1, + grown_count=5, + ) + + class InitialPeak: + success = True + mean = 1.0 + rms = 2.0 + + class UpdatedPeak: + success = True + mean = 0.2 + rms = 0.4 + + initial_peak = InitialPeak() + updated_peak = UpdatedPeak() + + expected_background = np.full_like(data, 1.25) + coefficients = np.arange(6, dtype=float) + singular_values = np.linspace(6.0, 1.0, 6) + + captured: dict[str, object] = {} + + def fake_mask( + received: np.ndarray, + *, + peak: InitialPeak, + degree: int, + ) -> flatten_base_core.FlattenBaseMask: + np.testing.assert_array_equal(received, data) + assert peak is initial_peak + assert degree == 2 + return automatic_mask + + def fake_fit( + received: np.ndarray, + *, + powers: tuple[tuple[int, int], ...], + selection: np.ndarray, + operation: str, + ) -> tuple[np.ndarray, np.ndarray, int, np.ndarray]: + np.testing.assert_array_equal(received, data) + + captured["powers"] = powers + captured["selection"] = selection.copy() + captured["operation"] = operation + + return ( + expected_background.copy(), + coefficients.copy(), + 6, + singular_values.copy(), + ) + + def fake_peak(received: np.ndarray) -> UpdatedPeak: + np.testing.assert_allclose( + received, + data - expected_background, + ) + return updated_peak + + monkeypatch.setattr( + flatten_base_core, + "_build_flatten_base_mask", + fake_mask, + ) + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + fake_peak, + ) + + import spmkit.core.analysis.leveling as leveling + + monkeypatch.setattr( + leveling, + "_fit_polynomial_surface_data", + fake_fit, + ) + + result = flatten_base_core._run_flatten_base_polynomial_iteration( + data, + peak=initial_peak, + degree=2, + ) + + expected_powers = ( + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (2, 0), + ) + + assert captured["powers"] == expected_powers + assert captured["operation"] == "Flatten Base degree 2" + np.testing.assert_array_equal( + captured["selection"], + ~grown_mask, + ) + + assert result.degree == 2 + assert result.powers == expected_powers + assert result.mask is automatic_mask + assert result.selected_count == data.size - 5 + assert result.rank == 6 + assert result.peak is updated_peak + + np.testing.assert_array_equal(result.coefficients, coefficients) + np.testing.assert_array_equal( + result.singular_values, + singular_values, + ) + np.testing.assert_allclose( + result.background, + expected_background, + ) + np.testing.assert_allclose( + result.corrected, + data - expected_background, + ) + + assert not result.coefficients.flags.writeable + assert not result.singular_values.flags.writeable + assert not result.background.flags.writeable + assert not result.corrected.flags.writeable + + np.testing.assert_array_equal(data, original) + + +def test_flatten_base_polynomial_iteration_recovers_exact_surface( + monkeypatch: pytest.MonkeyPatch, +) -> None: + rows = 11 + columns = 11 + x = np.linspace(-1.0, 1.0, columns) + y = np.linspace(-1.0, 1.0, rows) + xx, yy = np.meshgrid(x, y) + + expected_background = ( + 0.10 + 0.05 * xx - 0.04 * yy + 0.03 * xx * yy + 0.02 * xx**2 - 0.01 * yy**2 + ) + + data = expected_background.copy() + data[5, 5] += 5.0 + original = data.copy() + + class InitialPeak: + success = True + mean = 0.0 + rms = 0.2 + + class UpdatedPeak: + success = True + + updated_peak = UpdatedPeak() + + def fake_updated_peak(received: np.ndarray) -> UpdatedPeak: + expected_corrected = np.zeros_like(data) + expected_corrected[5, 5] = 5.0 + np.testing.assert_allclose( + received, + expected_corrected, + atol=2e-13, + ) + return updated_peak + + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + fake_updated_peak, + ) + + result = flatten_base_core._run_flatten_base_polynomial_iteration( + data, + peak=InitialPeak(), + degree=2, + ) + + expected_coefficients = np.array( + [ + 0.10, + -0.04, + -0.01, + 0.05, + 0.03, + 0.02, + ], + dtype=float, + ) + expected_corrected = np.zeros_like(data) + expected_corrected[5, 5] = 5.0 + + assert result.powers == ( + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (2, 0), + ) + assert result.mask.raw_count == 1 + assert result.mask.grown_count == 13 + assert result.selected_count == data.size - 13 + assert result.rank == 6 + assert result.peak is updated_peak + + np.testing.assert_allclose( + result.coefficients, + expected_coefficients, + atol=2e-13, + ) + np.testing.assert_allclose( + result.background, + expected_background, + atol=2e-13, + ) + np.testing.assert_allclose( + result.corrected, + expected_corrected, + atol=2e-13, + ) + np.testing.assert_array_equal(data, original) + + +def test_grow_mask_conn4_matches_gwyddion_271_right_edge_reference() -> None: + mask = np.zeros((8, 9), dtype=bool) + mask[2, 4] = True + mask[5, 7] = True + + observed = flatten_base_core._grow_mask_conn4( + mask, + radius=2, + ) + + frozen = ( + "000010000" + "000111000" + "001111100" + "000111010" + "000010111" + "000001110" + "000000111" + "000000010" + ) + expected = np.array( + [value == "1" for value in frozen], + dtype=bool, + ).reshape(8, 9) + + np.testing.assert_array_equal(observed, expected) + assert np.count_nonzero(observed) == 24 + assert not observed[5, 8] + + +def test_flatten_base_polynomial_iteration_matches_gwyddion_271( + monkeypatch: pytest.MonkeyPatch, +) -> None: + rows = 8 + columns = 9 + data = np.empty((rows, columns), dtype=float) + + for row in range(rows): + y = 2.0 * row / (rows - 1.0) - 1.0 + + for column in range(columns): + x = 2.0 * column / (columns - 1.0) - 1.0 + value = ( + 0.72 + + 0.18 * x + - 0.11 * y + + 0.07 * x * y + + 0.035 * x**2 + - 0.02 * y**2 + + 0.025 * np.sin(0.9 * column + 0.4 * row) + ) + + if row == 2 and column == 4: + value += 1.5 + if row == 5 and column == 7: + value += 1.0 + + data[row, column] = value + + original = data.copy() + + class InitialPeak: + success = True + mean = 0.75 + rms = 0.10 + + class UpdatedPeak: + success = True + + updated_peak = UpdatedPeak() + + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + lambda received: updated_peak, + ) + + result = flatten_base_core._run_flatten_base_polynomial_iteration( + data, + peak=InitialPeak(), + degree=2, + ) + + raw_frozen = ( + "000000000" + "000000000" + "000010000" + "000000000" + "000000000" + "000000010" + "000000000" + "000000000" + ) + grown_frozen = ( + "000010000" + "000111000" + "001111100" + "000111010" + "000010111" + "000001110" + "000000111" + "000000010" + ) + + expected_raw = np.array( + [value == "1" for value in raw_frozen], + dtype=bool, + ).reshape(rows, columns) + expected_grown = np.array( + [value == "1" for value in grown_frozen], + dtype=bool, + ).reshape(rows, columns) + + expected_coefficients = np.array( + [ + 0.71670865227920233, + -0.11459538697342461, + -0.024654671854024313, + 0.18007084915965604, + 0.0761606492072983, + 0.056803123388348246, + ], + dtype=float, + ) + + x = np.linspace(-1.0, 1.0, columns) + y = np.linspace(-1.0, 1.0, rows) + xx, yy = np.meshgrid(x, y) + + expected_background = ( + expected_coefficients[0] + + expected_coefficients[1] * yy + + expected_coefficients[2] * yy**2 + + expected_coefficients[3] * xx + + expected_coefficients[4] * xx * yy + + expected_coefficients[5] * xx**2 + ) + + assert result.degree == 2 + assert result.rank == 6 + assert result.selected_count == 48 + assert result.mask.raw_count == 2 + assert result.mask.grown_count == 24 + assert result.peak is updated_peak + + np.testing.assert_array_equal(result.mask.raw, expected_raw) + np.testing.assert_array_equal(result.mask.grown, expected_grown) + np.testing.assert_allclose( + result.coefficients, + expected_coefficients, + atol=5e-13, + rtol=0.0, + ) + np.testing.assert_allclose( + result.background, + expected_background, + atol=5e-13, + rtol=0.0, + ) + np.testing.assert_allclose( + result.corrected, + data - expected_background, + atol=5e-13, + rtol=0.0, + ) + np.testing.assert_array_equal(data, original) + + +def test_flatten_base_polynomial_stage_runs_degrees_two_to_five( + monkeypatch: pytest.MonkeyPatch, +) -> None: + rows = 4 + columns = 5 + data = np.arange(rows * columns, dtype=float).reshape(rows, columns) + original = data.copy() + + class Peak: + success = True + + def __init__(self, label: str) -> None: + self.label = label + + peaks = [Peak(f"peak-{index}") for index in range(5)] + calls: list[tuple[np.ndarray, Peak, int]] = [] + produced_iterations: list[flatten_base_core.FlattenBasePolynomialIteration] = [] + + def fake_iteration( + received: np.ndarray, + *, + peak: Peak, + degree: int, + ) -> flatten_base_core.FlattenBasePolynomialIteration: + expected_index = len(calls) + expected_degree = (2, 3, 4, 5)[expected_index] + + assert degree == expected_degree + assert peak is peaks[expected_index] + + calls.append((received.copy(), peak, degree)) + + background = np.full_like( + data, + float(degree), + ) + corrected = received - background + + background.setflags(write=False) + corrected.setflags(write=False) + + coefficients = np.zeros(1, dtype=float) + singular_values = np.ones(1, dtype=float) + coefficients.setflags(write=False) + singular_values.setflags(write=False) + + empty_mask = np.zeros_like(data, dtype=bool) + empty_mask.setflags(write=False) + + automatic_mask = flatten_base_core.FlattenBaseMask( + degree=degree, + threshold=0.0, + growth_radius=1 + degree // 2, + raw=empty_mask, + grown=empty_mask, + raw_count=0, + grown_count=0, + ) + + iteration = flatten_base_core.FlattenBasePolynomialIteration( + degree=degree, + powers=((0, 0),), + mask=automatic_mask, + selected_count=data.size, + coefficients=coefficients, + rank=1, + singular_values=singular_values, + background=background, + corrected=corrected, + peak=peaks[expected_index + 1], + ) + produced_iterations.append(iteration) + return iteration + + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_iteration", + fake_iteration, + ) + + result = flatten_base_core._run_flatten_base_polynomial_stage( + data, + peak=peaks[0], + ) + + expected_background = np.full_like( + data, + 2.0 + 3.0 + 4.0 + 5.0, + ) + + assert [call[2] for call in calls] == [2, 3, 4, 5] + assert result.initial_peak is peaks[0] + assert result.iterations == tuple(produced_iterations) + assert result.completed_degrees == (2, 3, 4, 5) + assert result.termination == "completed" + + for index, (received, received_peak, degree) in enumerate(calls): + expected_previous = sum((2, 3, 4, 5)[:index]) + np.testing.assert_allclose( + received, + data - expected_previous, + ) + assert received_peak is peaks[index] + assert degree == (2, 3, 4, 5)[index] + + np.testing.assert_allclose( + result.background, + expected_background, + ) + np.testing.assert_allclose( + result.corrected, + data - expected_background, + ) + + assert not result.background.flags.writeable + assert not result.corrected.flags.writeable + np.testing.assert_array_equal(data, original) + + +def test_flatten_base_polynomial_stage_stops_after_peak_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.arange(20, dtype=float).reshape(4, 5) + original = data.copy() + + class Peak: + def __init__(self, success: bool) -> None: + self.success = success + + initial_peak = Peak(success=True) + degree_two_peak = Peak(success=True) + failed_peak = Peak(success=False) + + calls: list[int] = [] + + def fake_iteration( + received: np.ndarray, + *, + peak: Peak, + degree: int, + ) -> flatten_base_core.FlattenBasePolynomialIteration: + calls.append(degree) + + if degree == 2: + assert peak is initial_peak + next_peak = degree_two_peak + elif degree == 3: + assert peak is degree_two_peak + next_peak = failed_peak + else: + raise AssertionError(f"unexpected polynomial degree after failure: {degree}") + + background = np.full_like(received, float(degree)) + corrected = received - background + + coefficients = np.zeros(1, dtype=float) + singular_values = np.ones(1, dtype=float) + empty_mask = np.zeros_like(received, dtype=bool) + + background.setflags(write=False) + corrected.setflags(write=False) + coefficients.setflags(write=False) + singular_values.setflags(write=False) + empty_mask.setflags(write=False) + + automatic_mask = flatten_base_core.FlattenBaseMask( + degree=degree, + threshold=0.0, + growth_radius=1 + degree // 2, + raw=empty_mask, + grown=empty_mask, + raw_count=0, + grown_count=0, + ) + + return flatten_base_core.FlattenBasePolynomialIteration( + degree=degree, + powers=((0, 0),), + mask=automatic_mask, + selected_count=received.size, + coefficients=coefficients, + rank=1, + singular_values=singular_values, + background=background, + corrected=corrected, + peak=next_peak, + ) + + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_iteration", + fake_iteration, + ) + + result = flatten_base_core._run_flatten_base_polynomial_stage( + data, + peak=initial_peak, + ) + + expected_background = np.full_like(data, 5.0) + + assert calls == [2, 3] + assert result.initial_peak is initial_peak + assert result.completed_degrees == (2, 3) + assert result.termination == "peak_failure" + assert result.iterations[-1].peak is failed_peak + + np.testing.assert_allclose( + result.background, + expected_background, + ) + np.testing.assert_allclose( + result.corrected, + data - expected_background, + ) + np.testing.assert_array_equal(data, original) + + assert not result.background.flags.writeable + assert not result.corrected.flags.writeable + + +def test_flatten_base_mask_uses_parameters_from_unsuccessful_peak() -> None: + data = np.zeros((5, 5), dtype=float) + data[2, 2] = 4.0 + + class Peak: + success = False + mean = 1.0 + rms = 0.5 + + result = flatten_base_core._build_flatten_base_mask( + data, + peak=Peak(), + degree=2, + ) + + assert result.threshold == 2.5 + assert result.raw_count == 1 + assert result.grown_count == 13 + assert result.raw[2, 2] + + +def test_polynomial_stage_runs_degree_two_with_failed_incoming_peak( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.arange(20, dtype=float).reshape(4, 5) + + class Peak: + def __init__(self, success: bool) -> None: + self.success = success + + incoming_peak = Peak(success=False) + failed_updated_peak = Peak(success=False) + calls: list[int] = [] + + class Iteration: + degree = 2 + background = np.ones_like(data) + corrected = data - 1.0 + peak = failed_updated_peak + + def fake_iteration( + received: np.ndarray, + *, + peak: Peak, + degree: int, + ) -> Iteration: + np.testing.assert_array_equal(received, data) + assert peak is incoming_peak + assert degree == 2 + calls.append(degree) + return Iteration() + + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_iteration", + fake_iteration, + ) + + result = flatten_base_core._run_flatten_base_polynomial_stage( + data, + peak=incoming_peak, + ) + + assert calls == [2] + assert result.completed_degrees == (2,) + assert result.termination == "peak_failure" + + np.testing.assert_array_equal( + result.background, + np.ones_like(data), + ) + np.testing.assert_array_equal( + result.corrected, + data - 1.0, + ) + + +def test_polynomial_iteration_skips_constant_field_and_reestimates_peak( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.full((5, 5), 2.5, dtype=float) + original = data.copy() + + class IncomingPeak: + success = True + mean = 2.5 + rms = 0.0 + + class UpdatedPeak: + success = False + mean = 2.5 + rms = 0.0 + + incoming_peak = IncomingPeak() + updated_peak = UpdatedPeak() + peak_calls: list[np.ndarray] = [] + + def forbidden_mask(*args: object, **kwargs: object) -> None: + del args, kwargs + raise AssertionError("constant-field iteration must not construct a mask") + + def forbidden_fit(*args: object, **kwargs: object) -> None: + del args, kwargs + raise AssertionError("constant-field iteration must not fit a polynomial") + + def fake_peak(received: np.ndarray) -> UpdatedPeak: + peak_calls.append(received.copy()) + np.testing.assert_array_equal(received, data) + return updated_peak + + monkeypatch.setattr( + flatten_base_core, + "_build_flatten_base_mask", + forbidden_mask, + ) + monkeypatch.setattr( + flatten_base_core, + "_estimate_base_peak", + fake_peak, + ) + + import spmkit.core.analysis.leveling as leveling + + monkeypatch.setattr( + leveling, + "_fit_polynomial_surface_data", + forbidden_fit, + ) + + result = flatten_base_core._run_flatten_base_polynomial_iteration( + data, + peak=incoming_peak, + degree=2, + ) + + assert not result.applied + assert result.degree == 2 + assert result.powers == () + assert result.mask is None + assert result.selected_count == 0 + assert result.rank == 0 + assert result.coefficients.size == 0 + assert result.singular_values.size == 0 + assert result.peak is updated_peak + assert len(peak_calls) == 1 + + np.testing.assert_array_equal( + result.background, + np.zeros_like(data), + ) + np.testing.assert_array_equal(result.corrected, data) + np.testing.assert_array_equal(data, original) + + assert not result.coefficients.flags.writeable + assert not result.singular_values.flags.writeable + assert not result.background.flags.writeable + assert not result.corrected.flags.writeable + + +def test_polynomial_stage_records_unapplied_degree_before_peak_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.arange(20, dtype=float).reshape(4, 5) + original = data.copy() + + class Peak: + def __init__(self, success: bool) -> None: + self.success = success + + incoming_peak = Peak(success=True) + failed_peak = Peak(success=False) + + background = np.zeros_like(data) + corrected = data.copy() + coefficients = np.empty(0, dtype=float) + singular_values = np.empty(0, dtype=float) + + background.setflags(write=False) + corrected.setflags(write=False) + coefficients.setflags(write=False) + singular_values.setflags(write=False) + + skipped_iteration = flatten_base_core.FlattenBasePolynomialIteration( + degree=2, + powers=(), + mask=None, + selected_count=0, + coefficients=coefficients, + rank=0, + singular_values=singular_values, + background=background, + corrected=corrected, + peak=failed_peak, + applied=False, + ) + + calls: list[int] = [] + + def fake_iteration( + received: np.ndarray, + *, + peak: Peak, + degree: int, + ) -> flatten_base_core.FlattenBasePolynomialIteration: + np.testing.assert_array_equal(received, data) + assert peak is incoming_peak + assert degree == 2 + calls.append(degree) + return skipped_iteration + + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_iteration", + fake_iteration, + ) + + result = flatten_base_core._run_flatten_base_polynomial_stage( + data, + peak=incoming_peak, + ) + + assert calls == [2] + assert result.attempted_degrees == (2,) + assert result.completed_degrees == () + assert result.iterations == (skipped_iteration,) + assert result.termination == "peak_failure" + + np.testing.assert_array_equal( + result.background, + np.zeros_like(data), + ) + np.testing.assert_array_equal(result.corrected, data) + np.testing.assert_array_equal(data, original) + + assert not result.background.flags.writeable + assert not result.corrected.flags.writeable + + +def test_flatten_base_composes_stages_and_final_offsets( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.array( + [ + [10.0, 11.0, 12.0], + [13.0, 14.0, 15.0], + [16.0, 17.0, 18.0], + ], + dtype=float, + ) + original = data.copy() + + class FacetPeak: + success = True + mean = 2.0 + rms = 0.5 + + class FinalPeak: + success = True + mean = 1.5 + rms = 0.2 + + facet_peak = FacetPeak() + final_peak = FinalPeak() + + facet_background = np.full_like(data, 2.0) + facet_corrected = data - facet_background + facet_background.setflags(write=False) + facet_corrected.setflags(write=False) + + facet_stage = flatten_base_core.FacetStageResult( + corrected=facet_corrected, + background=facet_background, + initial_peak=facet_peak, + iterations=(), + termination="degenerate_plane", + ) + + polynomial_background = np.full_like(data, 3.0) + polynomial_corrected = facet_corrected - polynomial_background + polynomial_background.setflags(write=False) + polynomial_corrected.setflags(write=False) + + class FinalIteration: + degree = 5 + peak = final_peak + applied = True + + polynomial_stage = flatten_base_core.FlattenBasePolynomialStage( + corrected=polynomial_corrected, + background=polynomial_background, + initial_peak=facet_peak, + iterations=(FinalIteration(),), + termination="completed", + ) + + calls: dict[str, object] = {} + + def fake_facet_stage( + received: np.ndarray, + *, + pixel_size_x: float, + pixel_size_y: float, + ) -> flatten_base_core.FacetStageResult: + np.testing.assert_array_equal(received, data) + assert pixel_size_x == 2.0 + assert pixel_size_y == 0.5 + calls["facet"] = True + return facet_stage + + def fake_polynomial_stage( + received: np.ndarray, + *, + peak: FacetPeak, + ) -> flatten_base_core.FlattenBasePolynomialStage: + np.testing.assert_array_equal(received, facet_corrected) + assert peak is facet_peak + calls["polynomial"] = True + return polynomial_stage + + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_facet_stage", + fake_facet_stage, + ) + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_stage", + fake_polynomial_stage, + ) + + result = flatten_base_core._run_flatten_base( + data, + pixel_size_x=2.0, + pixel_size_y=0.5, + ) + + after_mean_centering = polynomial_corrected - final_peak.mean + expected_minimum_offset = float(np.min(after_mean_centering)) + expected_corrected = after_mean_centering - expected_minimum_offset + expected_background = ( + facet_background + polynomial_background + final_peak.mean + expected_minimum_offset + ) + + assert calls == { + "facet": True, + "polynomial": True, + } + assert result.facet_stage is facet_stage + assert result.polynomial_stage is polynomial_stage + assert result.final_peak is final_peak + assert result.mean_centered + assert result.mean_offset == 1.5 + assert result.minimum_offset == 3.5 + assert result.total_offset == 5.0 + + np.testing.assert_allclose( + result.corrected, + expected_corrected, + ) + np.testing.assert_allclose( + result.background, + expected_background, + ) + np.testing.assert_allclose( + result.corrected + result.background, + data, + ) + np.testing.assert_array_equal(data, original) + + assert not result.corrected.flags.writeable + assert not result.background.flags.writeable + + +def test_flatten_base_skips_mean_after_failed_final_peak() -> None: + data = np.array( + [ + [8.0, 9.0], + [10.0, 11.0], + ], + dtype=float, + ) + original = data.copy() + + class FacetPeak: + success = True + mean = 1.0 + rms = 0.25 + + class FailedPeak: + success = False + mean = 999.0 + rms = 0.5 + + facet_peak = FacetPeak() + failed_peak = FailedPeak() + + facet_background = np.full_like(data, 1.0) + facet_corrected = data - facet_background + + facet_stage = flatten_base_core.FacetStageResult( + corrected=facet_corrected, + background=facet_background, + initial_peak=facet_peak, + iterations=(), + termination="completed", + ) + + polynomial_background = np.full_like(data, 2.0) + polynomial_corrected = facet_corrected - polynomial_background + + class FinalIteration: + degree = 2 + peak = failed_peak + applied = True + + polynomial_stage = flatten_base_core.FlattenBasePolynomialStage( + corrected=polynomial_corrected, + background=polynomial_background, + initial_peak=facet_peak, + iterations=(FinalIteration(),), + termination="peak_failure", + ) + + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_facet_stage", + lambda *args, **kwargs: facet_stage, + ) + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_stage", + lambda *args, **kwargs: polynomial_stage, + ) + + result = flatten_base_core._run_flatten_base( + data, + pixel_size_x=1.0, + pixel_size_y=1.0, + ) + + expected_minimum_offset = 5.0 + expected_corrected = polynomial_corrected - expected_minimum_offset + expected_background = facet_background + polynomial_background + expected_minimum_offset + + assert result.final_peak is failed_peak + assert not result.mean_centered + assert result.mean_offset == 0.0 + assert result.minimum_offset == expected_minimum_offset + assert result.total_offset == expected_minimum_offset + + np.testing.assert_array_equal( + result.corrected, + expected_corrected, + ) + np.testing.assert_array_equal( + result.background, + expected_background, + ) + np.testing.assert_array_equal( + result.corrected + result.background, + data, + ) + np.testing.assert_array_equal(data, original) + + assert not result.corrected.flags.writeable + assert not result.background.flags.writeable + + +def test_flatten_base_preserves_nonpositive_minimum_after_mean_centering( + monkeypatch: pytest.MonkeyPatch, +) -> None: + data = np.array( + [ + [2.0, 3.0], + [4.0, 5.0], + ], + dtype=float, + ) + original = data.copy() + + class FacetPeak: + success = True + mean = 0.0 + rms = 0.25 + + class FinalPeak: + success = True + mean = 0.5 + rms = 0.2 + + facet_peak = FacetPeak() + final_peak = FinalPeak() + + facet_background = np.full_like(data, 1.0) + facet_corrected = data - facet_background + + facet_stage = flatten_base_core.FacetStageResult( + corrected=facet_corrected, + background=facet_background, + initial_peak=facet_peak, + iterations=(), + termination="completed", + ) + + polynomial_background = np.full_like(data, 2.0) + polynomial_corrected = facet_corrected - polynomial_background + + class FinalIteration: + degree = 5 + peak = final_peak + applied = True + + polynomial_stage = flatten_base_core.FlattenBasePolynomialStage( + corrected=polynomial_corrected, + background=polynomial_background, + initial_peak=facet_peak, + iterations=(FinalIteration(),), + termination="completed", + ) + + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_facet_stage", + lambda *args, **kwargs: facet_stage, + ) + monkeypatch.setattr( + flatten_base_core, + "_run_flatten_base_polynomial_stage", + lambda *args, **kwargs: polynomial_stage, + ) + + result = flatten_base_core._run_flatten_base( + data, + pixel_size_x=1.0, + pixel_size_y=1.0, + ) + + expected_corrected = polynomial_corrected - final_peak.mean + expected_background = facet_background + polynomial_background + final_peak.mean + + assert result.final_peak is final_peak + assert result.mean_centered + assert result.mean_offset == 0.5 + assert result.minimum_offset == 0.0 + assert result.total_offset == 0.5 + assert float(np.min(result.corrected)) == -1.5 + + np.testing.assert_allclose( + result.corrected, + expected_corrected, + ) + np.testing.assert_allclose( + result.background, + expected_background, + ) + np.testing.assert_allclose( + result.corrected + result.background, + data, + ) + np.testing.assert_array_equal(data, original) + + assert not result.corrected.flags.writeable + assert not result.background.flags.writeable + + +def test_gwyddion_lm_reproduces_edge_peak_solution() -> None: + centers = np.array( + [ + 0.075611686318400137, + 0.26307892775469505, + 0.45054616919099005, + 0.638013410627285, + 0.82548065206358001, + 1.0129478934998748, + 1.2004151349361698, + ], + dtype=float, + ) + density = np.array( + [ + 4.514677658785919, + 0.18058710635143677, + 0.13891315873187443, + 0.076402237302530943, + 0.0, + 0.083347895239124656, + 0.041673947619562328, + ], + dtype=float, + ) + + original_centers = centers.copy() + original_density = density.copy() + + window = flatten_base_core.BasePeakWindow( + centers=centers, + density=density, + peak_index=0, + start_index=0, + stop_index=7, + initial_mean=0.075611686318400137, + initial_offset=0.0, + initial_amplitude=4.514677658785919, + initial_width=0.39368120701621939, + ) + + result = flatten_base_core._fit_base_peak_gwyddion_lm(window) + + assert result.solver_success + assert result.covariance_available + assert result.jacobian_rank == 4 + assert np.isfinite(result.condition_estimate) + + assert result.mean == pytest.approx( + -0.38015369096654944, + abs=5e-10, + rel=0.0, + ) + assert result.offset == pytest.approx( + 0.067658206848585964, + abs=5e-10, + rel=0.0, + ) + assert result.amplitude == pytest.approx( + 178.54320058289358, + abs=5e-7, + rel=0.0, + ) + assert result.width == pytest.approx( + 0.23717840912131738, + abs=5e-10, + rel=0.0, + ) + assert result.rms == pytest.approx( + 0.1677104614407208, + abs=5e-10, + rel=0.0, + ) + + np.testing.assert_array_equal( + centers, + original_centers, + ) + np.testing.assert_array_equal( + density, + original_density, + ) + + +def test_gwyddion_packed_cholesky_matches_dense_reference() -> None: + matrix = np.array( + [ + [7.0, 1.2, 0.4, -0.3], + [1.2, 5.0, 0.8, 0.2], + [0.4, 0.8, 4.0, 0.6], + [-0.3, 0.2, 0.6, 3.0], + ], + dtype=float, + ) + right_hand_side = np.array( + [1.0, -2.0, 0.5, 3.0], + dtype=float, + ) + + packed = np.array( + [matrix[row, column] for row in range(matrix.shape[0]) for column in range(row + 1)], + dtype=float, + ) + + decomposition = packed.copy() + + assert flatten_base_core._gwyddion_cholesky_decompose( + 4, + decomposition, + ) + + solution = right_hand_side.copy() + + flatten_base_core._gwyddion_cholesky_solve( + 4, + decomposition, + solution, + ) + + np.testing.assert_allclose( + solution, + np.linalg.solve(matrix, right_hand_side), + atol=5e-15, + rtol=5e-15, + ) + + inverse = packed.copy() + + assert flatten_base_core._gwyddion_cholesky_invert( + 4, + inverse, + ) + + expected_inverse = np.linalg.inv(matrix) + + expected_packed_inverse = np.array( + [ + expected_inverse[row, column] + for row in range(matrix.shape[0]) + for column in range(row + 1) + ], + dtype=float, + ) + + np.testing.assert_allclose( + inverse, + expected_packed_inverse, + atol=5e-15, + rtol=5e-15, + ) diff --git a/tests/core/test_geometry.py b/tests/core/test_geometry.py new file mode 100644 index 0000000..816a831 --- /dev/null +++ b/tests/core/test_geometry.py @@ -0,0 +1,191 @@ +"""Tests for physical geometry primitives.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from spmkit.core.geometry import ( + bilinear_sample, + length_scale_to_metres, + length_values_from_metres, + length_values_to_metres, + physical_to_pixel_indices, + pixel_center_axes, +) + + +@pytest.mark.parametrize( + ("unit", "expected"), + [ + ("m", 1.0), + ("mm", 1e-3), + ("µm", 1e-6), + ("μm", 1e-6), + ("um", 1e-6), + ("nm", 1e-9), + ("pm", 1e-12), + ("Å", 1e-10), + ("angstrom", 1e-10), + ], +) +def test_length_scale_to_metres_supports_geometric_units( + unit: str, + expected: float, +) -> None: + assert length_scale_to_metres(unit) == expected + + +@pytest.mark.parametrize("unit", ["V", "A", "nN", "arbitrary"]) +def test_length_scale_to_metres_rejects_non_length_units( + unit: str, +) -> None: + with pytest.raises( + ValueError, + match="unsupported geometric length unit", + ): + length_scale_to_metres(unit) + + +def test_length_value_conversion_round_trip() -> None: + values = np.array([-3.0, 0.0, 12.5]) + + metres = length_values_to_metres(values, unit="nm") + recovered = length_values_from_metres(metres, unit="nm") + + assert np.allclose( + metres, + values * 1e-9, + ) + assert np.allclose(recovered, values) + + +def test_pixel_center_axes_use_physical_pixel_centres() -> None: + x_coordinates, y_coordinates = pixel_center_axes( + (2, 4), + x_range=4.0, + y_range=2.0, + ) + + assert np.allclose( + x_coordinates, + [-1.5, -0.5, 0.5, 1.5], + ) + assert np.allclose( + y_coordinates, + [-0.5, 0.5], + ) + + +def test_physical_coordinates_round_trip_to_pixel_indices() -> None: + shape = (3, 5) + x_coordinates, y_coordinates = pixel_center_axes( + shape, + x_range=10.0, + y_range=6.0, + ) + xx, yy = np.meshgrid( + x_coordinates, + y_coordinates, + ) + + x_indices, y_indices = physical_to_pixel_indices( + xx, + yy, + shape=shape, + x_range=10.0, + y_range=6.0, + ) + + expected_x, expected_y = np.meshgrid( + np.arange(shape[1], dtype=float), + np.arange(shape[0], dtype=float), + ) + + assert np.allclose(x_indices, expected_x) + assert np.allclose(y_indices, expected_y) + + +def test_bilinear_sample_is_exact_for_affine_surface() -> None: + yy, xx = np.mgrid[0:4, 0:5] + data = 2.0 * xx - 3.0 * yy + 7.0 + + sample_x = np.array([0.25, 1.5, 3.75]) + sample_y = np.array([0.5, 2.25, 1.75]) + + result = bilinear_sample( + data, + x_index=sample_x, + y_index=sample_y, + ) + expected = 2.0 * sample_x - 3.0 * sample_y + 7.0 + + assert np.allclose(result, expected) + + +def test_bilinear_sample_nearest_fill_clamps_to_border() -> None: + data = np.arange(9.0).reshape(3, 3) + + result = bilinear_sample( + data, + x_index=np.array([-2.0, 4.0]), + y_index=np.array([1.0, 1.0]), + fill_mode="nearest", + ) + + assert np.allclose(result, [3.0, 5.0]) + + +def test_bilinear_sample_constant_fill_marks_outside_domain() -> None: + data = np.arange(9.0).reshape(3, 3) + + result = bilinear_sample( + data, + x_index=np.array([-1.0, 1.0, 3.0]), + y_index=np.array([1.0, 1.0, 1.0]), + fill_mode="constant", + fill_value=-5.0, + ) + + assert np.allclose(result, [-5.0, 4.0, -5.0]) + + +@pytest.mark.parametrize( + ("shape", "x_range", "y_range", "error_type", "message"), + [ + ( + (0, 4), + 1.0, + 1.0, + ValueError, + "non-empty two-dimensional shape", + ), + ( + (3, 4), + 0.0, + 1.0, + ValueError, + "x_range to be positive", + ), + ( + (3, 4), + 1.0, + np.inf, + ValueError, + "y_range to be finite", + ), + ], +) +def test_pixel_center_axes_reject_invalid_geometry( + shape: tuple[int, int], + x_range: float, + y_range: float, + error_type: type[Exception], + message: str, +) -> None: + with pytest.raises(error_type, match=message): + pixel_center_axes( + shape, + x_range=x_range, + y_range=y_range, + ) diff --git a/tests/core/test_gwyddion_align_rows_statistics.py b/tests/core/test_gwyddion_align_rows_statistics.py new file mode 100644 index 0000000..2cc42c5 --- /dev/null +++ b/tests/core/test_gwyddion_align_rows_statistics.py @@ -0,0 +1,409 @@ +"""Public-contract tests for Gwyddion 2.71 Align Rows statistics.""" + +from __future__ import annotations + +import inspect +import json +from collections.abc import Callable +from pathlib import Path +from types import SimpleNamespace +from typing import Any, get_args + +import numpy as np +import pytest + +import spmkit.core.analysis as analysis +import spmkit.core.analysis.leveling as leveling_module +from spmkit.core.analysis import ( + GwyddionAlignRowsDirection, + GwyddionAlignRowsMaskMode, + gwyddion_align_rows_median, + gwyddion_align_rows_median_of_differences, + gwyddion_align_rows_trimmed_mean, + gwyddion_align_rows_trimmed_mean_of_differences, +) +from spmkit.core.models import SPMChannel + +_FIXTURE = ( + Path(__file__).resolve().parents[1] + / "validation" + / "fixtures" + / "gwyddion" + / "align_rows_statistics" +) +_FUNCTIONS: dict[int, Callable[..., SPMChannel]] = { + 1: gwyddion_align_rows_median, + 2: gwyddion_align_rows_median_of_differences, + 5: gwyddion_align_rows_trimmed_mean, + 6: gwyddion_align_rows_trimmed_mean_of_differences, +} +_MASK_MODES: dict[int, str] = {0: "exclude", 1: "include", 2: "ignore"} +_DIRECTIONS: dict[int, str] = {0: "horizontal", 1: "vertical"} +_EXCEPTIONAL_CASES = { + "median__plateaus_signed_zero__10", + "median_of_differences__irregular__11", + "trimmed_mean_of_differences__irregular__11", +} + + +def _load() -> tuple[dict[str, Any], dict[str, np.ndarray]]: + manifest = json.loads((_FIXTURE / "align_rows_statistics_reference.json").read_text()) + with np.load(_FIXTURE / "align_rows_statistics_reference.npz", allow_pickle=False) as archive: + arrays = { + name: np.array(archive[name], dtype=np.float64, order="C", copy=True) + for name in archive.files + } + return manifest, arrays + + +def _bits(array: np.ndarray) -> np.ndarray: + return np.ascontiguousarray(array, dtype=np.float64).view(np.uint64) + + +def _ordered_uint64(bits: int) -> int: + return (~bits + 1) & ((1 << 64) - 1) if bits >> 63 else bits | (1 << 63) + + +def _ulp_distance(left: np.uint64, right: np.uint64) -> int: + return abs(_ordered_uint64(int(left)) - _ordered_uint64(int(right))) + + +def _assert_bitwise(actual: np.ndarray, expected: np.ndarray, *, case_id: str) -> None: + differing = _bits(actual) != _bits(expected) + if not differing.any(): + return + row, column = (int(item) for item in np.argwhere(differing)[0]) + pytest.fail( + f"case={case_id} coordinate=({row}, {column}) " + f"expected_bits={_bits(expected)[row, column]:016x} " + f"actual_bits={_bits(actual)[row, column]:016x}" + ) + + +def _channel(data: np.ndarray, *, xreal: float, yreal: float) -> SPMChannel: + return SPMChannel( + name="Align Rows fixture", + data=data, + unit="V", + x_range=xreal, + y_range=yreal, + direction="backward", + group="Frozen Align Rows evidence", + metadata={"source": "gwyddion-2.71-align-rows", "context": {"id": 64}}, + ) + + +def _run(case: dict[str, Any], arrays: dict[str, np.ndarray], channel: SPMChannel) -> SPMChannel: + function = _FUNCTIONS[int(case["method"])] + kwargs: dict[str, object] = { + "mask": None if case["mask_key"] is None else arrays[str(case["mask_key"])], + "mask_mode": _MASK_MODES[int(case["masking_mode"])], + "direction": _DIRECTIONS[int(case["direction"])], + } + if int(case["method"]) in {5, 6}: + kwargs["trim_fraction"] = float.fromhex(str(case["trim_fraction_hex"])) + return function(channel, **kwargs) + + +def test_public_exports_types_and_signatures() -> None: + expected = { + "GwyddionAlignRowsDirection", + "GwyddionAlignRowsMaskMode", + "gwyddion_align_rows_median", + "gwyddion_align_rows_median_of_differences", + "gwyddion_align_rows_trimmed_mean", + "gwyddion_align_rows_trimmed_mean_of_differences", + } + assert expected <= set(analysis.__all__) + assert get_args(GwyddionAlignRowsMaskMode) == ("exclude", "include", "ignore") + assert get_args(GwyddionAlignRowsDirection) == ("horizontal", "vertical") + + for method, function in _FUNCTIONS.items(): + assert getattr(analysis, function.__name__) is function + signature = inspect.signature(function) + assert list(signature.parameters) == ( + ["channel", "trim_fraction", "mask", "mask_mode", "direction"] + if method in {5, 6} + else ["channel", "mask", "mask_mode", "direction"] + ) + assert all( + parameter.kind is inspect.Parameter.KEYWORD_ONLY + for name, parameter in signature.parameters.items() + if name != "channel" + ) + assert "extract_background" not in signature.parameters + assert "method" not in signature.parameters + for private_name in ( + "_GwyddionAlignRowsDirection", + "_GwyddionAlignRowsMethod", + "_GwyddionAlignRowsStatisticsResult", + "_GwyddionMaskMode", + "_gwyddion_align_rows_statistics_result", + ): + assert private_name not in analysis.__all__ + assert not hasattr(analysis, private_name) + + +def test_explicit_gwyddion_wrappers_remain_separate_from_generic_align_rows() -> None: + """The validated Gwyddion entry point is explicit and returns a new channel.""" + channel = _channel( + np.array([[1.0, 2.0], [4.0, 8.0]], dtype=np.float64), + xreal=2.0, + yreal=2.0, + ) + + gwyddion_result = gwyddion_align_rows_median(channel) + generic_result = leveling_module.align_rows(channel, method="median") + + assert gwyddion_result is not generic_result + assert gwyddion_result is not channel + assert generic_result is not channel + assert not np.shares_memory(gwyddion_result.data, generic_result.data) + assert not np.array_equal(_bits(gwyddion_result.data), _bits(generic_result.data)) + + +def test_all_portable_cases_are_bitwise_exact_deterministic_and_non_mutating() -> None: + manifest, arrays = _load() + exact_elements = mutation_matches = no_op_matches = 0 + seen_methods: set[int] = set() + seen_modes: set[int] = set() + seen_directions: set[int] = set() + seen_trims: set[float] = set() + absent_mask_modes: set[int] = set() + for case in manifest["cases"]: + source = arrays[case["input_key"]] + mask = None if case["mask_key"] is None else arrays[case["mask_key"]] + source_before = source.copy(order="C") + mask_before = None if mask is None else mask.copy(order="C") + channel = _channel( + source, + xreal=float.fromhex(case["xreal_hex"]), + yreal=float.fromhex(case["yreal_hex"]), + ) + first = _run(case, arrays, channel) + second = _run(case, arrays, channel) + expected = arrays[case["portable_corrected_key"]] + _assert_bitwise(first.data, expected, case_id=case["case_identifier"]) + _assert_bitwise(second.data, first.data, case_id=case["case_identifier"] + "/repeat") + assert first.data.dtype == np.float64 and first.data.flags.c_contiguous + assert first.data.shape == source.shape + assert not np.shares_memory(first.data, source) + assert np.array_equal(_bits(source), _bits(source_before)) + if mask is not None: + assert mask_before is not None + assert np.array_equal(_bits(mask), _bits(mask_before)) + else: + absent_mask_modes.add(int(case["masking_mode"])) + changed = bool((_bits(first.data) != _bits(source)).any()) + mutation_matches += int(changed == case["portable_mutated"] == case["installed_mutated"]) + no_op_matches += int((not changed) == (not case["portable_mutated"])) + exact_elements += first.data.size + seen_methods.add(int(case["method"])) + seen_modes.add(int(case["masking_mode"])) + seen_directions.add(int(case["direction"])) + seen_trims.add(float.fromhex(case["trim_fraction_hex"])) + assert exact_elements == 3888 + assert mutation_matches == no_op_matches == 64 + assert seen_methods == {1, 2, 5, 6} + assert seen_modes == {0, 1, 2} + assert seen_directions == {0, 1} + assert {0.0, 0.05, 0.5} <= seen_trims + assert absent_mask_modes == {0} + + +@pytest.mark.parametrize("function", list(_FUNCTIONS.values())) +def test_absent_mask_ignores_the_stored_mask_mode(function: Callable[..., SPMChannel]) -> None: + channel = _channel( + np.array([[1.0, 2.0, 4.0], [4.0, 5.0, 8.0], [7.0, 8.0, 12.0]], dtype=np.float64), + xreal=3.0, + yreal=3.0, + ) + kwargs: dict[str, object] = {"mask": None, "direction": "vertical"} + if function in { + gwyddion_align_rows_trimmed_mean, + gwyddion_align_rows_trimmed_mean_of_differences, + }: + kwargs["trim_fraction"] = 0.05 + outputs = [ + function(channel, mask_mode=mask_mode, **kwargs) + for mask_mode in ("exclude", "include", "ignore") + ] + _assert_bitwise(outputs[0].data, outputs[1].data, case_id="absent-mask/include") + _assert_bitwise(outputs[0].data, outputs[2].data, case_id="absent-mask/ignore") + + +def test_absolute_mask_thresholds_ignore_routing_and_global_fallback() -> None: + data = np.array([[100.0, -1000.0, 7.0, 0.0], [200.0, -800.0, 9.0, 10.0]]) + mask = np.array([[-0.0, 0.0, 0.5, 1.0], [-0.0, 0.0, 0.5, 1.0]]) + channel = _channel(data, xreal=4.0, yreal=2.0) + included = gwyddion_align_rows_median(channel, mask=mask, mask_mode="include") + excluded = gwyddion_align_rows_median(channel, mask=mask, mask_mode="exclude") + ignored = gwyddion_align_rows_median(channel, mask=mask, mask_mode="ignore") + _assert_bitwise( + included.data, + np.array([[101.5, -998.5, 8.5, 1.5], [198.5, -801.5, 7.5, 8.5]]), + case_id="absolute/include", + ) + _assert_bitwise( + excluded.data, + np.array([[101.0, -999.0, 8.0, 1.0], [199.0, -801.0, 8.0, 9.0]]), + case_id="absolute/exclude", + ) + _assert_bitwise(included.data, ignored.data, case_id="absolute/ignore") + + fallback_data = np.array( + [[0.0, 70.0, 80.0, 90.0], [10.0, 100.0, 80.0, 90.0], [30.0, 70.0, 80.0, 90.0]] + ) + fallback_mask = np.array([[1.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]]) + fallback = gwyddion_align_rows_median( + _channel(fallback_data, xreal=4.0, yreal=3.0), + mask=fallback_mask, + mask_mode="include", + ) + _assert_bitwise( + fallback.data, + fallback_data - np.array([[-30.0], [60.0], [-30.0]]), + case_id="absolute/zero-one-fallback", + ) + + +def test_difference_joint_thresholds_and_zero_one_pair_fallback() -> None: + data = np.array( + [[0.0, 0.0, 100.0, 500.0], [10.0, 10.0, 1000.0, 700.0], [40.0, 40.0, 2000.0, 900.0]] + ) + channel = _channel(data, xreal=4.0, yreal=3.0) + include_mask = np.array([[2.0, 2.0, 1.0, 0.5]] * 3) + exclude_mask = np.array([[0.0, 0.5, 1.0, 2.0]] * 3) + included = gwyddion_align_rows_median_of_differences( + channel, mask=include_mask, mask_mode="include" + ) + excluded = gwyddion_align_rows_median_of_differences( + channel, mask=exclude_mask, mask_mode="exclude" + ) + assert not np.array_equal(_bits(included.data), _bits(data)) + assert not np.array_equal(_bits(excluded.data), _bits(data)) + + one_pair_mask = np.array([[2.0, 0.0, 0.0, 0.0]] * 3) + fallback = gwyddion_align_rows_median_of_differences( + channel, mask=one_pair_mask, mask_mode="include" + ) + _assert_bitwise(fallback.data, data, case_id="difference/zero-one-pair-fallback") + + +def test_installed_fast_math_profile_matches_the_frozen_exception_policy() -> None: + manifest, arrays = _load() + exact_arrays = exact_elements = finite_nonzero = signed_zero = nan = infinity = 0 + maximum_absolute = 0.0 + maximum_ulp = 0 + exceptional_cases: set[str] = set() + mutation_matches = 0 + for case in manifest["cases"]: + source = arrays[case["input_key"]] + channel = _channel( + source, + xreal=float.fromhex(case["xreal_hex"]), + yreal=float.fromhex(case["yreal_hex"]), + ) + portable = _run(case, arrays, channel).data + installed = arrays[case["installed_corrected_key"]] + differing = _bits(portable) != _bits(installed) + exact_arrays += int(not differing.any()) + exact_elements += int((~differing).sum()) + if differing.any(): + exceptional_cases.add(case["case_identifier"]) + for row, column in np.argwhere(differing): + left = portable[row, column] + right = installed[row, column] + if np.isnan(left) or np.isnan(right): + nan += 1 + elif np.isinf(left) or np.isinf(right): + infinity += 1 + elif left == right == 0.0: + signed_zero += 1 + else: + finite_nonzero += 1 + maximum_absolute = max(maximum_absolute, abs(left - right)) + maximum_ulp = max( + maximum_ulp, + _ulp_distance(_bits(portable)[row, column], _bits(installed)[row, column]), + ) + changed = bool((_bits(portable) != _bits(source)).any()) + mutation_matches += int(changed == case["installed_mutated"]) + assert exact_arrays == 61 + assert exact_elements == 3757 + assert finite_nonzero == 128 + assert signed_zero == 3 + assert nan == infinity == 0 + assert maximum_absolute <= 5.329070518200751e-15 + assert maximum_ulp <= 144 + assert exceptional_cases == _EXCEPTIONAL_CASES + assert mutation_matches == 64 + + +def test_channel_context_is_preserved_with_independent_metadata_and_output() -> None: + manifest, arrays = _load() + case = next( + item for item in manifest["cases"] if item["case_identifier"] == "median__constant__00" + ) + source = arrays[case["input_key"]].copy(order="C") + channel = _channel( + source, + xreal=float.fromhex(case["xreal_hex"]), + yreal=float.fromhex(case["yreal_hex"]), + ) + output = _run(case, arrays, channel) + assert output.name == channel.name and output.unit == channel.unit + assert output.x_range == channel.x_range and output.y_range == channel.y_range + assert output.direction == channel.direction and output.group == channel.group + assert output.metadata == channel.metadata and output.metadata is not channel.metadata + output.metadata["new_key"] = True + assert "new_key" not in channel.metadata + assert output.data.flags.c_contiguous and not np.shares_memory(output.data, channel.data) + + +@pytest.mark.parametrize( + ("function", "kwargs", "error_type"), + [ + (gwyddion_align_rows_median, {"mask_mode": "selected"}, ValueError), + (gwyddion_align_rows_median, {"direction": "diagonal"}, ValueError), + (gwyddion_align_rows_median, {"mask": np.ones((2, 3))}, ValueError), + (gwyddion_align_rows_median, {"mask": np.array([[np.nan, 0.0], [0.0, 0.0]])}, ValueError), + (gwyddion_align_rows_median, {"mask": np.array([["mask"]])}, TypeError), + (gwyddion_align_rows_trimmed_mean, {"trim_fraction": -0.01}, ValueError), + (gwyddion_align_rows_trimmed_mean, {"trim_fraction": 0.51}, ValueError), + (gwyddion_align_rows_trimmed_mean_of_differences, {"trim_fraction": True}, TypeError), + ], +) +def test_public_validation_errors( + function: Callable[..., SPMChannel], kwargs: dict[str, object], error_type: type[Exception] +) -> None: + channel = _channel(np.ones((2, 2), dtype=np.float64), xreal=2.0, yreal=2.0) + with pytest.raises(error_type): + function(channel, **kwargs) + + +def test_each_public_call_delegates_to_the_private_entry_once( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[dict[str, object]] = [] + + def counted_entry(*args: object, **kwargs: object) -> SimpleNamespace: + calls.append(dict(kwargs)) + return SimpleNamespace(corrected=np.ones((2, 2), dtype=np.float64)) + + monkeypatch.setattr(leveling_module, "_gwyddion_align_rows_statistics_result", counted_entry) + channel = _channel(np.zeros((2, 2), dtype=np.float64), xreal=2.0, yreal=2.0) + for method, function in _FUNCTIONS.items(): + kwargs: dict[str, object] = {} + if method in {5, 6}: + kwargs["trim_fraction"] = 0.5 + output = function(channel, **kwargs) + assert output.data.flags.c_contiguous and output.data.dtype == np.float64 + assert len(calls) == 4 + assert [call["method"] for call in calls] == [ + leveling_module._GwyddionAlignRowsMethod.MEDIAN, + leveling_module._GwyddionAlignRowsMethod.MEDIAN_OF_DIFFERENCES, + leveling_module._GwyddionAlignRowsMethod.TRIMMED_MEAN, + leveling_module._GwyddionAlignRowsMethod.TRIMMED_MEAN_OF_DIFFERENCES, + ] + assert all("extract_background" not in call for call in calls) diff --git a/tests/core/test_gwyddion_align_rows_statistics_private.py b/tests/core/test_gwyddion_align_rows_statistics_private.py new file mode 100644 index 0000000..539f3a6 --- /dev/null +++ b/tests/core/test_gwyddion_align_rows_statistics_private.py @@ -0,0 +1,357 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import numpy as np +import pytest + +from spmkit.core.analysis._gwyddion_align_rows_statistics import ( + _gwyddion_align_rows_statistics_result, + _GwyddionAlignRowsDirection, + _GwyddionAlignRowsMethod, + _GwyddionAlignRowsStatisticsResult, + _GwyddionMaskMode, + _minimum_sample_count, + _paired_differences, + _selected_row_values, + _trimmed_mean_or_median, +) + +FIXTURE = Path(__file__).resolve().parents[1] / "validation/fixtures/gwyddion/align_rows_statistics" + + +def _load() -> tuple[dict[str, Any], dict[str, np.ndarray]]: + manifest = json.loads((FIXTURE / "align_rows_statistics_reference.json").read_text()) + with np.load(FIXTURE / "align_rows_statistics_reference.npz", allow_pickle=False) as archive: + arrays = {name: archive[name].copy(order="C") for name in archive.files} + return manifest, arrays + + +def _bits(array: np.ndarray) -> np.ndarray: + return np.ascontiguousarray(array, dtype=np.float64).view(np.uint64) + + +def _assert_bits(case_id: str, expected: np.ndarray, actual: np.ndarray) -> None: + mismatch = np.argwhere(_bits(expected) != _bits(actual)) + if not len(mismatch): + return + row, column = (int(value) for value in mismatch[0]) + raise AssertionError( + f"{case_id}: row={row}, column={column}, expected={_bits(expected)[row, column]:016x}, " + f"actual={_bits(actual)[row, column]:016x}" + ) + + +def _ulp_distance(left: np.uint64, right: np.uint64) -> int: + def ordered(value: int) -> int: + return (~value + 1) & ((1 << 64) - 1) if value >> 63 else value | (1 << 63) + + return abs(ordered(int(left)) - ordered(int(right))) + + +def _run(case: dict[str, Any], arrays: dict[str, np.ndarray]) -> _GwyddionAlignRowsStatisticsResult: + return _gwyddion_align_rows_statistics_result( + arrays[case["input_key"]], + method=case["method"], + masking_mode=case["masking_mode"], + direction=case["direction"], + trim_fraction=float.fromhex(case["trim_fraction_hex"]), + mask=None if case["mask_key"] is None else arrays[case["mask_key"]], + extract_background=case["extract_background_request"], + ) + + +def test_all_portable_v2_cases_are_bitwise_exact_and_non_mutating() -> None: + manifest, arrays = _load() + exact = backgrounds = mutation_matches = 0 + for case in manifest["cases"]: + input_data = arrays[case["input_key"]] + mask = None if case["mask_key"] is None else arrays[case["mask_key"]] + input_before = input_data.copy(order="C") + mask_before = None if mask is None else mask.copy(order="C") + first = _run(case, arrays) + second = _run(case, arrays) + _assert_bits( + case["case_identifier"], arrays[case["portable_corrected_key"]], first.corrected + ) + _assert_bits(case["case_identifier"] + "/repeat", first.corrected, second.corrected) + expected_corrections = np.array( + [int(value, 16) for value in case["portable_correction_sequence_bits"]], dtype=np.uint64 + ).view(np.float64) + _assert_bits( + case["case_identifier"] + "/corrections", + expected_corrections, + first.correction_sequence, + ) + assert first.corrected.dtype == np.float64 and first.corrected.flags.c_contiguous + assert ( + first.correction_sequence.dtype == np.float64 + and first.correction_sequence.flags.c_contiguous + ) + assert not np.shares_memory(first.corrected, input_data) + assert np.array_equal(_bits(input_data), _bits(input_before)) + if mask is not None: + assert mask_before is not None + assert np.array_equal(_bits(mask), _bits(mask_before)) + if case["extract_background_request"]: + assert first.background is not None + _assert_bits( + case["case_identifier"] + "/background", + arrays[case["portable_background_key"]], + first.background, + ) + reconstruction = np.empty_like(input_data) + for row in range(input_data.shape[0]): + for column in range(input_data.shape[1]): + reconstruction[row, column] = ( + input_data[row, column] - first.background[row, column] + ) + _assert_bits( + case["case_identifier"] + "/reconstruction", first.corrected, reconstruction + ) + backgrounds += first.background.size + else: + assert first.background is None + exact += first.corrected.size + changed = bool((_bits(first.corrected) != _bits(input_data)).any()) + mutation_matches += int(changed == case["installed_mutated"]) + assert exact == 3888 + assert backgrounds == 504 + assert mutation_matches == 64 + + +def test_installed_profile_divergence_policy_is_exactly_preserved() -> None: + manifest, arrays = _load() + exceptional = { + "median__plateaus_signed_zero__10", + "median_of_differences__irregular__11", + "trimmed_mean_of_differences__irregular__11", + } + finite_nonzero = signed_zero = exact = maximum_ulp = 0 + maximum_absolute = 0.0 + for case in manifest["cases"]: + portable = _run(case, arrays).corrected + installed = arrays[case["installed_corrected_key"]] + differing = _bits(portable) != _bits(installed) + if differing.any(): + assert case["case_identifier"] in exceptional + for row, column in np.argwhere(differing): + if portable[row, column] == installed[row, column] == 0.0: + signed_zero += 1 + else: + assert np.isfinite(portable[row, column]) and np.isfinite(installed[row, column]) + finite_nonzero += 1 + maximum_absolute = max( + maximum_absolute, abs(portable[row, column] - installed[row, column]) + ) + maximum_ulp = max( + maximum_ulp, + _ulp_distance(_bits(portable)[row, column], _bits(installed)[row, column]), + ) + exact += int((~differing).sum()) + if case["extract_background_request"]: + result = _run(case, arrays) + assert result.background is not None + _assert_bits( + case["case_identifier"] + "/installed-background", + arrays[case["installed_background_key"]], + result.background, + ) + assert exact == 3757 + assert finite_nonzero == 128 + assert signed_zero == 3 + assert maximum_absolute <= 5.329070518200751e-15 + assert maximum_ulp <= 144 + + +def test_mask_threshold_fallback_and_reduction_contracts() -> None: + data = np.array([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]], dtype=np.float64) + mask = np.array([[0.0, 0.5, 1.0], [-1.0, 0.5, 2.0]], dtype=np.float64) + include = _gwyddion_align_rows_statistics_result( + data, method=1, masking_mode=1, direction=0, trim_fraction=0.05, mask=mask + ) + exclude = _gwyddion_align_rows_statistics_result( + data, method=1, masking_mode=0, direction=0, trim_fraction=0.05, mask=mask + ) + ignored = _gwyddion_align_rows_statistics_result( + data, method=1, masking_mode=2, direction=0, trim_fraction=0.05, mask=mask + ) + assert not np.array_equal(_bits(include.corrected), _bits(exclude.corrected)) + assert _selected_row_values(data[0], mask[0], _GwyddionMaskMode.INCLUDE) == [2.0, 3.0] + assert _selected_row_values(data[0], mask[0], _GwyddionMaskMode.EXCLUDE) == [1.0, 2.0] + assert _selected_row_values(data[0], mask[0], _GwyddionMaskMode.IGNORE) == [1.0, 2.0, 3.0] + assert _paired_differences(data, mask, _GwyddionMaskMode.INCLUDE, 0) == [] + assert _paired_differences(data, mask, _GwyddionMaskMode.EXCLUDE, 0) == [9.0, 18.0] + assert ignored.correction_sequence.shape == (2,) + assert _minimum_sample_count(3) == 2 + assert _trimmed_mean_or_median([1.0, 2.0, 9.0], 0.0) == 4.0 + assert _trimmed_mean_or_median([1.0, 2.0, 9.0], 0.5) == 2.0 + assert _trimmed_mean_or_median(list(range(10)), 0.05) == 4.5 + assert _trimmed_mean_or_median(list(range(11)), 0.05) == 5.0 + no_mask = _gwyddion_align_rows_statistics_result( + data, method=2, masking_mode=0, direction=0, trim_fraction=0.05, mask=None + ) + no_mask_ignore = _gwyddion_align_rows_statistics_result( + data, method=2, masking_mode=2, direction=0, trim_fraction=0.05, mask=None + ) + _assert_bits("no_mask_mode", no_mask.corrected, no_mask_ignore.corrected) + assert _GwyddionMaskMode.EXCLUDE == 0 + assert _GwyddionMaskMode.INCLUDE == 1 + assert _GwyddionMaskMode.IGNORE == 2 + assert _GwyddionAlignRowsDirection.HORIZONTAL == 0 + assert _GwyddionAlignRowsDirection.VERTICAL == 1 + assert _GwyddionAlignRowsMethod.MEDIAN_OF_DIFFERENCES == 2 + + +def test_zero_one_selection_fallbacks_and_vertical_transpose_contract() -> None: + absolute_data = np.array( + [[0.0, 70.0, 80.0, 90.0], [10.0, 100.0, 80.0, 90.0], [30.0, 70.0, 80.0, 90.0]], + dtype=np.float64, + ) + absolute_mask = np.array( + [[1.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], + dtype=np.float64, + ) + absolute = _gwyddion_align_rows_statistics_result( + absolute_data, + method=1, + masking_mode=1, + direction=0, + trim_fraction=0.05, + mask=absolute_mask, + ) + _assert_bits( + "absolute_zero_one_fallback", + np.array([-30.0, 60.0, -30.0], dtype=np.float64), + absolute.correction_sequence, + ) + + difference_data = np.arange(12, dtype=np.float64).reshape(3, 4) + difference_mask = np.array( + [[2.0, 0.0, 0.0, 0.0], [2.0, 0.0, 0.0, 0.0], [2.0, 0.0, 0.0, 0.0]], + dtype=np.float64, + ) + difference = _gwyddion_align_rows_statistics_result( + difference_data, + method=2, + masking_mode=1, + direction=0, + trim_fraction=0.05, + mask=difference_mask, + ) + _assert_bits( + "difference_zero_one_fallback", + np.zeros(3, dtype=np.float64), + difference.correction_sequence, + ) + + vertical = _gwyddion_align_rows_statistics_result( + absolute_data, + method=5, + masking_mode=0, + direction=1, + trim_fraction=0.05, + mask=absolute_mask, + ) + transposed = _gwyddion_align_rows_statistics_result( + absolute_data.T, + method=5, + masking_mode=0, + direction=0, + trim_fraction=0.05, + mask=absolute_mask.T, + ) + _assert_bits("vertical_transpose", vertical.corrected, transposed.corrected.T) + + +@pytest.mark.parametrize( + "kwargs, error_type", + [ + ( + { + "data": np.array([1.0]), + "method": 1, + "masking_mode": 2, + "direction": 0, + "trim_fraction": 0.05, + }, + ValueError, + ), + ( + { + "data": np.array([[np.nan]]), + "method": 1, + "masking_mode": 2, + "direction": 0, + "trim_fraction": 0.05, + }, + ValueError, + ), + ( + { + "data": np.ones((2, 2)), + "method": 99, + "masking_mode": 2, + "direction": 0, + "trim_fraction": 0.05, + }, + ValueError, + ), + ( + { + "data": np.ones((2, 2)), + "method": 1, + "masking_mode": 3, + "direction": 0, + "trim_fraction": 0.05, + }, + ValueError, + ), + ( + { + "data": np.ones((2, 2)), + "method": 1, + "masking_mode": 2, + "direction": 7, + "trim_fraction": 0.05, + }, + ValueError, + ), + ( + { + "data": np.ones((2, 2)), + "method": 1, + "masking_mode": 2, + "direction": 0, + "trim_fraction": 0.6, + }, + ValueError, + ), + ( + { + "data": np.ones((2, 2)), + "method": 1, + "masking_mode": 2, + "direction": 0, + "trim_fraction": True, + }, + TypeError, + ), + ( + { + "data": np.ones((2, 2)), + "method": 1, + "masking_mode": 2, + "direction": 0, + "trim_fraction": 0.05, + "mask": np.ones((3, 2)), + }, + ValueError, + ), + ], +) +def test_invalid_contracts(kwargs: dict[str, Any], error_type: type[Exception]) -> None: + with pytest.raises(error_type): + _gwyddion_align_rows_statistics_result(**kwargs) diff --git a/tests/core/test_gwyddion_arc_revolution_background.py b/tests/core/test_gwyddion_arc_revolution_background.py new file mode 100644 index 0000000..de45bb0 --- /dev/null +++ b/tests/core/test_gwyddion_arc_revolution_background.py @@ -0,0 +1,559 @@ +"""Public-contract tests for Gwyddion-compatible Revolve Arc.""" + +from __future__ import annotations + +import inspect +from typing import get_args + +import numpy as np +import pytest + +import spmkit.core.analysis as analysis +import spmkit.core.analysis.background as background_module +from spmkit.core.analysis import ( + BackgroundResult, + GwyddionArcDirection, + analyze_gwyddion_arc_revolution_background, + estimate_gwyddion_arc_revolution_background, + remove_gwyddion_arc_revolution_background, +) +from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_result, +) +from spmkit.core.models import SPMChannel + +_ROUTES = [ + ("horizontal", False), + ("horizontal", True), + ("vertical", False), + ("vertical", True), + ("both", False), + ("both", True), +] + + +def _field() -> np.ndarray: + return np.array( + [ + [2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5], + [1.5, 1.75, 2.0, 2.25, 6.0, 2.75, 3.0], + [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5], + [0.5, 0.75, 1.0, -2.0, 1.5, 1.75, 2.0], + [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5], + ], + dtype=np.float64, + ) + + +def _channel( + data: np.ndarray | None = None, + *, + unit: str = "V", + x_range: float = 8.0e-6, + y_range: float = 5.0e-6, +) -> SPMChannel: + return SPMChannel( + name="Synthetic CPD", + data=_field() if data is None else data, + unit=unit, + x_range=x_range, + y_range=y_range, + direction="backward", + group="Validation group", + metadata={ + "source": "frozen synthetic contract", + "operator": "public-adapter-test", + }, + ) + + +def _assert_context_preserved( + source: SPMChannel, + result: SPMChannel, +) -> None: + assert result.name == source.name + assert result.unit == source.unit + assert result.x_range == source.x_range + assert result.y_range == source.y_range + assert result.direction == source.direction + assert result.group == source.group + assert result.metadata == source.metadata + assert result.metadata is not source.metadata + + +def _assert_array_contract(data: np.ndarray) -> None: + assert data.dtype == np.float64 + assert data.flags.c_contiguous + assert not data.flags.writeable + + +def _roundoff_bound(expected: np.ndarray) -> float: + scale = max( + 1.0, + float(np.max(np.abs(expected))), + ) + return 512.0 * np.finfo(np.float64).eps * scale + + +def test_public_exports_and_defaults_are_stable() -> None: + expected_names = { + "GwyddionArcDirection", + "estimate_gwyddion_arc_revolution_background", + "remove_gwyddion_arc_revolution_background", + "analyze_gwyddion_arc_revolution_background", + } + + assert expected_names <= set(analysis.__all__) + + for name in expected_names: + assert getattr(analysis, name) is not None + + assert set(get_args(GwyddionArcDirection)) == { + "horizontal", + "vertical", + "both", + } + + for function in ( + estimate_gwyddion_arc_revolution_background, + remove_gwyddion_arc_revolution_background, + analyze_gwyddion_arc_revolution_background, + ): + signature = inspect.signature(function) + assert signature.parameters["radius_px"].default == 20.0 + assert signature.parameters["direction"].default == "horizontal" + assert signature.parameters["inverted"].default is False + + +@pytest.mark.parametrize(("direction", "inverted"), _ROUTES) +def test_public_family_matches_authoritative_private_result( + direction: str, + inverted: bool, +) -> None: + channel = _channel() + original_data = channel.data.copy() + original_metadata = dict(channel.metadata) + + expected_background, expected_corrected = _gwyddion_arc_result( + channel.data, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + + estimated = estimate_gwyddion_arc_revolution_background( + channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + removed = remove_gwyddion_arc_revolution_background( + channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + analyzed = analyze_gwyddion_arc_revolution_background( + channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + + assert isinstance(analyzed, BackgroundResult) + + np.testing.assert_array_equal( + estimated.data, + expected_background, + ) + np.testing.assert_array_equal( + removed.data, + expected_corrected, + ) + np.testing.assert_array_equal( + analyzed.background.data, + expected_background, + ) + np.testing.assert_array_equal( + analyzed.corrected.data, + expected_corrected, + ) + np.testing.assert_array_equal( + analyzed.corrected.data + analyzed.background.data, + channel.data, + ) + + assert analyzed.method == "gwyddion_arc_revolution" + assert analyzed.parameters == { + "radius_px": 2.5, + "direction": direction, + "inverted": inverted, + } + + for result_channel in ( + estimated, + removed, + analyzed.background, + analyzed.corrected, + ): + _assert_context_preserved(channel, result_channel) + _assert_array_contract(result_channel.data) + + np.testing.assert_array_equal(channel.data, original_data) + assert channel.metadata == original_metadata + + payload = analyzed.to_dict() + assert payload["method"] == "gwyddion_arc_revolution" + assert payload["parameters"] == analyzed.parameters + assert payload["background"]["shape"] == [5, 7] # type: ignore[index] + assert payload["corrected"]["unit"] == "V" # type: ignore[index] + + +def test_analyze_executes_single_authoritative_result_route( + monkeypatch: pytest.MonkeyPatch, +) -> None: + call_count = 0 + original = background_module._gwyddion_arc_result + + def counted_result(*args: object, **kwargs: object) -> object: + nonlocal call_count + call_count += 1 + return original(*args, **kwargs) # type: ignore[arg-type] + + monkeypatch.setattr( + background_module, + "_gwyddion_arc_result", + counted_result, + ) + + result = analyze_gwyddion_arc_revolution_background( + _channel(), + 2.5, + direction="both", + inverted=True, + ) + + assert isinstance(result, BackgroundResult) + assert call_count == 1 + + +@pytest.mark.parametrize(("direction", "inverted"), _ROUTES) +def test_metamorphic_translation_and_positive_scale( + direction: str, + inverted: bool, +) -> None: + data = _field() + shift = 16.0 + scale = 8.0 + + base_channel = _channel(data) + shifted_channel = _channel(data + shift) + scaled_channel = _channel(data * scale) + + base_background = estimate_gwyddion_arc_revolution_background( + base_channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ).data + shifted_background = estimate_gwyddion_arc_revolution_background( + shifted_channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ).data + scaled_background = estimate_gwyddion_arc_revolution_background( + scaled_channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ).data + + base_corrected = remove_gwyddion_arc_revolution_background( + base_channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ).data + shifted_corrected = remove_gwyddion_arc_revolution_background( + shifted_channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ).data + scaled_corrected = remove_gwyddion_arc_revolution_background( + scaled_channel, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ).data + + expected_shifted_background = base_background + shift + expected_scaled_background = base_background * scale + expected_scaled_corrected = base_corrected * scale + + np.testing.assert_allclose( + shifted_background, + expected_shifted_background, + atol=_roundoff_bound(expected_shifted_background), + rtol=0.0, + ) + np.testing.assert_allclose( + shifted_corrected, + base_corrected, + atol=_roundoff_bound(base_corrected), + rtol=0.0, + ) + np.testing.assert_allclose( + scaled_background, + expected_scaled_background, + atol=_roundoff_bound(expected_scaled_background), + rtol=0.0, + ) + np.testing.assert_allclose( + scaled_corrected, + expected_scaled_corrected, + atol=_roundoff_bound(expected_scaled_corrected), + rtol=0.0, + ) + + +def test_units_and_lateral_ranges_are_numerically_irrelevant() -> None: + voltage = _channel( + unit="V", + x_range=1.0e-9, + y_range=2.0e-9, + ) + phase = _channel( + unit="deg", + x_range=0.25, + y_range=12.0, + ) + + voltage_result = analyze_gwyddion_arc_revolution_background( + voltage, + 2.5, + direction="both", + ) + phase_result = analyze_gwyddion_arc_revolution_background( + phase, + 2.5, + direction="both", + ) + + np.testing.assert_array_equal( + voltage_result.background.data, + phase_result.background.data, + ) + np.testing.assert_array_equal( + voltage_result.corrected.data, + phase_result.corrected.data, + ) + + assert voltage_result.background.unit == "V" + assert voltage_result.corrected.unit == "V" + assert phase_result.background.unit == "deg" + assert phase_result.corrected.unit == "deg" + + +@pytest.mark.parametrize("radius_px", [1.0, 1000.0, np.float64(20.0)]) +def test_public_radius_boundaries_are_accepted( + radius_px: object, +) -> None: + result = analyze_gwyddion_arc_revolution_background( + _channel(), + radius_px, # type: ignore[arg-type] + ) + + assert result.parameters["radius_px"] == float(radius_px) + + +@pytest.mark.parametrize( + "radius_px", + [ + 0.0, + -1.0, + 0.999999, + 1000.000001, + np.nan, + np.inf, + -np.inf, + ], +) +def test_invalid_radius_values_are_rejected( + radius_px: float, +) -> None: + with pytest.raises(ValueError): + estimate_gwyddion_arc_revolution_background( + _channel(), + radius_px, + ) + + +@pytest.mark.parametrize( + "radius_px", + [ + True, + "20", + 20.0 + 0.0j, + [20.0], + np.array([20.0]), + ], +) +def test_invalid_radius_types_are_rejected( + radius_px: object, +) -> None: + with pytest.raises(TypeError): + estimate_gwyddion_arc_revolution_background( + _channel(), + radius_px, # type: ignore[arg-type] + ) + + +@pytest.mark.parametrize( + "direction", + ["diagonal", "", 1, None], +) +def test_invalid_direction_is_rejected( + direction: object, +) -> None: + expected_exception = TypeError if not isinstance(direction, str) else ValueError + + with pytest.raises(expected_exception): + estimate_gwyddion_arc_revolution_background( + _channel(), + 2.5, + direction=direction, # type: ignore[arg-type] + ) + + +@pytest.mark.parametrize( + "inverted", + [0, 1, "yes", None], +) +def test_non_boolean_inversion_is_rejected( + inverted: object, +) -> None: + with pytest.raises(TypeError): + estimate_gwyddion_arc_revolution_background( + _channel(), + 2.5, + inverted=inverted, # type: ignore[arg-type] + ) + + +@pytest.mark.parametrize( + "data", + [ + np.array([[1.0, np.nan]]), + np.array([[1.0, np.inf]]), + np.array([[1.0 + 0.0j, 2.0 + 0.0j]]), + np.array([1.0, 2.0]), + np.empty((0, 3)), + ], +) +def test_invalid_channel_data_is_rejected( + data: np.ndarray, +) -> None: + expected_exception = TypeError if np.iscomplexobj(data) else ValueError + + with pytest.raises(expected_exception): + estimate_gwyddion_arc_revolution_background( + _channel(data), + 2.5, + ) + + +@pytest.mark.parametrize( + ("shape", "direction"), + [ + ((1, 1), "horizontal"), + ((5, 1), "horizontal"), + ((1, 5), "vertical"), + ((1, 1), "both"), + ], +) +def test_single_sample_processing_axis_has_safe_identity_semantics( + shape: tuple[int, int], + direction: str, +) -> None: + data = np.arange( + shape[0] * shape[1], + dtype=np.float64, + ).reshape(shape) + channel = _channel(data) + + result = analyze_gwyddion_arc_revolution_background( + channel, + 2.5, + direction=direction, # type: ignore[arg-type] + ) + + np.testing.assert_array_equal( + result.background.data, + data, + ) + np.testing.assert_array_equal( + result.corrected.data, + np.zeros_like(data), + ) + + +@pytest.mark.parametrize( + ("shape", "direction"), + [ + ((1, 5), "horizontal"), + ((5, 1), "vertical"), + ], +) +def test_singleton_orthogonal_axis_does_not_force_identity( + shape: tuple[int, int], + direction: str, +) -> None: + """A singleton orthogonal dimension is not a singleton processed axis.""" + data = np.arange( + shape[0] * shape[1], + dtype=np.float64, + ).reshape(shape) + channel = _channel(data) + + result = analyze_gwyddion_arc_revolution_background( + channel, + 2.5, + direction=direction, # type: ignore[arg-type] + ) + + assert not np.array_equal( + result.background.data, + data, + ) + np.testing.assert_array_equal( + result.corrected.data + result.background.data, + data, + ) + + +def test_public_result_is_deterministic() -> None: + channel = _channel() + + first = analyze_gwyddion_arc_revolution_background( + channel, + 2.5, + direction="both", + inverted=True, + ) + second = analyze_gwyddion_arc_revolution_background( + channel, + 2.5, + direction="both", + inverted=True, + ) + + np.testing.assert_array_equal( + first.background.data, + second.background.data, + ) + np.testing.assert_array_equal( + first.corrected.data, + second.corrected.data, + ) diff --git a/tests/core/test_gwyddion_arc_revolution_kernel.py b/tests/core/test_gwyddion_arc_revolution_kernel.py new file mode 100644 index 0000000..6c54ee8 --- /dev/null +++ b/tests/core/test_gwyddion_arc_revolution_kernel.py @@ -0,0 +1,725 @@ +"""Tests for the Gwyddion-compatible Revolve Arc numerical kernel.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_round_positive, + _make_gwyddion_arc, +) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0.0, 0), + (1.49, 1), + (1.5, 2), + (2.49, 2), + (2.5, 3), + (3.5, 4), + (4.5, 5), + (7.999999999999, 8), + (8.0, 8), + ], +) +def test_gwyddion_round_uses_half_up_semantics( + value: float, + expected: int, +) -> None: + assert _gwyddion_round_positive(value) == expected + + +@pytest.mark.parametrize( + "value", + [True, "2.5", [2.5], 1.0 + 1.0j], +) +def test_gwyddion_round_rejects_non_real_scalars(value: object) -> None: + with pytest.raises(TypeError): + _gwyddion_round_positive(value) + + +@pytest.mark.parametrize( + "value", + [-1.0, np.nan, np.inf, -np.inf], +) +def test_gwyddion_round_rejects_invalid_values(value: float) -> None: + with pytest.raises(ValueError): + _gwyddion_round_positive(value) + + +@pytest.mark.parametrize( + ("radius", "maxres", "expected"), + [ + ( + 1.0, + 7, + np.array( + [ + 1.0, + 0.0, + 1.0, + ] + ), + ), + ( + 2.49, + 7, + np.array( + [ + 0.40430786866297319, + 0.084187639942432058, + 0.0, + 0.084187639942432058, + 0.40430786866297319, + ] + ), + ), + ( + 2.5, + 7, + np.array( + [ + 1.0, + 0.40000000000000013, + 0.083484861008832012, + 0.0, + 0.083484861008832012, + 0.40000000000000013, + 1.0, + ] + ), + ), + ( + 4.0, + 16, + np.array( + [ + 1.0, + 0.33856217223385232, + 0.1339745962155614, + 0.031754163448145745, + 0.0, + 0.031754163448145745, + 0.1339745962155614, + 0.33856217223385232, + 1.0, + ] + ), + ), + ( + 20.0, + 3, + np.array( + [ + 0.011314003335740508, + 0.0050125628933800348, + 0.0012507822280910519, + 0.0, + 0.0012507822280910519, + 0.0050125628933800348, + 0.011314003335740508, + ] + ), + ), + ( + 1000.0, + 7, + np.array( + [ + 2.4500300132353065e-05, + 1.8000162002915999e-05, + 1.2500078125976563e-05, + 8.000032000256001e-06, + 4.5000101250455631e-06, + 2.0000020000040001e-06, + 5.0000012500006248e-07, + 0.0, + 5.0000012500006248e-07, + 2.0000020000040001e-06, + 4.5000101250455631e-06, + 8.000032000256001e-06, + 1.2500078125976563e-05, + 1.8000162002915999e-05, + 2.4500300132353065e-05, + ] + ), + ), + ], +) +def test_make_arc_matches_gwyddion_2_71_reference( + radius: float, + maxres: int, + expected: np.ndarray, +) -> None: + result = _make_gwyddion_arc(radius, maxres) + + np.testing.assert_allclose( + result, + expected, + atol=5e-16, + rtol=0.0, + ) + + +def test_half_integer_radius_changes_arc_resolution() -> None: + below_half = _make_gwyddion_arc(2.49, 7) + half_up = _make_gwyddion_arc(2.5, 7) + + assert below_half.shape == (5,) + assert half_up.shape == (7,) + + +def test_arc_is_symmetric_centered_float64_and_read_only() -> None: + arc = _make_gwyddion_arc(4.0, 16) + + assert arc.dtype == np.float64 + assert arc.shape == (9,) + assert arc[arc.size // 2] == 0.0 + np.testing.assert_array_equal(arc, arc[::-1]) + assert not arc.flags.writeable + + +@pytest.mark.parametrize( + "radius", + [0.0, -1.0, np.nan, np.inf, -np.inf], +) +def test_make_arc_rejects_invalid_radius(radius: float) -> None: + with pytest.raises(ValueError): + _make_gwyddion_arc(radius, 7) + + +@pytest.mark.parametrize( + "radius", + [True, "2.5", [2.5], 1.0 + 1.0j], +) +def test_make_arc_rejects_non_real_radius(radius: object) -> None: + with pytest.raises(TypeError): + _make_gwyddion_arc(radius, 7) + + +@pytest.mark.parametrize( + "maxres", + [0, -1], +) +def test_make_arc_rejects_non_positive_resolution(maxres: int) -> None: + with pytest.raises(ValueError): + _make_gwyddion_arc(2.5, maxres) + + +@pytest.mark.parametrize( + "maxres", + [True, 3.5, "7", [7]], +) +def test_make_arc_rejects_non_integer_resolution(maxres: object) -> None: + with pytest.raises(TypeError): + _make_gwyddion_arc(2.5, maxres) + + +def _asymmetric_reference_field() -> np.ndarray: + data = np.empty((5, 7), dtype=float) + + for row in range(5): + for column in range(7): + value = ( + 2.0 + + 0.12 * column + - 0.07 * row + + 0.015 * column * row + + 0.03 * np.sin(0.9 * column + 0.4 * row) + ) + + if row == 1 and column == 4: + value += 3.5 + if row == 3 and column == 2: + value -= 4.0 + if row == 4 and column == 6: + value += 1.2 + + data[row, column] = value + + return data + + +def _independent_truncated_moving_sums( + row: np.ndarray, + size: int, +) -> tuple[np.ndarray, np.ndarray]: + left_half = size // 2 + right_half = 0 if size == 0 else (size - 1) // 2 + + sums = np.empty(row.size, dtype=float) + squared_sums = np.empty(row.size, dtype=float) + + for index in range(row.size): + start = max(0, index - left_half) + stop = min(row.size, index + right_half + 1) + window = row[start:stop] + + sums[index] = sum(float(value) for value in window) + squared_sums[index] = sum(float(value) ** 2 for value in window) + + return sums, squared_sums + + +def test_population_rms_matches_gwyddion_reference() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_population_rms, + ) + + data = _asymmetric_reference_field() + rms = _gwyddion_population_rms(data) + scale = rms / np.sqrt(2.0 / 3.0 - np.pi / 16.0) + + assert scale == pytest.approx( + 1.485841488666283, + abs=2e-15, + rel=0.0, + ) + + +@pytest.mark.parametrize("size", range(0, 6)) +def test_moving_sums_match_independent_truncated_oracle(size: int) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import _moving_sums + + row = np.array([1.0, -2.0, 4.0, 8.0, -1.0, 3.0]) + expected_sum, expected_sum2 = _independent_truncated_moving_sums( + row, + size, + ) + + result_sum, result_sum2 = _moving_sums( + row, + size, + ) + + np.testing.assert_array_equal(result_sum, expected_sum) + np.testing.assert_array_equal(result_sum2, expected_sum2) + + +@pytest.mark.parametrize( + ("size", "expected_sum", "expected_sum2"), + [ + ( + 6, + np.array([3.0, 11.0, 10.0, 10.0, 9.0, 11.0]), + np.array([21.0, 85.0, 86.0, 86.0, 85.0, 81.0]), + ), + ( + 7, + np.array([11.0, 10.0, 10.0, 10.0, 9.0, 11.0]), + np.array([85.0, 86.0, 86.0, 86.0, 85.0, 81.0]), + ), + ], +) +def test_moving_sums_match_gwyddion_whale_branch( + size: int, + expected_sum: np.ndarray, + expected_sum2: np.ndarray, +) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import _moving_sums + + row = np.array([1.0, -2.0, 4.0, 8.0, -1.0, 3.0]) + + result_sum, result_sum2 = _moving_sums( + row, + size, + ) + + np.testing.assert_array_equal(result_sum, expected_sum) + np.testing.assert_array_equal(result_sum2, expected_sum2) + + +def test_moving_sums_reproduce_reference_large_window_shortcut() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import _moving_sums + + row = np.array([1.0, -2.0, 4.0, 8.0, -1.0, 3.0]) + result_sum, result_sum2 = _moving_sums(row, 13) + + np.testing.assert_array_equal( + result_sum, + np.ones(6), + ) + np.testing.assert_array_equal( + result_sum2, + np.ones(6), + ) + + +def test_moving_sums_reject_reference_undefined_memory_case() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import _moving_sums + + with pytest.raises( + ValueError, + match="undefined", + ): + _moving_sums( + np.array([4.0]), + 1, + ) + + +def test_horizontal_kernel_matches_asymmetric_gwyddion_reference() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_horizontal, + ) + + expected = np.array( + [ + [ + 2.0, + 2.1240452701624606, + 2.2675450774512851, + 2.3728213964070148, + 2.4667243867011543, + 2.570674096470047, + 2.6947193666325076, + ], + [ + 1.9416825502692594, + 2.0657278204317202, + 2.2179520157249768, + 2.3362474198729988, + 2.4602926900354594, + 2.5755264216212703, + 2.6995716917837309, + ], + [ + 1.8815206827269855, + 2.0055659528894463, + 2.1637952144760346, + 2.299476503169311, + 2.4235217733317715, + 2.5554972079457752, + 2.7090772468957436, + ], + [ + -1.2814298042916905, + -1.7517211295957433, + -1.8757663997582039, + -1.7517211295957433, + -1.2814298042916905, + -0.38992491109192096, + 2.7225247038845315, + ], + [ + 1.7499872080912451, + 1.8740324782537057, + 2.0419994344855796, + 2.1963790371016558, + 2.3565602920599771, + 2.5375416304908565, + 2.738580395034298, + ], + ] + ) + + result = _gwyddion_arc_horizontal( + _asymmetric_reference_field(), + 2.5, + ) + + np.testing.assert_allclose( + result, + expected, + atol=5e-15, + rtol=0.0, + ) + + +def test_horizontal_kernel_preserves_constant_field() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_horizontal, + ) + + data = np.full((2, 5), 3.25) + result = _gwyddion_arc_horizontal(data, 2.5) + + np.testing.assert_array_equal(result, data) + + +def test_horizontal_kernel_matches_single_row_reference() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_horizontal, + ) + + data = np.array([[0.0, 1.0, -2.0, 4.0, 1.0]]) + expected = np.array( + [ + [ + 0.0, + -1.2800008456684795, + -2.0, + -1.2800008456684795, + 0.82747338686665239, + ] + ] + ) + + result = _gwyddion_arc_horizontal(data, 1.5) + + np.testing.assert_allclose( + result, + expected, + atol=5e-15, + rtol=0.0, + ) + + +def test_horizontal_kernel_matches_large_radius_reference() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_horizontal, + ) + + data = np.array( + [ + [0.0, 1.0, 4.0, 2.0, -1.0, 3.0, 0.0], + [2.0, 2.5, 1.0, 0.0, -2.0, 1.0, 4.0], + ] + ) + expected = np.array( + [ + [ + -0.99997994598602924, + -0.9999887196368823, + -0.99999498651154795, + -0.99999874662882704, + -1.0, + -0.99999874662882704, + -0.99999498651154795, + ], + [ + -1.9999799459860292, + -1.9999887196368822, + -1.9999949865115478, + -1.9999987466288269, + -2.0, + -1.9999987466288269, + -1.9999949865115478, + ], + ] + ) + + result = _gwyddion_arc_horizontal(data, 1000.0) + + np.testing.assert_allclose( + result, + expected, + atol=5e-15, + rtol=0.0, + ) + + +def test_horizontal_kernel_defines_one_sample_axis_as_identity() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_horizontal, + ) + + data = np.array([[0.0], [-1.0], [2.0], [5.0]]) + result = _gwyddion_arc_horizontal(data, 2.5) + + np.testing.assert_array_equal(result, data) + + +def test_horizontal_kernel_does_not_mutate_input_and_returns_read_only() -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_horizontal, + ) + + data = _asymmetric_reference_field() + original = data.copy() + + result = _gwyddion_arc_horizontal(data, 2.5) + + np.testing.assert_array_equal(data, original) + assert result.dtype == np.float64 + assert not result.flags.writeable + + +@pytest.mark.parametrize( + ("direction", "inverted"), + [ + ("horizontal", False), + ("horizontal", True), + ("vertical", False), + ("vertical", True), + ("both", False), + ("both", True), + ], +) +def test_directional_background_matches_explicit_composition( + direction: str, + inverted: bool, +) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_background, + _gwyddion_arc_horizontal, + ) + + data = _asymmetric_reference_field() + working = -data if inverted else data + + if direction == "horizontal": + expected = _gwyddion_arc_horizontal( + working, + 2.5, + ) + elif direction == "vertical": + expected = _gwyddion_arc_horizontal( + working.T, + 2.5, + ).T + else: + horizontal = _gwyddion_arc_horizontal( + working, + 2.5, + ) + expected = _gwyddion_arc_horizontal( + horizontal.T, + 2.5, + ).T + + if inverted: + expected = -expected + + result = _gwyddion_arc_background( + data, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + + np.testing.assert_array_equal(result, expected) + + +@pytest.mark.parametrize( + ("direction", "inverted"), + [ + ("horizontal", False), + ("horizontal", True), + ("vertical", False), + ("vertical", True), + ("both", False), + ("both", True), + ], +) +def test_directional_corrected_reconstructs_input( + direction: str, + inverted: bool, +) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_background, + _gwyddion_arc_corrected, + ) + + data = _asymmetric_reference_field() + original = data.copy() + + background = _gwyddion_arc_background( + data, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + corrected = _gwyddion_arc_corrected( + data, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + + np.testing.assert_array_equal(data, original) + np.testing.assert_allclose( + corrected + background, + data, + atol=5e-15, + rtol=0.0, + ) + + assert background.dtype == np.float64 + assert corrected.dtype == np.float64 + assert background.flags.c_contiguous + assert corrected.flags.c_contiguous + assert not background.flags.writeable + assert not corrected.flags.writeable + + +@pytest.mark.parametrize( + "direction", + ["horizontal", "vertical", "both"], +) +@pytest.mark.parametrize( + "inverted", + [False, True], +) +def test_directional_one_by_one_field_is_identity( + direction: str, + inverted: bool, +) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_background, + _gwyddion_arc_corrected, + ) + + data = np.array([[4.25]]) + + background = _gwyddion_arc_background( + data, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + corrected = _gwyddion_arc_corrected( + data, + 2.5, + direction=direction, # type: ignore[arg-type] + inverted=inverted, + ) + + np.testing.assert_array_equal(background, data) + np.testing.assert_array_equal( + corrected, + np.zeros_like(data), + ) + + +@pytest.mark.parametrize( + "direction", + ["diagonal", "", 1], +) +def test_directional_background_rejects_invalid_direction( + direction: object, +) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_background, + ) + + expected_exception = TypeError if not isinstance(direction, str) else ValueError + + with pytest.raises(expected_exception): + _gwyddion_arc_background( + np.ones((2, 3)), + 2.5, + direction=direction, # type: ignore[arg-type] + ) + + +@pytest.mark.parametrize( + "inverted", + [0, 1, "yes", None], +) +def test_directional_background_rejects_non_boolean_inversion( + inverted: object, +) -> None: + from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_background, + ) + + with pytest.raises(TypeError): + _gwyddion_arc_background( + np.ones((2, 3)), + 2.5, + inverted=inverted, # type: ignore[arg-type] + ) diff --git a/tests/core/test_gwyddion_flat_disc_morphology.py b/tests/core/test_gwyddion_flat_disc_morphology.py new file mode 100644 index 0000000..7fa8aac --- /dev/null +++ b/tests/core/test_gwyddion_flat_disc_morphology.py @@ -0,0 +1,193 @@ +"""Public-contract tests for Gwyddion 2.71 flat-disc morphology.""" + +from __future__ import annotations + +import inspect +import json +from collections.abc import Callable +from pathlib import Path + +import numpy as np +import pytest + +import spmkit.core.analysis as analysis +import spmkit.core.analysis.background as background_module +from spmkit.core.analysis import ( + gwyddion_flat_disc_closing, + gwyddion_flat_disc_opening, +) +from spmkit.core.models import SPMChannel + +_FIXTURE_DIRECTORY = ( + Path(__file__).resolve().parents[1] + / "validation" + / "fixtures" + / "gwyddion" + / "flat_disc_morphology" +) +_FIXTURE_PATH = _FIXTURE_DIRECTORY / "flat_disc_morphology_reference.npz" +_MANIFEST_PATH = _FIXTURE_DIRECTORY / "flat_disc_morphology_reference.json" +_OPERATIONS: tuple[Callable[..., SPMChannel], ...] = ( + gwyddion_flat_disc_opening, + gwyddion_flat_disc_closing, +) + + +def _manifest() -> dict[str, object]: + return json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + + +def _channel(data: np.ndarray) -> SPMChannel: + return SPMChannel( + name="Flat-disc fixture", + data=data, + unit="V", + x_range=8.5e-6, + y_range=6.5e-6, + direction="backward", + group="Frozen morphology evidence", + metadata={"source": "gwyddion-2.71-flat-disc", "context": {"id": 7}}, + ) + + +def _ordered_uint64(bits: int) -> int: + sign_bit = 1 << 63 + return ((~bits + 1) & ((1 << 64) - 1)) if bits & sign_bit else bits | sign_bit + + +def _assert_bitwise_equal( + actual: np.ndarray, + expected: np.ndarray, + *, + case_id: str, + operation: str, +) -> None: + actual_bits = actual.view(np.uint64) + expected_bits = expected.view(np.uint64) + if np.array_equal(actual_bits, expected_bits): + return + + row, column = np.argwhere(actual_bits != expected_bits)[0] + actual_bits_value = int(actual_bits[row, column]) + expected_bits_value = int(expected_bits[row, column]) + ulp_distance = abs(_ordered_uint64(actual_bits_value) - _ordered_uint64(expected_bits_value)) + pytest.fail( + f"case={case_id} operation={operation} coordinate=({row}, {column}) " + f"expected={expected[row, column]!r} actual={actual[row, column]!r} " + f"expected_uint64={expected_bits_value} actual_uint64={actual_bits_value} " + f"absolute_difference={abs(actual[row, column] - expected[row, column])!r} " + f"ulp_distance={ulp_distance}" + ) + + +def test_public_exports_signature_and_parameter_contract() -> None: + expected_names = {"gwyddion_flat_disc_opening", "gwyddion_flat_disc_closing"} + assert expected_names <= set(analysis.__all__) + for name in expected_names: + assert getattr(analysis, name) is not None + + for operation in _OPERATIONS: + signature = inspect.signature(operation) + assert list(signature.parameters) == ["channel", "size_px"] + assert signature.parameters["size_px"].default == 5 + assert signature.parameters["size_px"].kind is inspect.Parameter.KEYWORD_ONLY + assert "border" not in signature.parameters + assert "shape" not in signature.parameters + assert "rank" not in signature.parameters + assert "backend" not in signature.parameters + + for private_name in ( + "_GwyddionFlatDiscKernelSpec", + "_GwyddionFlatDiscMorphologyResult", + "_gwyddion_flat_disc_kernel", + "_gwyddion_flat_disc_extremum", + "_gwyddion_flat_disc_morphology_result", + ): + assert private_name not in analysis.__all__ + assert not hasattr(analysis, private_name) + + +@pytest.mark.parametrize("case", _manifest()["cases"], ids=lambda case: case["case_id"]) +def test_public_opening_and_closing_are_bitwise_exact(case: dict[str, object]) -> None: + with np.load(_FIXTURE_PATH, allow_pickle=False) as archive: + source_data = np.array(archive[case["input_key"]], dtype=np.float64, order="C", copy=True) + original_bits = source_data.view(np.uint64).copy() + source = _channel(source_data) + for size_entry in case["sizes"]: + size_px = size_entry["size_px"] + opening = gwyddion_flat_disc_opening(source, size_px=size_px) + closing = gwyddion_flat_disc_closing(source, size_px=size_px) + _assert_bitwise_equal( + opening.data, + archive[size_entry["opening_key"]], + case_id=case["case_id"], + operation="opening", + ) + _assert_bitwise_equal( + closing.data, + archive[size_entry["closing_key"]], + case_id=case["case_id"], + operation="closing", + ) + for output in (opening, closing): + assert output.data.dtype == np.float64 + assert output.data.flags.c_contiguous + assert output.data.shape == source.data.shape + assert np.isfinite(output.data).all() + assert not np.shares_memory(output.data, source.data) + assert not np.shares_memory(opening.data, closing.data) + assert np.array_equal(source.data.view(np.uint64), original_bits) + + +def test_context_and_metadata_are_preserved_without_sharing() -> None: + with np.load(_FIXTURE_PATH, allow_pickle=False) as archive: + source = _channel(np.array(archive["input__wide_large_gradient"], copy=True, order="C")) + opening = gwyddion_flat_disc_opening(source, size_px=3) + closing = gwyddion_flat_disc_closing(source, size_px=3) + for output in (opening, closing): + assert output.name == source.name + assert output.unit == source.unit + assert output.x_range == source.x_range + assert output.y_range == source.y_range + assert output.direction == source.direction + assert output.group == source.group + assert output.metadata == source.metadata + assert output.metadata is not source.metadata + opening.metadata["new_key"] = True + assert "new_key" not in source.metadata + assert "new_key" not in closing.metadata + + +@pytest.mark.parametrize("operation", _OPERATIONS) +@pytest.mark.parametrize("size_px", [True, np.array(2), 0, 32, 10**100, 2.0, "2"]) +def test_validation_is_delegated(operation: Callable[..., SPMChannel], size_px: object) -> None: + data = np.ones((3, 4), dtype=np.float64) + expected = TypeError if isinstance(size_px, (bool, np.ndarray, float, str)) else ValueError + with pytest.raises(expected): + operation(_channel(data), size_px=size_px) + + +@pytest.mark.parametrize("operation", _OPERATIONS) +def test_nonfinite_data_is_rejected(operation: Callable[..., SPMChannel]) -> None: + data = np.ones((2, 3), dtype=np.float64) + data[0, 1] = np.nan + with pytest.raises(ValueError, match="finite"): + operation(_channel(data), size_px=3) + + +@pytest.mark.parametrize("operation", _OPERATIONS) +def test_each_public_call_invokes_private_entry_once( + monkeypatch: pytest.MonkeyPatch, + operation: Callable[..., SPMChannel], +) -> None: + calls = 0 + original = background_module._gwyddion_flat_disc_morphology_result + + def counted_entry(data: object, size_px: object) -> object: + nonlocal calls + calls += 1 + return original(data, size_px) + + monkeypatch.setattr(background_module, "_gwyddion_flat_disc_morphology_result", counted_entry) + operation(_channel(np.arange(12, dtype=np.float64).reshape(3, 4)), size_px=4) + assert calls == 1 diff --git a/tests/core/test_gwyddion_flat_disc_morphology_private.py b/tests/core/test_gwyddion_flat_disc_morphology_private.py new file mode 100644 index 0000000..7c73740 --- /dev/null +++ b/tests/core/test_gwyddion_flat_disc_morphology_private.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pytest + +from spmkit.core.analysis._gwyddion_flat_disc_morphology import ( + Plan, + _build_requirement, + _gwyddion_flat_disc_kernel, + _gwyddion_flat_disc_morphology_result, + _segments, + _validated_gwyddion_flat_disc_size, +) + +FIXTURE = Path(__file__).resolve().parents[1] / "validation/fixtures/gwyddion/flat_disc_morphology" + + +def test_kernel_inventory_and_validation() -> None: + assert [_gwyddion_flat_disc_kernel(size).kernel_active_count for size in range(2, 32)][0:4] == [ + 4, + 9, + 12, + 21, + ] + assert _gwyddion_flat_disc_kernel(30).kernel_active_count == 716 + assert _gwyddion_flat_disc_kernel(31).kernel_active_count == 749 + assert isinstance(_validated_gwyddion_flat_disc_size(np.uint8(2)), int) + for value in (True, np.array(2), 2.0, "2"): + with pytest.raises(TypeError): + _validated_gwyddion_flat_disc_size(value) + for value in (1, 32, 10**100): + with pytest.raises(ValueError): + _validated_gwyddion_flat_disc_size(value) + + +def test_frozen_opening_and_closing_are_bitwise_exact() -> None: + manifest = json.loads((FIXTURE / "flat_disc_morphology_reference.json").read_text()) + with np.load(FIXTURE / "flat_disc_morphology_reference.npz", allow_pickle=False) as archive: + for case in manifest["cases"]: + input_data = archive[case["input_key"]].copy(order="C") + before = input_data.copy(order="C") + for size in case["sizes"]: + result = _gwyddion_flat_disc_morphology_result(input_data, size["size_px"]) + for operation in ("opening", "closing"): + expected = archive[size[f"{operation}_key"]] + actual = getattr(result, operation) + assert np.array_equal(actual.view(np.uint64), expected.view(np.uint64)), ( + case["case_id"], + operation, + size["size_px"], + ) + assert actual.dtype == np.float64 and actual.flags.c_contiguous + assert not np.shares_memory(result.opening, result.closing) + assert np.array_equal(input_data.view(np.uint64), before.view(np.uint64)) + + +def test_input_contract() -> None: + for value in (np.array([]), np.array([1.0]), np.zeros((1, 1, 1)), np.array([[np.nan]])): + with pytest.raises(ValueError): + _gwyddion_flat_disc_morphology_result(value, 2) + + +def _requirement_signature(plan: Plan) -> tuple[tuple[object, ...], ...]: + rows: list[tuple[object, ...]] = [] + for kind, mapping in (("each", plan.each), ("even", plan.even)): + for length, requirement in sorted(mapping.items()): + rows.append( + ( + kind, + length, + requirement.needed, + requirement.sublen1, + requirement.sublen2, + requirement.even_odd, + requirement.even_even, + ) + ) + return tuple(rows) + + +def test_requirement_tree_is_complete_and_deterministic() -> None: + lengths = {int(segment[2]) for size_px in range(2, 32) for segment in _segments(size_px, False)} + first = Plan() + for length in sorted(lengths): + _build_requirement(first, length, False) + second = Plan() + for length in sorted(lengths): + _build_requirement(second, length, False) + + assert _requirement_signature(first) == _requirement_signature(second) + for mapping in (first.each, first.even): + for length, requirement in mapping.items(): + if not requirement.needed or length == 1: + continue + assert requirement.sublen1 > 0 + assert requirement.sublen2 > 0 + assert requirement.sublen1 + requirement.sublen2 == length + + +def test_singleton_and_thin_fields_execute_all_sizes() -> None: + for shape in ((1, 1), (1, 7), (7, 1)): + field = np.arange(np.prod(shape), dtype=np.float64).reshape(shape) + for size_px in range(2, 32): + result = _gwyddion_flat_disc_morphology_result(field, size_px) + assert result.opening.shape == shape + assert result.closing.shape == shape diff --git a/tests/core/test_gwyddion_median_background.py b/tests/core/test_gwyddion_median_background.py new file mode 100644 index 0000000..f5ecac6 --- /dev/null +++ b/tests/core/test_gwyddion_median_background.py @@ -0,0 +1,340 @@ +"""Public-contract tests for Gwyddion 2.71 Median Background.""" + +from __future__ import annotations + +import inspect +import json +from collections.abc import Callable +from pathlib import Path + +import numpy as np +import pytest + +import spmkit.core.analysis as analysis +import spmkit.core.analysis.background as background_module +from spmkit.core.analysis import ( + BackgroundResult, + analyze_gwyddion_median_background, + estimate_gwyddion_median_background, + remove_gwyddion_median_background, +) +from spmkit.core.analysis._median_background import _gwyddion_median_background_result +from spmkit.core.models import SPMChannel + +_FIXTURE_DIRECTORY = ( + Path(__file__).resolve().parents[1] + / "validation" + / "fixtures" + / "gwyddion" + / "median_background" +) +_FIXTURE_PATH = _FIXTURE_DIRECTORY / "median_background_reference.npz" +_MANIFEST_PATH = _FIXTURE_DIRECTORY / "median_background_reference.json" +_PUBLIC_OPERATIONS: tuple[Callable[[SPMChannel, object], object], ...] = ( + estimate_gwyddion_median_background, + remove_gwyddion_median_background, + analyze_gwyddion_median_background, +) + + +def _manifest() -> dict[str, object]: + return json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + + +def _cases() -> tuple[dict[str, object], ...]: + manifest = _manifest() + cases = manifest["cases"] + assert isinstance(cases, list) + return tuple(cases) + + +def _case(name: str) -> dict[str, object]: + return next(case for case in _cases() if case["name"] == name) + + +def _case_arrays(case: dict[str, object]) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + arrays = case["arrays"] + assert isinstance(arrays, dict) + with np.load(_FIXTURE_PATH, allow_pickle=False) as archive: + return ( + np.array(archive[arrays["input"]], dtype=np.float64, order="C", copy=True), + np.array(archive[arrays["background"]], dtype=np.float64, order="C", copy=True), + np.array(archive[arrays["corrected"]], dtype=np.float64, order="C", copy=True), + ) + + +def _channel(data: np.ndarray) -> SPMChannel: + return SPMChannel( + name="Median Background fixture", + data=data, + unit="V", + x_range=9.5e-6, + y_range=6.5e-6, + direction="backward", + group="Frozen external evidence", + metadata={ + "source": "gwyddion-2.71-median-background", + "context": {"campaign": "frozen"}, + }, + ) + + +def _ordered_bits(bits: int) -> int: + sign_bit = 1 << 63 + return ((~bits + 1) & ((1 << 64) - 1)) if bits & sign_bit else bits | sign_bit + + +def _assert_bitwise_equal( + actual: np.ndarray, + expected: np.ndarray, + *, + case: str, + operation: str, + array: str, +) -> None: + actual_bits = actual.view(np.uint64) + expected_bits = expected.view(np.uint64) + if np.array_equal(actual_bits, expected_bits): + return + + row, column = np.argwhere(actual_bits != expected_bits)[0] + actual_bit_value = int(actual_bits[row, column]) + expected_bit_value = int(expected_bits[row, column]) + ulp_distance = abs(_ordered_bits(actual_bit_value) - _ordered_bits(expected_bit_value)) + pytest.fail( + f"case={case} operation={operation} array={array} " + f"coordinate=({row}, {column}) expected={expected[row, column]!r} " + f"actual={actual[row, column]!r} expected_uint64={expected_bit_value} " + f"actual_uint64={actual_bit_value} " + f"absolute_difference={abs(actual[row, column] - expected[row, column])!r} " + f"ulp_distance={ulp_distance}" + ) + + +def _expected_parameters(case: dict[str, object]) -> dict[str, object]: + return { + "radius_px": case["radius"], + "kernel_resolution": case["kernel_resolution"], + "kernel_active_count": case["kernel_active_count"], + "rank_index": case["rank_index"], + "rank_backend_reference": case["rank_backend_reference"], + "border_policy": "gwyddion_border_extend", + "kernel_geometry": "gwyddion_digital_ellipse", + } + + +def test_public_exports_and_signature_contract() -> None: + expected_names = { + "estimate_gwyddion_median_background", + "remove_gwyddion_median_background", + "analyze_gwyddion_median_background", + } + + assert expected_names <= set(analysis.__all__) + for name in expected_names: + assert getattr(analysis, name) is not None + + for operation in _PUBLIC_OPERATIONS: + signature = inspect.signature(operation) + assert list(signature.parameters) == ["channel", "radius_px"] + assert signature.parameters["radius_px"].default == 20 + assert signature.parameters["radius_px"].kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + + for private_name in ( + "_MedianBackgroundKernelSpec", + "_validated_median_background_radius", + "_median_background_active_offsets", + "_median_background_kernel_spec", + "_gwyddion_median_background_result", + ): + assert private_name not in analysis.__all__ + assert not hasattr(analysis, private_name) + + +@pytest.mark.parametrize( + "case_name", + ["wide_r1", "wide_r3", "wide_r20", "singleton_1x1_r1024"], +) +def test_analyze_reports_exact_frozen_metadata(case_name: str) -> None: + case = _case(case_name) + data, _, _ = _case_arrays(case) + + result = analyze_gwyddion_median_background( + _channel(data), + case["radius"], + ) + + assert isinstance(result, BackgroundResult) + assert result.method == "gwyddion_median_background" + assert result.parameters == _expected_parameters(case) + + +@pytest.mark.parametrize("case", _cases(), ids=lambda case: str(case["name"])) +def test_analyze_matches_all_frozen_cases_bitwise(case: dict[str, object]) -> None: + data, expected_background, expected_corrected = _case_arrays(case) + source = _channel(data) + original = source.data.copy() + + result = analyze_gwyddion_median_background(source, case["radius"]) + + _assert_bitwise_equal( + result.background.data, + expected_background, + case=str(case["name"]), + operation="analyze", + array="background", + ) + _assert_bitwise_equal( + result.corrected.data, + expected_corrected, + case=str(case["name"]), + operation="analyze", + array="corrected", + ) + assert result.parameters == _expected_parameters(case) + assert np.array_equal(source.data, original) + assert np.max(np.abs(source.data - (result.background.data + result.corrected.data))) <= 1e-15 + + +@pytest.mark.parametrize("case_name", ["wide_r1", "wide_r3"]) +def test_estimate_and_remove_match_private_and_fixture(case_name: str) -> None: + case = _case(case_name) + data, expected_background, expected_corrected = _case_arrays(case) + source = _channel(data) + private_background, private_corrected, _ = _gwyddion_median_background_result( + source.data, + case["radius"], + ) + + estimated = estimate_gwyddion_median_background(source, case["radius"]) + removed = remove_gwyddion_median_background(source, case["radius"]) + + for actual, expected, operation, array in ( + (estimated.data, private_background, "estimate", "background-private"), + (removed.data, private_corrected, "remove", "corrected-private"), + (estimated.data, expected_background, "estimate", "background-fixture"), + (removed.data, expected_corrected, "remove", "corrected-fixture"), + ): + _assert_bitwise_equal( + actual, + expected, + case=case_name, + operation=operation, + array=array, + ) + + +def test_context_array_contract_and_memory_independence_are_preserved() -> None: + case = _case("signed_r3") + data, _, _ = _case_arrays(case) + source = _channel(data) + result = analyze_gwyddion_median_background(source, case["radius"]) + + for output in (result.background, result.corrected): + assert output.name == source.name + assert output.unit == source.unit + assert output.x_range == source.x_range + assert output.y_range == source.y_range + assert output.direction == source.direction + assert output.group == source.group + assert output.metadata == source.metadata + assert output.metadata is not source.metadata + assert output.data.shape == source.data.shape + assert output.data.dtype == np.float64 + assert output.data.flags.c_contiguous + assert np.all(np.isfinite(output.data)) + assert not np.shares_memory(output.data, source.data) + + assert not np.shares_memory(result.background.data, result.corrected.data) + result.background.metadata["adapter"] = "background" + assert "adapter" not in source.metadata + assert "adapter" not in result.corrected.metadata + + +@pytest.mark.parametrize( + ("radius_px", "exception"), + [ + (True, TypeError), + (np.array(2, dtype=np.int64), TypeError), + (0, ValueError), + (1025, ValueError), + (10**100, ValueError), + ], +) +@pytest.mark.parametrize("operation", _PUBLIC_OPERATIONS) +def test_invalid_radius_is_delegated_without_relaxation( + radius_px: object, + exception: type[Exception], + operation: Callable[[SPMChannel, object], object], +) -> None: + case = _case("wide_r1") + data, _, _ = _case_arrays(case) + + with pytest.raises(exception): + operation(_channel(data), radius_px) + + +@pytest.mark.parametrize("operation", _PUBLIC_OPERATIONS) +@pytest.mark.parametrize("nonfinite", [np.nan, np.inf, -np.inf]) +def test_nonfinite_data_rejection_is_delegated( + operation: Callable[[SPMChannel, object], object], + nonfinite: float, +) -> None: + data = np.ones((2, 3), dtype=np.float64) + data[0, 1] = nonfinite + + with pytest.raises(ValueError, match="finite data"): + operation(_channel(data), 1) + + +@pytest.mark.parametrize("operation", _PUBLIC_OPERATIONS) +def test_each_public_operation_invokes_private_kernel_once( + monkeypatch: pytest.MonkeyPatch, + operation: Callable[[SPMChannel, object], object], +) -> None: + case = _case("wide_r3") + data, _, _ = _case_arrays(case) + call_count = 0 + original = background_module._gwyddion_median_background_result + + def counted_result( + received_data: object, + received_radius_px: object, + ) -> tuple[np.ndarray, np.ndarray, object]: + nonlocal call_count + call_count += 1 + return original(received_data, received_radius_px) + + monkeypatch.setattr( + background_module, + "_gwyddion_median_background_result", + counted_result, + ) + + operation(_channel(data), case["radius"]) + + assert call_count == 1 + + +def test_default_radius_and_to_dict_are_publicly_stable() -> None: + case = _case("wide_r20") + data, expected_background, expected_corrected = _case_arrays(case) + + result = analyze_gwyddion_median_background(_channel(data)) + + _assert_bitwise_equal( + result.background.data, + expected_background, + case="wide_r20", + operation="analyze-default", + array="background", + ) + _assert_bitwise_equal( + result.corrected.data, + expected_corrected, + case="wide_r20", + operation="analyze-default", + array="corrected", + ) + assert result.parameters == _expected_parameters(case) + assert result.to_dict()["method"] == "gwyddion_median_background" diff --git a/tests/core/test_gwyddion_median_background_private.py b/tests/core/test_gwyddion_median_background_private.py new file mode 100644 index 0000000..0310e26 --- /dev/null +++ b/tests/core/test_gwyddion_median_background_private.py @@ -0,0 +1,392 @@ +"""Focused tests for the private Gwyddion 2.71 Median Background kernel.""" + +from __future__ import annotations + +import json +from functools import cache, lru_cache +from pathlib import Path + +import numpy as np +import pytest + +from spmkit.core.analysis._median_background import ( + _gwyddion_median_background_result, + _median_background_active_offsets, + _median_background_kernel_spec, +) + +_FIXTURE_DIR = ( + Path(__file__).parents[1] / "validation" / "fixtures" / "gwyddion" / "median_background" +) +_FIXTURE_PATH = _FIXTURE_DIR / "median_background_reference.npz" +_MANIFEST_PATH = _FIXTURE_DIR / "median_background_reference.json" +_KERNEL_INVENTORY = { + 1: (3, 9, 4, "direct"), + 2: (5, 21, 10, "direct"), + 3: (7, 37, 18, "radixtree"), + 4: (9, 69, 34, "radixtree"), + 20: (41, 1313, 656, "radixtree"), + 1024: (2049, 3297401, 1648700, "radixtree"), +} +_UINT64_MASK = (1 << 64) - 1 +_UINT64_SIGN = 1 << 63 + + +@lru_cache(maxsize=1) +def _fixture() -> tuple[dict[str, object], dict[str, np.ndarray]]: + """Load the frozen fixture only as test evidence.""" + manifest = json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + arrays: dict[str, np.ndarray] = {} + with np.load(_FIXTURE_PATH, allow_pickle=False) as archive: + for name in archive.files: + arrays[name] = np.array(archive[name], dtype=np.float64, order="C", copy=True) + return manifest, arrays + + +def _case(case_name: str) -> dict[str, object]: + manifest, _ = _fixture() + for case in manifest["cases"]: # type: ignore[index] + if case["name"] == case_name: + return case + raise AssertionError(f"frozen fixture case was not found: {case_name}") + + +@cache +def _result_for_case(case_name: str) -> tuple[np.ndarray, np.ndarray, object]: + case = _case(case_name) + _, arrays = _fixture() + keys = case["arrays"] + return _gwyddion_median_background_result( + arrays[keys["input"]], # type: ignore[index] + case["radius"], + ) + + +def _ordered_uint64(bits: int) -> int: + """Map IEEE-754 bits to an ordering suitable for a ULP distance.""" + if bits & _UINT64_SIGN: + return (-bits) & _UINT64_MASK + return bits | _UINT64_SIGN + + +def _assert_bitwise_equal( + case_name: str, + array_name: str, + expected: np.ndarray, + actual: np.ndarray, +) -> None: + expected_bits = expected.view(np.uint64) + actual_bits = actual.view(np.uint64) + mismatch = np.argwhere(expected_bits != actual_bits) + if mismatch.size == 0: + return + + row, column = (int(value) for value in mismatch[0]) + expected_bit = int(expected_bits[row, column]) + actual_bit = int(actual_bits[row, column]) + expected_value = float(expected[row, column]) + actual_value = float(actual[row, column]) + ulp_distance = abs(_ordered_uint64(expected_bit) - _ordered_uint64(actual_bit)) + raise AssertionError( + f"case={case_name} array={array_name} coordinate=({row}, {column}) " + f"expected={expected_value!r} actual={actual_value!r} " + f"expected_uint64={expected_bit} actual_uint64={actual_bit} " + f"absolute_difference={abs(expected_value - actual_value)!r} " + f"ulp_distance={ulp_distance}" + ) + + +def test_kernel_inventory_is_exact() -> None: + for radius, expected in _KERNEL_INVENTORY.items(): + specification = _median_background_kernel_spec(radius) + assert ( + specification.kernel_resolution, + specification.kernel_active_count, + specification.rank_index, + specification.rank_backend_reference, + ) == expected + + +def test_kernel_resolution_is_two_radius_plus_one() -> None: + for radius in _KERNEL_INVENTORY: + assert _median_background_kernel_spec(radius).kernel_resolution == 2 * radius + 1 + + +def test_kernel_active_count_is_odd() -> None: + for radius in _KERNEL_INVENTORY: + assert _median_background_kernel_spec(radius).kernel_active_count % 2 == 1 + + +def test_kernel_rank_is_half_the_active_count() -> None: + for radius in _KERNEL_INVENTORY: + specification = _median_background_kernel_spec(radius) + assert specification.rank_index == specification.kernel_active_count // 2 + + +def test_kernel_backend_reference_uses_frozen_threshold() -> None: + assert _median_background_kernel_spec(2).rank_backend_reference == "direct" + assert _median_background_kernel_spec(3).rank_backend_reference == "radixtree" + + +def test_offsets_are_in_row_major_order() -> None: + offsets = _median_background_active_offsets(20) + keys = offsets[:, 0] * 10000 + offsets[:, 1] + assert np.array_equal(keys, np.sort(keys)) + + +def test_offsets_obey_the_inclusive_integer_ellipse_condition() -> None: + radius = 20 + offsets = _median_background_active_offsets(radius) + left = 4 * (offsets[:, 0] * offsets[:, 0] + offsets[:, 1] * offsets[:, 1]) + assert np.all(left <= (2 * radius + 1) ** 2) + + +def test_offsets_exclude_immediately_exterior_integer_positions() -> None: + radius = 20 + offsets = _median_background_active_offsets(radius) + radius_square = (2 * radius + 1) ** 2 + for dr in range(-radius, radius + 1): + row_offsets = offsets[offsets[:, 0] == dr, 1] + next_dc = int(np.max(np.abs(row_offsets))) + 1 + assert 4 * (dr * dr + next_dc * next_dc) > radius_square + + +def test_offsets_contain_the_central_pixel() -> None: + offsets = _median_background_active_offsets(20) + assert np.any(np.all(offsets == np.array([0, 0]), axis=1)) + + +def test_offsets_are_c_contiguous_and_read_only() -> None: + offsets = _median_background_active_offsets(20) + assert offsets.flags.c_contiguous + assert not offsets.flags.writeable + + +def test_offset_cache_preserves_content() -> None: + first = _median_background_active_offsets(20) + second = _median_background_active_offsets(np.int64(20)) + assert first is second + assert np.array_equal(first, second) + + +@pytest.mark.parametrize( + "radius", + [True, 0, -1, 1025, 1.0, 1.5, "20"], +) +def test_radius_validation_rejects_values_outside_the_contract(radius: object) -> None: + expected_error = TypeError if isinstance(radius, (bool, float, str)) else ValueError + with pytest.raises(expected_error): + _median_background_kernel_spec(radius) + + +@pytest.mark.parametrize( + ("radius", "expected"), + [ + (1, 1), + (1024, 1024), + (np.int8(2), 2), + (np.int64(20), 20), + (np.uint8(3), 3), + (np.uint64(1024), 1024), + ], +) +def test_radius_validation_accepts_python_and_numpy_integer_scalars( + radius: object, + expected: int, +) -> None: + specification = _median_background_kernel_spec(radius) + assert type(specification.radius_px) is int + assert specification.radius_px == expected + + +@pytest.mark.parametrize( + "radius", + [ + True, + False, + np.bool_(True), + np.bool_(False), + np.array(2, dtype=np.int64), + np.array(2, dtype=np.uint64), + np.array(True), + 2.0, + "2", + ], +) +def test_radius_validation_rejects_non_integer_scalars_with_type_error(radius: object) -> None: + with pytest.raises(TypeError, match="Python or NumPy integer scalar"): + _median_background_kernel_spec(radius) + + +@pytest.mark.parametrize( + "radius", + [0, -1, 1025, 10**100, -(10**100), np.uint64(1025)], +) +def test_radius_validation_rejects_all_out_of_range_integers_with_value_error( + radius: object, +) -> None: + with pytest.raises(ValueError, match=r"1\.\.1024"): + _median_background_kernel_spec(radius) + + +@pytest.mark.parametrize("radius", [True, False, np.bool_(True), np.bool_(False)]) +def test_radius_validation_explains_that_booleans_are_invalid(radius: object) -> None: + with pytest.raises(TypeError, match="booleans are not valid"): + _median_background_kernel_spec(radius) + + +@pytest.mark.parametrize( + "data, exception, message", + [ + (np.array(1.0), ValueError, "two-dimensional"), + (np.array([1.0, 2.0]), ValueError, "two-dimensional"), + (np.ones((1, 1, 1)), ValueError, "two-dimensional"), + (np.empty((0, 2)), ValueError, "non-empty"), + (np.array([[np.nan]]), ValueError, "finite"), + (np.array([[np.inf]]), ValueError, "finite"), + (np.array([[-np.inf]]), ValueError, "finite"), + ], +) +def test_input_validation_rejects_outside_the_frozen_domain( + data: np.ndarray, + exception: type[Exception], + message: str, +) -> None: + with pytest.raises(exception, match=message): + _gwyddion_median_background_result(data, 1) + + +def test_compatible_input_is_converted_to_float64() -> None: + background, corrected, _ = _gwyddion_median_background_result([[1, 2], [3, 4]], 1) + assert background.dtype == np.float64 + assert corrected.dtype == np.float64 + + +def test_input_is_not_mutated() -> None: + data = np.array([[1, -2, 3], [4, 5, -6]], dtype=np.float32) + before = data.copy() + _gwyddion_median_background_result(data, 2) + assert np.array_equal(data, before) + + +def test_outputs_do_not_share_memory_with_input_or_each_other() -> None: + data = np.arange(12, dtype=np.float64).reshape(3, 4) + background, corrected, _ = _gwyddion_median_background_result(data, 1) + assert not np.shares_memory(data, background) + assert not np.shares_memory(data, corrected) + assert not np.shares_memory(background, corrected) + + +def test_outputs_are_float64_two_dimensional_c_contiguous_and_finite() -> None: + background, corrected, _ = _gwyddion_median_background_result([[1, 2], [3, 4]], 1) + for output in (background, corrected): + assert output.dtype == np.float64 + assert output.ndim == 2 + assert output.flags.c_contiguous + assert np.all(np.isfinite(output)) + + +def test_output_shape_is_preserved() -> None: + data = np.arange(15, dtype=np.float64).reshape(3, 5) + background, corrected, _ = _gwyddion_median_background_result(data, 2) + assert background.shape == data.shape + assert corrected.shape == data.shape + + +def test_border_extension_clamps_to_the_nearest_edge() -> None: + data = np.array([[0.0, 10.0], [20.0, 30.0]]) + background, corrected, _ = _gwyddion_median_background_result(data, 1) + assert background[0, 0] == 10.0 + assert corrected[0, 0] == -10.0 + + +def test_constant_field_has_identity_background_and_zero_corrected() -> None: + data = np.full((4, 5), 7.25, dtype=np.float64) + background, corrected, _ = _gwyddion_median_background_result(data, 3) + assert np.array_equal(background.view(np.uint64), data.view(np.uint64)) + assert np.array_equal(corrected.view(np.uint64), np.zeros_like(corrected).view(np.uint64)) + + +def test_signed_field_produces_finite_reconstructable_outputs() -> None: + data = np.array([[-4.0, -1.0, 2.0], [3.0, -5.0, 7.0]], dtype=np.float64) + background, corrected, _ = _gwyddion_median_background_result(data, 2) + assert np.all(np.isfinite(background)) + assert np.all(np.isfinite(corrected)) + np.testing.assert_allclose(data, background + corrected, atol=1e-15, rtol=0.0) + + +def test_positive_impulse_uses_the_rank_background() -> None: + data = np.zeros((5, 5), dtype=np.float64) + data[2, 2] = 100.0 + background, corrected, _ = _gwyddion_median_background_result(data, 1) + assert np.array_equal(background.view(np.uint64), np.zeros_like(background).view(np.uint64)) + assert corrected[2, 2] == 100.0 + + +def test_negative_impulse_uses_the_rank_background() -> None: + data = np.zeros((5, 5), dtype=np.float64) + data[2, 2] = -100.0 + background, corrected, _ = _gwyddion_median_background_result(data, 1) + assert np.array_equal(background.view(np.uint64), np.zeros_like(background).view(np.uint64)) + assert corrected[2, 2] == -100.0 + + +def test_singleton_one_by_one_field() -> None: + data = np.array([[3.5]], dtype=np.float64) + background, corrected, _ = _gwyddion_median_background_result(data, 1024) + assert np.array_equal(background.view(np.uint64), data.view(np.uint64)) + assert np.array_equal(corrected.view(np.uint64), np.zeros_like(corrected).view(np.uint64)) + + +def test_singleton_row_field() -> None: + data = np.array([[2.0, -1.0, 5.0, 0.0]], dtype=np.float64) + background, corrected, _ = _gwyddion_median_background_result(data, 3) + assert background.shape == data.shape + assert corrected.shape == data.shape + + +def test_singleton_column_field() -> None: + data = np.array([[2.0], [-1.0], [5.0], [0.0]], dtype=np.float64) + background, corrected, _ = _gwyddion_median_background_result(data, 3) + assert background.shape == data.shape + assert corrected.shape == data.shape + + +def test_all_fixture_backgrounds_are_bitwise_exact() -> None: + manifest, arrays = _fixture() + for case in manifest["cases"]: # type: ignore[index] + name = case["name"] + background, _, _ = _result_for_case(name) + _assert_bitwise_equal(name, "background", arrays[case["arrays"]["background"]], background) + + +def test_all_fixture_corrected_fields_are_bitwise_exact() -> None: + manifest, arrays = _fixture() + for case in manifest["cases"]: # type: ignore[index] + name = case["name"] + _, corrected, _ = _result_for_case(name) + _assert_bitwise_equal(name, "corrected", arrays[case["arrays"]["corrected"]], corrected) + + +def test_fixture_metadata_matches_the_private_kernel_specification() -> None: + manifest, _ = _fixture() + for case in manifest["cases"]: # type: ignore[index] + _, _, specification = _result_for_case(case["name"]) + assert specification.radius_px == case["radius"] + assert specification.kernel_resolution == case["kernel_resolution"] + assert specification.kernel_active_count == case["kernel_active_count"] + assert specification.rank_index == case["rank_index"] + assert specification.rank_backend_reference == case["rank_backend_reference"] + + +def test_fixture_results_obey_the_reconstruction_contract() -> None: + manifest, arrays = _fixture() + for case in manifest["cases"]: # type: ignore[index] + name = case["name"] + background, corrected, _ = _result_for_case(name) + np.testing.assert_allclose( + arrays[case["arrays"]["input"]], + background + corrected, + atol=1e-15, + rtol=0.0, + ) diff --git a/tests/core/test_gwyddion_path_level.py b/tests/core/test_gwyddion_path_level.py new file mode 100644 index 0000000..7af7a00 --- /dev/null +++ b/tests/core/test_gwyddion_path_level.py @@ -0,0 +1,246 @@ +"""Public-contract tests for frozen Gwyddion 2.71 Path Level.""" + +from __future__ import annotations + +import inspect +import json +from pathlib import Path + +import numpy as np +import pytest + +import spmkit.core.analysis as analysis +import spmkit.core.analysis.leveling as leveling_module +from spmkit.core.analysis import gwyddion_path_level +from spmkit.core.models import SPMChannel + +_FIXTURE_DIRECTORY = ( + Path(__file__).resolve().parents[1] / "validation" / "fixtures" / "gwyddion" / "path_level" +) +_FIXTURE_PATH = _FIXTURE_DIRECTORY / "path_level_reference.npz" +_MANIFEST_PATH = _FIXTURE_DIRECTORY / "path_level_reference.json" + + +def _manifest() -> dict[str, object]: + return json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + + +def _channel(data: np.ndarray, *, xreal: float, yreal: float) -> SPMChannel: + return SPMChannel( + name="Path Level fixture", + data=data, + unit="V", + x_range=xreal, + y_range=yreal, + direction="backward", + group="Frozen Path Level evidence", + metadata={"source": "gwyddion-2.71-pathlevel", "context": {"id": 11}}, + ) + + +def _lines(case: dict[str, object]) -> np.ndarray: + values = [float.fromhex(value) for value in case["lines_hex"]] # type: ignore[index] + if not values: + return np.empty((0, 4), dtype=np.float64) + return np.array(values, dtype=np.float64).reshape((-1, 4)) + + +def _ordered_uint64(bits: int) -> int: + sign_bit = 1 << 63 + return ((~bits + 1) & ((1 << 64) - 1)) if bits & sign_bit else bits | sign_bit + + +def _maximum_ulp_distance(expected: np.ndarray, actual: np.ndarray) -> int: + expected_bits = expected.view(np.uint64).ravel() + actual_bits = actual.view(np.uint64).ravel() + return max( + abs(_ordered_uint64(int(wanted)) - _ordered_uint64(int(received))) + for wanted, received in zip(expected_bits, actual_bits, strict=True) + ) + + +def _assert_bitwise_equal(actual: np.ndarray, expected: np.ndarray, *, case_id: str) -> None: + actual_bits = actual.view(np.uint64) + expected_bits = expected.view(np.uint64) + if np.array_equal(actual_bits, expected_bits): + return + row, column = np.argwhere(actual_bits != expected_bits)[0] + actual_value = int(actual_bits[row, column]) + expected_value = int(expected_bits[row, column]) + ulp_distance = abs(_ordered_uint64(actual_value) - _ordered_uint64(expected_value)) + pytest.fail( + f"case={case_id} coordinate=({row}, {column}) " + f"expected={expected[row, column]!r} actual={actual[row, column]!r} " + f"expected_uint64={expected_value} actual_uint64={actual_value} " + f"absolute_difference={abs(actual[row, column] - expected[row, column])!r} " + f"ulp_distance={ulp_distance}" + ) + + +def test_public_export_and_signature() -> None: + assert "gwyddion_path_level" in analysis.__all__ + assert analysis.gwyddion_path_level is gwyddion_path_level + signature = inspect.signature(gwyddion_path_level) + assert list(signature.parameters) == ["channel", "lines", "thickness_px"] + assert signature.parameters["thickness_px"].kind is inspect.Parameter.KEYWORD_ONLY + assert signature.parameters["thickness_px"].default == 1 + for forbidden in ("mask", "roi", "path", "interpolation", "origin"): + assert forbidden not in signature.parameters + for private_name in ( + "_GwyddionPathLevelLine", + "_GwyddionPathLevelResult", + "_gwyddion_c_trunc_div", + "_gwyddion_normalized_path_level_lines", + "_gwyddion_path_level_result", + ): + assert private_name not in analysis.__all__ + assert not hasattr(analysis, private_name) + + +def test_all_frozen_public_outputs_are_bitwise_exact() -> None: + manifest = _manifest() + with np.load(_FIXTURE_PATH, allow_pickle=False) as archive: + exact_elements = signed_zero_mismatches = mutation_matches = no_op_matches = 0 + maximum_absolute_difference = 0.0 + maximum_ulp_distance = 0 + ordered_outputs: dict[str, np.ndarray] = {} + for case in manifest["cases"]: # type: ignore[index] + base = next( + base for base in manifest["bases"] if base["base_id"] == case["base_id"] # type: ignore[index] + ) + source_data = np.array( + archive[base["input_key"]], + dtype=np.float64, + order="C", + copy=True, + ) + original_bits = source_data.view(np.uint64).copy() + channel = _channel(source_data, xreal=base["xreal"], yreal=base["yreal"]) + output = gwyddion_path_level( + channel, + _lines(case), + thickness_px=case["thickness"], + ) + expected = archive[case["output_key"]] + _assert_bitwise_equal(output.data, expected, case_id=case["case_id"]) + exact_elements += expected.size + maximum_absolute_difference = max( + maximum_absolute_difference, + float(np.max(np.abs(output.data - expected))), + ) + maximum_ulp_distance = max( + maximum_ulp_distance, + _maximum_ulp_distance(expected, output.data), + ) + signed_zero_mismatches += int( + np.count_nonzero( + (output.data == 0.0) + & (expected == 0.0) + & (output.data.view(np.uint64) != expected.view(np.uint64)) + ) + ) + changed = not np.array_equal(output.data.view(np.uint64), original_bits) + mutation_matches += changed == case["external_mutation_of_data_field"] + no_op_matches += (not changed) == case["external_no_op"] + assert np.array_equal(channel.data.view(np.uint64), original_bits) + assert output.data.dtype == np.float64 and output.data.flags.c_contiguous + assert output.data.shape == channel.data.shape + assert not np.shares_memory(output.data, channel.data) + ordered_outputs[case["case_id"]] = output.data + assert exact_elements == 4652 + assert maximum_absolute_difference == 0.0 + assert maximum_ulp_distance == 0 + assert signed_zero_mismatches == 0 + assert mutation_matches == 72 + assert no_op_matches == 72 + assert not np.array_equal( + ordered_outputs["line_order_a__t1"].view(np.uint64), + ordered_outputs["line_order_b_permuted__t1"].view(np.uint64), + ) + + +def test_context_is_preserved_with_independent_metadata_and_data() -> None: + manifest = _manifest() + base = next( + base for base in manifest["bases"] if base["base_id"] == "signed_gradient_positive_slope" + ) # type: ignore[index] + case = next(case for case in manifest["cases"] if case["base_id"] == base["base_id"]) # type: ignore[index] + with np.load(_FIXTURE_PATH, allow_pickle=False) as archive: + channel = _channel( + np.array(archive[base["input_key"]], dtype=np.float64, order="C", copy=True), + xreal=base["xreal"], + yreal=base["yreal"], + ) + output = gwyddion_path_level(channel, _lines(case), thickness_px=case["thickness"]) + assert output.name == channel.name + assert output.unit == channel.unit + assert output.x_range == channel.x_range and output.y_range == channel.y_range + assert output.direction == channel.direction and output.group == channel.group + assert output.metadata == channel.metadata and output.metadata is not channel.metadata + output.metadata["new_key"] = True + assert "new_key" not in channel.metadata + + +@pytest.mark.parametrize( + ("lines", "thickness_px", "error_type"), + [ + ([(0.0, 0.0, 1.0, 1.0)], True, TypeError), + ([(0.0, 0.0, 1.0, 1.0)], np.array(1), TypeError), + ([(0.0, 0.0, 1.0, 1.0)], 0, ValueError), + ([(0.0, 0.0, 1.0, 1.0)], 129, ValueError), + ([(0.0, 0.0, 1.0, 1.0)], 1.0, TypeError), + ([(0.0, 0.0, 1.0, 1.0)], "1", TypeError), + ([(0.0, 1.0)], 1, ValueError), + ("line", 1, TypeError), + (np.array([["a", "b", "c", "d"]], dtype=object), 1, TypeError), + (np.array([[np.inf, 0.0, 1.0, 1.0]]), 1, ValueError), + ], +) +def test_public_validation_is_delegated( + lines: object, + thickness_px: object, + error_type: type[Exception], +) -> None: + channel = _channel(np.ones((3, 4), dtype=np.float64), xreal=4.0, yreal=3.0) + with pytest.raises(error_type): + gwyddion_path_level(channel, lines, thickness_px=thickness_px) + invalid_data = _channel(np.array([[np.nan]], dtype=np.float64), xreal=1.0, yreal=1.0) + with pytest.raises(ValueError, match="finite"): + gwyddion_path_level(invalid_data, [], thickness_px=1) + + +@pytest.mark.parametrize( + ("data", "error_type"), + [ + (np.ones(3), ValueError), + (np.empty((0, 2)), ValueError), + (np.array([["a"]]), TypeError), + (np.array([[np.inf]]), ValueError), + ], +) +def test_public_data_and_extent_validation_is_delegated( + data: np.ndarray, + error_type: type[Exception], +) -> None: + channel = _channel(data, xreal=1.0, yreal=1.0) + with pytest.raises(error_type): + gwyddion_path_level(channel, [], thickness_px=1) + for xreal, yreal in ((0.0, 1.0), (1.0, np.inf)): + valid = _channel(np.ones((2, 2)), xreal=xreal, yreal=yreal) + with pytest.raises(ValueError): + gwyddion_path_level(valid, [], thickness_px=1) + + +def test_each_public_call_invokes_private_entry_once(monkeypatch: pytest.MonkeyPatch) -> None: + calls = 0 + original = leveling_module._gwyddion_path_level_result + + def counted_entry(*args: object, **kwargs: object) -> object: + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(leveling_module, "_gwyddion_path_level_result", counted_entry) + channel = _channel(np.arange(12, dtype=np.float64).reshape(3, 4), xreal=4.0, yreal=3.0) + gwyddion_path_level(channel, [(0.0, 0.0, 3.0, 2.0)], thickness_px=2) + assert calls == 1 diff --git a/tests/core/test_gwyddion_path_level_private.py b/tests/core/test_gwyddion_path_level_private.py new file mode 100644 index 0000000..59d77de --- /dev/null +++ b/tests/core/test_gwyddion_path_level_private.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np +import pytest + +from spmkit.core.analysis._gwyddion_path_level import ( + _gwyddion_c_trunc_div, + _gwyddion_normalized_path_level_lines, + _gwyddion_path_level_result, + _validated_gwyddion_path_level_data, + _validated_gwyddion_path_level_lines, + _validated_gwyddion_path_level_thickness, +) + +FIXTURE = Path(__file__).resolve().parents[1] / "validation/fixtures/gwyddion/path_level" + + +def _fixture() -> tuple[dict[str, object], dict[str, np.ndarray]]: + manifest = json.loads((FIXTURE / "path_level_reference.json").read_text()) + with np.load(FIXTURE / "path_level_reference.npz", allow_pickle=False) as archive: + arrays = {name: archive[name].copy(order="C") for name in archive.files} + return manifest, arrays + + +def _bits(array: np.ndarray) -> np.ndarray: + return np.ascontiguousarray(array, dtype=np.float64).view(np.uint64) + + +def _ulp_distance(expected: int, actual: int) -> int: + def ordered(value: int) -> int: + return (~value + 1) & ((1 << 64) - 1) if value >> 63 else value | (1 << 63) + + return abs(ordered(expected) - ordered(actual)) + + +def _assert_bits(case_id: str, expected: np.ndarray, actual: np.ndarray) -> None: + expected_bits = _bits(expected) + actual_bits = _bits(actual) + locations = np.argwhere(expected_bits != actual_bits) + if not len(locations): + return + row, column = (int(value) for value in locations[0]) + wanted = int(expected_bits[row, column]) + received = int(actual_bits[row, column]) + raise AssertionError( + f"{case_id}: coordinate=({row}, {column}), expected={expected[row, column]!r}, " + f"actual={actual[row, column]!r}, expected_uint64={wanted:016x}, " + f"actual_uint64={received:016x}, abs={abs(expected[row, column] - actual[row, column])!r}, " + f"ulp={_ulp_distance(wanted, received)}" + ) + + +def _lines(case: dict[str, object]) -> np.ndarray: + values = [float.fromhex(value) for value in case["lines_hex"]] # type: ignore[index] + return np.array(values, dtype=np.float64).reshape((-1, 4)) if values else np.empty((0, 4)) + + +def _array_from_bits(bits: list[str]) -> np.ndarray: + return np.array([int(value, 16) for value in bits], dtype=np.uint64).view(np.float64) + + +def test_all_frozen_cases_are_bitwise_exact_with_source_diagnostics() -> None: + manifest, arrays = _fixture() + endpoint_matches = mutation_matches = no_op_matches = exact_elements = 0 + for case in manifest["cases"]: # type: ignore[index] + base = next(base for base in manifest["bases"] if base["base_id"] == case["base_id"]) # type: ignore[index] + input_data = arrays[base["input_key"]].copy(order="C") + before = input_data.copy(order="C") + result = _gwyddion_path_level_result( + input_data, + _lines(case), + xreal=base["xreal"], + yreal=base["yreal"], + thickness_px=case["thickness"], + ) + expected = arrays[case["output_key"]] + _assert_bits(case["case_id"], expected, result.corrected) + exact_elements += expected.size + assert result.normalized_lines == tuple( + tuple(case["normalized_endpoints"][index : index + 4]) + for index in range(0, len(case["normalized_endpoints"]), 4) + ) + endpoint_matches += 1 + _assert_bits( + case["case_id"] + "/row_differences", + _array_from_bits(case["oracle_row_differences_bits"]), + result.row_differences, + ) + _assert_bits( + case["case_id"] + "/cumulative", + _array_from_bits(case["oracle_cumulative_correction_bits"]), + result.cumulative_row_correction, + ) + changed = not np.array_equal(_bits(result.corrected), _bits(before)) + mutation_matches += changed == case["external_mutation_of_data_field"] + no_op_matches += (not changed) == case["external_no_op"] + assert np.array_equal(_bits(input_data), _bits(before)) + assert result.corrected.dtype == np.float64 and result.corrected.flags.c_contiguous + assert result.corrected.shape == input_data.shape + assert not np.shares_memory(result.corrected, input_data) + assert endpoint_matches == 72 + assert mutation_matches == 72 + assert no_op_matches == 72 + assert exact_elements == 4652 + + +def test_line_order_discriminator_is_preserved() -> None: + manifest, arrays = _fixture() + selected = {case["case_id"]: case for case in manifest["cases"]} + outputs = [] + for case_id in ("line_order_a__t1", "line_order_b_permuted__t1"): + case = selected[case_id] + base = next(base for base in manifest["bases"] if base["base_id"] == case["base_id"]) + result = _gwyddion_path_level_result( + arrays[base["input_key"]], + _lines(case), + xreal=base["xreal"], + yreal=base["yreal"], + thickness_px=1, + ) + outputs.append(result.corrected) + assert not np.array_equal(_bits(outputs[0]), _bits(outputs[1])) + + +def test_endpoint_geometry_and_c_integer_division_contract() -> None: + lines = _validated_gwyddion_path_level_lines([(7.9, 8.2, 1.1, 0.2), (-5.0, -2.0, 15.0, 12.0)]) + assert _gwyddion_normalized_path_level_lines(lines, xres=9, yres=9, xreal=9.0, yreal=9.0) == ( + (1, 0, 7, 8), + (0, 0, 8, 8), + ) + assert [_gwyddion_c_trunc_div(value, 3) for value in (-8, -7, -1, 0, 1, 7, 8)] == [ + -2, + -2, + 0, + 0, + 0, + 2, + 2, + ] + + +def test_validation_and_memory_contracts() -> None: + assert isinstance(_validated_gwyddion_path_level_thickness(np.uint8(128)), int) + for value in (True, np.bool_(False), np.array(1), 1.0, "1"): + with pytest.raises(TypeError): + _validated_gwyddion_path_level_thickness(value) + for value in (0, 129, 10**100): + with pytest.raises(ValueError): + _validated_gwyddion_path_level_thickness(value) + for value in (np.array([]), np.array([1.0]), np.empty((0, 2)), np.array([[np.nan]])): + with pytest.raises(ValueError): + _validated_gwyddion_path_level_data(value) + for value in ("line", np.array([1.0, 2.0, 3.0]), np.array([[np.inf, 0, 0, 0]])): + with pytest.raises((TypeError, ValueError)): + _validated_gwyddion_path_level_lines(value) + data = [[0, 1], [2, 3]] + result = _gwyddion_path_level_result(data, [], xreal=2.0, yreal=2.0, thickness_px=128) + assert result.corrected.dtype == np.float64 and result.corrected.flags.c_contiguous + assert not np.shares_memory(result.corrected, np.asarray(data)) + + +def test_signed_zero_and_repeated_execution_are_deterministic() -> None: + data = np.array([[-0.0, +0.0], [-0.0, +0.0]], dtype=np.float64) + lines = np.array([[0.0, 0.0, 1.0, 1.0]], dtype=np.float64) + first = _gwyddion_path_level_result(data, lines, xreal=2.0, yreal=2.0, thickness_px=2) + second = _gwyddion_path_level_result(data, lines, xreal=2.0, yreal=2.0, thickness_px=2) + _assert_bits("signed_zero_repeat", first.corrected, second.corrected) + assert ( + hashlib.sha256(_bits(first.corrected).tobytes()).digest() + == hashlib.sha256(_bits(second.corrected).tobytes()).digest() + ) diff --git a/tests/core/test_gwyddion_sphere_revolution_background.py b/tests/core/test_gwyddion_sphere_revolution_background.py new file mode 100644 index 0000000..8482695 --- /dev/null +++ b/tests/core/test_gwyddion_sphere_revolution_background.py @@ -0,0 +1,370 @@ +"""Tests for private Gwyddion 2.71 Sphere Revolution numerical kernel.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from spmkit.core.analysis import ( + BackgroundResult, + analyze_gwyddion_sphere_revolution_background, + estimate_gwyddion_sphere_revolution_background, + remove_gwyddion_sphere_revolution_background, +) +from spmkit.core.analysis._gwyddion_sphere_revolution import ( + _gwyddion_sphere_background, + _gwyddion_sphere_corrected, + _gwyddion_sphere_result, +) +from spmkit.core.models import SPMChannel + + +def test_gwyddion_sphere_rejects_non_2d_input() -> None: + with pytest.raises(TypeError, match="real 2D array"): + _gwyddion_sphere_background(np.array([1.0, 2.0, 3.0]), 5.0) + + with pytest.raises(TypeError, match="real 2D array"): + _gwyddion_sphere_background(np.ones((2, 2, 2)), 5.0) + + +def test_gwyddion_sphere_rejects_empty_dimensions() -> None: + with pytest.raises(TypeError, match="real 2D array"): + _gwyddion_sphere_background(np.zeros((0, 5)), 5.0) + + with pytest.raises(TypeError, match="real 2D array"): + _gwyddion_sphere_background(np.zeros((5, 0)), 5.0) + + +def test_gwyddion_sphere_rejects_boolean_radius() -> None: + data = np.ones((5, 5), dtype=np.float64) + + with pytest.raises(TypeError, match="radius to be a real scalar"): + _gwyddion_sphere_background(data, True) + + with pytest.raises(TypeError, match="radius to be a real scalar"): + _gwyddion_sphere_background(data, False) + + +def test_gwyddion_sphere_rejects_nonfinite_radius() -> None: + data = np.ones((5, 5), dtype=np.float64) + + with pytest.raises(ValueError, match="radius to be finite"): + _gwyddion_sphere_background(data, float("nan")) + + with pytest.raises(ValueError, match="radius to be finite"): + _gwyddion_sphere_background(data, float("inf")) + + +def test_gwyddion_sphere_rejects_radius_below_one() -> None: + data = np.ones((5, 5), dtype=np.float64) + + with pytest.raises(ValueError, match="between 1.0 and 1000.0 samples"): + _gwyddion_sphere_background(data, 0.9) + + +def test_gwyddion_sphere_rejects_radius_above_thousand() -> None: + data = np.ones((5, 5), dtype=np.float64) + + with pytest.raises(ValueError, match="between 1.0 and 1000.0 samples"): + _gwyddion_sphere_background(data, 1000.1) + + +def test_gwyddion_sphere_accepts_radius_boundaries() -> None: + data = np.ones((5, 5), dtype=np.float64) + + bg_min = _gwyddion_sphere_background(data, 1.0) + assert bg_min.shape == (5, 5) + assert np.all(np.isfinite(bg_min)) + + bg_max = _gwyddion_sphere_background(data, 1000.0) + assert bg_max.shape == (5, 5) + assert np.all(np.isfinite(bg_max)) + + +def test_gwyddion_sphere_converts_input_to_float64() -> None: + data_int = np.array([[1, 2], [3, 4]], dtype=np.int32) + bg = _gwyddion_sphere_background(data_int, 2.0) # type: ignore[arg-type] + + assert bg.dtype == np.float64 + + data_f32 = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32) + bg32 = _gwyddion_sphere_background(data_f32, 2.0) # type: ignore[arg-type] + + assert bg32.dtype == np.float64 + + +def test_gwyddion_sphere_does_not_mutate_input() -> None: + data = np.array([[-4.0, -2.0, 0.0], [1.0, 3.0, 7.0], [-1.0, 2.0, 5.0]], dtype=np.float64) + data_copy = data.copy() + + _gwyddion_sphere_background(data, 1.0) + np.testing.assert_array_equal(data, data_copy) + + _gwyddion_sphere_result(data, 1.0, inverted=True) + np.testing.assert_array_equal(data, data_copy) + + +def test_gwyddion_sphere_background_is_c_contiguous_and_read_only() -> None: + data = np.ones((5, 5), dtype=np.float64) + bg = _gwyddion_sphere_background(data, 2.5) + + assert bg.flags.c_contiguous + assert not bg.flags.writeable + + with pytest.raises(ValueError, match="read-only"): + bg[0, 0] = 99.0 + + +def test_gwyddion_sphere_result_arrays_are_distinct_and_read_only() -> None: + data = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64) + bg, corr = _gwyddion_sphere_result(data, 2.0, inverted=False) + + assert bg is not corr + assert bg.base is not corr + assert bg.flags.c_contiguous and not bg.flags.writeable + assert corr.flags.c_contiguous and not corr.flags.writeable + + +def test_gwyddion_sphere_constant_field_returns_identity_background() -> None: + data = np.full((7, 7), 5.0, dtype=np.float64) + bg = _gwyddion_sphere_background(data, 3.0) + + np.testing.assert_allclose(bg, data, atol=0.0, rtol=0.0) + + +def test_gwyddion_sphere_constant_field_corrected_is_zero() -> None: + data = np.full((7, 7), 5.0, dtype=np.float64) + bg, corr = _gwyddion_sphere_result(data, 3.0, inverted=False) + + np.testing.assert_allclose(corr, 0.0, atol=0.0, rtol=0.0) + + +def test_gwyddion_sphere_radius_one_signed_field_matches_frozen_values() -> None: + data = np.array( + [ + [-4.0, -2.0, 0.0], + [1.0, 3.0, 7.0], + [-1.0, 2.0, 5.0], + ], + dtype=np.float64, + ) + + expected_bg = np.array( + [ + [4.5694174231575584, 3.0, 0.0], + [1.0, 3.0, 3.5694174231575579], + [1.5, 2.0, 5.0], + ], + dtype=np.float64, + ) + + expected_corr = np.array( + [ + [-8.5694174231575584, -5.0, 0.0], + [0.0, 0.0, 3.4305825768424421], + [-2.5, 0.0, 0.0], + ], + dtype=np.float64, + ) + + bg, corr = _gwyddion_sphere_result(data, 1.0, inverted=False) + + np.testing.assert_allclose(bg, expected_bg, atol=5e-14, rtol=0.0) + np.testing.assert_allclose(corr, expected_corr, atol=5e-14, rtol=0.0) + + +def test_gwyddion_sphere_very_flat_branch_returns_finite_values() -> None: + data = np.arange(25, dtype=np.float64).reshape((5, 5)) + bg = _gwyddion_sphere_background(data, 50.0) + + assert bg.shape == (5, 5) + assert np.all(np.isfinite(bg)) + + +def test_gwyddion_sphere_uses_xres_for_sphere_size() -> None: + # On non-square grid (5 rows, 10 cols), radius=8 => sphere_size = min(8, 10) = 8. + # On transposed grid (10 rows, 5 cols), radius=8 => sphere_size = min(8, 5) = 5. + data_asym = np.arange(50, dtype=np.float64).reshape((5, 10)) + bg_orig = _gwyddion_sphere_background(data_asym, 8.0) + + data_transposed = data_asym.T + bg_trans = _gwyddion_sphere_background(data_transposed, 8.0) + + # Verify that transposed background is valid and finite + assert bg_orig.shape == (5, 10) + assert bg_trans.shape == (10, 5) + assert np.all(np.isfinite(bg_orig)) + assert np.all(np.isfinite(bg_trans)) + + +def test_gwyddion_sphere_safe_inversion_duality() -> None: + data = np.array( + [ + [-4.0, -2.0, 0.0], + [1.0, 3.0, 7.0], + [-1.0, 2.0, 5.0], + ], + dtype=np.float64, + ) + + bg_inv, _ = _gwyddion_sphere_result(data, 2.5, inverted=True) + neg_bg = _gwyddion_sphere_background(-data, 2.5) + + np.testing.assert_allclose(bg_inv, -neg_bg, atol=0.0, rtol=0.0) + + +def test_gwyddion_sphere_result_reconstructs_input() -> None: + data = np.array( + [ + [-4.0, -2.0, 0.0], + [1.0, 3.0, 7.0], + [-1.0, 2.0, 5.0], + ], + dtype=np.float64, + ) + + for inv in (False, True): + bg, corr = _gwyddion_sphere_result(data, 2.5, inverted=inv) + reconstruction = corr + bg + np.testing.assert_allclose(reconstruction, data, atol=5e-14, rtol=0.0) + + +def test_gwyddion_sphere_corrected_delegates_to_result() -> None: + data = np.array( + [ + [-4.0, -2.0, 0.0], + [1.0, 3.0, 7.0], + [-1.0, 2.0, 5.0], + ], + dtype=np.float64, + ) + + for inv in (False, True): + corr_direct = _gwyddion_sphere_corrected(data, 2.5, inverted=inv) + _, corr_result = _gwyddion_sphere_result(data, 2.5, inverted=inv) + np.testing.assert_allclose(corr_direct, corr_result, atol=0.0, rtol=0.0) + + +def test_gwyddion_sphere_accepts_singleton_2d_fields() -> None: + shapes = [(1, 1), (1, 5), (5, 1)] + for shape in shapes: + data = np.arange(shape[0] * shape[1], dtype=np.float64).reshape(shape) + bg, corr = _gwyddion_sphere_result(data, 2.0, inverted=False) + + assert bg.shape == shape + assert corr.shape == shape + assert np.all(np.isfinite(bg)) + assert np.all(np.isfinite(corr)) + np.testing.assert_allclose(corr + bg, data, atol=5e-14, rtol=0.0) + + +def _channel() -> SPMChannel: + data = np.array( + [ + [-4.0, -2.0, 0.0], + [1.0, 3.0, 7.0], + [-1.0, 2.0, 5.0], + ], + dtype=np.float64, + ) + return SPMChannel( + name="Test Sphere", + data=data, + unit="m", + x_range=8.0e-6, + y_range=5.0e-6, + metadata={"source": "test_gwyddion_sphere"}, + ) + + +def test_estimate_gwyddion_sphere_background_matches_private_result() -> None: + ch = _channel() + for inv in (False, True): + pub_bg = estimate_gwyddion_sphere_revolution_background(ch, 2.5, inverted=inv) + priv_bg, _ = _gwyddion_sphere_result(ch.data, 2.5, inverted=inv) + + np.testing.assert_allclose(pub_bg.data, priv_bg, atol=0.0, rtol=0.0) + + +def test_remove_gwyddion_sphere_background_matches_private_result() -> None: + ch = _channel() + for inv in (False, True): + pub_corr = remove_gwyddion_sphere_revolution_background(ch, 2.5, inverted=inv) + _, priv_corr = _gwyddion_sphere_result(ch.data, 2.5, inverted=inv) + + np.testing.assert_allclose(pub_corr.data, priv_corr, atol=0.0, rtol=0.0) + + +def test_analyze_gwyddion_sphere_background_returns_consistent_result() -> None: + ch = _channel() + for inv in (False, True): + res = analyze_gwyddion_sphere_revolution_background(ch, 2.5, inverted=inv) + + assert isinstance(res, BackgroundResult) + assert res.method == "gwyddion_sphere_revolution" + assert res.parameters == {"radius_px": 2.5, "inverted": inv} + + priv_bg, priv_corr = _gwyddion_sphere_result(ch.data, 2.5, inverted=inv) + np.testing.assert_allclose(res.background.data, priv_bg, atol=0.0, rtol=0.0) + np.testing.assert_allclose(res.corrected.data, priv_corr, atol=0.0, rtol=0.0) + np.testing.assert_allclose( + res.corrected.data + res.background.data, + ch.data, + atol=5e-14, + rtol=0.0, + ) + + +def test_gwyddion_sphere_public_method_and_parameters() -> None: + ch = _channel() + res = analyze_gwyddion_sphere_revolution_background(ch, 15.0, inverted=True) + + assert res.method == "gwyddion_sphere_revolution" + assert res.parameters == {"radius_px": 15.0, "inverted": True} + + +def test_gwyddion_sphere_public_preserves_channel_context() -> None: + ch = _channel() + res = analyze_gwyddion_sphere_revolution_background(ch, 2.5) + + assert res.background.unit == ch.unit + assert res.background.x_range == ch.x_range + assert res.background.y_range == ch.y_range + assert res.background.metadata == ch.metadata + + assert res.corrected.unit == ch.unit + assert res.corrected.x_range == ch.x_range + assert res.corrected.y_range == ch.y_range + assert res.corrected.metadata == ch.metadata + + +def test_gwyddion_sphere_public_does_not_mutate_channel() -> None: + ch = _channel() + ch_data_copy = ch.data.copy() + + estimate_gwyddion_sphere_revolution_background(ch, 2.5) + remove_gwyddion_sphere_revolution_background(ch, 2.5) + analyze_gwyddion_sphere_revolution_background(ch, 2.5) + + np.testing.assert_array_equal(ch.data, ch_data_copy) + + +def test_gwyddion_sphere_public_exports_are_available() -> None: + import spmkit.core.analysis as analysis_mod + + assert hasattr(analysis_mod, "estimate_gwyddion_sphere_revolution_background") + assert hasattr(analysis_mod, "remove_gwyddion_sphere_revolution_background") + assert hasattr(analysis_mod, "analyze_gwyddion_sphere_revolution_background") + + assert "estimate_gwyddion_sphere_revolution_background" in analysis_mod.__all__ + assert "remove_gwyddion_sphere_revolution_background" in analysis_mod.__all__ + assert "analyze_gwyddion_sphere_revolution_background" in analysis_mod.__all__ + + +def test_gwyddion_sphere_physical_api_remains_distinct() -> None: + import spmkit.core.analysis as analysis_mod + + phys_estimate = analysis_mod.estimate_sphere_revolution_background + gwy_estimate = analysis_mod.estimate_gwyddion_sphere_revolution_background + + assert phys_estimate is not gwy_estimate diff --git a/tests/core/test_leveling.py b/tests/core/test_leveling.py index ff6474a..a8e51f1 100644 --- a/tests/core/test_leveling.py +++ b/tests/core/test_leveling.py @@ -3,6 +3,7 @@ from __future__ import annotations import numpy as np +import pytest from spmkit.core.analysis import leveling from spmkit.core.models import SPMChannel @@ -37,3 +38,2281 @@ def test_align_rows() -> None: ch = SPMChannel(name="Z", data=data, unit="m", x_range=1e-6, y_range=1e-6) leveled = leveling.align_rows(ch, method="median") assert np.allclose(leveled.data, 0.0) + + +def test_align_rows_preserves_historical_median_mean_calls() -> None: + """The original default, positional, and keyword calls remain equivalent.""" + data = np.array( + [[1.0, 3.0, 5.0], [10.0, 20.0, 30.0]], + dtype=np.float64, + ) + channel = SPMChannel( + name="legacy", + data=data, + unit="nm", + x_range=3e-6, + y_range=2e-6, + direction="backward", + group="legacy-group", + metadata={"source": "legacy"}, + ) + original = data.copy() + + expected_median = data - np.median(data, axis=1, keepdims=True) + expected_mean = data - np.mean(data, axis=1, keepdims=True) + results = ( + leveling.align_rows(channel), + leveling.align_rows(channel, "median"), + leveling.align_rows(channel, method="median"), + leveling.align_rows(channel, "mean"), + leveling.align_rows(channel, method="mean"), + ) + + for result in results[:3]: + assert np.array_equal(result.data, expected_median) + assert result is not channel + assert not np.shares_memory(result.data, channel.data) + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + + assert np.array_equal(results[3].data, expected_mean) + assert np.array_equal(results[4].data, expected_mean) + assert np.array_equal(channel.data, original) + + with pytest.raises(ValueError): + leveling.align_rows(channel, method="unknown") # type: ignore[arg-type] + + +@pytest.mark.parametrize("method", ["median", "mean"]) +def test_align_rows_legacy_calls_retain_nonfinite_behavior(method: str) -> None: + """Legacy defaults retain origin/main handling outside the strict extension.""" + data = np.array([[1.0, np.nan], [np.inf, 4.0]], dtype=np.float64) + channel = SPMChannel( + name="legacy-nonfinite", + data=data, + unit="nm", + x_range=2e-6, + y_range=2e-6, + ) + + with np.errstate(all="ignore"): + result = leveling.align_rows(channel, method=method) # type: ignore[arg-type] + + if method == "median": + expected = data - np.median(data, axis=1, keepdims=True) + else: + expected = data - np.mean(data, axis=1, keepdims=True) + assert np.array_equal(result.data, expected, equal_nan=True) + + +def test_plane_fit_returns_new_channel_without_mutating_input( + tilted_surface: SPMChannel, +) -> None: + """Plane fitting must preserve the input and channel identity metadata.""" + original_data = tilted_surface.data.copy() + original_metadata = dict(tilted_surface.metadata) + + leveled = leveling.plane_fit(tilted_surface) + + # Un objeto nuevo, no el mismo canal. + assert leveled is not tilted_surface + + # El canal original no fue alterado. + assert np.array_equal(tilted_surface.data, original_data) + assert tilted_surface.metadata == original_metadata + + # Los datos nivelados viven en otro array. + assert leveled.data is not tilted_surface.data + + # Se conserva la identidad física del canal. + assert leveled.name == tilted_surface.name + assert leveled.unit == tilted_surface.unit + assert leveled.x_range == tilted_surface.x_range + assert leveled.y_range == tilted_surface.y_range + assert leveled.direction == tilted_surface.direction + assert leveled.group == tilted_surface.group + + # with_data crea un diccionario exterior nuevo. + assert leveled.metadata == tilted_surface.metadata + assert leveled.metadata is not tilted_surface.metadata + + +def test_zero_mean_sets_arithmetic_mean_to_zero() -> None: + """Zero-mean leveling must shift only the vertical reference.""" + data = np.array( + [ + [10.0, 12.0], + [14.0, 20.0], + ] + ) + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=2e-6, + y_range=3e-6, + direction="backward", + group="Scan backward", + metadata={"source": "synthetic"}, + ) + + original_data = data.copy() + + result = leveling.zero_mean(channel) + + assert np.isclose(np.mean(result.data), 0.0) + # The operation returns independent data without mutating the input. + assert result is not channel + assert result.data is not channel.data + assert np.array_equal(channel.data, original_data) + + # Physical channel context is preserved. + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + + # with_data copies the outer metadata dictionary. + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + # Subtracting a constant must preserve all relative heights. + assert np.allclose( + result.data - result.data[0, 0], + data - data[0, 0], + ) + + +@pytest.mark.parametrize( + ("data", "error_type", "message"), + [ + ( + np.array([1.0, 2.0]), + ValueError, + "zero_mean requires a 2D channel", + ), + ( + np.empty((0, 2), dtype=float), + ValueError, + "zero_mean requires non-empty data", + ), + ( + np.array([["a", "b"], ["c", "d"]]), + TypeError, + "zero_mean requires numeric data", + ), + ( + np.array([[1.0, np.nan], [2.0, 3.0]]), + ValueError, + "zero_mean requires finite data", + ), + ( + np.array([[1.0, np.inf], [2.0, 3.0]]), + ValueError, + "zero_mean requires finite data", + ), + ], + ids=[ + "one-dimensional", + "empty", + "non-numeric", + "nan", + "infinite", + ], +) +def test_zero_mean_rejects_invalid_data( + data: np.ndarray, + error_type: type[Exception], + message: str, +) -> None: + """Invalid inputs must fail explicitly instead of producing bad data.""" + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=2e-6, + y_range=2e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.zero_mean(channel) + + +def test_zero_minimum_sets_lowest_height_to_zero() -> None: + """Minimum leveling must shift only the vertical reference.""" + data = np.array( + [ + [-3.0, 1.0], + [4.0, 9.0], + ] + ) + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=2e-6, + y_range=3e-6, + direction="backward", + group="Scan backward", + metadata={"source": "synthetic"}, + ) + original_data = data.copy() + + result = leveling.zero_minimum(channel) + + assert np.isclose(np.min(result.data), 0.0) + + # Subtracting a constant must preserve all relative heights. + assert np.allclose( + result.data - result.data[0, 0], + data - data[0, 0], + ) + + # Input data and physical channel context are preserved. + assert result is not channel + assert result.data is not channel.data + assert np.array_equal(channel.data, original_data) + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +def test_shift_vertical_adds_requested_offset() -> None: + """Vertical shifting must add the requested offset to every pixel.""" + data = np.array( + [ + [-2.0, 0.0], + [3.0, 7.0], + ] + ) + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=2e-6, + y_range=3e-6, + direction="forward", + group="Scan forward", + metadata={"source": "synthetic"}, + ) + original_data = data.copy() + + result = leveling.shift_vertical(channel, offset=2.5) + + assert np.allclose(result.data, data + 2.5) + + # A constant shift preserves all relative heights. + assert np.allclose( + result.data - result.data[0, 0], + data - data[0, 0], + ) + + # The input remains unchanged and the channel context is preserved. + assert np.array_equal(channel.data, original_data) + assert result is not channel + assert result.data is not channel.data + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +@pytest.mark.parametrize( + ("offset", "error_type", "message"), + [ + ( + "2.5", + TypeError, + "shift_vertical requires a real numeric scalar offset", + ), + ( + [2.5], + TypeError, + "shift_vertical requires a real numeric scalar offset", + ), + ( + True, + TypeError, + "shift_vertical requires a real numeric scalar offset", + ), + ( + 1.0 + 2.0j, + TypeError, + "shift_vertical requires a real numeric scalar offset", + ), + ( + np.nan, + ValueError, + "shift_vertical requires a finite offset", + ), + ( + np.inf, + ValueError, + "shift_vertical requires a finite offset", + ), + ], + ids=[ + "string", + "array", + "boolean", + "complex", + "nan", + "infinite", + ], +) +def test_shift_vertical_rejects_invalid_offsets( + offset: object, + error_type: type[Exception], + message: str, +) -> None: + """Invalid offsets must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.array([[1.0, 2.0], [3.0, 4.0]]), + unit="nm", + x_range=2e-6, + y_range=2e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.shift_vertical(channel, offset=offset) # type: ignore[arg-type] + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_plane_fit_mask_controls_fit_selection(mask_mode: str) -> None: + """Masked plane fitting must ignore an excluded surface feature.""" + rows, cols = 7, 7 + yy, xx = np.mgrid[0:rows, 0:cols] + + background = 2.0 * xx - 0.5 * yy + 10.0 + data = background.copy() + data[3, 3] += 1000.0 + + excluded = np.zeros_like(data, dtype=bool) + excluded[3, 3] = True + + mask = ~excluded if mask_mode == "include" else excluded + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=7e-6, + y_range=7e-6, + ) + + result = leveling.plane_fit( + channel, + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + assert np.allclose(result.data[~excluded], 0.0, atol=1e-10) + assert np.isclose(result.data[3, 3], 1000.0, atol=1e-10) + + +@pytest.mark.parametrize( + ("mask", "mask_mode", "error_type", "message"), + [ + ( + None, + "include", + ValueError, + "plane_fit requires a mask", + ), + ( + np.ones((2, 3), dtype=bool), + "exclude", + ValueError, + "plane_fit requires mask shape to match channel data", + ), + ( + np.ones((4, 4), dtype=int), + "exclude", + TypeError, + "plane_fit requires a boolean mask", + ), + ( + np.zeros((4, 4), dtype=bool), + "include", + ValueError, + "plane_fit requires at least 3 selected points", + ), + ( + None, + "invalid", + ValueError, + "plane_fit mask_mode must be", + ), + ], + ids=[ + "missing-mask", + "wrong-shape", + "non-boolean", + "too-few-points", + "invalid-mode", + ], +) +def test_plane_fit_rejects_invalid_mask_configuration( + mask: object, + mask_mode: str, + error_type: type[Exception], + message: str, +) -> None: + """Invalid mask configurations must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(16.0).reshape(4, 4), + unit="nm", + x_range=4e-6, + y_range=4e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.plane_fit( + channel, + mask=mask, # type: ignore[arg-type] + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + +def test_plane_fit_rejects_collinear_selected_points() -> None: + """Three collinear pixels cannot determine a unique plane.""" + mask = np.zeros((3, 3), dtype=bool) + mask[0, :] = True + + channel = SPMChannel( + name="Z-Axis", + data=np.arange(9.0).reshape(3, 3), + unit="nm", + x_range=3e-6, + y_range=3e-6, + ) + + with pytest.raises( + ValueError, + match="selected points do not define a unique plane", + ): + leveling.plane_fit( + channel, + mask=mask, + mask_mode="include", + ) + + +def test_three_point_level_subtracts_plane_defined_by_reference_points() -> None: + """Three reference pixels must define the plane subtracted from the channel.""" + rows, cols = 5, 6 + yy, xx = np.mgrid[0:rows, 0:cols] + + background = 1.5 * xx - 0.25 * yy + 7.0 + data = background.copy() + data[2, 3] += 4.0 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=6e-6, + y_range=5e-6, + direction="forward", + group="Scan forward", + metadata={"source": "synthetic"}, + ) + original_data = data.copy() + + points = ((0, 0), (0, 5), (4, 0)) + result = leveling.three_point_level(channel, points=points) + + # The three reference pixels define zero height after leveling. + for row, column in points: + assert np.isclose(result.data[row, column], 0.0, atol=1e-12) + + feature_mask = np.ones(data.shape, dtype=bool) + feature_mask[2, 3] = False + + assert np.allclose(result.data[feature_mask], 0.0, atol=1e-12) + assert np.isclose(result.data[2, 3], 4.0, atol=1e-12) + + # Input and physical channel context are preserved. + assert np.array_equal(channel.data, original_data) + assert result is not channel + assert result.data is not channel.data + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +@pytest.mark.parametrize( + ("points", "error_type", "message"), + [ + ( + ((0, 0), (0, 1)), + ValueError, + "three_point_level requires exactly three", + ), + ( + ((0.0, 0.0), (0.0, 2.0), (2.0, 0.0)), + TypeError, + "three_point_level requires integer pixel coordinates", + ), + ( + ((0, 0), (0, 2), (8, 0)), + ValueError, + "three_point_level requires points within channel bounds", + ), + ( + ((0, 0), (0, 1), (0, 2)), + ValueError, + "three_point_level requires three non-collinear points", + ), + ], + ids=[ + "wrong-count", + "non-integer", + "out-of-bounds", + "collinear", + ], +) +def test_three_point_level_rejects_invalid_points( + points: object, + error_type: type[Exception], + message: str, +) -> None: + """Invalid reference-point configurations must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(16.0).reshape(4, 4), + unit="nm", + x_range=4e-6, + y_range=4e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.three_point_level( + channel, + points=points, # type: ignore[arg-type] + ) + + +def test_polynomial_background_total_degree_excludes_masked_feature() -> None: + """Total-degree fitting must preserve an excluded surface feature.""" + rows, cols = 9, 10 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, cols)[np.newaxis, :] + + background = 4.0 + 2.0 * x - 3.0 * y + 0.5 * x**2 + 0.75 * x * y - 0.25 * y**2 + data = background.copy() + data[4, 5] += 50.0 + + excluded = np.zeros(data.shape, dtype=bool) + excluded[4, 5] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=10e-6, + y_range=9e-6, + ) + + result = leveling.polynomial_background( + channel, + degree_mode="total", + degree=2, + mask=excluded, + mask_mode="exclude", + ) + + assert np.allclose(result.data[~excluded], 0.0, atol=1e-10) + assert np.isclose(result.data[4, 5], 50.0, atol=1e-10) + + +def test_polynomial_background_supports_independent_degrees() -> None: + """Independent degrees must permit terms beyond the total-degree limit.""" + rows, cols = 8, 9 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, cols)[np.newaxis, :] + + # x**3 * y requires independent degrees (3, 1). + background = 1.0 + 0.5 * x**2 - 0.25 * y + 2.0 * x**3 * y + + channel = SPMChannel( + name="Z-Axis", + data=background, + unit="nm", + x_range=9e-6, + y_range=8e-6, + ) + + result = leveling.polynomial_background( + channel, + degree_mode="independent", + x_degree=3, + y_degree=1, + ) + + assert np.allclose(result.data, 0.0, atol=1e-10) + + +def test_polynomial_legacy_api_matches_total_degree_background() -> None: + """The legacy polynomial API must retain its current total-degree meaning.""" + rows, cols = 6, 7 + yy, xx = np.mgrid[0:rows, 0:cols] + data = 3.0 + 2.0 * xx - yy + 0.25 * xx**2 + 0.5 * xx * yy + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=7e-6, + y_range=6e-6, + ) + + legacy = leveling.polynomial(channel, order=2) + explicit = leveling.polynomial_background( + channel, + degree_mode="total", + degree=2, + ) + + assert np.allclose(legacy.data, explicit.data, atol=1e-10) + + +@pytest.mark.parametrize( + ("kwargs", "error_type", "message"), + [ + ( + {"degree_mode": "unknown"}, + ValueError, + "polynomial_background degree_mode must be", + ), + ( + {"degree_mode": "total", "degree": True}, + TypeError, + "polynomial_background requires degree to be a non-negative integer", + ), + ( + {"degree_mode": "total", "degree": -1}, + ValueError, + "polynomial_background requires degree to be non-negative", + ), + ( + { + "degree_mode": "total", + "degree": 2, + "x_degree": 2, + }, + ValueError, + "total degree mode does not accept x_degree or y_degree", + ), + ( + {"degree_mode": "independent"}, + ValueError, + "independent degree mode requires x_degree and y_degree", + ), + ], + ids=[ + "invalid-mode", + "boolean-degree", + "negative-degree", + "total-with-axis-degree", + "independent-missing-degrees", + ], +) +def test_polynomial_background_rejects_invalid_degree_configuration( + kwargs: dict[str, object], + error_type: type[Exception], + message: str, +) -> None: + """Invalid polynomial degree configurations must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(25.0).reshape(5, 5), + unit="nm", + x_range=5e-6, + y_range=5e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.polynomial_background( + channel, + **kwargs, # type: ignore[arg-type] + ) + + +def test_polynomial_background_rejects_rank_deficient_selection() -> None: + """Selected pixels must determine every requested polynomial term.""" + data = np.arange(25.0).reshape(5, 5) + mask = np.zeros(data.shape, dtype=bool) + mask[0, :] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=5e-6, + ) + + with pytest.raises( + ValueError, + match="selected points do not define a unique polynomial background", + ): + leveling.polynomial_background( + channel, + degree_mode="independent", + x_degree=1, + y_degree=1, + mask=mask, + mask_mode="include", + ) + + +def test_align_rows_can_preserve_global_mean() -> None: + """Mean-preserving alignment must keep the absolute global level.""" + data = np.array( + [ + [1.0, 1.0, 1.0], + [2.0, 2.0, 2.0], + [6.0, 6.0, 6.0], + ] + ) + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=3e-6, + y_range=3e-6, + ) + + result = leveling.align_rows( + channel, + method="median", + preserve_mean=True, + ) + + expected_level = np.mean(data) + + assert np.allclose( + np.mean(result.data, axis=1), + expected_level, + ) + assert np.isclose(np.mean(result.data), np.mean(data)) + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_align_rows_mask_controls_row_statistic(mask_mode: str) -> None: + """Masked row alignment must ignore excluded surface features.""" + data = np.array( + [ + [1.0, 1.0, 100.0], + [2.0, 2.0, 200.0], + ] + ) + excluded = np.zeros(data.shape, dtype=bool) + excluded[:, 2] = True + + mask = ~excluded if mask_mode == "include" else excluded + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=3e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="mean", + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + assert np.allclose(result.data[~excluded], 0.0) + assert np.allclose(result.data[:, 2], [99.0, 198.0]) + + +@pytest.mark.parametrize( + ("trim_fraction", "reference_method"), + [ + (0.0, "mean"), + (0.5, "median"), + ], + ids=["no-trimming-is-mean", "maximum-trimming-is-median"], +) +def test_align_rows_trimmed_mean_endpoints( + trim_fraction: float, + reference_method: str, +) -> None: + """Trimmed mean must interpolate between mean and median.""" + data = np.array( + [ + [0.0, 1.0, 2.0, 100.0], + [4.0, 5.0, 6.0, 200.0], + ] + ) + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=4e-6, + y_range=2e-6, + ) + + trimmed = leveling.align_rows( + channel, + method="trimmed_mean", + trim_fraction=trim_fraction, + ) + reference = leveling.align_rows( + channel, + method=reference_method, # type: ignore[arg-type] + ) + + assert np.allclose(trimmed.data, reference.data) + + +@pytest.mark.parametrize( + ("kwargs", "error_type", "message"), + [ + ( + {"method": "unknown"}, + ValueError, + "align_rows method must be", + ), + ( + {"method": "trimmed_mean", "trim_fraction": True}, + TypeError, + "align_rows requires trim_fraction to be a real scalar", + ), + ( + {"method": "trimmed_mean", "trim_fraction": -0.1}, + ValueError, + "align_rows requires trim_fraction between 0 and 0.5", + ), + ( + {"method": "trimmed_mean", "trim_fraction": 0.6}, + ValueError, + "align_rows requires trim_fraction between 0 and 0.5", + ), + ( + {"preserve_mean": "yes"}, + TypeError, + "align_rows requires preserve_mean to be boolean", + ), + ], + ids=[ + "unknown-method", + "boolean-trim", + "negative-trim", + "excessive-trim", + "non-boolean-preserve-mean", + ], +) +def test_align_rows_rejects_invalid_configuration( + kwargs: dict[str, object], + error_type: type[Exception], + message: str, +) -> None: + """Invalid row-alignment configurations must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(12.0).reshape(3, 4), + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.align_rows( + channel, + **kwargs, # type: ignore[arg-type] + ) + + +def test_align_rows_rejects_rows_without_selected_points() -> None: + """Every row must contain data selected for its statistic.""" + data = np.arange(12.0).reshape(3, 4) + mask = np.ones(data.shape, dtype=bool) + mask[1, :] = False + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + with pytest.raises( + ValueError, + match="align_rows requires at least 1 selected point in every row", + ): + leveling.align_rows( + channel, + method="median", + mask=mask, + mask_mode="include", + ) + + +def test_align_rows_polynomial_removes_row_background_and_preserves_feature() -> None: + """Polynomial row alignment must preserve excluded surface features.""" + rows, columns = 4, 9 + x = np.linspace(-1.0, 1.0, columns) + + offsets = np.array([1.0, 3.0, -2.0, 5.0]) + slopes = np.array([0.5, -1.0, 2.0, -0.25]) + curvatures = np.array([0.2, -0.4, 0.75, 0.1]) + + data = np.vstack( + [offsets[row] + slopes[row] * x + curvatures[row] * x**2 for row in range(rows)] + ) + + data[2, 4] += 12.0 + + excluded = np.zeros(data.shape, dtype=bool) + excluded[2, 4] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=9e-6, + y_range=4e-6, + metadata={"source": "synthetic"}, + ) + + result = leveling.align_rows( + channel, + method="polynomial", + polynomial_degree=2, + mask=excluded, + mask_mode="exclude", + ) + + assert np.allclose(result.data[~excluded], 0.0, atol=1e-10) + assert np.isclose(result.data[2, 4], 12.0, atol=1e-10) + + +def test_align_rows_polynomial_degree_zero_matches_mean() -> None: + """A degree-zero row polynomial must reproduce mean alignment.""" + data = np.array( + [ + [1.0, 2.0, 6.0], + [4.0, 8.0, 12.0], + ] + ) + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=3e-6, + y_range=2e-6, + ) + + polynomial = leveling.align_rows( + channel, + method="polynomial", + polynomial_degree=0, + ) + mean = leveling.align_rows( + channel, + method="mean", + ) + + assert np.allclose(polynomial.data, mean.data) + + +def test_align_rows_polynomial_can_preserve_global_mean() -> None: + """Mean-preserving polynomial alignment must retain the global level.""" + columns = 7 + x = np.linspace(-1.0, 1.0, columns) + + data = np.vstack( + [ + 5.0 + x, + 8.0 - 2.0 * x, + 12.0 + 0.5 * x, + ] + ) + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=7e-6, + y_range=3e-6, + ) + + result = leveling.align_rows( + channel, + method="polynomial", + polynomial_degree=1, + preserve_mean=True, + ) + + assert np.isclose(np.mean(result.data), np.mean(data)) + assert np.allclose( + result.data, + np.mean(data), + atol=1e-10, + ) + + +@pytest.mark.parametrize( + ("degree", "error_type", "message"), + [ + ( + True, + TypeError, + "align_rows requires polynomial_degree to be a non-negative integer", + ), + ( + 1.5, + TypeError, + "align_rows requires polynomial_degree to be a non-negative integer", + ), + ( + -1, + ValueError, + "align_rows requires polynomial_degree to be non-negative", + ), + ], + ids=[ + "boolean", + "non-integer", + "negative", + ], +) +def test_align_rows_polynomial_rejects_invalid_degree( + degree: object, + error_type: type[Exception], + message: str, +) -> None: + """Polynomial row degree must be a valid non-negative integer.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(12.0).reshape(3, 4), + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.align_rows( + channel, + method="polynomial", + polynomial_degree=degree, # type: ignore[arg-type] + ) + + +def test_align_rows_polynomial_rejects_rank_deficient_row() -> None: + """Every row must contain enough independent points for its polynomial.""" + data = np.arange(15.0).reshape(3, 5) + mask = np.ones(data.shape, dtype=bool) + mask[1, :] = False + mask[1, :2] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=3e-6, + ) + + with pytest.raises( + ValueError, + match="align_rows requires at least 3 selected points in every row", + ): + leveling.align_rows( + channel, + method="polynomial", + polynomial_degree=2, + mask=mask, + mask_mode="include", + ) + + +def test_align_rows_median_difference_preserves_large_feature() -> None: + """Median differences must align offsets without flattening shared features.""" + base_profile = np.array([0.0, 0.0, 8.0, 8.0, 8.0, 0.0, 0.0]) + row_offsets = np.array([1.0, -1.0, -1.0, 1.0]) + + data = base_profile[np.newaxis, :] + row_offsets[:, np.newaxis] + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=7e-6, + y_range=4e-6, + ) + + result = leveling.align_rows( + channel, + method="median_difference", + ) + + expected = base_profile + row_offsets[0] + + assert np.allclose( + result.data, + expected[np.newaxis, :], + atol=1e-12, + ) + + +def test_align_rows_median_difference_preserves_global_tilt() -> None: + """Difference alignment must preserve the linear slow-axis trend.""" + rows, columns = 7, 9 + row_coordinate = np.arange(rows, dtype=float) + base_profile = np.linspace(-2.0, 3.0, columns) + + global_tilt = 1.75 * row_coordinate + row_defects = np.array([0.0, 2.0, -1.0, 1.5, -2.0, 1.0, 0.0]) + + data = base_profile[np.newaxis, :] + global_tilt[:, np.newaxis] + row_defects[:, np.newaxis] + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=9e-6, + y_range=7e-6, + ) + + result = leveling.align_rows( + channel, + method="median_difference", + preserve_tilt=True, + preserve_mean=True, + ) + + original_slope = np.polyfit( + row_coordinate, + np.mean(data, axis=1), + deg=1, + )[0] + corrected_slope = np.polyfit( + row_coordinate, + np.mean(result.data, axis=1), + deg=1, + )[0] + + assert np.isclose(corrected_slope, original_slope, atol=1e-12) + + +def test_align_rows_median_difference_can_remove_global_tilt() -> None: + """Tilt preservation must be explicitly disableable.""" + rows, columns = 5, 6 + row_coordinate = np.arange(rows, dtype=float) + base_profile = np.linspace(0.0, 2.0, columns) + + data = base_profile[np.newaxis, :] + 3.0 * row_coordinate[:, np.newaxis] + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=6e-6, + y_range=5e-6, + ) + + result = leveling.align_rows( + channel, + method="median_difference", + preserve_tilt=False, + ) + + assert np.allclose( + result.data, + result.data[0], + atol=1e-12, + ) + + +def test_align_rows_trimmed_mean_difference_half_matches_median_difference() -> None: + """Maximum trimming must reproduce median-difference alignment.""" + data = np.array( + [ + [0.0, 1.0, 2.0, 80.0, 4.0], + [2.0, 3.0, 4.0, 150.0, 6.0], + [-1.0, 0.0, 1.0, -100.0, 3.0], + ] + ) + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=3e-6, + ) + + trimmed = leveling.align_rows( + channel, + method="trimmed_mean_difference", + trim_fraction=0.5, + ) + median = leveling.align_rows( + channel, + method="median_difference", + ) + + assert np.allclose(trimmed.data, median.data) + + +@pytest.mark.parametrize( + ("preserve_tilt", "error_type", "message"), + [ + ( + "yes", + TypeError, + "align_rows requires preserve_tilt to be boolean", + ), + ( + 1, + TypeError, + "align_rows requires preserve_tilt to be boolean", + ), + ], + ids=["string", "integer"], +) +def test_align_rows_rejects_invalid_preserve_tilt( + preserve_tilt: object, + error_type: type[Exception], + message: str, +) -> None: + """Tilt-preservation configuration must be explicitly boolean.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(12.0).reshape(3, 4), + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.align_rows( + channel, + method="median_difference", + preserve_tilt=preserve_tilt, # type: ignore[arg-type] + ) + + +def test_align_rows_difference_requires_shared_selected_pixels() -> None: + """Adjacent rows must share at least one selected column.""" + data = np.arange(12.0).reshape(3, 4) + mask = np.zeros(data.shape, dtype=bool) + mask[0, :2] = True + mask[1, 2:] = True + mask[2, 2:] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + with pytest.raises( + ValueError, + match="align_rows requires adjacent rows to share selected points", + ): + leveling.align_rows( + channel, + method="median_difference", + mask=mask, + mask_mode="include", + ) + + +def test_align_rows_matching_downweights_local_slope_mismatch() -> None: + """Matching must downweight a local defect with incompatible slopes.""" + columns = 9 + base_profile = np.linspace(-1.0, 1.0, columns) + + data = np.vstack( + [ + base_profile, + base_profile + 2.0, + ] + ) + data[1, 4] += 100.0 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=9e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="matching", + preserve_tilt=False, + ) + + clean_columns = np.ones(columns, dtype=bool) + clean_columns[4] = False + + assert np.allclose( + result.data[1, clean_columns], + result.data[0, clean_columns], + atol=1e-2, + ) + assert result.data[1, 4] - result.data[0, 4] > 99.0 + + +def test_align_rows_matching_aligns_constant_row_offsets() -> None: + """Matching must exactly align rows differing only by vertical offsets.""" + base_profile = np.array([0.0, 1.0, 3.0, 2.0, -1.0, 4.0, 5.0]) + offsets = np.array([0.0, 3.0, -2.0, 1.0]) + + data = base_profile[np.newaxis, :] + offsets[:, np.newaxis] + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=7e-6, + y_range=4e-6, + ) + + result = leveling.align_rows( + channel, + method="matching", + preserve_tilt=False, + ) + + assert np.allclose( + result.data, + base_profile[np.newaxis, :], + atol=1e-12, + ) + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_align_rows_matching_respects_mask_selection( + mask_mode: str, +) -> None: + """Matching must estimate offsets only from selected neighbouring pixels.""" + data = np.array( + [ + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [3.0, 3.0, 3.0, 50.0, 50.0, 50.0], + ] + ) + + excluded = np.zeros(data.shape, dtype=bool) + excluded[:, 3:] = True + + mask = ~excluded if mask_mode == "include" else excluded + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=6e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="matching", + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + preserve_tilt=False, + ) + + assert np.allclose(result.data[1, :3], 0.0, atol=1e-12) + assert np.allclose(result.data[1, 3:], 47.0, atol=1e-12) + + +def test_align_rows_matching_preserves_global_tilt() -> None: + """Matching must preserve slow-axis tilt when requested.""" + rows, columns = 6, 8 + row_coordinate = np.arange(rows, dtype=float) + base_profile = np.linspace(-2.0, 3.0, columns) + + global_tilt = 1.25 * row_coordinate + row_defects = np.array([0.0, 2.0, -1.0, 1.5, -2.0, 0.5]) + + data = base_profile[np.newaxis, :] + global_tilt[:, np.newaxis] + row_defects[:, np.newaxis] + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=8e-6, + y_range=6e-6, + ) + + result = leveling.align_rows( + channel, + method="matching", + preserve_tilt=True, + preserve_mean=True, + ) + + original_slope = np.polyfit( + row_coordinate, + np.mean(data, axis=1), + deg=1, + )[0] + corrected_slope = np.polyfit( + row_coordinate, + np.mean(result.data, axis=1), + deg=1, + )[0] + + assert np.isclose( + corrected_slope, + original_slope, + atol=1e-12, + ) + + +def test_align_rows_matching_requires_shared_selected_edges() -> None: + """Adjacent rows must share a selected neighbouring-pixel pair.""" + data = np.arange(15.0).reshape(3, 5) + + mask = np.zeros(data.shape, dtype=bool) + mask[0, [0, 2, 4]] = True + mask[1, [0, 2, 4]] = True + mask[2, [0, 2, 4]] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=3e-6, + ) + + with pytest.raises( + ValueError, + match=( + "align_rows matching requires adjacent rows to share " "selected neighbouring pixels" + ), + ): + leveling.align_rows( + channel, + method="matching", + mask=mask, + mask_mode="include", + ) + + +def test_align_rows_mode_tracks_dominant_row_level() -> None: + """Mode alignment must follow the densest cluster in each row.""" + data = np.array( + [ + [1.0, 1.0, 1.0, 8.0, 20.0], + [4.0, 4.0, 4.0, -10.0, 30.0], + ] + ) + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="mode", + ) + + expected = np.array( + [ + [0.0, 0.0, 0.0, 7.0, 19.0], + [0.0, 0.0, 0.0, -14.0, 26.0], + ] + ) + + assert np.allclose(result.data, expected) + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_align_rows_mode_respects_mask_selection( + mask_mode: str, +) -> None: + """Modal row level must be estimated only from selected pixels.""" + data = np.array( + [ + [1.0, 1.0, 1.0, 100.0, 200.0], + [2.0, 2.0, 2.0, -50.0, 80.0], + ] + ) + + excluded = np.zeros(data.shape, dtype=bool) + excluded[:, 3:] = True + + mask = ~excluded if mask_mode == "include" else excluded + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="mode", + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + assert np.allclose(result.data[:, :3], 0.0) + assert np.allclose( + result.data[:, 3:], + np.array( + [ + [99.0, 199.0], + [-52.0, 78.0], + ] + ), + ) + + +def test_align_rows_mode_can_preserve_global_mean() -> None: + """Mean-preserving mode alignment must retain the global level.""" + data = np.array( + [ + [1.0, 1.0, 1.0, 9.0], + [4.0, 4.0, 4.0, 20.0], + [8.0, 8.0, 8.0, -5.0], + ] + ) + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + result = leveling.align_rows( + channel, + method="mode", + preserve_mean=True, + ) + + assert np.isclose( + np.mean(result.data), + np.mean(data), + ) + + +def test_align_rows_mode_supports_one_selected_point_per_row() -> None: + """A single selected pixel must define the modal row level.""" + data = np.array( + [ + [1.0, 5.0, 9.0], + [2.0, 6.0, 10.0], + ] + ) + + mask = np.zeros(data.shape, dtype=bool) + mask[:, 1] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=3e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="mode", + mask=mask, + mask_mode="include", + ) + + assert np.allclose(result.data[:, 1], 0.0) + assert np.allclose( + result.data, + np.array( + [ + [-4.0, 0.0, 4.0], + [-4.0, 0.0, 4.0], + ] + ), + ) + + +def test_align_rows_facet_tilt_removes_row_slopes_preserving_offsets() -> None: + """Facet tilt must remove row slopes without changing row offsets.""" + columns = 9 + x = np.linspace(-1.0, 1.0, columns) + + offsets = np.array([2.0, -3.0, 7.0]) + slopes = np.array([1.5, -2.0, 0.75]) + + data = np.vstack([offsets[row] + slopes[row] * x for row in range(offsets.size)]) + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=9e-6, + y_range=3e-6, + ) + + result = leveling.align_rows( + channel, + method="facet_tilt", + ) + + expected = np.repeat( + offsets[:, np.newaxis], + columns, + axis=1, + ) + + assert np.allclose(result.data, expected, atol=1e-12) + assert np.allclose( + np.mean(result.data, axis=1), + np.mean(data, axis=1), + atol=1e-12, + ) + + +def test_align_rows_facet_tilt_is_robust_to_local_spike() -> None: + """A local spike must not dominate the prevalent row slope.""" + columns = 9 + x = np.linspace(-1.0, 1.0, columns) + + data = np.vstack( + [ + 3.0 + 2.0 * x, + -4.0 - 1.5 * x, + ] + ) + data[0, 4] += 100.0 + data[1, 5] -= 80.0 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=9e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="facet_tilt", + ) + + clean_first = np.ones(columns, dtype=bool) + clean_first[4] = False + + clean_second = np.ones(columns, dtype=bool) + clean_second[5] = False + + assert np.allclose( + result.data[0, clean_first], + 3.0, + atol=1e-12, + ) + assert np.allclose( + result.data[1, clean_second], + -4.0, + atol=1e-12, + ) + assert np.isclose(result.data[0, 4], 103.0, atol=1e-12) + assert np.isclose(result.data[1, 5], -84.0, atol=1e-12) + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_align_rows_facet_tilt_respects_mask_selection( + mask_mode: str, +) -> None: + """Facet tilt must estimate slopes only from selected adjacent pixels.""" + columns = 8 + x = np.linspace(-1.0, 1.0, columns) + + data = np.vstack( + [ + 2.0 + 3.0 * x, + -1.0 - 2.0 * x, + ] + ) + + data[:, 5:] += np.array([[20.0], [-30.0]]) + + excluded = np.zeros(data.shape, dtype=bool) + excluded[:, 5:] = True + + mask = ~excluded if mask_mode == "include" else excluded + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=8e-6, + y_range=2e-6, + ) + + result = leveling.align_rows( + channel, + method="facet_tilt", + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + assert np.allclose(result.data[0, :5], 2.0, atol=1e-12) + assert np.allclose(result.data[1, :5], -1.0, atol=1e-12) + + assert np.allclose(result.data[0, 5:], 22.0, atol=1e-12) + assert np.allclose(result.data[1, 5:], -31.0, atol=1e-12) + + +def test_align_rows_facet_tilt_requires_selected_adjacent_pixels() -> None: + """Each row must contain a selected neighbouring-pixel pair.""" + data = np.arange(15.0).reshape(3, 5) + + mask = np.zeros(data.shape, dtype=bool) + mask[:, [0, 2, 4]] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=3e-6, + ) + + with pytest.raises( + ValueError, + match=("align_rows facet_tilt requires selected " "neighbouring pixels in every row"), + ): + leveling.align_rows( + channel, + method="facet_tilt", + mask=mask, + mask_mode="include", + ) + + +def test_facet_level_flattens_exact_plane_and_preserves_context() -> None: + """Facet levelling must remove an exact plane without mutating the input.""" + rows, columns = 9, 10 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + data = 4.0 + 2.0 * x - 3.0 * y + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=10e-6, + y_range=9e-6, + direction="forward", + group="Scan forward", + metadata={"source": "synthetic"}, + ) + original_data = data.copy() + + result = leveling.facet_level(channel) + + assert np.allclose(result.data, 0.0, atol=1e-12) + + assert np.array_equal(channel.data, original_data) + assert result is not channel + assert result.data is not channel.data + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +def test_facet_level_preserves_large_raised_feature() -> None: + """Facet levelling must remove tilt without flattening a raised plateau.""" + rows, columns = 11, 12 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + background = 5.0 + 1.25 * x - 0.75 * y + data = background.copy() + + feature = np.zeros(data.shape, dtype=bool) + feature[4:7, 5:8] = True + data[feature] += 20.0 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=12e-6, + y_range=11e-6, + ) + + result = leveling.facet_level(channel) + + base_values = result.data[~feature] + feature_values = result.data[feature] + + assert np.allclose( + base_values, + np.mean(base_values), + atol=1e-10, + ) + assert np.allclose( + feature_values, + np.mean(feature_values), + atol=1e-10, + ) + assert np.isclose( + np.mean(feature_values) - np.mean(base_values), + 20.0, + atol=1e-10, + ) + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_facet_level_respects_mask_selection(mask_mode: str) -> None: + """Facet levelling must estimate its plane only from selected regions.""" + rows, columns = 11, 12 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + background = 3.0 - 2.0 * x + 0.5 * y + data = background.copy() + + excluded = np.zeros(data.shape, dtype=bool) + excluded[4:7, 5:8] = True + data[excluded] += 15.0 + + mask = ~excluded if mask_mode == "include" else excluded + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=12e-6, + y_range=11e-6, + ) + + result = leveling.facet_level( + channel, + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + assert np.allclose(result.data[~excluded], 0.0, atol=1e-10) + assert np.allclose(result.data[excluded], 15.0, atol=1e-10) + + +def test_facet_level_can_preserve_global_mean() -> None: + """Optional mean preservation must retain the absolute vertical level.""" + rows, columns = 8, 9 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + data = 7.0 + 1.5 * x - 2.5 * y + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=9e-6, + y_range=8e-6, + ) + + result = leveling.facet_level( + channel, + preserve_mean=True, + ) + + assert np.isclose(np.mean(result.data), np.mean(data)) + assert np.allclose(result.data, np.mean(data), atol=1e-12) + + +@pytest.mark.parametrize( + ("kwargs", "error_type", "message"), + [ + ( + {"max_iterations": True}, + TypeError, + "facet_level requires max_iterations to be a positive integer", + ), + ( + {"max_iterations": 0}, + ValueError, + "facet_level requires max_iterations to be positive", + ), + ( + {"tolerance": 0.0}, + ValueError, + "facet_level requires tolerance to be positive", + ), + ( + {"preserve_mean": "yes"}, + TypeError, + "facet_level requires preserve_mean to be boolean", + ), + ], + ids=[ + "boolean-iterations", + "zero-iterations", + "zero-tolerance", + "non-boolean-preserve-mean", + ], +) +def test_facet_level_rejects_invalid_configuration( + kwargs: dict[str, object], + error_type: type[Exception], + message: str, +) -> None: + """Invalid facet-level configuration must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(25.0).reshape(5, 5), + unit="nm", + x_range=5e-6, + y_range=5e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.facet_level( + channel, + **kwargs, # type: ignore[arg-type] + ) + + +def test_facet_level_requires_selected_local_facets() -> None: + """Selected pixels must form at least one complete local facet.""" + data = np.arange(36.0).reshape(6, 6) + + mask = np.zeros(data.shape, dtype=bool) + mask[::2, ::2] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=6e-6, + y_range=6e-6, + ) + + with pytest.raises( + ValueError, + match="facet_level requires selected neighbouring pixel cells", + ): + leveling.facet_level( + channel, + mask=mask, + mask_mode="include", + ) + + +def test_rotate_level_flattens_exact_physical_plane_and_preserves_context() -> None: + """Level rotation must flatten a physical plane without mutating its channel.""" + rows, columns = 13, 15 + x_range = 14e-6 + y_range = 12e-6 + + x = (np.arange(columns, dtype=float) + 0.5) * x_range / columns - 0.5 * x_range + y = (np.arange(rows, dtype=float) + 0.5) * y_range / rows - 0.5 * y_range + xx, yy = np.meshgrid(x, y) + + x_slope = 0.08 + y_slope = -0.05 + intercept = 7e-9 + + data = (intercept + x_slope * xx + y_slope * yy) / 1e-9 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=x_range, + y_range=y_range, + direction="forward", + group="Scan forward", + metadata={"source": "synthetic"}, + ) + original_data = data.copy() + + result = leveling.rotate_level(channel) + + assert np.allclose(result.data, 0.0, atol=1e-6) + + assert np.array_equal(channel.data, original_data) + assert result is not channel + assert result.data is not channel.data + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +@pytest.mark.parametrize("mask_mode", ["include", "exclude"]) +def test_rotate_level_respects_mask_selection_and_rotates_feature_height( + mask_mode: str, +) -> None: + """The mask must control plane fitting while relief is geometrically rotated.""" + rows = columns = 21 + x_range = y_range = 20e-6 + + x = (np.arange(columns, dtype=float) + 0.5) * x_range / columns - 0.5 * x_range + y = (np.arange(rows, dtype=float) + 0.5) * y_range / rows - 0.5 * y_range + xx, yy = np.meshgrid(x, y) + + x_slope = 0.10 + y_slope = -0.05 + intercept = 5e-9 + feature_height = 20.0 + + data = (intercept + x_slope * xx + y_slope * yy) / 1e-9 + + feature = np.zeros(data.shape, dtype=bool) + feature[7:15, 10:19] = True + data[feature] += feature_height + + mask = ~feature if mask_mode == "include" else feature + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=x_range, + y_range=y_range, + ) + + result = leveling.rotate_level( + channel, + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + base_probe = np.zeros(data.shape, dtype=bool) + base_probe[2:6, 2:6] = True + + feature_probe = np.zeros(data.shape, dtype=bool) + feature_probe[9:13, 12:17] = True + + normal_z = 1.0 / np.sqrt(1.0 + x_slope**2 + y_slope**2) + expected_feature_height = feature_height * normal_z + + assert np.allclose( + result.data[base_probe], + 0.0, + atol=1e-5, + ) + assert np.allclose( + result.data[feature_probe], + expected_feature_height, + atol=1e-5, + ) + + +def test_rotate_level_can_preserve_global_mean() -> None: + """Mean preservation must retain the original absolute vertical level.""" + rows, columns = 9, 11 + x_range = 10e-6 + y_range = 8e-6 + + x = (np.arange(columns, dtype=float) + 0.5) * x_range / columns - 0.5 * x_range + y = (np.arange(rows, dtype=float) + 0.5) * y_range / rows - 0.5 * y_range + xx, yy = np.meshgrid(x, y) + + data = (12e-9 + 0.04 * xx - 0.03 * yy) / 1e-9 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=x_range, + y_range=y_range, + ) + + result = leveling.rotate_level( + channel, + preserve_mean=True, + ) + + assert np.isclose( + np.mean(result.data), + np.mean(data), + atol=1e-12, + ) + assert np.allclose( + result.data, + np.mean(data), + atol=1e-6, + ) + + +def test_rotate_level_constant_fill_marks_exterior_pixels() -> None: + """Constant fill must explicitly identify pixels outside the source domain.""" + rows = columns = 11 + x_range = y_range = 10e-6 + + x = (np.arange(columns, dtype=float) + 0.5) * x_range / columns - 0.5 * x_range + y = (np.arange(rows, dtype=float) + 0.5) * y_range / rows - 0.5 * y_range + xx, yy = np.meshgrid(x, y) + + data = (3e-9 + 0.45 * xx - 0.30 * yy) / 1e-9 + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=x_range, + y_range=y_range, + ) + + result = leveling.rotate_level( + channel, + fill_mode="constant", + fill_value=-7.0, + ) + + assert np.all(np.isfinite(result.data)) + assert np.any(result.data == -7.0) + + inside = result.data != -7.0 + assert np.allclose( + result.data[inside], + 0.0, + atol=1e-5, + ) + + +def test_rotate_level_rejects_non_geometric_channel_unit() -> None: + """True geometric rotation requires Z to represent a physical length.""" + channel = SPMChannel( + name="Current", + data=np.arange(25.0).reshape(5, 5), + unit="V", + x_range=5e-6, + y_range=5e-6, + ) + + with pytest.raises( + ValueError, + match="unsupported geometric length unit", + ): + leveling.rotate_level(channel) + + +@pytest.mark.parametrize( + ("kwargs", "error_type", "message"), + [ + ( + {"interpolation": "cubic"}, + ValueError, + "rotate_level interpolation must be 'linear'", + ), + ( + {"fill_mode": "nan"}, + ValueError, + "rotate_level fill_mode must be 'nearest' or 'constant'", + ), + ( + {"preserve_mean": "yes"}, + TypeError, + "rotate_level requires preserve_mean to be boolean", + ), + ( + {"fill_value": True}, + TypeError, + "rotate_level requires fill_value to be a real scalar", + ), + ], + ids=[ + "unsupported-interpolation", + "unsupported-fill-mode", + "non-boolean-preserve-mean", + "boolean-fill-value", + ], +) +def test_rotate_level_rejects_invalid_configuration( + kwargs: dict[str, object], + error_type: type[Exception], + message: str, +) -> None: + """Invalid level-rotation settings must fail explicitly.""" + channel = SPMChannel( + name="Z-Axis", + data=np.arange(25.0).reshape(5, 5), + unit="nm", + x_range=5e-6, + y_range=5e-6, + ) + + with pytest.raises(error_type, match=message): + leveling.rotate_level( + channel, + **kwargs, # type: ignore[arg-type] + ) + + +def test_rotate_level_rejects_rank_deficient_plane_selection() -> None: + """Selected pixels must determine a unique physical plane.""" + data = np.arange(25.0).reshape(5, 5) + + mask = np.zeros(data.shape, dtype=bool) + mask[0, :] = True + + channel = SPMChannel( + name="Z-Axis", + data=data, + unit="nm", + x_range=5e-6, + y_range=5e-6, + ) + + with pytest.raises( + ValueError, + match=("rotate_level selected points do not define " "a unique plane"), + ): + leveling.rotate_level( + channel, + mask=mask, + mask_mode="include", + ) diff --git a/tests/core/test_median_background.py b/tests/core/test_median_background.py new file mode 100644 index 0000000..cf79cc4 --- /dev/null +++ b/tests/core/test_median_background.py @@ -0,0 +1,481 @@ +"""Tests for circular local-median background estimation.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from spmkit.core.analysis.background import ( + _median_disk_footprint, + estimate_median_background, + remove_median_background, +) +from spmkit.core.models import SPMChannel + + +def _channel( + data: np.ndarray, + *, + unit: str = "nm", + x_range: float | None = None, + y_range: float | None = None, +) -> SPMChannel: + rows, columns = data.shape + + return SPMChannel( + name="Signal", + data=np.asarray(data), + unit=unit, + x_range=float(columns) if x_range is None else x_range, + y_range=float(rows) if y_range is None else y_range, + direction="forward", + group="Synthetic", + metadata={"source": "median-background-test"}, + ) + + +def _disk_offsets( + radius_pixels: int, +) -> list[tuple[int, int]]: + """Independent pixel-centre ellipse oracle.""" + diameter = 2 * radius_pixels + 1 + + return [ + (row_offset, column_offset) + for row_offset in range( + -radius_pixels, + radius_pixels + 1, + ) + for column_offset in range( + -radius_pixels, + radius_pixels + 1, + ) + if (2 * row_offset) ** 2 + (2 * column_offset) ** 2 <= diameter**2 + ] + + +def _nearest_index(index: int, size: int) -> int: + """Map an index using nearest boundary extension.""" + return min(max(index, 0), size - 1) + + +def _brute_force_border_extend_median( + data: np.ndarray, + *, + radius_pixels: int, +) -> np.ndarray: + """Independent circular-median oracle with extended borders.""" + values = np.asarray(data, dtype=float) + rows, columns = values.shape + offsets = _disk_offsets(radius_pixels) + result = np.empty_like(values) + + for row in range(rows): + for column in range(columns): + neighbourhood = [ + values[ + _nearest_index(row + y_offset, rows), + _nearest_index(column + x_offset, columns), + ] + for y_offset, x_offset in offsets + ] + + result[row, column] = float(np.median(neighbourhood)) + + return result + + +def test_radius_one_uses_full_three_by_three_ellipse() -> None: + footprint = _median_disk_footprint(1) + + assert np.array_equal( + footprint, + np.ones((3, 3), dtype=bool), + ) + + data = np.array( + [ + [100.0, 0.0, 100.0], + [0.0, 1.0, 0.0], + [100.0, 0.0, 100.0], + ] + ) + + result = estimate_median_background( + _channel(data), + radius_pixels=1, + ) + + # A radius-one Euclidean-centre disk would be a five-pixel cross + # and would return zero here. Gwyddion's 3×3 ellipse returns one. + assert result.data[1, 1] == 1.0 + + +def test_border_uses_nearest_extension() -> None: + data = np.array( + [ + [0.0, 10.0], + [20.0, 30.0], + ] + ) + channel = _channel(data) + + result = estimate_median_background( + channel, + radius_pixels=1, + ) + expected = _brute_force_border_extend_median( + data, + radius_pixels=1, + ) + + assert np.array_equal(result.data, expected) + + # Gwyddion radius one is a full 3×3 elliptic kernel. Nearest + # extension produces four zeroes, two tens, two twenties and 30, + # so the middle value is 10. + assert result.data[0, 0] == 10.0 + + +def test_matches_independent_border_extend_oracle() -> None: + data = np.array( + [ + [8.0, 1.0, 7.0, 2.0], + [3.0, 9.0, 0.0, 6.0], + [5.0, 4.0, 2.0, 1.0], + ] + ) + channel = _channel(data) + + result = estimate_median_background( + channel, + radius_pixels=2, + ) + expected = _brute_force_border_extend_median( + data, + radius_pixels=2, + ) + + assert np.allclose( + result.data, + expected, + rtol=0.0, + atol=0.0, + ) + + +def test_flat_surface_is_preserved() -> None: + data = np.full((5, 7), 3.25) + channel = _channel(data) + + background = estimate_median_background( + channel, + radius_pixels=2, + ) + corrected = remove_median_background( + channel, + radius_pixels=2, + ) + + assert np.array_equal(background.data, data) + assert np.array_equal(corrected.data, np.zeros_like(data)) + + +def test_reconstruction_identity() -> None: + data = np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.5, 3.0, 0.4], + [0.1, 0.6, 1.8, 0.2], + ] + ) + channel = _channel(data) + + background = estimate_median_background( + channel, + radius_pixels=2, + ) + corrected = remove_median_background( + channel, + radius_pixels=2, + ) + + assert np.allclose( + corrected.data + background.data, + data, + rtol=1e-13, + atol=1e-13, + ) + + +def test_non_geometric_scalar_unit_is_supported() -> None: + data = np.array( + [ + [0.1, 0.5, 0.2], + [0.7, 4.0, 0.3], + ] + ) + channel = _channel( + data, + unit="V", + ) + + result = estimate_median_background( + channel, + radius_pixels=1, + ) + + assert result.unit == "V" + assert np.all(np.isfinite(result.data)) + + +def test_input_is_not_mutated_and_context_is_preserved() -> None: + data = np.array( + [ + [0.0, 0.2, 1.0], + [0.3, 1.5, 0.4], + ] + ) + original = data.copy() + channel = _channel(data) + + background = estimate_median_background( + channel, + radius_pixels=1, + ) + corrected = remove_median_background( + channel, + radius_pixels=1, + ) + + assert np.array_equal(channel.data, original) + + for result in (background, corrected): + assert result is not channel + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +@pytest.mark.parametrize( + "radius_pixels", + [ + 0, + -1, + -20, + ], +) +def test_nonpositive_radius_is_rejected( + radius_pixels: int, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises( + ValueError, + match="radius_pixels to be positive", + ): + estimate_median_background( + channel, + radius_pixels=radius_pixels, + ) + + +@pytest.mark.parametrize( + "radius_pixels", + [ + True, + None, + 1.0, + "2", + [2], + ], +) +def test_non_integer_radius_is_rejected( + radius_pixels: object, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises( + TypeError, + match="radius_pixels to be a positive integer", + ): + estimate_median_background( + channel, + radius_pixels=radius_pixels, + ) + + +@pytest.mark.parametrize( + "nonfinite", + [ + np.nan, + np.inf, + -np.inf, + ], +) +def test_nonfinite_data_are_rejected( + nonfinite: float, +) -> None: + data = np.ones((3, 4)) + data[1, 2] = nonfinite + channel = _channel(data) + + with pytest.raises( + ValueError, + match="requires finite data", + ): + estimate_median_background( + channel, + radius_pixels=1, + ) + + +@pytest.mark.parametrize( + "data", + [ + np.ones(4), + np.empty((0, 3)), + np.array([["a", "b"], ["c", "d"]]), + np.ones((2, 3), dtype=complex), + ], + ids=[ + "one-dimensional", + "empty", + "non-numeric", + "complex", + ], +) +def test_invalid_channel_data_are_rejected( + data: np.ndarray, +) -> None: + channel = SPMChannel( + name="invalid", + data=data, + unit="nm", + x_range=3.0, + y_range=2.0, + ) + + with pytest.raises((TypeError, ValueError)): + estimate_median_background( + channel, + radius_pixels=1, + ) + + +def test_radius_above_gwyddion_limit_is_rejected_before_allocation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from spmkit.core.analysis import background as background_module + + def fail_if_allocated( + radius_pixels: int, + ) -> np.ndarray: + raise AssertionError(f"footprint was allocated for radius {radius_pixels}") + + monkeypatch.setattr( + background_module, + "_median_disk_footprint", + fail_if_allocated, + ) + + with pytest.raises( + ValueError, + match=r"\[1, 1024\]", + ): + background_module.estimate_median_background( + _channel(np.ones((2, 3), dtype=float)), + radius_pixels=10_000, + ) + + +@pytest.mark.parametrize( + "data", + [ + np.array([[2.0]]), + np.array([[0.0, 2.0, 0.5, 1.0]]), + np.array([[0.0], [2.0], [0.5], [1.0]]), + ], + ids=[ + "one-by-one", + "one-row", + "one-column", + ], +) +def test_degenerate_dimensions_are_defined( + data: np.ndarray, +) -> None: + channel = _channel(data) + + background = estimate_median_background( + channel, + radius_pixels=1, + ) + corrected = remove_median_background( + channel, + radius_pixels=1, + ) + + assert background.data.shape == data.shape + assert corrected.data.shape == data.shape + assert np.all(np.isfinite(background.data)) + assert np.allclose( + corrected.data + background.data, + data, + ) + + +@pytest.mark.parametrize( + ("radius_pixels", "expected_count"), + [ + (1, 9), + (2, 21), + (3, 37), + (4, 69), + (5, 97), + (6, 137), + (7, 177), + (8, 225), + ], +) +def test_footprint_counts_match_gwyddion_271( + radius_pixels: int, + expected_count: int, +) -> None: + footprint = _median_disk_footprint(radius_pixels) + + assert int(np.count_nonzero(footprint)) == expected_count + + +def test_radius_two_matches_frozen_gwyddion_mask() -> None: + expected = np.array( + [ + [0, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + ], + dtype=bool, + ) + + assert np.array_equal( + _median_disk_footprint(2), + expected, + ) + + +def test_median_background_functions_are_publicly_exported() -> None: + from spmkit.core import analysis + from spmkit.core.analysis.background import ( + estimate_median_background, + remove_median_background, + ) + + assert analysis.estimate_median_background is estimate_median_background + assert analysis.remove_median_background is remove_median_background diff --git a/tests/core/test_polynomial_background.py b/tests/core/test_polynomial_background.py new file mode 100644 index 0000000..8f76165 --- /dev/null +++ b/tests/core/test_polynomial_background.py @@ -0,0 +1,192 @@ +"""Tests for global polynomial background estimation.""" + +from __future__ import annotations + +import numpy as np +import pytest + +import spmkit.core.analysis as analysis +from spmkit.core.analysis import ( + analyze_polynomial_background, + estimate_polynomial_background, + leveling, + remove_polynomial_background, +) +from spmkit.core.models import SPMChannel + + +def _channel(data: np.ndarray) -> SPMChannel: + return SPMChannel( + name="Z-Axis", + data=np.asarray(data, dtype=float), + unit="nm", + x_range=9e-6, + y_range=8e-6, + direction="forward", + group="Topography", + metadata={"source": "synthetic"}, + ) + + +def test_total_degree_estimates_exact_polynomial_surface() -> None: + rows, columns = 8, 9 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + expected = 4.0 + 1.5 * x - 0.75 * y + 0.4 * x * y + 0.2 * x**2 - 0.1 * y**2 + + channel = _channel(expected) + + observed = estimate_polynomial_background( + channel, + degree_mode="total", + degree=2, + ) + + assert np.allclose(observed.data, expected, atol=1e-12) + + +def test_independent_degree_supports_tensor_product_terms() -> None: + rows, columns = 8, 9 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + expected = 1.0 + 0.5 * x**2 - 0.25 * y + 2.0 * x**3 * y + + channel = _channel(expected) + + observed = estimate_polynomial_background( + channel, + degree_mode="independent", + x_degree=3, + y_degree=1, + ) + + assert np.allclose(observed.data, expected, atol=1e-12) + + +def test_remove_matches_legacy_leveling_api() -> None: + rows, columns = 7, 8 + yy, xx = np.mgrid[0:rows, 0:columns] + + data = 2.0 + 0.4 * xx - 0.3 * yy + 0.05 * xx * yy + np.sin(xx) + + channel = _channel(data) + + expected = leveling.polynomial_background( + channel, + degree_mode="total", + degree=2, + ) + observed = remove_polynomial_background( + channel, + degree_mode="total", + degree=2, + ) + + assert np.array_equal(observed.data, expected.data) + + +def test_mask_excludes_feature_from_fit() -> None: + rows = columns = 9 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + expected = 3.0 + 0.75 * x - 0.5 * y + data = expected.copy() + data[4, 4] += 100.0 + + mask = np.zeros(data.shape, dtype=bool) + mask[4, 4] = True + + observed = estimate_polynomial_background( + _channel(data), + degree_mode="total", + degree=1, + mask=mask, + mask_mode="exclude", + ) + + assert np.allclose(observed.data, expected, atol=1e-12) + + +def test_analyze_returns_model_corrected_and_provenance() -> None: + rows, columns = 8, 9 + y = np.linspace(-1.0, 1.0, rows)[:, np.newaxis] + x = np.linspace(-1.0, 1.0, columns)[np.newaxis, :] + + data = 2.0 + x - 0.5 * y + 0.25 * x * y + channel = _channel(data) + + result = analyze_polynomial_background( + channel, + degree_mode="total", + degree=2, + ) + + assert result.method == "polynomial" + assert result.parameters == { + "degree_mode": "total", + "degree": 2, + "x_degree": None, + "y_degree": None, + "mask_mode": "ignore", + "mask_provided": False, + "coordinates": "normalized_-1_1", + } + assert np.allclose(result.background.data, data, atol=1e-12) + assert np.allclose(result.corrected.data, 0.0, atol=1e-12) + assert np.allclose( + result.background.data + result.corrected.data, + channel.data, + atol=1e-12, + ) + + +def test_outputs_preserve_context_without_mutating_input() -> None: + data = np.arange(72.0).reshape(8, 9) + channel = _channel(data) + + original_data = channel.data.copy() + original_metadata = dict(channel.metadata) + + estimated = estimate_polynomial_background( + channel, + degree=2, + ) + corrected = remove_polynomial_background( + channel, + degree=2, + ) + + for output in (estimated, corrected): + assert output is not channel + assert output.name == channel.name + assert output.unit == channel.unit + assert output.x_range == channel.x_range + assert output.y_range == channel.y_range + assert output.direction == channel.direction + assert output.group == channel.group + assert output.metadata == channel.metadata + + assert np.array_equal(channel.data, original_data) + assert channel.metadata == original_metadata + + +def test_invalid_configuration_is_rejected_by_shared_solver() -> None: + channel = _channel(np.arange(72.0).reshape(8, 9)) + + with pytest.raises( + ValueError, + match="polynomial_background degree_mode must be", + ): + estimate_polynomial_background( + channel, + degree_mode="unknown", # type: ignore[arg-type] + ) + + +def test_polynomial_background_api_is_public() -> None: + assert analysis.estimate_polynomial_background is estimate_polynomial_background + assert analysis.remove_polynomial_background is remove_polynomial_background + assert analysis.analyze_polynomial_background is analyze_polynomial_background diff --git a/tests/core/test_pspline_fitpack_validation.py b/tests/core/test_pspline_fitpack_validation.py new file mode 100644 index 0000000..816e682 --- /dev/null +++ b/tests/core/test_pspline_fitpack_validation.py @@ -0,0 +1,181 @@ +"""Independent FITPACK checks for exact P-spline reconstruction.""" + +from __future__ import annotations + +import numpy as np +import pytest +from scipy.interpolate import ( + BSpline, + LSQBivariateSpline, +) + +from spmkit.core.analysis._pspline import ( + _open_uniform_knots, + fit_pspline_surface, +) + + +def _validation_problem() -> tuple[ + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, +]: + degree = 3 + n_basis_x = 7 + n_basis_y = 6 + + x = np.linspace(0.0, 1.0, 19) + y = np.linspace(0.0, 1.0, 17) + + knots_x = _open_uniform_knots( + n_basis_x, + degree, + ) + knots_y = _open_uniform_knots( + n_basis_y, + degree, + ) + + basis_x = BSpline.design_matrix( + x, + knots_x, + degree, + ).toarray() + basis_y = BSpline.design_matrix( + y, + knots_y, + degree, + ).toarray() + + x_index = np.arange( + n_basis_x, + dtype=float, + )[None, :] + y_index = np.arange( + n_basis_y, + dtype=float, + )[:, None] + + coefficients = 1.7 + 0.31 * x_index - 0.23 * y_index + 0.047 * x_index * y_index + + surface = basis_y @ coefficients @ basis_x.T + + return ( + x, + y, + knots_x, + knots_y, + np.asarray(surface, dtype=float), + ) + + +def _fitpack_model( + x: np.ndarray, + y: np.ndarray, + knots_x: np.ndarray, + knots_y: np.ndarray, + data: np.ndarray, + selection: np.ndarray, +) -> np.ndarray: + degree = 3 + xx, yy = np.meshgrid( + x, + y, + indexing="xy", + ) + + interior_x = knots_x[degree + 1 : -(degree + 1)] + interior_y = knots_y[degree + 1 : -(degree + 1)] + + spline = LSQBivariateSpline( + xx[selection], + yy[selection], + data[selection], + interior_x, + interior_y, + kx=degree, + ky=degree, + ) + + return np.asarray( + spline.ev( + xx.ravel(order="C"), + yy.ravel(order="C"), + ).reshape(data.shape), + dtype=float, + order="C", + ) + + +@pytest.mark.parametrize( + "exclude_feature", + [False, True], + ids=[ + "complete_surface", + "excluded_feature", + ], +) +def test_fitpack_recovers_penalty_null_surface( + exclude_feature: bool, +) -> None: + x, y, knots_x, knots_y, expected = _validation_problem() + + observed = expected.copy() + selection = np.ones( + expected.shape, + dtype=bool, + ) + + if exclude_feature: + observed[8, 9] += 100.0 + selection[8, 9] = False + + fit = fit_pspline_surface( + observed, + x=x, + y=y, + mask=selection, + n_basis_x=7, + n_basis_y=6, + degree_x=3, + degree_y=3, + penalty_order_x=2, + penalty_order_y=2, + smoothing_x=3.0, + smoothing_y=7.0, + atol=1e-14, + btol=1e-14, + ) + + fitpack = _fitpack_model( + x, + y, + knots_x, + knots_y, + observed, + selection, + ) + + np.testing.assert_allclose( + fit.model, + expected, + rtol=0.0, + atol=1e-9, + ) + np.testing.assert_allclose( + fitpack, + expected, + rtol=0.0, + atol=1e-9, + ) + np.testing.assert_allclose( + fit.model, + fitpack, + rtol=0.0, + atol=1e-9, + ) + + assert fit.penalty_x_norm < 1e-9 + assert fit.penalty_y_norm < 1e-9 diff --git a/tests/core/test_pspline_surface.py b/tests/core/test_pspline_surface.py new file mode 100644 index 0000000..1cbb286 --- /dev/null +++ b/tests/core/test_pspline_surface.py @@ -0,0 +1,460 @@ +from __future__ import annotations + +import numpy as np +import pytest +from scipy.interpolate import BSpline +from scipy.sparse import eye, kron, vstack + +from spmkit.core.analysis._pspline import ( + _difference_matrix, + _open_uniform_knots, + fit_pspline_surface, +) + + +def _surface_from_coefficients( + coefficients: np.ndarray, + *, + rows: int, + columns: int, + degree_x: int, + degree_y: int, +) -> np.ndarray: + knots_x = _open_uniform_knots( + coefficients.shape[1], + degree_x, + ) + knots_y = _open_uniform_knots( + coefficients.shape[0], + degree_y, + ) + + basis_x = BSpline.design_matrix( + np.linspace(0.0, 1.0, columns), + knots_x, + degree_x, + ) + basis_y = BSpline.design_matrix( + np.linspace(0.0, 1.0, rows), + knots_y, + degree_y, + ) + + return np.asarray( + basis_y @ coefficients @ basis_x.T, + dtype=float, + order="C", + ) + + +def test_recovers_zero_penalty_tensor_surface() -> None: + rows = 15 + columns = 17 + n_basis_x = 8 + n_basis_y = 7 + + x_index = np.arange( + n_basis_x, + dtype=float, + )[None, :] + y_index = np.arange( + n_basis_y, + dtype=float, + )[:, None] + + expected_coefficients = 2.0 + 0.3 * x_index - 0.2 * y_index + 0.05 * x_index * y_index + + data = _surface_from_coefficients( + expected_coefficients, + rows=rows, + columns=columns, + degree_x=3, + degree_y=3, + ) + + result = fit_pspline_surface( + data, + n_basis_x=n_basis_x, + n_basis_y=n_basis_y, + degree_x=3, + degree_y=3, + penalty_order_x=2, + penalty_order_y=2, + smoothing_x=4.0, + smoothing_y=7.0, + atol=1e-14, + btol=1e-14, + ) + + np.testing.assert_allclose( + result.coefficients, + expected_coefficients, + rtol=0.0, + atol=2e-10, + ) + np.testing.assert_allclose( + result.model, + data, + rtol=0.0, + atol=2e-10, + ) + + assert result.penalty_x_norm < 2e-10 + assert result.penalty_y_norm < 2e-10 + + +def test_matches_explicit_dense_weighted_masked_oracle() -> None: + rows = 11 + columns = 13 + n_basis_x = 7 + n_basis_y = 6 + degree_x = 3 + degree_y = 3 + penalty_order_x = 2 + penalty_order_y = 2 + smoothing_x = 0.8 + smoothing_y = 2.1 + + rng = np.random.default_rng(20260801) + + data = rng.normal(size=(rows, columns)) + mask = np.ones( + data.shape, + dtype=bool, + ) + mask[3:7, 4:9] = False + + weights = np.linspace( + 0.4, + 1.6, + data.size, + ).reshape(data.shape) + + result = fit_pspline_surface( + data, + mask=mask, + weights=weights, + n_basis_x=n_basis_x, + n_basis_y=n_basis_y, + degree_x=degree_x, + degree_y=degree_y, + penalty_order_x=penalty_order_x, + penalty_order_y=penalty_order_y, + smoothing_x=smoothing_x, + smoothing_y=smoothing_y, + atol=1e-14, + btol=1e-14, + maxiter=4000, + ) + + knots_x = _open_uniform_knots( + n_basis_x, + degree_x, + ) + knots_y = _open_uniform_knots( + n_basis_y, + degree_y, + ) + + basis_x = BSpline.design_matrix( + np.linspace(0.0, 1.0, columns), + knots_x, + degree_x, + ) + basis_y = BSpline.design_matrix( + np.linspace(0.0, 1.0, rows), + knots_y, + degree_y, + ) + + difference_x = _difference_matrix( + n_basis_x, + penalty_order_x, + ) + difference_y = _difference_matrix( + n_basis_y, + penalty_order_y, + ) + + selected = np.flatnonzero(mask.ravel(order="C")) + sqrt_weights = np.sqrt(weights.ravel(order="C")[selected]) + + data_operator = kron( + basis_y, + basis_x, + format="csr", + )[selected] + + weighted_data_operator = data_operator.multiply(sqrt_weights[:, None]) + + penalty_x = kron( + eye(n_basis_y, format="csr"), + difference_x, + format="csr", + ) + penalty_y = kron( + difference_y, + eye(n_basis_x, format="csr"), + format="csr", + ) + + explicit_system = vstack( + ( + weighted_data_operator, + np.sqrt(smoothing_x) * penalty_x, + np.sqrt(smoothing_y) * penalty_y, + ), + format="csr", + ) + + right_hand_side = np.concatenate( + ( + sqrt_weights * data.ravel(order="C")[selected], + np.zeros( + explicit_system.shape[0] - selected.size, + dtype=float, + ), + ) + ) + + expected_vector = np.linalg.lstsq( + explicit_system.toarray(), + right_hand_side, + rcond=None, + )[0] + + expected_coefficients = expected_vector.reshape( + n_basis_y, + n_basis_x, + order="C", + ) + expected_model = np.asarray( + basis_y @ expected_coefficients @ basis_x.T, + dtype=float, + order="C", + ) + + np.testing.assert_allclose( + result.coefficients, + expected_coefficients, + rtol=0.0, + atol=2e-10, + ) + np.testing.assert_allclose( + result.model, + expected_model, + rtol=0.0, + atol=2e-10, + ) + + +def test_mask_can_exclude_nonfinite_data() -> None: + data = np.arange( + 99, + dtype=float, + ).reshape(9, 11) + + mask = np.ones( + data.shape, + dtype=bool, + ) + mask[4, 5] = False + data[4, 5] = np.nan + + result = fit_pspline_surface( + data, + mask=mask, + n_basis_x=6, + n_basis_y=6, + ) + + assert np.all(np.isfinite(result.model)) + assert result.selected_points == data.size - 1 + + +def test_selected_nonfinite_data_is_rejected() -> None: + data = np.ones( + (9, 11), + dtype=float, + ) + data[4, 5] = np.nan + + with pytest.raises( + ValueError, + match="selected P-spline data must be finite", + ): + fit_pspline_surface( + data, + n_basis_x=6, + n_basis_y=6, + ) + + +def test_penalty_null_space_must_be_identifiable() -> None: + data = np.ones( + (9, 11), + dtype=float, + ) + mask = np.zeros( + data.shape, + dtype=bool, + ) + mask[0, 0] = True + + with pytest.raises( + ValueError, + match="penalty null space", + ): + fit_pspline_surface( + data, + mask=mask, + n_basis_x=6, + n_basis_y=6, + ) + + +def test_input_is_not_mutated_and_results_are_read_only() -> None: + rng = np.random.default_rng(91) + data = rng.normal(size=(10, 12)) + original = data.copy() + + result = fit_pspline_surface( + data, + n_basis_x=6, + n_basis_y=6, + ) + + np.testing.assert_array_equal( + data, + original, + ) + + assert not result.model.flags.writeable + assert not result.coefficients.flags.writeable + assert not result.knots_x.flags.writeable + assert not result.knots_y.flags.writeable + + with pytest.raises(ValueError): + result.model[0, 0] = 0.0 + + +@pytest.mark.parametrize( + "kwargs", + [ + {"n_basis_x": 3, "degree_x": 3}, + {"degree_x": -1}, + {"penalty_order_x": 0}, + { + "n_basis_x": 6, + "penalty_order_x": 6, + }, + {"smoothing_x": 0.0}, + {"smoothing_y": np.inf}, + {"atol": 0.0}, + {"btol": 0.0}, + {"conlim": 0.0}, + {"maxiter": 0}, + ], +) +def test_invalid_configuration_is_rejected( + kwargs: dict[str, object], +) -> None: + with pytest.raises( + (TypeError, ValueError), + ): + fit_pspline_surface( + np.ones( + (9, 11), + dtype=float, + ), + n_basis_x=6, + n_basis_y=6, + **kwargs, + ) + + +def test_complex_surface_data_is_rejected() -> None: + data = np.ones( + (9, 11), + dtype=complex, + ) + + with pytest.raises( + TypeError, + match="P-spline surface data must be real numeric", + ): + fit_pspline_surface( + data, + n_basis_x=6, + n_basis_y=6, + ) + + +def test_complex_weights_are_rejected() -> None: + data = np.ones( + (9, 11), + dtype=float, + ) + weights = np.ones( + data.shape, + dtype=complex, + ) + + with pytest.raises( + TypeError, + match="P-spline weights must be real numeric", + ): + fit_pspline_surface( + data, + weights=weights, + n_basis_x=6, + n_basis_y=6, + ) + + +def test_complex_coordinates_are_rejected() -> None: + data = np.ones( + (9, 11), + dtype=float, + ) + x = np.linspace( + 0.0, + 1.0, + data.shape[1], + ).astype(complex) + + with pytest.raises( + TypeError, + match="x coordinates must be real numeric", + ): + fit_pspline_surface( + data, + x=x, + n_basis_x=6, + n_basis_y=6, + ) + + +@pytest.mark.parametrize( + "kwargs", + [ + {"smoothing_x": True}, + {"atol": np.array(True)}, + ], +) +def test_boolean_solver_parameters_are_rejected( + kwargs: dict[str, object], +) -> None: + with pytest.raises( + TypeError, + match="must be a real numeric scalar", + ): + fit_pspline_surface( + np.ones( + (9, 11), + dtype=float, + ), + n_basis_x=6, + n_basis_y=6, + **kwargs, + ) diff --git a/tests/core/test_rolling_ball_background.py b/tests/core/test_rolling_ball_background.py new file mode 100644 index 0000000..4eab45e --- /dev/null +++ b/tests/core/test_rolling_ball_background.py @@ -0,0 +1,532 @@ +"""Physical rolling-ball background estimation.""" + +from __future__ import annotations + +from math import floor + +import numpy as np +import pytest + +from spmkit.core.analysis import ( + BackgroundResult, + analyze_rolling_ball_background, + estimate_rolling_ball_background, + remove_rolling_ball_background, +) +from spmkit.core.models import SPMChannel + + +def _channel( + data: np.ndarray, + *, + unit: str = "V", + x_range: float | None = None, + y_range: float | None = None, +) -> SPMChannel: + rows, columns = data.shape + + return SPMChannel( + name="Signal", + data=np.asarray(data), + unit=unit, + x_range=(float(columns) if x_range is None else x_range), + y_range=(float(rows) if y_range is None else y_range), + direction="forward", + group="Scan", + metadata={"source": "synthetic"}, + ) + + +def _oracle_below( + data: np.ndarray, + *, + radius: float, + vertical_radius: float, + x_spacing: float, + y_spacing: float, +) -> np.ndarray: + """Independent direct evaluation of the apex-height formula.""" + image = np.asarray(data, dtype=float) + rows, columns = image.shape + + x_offset = min( + columns - 1, + floor(radius / x_spacing), + ) + y_offset = min( + rows - 1, + floor(radius / y_spacing), + ) + + output = np.empty_like(image) + + for row in range(rows): + for column in range(columns): + minimum = np.inf + + for dy in range(-y_offset, y_offset + 1): + for dx in range(-x_offset, x_offset + 1): + source_row = row + dy + source_column = column + dx + + if not (0 <= source_row < rows and 0 <= source_column < columns): + continue + + squared_ratio = (dx * x_spacing / radius) ** 2 + (dy * y_spacing / radius) ** 2 + + if squared_ratio > 1.0: + continue + + cost = vertical_radius * (1.0 - np.sqrt(1.0 - squared_ratio)) + + candidate = image[source_row, source_column] + cost + minimum = min(minimum, candidate) + + output[row, column] = minimum + + return output + + +@pytest.mark.parametrize("side", ["below", "above"]) +def test_matches_independent_anisotropic_oracle( + side: str, +) -> None: + data = np.array( + [ + [0.0, 1.0, 3.0, 8.0, 5.0, 4.0], + [2.0, 4.0, 9.0, 7.0, 6.0, 3.0], + [1.0, 5.0, 8.0, 4.0, 2.0, 1.0], + [3.0, 6.0, 7.0, 5.0, 4.0, 2.0], + [4.0, 5.0, 6.0, 8.0, 7.0, 3.0], + ] + ) + channel = _channel( + data, + x_range=6.0, + y_range=10.0, + ) + + expected_below = _oracle_below( + data, + radius=2.5, + vertical_radius=4.0, + x_spacing=1.0, + y_spacing=2.0, + ) + expected = ( + expected_below + if side == "below" + else -_oracle_below( + -data, + radius=2.5, + vertical_radius=4.0, + x_spacing=1.0, + y_spacing=2.0, + ) + ) + + observed = estimate_rolling_ball_background( + channel, + radius=2.5, + vertical_radius=4.0, + side=side, + ) + + np.testing.assert_allclose( + observed.data, + expected, + rtol=1e-14, + atol=1e-14, + ) + + +def test_reference_corner_case_ignores_exterior() -> None: + channel = _channel( + np.array( + [ + [0.0, 10.0], + [20.0, 30.0], + ] + ) + ) + + background = estimate_rolling_ball_background( + channel, + radius=1.0, + vertical_radius=1.0, + ) + + expected = np.array( + [ + [0.0, 1.0], + [1.0, 11.0], + ] + ) + + assert np.array_equal( + background.data, + expected, + ) + + +def test_radius_smaller_than_pixel_spacing_is_identity() -> None: + channel = _channel( + np.arange(12, dtype=float).reshape(3, 4), + x_range=8.0, + y_range=6.0, + ) + + background = estimate_rolling_ball_background( + channel, + radius=0.5, + vertical_radius=2.0, + ) + + assert np.array_equal( + background.data, + channel.data, + ) + + +def test_geometric_automatic_sphere_matches_explicit_native_radius() -> None: + data_nm = np.array( + [ + [0.0, 1.0, 4.0, 2.0], + [1.0, 5.0, 8.0, 3.0], + [2.0, 4.0, 6.0, 1.0], + ] + ) + channel = _channel( + data_nm, + unit="nm", + x_range=4e-9, + y_range=3e-9, + ) + + automatic = estimate_rolling_ball_background( + channel, + radius=2e-9, + ) + explicit = estimate_rolling_ball_background( + channel, + radius=2e-9, + vertical_radius=2.0, + ) + + np.testing.assert_allclose( + automatic.data, + explicit.data, + rtol=1e-14, + atol=1e-14, + ) + + +def test_non_geometric_unit_requires_vertical_radius() -> None: + channel = _channel( + np.ones((3, 4)), + unit="V", + ) + + with pytest.raises( + ValueError, + match="unsupported geometric length unit", + ): + estimate_rolling_ball_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize( + "vertical_radius", + [ + 0.0, + -1.0, + np.nan, + np.inf, + -np.inf, + ], +) +def test_invalid_vertical_radius_value_is_rejected( + vertical_radius: float, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(ValueError): + estimate_rolling_ball_background( + channel, + radius=1.0, + vertical_radius=vertical_radius, + ) + + +@pytest.mark.parametrize( + "vertical_radius", + [ + True, + "1.0", + [1.0], + 1.0 + 0.0j, + ], +) +def test_invalid_vertical_radius_type_is_rejected( + vertical_radius: object, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(TypeError): + estimate_rolling_ball_background( + channel, + radius=1.0, + vertical_radius=vertical_radius, + ) + + +@pytest.mark.parametrize( + "radius", + [ + 0.0, + -1.0, + np.nan, + np.inf, + -np.inf, + ], +) +def test_invalid_lateral_radius_value_is_rejected( + radius: float, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(ValueError): + estimate_rolling_ball_background( + channel, + radius=radius, + vertical_radius=1.0, + ) + + +@pytest.mark.parametrize( + "radius", + [ + True, + None, + "1.0", + [1.0], + 1.0 + 0.0j, + ], +) +def test_invalid_lateral_radius_type_is_rejected( + radius: object, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(TypeError): + estimate_rolling_ball_background( + channel, + radius=radius, + vertical_radius=1.0, + ) + + +@pytest.mark.parametrize( + "side", + [ + "underneath", + "nearest", + ], +) +def test_invalid_side_value_is_rejected( + side: str, +) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(ValueError): + estimate_rolling_ball_background( + channel, + radius=1.0, + vertical_radius=1.0, + side=side, + ) + + +def test_non_string_side_is_rejected() -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises( + TypeError, + match="requires side to be a string", + ): + estimate_rolling_ball_background( + channel, + radius=1.0, + vertical_radius=1.0, + side=None, + ) + + +@pytest.mark.parametrize( + ("x_range", "y_range"), + [ + (0.0, 3.0), + (-1.0, 3.0), + (np.inf, 3.0), + (4.0, 0.0), + (4.0, -1.0), + (4.0, np.inf), + ], +) +def test_invalid_lateral_geometry_is_rejected( + x_range: float, + y_range: float, +) -> None: + channel = _channel( + np.ones((3, 4)), + x_range=x_range, + y_range=y_range, + ) + + with pytest.raises(ValueError): + estimate_rolling_ball_background( + channel, + radius=1.0, + vertical_radius=1.0, + ) + + +def test_remove_reconstructs_original_data() -> None: + channel = _channel( + np.array( + [ + [0.0, 1.0, 5.0, 2.0], + [2.0, 6.0, 9.0, 3.0], + [1.0, 4.0, 7.0, 2.0], + ] + ) + ) + + background = estimate_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + ) + corrected = remove_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + ) + + assert np.array_equal( + corrected.data + background.data, + channel.data, + ) + + +def test_outputs_preserve_context_without_mutating_input() -> None: + channel = _channel( + np.arange(20, dtype=float).reshape(4, 5), + ) + original = channel.data.copy() + + background = estimate_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + ) + corrected = remove_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + ) + + assert np.array_equal(channel.data, original) + + for output in (background, corrected): + assert output.name == channel.name + assert output.unit == channel.unit + assert output.x_range == channel.x_range + assert output.y_range == channel.y_range + assert output.direction == channel.direction + assert output.group == channel.group + assert output.metadata == channel.metadata + assert output.metadata is not channel.metadata + + +def test_structured_result_matches_simple_functions() -> None: + channel = _channel( + np.arange(20, dtype=float).reshape(4, 5), + ) + + result = analyze_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + side="above", + ) + + expected_background = estimate_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + side="above", + ) + expected_corrected = remove_rolling_ball_background( + channel, + radius=2.0, + vertical_radius=3.0, + side="above", + ) + + assert isinstance(result, BackgroundResult) + assert result.method == "rolling_ball" + assert result.parameters == { + "radius": 2.0, + "vertical_radius": 3.0, + "side": "above", + "boundary": "ignore", + } + assert np.array_equal( + result.background.data, + expected_background.data, + ) + assert np.array_equal( + result.corrected.data, + expected_corrected.data, + ) + + +def test_geometric_structured_result_records_automatic_mode() -> None: + channel = _channel( + np.arange(12, dtype=float).reshape(3, 4), + unit="nm", + x_range=4e-9, + y_range=3e-9, + ) + + result = analyze_rolling_ball_background( + channel, + radius=2e-9, + ) + + assert result.parameters == { + "radius": 2e-9, + "vertical_radius": None, + "side": "below", + "boundary": "ignore", + } + + +def test_rolling_ball_api_is_public() -> None: + from spmkit.core import analysis + from spmkit.core.analysis.background import ( + analyze_rolling_ball_background as module_analyze, + ) + from spmkit.core.analysis.background import ( + estimate_rolling_ball_background as module_estimate, + ) + from spmkit.core.analysis.background import ( + remove_rolling_ball_background as module_remove, + ) + + assert analysis.analyze_rolling_ball_background is module_analyze + assert analysis.estimate_rolling_ball_background is module_estimate + assert analysis.remove_rolling_ball_background is module_remove diff --git a/tests/core/test_sphere_revolution_background.py b/tests/core/test_sphere_revolution_background.py new file mode 100644 index 0000000..103ba78 --- /dev/null +++ b/tests/core/test_sphere_revolution_background.py @@ -0,0 +1,878 @@ +"""Tests for physical sphere-revolution background estimation.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from spmkit.core.analysis.background import ( + _sphere_structure, + estimate_arc_revolution_background, + estimate_sphere_revolution_background, + remove_sphere_revolution_background, +) +from spmkit.core.models import SPMChannel + + +def _channel( + data: np.ndarray, + *, + unit: str = "m", + x_range: float | None = None, + y_range: float | None = None, +) -> SPMChannel: + rows, columns = data.shape + + return SPMChannel( + name="Z-Axis", + data=np.asarray(data), + unit=unit, + x_range=float(columns) if x_range is None else x_range, + y_range=float(rows) if y_range is None else y_range, + direction="backward", + group="Synthetic", + metadata={"source": "sphere-test"}, + ) + + +def _nearest_index(index: int, size: int) -> int: + return min(max(index, 0), size - 1) + + +def _reflect_index(index: int, size: int) -> int: + """Map an integer index using SciPy's half-sample reflection.""" + period = 2 * size + position = index % period + + if position < size: + return position + + return period - 1 - position + + +def _brute_force_sphere_below_nearest( + data: np.ndarray, + *, + radius: float, + x_spacing: float, + y_spacing: float, +) -> np.ndarray: + """Independent two-dimensional spherical-opening oracle.""" + values = np.asarray(data, dtype=float) + rows, columns = values.shape + + maximum_x_offset = min( + int(np.floor(radius / x_spacing)), + columns - 1, + ) + maximum_y_offset = min( + int(np.floor(radius / y_spacing)), + rows - 1, + ) + + offsets: list[tuple[int, int, float]] = [] + + for y_offset in range( + -maximum_y_offset, + maximum_y_offset + 1, + ): + for x_offset in range( + -maximum_x_offset, + maximum_x_offset + 1, + ): + normalized_x = x_offset * x_spacing / radius + normalized_y = y_offset * y_spacing / radius + squared_ratio = normalized_x**2 + normalized_y**2 + + if squared_ratio > 1.0 + 8.0 * np.finfo(float).eps: + continue + + clipped_ratio = min(squared_ratio, 1.0) + root = np.sqrt(max(1.0 - clipped_ratio, 0.0)) + sagitta = radius * clipped_ratio / (1.0 + root) + + offsets.append( + ( + y_offset, + x_offset, + sagitta, + ) + ) + + eroded = np.empty_like(values) + + for row in range(rows): + for column in range(columns): + candidates = [ + values[ + _nearest_index(row + y_offset, rows), + _nearest_index(column + x_offset, columns), + ] + + sagitta + for y_offset, x_offset, sagitta in offsets + ] + eroded[row, column] = min(candidates) + + opened = np.empty_like(values) + + for row in range(rows): + for column in range(columns): + candidates = [ + eroded[ + _nearest_index(row - y_offset, rows), + _nearest_index(column - x_offset, columns), + ] + - sagitta + for y_offset, x_offset, sagitta in offsets + ] + opened[row, column] = max(candidates) + + return opened + + +def _brute_force_sphere_below_reflect( + data: np.ndarray, + *, + radius: float, + x_spacing: float, + y_spacing: float, +) -> np.ndarray: + """Independent spherical-opening oracle with reflected boundaries.""" + values = np.asarray(data, dtype=float) + rows, columns = values.shape + + maximum_x_offset = min( + int(np.floor(radius / x_spacing)), + columns - 1, + ) + maximum_y_offset = min( + int(np.floor(radius / y_spacing)), + rows - 1, + ) + + offsets: list[tuple[int, int, float]] = [] + + for y_offset in range( + -maximum_y_offset, + maximum_y_offset + 1, + ): + for x_offset in range( + -maximum_x_offset, + maximum_x_offset + 1, + ): + normalized_x = x_offset * x_spacing / radius + normalized_y = y_offset * y_spacing / radius + squared_ratio = normalized_x**2 + normalized_y**2 + + if squared_ratio > 1.0 + 8.0 * np.finfo(float).eps: + continue + + clipped_ratio = min(squared_ratio, 1.0) + root = np.sqrt(max(1.0 - clipped_ratio, 0.0)) + sagitta = radius * clipped_ratio / (1.0 + root) + + offsets.append( + ( + y_offset, + x_offset, + sagitta, + ) + ) + + eroded = np.empty_like(values) + + for row in range(rows): + for column in range(columns): + candidates = [ + values[ + _reflect_index(row + y_offset, rows), + _reflect_index(column + x_offset, columns), + ] + + sagitta + for y_offset, x_offset, sagitta in offsets + ] + eroded[row, column] = min(candidates) + + opened = np.empty_like(values) + + for row in range(rows): + for column in range(columns): + candidates = [ + eroded[ + _reflect_index(row - y_offset, rows), + _reflect_index(column - x_offset, columns), + ] + - sagitta + for y_offset, x_offset, sagitta in offsets + ] + opened[row, column] = max(candidates) + + return opened + + +def test_sphere_structure_uses_circular_physical_footprint() -> None: + structure, footprint = _sphere_structure( + radius=1.1, + x_spacing=1.0, + y_spacing=1.0, + shape=(5, 5), + ) + + expected_footprint = np.array( + [ + [False, True, False], + [True, True, True], + [False, True, False], + ] + ) + + assert structure.shape == (3, 3) + assert np.array_equal(footprint, expected_footprint) + assert structure[1, 1] == 0.0 + assert np.all(structure[footprint] <= 0.0) + + +def test_flat_surface_is_preserved() -> None: + data = np.full((5, 7), 3.25) + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=2.0, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=2.0, + ) + + assert np.allclose(background.data, data) + assert np.allclose(corrected.data, 0.0) + + +def test_nearest_matches_independent_two_dimensional_oracle() -> None: + data = np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.5, 3.0, 0.4], + [0.1, 0.6, 1.8, 0.2], + ] + ) + channel = _channel( + data, + x_range=4.0, + y_range=3.0, + ) + + result = estimate_sphere_revolution_background( + channel, + radius=1.5, + border="nearest", + ) + expected = _brute_force_sphere_below_nearest( + data, + radius=1.5, + x_spacing=1.0, + y_spacing=1.0, + ) + + assert np.allclose( + result.data, + expected, + rtol=1e-13, + atol=1e-13, + ) + + +def test_above_is_exact_inversion_dual() -> None: + data = np.array( + [ + [0.0, -0.2, -1.0, -0.1], + [-0.3, -1.5, -3.0, -0.4], + [-0.1, -0.6, -1.8, -0.2], + ] + ) + channel = _channel(data) + inverted = channel.with_data(-data) + + above = estimate_sphere_revolution_background( + channel, + radius=1.5, + side="above", + ) + below_inverted = estimate_sphere_revolution_background( + inverted, + radius=1.5, + side="below", + ) + + assert np.allclose( + above.data, + -below_inverted.data, + ) + + +def test_reconstruction_identity() -> None: + yy, xx = np.mgrid[0:7, 0:9] + data = 0.02 * xx + 0.03 * yy + 2.0 * np.exp(-((xx - 4) ** 2 + (yy - 3) ** 2) / 2.0) + channel = _channel( + data, + x_range=9e-6, + y_range=14e-6, + ) + + background = estimate_sphere_revolution_background( + channel, + radius=4e-6, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=4e-6, + ) + + assert np.allclose( + corrected.data + background.data, + data, + rtol=1e-12, + atol=1e-12, + ) + + +def test_sphere_structure_respects_anisotropic_physical_spacing() -> None: + structure, footprint = _sphere_structure( + radius=2.1, + x_spacing=1.0, + y_spacing=2.0, + shape=(5, 5), + ) + + expected_footprint = np.array( + [ + [False, False, True, False, False], + [True, True, True, True, True], + [False, False, True, False, False], + ] + ) + + assert structure.shape == (3, 5) + assert np.array_equal(footprint, expected_footprint) + + # ±2 pixels in X and ±1 pixel in Y are both physical distances of 2. + assert footprint[1, 0] + assert footprint[0, 2] + + # A diagonal offset of (1 px X, 1 px Y) has physical distance sqrt(5), + # which lies outside a sphere of radius 2.1. + assert not footprint[0, 1] + + +def test_sphere_is_not_separable_arc_revolution() -> None: + data = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + [0.0, 1.0, 2.0], + ] + ) + channel = _channel( + data, + x_range=3.0, + y_range=3.0, + ) + + sphere = estimate_sphere_revolution_background( + channel, + radius=1.1, + border="nearest", + ) + separable_arc = estimate_arc_revolution_background( + channel, + radius=1.1, + direction="both", + border="nearest", + ) + + assert not np.allclose( + sphere.data, + separable_arc.data, + ) + + +def test_reflect_matches_independent_two_dimensional_oracle() -> None: + data = np.array( + [ + [3.0, 0.2, 0.1, 2.0], + [0.4, 1.5, 0.3, 0.0], + [2.0, 0.6, 1.8, 4.0], + ] + ) + channel = _channel( + data, + x_range=4.0, + y_range=3.0, + ) + + result = estimate_sphere_revolution_background( + channel, + radius=1.5, + border="reflect", + ) + expected = _brute_force_sphere_below_reflect( + data, + radius=1.5, + x_spacing=1.0, + y_spacing=1.0, + ) + + assert np.allclose( + result.data, + expected, + rtol=1e-13, + atol=1e-13, + ) + + +def test_radius_smaller_than_both_pixel_spacings_is_identity() -> None: + data = np.array( + [ + [0.2, 1.0, 0.4], + [2.0, 0.1, 1.5], + ] + ) + channel = _channel( + data, + x_range=3.0, + y_range=2.0, + ) + + background = estimate_sphere_revolution_background( + channel, + radius=0.5, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=0.5, + ) + + assert np.array_equal(background.data, data) + assert np.array_equal(corrected.data, np.zeros_like(data)) + + +def test_radius_larger_than_domain_is_supported() -> None: + data = np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.5, 3.0, 0.4], + [0.1, 0.6, 1.8, 0.2], + ] + ) + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=1e12, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=1e12, + ) + + assert background.data.shape == data.shape + assert np.all(np.isfinite(background.data)) + assert np.allclose( + corrected.data + background.data, + data, + rtol=1e-13, + atol=1e-13, + ) + + +def test_sphere_structure_preserves_small_sagitta_for_large_radius() -> None: + structure, footprint = _sphere_structure( + radius=1e12, + x_spacing=1.0, + y_spacing=1.0, + shape=(3, 3), + ) + + assert structure.shape == (5, 5) + assert np.all(footprint) + assert structure[2, 2] == 0.0 + assert structure[2, 3] == pytest.approx(-5e-13, rel=1e-12) + assert structure[3, 3] == pytest.approx(-1e-12, rel=1e-12) + + +def test_equivalent_metres_and_nanometres_agree_physically() -> None: + data_metres = ( + np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.5, 3.0, 0.4], + [0.1, 0.6, 1.8, 0.2], + ] + ) + * 1e-9 + ) + + channel_metres = _channel( + data_metres, + unit="m", + x_range=4e-6, + y_range=3e-6, + ) + channel_nanometres = _channel( + data_metres * 1e9, + unit="nm", + x_range=4e-6, + y_range=3e-6, + ) + + background_metres = estimate_sphere_revolution_background( + channel_metres, + radius=1.5e-6, + ) + background_nanometres = estimate_sphere_revolution_background( + channel_nanometres, + radius=1.5e-6, + ) + + assert np.allclose( + background_metres.data, + background_nanometres.data * 1e-9, + rtol=1e-12, + atol=1e-18, + ) + + +def test_input_is_not_mutated_and_context_is_preserved() -> None: + data = np.array( + [ + [0.0, 0.2, 1.0], + [0.3, 1.5, 0.4], + ] + ) + original = data.copy() + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=1.5, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=1.5, + ) + + assert np.array_equal(channel.data, original) + + for result in (background, corrected): + assert result is not channel + assert result.name == channel.name + assert result.unit == channel.unit + assert result.x_range == channel.x_range + assert result.y_range == channel.y_range + assert result.direction == channel.direction + assert result.group == channel.group + assert result.metadata == channel.metadata + assert result.metadata is not channel.metadata + + +@pytest.mark.parametrize( + "nonfinite", + [ + np.nan, + np.inf, + -np.inf, + ], +) +def test_nonfinite_data_are_rejected(nonfinite: float) -> None: + data = np.ones((3, 4)) + data[1, 2] = nonfinite + channel = _channel(data) + + with pytest.raises( + ValueError, + match="requires finite data", + ): + estimate_sphere_revolution_background( + channel, + radius=1.0, + ) + + +def test_non_geometric_z_unit_is_rejected() -> None: + channel = _channel( + np.ones((3, 4)), + unit="V", + ) + + with pytest.raises((TypeError, ValueError)): + estimate_sphere_revolution_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize( + "radius", + [ + 0.0, + -1.0, + np.nan, + np.inf, + -np.inf, + ], +) +def test_invalid_radius_is_rejected(radius: float) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(ValueError): + estimate_sphere_revolution_background( + channel, + radius=radius, + ) + + +@pytest.mark.parametrize( + "radius", + [ + True, + None, + "1.0", + [1.0], + 1.0 + 0.0j, + ], +) +def test_non_real_scalar_radius_is_rejected(radius: object) -> None: + channel = _channel(np.ones((3, 4))) + + with pytest.raises(TypeError): + estimate_sphere_revolution_background( + channel, + radius=radius, + ) + + +@pytest.mark.parametrize( + ("parameter", "value"), + [ + ("side", "underneath"), + ("border", "wrap"), + ], +) +def test_invalid_public_options_are_rejected( + parameter: str, + value: str, +) -> None: + channel = _channel(np.ones((3, 4))) + kwargs = {parameter: value} + + with pytest.raises(ValueError): + estimate_sphere_revolution_background( + channel, + radius=1.0, + **kwargs, + ) + + +@pytest.mark.parametrize( + ("parameter", "value"), + [ + ("side", None), + ("border", 1), + ], +) +def test_non_string_public_options_are_rejected( + parameter: str, + value: object, +) -> None: + channel = _channel(np.ones((3, 4))) + kwargs = {parameter: value} + + with pytest.raises( + TypeError, + match=rf"requires {parameter} to be a string", + ): + estimate_sphere_revolution_background( + channel, + radius=1.0, + **kwargs, + ) + + +@pytest.mark.parametrize( + "data", + [ + np.ones(4), + np.empty((0, 3)), + np.array([["a", "b"], ["c", "d"]]), + np.ones((2, 3), dtype=complex), + ], + ids=[ + "one-dimensional", + "empty", + "non-numeric", + "complex", + ], +) +def test_invalid_channel_data_are_rejected(data: np.ndarray) -> None: + channel = SPMChannel( + name="invalid", + data=data, + unit="m", + x_range=3.0, + y_range=2.0, + ) + + with pytest.raises((TypeError, ValueError)): + estimate_sphere_revolution_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize( + ("x_range", "y_range"), + [ + (0.0, 3.0), + (-1.0, 3.0), + (np.inf, 3.0), + (4.0, 0.0), + (4.0, -1.0), + (4.0, np.inf), + ], +) +def test_invalid_lateral_geometry_is_rejected( + x_range: float, + y_range: float, +) -> None: + channel = _channel( + np.ones((3, 4)), + x_range=x_range, + y_range=y_range, + ) + + with pytest.raises(ValueError): + estimate_sphere_revolution_background( + channel, + radius=1.0, + ) + + +@pytest.mark.parametrize("side", ["below", "above"]) +@pytest.mark.parametrize("border", ["nearest", "reflect"]) +def test_reconstruction_identity_for_every_mode( + side: str, + border: str, +) -> None: + data = np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.5, 3.0, 0.4], + [0.1, 0.6, 1.8, 0.2], + ] + ) + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=1.5, + side=side, + border=border, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=1.5, + side=side, + border=border, + ) + + assert np.allclose( + corrected.data + background.data, + data, + rtol=1e-13, + atol=1e-13, + ) + + +@pytest.mark.parametrize( + "data", + [ + np.array([[2.0]]), + np.array([[0.0, 2.0, 0.5, 1.0]]), + np.array([[0.0], [2.0], [0.5], [1.0]]), + ], + ids=[ + "one-by-one", + "one-row", + "one-column", + ], +) +def test_degenerate_dimensions_are_defined(data: np.ndarray) -> None: + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=2.0, + ) + corrected = remove_sphere_revolution_background( + channel, + radius=2.0, + ) + + assert background.data.shape == data.shape + assert corrected.data.shape == data.shape + assert np.all(np.isfinite(background.data)) + assert np.allclose( + corrected.data + background.data, + data, + ) + + +def test_below_background_does_not_exceed_surface() -> None: + data = np.array( + [ + [0.0, 0.2, 1.0, 0.1], + [0.3, 1.5, 3.0, 0.4], + [0.1, 0.6, 1.8, 0.2], + ] + ) + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=1.5, + side="below", + ) + + assert np.all(background.data <= data + 1e-13) + + +def test_above_background_does_not_fall_below_surface() -> None: + data = np.array( + [ + [0.0, -0.2, -1.0, -0.1], + [-0.3, -1.5, -3.0, -0.4], + [-0.1, -0.6, -1.8, -0.2], + ] + ) + channel = _channel(data) + + background = estimate_sphere_revolution_background( + channel, + radius=1.5, + side="above", + ) + + assert np.all(background.data >= data - 1e-13) + + +def test_functions_are_available_from_public_analysis_api() -> None: + from spmkit.core.analysis import ( + estimate_sphere_revolution_background as public_estimate, + ) + from spmkit.core.analysis import ( + remove_sphere_revolution_background as public_remove, + ) + + assert public_estimate is estimate_sphere_revolution_background + assert public_remove is remove_sphere_revolution_background diff --git a/tests/core/test_spline_background.py b/tests/core/test_spline_background.py new file mode 100644 index 0000000..37267a0 --- /dev/null +++ b/tests/core/test_spline_background.py @@ -0,0 +1,420 @@ +"""Tests for the private P-spline background adapter.""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import numpy as np +import pytest +from scipy.interpolate import BSpline + +import spmkit.core.analysis as analysis +from spmkit.core.analysis._pspline import ( + _open_uniform_knots, + fit_pspline_surface, +) +from spmkit.core.analysis.background import ( + BackgroundResult, + _fit_spline_background, + analyze_spline_background, + estimate_spline_background, + remove_spline_background, +) +from spmkit.core.models import SPMChannel + + +def _channel(data: np.ndarray) -> SPMChannel: + return SPMChannel( + name="Z-Axis", + data=np.asarray(data), + unit="nm", + x_range=17e-6, + y_range=15e-6, + direction="forward", + group="Topography", + metadata={"source": "synthetic"}, + ) + + +def _zero_penalty_surface( + *, + rows: int = 15, + columns: int = 17, + n_basis_x: int = 6, + n_basis_y: int = 6, +) -> np.ndarray: + degree = 3 + + knots_x = _open_uniform_knots( + n_basis_x, + degree, + ) + knots_y = _open_uniform_knots( + n_basis_y, + degree, + ) + + basis_x = BSpline.design_matrix( + np.linspace(0.0, 1.0, columns), + knots_x, + degree, + ) + basis_y = BSpline.design_matrix( + np.linspace(0.0, 1.0, rows), + knots_y, + degree, + ) + + x_index = np.arange( + n_basis_x, + dtype=float, + )[None, :] + y_index = np.arange( + n_basis_y, + dtype=float, + )[:, None] + + coefficients = 2.0 + 0.3 * x_index - 0.2 * y_index + 0.05 * x_index * y_index + + return np.asarray( + basis_y @ coefficients @ basis_x.T, + dtype=float, + order="C", + ) + + +def test_adapter_uses_physical_pixel_centres() -> None: + data = _zero_penalty_surface() + channel = _channel(data) + + fit = _fit_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + smoothing_x=2.0, + smoothing_y=3.0, + ) + + assert fit.x_min == pytest.approx( + 0.5 * channel.pixel_size_x, + ) + assert fit.x_max == pytest.approx( + channel.x_range - 0.5 * channel.pixel_size_x, + ) + assert fit.y_min == pytest.approx( + 0.5 * channel.pixel_size_y, + ) + assert fit.y_max == pytest.approx( + channel.y_range - 0.5 * channel.pixel_size_y, + ) + + +def test_adapter_matches_direct_core_fit() -> None: + rng = np.random.default_rng(20260801) + data = rng.normal( + size=(15, 17), + ) + channel = _channel(data) + + weights = np.linspace( + 0.5, + 1.5, + data.size, + ).reshape(data.shape) + + x = (np.arange(data.shape[1], dtype=float) + 0.5) * channel.pixel_size_x + y = (np.arange(data.shape[0], dtype=float) + 0.5) * channel.pixel_size_y + + expected = fit_pspline_surface( + data, + x=x, + y=y, + mask=np.ones( + data.shape, + dtype=bool, + ), + weights=weights, + n_basis_x=6, + n_basis_y=6, + smoothing_x=0.8, + smoothing_y=1.7, + ) + + observed = _fit_spline_background( + channel, + weights=weights, + n_basis_x=6, + n_basis_y=6, + smoothing_x=0.8, + smoothing_y=1.7, + ) + + np.testing.assert_array_equal( + observed.model, + expected.model, + ) + np.testing.assert_array_equal( + observed.coefficients, + expected.coefficients, + ) + + +def test_exclude_mask_removes_feature_from_fit() -> None: + expected = _zero_penalty_surface() + data = expected.copy() + data[7, 8] += 100.0 + + mask = np.zeros( + data.shape, + dtype=bool, + ) + mask[7, 8] = True + + observed = estimate_spline_background( + _channel(data), + n_basis_x=6, + n_basis_y=6, + smoothing_x=4.0, + smoothing_y=7.0, + mask=mask, + mask_mode="exclude", + ) + + np.testing.assert_allclose( + observed.data, + expected, + rtol=0.0, + atol=3e-10, + ) + + +def test_include_mask_can_exclude_nonfinite_data() -> None: + data = _zero_penalty_surface() + mask = np.ones( + data.shape, + dtype=bool, + ) + + mask[7, 8] = False + data[7, 8] = np.nan + + observed = estimate_spline_background( + _channel(data), + n_basis_x=6, + n_basis_y=6, + mask=mask, + mask_mode="include", + ) + + assert np.all(np.isfinite(observed.data)) + + +def test_ignore_mode_selects_nonfinite_data() -> None: + data = _zero_penalty_surface() + data[7, 8] = np.nan + + with pytest.raises( + ValueError, + match="selected P-spline data must be finite", + ): + estimate_spline_background( + _channel(data), + n_basis_x=6, + n_basis_y=6, + mask_mode="ignore", + ) + + +@pytest.mark.parametrize( + ("mask", "mask_mode", "message"), + [ + ( + None, + "include", + "requires a mask", + ), + ( + np.ones( + (15, 17), + dtype=int, + ), + "include", + "boolean mask", + ), + ( + np.ones( + (15, 17), + dtype=bool, + ), + "unknown", + "mask_mode must be", + ), + ], +) +def test_shared_mask_contract_is_enforced( + mask: np.ndarray | None, + mask_mode: str, + message: str, +) -> None: + with pytest.raises( + (TypeError, ValueError), + match=message, + ): + estimate_spline_background( + _channel(_zero_penalty_surface()), + n_basis_x=6, + n_basis_y=6, + mask=mask, + mask_mode=mask_mode, # type: ignore[arg-type] + ) + + +def test_output_preserves_context_without_mutation() -> None: + data = _zero_penalty_surface() + channel = _channel(data) + + original_data = channel.data.copy() + original_metadata = dict(channel.metadata) + + observed = estimate_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + ) + + assert observed is not channel + assert observed.name == channel.name + assert observed.unit == channel.unit + assert observed.x_range == channel.x_range + assert observed.y_range == channel.y_range + assert observed.direction == channel.direction + assert observed.group == channel.group + assert observed.metadata == channel.metadata + assert observed.data.flags.c_contiguous + assert observed.data.flags.writeable + + np.testing.assert_array_equal( + channel.data, + original_data, + ) + assert channel.metadata == original_metadata + + +def test_spline_background_api_is_public() -> None: + assert analysis.estimate_spline_background is estimate_spline_background + assert analysis.remove_spline_background is remove_spline_background + assert analysis.analyze_spline_background is analyze_spline_background + + exported = getattr(analysis, "__all__", ()) + + assert "estimate_spline_background" in exported + assert "remove_spline_background" in exported + assert "analyze_spline_background" in exported + + +def test_remove_matches_input_minus_estimate() -> None: + data = _zero_penalty_surface() + channel = _channel(data) + + background = estimate_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + ) + corrected = remove_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + ) + + np.testing.assert_allclose( + corrected.data, + channel.data - background.data, + rtol=0.0, + atol=1e-12, + ) + + +def test_remove_preserves_context_without_mutation() -> None: + data = _zero_penalty_surface() + channel = _channel(data) + original = channel.data.copy() + + corrected = remove_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + ) + + assert corrected is not channel + assert corrected.name == channel.name + assert corrected.unit == channel.unit + assert corrected.x_range == channel.x_range + assert corrected.y_range == channel.y_range + assert corrected.direction == channel.direction + assert corrected.group == channel.group + assert corrected.metadata == channel.metadata + + np.testing.assert_array_equal( + channel.data, + original, + ) + + +def test_analyze_returns_serializable_structured_result() -> None: + data = _zero_penalty_surface() + channel = _channel(data) + + result = analyze_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + smoothing_x=2.0, + smoothing_y=3.0, + ) + + assert isinstance(result, BackgroundResult) + assert result.method == "spline" + + np.testing.assert_allclose( + result.corrected.data, + channel.data - result.background.data, + rtol=0.0, + atol=1e-12, + ) + + assert result.parameters["n_basis_x"] == 6 + assert result.parameters["n_basis_y"] == 6 + assert result.parameters["smoothing_x"] == 2.0 + assert result.parameters["smoothing_y"] == 3.0 + assert result.parameters["mask_provided"] is False + assert result.parameters["weights_provided"] is False + + diagnostics = result.parameters["diagnostics"] + + assert isinstance(diagnostics, dict) + assert diagnostics["selected_points"] == data.size + assert diagnostics["total_points"] == data.size + assert diagnostics["solver_iterations"] >= 0 + assert diagnostics["condition_estimate"] >= 0.0 + + json.dumps(result.to_dict()) + + +def test_analyze_performs_exactly_one_fit() -> None: + data = _zero_penalty_surface() + channel = _channel(data) + + with patch( + "spmkit.core.analysis.background._fit_spline_background", + wraps=_fit_spline_background, + ) as fit_mock: + result = analyze_spline_background( + channel, + n_basis_x=6, + n_basis_y=6, + ) + + assert fit_mock.call_count == 1 + assert result.method == "spline" diff --git a/tests/gui/conftest.py b/tests/gui/conftest.py index abf65e6..472c23f 100644 --- a/tests/gui/conftest.py +++ b/tests/gui/conftest.py @@ -28,23 +28,34 @@ if _HAS_GUI: - @pytest.fixture(autouse=True) - def _flush_qt(): # type: ignore[no-untyped-def] - """Purga los widgets pendientes (deleteLater) entre tests. + @pytest.fixture(scope="session", autouse=True) + def _disable_cyclic_gc_for_gui_tests(): # type: ignore[no-untyped-def] + """Desactiva el GC cíclico durante la fase GUI de pytest. + + Los widgets Qt, los ViewBox de pyqtgraph y los FigureCanvas de Matplotlib + forman ciclos con callbacks nativos. Una recolección automática mientras + Qt procesa eventos puede destruir parcialmente esos objetos y provocar un + segmentation fault. El proceso de pytest es efímero, por lo que el sistema + operativo recupera sus recursos al terminar. + """ + gc.disable() + yield - Evita el segfault por acumulación de recursos nativos al correr muchos tests - de GUI pesados en un mismo proceso (cada uno crea un Workspace completo). + @pytest.fixture(autouse=True) + def _flush_qt(qapp): # type: ignore[no-untyped-def] + """Drena eliminaciones diferidas y eventos Qt entre tests. + + pytest-qt cierra los widgets registrados con ``qtbot.addWidget``. Aquí solo + procesamos ``DeferredDelete`` y eventos pendientes. No se fuerza + ``gc.collect()`` porque Qt, pyqtgraph y Matplotlib pueden conservar callbacks + nativos durante el teardown y recolectarlos en ese punto puede causar un + segmentation fault. """ yield - from PyQt6.QtWidgets import QApplication - - app = QApplication.instance() - if app is not None: - app.processEvents() - app.sendPostedEvents(None, 0) # ejecuta los deleteLater encolados - gc.collect() - if app is not None: - app.processEvents() + from PyQt6.QtCore import QCoreApplication, QEvent + + QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) + qapp.processEvents() def _hertz_curve( diff --git a/tests/gui/test_grains_spectral.py b/tests/gui/test_grains_spectral.py index 48590ae..ca0701d 100644 --- a/tests/gui/test_grains_spectral.py +++ b/tests/gui/test_grains_spectral.py @@ -2,18 +2,13 @@ from __future__ import annotations -import importlib.util - import numpy as np -import pytest from spmkit.core.models import SPMChannel, SPMData from spmkit.gui.panels.grains_canvas import GrainsCanvasPanel from spmkit.gui.panels.spectral_canvas import SpectralCanvasPanel from spmkit.gui.viewmodels import GrainsViewModel, ImageViewModel, SpectralViewModel -_HAS_SCIPY = importlib.util.find_spec("scipy") is not None - def _bumps() -> SPMData: z = np.zeros((32, 32), dtype=float) @@ -117,7 +112,6 @@ def test_grains_panel_auto_toggle(qtbot) -> None: # type: ignore[no-untyped-def assert vm.threshold is not None and abs(vm.threshold - 12e-9) < 1e-15 # nm → m -@pytest.mark.skipif(not _HAS_SCIPY, reason="grains requiere scipy (extra 'grains')") def test_grains_vm_detects() -> None: image_vm = ImageViewModel() image_vm.set_data(_bumps()) @@ -131,7 +125,6 @@ def test_grains_vm_detects() -> None: assert seen[-1] is vm.result -@pytest.mark.skipif(not _HAS_SCIPY, reason="grains requiere scipy (extra 'grains')") def test_grains_panel_overlay(qtbot) -> None: # type: ignore[no-untyped-def] image_vm = ImageViewModel() image_vm.set_data(_bumps()) diff --git a/tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.json b/tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.json new file mode 100644 index 0000000..580e842 --- /dev/null +++ b/tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.json @@ -0,0 +1,12228 @@ +{ + "capability": "gwyddion_align_rows_statistics", + "case_count": 64, + "cases": [ + { + "case_identifier": "median__alternating_offsets__02", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c000000000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4010800000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "bffc000000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4011800000000000", + "4013800000000000", + "4015800000000000", + "4017800000000000", + "4019800000000000", + "401b800000000000", + "401d800000000000", + "401f800000000000", + "4020c00000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4004000000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "4021400000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4006000000000000" + ], + "input_key": "input__median__alternating_offsets__02", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__alternating_offsets__02", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median__alternating_offsets__02", + "masking_mode": 2, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__alternating_offsets__02", + "portable_correction_sequence_bits": [ + "c007924924924925", + "40096db6db6db6db", + "c005924924924925", + "400b6db6db6db6db", + "c003924924924925", + "400d6db6db6db6db", + "c001924924924925" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__constant__00", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000" + ], + "input_key": "input__median__constant__00", + "installed_background_key": "installed_background__median__constant__00", + "installed_corrected_key": "installed_corrected__median__constant__00", + "installed_mutated": false, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 1, + "method_name": "Median", + "portable_background_key": "portable_background__median__constant__00", + "portable_corrected_key": "portable_corrected__median__constant__00", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__impulses__07", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "0000000000000000", + "3fd3333333333333", + "3fe3333333333333", + "3feccccccccccccc", + "3ff3333333333333", + "3ff8000000000000", + "3ffccccccccccccc", + "4000cccccccccccd", + "4003333333333333", + "bfc999999999999a", + "3fb9999999999998", + "4033666666666666", + "3fe6666666666666", + "3ff0000000000000", + "3ff4cccccccccccd", + "3ff9999999999999", + "3ffe666666666667", + "4001999999999999", + "bfd999999999999a", + "bfb999999999999c", + "3fc9999999999998", + "3fdffffffffffffe", + "3fe9999999999999", + "3ff199999999999a", + "3ff6666666666666", + "3ffb333333333334", + "4000000000000000", + "bfe3333333333334", + "bfd3333333333335", + "bca0000000000000", + "3fd3333333333330", + "3fe3333333333332", + "3feccccccccccccc", + "3ff3333333333332", + "3ff8000000000000", + "3ffccccccccccccc", + "bfe999999999999a", + "bfe0000000000000", + "bfc999999999999c", + "3fb9999999999990", + "3fd9999999999998", + "3fe6666666666666", + "3feffffffffffffe", + "3ff4cccccccccccd", + "3ff9999999999999", + "bff0000000000000", + "bfe6666666666666", + "bfd999999999999a", + "bfb99999999999a0", + "3fc9999999999998", + "3fe0000000000000", + "c030333333333333", + "3ff199999999999a", + "3ff6666666666666", + "bff3333333333334", + "bfecccccccccccce", + "bfe3333333333335", + "bfd3333333333338", + "bcb0000000000000", + "3fd3333333333330", + "3fe3333333333330", + "3feccccccccccccc", + "3ff3333333333332" + ], + "input_key": "input__median__impulses__07", + "installed_background_key": "installed_background__median__impulses__07", + "installed_corrected_key": "installed_corrected__median__impulses__07", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median__impulses__07", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": "portable_background__median__impulses__07", + "portable_corrected_key": "portable_corrected__median__impulses__07", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__irregular__11", + "columns": 10, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c02c000000000000", + "4013c00000000000", + "c014800000000000", + "402ba00000000000", + "400e000000000000", + "c019400000000000", + "4029400000000000", + "4004800000000000", + "c01e000000000000", + "4026e00000000000", + "c017e00000000000", + "c022100000000000", + "c028300000000000", + "402bb00000000000", + "4025900000000000", + "401ee00000000000", + "4012a00000000000", + "3ff9800000000000", + "bff7800000000000", + "c012200000000000", + "4000800000000000", + "4018000000000000", + "4023e00000000000", + "402bc00000000000", + "c026600000000000", + "c01d000000000000", + "c00a800000000000", + "3fe4000000000000", + "4012400000000000", + "4021000000000000", + "4024300000000000", + "c01fe00000000000", + "4007c00000000000", + "402bd00000000000", + "c010a00000000000", + "401b200000000000", + "c026900000000000", + "bfd6000000000000", + "4025300000000000", + "c01de00000000000", + "c025c00000000000", + "401c400000000000", + "c010000000000000", + "402be00000000000", + "4007000000000000", + "c020600000000000", + "4023800000000000", + "bff5000000000000", + "c028c00000000000", + "4016400000000000", + "c006c00000000000", + "c01ba00000000000", + "c025f00000000000", + "402bf00000000000", + "4023d00000000000", + "4017600000000000", + "3ffc800000000000", + "c002400000000000", + "c019600000000000", + "c024d00000000000", + "4014c00000000000", + "4020400000000000", + "4026200000000000", + "402c000000000000", + "c028200000000000", + "c022400000000000", + "c018c00000000000", + "c00a000000000000", + "bfd4000000000000", + "4005000000000000", + "402a700000000000", + "c017600000000000", + "4010600000000000", + "402c100000000000", + "c014200000000000", + "4013a00000000000", + "c02c500000000000", + "c010e00000000000", + "4016e00000000000", + "c02ab00000000000" + ], + "input_key": "input__median__irregular__11", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__irregular__11", + "installed_mutated": true, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median__irregular__11", + "masking_mode": 2, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__irregular__11", + "portable_correction_sequence_bits": [ + "3ff3a00000000000", + "bfedc00000000000", + "4000500000000000", + "3fdc800000000000", + "3fd6800000000000", + "c013380000000000", + "3fba000000000000", + "3ff9200000000000" + ], + "portable_mutated": true, + "rows": 8, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.119999999999ap+3", + "yreal_hex": "0x1.f8f5c28f5c28fp+3" + }, + { + "case_identifier": "median__linear__03", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "c004cccccccccccd", + "bff6666666666666", + "bfc9999999999998", + "3ff0000000000002", + "400199999999999a", + "400b333333333333", + "4012666666666668", + "4017333333333334", + "401c000000000000", + "c00f333333333334", + "c004000000000001", + "bff199999999999a", + "3fd3333333333338", + "3ffb333333333334", + "4008cccccccccccd", + "4012000000000001", + "401799999999999b", + "401d333333333334", + "c014cccccccccccd", + "c00cccccccccccce", + "c000000000000000", + "bfd9999999999990", + "3ff3333333333334", + "4006666666666666", + "401199999999999b", + "4018000000000001", + "401e666666666667", + "c01a000000000000", + "c012cccccccccccd", + "c007333333333334", + "bff1999999999998", + "3fe6666666666668", + "4004000000000000", + "4011333333333334", + "4018666666666667", + "401f99999999999a", + "c01f333333333334", + "c017333333333334", + "c00e666666666668", + "bffccccccccccccc", + "3fc99999999999a0", + "400199999999999a", + "4010ccccccccccce", + "4018cccccccccccf", + "4020666666666667" + ], + "input_key": "input__median__linear__03", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__linear__03", + "installed_mutated": true, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median__linear__03", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__linear__03", + "portable_correction_sequence_bits": [ + "3ff8000000000002", + "3ff0000000000002", + "3fe0000000000004", + "3cc0000000000000", + "bfdffffffffffff8", + "bfeffffffffffffc", + "bff7fffffffffffe" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__linear__12", + "columns": 11, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "401ccccccccccccd", + "4020000000000000", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "401ecccccccccccd", + "4021666666666666" + ], + "input_key": "input__median__linear__12", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__linear__12", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median__linear__12", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__linear__12", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 2, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.28f5c28f5c28fp+3", + "yreal_hex": "0x1.147ae147ae148p+2" + }, + { + "case_identifier": "median__multimodal__09", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "c00f000000000000", + "c010000000000000", + "c010000000000000", + "4008000000000000", + "4009000000000000", + "4008000000000000", + "4020000000000000", + "4020000000000000", + "4020400000000000", + "c00e000000000000", + "c00e000000000000", + "c00e000000000000", + "400b000000000000", + "400a000000000000", + "400a000000000000", + "4020800000000000", + "4020c00000000000", + "4020800000000000", + "c00c000000000000", + "c00c000000000000", + "c00b000000000000", + "400c000000000000", + "400c000000000000", + "400c000000000000", + "4021400000000000", + "4021000000000000", + "4021000000000000", + "c00a000000000000", + "c009000000000000", + "c00a000000000000", + "400e000000000000", + "400e000000000000", + "400f000000000000", + "4021800000000000", + "4021800000000000", + "4021800000000000", + "c007000000000000", + "c008000000000000", + "c008000000000000", + "4010000000000000", + "4010800000000000", + "4010000000000000", + "4022000000000000", + "4022000000000000", + "4022400000000000", + "c006000000000000", + "c006000000000000", + "c006000000000000", + "4011800000000000", + "4011000000000000", + "4011000000000000", + "4022800000000000", + "4022c00000000000", + "4022800000000000", + "c004000000000000", + "c004000000000000", + "c003000000000000", + "4012000000000000", + "4012000000000000", + "4012000000000000", + "4023400000000000", + "4023000000000000", + "4023000000000000" + ], + "input_key": "input__median__multimodal__09", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__multimodal__09", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median__multimodal__09", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__multimodal__09", + "portable_correction_sequence_bits": [ + "c019aaaaaaaaaaaa", + "c0192aaaaaaaaaaa", + "c019aaaaaaaaaaaa", + "3feaaaaaaaaaaaac", + "3fe2aaaaaaaaaaac", + "3fe6aaaaaaaaaaac", + "4016555555555556", + "4017555555555556", + "4016555555555556" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__nonlinear__04", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3ff1eb851eb851ec", + "3ff2b851eb851eb8", + "40030a3d70a3d70a", + "400ecccccccccccd", + "4012b851eb851eb8", + "401aae147ae147ae", + "4021d70a3d70a3d7", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "3fd6666666666666", + "3ff4cccccccccccc", + "4004147ae147ae15", + "4008a3d70a3d70a4", + "40133d70a3d70a3d", + "401b333333333333", + "40204ccccccccccd", + "bfb47ae147ae1480", + "bfe199999999999a", + "3fc1eb851eb851ea", + "3ff170a3d70a3d70", + "3ff6666666666666", + "4006f5c28f5c28f5", + "4012666666666666", + "4016c28f5c28f5c3", + "401fc28f5c28f5c3", + "bff87ae147ae147b", + "bff1999999999999", + "bfda3d70a3d70a3e", + "bfd70a3d70a3d70c", + "3feb333333333334", + "40028f5c28f5c28f", + "4009333333333332", + "40148f5c28f5c28f", + "401d8f5c28f5c28f", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00199999999999a", + "bff4000000000001", + "bfa47ae147ae1490", + "3fe0f5c28f5c28f4", + "4002147ae147ae13", + "4011000000000000", + "4016666666666666", + "c00d333333333333", + "c0107ae147ae147b", + "c00b70a3d70a3d71", + "c003d70a3d70a3d7", + "c0015c28f5c28f5c", + "bfe6666666666666", + "3ff07ae147ae147a", + "4000f5c28f5c28f6", + "40117ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c014000000000000", + "c013cccccccccccd", + "c00deb851eb851ec", + "c00228f5c28f5c29", + "bff70a3d70a3d70c", + "3fe199999999999a", + "4006666666666667" + ], + "input_key": "input__median__nonlinear__04", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__nonlinear__04", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median__nonlinear__04", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__nonlinear__04", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__nonlinear__13", + "columns": 2, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "bfb47ae147ae1480", + "bfe199999999999a", + "bff87ae147ae147b", + "bff1999999999999", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00d333333333333", + "c0107ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c0200f5c28f5c290", + "c01e666666666668", + "c0248f5c28f5c290", + "c025800000000000", + "c02b8a3d70a3d70a", + "c02aae147ae147ad", + "c030b33333333333", + "c030451eb851eb85" + ], + "input_key": "input__median__nonlinear__13", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__nonlinear__13", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median__nonlinear__13", + "masking_mode": 2, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__nonlinear__13", + "portable_correction_sequence_bits": [ + "3fce147ae147ae10", + "bfce147ae147ae20" + ], + "portable_mutated": true, + "rows": 11, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.5ae147ae147aep+1", + "yreal_hex": "0x1.5828f5c28f5c2p+4" + }, + { + "case_identifier": "median__plane__05", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3ffb333333333333", + "400b333333333333", + "4014666666666666", + "401b333333333333", + "4021000000000000", + "4024666666666666", + "4027cccccccccccd", + "402b333333333333", + "c002666666666666", + "bfdffffffffffffe", + "3ff4cccccccccccd", + "4008cccccccccccc", + "401399999999999a", + "401acccccccccccd", + "4021000000000000", + "402499999999999a", + "4028333333333334", + "c012666666666666", + "c005999999999998", + "bfe9999999999997", + "3ff199999999999a", + "4008000000000000", + "401399999999999a", + "401b333333333333", + "4021666666666667", + "4025333333333333", + "c01b999999999999", + "c013999999999999", + "c007333333333332", + "bfecccccccccccca", + "3ff199999999999c", + "4008ccccccccccce", + "4014666666666666", + "401c666666666668", + "4022333333333334", + "c022666666666666", + "c01c666666666665", + "c013ffffffffffff", + "c007333333333332", + "bfe9999999999994", + "3ff4ccccccccccd0", + "400b333333333334", + "4016000000000002", + "401e666666666667", + "c027000000000000", + "c02299999999999a", + "c01c666666666666", + "c01399999999999a", + "c00599999999999a", + "bfe0000000000000", + "3ffb333333333330", + "400f333333333334", + "4018666666666666", + "c02b999999999999", + "c027000000000000", + "c022666666666666", + "c01b999999999998", + "c012666666666665", + "c002666666666663", + "3cd0000000000000", + "400266666666666c", + "4012666666666668" + ], + "input_key": "input__median__plane__05", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__plane__05", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__median__plane__05", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__plane__05", + "portable_correction_sequence_bits": [ + "c019777777777778", + "c01aaaaaaaaaaaaa", + "c012aaaaaaaaaaaa", + "bfe5555555555558", + "bfe5555555555554", + "3ff5555555555556", + "4014222222222222", + "4015555555555556", + "401d555555555556" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__plateaus_signed_zero__10", + "columns": 8, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "c008000000000000", + "c000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "c000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4000000000000000" + ], + "input_key": "input__median__plateaus_signed_zero__10", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__plateaus_signed_zero__10", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median__plateaus_signed_zero__10", + "masking_mode": 2, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__plateaus_signed_zero__10", + "portable_correction_sequence_bits": [ + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": true, + "rows": 6, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.c5c28f5c28f5cp+2", + "yreal_hex": "0x1.7eb851eb851ebp+3" + }, + { + "case_identifier": "median__row_offsets__01", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd0000000000000", + "3fe0000000000000", + "3fe8000000000000", + "3ff0000000000000", + "3ff4000000000000", + "3ff8000000000000", + "3ffc000000000000", + "4000000000000000", + "4008000000000000", + "400a000000000000", + "400c000000000000", + "400e000000000000", + "4010000000000000", + "4011000000000000", + "4012000000000000", + "4013000000000000", + "4014000000000000", + "4018000000000000", + "4019000000000000", + "401a000000000000", + "401b000000000000", + "401c000000000000", + "401d000000000000", + "401e000000000000", + "401f000000000000", + "4020000000000000", + "4022000000000000", + "4022800000000000", + "4023000000000000", + "4023800000000000", + "4024000000000000", + "4024800000000000", + "4025000000000000", + "4025800000000000", + "4026000000000000", + "4028000000000000", + "4028800000000000", + "4029000000000000", + "4029800000000000", + "402a000000000000", + "402a800000000000", + "402b000000000000", + "402b800000000000", + "402c000000000000", + "402e000000000000", + "402e800000000000", + "402f000000000000", + "402f800000000000", + "4030000000000000", + "4030400000000000", + "4030800000000000", + "4030c00000000000", + "4031000000000000", + "4032000000000000", + "4032400000000000", + "4032800000000000", + "4032c00000000000", + "4033000000000000", + "4033400000000000", + "4033800000000000", + "4033c00000000000", + "4034000000000000" + ], + "input_key": "input__median__row_offsets__01", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__row_offsets__01", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median__row_offsets__01", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__row_offsets__01", + "portable_correction_sequence_bits": [ + "c021a49249249249", + "c018492492492492", + "c008924924924924", + "bfb2492492492480", + "40096db6db6db6dc", + "4017b6db6db6db6e", + "4021db6db6db6db7" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__scars__08", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "c01b666666666666", + "3fd3333333333333", + "3fdccccccccccccc", + "3fe3333333333333", + "3fe8000000000000", + "3feccccccccccccc", + "c017cccccccccccd", + "3ff3333333333333", + "3ff0000000000000", + "40224ccccccccccd", + "3ff4cccccccccccd", + "3ff7333333333333", + "3ff999999999999a", + "3ffc000000000000", + "3ffe666666666666", + "402419999999999a", + "400199999999999a", + "4000000000000000", + "40244ccccccccccd", + "4002666666666666", + "400399999999999a", + "4004cccccccccccd", + "4006000000000000", + "4007333333333333", + "402619999999999a", + "400999999999999a", + "4008000000000000", + "c00ecccccccccccd", + "400a666666666666", + "400b99999999999a", + "400ccccccccccccd", + "400e000000000000", + "400f333333333333", + "c00799999999999a", + "4010cccccccccccd", + "4010000000000000", + "40284ccccccccccd", + "4011333333333333", + "4011cccccccccccd", + "4012666666666666", + "4013000000000000", + "401399999999999a", + "402a19999999999a", + "4014cccccccccccd", + "4014000000000000", + "402a4ccccccccccd", + "4015333333333333", + "4015cccccccccccd", + "4016666666666666", + "4017000000000000", + "401799999999999a", + "402c19999999999a", + "4018cccccccccccd", + "4018000000000000", + "bfeb333333333330", + "4019333333333333", + "4019cccccccccccd", + "401a666666666666", + "401b000000000000", + "401b99999999999a", + "3fa9999999999980", + "401ccccccccccccd" + ], + "input_key": "input__median__scars__08", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__scars__08", + "installed_mutated": true, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__scars__08", + "portable_correction_sequence_bits": [ + "c0095f15f15f15f0", + "bffdf15f15f15f14", + "bfebe2be2be2be28", + "bfc5f15f15f15f00", + "3ff20ea0ea0ea0ec", + "4001075075075076", + "4006a0ea0ea0ea10" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__step__06", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666" + ], + "input_key": "input__median__step__06", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__step__06", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__median__step__06", + "masking_mode": 2, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__step__06", + "portable_correction_sequence_bits": [ + "c012492492492492", + "c012492492492492", + "c012492492492492", + "400b6db6db6db6db", + "400b6db6db6db6db", + "400b6db6db6db6db", + "400b6db6db6db6db" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median__tall__15", + "columns": 5, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd999999999999a", + "3fe999999999999a", + "3ff3333333333334", + "3ff999999999999a", + "bfe999999999999a", + "bfd5c28f5c28f5c3", + "3fbeb851eb851eb8", + "3fe28f5c28f5c290", + "3ff0a3d70a3d70a4", + "bff999999999999a", + "bff147ae147ae148", + "bfe1eb851eb851ec", + "bfa47ae147ae1470", + "3fdeb851eb851eb8", + "c003333333333334", + "bffd1eb851eb8521", + "bff3d70a3d70a3d8", + "bfe51eb851eb8520", + "bfb47ae147ae1490", + "c00999999999999a", + "c0047ae147ae147c", + "bffeb851eb851eba", + "bff47ae147ae147b", + "bfe47ae147ae147c", + "c010000000000000", + "c00a666666666667", + "c004cccccccccccd", + "bffe666666666666", + "bff3333333333333", + "c013333333333334", + "c01028f5c28f5c29", + "c00a3d70a3d70a40", + "c00428f5c28f5c2a", + "bffc28f5c28f5c2c", + "c016666666666667", + "c0131eb851eb851f", + "c00fae147ae147b0", + "c0091eb851eb8520", + "c0028f5c28f5c290", + "c01999999999999a", + "c016147ae147ae14", + "c0128f5c28f5c290", + "c00e147ae147ae15", + "c0070a3d70a3d70c", + "c01ccccccccccccd", + "c0190a3d70a3d70a", + "c01547ae147ae148", + "c011851eb851eb85", + "c00b851eb851eb84", + "c020000000000000", + "c01c000000000000", + "c018000000000000", + "c014000000000000", + "c010000000000000", + "c02199999999999a", + "c01ef5c28f5c28f6", + "c01ab851eb851eb8", + "c0167ae147ae147c", + "c0123d70a3d70a3f", + "c023333333333334", + "c020f5c28f5c28f6", + "c01d70a3d70a3d72", + "c018f5c28f5c28f8", + "c0147ae147ae147d", + "c024cccccccccccd", + "c02270a3d70a3d71", + "c020147ae147ae14", + "c01b70a3d70a3d70", + "c016b851eb851eb9", + "c026666666666667", + "c023eb851eb851ec", + "c02170a3d70a3d71", + "c01deb851eb851ec", + "c018f5c28f5c28f8", + "c028000000000000", + "c025666666666666", + "c022cccccccccccc", + "c020333333333334", + "c01b333333333334", + "c02999999999999a", + "c026e147ae147ae2", + "c02428f5c28f5c29", + "c02170a3d70a3d72", + "c01d70a3d70a3d72" + ], + "input_key": "input__median__tall__15", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__tall__15", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000" + ], + "mask_key": "mask__median__tall__15", + "masking_mode": 1, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__tall__15", + "portable_correction_sequence_bits": [ + "bff3b645a1cac084", + "bff26e978d4fdf38", + "bfd16872b020c4a0", + "3fe374bc6a7ef9dc", + "4000624dd2f1a9fc" + ], + "portable_mutated": true, + "rows": 17, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.399999999999ap+2", + "yreal_hex": "0x1.07c28f5c28f5cp+5" + }, + { + "case_identifier": "median__wide__14", + "columns": 17, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fb1eb851eb851ec", + "3fd1eb851eb851ec", + "3fe428f5c28f5c2a", + "3ff1eb851eb851ec", + "3ffc000000000001", + "400428f5c28f5c2a", + "400b70a3d70a3d71", + "4011eb851eb851ec", + "4016ae147ae147af", + "401c000000000001", + "4020f0a3d70a3d71", + "402428f5c28f5c2a", + "4027a8f5c28f5c2a", + "402b70a3d70a3d71", + "402f800000000000", + "4031eb851eb851ec", + "3ff199999999999a", + "3ff23d70a3d70a3e", + "3ff51eb851eb851f", + "3ffa3d70a3d70a3e", + "4000cccccccccccd", + "400599999999999b", + "400b851eb851eb86", + "401147ae147ae148", + "40155c28f5c28f5c", + "401a000000000002", + "401f333333333335", + "40227ae147ae147b", + "4025a3d70a3d70a5", + "4029147ae147ae15", + "402ccccccccccccd", + "4030666666666667", + "40328a3d70a3d70b", + "400199999999999a", + "4001ae147ae147ae", + "4002e147ae147ae2", + "4005333333333333", + "4008a3d70a3d70a4", + "400d333333333334", + "401170a3d70a3d71", + "4014d70a3d70a3d8", + "4018ccccccccccce", + "401d51eb851eb853", + "4021333333333334", + "4024051eb851eb86", + "40271eb851eb851f", + "402a800000000001", + "402e28f5c28f5c2a", + "40310ccccccccccd", + "403328f5c28f5c29", + "400a666666666667", + "400a3d70a3d70a3e", + "400b333333333333", + "400d47ae147ae149", + "40103d70a3d70a3d", + "4012666666666667", + "40151eb851eb851f", + "4018666666666667", + "401c3d70a3d70a3f", + "402051eb851eb852", + "4022cccccccccccd", + "40258f5c28f5c290", + "402899999999999b", + "402beb851eb851ed", + "402f851eb851eb85", + "4031b33333333333", + "4033c7ae147ae148", + "401199999999999a", + "4011666666666667", + "4011c28f5c28f5c3", + "4012ae147ae147ae", + "401428f5c28f5c2a", + "4016333333333334", + "4018ccccccccccce", + "401bf5c28f5c28f6", + "401fae147ae147af", + "4021fae147ae147b", + "4024666666666668", + "402719999999999a", + "402a147ae147ae16", + "402d570a3d70a3d9", + "403070a3d70a3d71", + "4032599999999999", + "4034666666666666" + ], + "input_key": "input__median__wide__14", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median__wide__14", + "installed_mutated": true, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 1, + "method_name": "Median", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median__wide__14", + "portable_correction_sequence_bits": [ + "bffb851eb851eb88", + "bfeb851eb851eb90", + "0000000000000000", + "3feb851eb851eb88", + "3ffb851eb851eb84" + ], + "portable_mutated": true, + "rows": 5, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.b51eb851eb852p+3", + "yreal_hex": "0x1.4199999999999p+3" + }, + { + "case_identifier": "median_of_differences__alternating_offsets__02", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c000000000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4010800000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "bffc000000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4011800000000000", + "4013800000000000", + "4015800000000000", + "4017800000000000", + "4019800000000000", + "401b800000000000", + "401d800000000000", + "401f800000000000", + "4020c00000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4004000000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "4021400000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4006000000000000" + ], + "input_key": "input__median_of_differences__alternating_offsets__02", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__alternating_offsets__02", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median_of_differences__alternating_offsets__02", + "masking_mode": 2, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__alternating_offsets__02", + "portable_correction_sequence_bits": [ + "c004924924924928", + "400b6db6db6db6d9", + "c004924924924926", + "400b6db6db6db6db", + "c004924924924924", + "400b6db6db6db6dd", + "c004924924924922" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__constant__00", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000" + ], + "input_key": "input__median_of_differences__constant__00", + "installed_background_key": "installed_background__median_of_differences__constant__00", + "installed_corrected_key": "installed_corrected__median_of_differences__constant__00", + "installed_mutated": false, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": "portable_background__median_of_differences__constant__00", + "portable_corrected_key": "portable_corrected__median_of_differences__constant__00", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__impulses__07", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "0000000000000000", + "3fd3333333333333", + "3fe3333333333333", + "3feccccccccccccc", + "3ff3333333333333", + "3ff8000000000000", + "3ffccccccccccccc", + "4000cccccccccccd", + "4003333333333333", + "bfc999999999999a", + "3fb9999999999998", + "4033666666666666", + "3fe6666666666666", + "3ff0000000000000", + "3ff4cccccccccccd", + "3ff9999999999999", + "3ffe666666666667", + "4001999999999999", + "bfd999999999999a", + "bfb999999999999c", + "3fc9999999999998", + "3fdffffffffffffe", + "3fe9999999999999", + "3ff199999999999a", + "3ff6666666666666", + "3ffb333333333334", + "4000000000000000", + "bfe3333333333334", + "bfd3333333333335", + "bca0000000000000", + "3fd3333333333330", + "3fe3333333333332", + "3feccccccccccccc", + "3ff3333333333332", + "3ff8000000000000", + "3ffccccccccccccc", + "bfe999999999999a", + "bfe0000000000000", + "bfc999999999999c", + "3fb9999999999990", + "3fd9999999999998", + "3fe6666666666666", + "3feffffffffffffe", + "3ff4cccccccccccd", + "3ff9999999999999", + "bff0000000000000", + "bfe6666666666666", + "bfd999999999999a", + "bfb99999999999a0", + "3fc9999999999998", + "3fe0000000000000", + "c030333333333333", + "3ff199999999999a", + "3ff6666666666666", + "bff3333333333334", + "bfecccccccccccce", + "bfe3333333333335", + "bfd3333333333338", + "bcb0000000000000", + "3fd3333333333330", + "3fe3333333333330", + "3feccccccccccccc", + "3ff3333333333332" + ], + "input_key": "input__median_of_differences__impulses__07", + "installed_background_key": "installed_background__median_of_differences__impulses__07", + "installed_corrected_key": "installed_corrected__median_of_differences__impulses__07", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median_of_differences__impulses__07", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": "portable_background__median_of_differences__impulses__07", + "portable_corrected_key": "portable_corrected__median_of_differences__impulses__07", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__irregular__11", + "columns": 10, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c02c000000000000", + "4013c00000000000", + "c014800000000000", + "402ba00000000000", + "400e000000000000", + "c019400000000000", + "4029400000000000", + "4004800000000000", + "c01e000000000000", + "4026e00000000000", + "c017e00000000000", + "c022100000000000", + "c028300000000000", + "402bb00000000000", + "4025900000000000", + "401ee00000000000", + "4012a00000000000", + "3ff9800000000000", + "bff7800000000000", + "c012200000000000", + "4000800000000000", + "4018000000000000", + "4023e00000000000", + "402bc00000000000", + "c026600000000000", + "c01d000000000000", + "c00a800000000000", + "3fe4000000000000", + "4012400000000000", + "4021000000000000", + "4024300000000000", + "c01fe00000000000", + "4007c00000000000", + "402bd00000000000", + "c010a00000000000", + "401b200000000000", + "c026900000000000", + "bfd6000000000000", + "4025300000000000", + "c01de00000000000", + "c025c00000000000", + "401c400000000000", + "c010000000000000", + "402be00000000000", + "4007000000000000", + "c020600000000000", + "4023800000000000", + "bff5000000000000", + "c028c00000000000", + "4016400000000000", + "c006c00000000000", + "c01ba00000000000", + "c025f00000000000", + "402bf00000000000", + "4023d00000000000", + "4017600000000000", + "3ffc800000000000", + "c002400000000000", + "c019600000000000", + "c024d00000000000", + "4014c00000000000", + "4020400000000000", + "4026200000000000", + "402c000000000000", + "c028200000000000", + "c022400000000000", + "c018c00000000000", + "c00a000000000000", + "bfd4000000000000", + "4005000000000000", + "402a700000000000", + "c017600000000000", + "4010600000000000", + "402c100000000000", + "c014200000000000", + "4013a00000000000", + "c02c500000000000", + "c010e00000000000", + "4016e00000000000", + "c02ab00000000000" + ], + "input_key": "input__median_of_differences__irregular__11", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__irregular__11", + "installed_mutated": true, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median_of_differences__irregular__11", + "masking_mode": 2, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__irregular__11", + "portable_correction_sequence_bits": [ + "0000000000000000", + "bffb6db6db6db6db", + "4004924924924925", + "3feb6db6db6db6e0", + "bfeb6db6db6db6d8", + "c004924924924924", + "3ffb6db6db6db6e0", + "0000000000000000" + ], + "portable_mutated": true, + "rows": 8, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.119999999999ap+3", + "yreal_hex": "0x1.f8f5c28f5c28fp+3" + }, + { + "case_identifier": "median_of_differences__linear__03", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "c004cccccccccccd", + "bff6666666666666", + "bfc9999999999998", + "3ff0000000000002", + "400199999999999a", + "400b333333333333", + "4012666666666668", + "4017333333333334", + "401c000000000000", + "c00f333333333334", + "c004000000000001", + "bff199999999999a", + "3fd3333333333338", + "3ffb333333333334", + "4008cccccccccccd", + "4012000000000001", + "401799999999999b", + "401d333333333334", + "c014cccccccccccd", + "c00cccccccccccce", + "c000000000000000", + "bfd9999999999990", + "3ff3333333333334", + "4006666666666666", + "401199999999999b", + "4018000000000001", + "401e666666666667", + "c01a000000000000", + "c012cccccccccccd", + "c007333333333334", + "bff1999999999998", + "3fe6666666666668", + "4004000000000000", + "4011333333333334", + "4018666666666667", + "401f99999999999a", + "c01f333333333334", + "c017333333333334", + "c00e666666666668", + "bffccccccccccccc", + "3fc99999999999a0", + "400199999999999a", + "4010ccccccccccce", + "4018cccccccccccf", + "4020666666666667" + ], + "input_key": "input__median_of_differences__linear__03", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__linear__03", + "installed_mutated": false, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median_of_differences__linear__03", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__linear__03", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__linear__12", + "columns": 11, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "401ccccccccccccd", + "4020000000000000", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "401ecccccccccccd", + "4021666666666666" + ], + "input_key": "input__median_of_differences__linear__12", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__linear__12", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median_of_differences__linear__12", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__linear__12", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 2, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.28f5c28f5c28fp+3", + "yreal_hex": "0x1.147ae147ae148p+2" + }, + { + "case_identifier": "median_of_differences__multimodal__09", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "c00f000000000000", + "c010000000000000", + "c010000000000000", + "4008000000000000", + "4009000000000000", + "4008000000000000", + "4020000000000000", + "4020000000000000", + "4020400000000000", + "c00e000000000000", + "c00e000000000000", + "c00e000000000000", + "400b000000000000", + "400a000000000000", + "400a000000000000", + "4020800000000000", + "4020c00000000000", + "4020800000000000", + "c00c000000000000", + "c00c000000000000", + "c00b000000000000", + "400c000000000000", + "400c000000000000", + "400c000000000000", + "4021400000000000", + "4021000000000000", + "4021000000000000", + "c00a000000000000", + "c009000000000000", + "c00a000000000000", + "400e000000000000", + "400e000000000000", + "400f000000000000", + "4021800000000000", + "4021800000000000", + "4021800000000000", + "c007000000000000", + "c008000000000000", + "c008000000000000", + "4010000000000000", + "4010800000000000", + "4010000000000000", + "4022000000000000", + "4022000000000000", + "4022400000000000", + "c006000000000000", + "c006000000000000", + "c006000000000000", + "4011800000000000", + "4011000000000000", + "4011000000000000", + "4022800000000000", + "4022c00000000000", + "4022800000000000", + "c004000000000000", + "c004000000000000", + "c003000000000000", + "4012000000000000", + "4012000000000000", + "4012000000000000", + "4023400000000000", + "4023000000000000", + "4023000000000000" + ], + "input_key": "input__median_of_differences__multimodal__09", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__multimodal__09", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median_of_differences__multimodal__09", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__multimodal__09", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__nonlinear__04", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3ff1eb851eb851ec", + "3ff2b851eb851eb8", + "40030a3d70a3d70a", + "400ecccccccccccd", + "4012b851eb851eb8", + "401aae147ae147ae", + "4021d70a3d70a3d7", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "3fd6666666666666", + "3ff4cccccccccccc", + "4004147ae147ae15", + "4008a3d70a3d70a4", + "40133d70a3d70a3d", + "401b333333333333", + "40204ccccccccccd", + "bfb47ae147ae1480", + "bfe199999999999a", + "3fc1eb851eb851ea", + "3ff170a3d70a3d70", + "3ff6666666666666", + "4006f5c28f5c28f5", + "4012666666666666", + "4016c28f5c28f5c3", + "401fc28f5c28f5c3", + "bff87ae147ae147b", + "bff1999999999999", + "bfda3d70a3d70a3e", + "bfd70a3d70a3d70c", + "3feb333333333334", + "40028f5c28f5c28f", + "4009333333333332", + "40148f5c28f5c28f", + "401d8f5c28f5c28f", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00199999999999a", + "bff4000000000001", + "bfa47ae147ae1490", + "3fe0f5c28f5c28f4", + "4002147ae147ae13", + "4011000000000000", + "4016666666666666", + "c00d333333333333", + "c0107ae147ae147b", + "c00b70a3d70a3d71", + "c003d70a3d70a3d7", + "c0015c28f5c28f5c", + "bfe6666666666666", + "3ff07ae147ae147a", + "4000f5c28f5c28f6", + "40117ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c014000000000000", + "c013cccccccccccd", + "c00deb851eb851ec", + "c00228f5c28f5c29", + "bff70a3d70a3d70c", + "3fe199999999999a", + "4006666666666667" + ], + "input_key": "input__median_of_differences__nonlinear__04", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__nonlinear__04", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median_of_differences__nonlinear__04", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__nonlinear__04", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__nonlinear__13", + "columns": 2, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "bfb47ae147ae1480", + "bfe199999999999a", + "bff87ae147ae147b", + "bff1999999999999", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00d333333333333", + "c0107ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c0200f5c28f5c290", + "c01e666666666668", + "c0248f5c28f5c290", + "c025800000000000", + "c02b8a3d70a3d70a", + "c02aae147ae147ad", + "c030b33333333333", + "c030451eb851eb85" + ], + "input_key": "input__median_of_differences__nonlinear__13", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__nonlinear__13", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__median_of_differences__nonlinear__13", + "masking_mode": 2, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__nonlinear__13", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 11, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.5ae147ae147aep+1", + "yreal_hex": "0x1.5828f5c28f5c2p+4" + }, + { + "case_identifier": "median_of_differences__plane__05", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3ffb333333333333", + "400b333333333333", + "4014666666666666", + "401b333333333333", + "4021000000000000", + "4024666666666666", + "4027cccccccccccd", + "402b333333333333", + "c002666666666666", + "bfdffffffffffffe", + "3ff4cccccccccccd", + "4008cccccccccccc", + "401399999999999a", + "401acccccccccccd", + "4021000000000000", + "402499999999999a", + "4028333333333334", + "c012666666666666", + "c005999999999998", + "bfe9999999999997", + "3ff199999999999a", + "4008000000000000", + "401399999999999a", + "401b333333333333", + "4021666666666667", + "4025333333333333", + "c01b999999999999", + "c013999999999999", + "c007333333333332", + "bfecccccccccccca", + "3ff199999999999c", + "4008ccccccccccce", + "4014666666666666", + "401c666666666668", + "4022333333333334", + "c022666666666666", + "c01c666666666665", + "c013ffffffffffff", + "c007333333333332", + "bfe9999999999994", + "3ff4ccccccccccd0", + "400b333333333334", + "4016000000000002", + "401e666666666667", + "c027000000000000", + "c02299999999999a", + "c01c666666666666", + "c01399999999999a", + "c00599999999999a", + "bfe0000000000000", + "3ffb333333333330", + "400f333333333334", + "4018666666666666", + "c02b999999999999", + "c027000000000000", + "c022666666666666", + "c01b999999999998", + "c012666666666665", + "c002666666666663", + "3cd0000000000000", + "400266666666666c", + "4012666666666668" + ], + "input_key": "input__median_of_differences__plane__05", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__plane__05", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__median_of_differences__plane__05", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__plane__05", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__plateaus_signed_zero__10", + "columns": 8, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "c008000000000000", + "c000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "c000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4000000000000000" + ], + "input_key": "input__median_of_differences__plateaus_signed_zero__10", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__plateaus_signed_zero__10", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median_of_differences__plateaus_signed_zero__10", + "masking_mode": 2, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__plateaus_signed_zero__10", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 6, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.c5c28f5c28f5cp+2", + "yreal_hex": "0x1.7eb851eb851ebp+3" + }, + { + "case_identifier": "median_of_differences__row_offsets__01", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd0000000000000", + "3fe0000000000000", + "3fe8000000000000", + "3ff0000000000000", + "3ff4000000000000", + "3ff8000000000000", + "3ffc000000000000", + "4000000000000000", + "4008000000000000", + "400a000000000000", + "400c000000000000", + "400e000000000000", + "4010000000000000", + "4011000000000000", + "4012000000000000", + "4013000000000000", + "4014000000000000", + "4018000000000000", + "4019000000000000", + "401a000000000000", + "401b000000000000", + "401c000000000000", + "401d000000000000", + "401e000000000000", + "401f000000000000", + "4020000000000000", + "4022000000000000", + "4022800000000000", + "4023000000000000", + "4023800000000000", + "4024000000000000", + "4024800000000000", + "4025000000000000", + "4025800000000000", + "4026000000000000", + "4028000000000000", + "4028800000000000", + "4029000000000000", + "4029800000000000", + "402a000000000000", + "402a800000000000", + "402b000000000000", + "402b800000000000", + "402c000000000000", + "402e000000000000", + "402e800000000000", + "402f000000000000", + "402f800000000000", + "4030000000000000", + "4030400000000000", + "4030800000000000", + "4030c00000000000", + "4031000000000000", + "4032000000000000", + "4032400000000000", + "4032800000000000", + "4032c00000000000", + "4033000000000000", + "4033400000000000", + "4033800000000000", + "4033c00000000000", + "4034000000000000" + ], + "input_key": "input__median_of_differences__row_offsets__01", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__row_offsets__01", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__median_of_differences__row_offsets__01", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__row_offsets__01", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__scars__08", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "c01b666666666666", + "3fd3333333333333", + "3fdccccccccccccc", + "3fe3333333333333", + "3fe8000000000000", + "3feccccccccccccc", + "c017cccccccccccd", + "3ff3333333333333", + "3ff0000000000000", + "40224ccccccccccd", + "3ff4cccccccccccd", + "3ff7333333333333", + "3ff999999999999a", + "3ffc000000000000", + "3ffe666666666666", + "402419999999999a", + "400199999999999a", + "4000000000000000", + "40244ccccccccccd", + "4002666666666666", + "400399999999999a", + "4004cccccccccccd", + "4006000000000000", + "4007333333333333", + "402619999999999a", + "400999999999999a", + "4008000000000000", + "c00ecccccccccccd", + "400a666666666666", + "400b99999999999a", + "400ccccccccccccd", + "400e000000000000", + "400f333333333333", + "c00799999999999a", + "4010cccccccccccd", + "4010000000000000", + "40284ccccccccccd", + "4011333333333333", + "4011cccccccccccd", + "4012666666666666", + "4013000000000000", + "401399999999999a", + "402a19999999999a", + "4014cccccccccccd", + "4014000000000000", + "402a4ccccccccccd", + "4015333333333333", + "4015cccccccccccd", + "4016666666666666", + "4017000000000000", + "401799999999999a", + "402c19999999999a", + "4018cccccccccccd", + "4018000000000000", + "bfeb333333333330", + "4019333333333333", + "4019cccccccccccd", + "401a666666666666", + "401b000000000000", + "401b99999999999a", + "3fa9999999999980", + "401ccccccccccccd" + ], + "input_key": "input__median_of_differences__scars__08", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__scars__08", + "installed_mutated": false, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__scars__08", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__step__06", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666" + ], + "input_key": "input__median_of_differences__step__06", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__step__06", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__median_of_differences__step__06", + "masking_mode": 2, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__step__06", + "portable_correction_sequence_bits": [ + "3fe24924924924b0", + "bff2492492492486", + "c006db6db6db6db2", + "400b6db6db6db6e0", + "3ffb6db6db6db6e0", + "0000000000000000", + "bffb6db6db6db6d8" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "median_of_differences__tall__15", + "columns": 5, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd999999999999a", + "3fe999999999999a", + "3ff3333333333334", + "3ff999999999999a", + "bfe999999999999a", + "bfd5c28f5c28f5c3", + "3fbeb851eb851eb8", + "3fe28f5c28f5c290", + "3ff0a3d70a3d70a4", + "bff999999999999a", + "bff147ae147ae148", + "bfe1eb851eb851ec", + "bfa47ae147ae1470", + "3fdeb851eb851eb8", + "c003333333333334", + "bffd1eb851eb8521", + "bff3d70a3d70a3d8", + "bfe51eb851eb8520", + "bfb47ae147ae1490", + "c00999999999999a", + "c0047ae147ae147c", + "bffeb851eb851eba", + "bff47ae147ae147b", + "bfe47ae147ae147c", + "c010000000000000", + "c00a666666666667", + "c004cccccccccccd", + "bffe666666666666", + "bff3333333333333", + "c013333333333334", + "c01028f5c28f5c29", + "c00a3d70a3d70a40", + "c00428f5c28f5c2a", + "bffc28f5c28f5c2c", + "c016666666666667", + "c0131eb851eb851f", + "c00fae147ae147b0", + "c0091eb851eb8520", + "c0028f5c28f5c290", + "c01999999999999a", + "c016147ae147ae14", + "c0128f5c28f5c290", + "c00e147ae147ae15", + "c0070a3d70a3d70c", + "c01ccccccccccccd", + "c0190a3d70a3d70a", + "c01547ae147ae148", + "c011851eb851eb85", + "c00b851eb851eb84", + "c020000000000000", + "c01c000000000000", + "c018000000000000", + "c014000000000000", + "c010000000000000", + "c02199999999999a", + "c01ef5c28f5c28f6", + "c01ab851eb851eb8", + "c0167ae147ae147c", + "c0123d70a3d70a3f", + "c023333333333334", + "c020f5c28f5c28f6", + "c01d70a3d70a3d72", + "c018f5c28f5c28f8", + "c0147ae147ae147d", + "c024cccccccccccd", + "c02270a3d70a3d71", + "c020147ae147ae14", + "c01b70a3d70a3d70", + "c016b851eb851eb9", + "c026666666666667", + "c023eb851eb851ec", + "c02170a3d70a3d71", + "c01deb851eb851ec", + "c018f5c28f5c28f8", + "c028000000000000", + "c025666666666666", + "c022cccccccccccc", + "c020333333333334", + "c01b333333333334", + "c02999999999999a", + "c026e147ae147ae2", + "c02428f5c28f5c29", + "c02170a3d70a3d72", + "c01d70a3d70a3d72" + ], + "input_key": "input__median_of_differences__tall__15", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__tall__15", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000" + ], + "mask_key": "mask__median_of_differences__tall__15", + "masking_mode": 1, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__tall__15", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 17, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.399999999999ap+2", + "yreal_hex": "0x1.07c28f5c28f5cp+5" + }, + { + "case_identifier": "median_of_differences__wide__14", + "columns": 17, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fb1eb851eb851ec", + "3fd1eb851eb851ec", + "3fe428f5c28f5c2a", + "3ff1eb851eb851ec", + "3ffc000000000001", + "400428f5c28f5c2a", + "400b70a3d70a3d71", + "4011eb851eb851ec", + "4016ae147ae147af", + "401c000000000001", + "4020f0a3d70a3d71", + "402428f5c28f5c2a", + "4027a8f5c28f5c2a", + "402b70a3d70a3d71", + "402f800000000000", + "4031eb851eb851ec", + "3ff199999999999a", + "3ff23d70a3d70a3e", + "3ff51eb851eb851f", + "3ffa3d70a3d70a3e", + "4000cccccccccccd", + "400599999999999b", + "400b851eb851eb86", + "401147ae147ae148", + "40155c28f5c28f5c", + "401a000000000002", + "401f333333333335", + "40227ae147ae147b", + "4025a3d70a3d70a5", + "4029147ae147ae15", + "402ccccccccccccd", + "4030666666666667", + "40328a3d70a3d70b", + "400199999999999a", + "4001ae147ae147ae", + "4002e147ae147ae2", + "4005333333333333", + "4008a3d70a3d70a4", + "400d333333333334", + "401170a3d70a3d71", + "4014d70a3d70a3d8", + "4018ccccccccccce", + "401d51eb851eb853", + "4021333333333334", + "4024051eb851eb86", + "40271eb851eb851f", + "402a800000000001", + "402e28f5c28f5c2a", + "40310ccccccccccd", + "403328f5c28f5c29", + "400a666666666667", + "400a3d70a3d70a3e", + "400b333333333333", + "400d47ae147ae149", + "40103d70a3d70a3d", + "4012666666666667", + "40151eb851eb851f", + "4018666666666667", + "401c3d70a3d70a3f", + "402051eb851eb852", + "4022cccccccccccd", + "40258f5c28f5c290", + "402899999999999b", + "402beb851eb851ed", + "402f851eb851eb85", + "4031b33333333333", + "4033c7ae147ae148", + "401199999999999a", + "4011666666666667", + "4011c28f5c28f5c3", + "4012ae147ae147ae", + "401428f5c28f5c2a", + "4016333333333334", + "4018ccccccccccce", + "401bf5c28f5c28f6", + "401fae147ae147af", + "4021fae147ae147b", + "4024666666666668", + "402719999999999a", + "402a147ae147ae16", + "402d570a3d70a3d9", + "403070a3d70a3d71", + "4032599999999999", + "4034666666666666" + ], + "input_key": "input__median_of_differences__wide__14", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__median_of_differences__wide__14", + "installed_mutated": true, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 2, + "method_name": "Median of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__median_of_differences__wide__14", + "portable_correction_sequence_bits": [ + "3cd0000000000000", + "0000000000000000", + "3cd0000000000000", + "3cd0000000000000", + "0000000000000000" + ], + "portable_mutated": true, + "rows": 5, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.b51eb851eb852p+3", + "yreal_hex": "0x1.4199999999999p+3" + }, + { + "case_identifier": "trimmed_mean__alternating_offsets__02", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c000000000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4010800000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "bffc000000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4011800000000000", + "4013800000000000", + "4015800000000000", + "4017800000000000", + "4019800000000000", + "401b800000000000", + "401d800000000000", + "401f800000000000", + "4020c00000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4004000000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "4021400000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4006000000000000" + ], + "input_key": "input__trimmed_mean__alternating_offsets__02", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__alternating_offsets__02", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean__alternating_offsets__02", + "masking_mode": 2, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__alternating_offsets__02", + "portable_correction_sequence_bits": [ + "c007924924924925", + "40096db6db6db6db", + "c005924924924925", + "400b6db6db6db6db", + "c003924924924925", + "400d6db6db6db6db", + "c001924924924925" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__constant__00", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000" + ], + "input_key": "input__trimmed_mean__constant__00", + "installed_background_key": "installed_background__trimmed_mean__constant__00", + "installed_corrected_key": "installed_corrected__trimmed_mean__constant__00", + "installed_mutated": false, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": "portable_background__trimmed_mean__constant__00", + "portable_corrected_key": "portable_corrected__trimmed_mean__constant__00", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__impulses__07", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "0000000000000000", + "3fd3333333333333", + "3fe3333333333333", + "3feccccccccccccc", + "3ff3333333333333", + "3ff8000000000000", + "3ffccccccccccccc", + "4000cccccccccccd", + "4003333333333333", + "bfc999999999999a", + "3fb9999999999998", + "4033666666666666", + "3fe6666666666666", + "3ff0000000000000", + "3ff4cccccccccccd", + "3ff9999999999999", + "3ffe666666666667", + "4001999999999999", + "bfd999999999999a", + "bfb999999999999c", + "3fc9999999999998", + "3fdffffffffffffe", + "3fe9999999999999", + "3ff199999999999a", + "3ff6666666666666", + "3ffb333333333334", + "4000000000000000", + "bfe3333333333334", + "bfd3333333333335", + "bca0000000000000", + "3fd3333333333330", + "3fe3333333333332", + "3feccccccccccccc", + "3ff3333333333332", + "3ff8000000000000", + "3ffccccccccccccc", + "bfe999999999999a", + "bfe0000000000000", + "bfc999999999999c", + "3fb9999999999990", + "3fd9999999999998", + "3fe6666666666666", + "3feffffffffffffe", + "3ff4cccccccccccd", + "3ff9999999999999", + "bff0000000000000", + "bfe6666666666666", + "bfd999999999999a", + "bfb99999999999a0", + "3fc9999999999998", + "3fe0000000000000", + "c030333333333333", + "3ff199999999999a", + "3ff6666666666666", + "bff3333333333334", + "bfecccccccccccce", + "bfe3333333333335", + "bfd3333333333338", + "bcb0000000000000", + "3fd3333333333330", + "3fe3333333333330", + "3feccccccccccccc", + "3ff3333333333332" + ], + "input_key": "input__trimmed_mean__impulses__07", + "installed_background_key": "installed_background__trimmed_mean__impulses__07", + "installed_corrected_key": "installed_corrected__trimmed_mean__impulses__07", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean__impulses__07", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": "portable_background__trimmed_mean__impulses__07", + "portable_corrected_key": "portable_corrected__trimmed_mean__impulses__07", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__irregular__11", + "columns": 10, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c02c000000000000", + "4013c00000000000", + "c014800000000000", + "402ba00000000000", + "400e000000000000", + "c019400000000000", + "4029400000000000", + "4004800000000000", + "c01e000000000000", + "4026e00000000000", + "c017e00000000000", + "c022100000000000", + "c028300000000000", + "402bb00000000000", + "4025900000000000", + "401ee00000000000", + "4012a00000000000", + "3ff9800000000000", + "bff7800000000000", + "c012200000000000", + "4000800000000000", + "4018000000000000", + "4023e00000000000", + "402bc00000000000", + "c026600000000000", + "c01d000000000000", + "c00a800000000000", + "3fe4000000000000", + "4012400000000000", + "4021000000000000", + "4024300000000000", + "c01fe00000000000", + "4007c00000000000", + "402bd00000000000", + "c010a00000000000", + "401b200000000000", + "c026900000000000", + "bfd6000000000000", + "4025300000000000", + "c01de00000000000", + "c025c00000000000", + "401c400000000000", + "c010000000000000", + "402be00000000000", + "4007000000000000", + "c020600000000000", + "4023800000000000", + "bff5000000000000", + "c028c00000000000", + "4016400000000000", + "c006c00000000000", + "c01ba00000000000", + "c025f00000000000", + "402bf00000000000", + "4023d00000000000", + "4017600000000000", + "3ffc800000000000", + "c002400000000000", + "c019600000000000", + "c024d00000000000", + "4014c00000000000", + "4020400000000000", + "4026200000000000", + "402c000000000000", + "c028200000000000", + "c022400000000000", + "c018c00000000000", + "c00a000000000000", + "bfd4000000000000", + "4005000000000000", + "402a700000000000", + "c017600000000000", + "4010600000000000", + "402c100000000000", + "c014200000000000", + "4013a00000000000", + "c02c500000000000", + "c010e00000000000", + "4016e00000000000", + "c02ab00000000000" + ], + "input_key": "input__trimmed_mean__irregular__11", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__irregular__11", + "installed_mutated": true, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean__irregular__11", + "masking_mode": 2, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__irregular__11", + "portable_correction_sequence_bits": [ + "3ff3a00000000000", + "bfedc00000000000", + "4000500000000000", + "3fdc800000000000", + "3fd6800000000000", + "c013380000000000", + "3fba000000000000", + "3ff9200000000000" + ], + "portable_mutated": true, + "rows": 8, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.119999999999ap+3", + "yreal_hex": "0x1.f8f5c28f5c28fp+3" + }, + { + "case_identifier": "trimmed_mean__linear__03", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "c004cccccccccccd", + "bff6666666666666", + "bfc9999999999998", + "3ff0000000000002", + "400199999999999a", + "400b333333333333", + "4012666666666668", + "4017333333333334", + "401c000000000000", + "c00f333333333334", + "c004000000000001", + "bff199999999999a", + "3fd3333333333338", + "3ffb333333333334", + "4008cccccccccccd", + "4012000000000001", + "401799999999999b", + "401d333333333334", + "c014cccccccccccd", + "c00cccccccccccce", + "c000000000000000", + "bfd9999999999990", + "3ff3333333333334", + "4006666666666666", + "401199999999999b", + "4018000000000001", + "401e666666666667", + "c01a000000000000", + "c012cccccccccccd", + "c007333333333334", + "bff1999999999998", + "3fe6666666666668", + "4004000000000000", + "4011333333333334", + "4018666666666667", + "401f99999999999a", + "c01f333333333334", + "c017333333333334", + "c00e666666666668", + "bffccccccccccccc", + "3fc99999999999a0", + "400199999999999a", + "4010ccccccccccce", + "4018cccccccccccf", + "4020666666666667" + ], + "input_key": "input__trimmed_mean__linear__03", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__linear__03", + "installed_mutated": true, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean__linear__03", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__linear__03", + "portable_correction_sequence_bits": [ + "3ff8000000000001", + "3ff0000000000001", + "3fe0000000000002", + "0000000000000000", + "bfe0000000000002", + "bff0000000000001", + "bff8000000000002" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__linear__12", + "columns": 11, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "401ccccccccccccd", + "4020000000000000", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "401ecccccccccccd", + "4021666666666666" + ], + "input_key": "input__trimmed_mean__linear__12", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__linear__12", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean__linear__12", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__linear__12", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 2, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.28f5c28f5c28fp+3", + "yreal_hex": "0x1.147ae147ae148p+2" + }, + { + "case_identifier": "trimmed_mean__multimodal__09", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "c00f000000000000", + "c010000000000000", + "c010000000000000", + "4008000000000000", + "4009000000000000", + "4008000000000000", + "4020000000000000", + "4020000000000000", + "4020400000000000", + "c00e000000000000", + "c00e000000000000", + "c00e000000000000", + "400b000000000000", + "400a000000000000", + "400a000000000000", + "4020800000000000", + "4020c00000000000", + "4020800000000000", + "c00c000000000000", + "c00c000000000000", + "c00b000000000000", + "400c000000000000", + "400c000000000000", + "400c000000000000", + "4021400000000000", + "4021000000000000", + "4021000000000000", + "c00a000000000000", + "c009000000000000", + "c00a000000000000", + "400e000000000000", + "400e000000000000", + "400f000000000000", + "4021800000000000", + "4021800000000000", + "4021800000000000", + "c007000000000000", + "c008000000000000", + "c008000000000000", + "4010000000000000", + "4010800000000000", + "4010000000000000", + "4022000000000000", + "4022000000000000", + "4022400000000000", + "c006000000000000", + "c006000000000000", + "c006000000000000", + "4011800000000000", + "4011000000000000", + "4011000000000000", + "4022800000000000", + "4022c00000000000", + "4022800000000000", + "c004000000000000", + "c004000000000000", + "c003000000000000", + "4012000000000000", + "4012000000000000", + "4012000000000000", + "4023400000000000", + "4023000000000000", + "4023000000000000" + ], + "input_key": "input__trimmed_mean__multimodal__09", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__multimodal__09", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean__multimodal__09", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__multimodal__09", + "portable_correction_sequence_bits": [ + "c019097b425ed098", + "c01956480f2b9d64", + "c019d6480f2b9d64", + "3fe5d6480f2b9d64", + "3fe7b425ed097b44", + "3fe54dbf86a314dc", + "401629b7f0d4629c", + "4016bac901e573ac", + "4016f684bda12f68" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__nonlinear__04", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3ff1eb851eb851ec", + "3ff2b851eb851eb8", + "40030a3d70a3d70a", + "400ecccccccccccd", + "4012b851eb851eb8", + "401aae147ae147ae", + "4021d70a3d70a3d7", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "3fd6666666666666", + "3ff4cccccccccccc", + "4004147ae147ae15", + "4008a3d70a3d70a4", + "40133d70a3d70a3d", + "401b333333333333", + "40204ccccccccccd", + "bfb47ae147ae1480", + "bfe199999999999a", + "3fc1eb851eb851ea", + "3ff170a3d70a3d70", + "3ff6666666666666", + "4006f5c28f5c28f5", + "4012666666666666", + "4016c28f5c28f5c3", + "401fc28f5c28f5c3", + "bff87ae147ae147b", + "bff1999999999999", + "bfda3d70a3d70a3e", + "bfd70a3d70a3d70c", + "3feb333333333334", + "40028f5c28f5c28f", + "4009333333333332", + "40148f5c28f5c28f", + "401d8f5c28f5c28f", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00199999999999a", + "bff4000000000001", + "bfa47ae147ae1490", + "3fe0f5c28f5c28f4", + "4002147ae147ae13", + "4011000000000000", + "4016666666666666", + "c00d333333333333", + "c0107ae147ae147b", + "c00b70a3d70a3d71", + "c003d70a3d70a3d7", + "c0015c28f5c28f5c", + "bfe6666666666666", + "3ff07ae147ae147a", + "4000f5c28f5c28f6", + "40117ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c014000000000000", + "c013cccccccccccd", + "c00deb851eb851ec", + "c00228f5c28f5c29", + "bff70a3d70a3d70c", + "3fe199999999999a", + "4006666666666667" + ], + "input_key": "input__trimmed_mean__nonlinear__04", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__nonlinear__04", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean__nonlinear__04", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__nonlinear__04", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__nonlinear__13", + "columns": 2, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "bfb47ae147ae1480", + "bfe199999999999a", + "bff87ae147ae147b", + "bff1999999999999", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00d333333333333", + "c0107ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c0200f5c28f5c290", + "c01e666666666668", + "c0248f5c28f5c290", + "c025800000000000", + "c02b8a3d70a3d70a", + "c02aae147ae147ad", + "c030b33333333333", + "c030451eb851eb85" + ], + "input_key": "input__trimmed_mean__nonlinear__13", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__nonlinear__13", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean__nonlinear__13", + "masking_mode": 2, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__nonlinear__13", + "portable_correction_sequence_bits": [ + "bfb0a3d70a3d70c0", + "3fb0a3d70a3d7080" + ], + "portable_mutated": true, + "rows": 11, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.5ae147ae147aep+1", + "yreal_hex": "0x1.5828f5c28f5c2p+4" + }, + { + "case_identifier": "trimmed_mean__plane__05", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3ffb333333333333", + "400b333333333333", + "4014666666666666", + "401b333333333333", + "4021000000000000", + "4024666666666666", + "4027cccccccccccd", + "402b333333333333", + "c002666666666666", + "bfdffffffffffffe", + "3ff4cccccccccccd", + "4008cccccccccccc", + "401399999999999a", + "401acccccccccccd", + "4021000000000000", + "402499999999999a", + "4028333333333334", + "c012666666666666", + "c005999999999998", + "bfe9999999999997", + "3ff199999999999a", + "4008000000000000", + "401399999999999a", + "401b333333333333", + "4021666666666667", + "4025333333333333", + "c01b999999999999", + "c013999999999999", + "c007333333333332", + "bfecccccccccccca", + "3ff199999999999c", + "4008ccccccccccce", + "4014666666666666", + "401c666666666668", + "4022333333333334", + "c022666666666666", + "c01c666666666665", + "c013ffffffffffff", + "c007333333333332", + "bfe9999999999994", + "3ff4ccccccccccd0", + "400b333333333334", + "4016000000000002", + "401e666666666667", + "c027000000000000", + "c02299999999999a", + "c01c666666666666", + "c01399999999999a", + "c00599999999999a", + "bfe0000000000000", + "3ffb333333333330", + "400f333333333334", + "4018666666666666", + "c02b999999999999", + "c027000000000000", + "c022666666666666", + "c01b999999999998", + "c012666666666665", + "c002666666666663", + "3cd0000000000000", + "400266666666666c", + "4012666666666668" + ], + "input_key": "input__trimmed_mean__plane__05", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__plane__05", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__trimmed_mean__plane__05", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__plane__05", + "portable_correction_sequence_bits": [ + "c019777777777778", + "c01aaaaaaaaaaaaa", + "c012aaaaaaaaaaaa", + "bfe5555555555558", + "bfe5555555555554", + "3ff5555555555556", + "4014222222222222", + "4015555555555556", + "401d555555555556" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__plateaus_signed_zero__10", + "columns": 8, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "c008000000000000", + "c000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "c000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4000000000000000" + ], + "input_key": "input__trimmed_mean__plateaus_signed_zero__10", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__plateaus_signed_zero__10", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean__plateaus_signed_zero__10", + "masking_mode": 2, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__plateaus_signed_zero__10", + "portable_correction_sequence_bits": [ + "bfe4000000000000", + "bfd8000000000000", + "bfc0000000000000", + "3fc0000000000000", + "3fd8000000000000", + "3fe4000000000000" + ], + "portable_mutated": true, + "rows": 6, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.c5c28f5c28f5cp+2", + "yreal_hex": "0x1.7eb851eb851ebp+3" + }, + { + "case_identifier": "trimmed_mean__row_offsets__01", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd0000000000000", + "3fe0000000000000", + "3fe8000000000000", + "3ff0000000000000", + "3ff4000000000000", + "3ff8000000000000", + "3ffc000000000000", + "4000000000000000", + "4008000000000000", + "400a000000000000", + "400c000000000000", + "400e000000000000", + "4010000000000000", + "4011000000000000", + "4012000000000000", + "4013000000000000", + "4014000000000000", + "4018000000000000", + "4019000000000000", + "401a000000000000", + "401b000000000000", + "401c000000000000", + "401d000000000000", + "401e000000000000", + "401f000000000000", + "4020000000000000", + "4022000000000000", + "4022800000000000", + "4023000000000000", + "4023800000000000", + "4024000000000000", + "4024800000000000", + "4025000000000000", + "4025800000000000", + "4026000000000000", + "4028000000000000", + "4028800000000000", + "4029000000000000", + "4029800000000000", + "402a000000000000", + "402a800000000000", + "402b000000000000", + "402b800000000000", + "402c000000000000", + "402e000000000000", + "402e800000000000", + "402f000000000000", + "402f800000000000", + "4030000000000000", + "4030400000000000", + "4030800000000000", + "4030c00000000000", + "4031000000000000", + "4032000000000000", + "4032400000000000", + "4032800000000000", + "4032c00000000000", + "4033000000000000", + "4033400000000000", + "4033800000000000", + "4033c00000000000", + "4034000000000000" + ], + "input_key": "input__trimmed_mean__row_offsets__01", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__row_offsets__01", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean__row_offsets__01", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__row_offsets__01", + "portable_correction_sequence_bits": [ + "c02205397829cbc1", + "c017c14e5e0a72f0", + "c00814e5e0a72f04", + "bfb4e5e0a72f0500", + "4007eb1a1f58d0fc", + "40183eb1a1f58d12", + "4021fac687d6343f" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__scars__08", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "c01b666666666666", + "3fd3333333333333", + "3fdccccccccccccc", + "3fe3333333333333", + "3fe8000000000000", + "3feccccccccccccc", + "c017cccccccccccd", + "3ff3333333333333", + "3ff0000000000000", + "40224ccccccccccd", + "3ff4cccccccccccd", + "3ff7333333333333", + "3ff999999999999a", + "3ffc000000000000", + "3ffe666666666666", + "402419999999999a", + "400199999999999a", + "4000000000000000", + "40244ccccccccccd", + "4002666666666666", + "400399999999999a", + "4004cccccccccccd", + "4006000000000000", + "4007333333333333", + "402619999999999a", + "400999999999999a", + "4008000000000000", + "c00ecccccccccccd", + "400a666666666666", + "400b99999999999a", + "400ccccccccccccd", + "400e000000000000", + "400f333333333333", + "c00799999999999a", + "4010cccccccccccd", + "4010000000000000", + "40284ccccccccccd", + "4011333333333333", + "4011cccccccccccd", + "4012666666666666", + "4013000000000000", + "401399999999999a", + "402a19999999999a", + "4014cccccccccccd", + "4014000000000000", + "402a4ccccccccccd", + "4015333333333333", + "4015cccccccccccd", + "4016666666666666", + "4017000000000000", + "401799999999999a", + "402c19999999999a", + "4018cccccccccccd", + "4018000000000000", + "bfeb333333333330", + "4019333333333333", + "4019cccccccccccd", + "401a666666666666", + "401b000000000000", + "401b99999999999a", + "3fa9999999999980", + "401ccccccccccccd" + ], + "input_key": "input__trimmed_mean__scars__08", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__scars__08", + "installed_mutated": true, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__scars__08", + "portable_correction_sequence_bits": [ + "c0095f15f15f15f0", + "bffdf15f15f15f14", + "bfebe2be2be2be28", + "bfc5f15f15f15f00", + "3ff20ea0ea0ea0ec", + "4001075075075076", + "4006a0ea0ea0ea10" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__step__06", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666" + ], + "input_key": "input__trimmed_mean__step__06", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__step__06", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__trimmed_mean__step__06", + "masking_mode": 2, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__step__06", + "portable_correction_sequence_bits": [ + "c012492492492492", + "c012492492492492", + "c012492492492492", + "400b6db6db6db6dc", + "400b6db6db6db6dc", + "400b6db6db6db6dc", + "400b6db6db6db6dc" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean__tall__15", + "columns": 5, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd999999999999a", + "3fe999999999999a", + "3ff3333333333334", + "3ff999999999999a", + "bfe999999999999a", + "bfd5c28f5c28f5c3", + "3fbeb851eb851eb8", + "3fe28f5c28f5c290", + "3ff0a3d70a3d70a4", + "bff999999999999a", + "bff147ae147ae148", + "bfe1eb851eb851ec", + "bfa47ae147ae1470", + "3fdeb851eb851eb8", + "c003333333333334", + "bffd1eb851eb8521", + "bff3d70a3d70a3d8", + "bfe51eb851eb8520", + "bfb47ae147ae1490", + "c00999999999999a", + "c0047ae147ae147c", + "bffeb851eb851eba", + "bff47ae147ae147b", + "bfe47ae147ae147c", + "c010000000000000", + "c00a666666666667", + "c004cccccccccccd", + "bffe666666666666", + "bff3333333333333", + "c013333333333334", + "c01028f5c28f5c29", + "c00a3d70a3d70a40", + "c00428f5c28f5c2a", + "bffc28f5c28f5c2c", + "c016666666666667", + "c0131eb851eb851f", + "c00fae147ae147b0", + "c0091eb851eb8520", + "c0028f5c28f5c290", + "c01999999999999a", + "c016147ae147ae14", + "c0128f5c28f5c290", + "c00e147ae147ae15", + "c0070a3d70a3d70c", + "c01ccccccccccccd", + "c0190a3d70a3d70a", + "c01547ae147ae148", + "c011851eb851eb85", + "c00b851eb851eb84", + "c020000000000000", + "c01c000000000000", + "c018000000000000", + "c014000000000000", + "c010000000000000", + "c02199999999999a", + "c01ef5c28f5c28f6", + "c01ab851eb851eb8", + "c0167ae147ae147c", + "c0123d70a3d70a3f", + "c023333333333334", + "c020f5c28f5c28f6", + "c01d70a3d70a3d72", + "c018f5c28f5c28f8", + "c0147ae147ae147d", + "c024cccccccccccd", + "c02270a3d70a3d71", + "c020147ae147ae14", + "c01b70a3d70a3d70", + "c016b851eb851eb9", + "c026666666666667", + "c023eb851eb851ec", + "c02170a3d70a3d71", + "c01deb851eb851ec", + "c018f5c28f5c28f8", + "c028000000000000", + "c025666666666666", + "c022cccccccccccc", + "c020333333333334", + "c01b333333333334", + "c02999999999999a", + "c026e147ae147ae2", + "c02428f5c28f5c29", + "c02170a3d70a3d72", + "c01d70a3d70a3d72" + ], + "input_key": "input__trimmed_mean__tall__15", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__tall__15", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean__tall__15", + "masking_mode": 1, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__tall__15", + "portable_correction_sequence_bits": [ + "bffc0ab66df63b8c", + "bff19adc3d1ffaf8", + "3f7e3f5499209c00", + "3ff1401e3f54991c", + "3ffc473517287cc2" + ], + "portable_mutated": true, + "rows": 17, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.399999999999ap+2", + "yreal_hex": "0x1.07c28f5c28f5cp+5" + }, + { + "case_identifier": "trimmed_mean__wide__14", + "columns": 17, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fb1eb851eb851ec", + "3fd1eb851eb851ec", + "3fe428f5c28f5c2a", + "3ff1eb851eb851ec", + "3ffc000000000001", + "400428f5c28f5c2a", + "400b70a3d70a3d71", + "4011eb851eb851ec", + "4016ae147ae147af", + "401c000000000001", + "4020f0a3d70a3d71", + "402428f5c28f5c2a", + "4027a8f5c28f5c2a", + "402b70a3d70a3d71", + "402f800000000000", + "4031eb851eb851ec", + "3ff199999999999a", + "3ff23d70a3d70a3e", + "3ff51eb851eb851f", + "3ffa3d70a3d70a3e", + "4000cccccccccccd", + "400599999999999b", + "400b851eb851eb86", + "401147ae147ae148", + "40155c28f5c28f5c", + "401a000000000002", + "401f333333333335", + "40227ae147ae147b", + "4025a3d70a3d70a5", + "4029147ae147ae15", + "402ccccccccccccd", + "4030666666666667", + "40328a3d70a3d70b", + "400199999999999a", + "4001ae147ae147ae", + "4002e147ae147ae2", + "4005333333333333", + "4008a3d70a3d70a4", + "400d333333333334", + "401170a3d70a3d71", + "4014d70a3d70a3d8", + "4018ccccccccccce", + "401d51eb851eb853", + "4021333333333334", + "4024051eb851eb86", + "40271eb851eb851f", + "402a800000000001", + "402e28f5c28f5c2a", + "40310ccccccccccd", + "403328f5c28f5c29", + "400a666666666667", + "400a3d70a3d70a3e", + "400b333333333333", + "400d47ae147ae149", + "40103d70a3d70a3d", + "4012666666666667", + "40151eb851eb851f", + "4018666666666667", + "401c3d70a3d70a3f", + "402051eb851eb852", + "4022cccccccccccd", + "40258f5c28f5c290", + "402899999999999b", + "402beb851eb851ed", + "402f851eb851eb85", + "4031b33333333333", + "4033c7ae147ae148", + "401199999999999a", + "4011666666666667", + "4011c28f5c28f5c3", + "4012ae147ae147ae", + "401428f5c28f5c2a", + "4016333333333334", + "4018ccccccccccce", + "401bf5c28f5c28f6", + "401fae147ae147af", + "4021fae147ae147b", + "4024666666666668", + "402719999999999a", + "402a147ae147ae16", + "402d570a3d70a3d9", + "403070a3d70a3d71", + "4032599999999999", + "4034666666666666" + ], + "input_key": "input__trimmed_mean__wide__14", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean__wide__14", + "installed_mutated": true, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 5, + "method_name": "Trimmed mean", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean__wide__14", + "portable_correction_sequence_bits": [ + "bffb851eb851eb88", + "bfeb851eb851eb90", + "0000000000000000", + "3feb851eb851eb88", + "3ffb851eb851eb84" + ], + "portable_mutated": true, + "rows": 5, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.b51eb851eb852p+3", + "yreal_hex": "0x1.4199999999999p+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__alternating_offsets__02", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c000000000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4010800000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "bffc000000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4011800000000000", + "4013800000000000", + "4015800000000000", + "4017800000000000", + "4019800000000000", + "401b800000000000", + "401d800000000000", + "401f800000000000", + "4020c00000000000", + "bff8000000000000", + "bff0000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "3ff0000000000000", + "3ff8000000000000", + "4000000000000000", + "4004000000000000", + "4012800000000000", + "4014800000000000", + "4016800000000000", + "4018800000000000", + "401a800000000000", + "401c800000000000", + "401e800000000000", + "4020400000000000", + "4021400000000000", + "bff4000000000000", + "bfe8000000000000", + "bfd0000000000000", + "3fd0000000000000", + "3fe8000000000000", + "3ff4000000000000", + "3ffc000000000000", + "4002000000000000", + "4006000000000000" + ], + "input_key": "input__trimmed_mean_of_differences__alternating_offsets__02", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__alternating_offsets__02", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__alternating_offsets__02", + "masking_mode": 2, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__alternating_offsets__02", + "portable_correction_sequence_bits": [ + "c004924924924928", + "400b6db6db6db6d9", + "c004924924924926", + "400b6db6db6db6db", + "c004924924924924", + "400b6db6db6db6dd", + "c004924924924922" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__constant__00", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000", + "400a000000000000" + ], + "input_key": "input__trimmed_mean_of_differences__constant__00", + "installed_background_key": "installed_background__trimmed_mean_of_differences__constant__00", + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__constant__00", + "installed_mutated": false, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": "portable_background__trimmed_mean_of_differences__constant__00", + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__constant__00", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__impulses__07", + "columns": 9, + "direction": 0, + "extract_background_request": true, + "input_bits": [ + "0000000000000000", + "3fd3333333333333", + "3fe3333333333333", + "3feccccccccccccc", + "3ff3333333333333", + "3ff8000000000000", + "3ffccccccccccccc", + "4000cccccccccccd", + "4003333333333333", + "bfc999999999999a", + "3fb9999999999998", + "4033666666666666", + "3fe6666666666666", + "3ff0000000000000", + "3ff4cccccccccccd", + "3ff9999999999999", + "3ffe666666666667", + "4001999999999999", + "bfd999999999999a", + "bfb999999999999c", + "3fc9999999999998", + "3fdffffffffffffe", + "3fe9999999999999", + "3ff199999999999a", + "3ff6666666666666", + "3ffb333333333334", + "4000000000000000", + "bfe3333333333334", + "bfd3333333333335", + "bca0000000000000", + "3fd3333333333330", + "3fe3333333333332", + "3feccccccccccccc", + "3ff3333333333332", + "3ff8000000000000", + "3ffccccccccccccc", + "bfe999999999999a", + "bfe0000000000000", + "bfc999999999999c", + "3fb9999999999990", + "3fd9999999999998", + "3fe6666666666666", + "3feffffffffffffe", + "3ff4cccccccccccd", + "3ff9999999999999", + "bff0000000000000", + "bfe6666666666666", + "bfd999999999999a", + "bfb99999999999a0", + "3fc9999999999998", + "3fe0000000000000", + "c030333333333333", + "3ff199999999999a", + "3ff6666666666666", + "bff3333333333334", + "bfecccccccccccce", + "bfe3333333333335", + "bfd3333333333338", + "bcb0000000000000", + "3fd3333333333330", + "3fe3333333333330", + "3feccccccccccccc", + "3ff3333333333332" + ], + "input_key": "input__trimmed_mean_of_differences__impulses__07", + "installed_background_key": "installed_background__trimmed_mean_of_differences__impulses__07", + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__impulses__07", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__impulses__07", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": "portable_background__trimmed_mean_of_differences__impulses__07", + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__impulses__07", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "columns": 10, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c02c000000000000", + "4013c00000000000", + "c014800000000000", + "402ba00000000000", + "400e000000000000", + "c019400000000000", + "4029400000000000", + "4004800000000000", + "c01e000000000000", + "4026e00000000000", + "c017e00000000000", + "c022100000000000", + "c028300000000000", + "402bb00000000000", + "4025900000000000", + "401ee00000000000", + "4012a00000000000", + "3ff9800000000000", + "bff7800000000000", + "c012200000000000", + "4000800000000000", + "4018000000000000", + "4023e00000000000", + "402bc00000000000", + "c026600000000000", + "c01d000000000000", + "c00a800000000000", + "3fe4000000000000", + "4012400000000000", + "4021000000000000", + "4024300000000000", + "c01fe00000000000", + "4007c00000000000", + "402bd00000000000", + "c010a00000000000", + "401b200000000000", + "c026900000000000", + "bfd6000000000000", + "4025300000000000", + "c01de00000000000", + "c025c00000000000", + "401c400000000000", + "c010000000000000", + "402be00000000000", + "4007000000000000", + "c020600000000000", + "4023800000000000", + "bff5000000000000", + "c028c00000000000", + "4016400000000000", + "c006c00000000000", + "c01ba00000000000", + "c025f00000000000", + "402bf00000000000", + "4023d00000000000", + "4017600000000000", + "3ffc800000000000", + "c002400000000000", + "c019600000000000", + "c024d00000000000", + "4014c00000000000", + "4020400000000000", + "4026200000000000", + "402c000000000000", + "c028200000000000", + "c022400000000000", + "c018c00000000000", + "c00a000000000000", + "bfd4000000000000", + "4005000000000000", + "402a700000000000", + "c017600000000000", + "4010600000000000", + "402c100000000000", + "c014200000000000", + "4013a00000000000", + "c02c500000000000", + "c010e00000000000", + "4016e00000000000", + "c02ab00000000000" + ], + "input_key": "input__trimmed_mean_of_differences__irregular__11", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__irregular__11", + "installed_mutated": true, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__irregular__11", + "masking_mode": 2, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__irregular__11", + "portable_correction_sequence_bits": [ + "0000000000000000", + "bffb6db6db6db6db", + "4004924924924925", + "3feb6db6db6db6e0", + "bfeb6db6db6db6d8", + "c004924924924924", + "3ffb6db6db6db6e0", + "0000000000000000" + ], + "portable_mutated": true, + "rows": 8, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.119999999999ap+3", + "yreal_hex": "0x1.f8f5c28f5c28fp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__linear__03", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "c004cccccccccccd", + "bff6666666666666", + "bfc9999999999998", + "3ff0000000000002", + "400199999999999a", + "400b333333333333", + "4012666666666668", + "4017333333333334", + "401c000000000000", + "c00f333333333334", + "c004000000000001", + "bff199999999999a", + "3fd3333333333338", + "3ffb333333333334", + "4008cccccccccccd", + "4012000000000001", + "401799999999999b", + "401d333333333334", + "c014cccccccccccd", + "c00cccccccccccce", + "c000000000000000", + "bfd9999999999990", + "3ff3333333333334", + "4006666666666666", + "401199999999999b", + "4018000000000001", + "401e666666666667", + "c01a000000000000", + "c012cccccccccccd", + "c007333333333334", + "bff1999999999998", + "3fe6666666666668", + "4004000000000000", + "4011333333333334", + "4018666666666667", + "401f99999999999a", + "c01f333333333334", + "c017333333333334", + "c00e666666666668", + "bffccccccccccccc", + "3fc99999999999a0", + "400199999999999a", + "4010ccccccccccce", + "4018cccccccccccf", + "4020666666666667" + ], + "input_key": "input__trimmed_mean_of_differences__linear__03", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__linear__03", + "installed_mutated": false, + "mask_bits": [ + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__linear__03", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__linear__03", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__linear__12", + "columns": 11, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fe999999999999a", + "3ff999999999999a", + "4003333333333334", + "400999999999999a", + "4010000000000000", + "4013333333333334", + "4016666666666667", + "401999999999999a", + "401ccccccccccccd", + "4020000000000000", + "bff4cccccccccccd", + "bfd3333333333333", + "3fe6666666666667", + "3ffb333333333335", + "400599999999999a", + "400d99999999999a", + "4012ccccccccccce", + "4016ccccccccccce", + "401accccccccccce", + "401ecccccccccccd", + "4021666666666666" + ], + "input_key": "input__trimmed_mean_of_differences__linear__12", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__linear__12", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__linear__12", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__linear__12", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 2, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.28f5c28f5c28fp+3", + "yreal_hex": "0x1.147ae147ae148p+2" + }, + { + "case_identifier": "trimmed_mean_of_differences__multimodal__09", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "c00f000000000000", + "c010000000000000", + "c010000000000000", + "4008000000000000", + "4009000000000000", + "4008000000000000", + "4020000000000000", + "4020000000000000", + "4020400000000000", + "c00e000000000000", + "c00e000000000000", + "c00e000000000000", + "400b000000000000", + "400a000000000000", + "400a000000000000", + "4020800000000000", + "4020c00000000000", + "4020800000000000", + "c00c000000000000", + "c00c000000000000", + "c00b000000000000", + "400c000000000000", + "400c000000000000", + "400c000000000000", + "4021400000000000", + "4021000000000000", + "4021000000000000", + "c00a000000000000", + "c009000000000000", + "c00a000000000000", + "400e000000000000", + "400e000000000000", + "400f000000000000", + "4021800000000000", + "4021800000000000", + "4021800000000000", + "c007000000000000", + "c008000000000000", + "c008000000000000", + "4010000000000000", + "4010800000000000", + "4010000000000000", + "4022000000000000", + "4022000000000000", + "4022400000000000", + "c006000000000000", + "c006000000000000", + "c006000000000000", + "4011800000000000", + "4011000000000000", + "4011000000000000", + "4022800000000000", + "4022c00000000000", + "4022800000000000", + "c004000000000000", + "c004000000000000", + "c003000000000000", + "4012000000000000", + "4012000000000000", + "4012000000000000", + "4023400000000000", + "4023000000000000", + "4023000000000000" + ], + "input_key": "input__trimmed_mean_of_differences__multimodal__09", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__multimodal__09", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__multimodal__09", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__multimodal__09", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__nonlinear__04", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3ff1eb851eb851ec", + "3ff2b851eb851eb8", + "40030a3d70a3d70a", + "400ecccccccccccd", + "4012b851eb851eb8", + "401aae147ae147ae", + "4021d70a3d70a3d7", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "3fd6666666666666", + "3ff4cccccccccccc", + "4004147ae147ae15", + "4008a3d70a3d70a4", + "40133d70a3d70a3d", + "401b333333333333", + "40204ccccccccccd", + "bfb47ae147ae1480", + "bfe199999999999a", + "3fc1eb851eb851ea", + "3ff170a3d70a3d70", + "3ff6666666666666", + "4006f5c28f5c28f5", + "4012666666666666", + "4016c28f5c28f5c3", + "401fc28f5c28f5c3", + "bff87ae147ae147b", + "bff1999999999999", + "bfda3d70a3d70a3e", + "bfd70a3d70a3d70c", + "3feb333333333334", + "40028f5c28f5c28f", + "4009333333333332", + "40148f5c28f5c28f", + "401d8f5c28f5c28f", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00199999999999a", + "bff4000000000001", + "bfa47ae147ae1490", + "3fe0f5c28f5c28f4", + "4002147ae147ae13", + "4011000000000000", + "4016666666666666", + "c00d333333333333", + "c0107ae147ae147b", + "c00b70a3d70a3d71", + "c003d70a3d70a3d7", + "c0015c28f5c28f5c", + "bfe6666666666666", + "3ff07ae147ae147a", + "4000f5c28f5c28f6", + "40117ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c014000000000000", + "c013cccccccccccd", + "c00deb851eb851ec", + "c00228f5c28f5c29", + "bff70a3d70a3d70c", + "3fe199999999999a", + "4006666666666667" + ], + "input_key": "input__trimmed_mean_of_differences__nonlinear__04", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__nonlinear__04", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__nonlinear__04", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__nonlinear__04", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__nonlinear__13", + "columns": 2, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fdb851eb851eb85", + "3fc0a3d70a3d70a3", + "3fe1eb851eb851eb", + "bfb47ae147ae1480", + "bfe199999999999a", + "bff87ae147ae147b", + "bff1999999999999", + "c0035c28f5c28f5d", + "bfffd70a3d70a3d8", + "c00d333333333333", + "c0107ae147ae147b", + "c0187ae147ae147b", + "c016c28f5c28f5c3", + "c0200f5c28f5c290", + "c01e666666666668", + "c0248f5c28f5c290", + "c025800000000000", + "c02b8a3d70a3d70a", + "c02aae147ae147ad", + "c030b33333333333", + "c030451eb851eb85" + ], + "input_key": "input__trimmed_mean_of_differences__nonlinear__13", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__nonlinear__13", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__nonlinear__13", + "masking_mode": 2, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__nonlinear__13", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 11, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.5ae147ae147aep+1", + "yreal_hex": "0x1.5828f5c28f5c2p+4" + }, + { + "case_identifier": "trimmed_mean_of_differences__plane__05", + "columns": 9, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3ffb333333333333", + "400b333333333333", + "4014666666666666", + "401b333333333333", + "4021000000000000", + "4024666666666666", + "4027cccccccccccd", + "402b333333333333", + "c002666666666666", + "bfdffffffffffffe", + "3ff4cccccccccccd", + "4008cccccccccccc", + "401399999999999a", + "401acccccccccccd", + "4021000000000000", + "402499999999999a", + "4028333333333334", + "c012666666666666", + "c005999999999998", + "bfe9999999999997", + "3ff199999999999a", + "4008000000000000", + "401399999999999a", + "401b333333333333", + "4021666666666667", + "4025333333333333", + "c01b999999999999", + "c013999999999999", + "c007333333333332", + "bfecccccccccccca", + "3ff199999999999c", + "4008ccccccccccce", + "4014666666666666", + "401c666666666668", + "4022333333333334", + "c022666666666666", + "c01c666666666665", + "c013ffffffffffff", + "c007333333333332", + "bfe9999999999994", + "3ff4ccccccccccd0", + "400b333333333334", + "4016000000000002", + "401e666666666667", + "c027000000000000", + "c02299999999999a", + "c01c666666666666", + "c01399999999999a", + "c00599999999999a", + "bfe0000000000000", + "3ffb333333333330", + "400f333333333334", + "4018666666666666", + "c02b999999999999", + "c027000000000000", + "c022666666666666", + "c01b999999999998", + "c012666666666665", + "c002666666666663", + "3cd0000000000000", + "400266666666666c", + "4012666666666668" + ], + "input_key": "input__trimmed_mean_of_differences__plane__05", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__plane__05", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__plane__05", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__plane__05", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__plateaus_signed_zero__10", + "columns": 8, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "c008000000000000", + "c000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "c000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "bff0000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "0000000000000000", + "0000000000000000", + "8000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4000000000000000" + ], + "input_key": "input__trimmed_mean_of_differences__plateaus_signed_zero__10", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__plateaus_signed_zero__10", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__plateaus_signed_zero__10", + "masking_mode": 2, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__plateaus_signed_zero__10", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 6, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.c5c28f5c28f5cp+2", + "yreal_hex": "0x1.7eb851eb851ebp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__row_offsets__01", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd0000000000000", + "3fe0000000000000", + "3fe8000000000000", + "3ff0000000000000", + "3ff4000000000000", + "3ff8000000000000", + "3ffc000000000000", + "4000000000000000", + "4008000000000000", + "400a000000000000", + "400c000000000000", + "400e000000000000", + "4010000000000000", + "4011000000000000", + "4012000000000000", + "4013000000000000", + "4014000000000000", + "4018000000000000", + "4019000000000000", + "401a000000000000", + "401b000000000000", + "401c000000000000", + "401d000000000000", + "401e000000000000", + "401f000000000000", + "4020000000000000", + "4022000000000000", + "4022800000000000", + "4023000000000000", + "4023800000000000", + "4024000000000000", + "4024800000000000", + "4025000000000000", + "4025800000000000", + "4026000000000000", + "4028000000000000", + "4028800000000000", + "4029000000000000", + "4029800000000000", + "402a000000000000", + "402a800000000000", + "402b000000000000", + "402b800000000000", + "402c000000000000", + "402e000000000000", + "402e800000000000", + "402f000000000000", + "402f800000000000", + "4030000000000000", + "4030400000000000", + "4030800000000000", + "4030c00000000000", + "4031000000000000", + "4032000000000000", + "4032400000000000", + "4032800000000000", + "4032c00000000000", + "4033000000000000", + "4033400000000000", + "4033800000000000", + "4033c00000000000", + "4034000000000000" + ], + "input_key": "input__trimmed_mean_of_differences__row_offsets__01", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__row_offsets__01", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__row_offsets__01", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__row_offsets__01", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.999999999999ap-5", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__scars__08", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "c01b666666666666", + "3fd3333333333333", + "3fdccccccccccccc", + "3fe3333333333333", + "3fe8000000000000", + "3feccccccccccccc", + "c017cccccccccccd", + "3ff3333333333333", + "3ff0000000000000", + "40224ccccccccccd", + "3ff4cccccccccccd", + "3ff7333333333333", + "3ff999999999999a", + "3ffc000000000000", + "3ffe666666666666", + "402419999999999a", + "400199999999999a", + "4000000000000000", + "40244ccccccccccd", + "4002666666666666", + "400399999999999a", + "4004cccccccccccd", + "4006000000000000", + "4007333333333333", + "402619999999999a", + "400999999999999a", + "4008000000000000", + "c00ecccccccccccd", + "400a666666666666", + "400b99999999999a", + "400ccccccccccccd", + "400e000000000000", + "400f333333333333", + "c00799999999999a", + "4010cccccccccccd", + "4010000000000000", + "40284ccccccccccd", + "4011333333333333", + "4011cccccccccccd", + "4012666666666666", + "4013000000000000", + "401399999999999a", + "402a19999999999a", + "4014cccccccccccd", + "4014000000000000", + "402a4ccccccccccd", + "4015333333333333", + "4015cccccccccccd", + "4016666666666666", + "4017000000000000", + "401799999999999a", + "402c19999999999a", + "4018cccccccccccd", + "4018000000000000", + "bfeb333333333330", + "4019333333333333", + "4019cccccccccccd", + "401a666666666666", + "401b000000000000", + "401b99999999999a", + "3fa9999999999980", + "401ccccccccccccd" + ], + "input_key": "input__trimmed_mean_of_differences__scars__08", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__scars__08", + "installed_mutated": false, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__scars__08", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 7, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__step__06", + "columns": 9, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "c008000000000000", + "c004cccccccccccd", + "c00199999999999a", + "bffccccccccccccc", + "bff6666666666666", + "bff0000000000000", + "bfe3333333333330", + "bfc9999999999990", + "3fc99999999999a0", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666", + "4014000000000000", + "401599999999999a", + "4017333333333333", + "4018cccccccccccd", + "401a666666666666", + "401c000000000000", + "401d99999999999a", + "401f333333333334", + "4020666666666666" + ], + "input_key": "input__trimmed_mean_of_differences__step__06", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__step__06", + "installed_mutated": true, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000", + "0000000000000000", + "3ff0000000000000", + "3fe0000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__step__06", + "masking_mode": 2, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__step__06", + "portable_correction_sequence_bits": [ + "3fe24924924924b0", + "bff2492492492486", + "c006db6db6db6db2", + "400b6db6db6db6e0", + "3ffb6db6db6db6e0", + "0000000000000000", + "bffb6db6db6db6d8" + ], + "portable_mutated": true, + "rows": 7, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.f47ae147ae148p+2", + "yreal_hex": "0x1.bbd70a3d70a3dp+3" + }, + { + "case_identifier": "trimmed_mean_of_differences__tall__15", + "columns": 5, + "direction": 1, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fd999999999999a", + "3fe999999999999a", + "3ff3333333333334", + "3ff999999999999a", + "bfe999999999999a", + "bfd5c28f5c28f5c3", + "3fbeb851eb851eb8", + "3fe28f5c28f5c290", + "3ff0a3d70a3d70a4", + "bff999999999999a", + "bff147ae147ae148", + "bfe1eb851eb851ec", + "bfa47ae147ae1470", + "3fdeb851eb851eb8", + "c003333333333334", + "bffd1eb851eb8521", + "bff3d70a3d70a3d8", + "bfe51eb851eb8520", + "bfb47ae147ae1490", + "c00999999999999a", + "c0047ae147ae147c", + "bffeb851eb851eba", + "bff47ae147ae147b", + "bfe47ae147ae147c", + "c010000000000000", + "c00a666666666667", + "c004cccccccccccd", + "bffe666666666666", + "bff3333333333333", + "c013333333333334", + "c01028f5c28f5c29", + "c00a3d70a3d70a40", + "c00428f5c28f5c2a", + "bffc28f5c28f5c2c", + "c016666666666667", + "c0131eb851eb851f", + "c00fae147ae147b0", + "c0091eb851eb8520", + "c0028f5c28f5c290", + "c01999999999999a", + "c016147ae147ae14", + "c0128f5c28f5c290", + "c00e147ae147ae15", + "c0070a3d70a3d70c", + "c01ccccccccccccd", + "c0190a3d70a3d70a", + "c01547ae147ae148", + "c011851eb851eb85", + "c00b851eb851eb84", + "c020000000000000", + "c01c000000000000", + "c018000000000000", + "c014000000000000", + "c010000000000000", + "c02199999999999a", + "c01ef5c28f5c28f6", + "c01ab851eb851eb8", + "c0167ae147ae147c", + "c0123d70a3d70a3f", + "c023333333333334", + "c020f5c28f5c28f6", + "c01d70a3d70a3d72", + "c018f5c28f5c28f8", + "c0147ae147ae147d", + "c024cccccccccccd", + "c02270a3d70a3d71", + "c020147ae147ae14", + "c01b70a3d70a3d70", + "c016b851eb851eb9", + "c026666666666667", + "c023eb851eb851ec", + "c02170a3d70a3d71", + "c01deb851eb851ec", + "c018f5c28f5c28f8", + "c028000000000000", + "c025666666666666", + "c022cccccccccccc", + "c020333333333334", + "c01b333333333334", + "c02999999999999a", + "c026e147ae147ae2", + "c02428f5c28f5c29", + "c02170a3d70a3d72", + "c01d70a3d70a3d72" + ], + "input_key": "input__trimmed_mean_of_differences__tall__15", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__tall__15", + "installed_mutated": false, + "mask_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "0000000000000000" + ], + "mask_key": "mask__trimmed_mean_of_differences__tall__15", + "masking_mode": 1, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__tall__15", + "portable_correction_sequence_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "portable_mutated": false, + "rows": 17, + "trim_fraction_hex": "0x0.0p+0", + "xreal_hex": "0x1.399999999999ap+2", + "yreal_hex": "0x1.07c28f5c28f5cp+5" + }, + { + "case_identifier": "trimmed_mean_of_differences__wide__14", + "columns": 17, + "direction": 0, + "extract_background_request": false, + "input_bits": [ + "0000000000000000", + "3fb1eb851eb851ec", + "3fd1eb851eb851ec", + "3fe428f5c28f5c2a", + "3ff1eb851eb851ec", + "3ffc000000000001", + "400428f5c28f5c2a", + "400b70a3d70a3d71", + "4011eb851eb851ec", + "4016ae147ae147af", + "401c000000000001", + "4020f0a3d70a3d71", + "402428f5c28f5c2a", + "4027a8f5c28f5c2a", + "402b70a3d70a3d71", + "402f800000000000", + "4031eb851eb851ec", + "3ff199999999999a", + "3ff23d70a3d70a3e", + "3ff51eb851eb851f", + "3ffa3d70a3d70a3e", + "4000cccccccccccd", + "400599999999999b", + "400b851eb851eb86", + "401147ae147ae148", + "40155c28f5c28f5c", + "401a000000000002", + "401f333333333335", + "40227ae147ae147b", + "4025a3d70a3d70a5", + "4029147ae147ae15", + "402ccccccccccccd", + "4030666666666667", + "40328a3d70a3d70b", + "400199999999999a", + "4001ae147ae147ae", + "4002e147ae147ae2", + "4005333333333333", + "4008a3d70a3d70a4", + "400d333333333334", + "401170a3d70a3d71", + "4014d70a3d70a3d8", + "4018ccccccccccce", + "401d51eb851eb853", + "4021333333333334", + "4024051eb851eb86", + "40271eb851eb851f", + "402a800000000001", + "402e28f5c28f5c2a", + "40310ccccccccccd", + "403328f5c28f5c29", + "400a666666666667", + "400a3d70a3d70a3e", + "400b333333333333", + "400d47ae147ae149", + "40103d70a3d70a3d", + "4012666666666667", + "40151eb851eb851f", + "4018666666666667", + "401c3d70a3d70a3f", + "402051eb851eb852", + "4022cccccccccccd", + "40258f5c28f5c290", + "402899999999999b", + "402beb851eb851ed", + "402f851eb851eb85", + "4031b33333333333", + "4033c7ae147ae148", + "401199999999999a", + "4011666666666667", + "4011c28f5c28f5c3", + "4012ae147ae147ae", + "401428f5c28f5c2a", + "4016333333333334", + "4018ccccccccccce", + "401bf5c28f5c28f6", + "401fae147ae147af", + "4021fae147ae147b", + "4024666666666668", + "402719999999999a", + "402a147ae147ae16", + "402d570a3d70a3d9", + "403070a3d70a3d71", + "4032599999999999", + "4034666666666666" + ], + "input_key": "input__trimmed_mean_of_differences__wide__14", + "installed_background_key": null, + "installed_corrected_key": "installed_corrected__trimmed_mean_of_differences__wide__14", + "installed_mutated": true, + "mask_bits": null, + "mask_key": null, + "masking_mode": 0, + "method": 6, + "method_name": "Trimmed mean of differences", + "portable_background_key": null, + "portable_corrected_key": "portable_corrected__trimmed_mean_of_differences__wide__14", + "portable_correction_sequence_bits": [ + "3cd0000000000000", + "0000000000000000", + "3cd0000000000000", + "3cd0000000000000", + "0000000000000000" + ], + "portable_mutated": true, + "rows": 5, + "trim_fraction_hex": "0x1.0000000000000p-1", + "xreal_hex": "0x1.b51eb851eb852p+3", + "yreal_hex": "0x1.4199999999999p+3" + } + ], + "comparison_metrics": { + "authorized_exceptions": [ + { + "case_identifier": "median__plateaus_signed_zero__10", + "classification": "signed-zero behavior", + "element_count": 3, + "elements": [ + { + "absolute_difference": 0.0, + "array": "corrected", + "case_identifier": "median__plateaus_signed_zero__10", + "column": 4, + "external_bits": "0000000000000000", + "external_class": "finite", + "external_value": 0.0, + "index": 4, + "input_bit": "8000000000000000", + "mask_bit": "0000000000000000", + "method": 1, + "method_name": "Median", + "oracle_bits": "8000000000000000", + "oracle_class": "finite", + "oracle_value": -0.0, + "probable_cause": "signed-zero behavior", + "relative_difference": null, + "row": 0, + "ulp_distance": 9223372036854775808 + }, + { + "absolute_difference": 0.0, + "array": "corrected", + "case_identifier": "median__plateaus_signed_zero__10", + "column": 3, + "external_bits": "8000000000000000", + "external_class": "finite", + "external_value": -0.0, + "index": 11, + "input_bit": "8000000000000000", + "mask_bit": "3ff0000000000000", + "method": 1, + "method_name": "Median", + "oracle_bits": "0000000000000000", + "oracle_class": "finite", + "oracle_value": 0.0, + "probable_cause": "signed-zero behavior", + "relative_difference": null, + "row": 1, + "ulp_distance": 9223372036854775808 + }, + { + "absolute_difference": 0.0, + "array": "corrected", + "case_identifier": "median__plateaus_signed_zero__10", + "column": 4, + "external_bits": "0000000000000000", + "external_class": "finite", + "external_value": 0.0, + "index": 36, + "input_bit": "8000000000000000", + "mask_bit": "0000000000000000", + "method": 1, + "method_name": "Median", + "oracle_bits": "8000000000000000", + "oracle_class": "finite", + "oracle_value": -0.0, + "probable_cause": "signed-zero behavior", + "relative_difference": null, + "row": 4, + "ulp_distance": 9223372036854775808 + } + ], + "finite_nonzero_count": 0, + "maximum_absolute_difference": 0.0, + "maximum_ulp_distance": 9223372036854775808, + "signed_zero_only_count": 3 + }, + { + "case_identifier": "median_of_differences__irregular__11", + "classification": "compiler/evaluation-order sensitivity", + "element_count": 64, + "elements": [ + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 0, + "external_bits": "c02c000000000003", + "external_class": "finite", + "external_value": -14.000000000000005, + "index": 0, + "input_bit": "c02c000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c02c000000000000", + "oracle_class": "finite", + "oracle_value": -14.0, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.8064789415719636e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 1, + "external_bits": "4013bffffffffffa", + "external_class": "finite", + "external_value": 4.937499999999995, + "index": 1, + "input_bit": "4013c00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4013c00000000000", + "oracle_class": "finite", + "oracle_value": 4.9375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.0793054214077483e-15, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "c014800000000006", + "external_class": "finite", + "external_value": -5.125000000000005, + "index": 2, + "input_bit": "c014800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c014800000000000", + "oracle_class": "finite", + "oracle_value": -5.125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.0398186376977065e-15, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 3, + "external_bits": "402b9ffffffffffd", + "external_class": "finite", + "external_value": 13.812499999999995, + "index": 3, + "input_bit": "402ba00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "402ba00000000000", + "oracle_class": "finite", + "oracle_value": 13.8125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.8581506014123103e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "400dfffffffffff4", + "external_class": "finite", + "external_value": 3.7499999999999947, + "index": 4, + "input_bit": "400e000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "400e000000000000", + "oracle_class": "finite", + "oracle_value": 3.75, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.4210854715202024e-15, + "row": 0, + "ulp_distance": 12 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "c019400000000006", + "external_class": "finite", + "external_value": -6.312500000000005, + "index": 5, + "input_bit": "c019400000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c019400000000000", + "oracle_class": "finite", + "oracle_value": -6.3125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 8.442091910020985e-16, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 6, + "external_bits": "40293ffffffffffd", + "external_class": "finite", + "external_value": 12.624999999999995, + "index": 6, + "input_bit": "4029400000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4029400000000000", + "oracle_class": "finite", + "oracle_value": 12.625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.221045955010498e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "40047ffffffffff4", + "external_class": "finite", + "external_value": 2.5624999999999947, + "index": 7, + "input_bit": "4004800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4004800000000000", + "oracle_class": "finite", + "oracle_value": 2.5625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.0796372753954194e-15, + "row": 0, + "ulp_distance": 12 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 8, + "external_bits": "c01e000000000006", + "external_class": "finite", + "external_value": -7.500000000000005, + "index": 8, + "input_bit": "c01e000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c01e000000000000", + "oracle_class": "finite", + "oracle_value": -7.5, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.105427357600996e-16, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 9, + "external_bits": "4026dffffffffffd", + "external_class": "finite", + "external_value": 11.437499999999995, + "index": 9, + "input_bit": "4026e00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4026e00000000000", + "oracle_class": "finite", + "oracle_value": 11.4375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.659296627935085e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 0, + "external_bits": "c01104924924924e", + "external_class": "finite", + "external_value": -4.25446428571429, + "index": 10, + "input_bit": "c017e00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c011049249249249", + "oracle_class": "finite", + "oracle_value": -4.254464285714286, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.0438193389969983e-15, + "row": 1, + "ulp_distance": 5 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 1, + "external_bits": "c01d44924924924e", + "external_class": "finite", + "external_value": -7.31696428571429, + "index": 11, + "input_bit": "c022100000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c01d449249249249", + "oracle_class": "finite", + "oracle_value": -7.316964285714286, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.069309518390114e-16, + "row": 1, + "ulp_distance": 5 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "c024c24924924927", + "external_class": "finite", + "external_value": -10.37946428571429, + "index": 12, + "input_bit": "c028300000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c024c24924924925", + "oracle_class": "finite", + "oracle_value": -10.379464285714286, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.4228295228013413e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 3, + "external_bits": "402f1db6db6db6d9", + "external_class": "finite", + "external_value": 15.55803571428571, + "index": 13, + "input_bit": "402bb00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "402f1db6db6db6db", + "oracle_class": "finite", + "oracle_value": 15.558035714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.2835232827871234e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "4028fdb6db6db6d9", + "external_class": "finite", + "external_value": 12.49553571428571, + "index": 14, + "input_bit": "4025900000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4028fdb6db6db6db", + "oracle_class": "finite", + "oracle_value": 12.495535714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.84318636674281e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "4022ddb6db6db6d9", + "external_class": "finite", + "external_value": 9.43303571428571, + "index": 15, + "input_bit": "401ee00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4022ddb6db6db6db", + "oracle_class": "finite", + "oracle_value": 9.433035714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.766246398728408e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 6, + "external_bits": "40197b6db6db6db2", + "external_class": "finite", + "external_value": 6.37053571428571, + "index": 16, + "input_bit": "4012a00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "40197b6db6db6db7", + "oracle_class": "finite", + "oracle_value": 6.370535714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.970986896034625e-16, + "row": 1, + "ulp_distance": 5 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "400a76db6db6db64", + "external_class": "finite", + "external_value": 3.30803571428571, + "index": 17, + "input_bit": "3ff9800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "400a76db6db6db6e", + "oracle_class": "finite", + "oracle_value": 3.3080357142857144, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.342455911017735e-15, + "row": 1, + "ulp_distance": 10 + }, + { + "absolute_difference": 3.9968028886505635e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 8, + "external_bits": "3fcf6db6db6db648", + "external_class": "finite", + "external_value": 0.2455357142857102, + "index": 18, + "input_bit": "bff7800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "3fcf6db6db6db6d8", + "oracle_class": "finite", + "oracle_value": 0.2455357142857142, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.6277888128322567e-14, + "row": 1, + "ulp_distance": 144 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 9, + "external_bits": "c00689249249249c", + "external_class": "finite", + "external_value": -2.81696428571429, + "index": 19, + "input_bit": "c012200000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c006892492492492", + "oracle_class": "finite", + "oracle_value": -2.8169642857142856, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.5764815056483974e-15, + "row": 1, + "ulp_distance": 10 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 0, + "external_bits": "bfe04924924924ac", + "external_class": "finite", + "external_value": -0.5089285714285743, + "index": 20, + "input_bit": "4000800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "bfe0492492492494", + "oracle_class": "finite", + "oracle_value": -0.5089285714285716, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 5.23557805296913e-15, + "row": 2, + "ulp_distance": 24 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 1, + "external_bits": "400b6db6db6db6d5", + "external_class": "finite", + "external_value": 3.4285714285714257, + "index": 21, + "input_bit": "4018000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "400b6db6db6db6db", + "oracle_class": "finite", + "oracle_value": 3.4285714285714284, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.771561172376103e-16, + "row": 2, + "ulp_distance": 6 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "401d76db6db6db6a", + "external_class": "finite", + "external_value": 7.366071428571425, + "index": 22, + "input_bit": "4023e00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "401d76db6db6db6e", + "oracle_class": "finite", + "oracle_value": 7.366071428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.823077963947349e-16, + "row": 2, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 3, + "external_bits": "40269b6db6db6db5", + "external_class": "finite", + "external_value": 11.303571428571425, + "index": 23, + "input_bit": "402bc00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "40269b6db6db6db7", + "oracle_class": "finite", + "oracle_value": 11.303571428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.1430010428566843e-16, + "row": 2, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "c02b84924924924b", + "external_class": "finite", + "external_value": -13.758928571428575, + "index": 24, + "input_bit": "c026600000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c02b849249249249", + "oracle_class": "finite", + "oracle_value": -13.758928571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.582115068304062e-16, + "row": 2, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "c023a4924924924b", + "external_class": "finite", + "external_value": -9.821428571428575, + "index": 25, + "input_bit": "c01d000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c023a49249249249", + "oracle_class": "finite", + "oracle_value": -9.821428571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.6173084729605087e-16, + "row": 2, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 6, + "external_bits": "c017892492492496", + "external_class": "finite", + "external_value": -5.883928571428575, + "index": 26, + "input_bit": "c00a800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c017892492492492", + "oracle_class": "finite", + "oracle_value": -5.883928571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.037995933621485e-16, + "row": 2, + "ulp_distance": 4 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "bfff249249249256", + "external_class": "finite", + "external_value": -1.9464285714285743, + "index": 27, + "input_bit": "3fe4000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "bfff24924924924a", + "oracle_class": "finite", + "oracle_value": -1.9464285714285716, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.368935545959824e-15, + "row": 2, + "ulp_distance": 12 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 8, + "external_bits": "3fffdb6db6db6daa", + "external_class": "finite", + "external_value": 1.9910714285714257, + "index": 28, + "input_bit": "4012400000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "3fffdb6db6db6db6", + "oracle_class": "finite", + "oracle_value": 1.9910714285714284, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.3382419238531054e-15, + "row": 2, + "ulp_distance": 12 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 9, + "external_bits": "4017b6db6db6db6a", + "external_class": "finite", + "external_value": 5.928571428571425, + "index": 29, + "input_bit": "4021000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4017b6db6db6db6e", + "oracle_class": "finite", + "oracle_value": 5.928571428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 5.992529096771933e-16, + "row": 2, + "ulp_distance": 4 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "4000e49249249246", + "external_class": "finite", + "external_value": 2.1116071428571415, + "index": 32, + "input_bit": "4007c00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4000e49249249248", + "oracle_class": "finite", + "oracle_value": 2.1116071428571423, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.20617264297734e-16, + "row": 3, + "ulp_distance": 2 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "c0140db6db6db6dd", + "external_class": "finite", + "external_value": -5.0133928571428585, + "index": 34, + "input_bit": "c010a00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c0140db6db6db6dc", + "oracle_class": "finite", + "oracle_value": -5.013392857142858, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.7716114515835085e-16, + "row": 3, + "ulp_distance": 1 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "4017b24924924923", + "external_class": "finite", + "external_value": 5.9241071428571415, + "index": 35, + "input_bit": "401b200000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4017b24924924924", + "oracle_class": "finite", + "oracle_value": 5.924107142857142, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.4992612359670543e-16, + "row": 3, + "ulp_distance": 1 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "bff336db6db6db74", + "external_class": "finite", + "external_value": -1.2008928571428585, + "index": 37, + "input_bit": "bfd6000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "bff336db6db6db70", + "oracle_class": "finite", + "oracle_value": -1.2008928571428577, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.395983866647874e-16, + "row": 3, + "ulp_distance": 4 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 0, + "external_bits": "bfd16db6db6db6c0", + "external_class": "finite", + "external_value": -0.27232142857142705, + "index": 50, + "input_bit": "c006c00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "bfd16db6db6db6e0", + "oracle_class": "finite", + "oracle_value": -0.2723214285714288, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.523015279109153e-15, + "row": 5, + "ulp_distance": 32 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 1, + "external_bits": "c01156db6db6db6c", + "external_class": "finite", + "external_value": -4.334821428571427, + "index": 51, + "input_bit": "c01ba00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c01156db6db6db6e", + "oracle_class": "finite", + "oracle_value": -4.334821428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.0978777757534115e-16, + "row": 5, + "ulp_distance": 2 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "c020cb6db6db6db6", + "external_class": "finite", + "external_value": -8.397321428571427, + "index": 52, + "input_bit": "c025f00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c020cb6db6db6db7", + "oracle_class": "finite", + "oracle_value": -8.397321428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.1153850719067315e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 3, + "external_bits": "40308a4924924925", + "external_class": "finite", + "external_value": 16.540178571428573, + "index": 53, + "input_bit": "402bf00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "40308a4924924924", + "oracle_class": "finite", + "oracle_value": 16.54017857142857, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.147929457628373e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "4028f4924924924a", + "external_class": "finite", + "external_value": 12.477678571428573, + "index": 54, + "input_bit": "4023d00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4028f49249249249", + "oracle_class": "finite", + "oracle_value": 12.477678571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.4236276637769447e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "4020d4924924924a", + "external_class": "finite", + "external_value": 8.415178571428573, + "index": 55, + "input_bit": "4017600000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4020d49249249249", + "oracle_class": "finite", + "oracle_value": 8.415178571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.1108961911175385e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 6, + "external_bits": "4011692492492494", + "external_class": "finite", + "external_value": 4.352678571428573, + "index": 56, + "input_bit": "3ffc800000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4011692492492492", + "oracle_class": "finite", + "oracle_value": 4.352678571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.0810659694939073e-16, + "row": 5, + "ulp_distance": 2 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "3fd2924924924940", + "external_class": "finite", + "external_value": 0.29017857142857295, + "index": 57, + "input_bit": "c002400000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "3fd2924924924920", + "oracle_class": "finite", + "oracle_value": 0.2901785714285712, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.121598954240831e-15, + "row": 5, + "ulp_distance": 32 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 8, + "external_bits": "c00e2db6db6db6d8", + "external_class": "finite", + "external_value": -3.772321428571427, + "index": 58, + "input_bit": "c019600000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c00e2db6db6db6dc", + "oracle_class": "finite", + "oracle_value": -3.772321428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.708922272492974e-16, + "row": 5, + "ulp_distance": 4 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 9, + "external_bits": "c01f56db6db6db6c", + "external_class": "finite", + "external_value": -7.834821428571427, + "index": 59, + "input_bit": "c024d00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c01f56db6db6db6e", + "oracle_class": "finite", + "oracle_value": -7.834821428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.267258871941061e-16, + "row": 5, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 0, + "external_bits": "400bc92492492498", + "external_class": "finite", + "external_value": 3.4732142857142883, + "index": 60, + "input_bit": "4014c00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "400bc92492492490", + "oracle_class": "finite", + "oracle_value": 3.4732142857142847, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.022889285412997e-15, + "row": 6, + "ulp_distance": 8 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 1, + "external_bits": "4019a4924924924c", + "external_class": "finite", + "external_value": 6.410714285714288, + "index": 61, + "input_bit": "4020400000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4019a49249249248", + "oracle_class": "finite", + "oracle_value": 6.410714285714285, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 5.541837493393537e-16, + "row": 6, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "4022b24924924926", + "external_class": "finite", + "external_value": 9.348214285714288, + "index": 62, + "input_bit": "4026200000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4022b24924924924", + "oracle_class": "finite", + "oracle_value": 9.348214285714285, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.8004195990989113e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 3, + "external_bits": "4028924924924926", + "external_class": "finite", + "external_value": 12.285714285714288, + "index": 63, + "input_bit": "402c000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4028924924924924", + "oracle_class": "finite", + "oracle_value": 12.285714285714285, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.8917436920469186e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "c02b8db6db6db6da", + "external_class": "finite", + "external_value": -13.776785714285712, + "index": 64, + "input_bit": "c028200000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c02b8db6db6db6dc", + "oracle_class": "finite", + "oracle_value": -13.776785714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.578768192000364e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "c025adb6db6db6da", + "external_class": "finite", + "external_value": -10.839285714285712, + "index": 65, + "input_bit": "c022400000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c025adb6db6db6dc", + "oracle_class": "finite", + "oracle_value": -10.839285714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.2776271171800345e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 6, + "external_bits": "c01f9b6db6db6db4", + "external_class": "finite", + "external_value": -7.901785714285712, + "index": 66, + "input_bit": "c018c00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c01f9b6db6db6db8", + "oracle_class": "finite", + "oracle_value": -7.901785714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.496089627408545e-16, + "row": 6, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "c013db6db6db6db4", + "external_class": "finite", + "external_value": -4.964285714285712, + "index": 67, + "input_bit": "c00a000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c013db6db6db6db8", + "oracle_class": "finite", + "oracle_value": -4.964285714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.15654554002979e-16, + "row": 6, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 8, + "external_bits": "c00036db6db6db68", + "external_class": "finite", + "external_value": -2.0267857142857117, + "index": 68, + "input_bit": "bfd4000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c00036db6db6db70", + "oracle_class": "finite", + "oracle_value": -2.0267857142857153, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.7528807578222758e-15, + "row": 6, + "ulp_distance": 8 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 9, + "external_bits": "3fed249249249260", + "external_class": "finite", + "external_value": 0.9107142857142883, + "index": 69, + "input_bit": "4005000000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "3fed249249249240", + "oracle_class": "finite", + "oracle_value": 0.9107142857142847, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.90101894142799e-15, + "row": 6, + "ulp_distance": 32 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 0, + "external_bits": "402a700000000002", + "external_class": "finite", + "external_value": 13.218750000000004, + "index": 70, + "input_bit": "402a700000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "402a700000000000", + "oracle_class": "finite", + "oracle_value": 13.21875, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.6876320974377306e-16, + "row": 7, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 1, + "external_bits": "c0175ffffffffffc", + "external_class": "finite", + "external_value": -5.8437499999999964, + "index": 71, + "input_bit": "c017600000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c017600000000000", + "oracle_class": "finite", + "oracle_value": -5.84375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.079510038589096e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 2, + "external_bits": "4010600000000004", + "external_class": "finite", + "external_value": 4.0937500000000036, + "index": 72, + "input_bit": "4010600000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4010600000000000", + "oracle_class": "finite", + "oracle_value": 4.09375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 8.67838455890198e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 3, + "external_bits": "402c100000000002", + "external_class": "finite", + "external_value": 14.031250000000004, + "index": 73, + "input_bit": "402c100000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "402c100000000000", + "oracle_class": "finite", + "oracle_value": 14.03125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.532000840125078e-16, + "row": 7, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 4, + "external_bits": "c0141ffffffffffc", + "external_class": "finite", + "external_value": -5.0312499999999964, + "index": 74, + "input_bit": "c014200000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c014200000000000", + "oracle_class": "finite", + "oracle_value": -5.03125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.061294268423361e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 5, + "external_bits": "4013a00000000004", + "external_class": "finite", + "external_value": 4.9062500000000036, + "index": 75, + "input_bit": "4013a00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4013a00000000000", + "oracle_class": "finite", + "oracle_value": 4.90625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.241199854879996e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 6, + "external_bits": "c02c4ffffffffffe", + "external_class": "finite", + "external_value": -14.156249999999996, + "index": 76, + "input_bit": "c02c500000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c02c500000000000", + "oracle_class": "finite", + "oracle_value": -14.15625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.5096432168127164e-16, + "row": 7, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 7, + "external_bits": "c010dffffffffffc", + "external_class": "finite", + "external_value": -4.2187499999999964, + "index": 77, + "input_bit": "c010e00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c010e00000000000", + "oracle_class": "finite", + "oracle_value": -4.21875, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 8.421247238638232e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 8, + "external_bits": "4016e00000000004", + "external_class": "finite", + "external_value": 5.7187500000000036, + "index": 78, + "input_bit": "4016e00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "4016e00000000000", + "oracle_class": "finite", + "oracle_value": 5.71875, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.21239550391344e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "median_of_differences__irregular__11", + "column": 9, + "external_bits": "c02aaffffffffffe", + "external_class": "finite", + "external_value": -13.343749999999996, + "index": 79, + "input_bit": "c02ab00000000000", + "mask_bit": "3ff0000000000000", + "method": 2, + "method_name": "Median of differences", + "oracle_bits": "c02ab00000000000", + "oracle_class": "finite", + "oracle_value": -13.34375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.662455215962905e-16, + "row": 7, + "ulp_distance": 2 + } + ], + "finite_nonzero_count": 64, + "maximum_absolute_difference": 5.329070518200751e-15, + "maximum_ulp_distance": 144, + "signed_zero_only_count": 0 + }, + { + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "classification": "compiler/evaluation-order sensitivity", + "element_count": 64, + "elements": [ + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 0, + "external_bits": "c02c000000000003", + "external_class": "finite", + "external_value": -14.000000000000005, + "index": 0, + "input_bit": "c02c000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c02c000000000000", + "oracle_class": "finite", + "oracle_value": -14.0, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.8064789415719636e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 1, + "external_bits": "4013bffffffffffa", + "external_class": "finite", + "external_value": 4.937499999999995, + "index": 1, + "input_bit": "4013c00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4013c00000000000", + "oracle_class": "finite", + "oracle_value": 4.9375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.0793054214077483e-15, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "c014800000000006", + "external_class": "finite", + "external_value": -5.125000000000005, + "index": 2, + "input_bit": "c014800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c014800000000000", + "oracle_class": "finite", + "oracle_value": -5.125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.0398186376977065e-15, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 3, + "external_bits": "402b9ffffffffffd", + "external_class": "finite", + "external_value": 13.812499999999995, + "index": 3, + "input_bit": "402ba00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "402ba00000000000", + "oracle_class": "finite", + "oracle_value": 13.8125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.8581506014123103e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "400dfffffffffff4", + "external_class": "finite", + "external_value": 3.7499999999999947, + "index": 4, + "input_bit": "400e000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "400e000000000000", + "oracle_class": "finite", + "oracle_value": 3.75, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.4210854715202024e-15, + "row": 0, + "ulp_distance": 12 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "c019400000000006", + "external_class": "finite", + "external_value": -6.312500000000005, + "index": 5, + "input_bit": "c019400000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c019400000000000", + "oracle_class": "finite", + "oracle_value": -6.3125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 8.442091910020985e-16, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 6, + "external_bits": "40293ffffffffffd", + "external_class": "finite", + "external_value": 12.624999999999995, + "index": 6, + "input_bit": "4029400000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4029400000000000", + "oracle_class": "finite", + "oracle_value": 12.625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.221045955010498e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "40047ffffffffff4", + "external_class": "finite", + "external_value": 2.5624999999999947, + "index": 7, + "input_bit": "4004800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4004800000000000", + "oracle_class": "finite", + "oracle_value": 2.5625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.0796372753954194e-15, + "row": 0, + "ulp_distance": 12 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 8, + "external_bits": "c01e000000000006", + "external_class": "finite", + "external_value": -7.500000000000005, + "index": 8, + "input_bit": "c01e000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c01e000000000000", + "oracle_class": "finite", + "oracle_value": -7.5, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.105427357600996e-16, + "row": 0, + "ulp_distance": 6 + }, + { + "absolute_difference": 5.329070518200751e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 9, + "external_bits": "4026dffffffffffd", + "external_class": "finite", + "external_value": 11.437499999999995, + "index": 9, + "input_bit": "4026e00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4026e00000000000", + "oracle_class": "finite", + "oracle_value": 11.4375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.659296627935085e-16, + "row": 0, + "ulp_distance": 3 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 0, + "external_bits": "c01104924924924e", + "external_class": "finite", + "external_value": -4.25446428571429, + "index": 10, + "input_bit": "c017e00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c011049249249249", + "oracle_class": "finite", + "oracle_value": -4.254464285714286, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.0438193389969983e-15, + "row": 1, + "ulp_distance": 5 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 1, + "external_bits": "c01d44924924924e", + "external_class": "finite", + "external_value": -7.31696428571429, + "index": 11, + "input_bit": "c022100000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c01d449249249249", + "oracle_class": "finite", + "oracle_value": -7.316964285714286, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.069309518390114e-16, + "row": 1, + "ulp_distance": 5 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "c024c24924924927", + "external_class": "finite", + "external_value": -10.37946428571429, + "index": 12, + "input_bit": "c028300000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c024c24924924925", + "oracle_class": "finite", + "oracle_value": -10.379464285714286, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.4228295228013413e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 3, + "external_bits": "402f1db6db6db6d9", + "external_class": "finite", + "external_value": 15.55803571428571, + "index": 13, + "input_bit": "402bb00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "402f1db6db6db6db", + "oracle_class": "finite", + "oracle_value": 15.558035714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.2835232827871234e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "4028fdb6db6db6d9", + "external_class": "finite", + "external_value": 12.49553571428571, + "index": 14, + "input_bit": "4025900000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4028fdb6db6db6db", + "oracle_class": "finite", + "oracle_value": 12.495535714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.84318636674281e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "4022ddb6db6db6d9", + "external_class": "finite", + "external_value": 9.43303571428571, + "index": 15, + "input_bit": "401ee00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4022ddb6db6db6db", + "oracle_class": "finite", + "oracle_value": 9.433035714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.766246398728408e-16, + "row": 1, + "ulp_distance": 2 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 6, + "external_bits": "40197b6db6db6db2", + "external_class": "finite", + "external_value": 6.37053571428571, + "index": 16, + "input_bit": "4012a00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "40197b6db6db6db7", + "oracle_class": "finite", + "oracle_value": 6.370535714285714, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.970986896034625e-16, + "row": 1, + "ulp_distance": 5 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "400a76db6db6db64", + "external_class": "finite", + "external_value": 3.30803571428571, + "index": 17, + "input_bit": "3ff9800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "400a76db6db6db6e", + "oracle_class": "finite", + "oracle_value": 3.3080357142857144, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.342455911017735e-15, + "row": 1, + "ulp_distance": 10 + }, + { + "absolute_difference": 3.9968028886505635e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 8, + "external_bits": "3fcf6db6db6db648", + "external_class": "finite", + "external_value": 0.2455357142857102, + "index": 18, + "input_bit": "bff7800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "3fcf6db6db6db6d8", + "oracle_class": "finite", + "oracle_value": 0.2455357142857142, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.6277888128322567e-14, + "row": 1, + "ulp_distance": 144 + }, + { + "absolute_difference": 4.440892098500626e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 9, + "external_bits": "c00689249249249c", + "external_class": "finite", + "external_value": -2.81696428571429, + "index": 19, + "input_bit": "c012200000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c006892492492492", + "oracle_class": "finite", + "oracle_value": -2.8169642857142856, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.5764815056483974e-15, + "row": 1, + "ulp_distance": 10 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 0, + "external_bits": "bfe04924924924ac", + "external_class": "finite", + "external_value": -0.5089285714285743, + "index": 20, + "input_bit": "4000800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "bfe0492492492494", + "oracle_class": "finite", + "oracle_value": -0.5089285714285716, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 5.23557805296913e-15, + "row": 2, + "ulp_distance": 24 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 1, + "external_bits": "400b6db6db6db6d5", + "external_class": "finite", + "external_value": 3.4285714285714257, + "index": 21, + "input_bit": "4018000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "400b6db6db6db6db", + "oracle_class": "finite", + "oracle_value": 3.4285714285714284, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.771561172376103e-16, + "row": 2, + "ulp_distance": 6 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "401d76db6db6db6a", + "external_class": "finite", + "external_value": 7.366071428571425, + "index": 22, + "input_bit": "4023e00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "401d76db6db6db6e", + "oracle_class": "finite", + "oracle_value": 7.366071428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.823077963947349e-16, + "row": 2, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 3, + "external_bits": "40269b6db6db6db5", + "external_class": "finite", + "external_value": 11.303571428571425, + "index": 23, + "input_bit": "402bc00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "40269b6db6db6db7", + "oracle_class": "finite", + "oracle_value": 11.303571428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.1430010428566843e-16, + "row": 2, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "c02b84924924924b", + "external_class": "finite", + "external_value": -13.758928571428575, + "index": 24, + "input_bit": "c026600000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c02b849249249249", + "oracle_class": "finite", + "oracle_value": -13.758928571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.582115068304062e-16, + "row": 2, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "c023a4924924924b", + "external_class": "finite", + "external_value": -9.821428571428575, + "index": 25, + "input_bit": "c01d000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c023a49249249249", + "oracle_class": "finite", + "oracle_value": -9.821428571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.6173084729605087e-16, + "row": 2, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 6, + "external_bits": "c017892492492496", + "external_class": "finite", + "external_value": -5.883928571428575, + "index": 26, + "input_bit": "c00a800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c017892492492492", + "oracle_class": "finite", + "oracle_value": -5.883928571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.037995933621485e-16, + "row": 2, + "ulp_distance": 4 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "bfff249249249256", + "external_class": "finite", + "external_value": -1.9464285714285743, + "index": 27, + "input_bit": "3fe4000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "bfff24924924924a", + "oracle_class": "finite", + "oracle_value": -1.9464285714285716, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.368935545959824e-15, + "row": 2, + "ulp_distance": 12 + }, + { + "absolute_difference": 2.6645352591003757e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 8, + "external_bits": "3fffdb6db6db6daa", + "external_class": "finite", + "external_value": 1.9910714285714257, + "index": 28, + "input_bit": "4012400000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "3fffdb6db6db6db6", + "oracle_class": "finite", + "oracle_value": 1.9910714285714284, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.3382419238531054e-15, + "row": 2, + "ulp_distance": 12 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 9, + "external_bits": "4017b6db6db6db6a", + "external_class": "finite", + "external_value": 5.928571428571425, + "index": 29, + "input_bit": "4021000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4017b6db6db6db6e", + "oracle_class": "finite", + "oracle_value": 5.928571428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 5.992529096771933e-16, + "row": 2, + "ulp_distance": 4 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "4000e49249249246", + "external_class": "finite", + "external_value": 2.1116071428571415, + "index": 32, + "input_bit": "4007c00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4000e49249249248", + "oracle_class": "finite", + "oracle_value": 2.1116071428571423, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.20617264297734e-16, + "row": 3, + "ulp_distance": 2 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "c0140db6db6db6dd", + "external_class": "finite", + "external_value": -5.0133928571428585, + "index": 34, + "input_bit": "c010a00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c0140db6db6db6dc", + "oracle_class": "finite", + "oracle_value": -5.013392857142858, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.7716114515835085e-16, + "row": 3, + "ulp_distance": 1 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "4017b24924924923", + "external_class": "finite", + "external_value": 5.9241071428571415, + "index": 35, + "input_bit": "401b200000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4017b24924924924", + "oracle_class": "finite", + "oracle_value": 5.924107142857142, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.4992612359670543e-16, + "row": 3, + "ulp_distance": 1 + }, + { + "absolute_difference": 8.881784197001252e-16, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "bff336db6db6db74", + "external_class": "finite", + "external_value": -1.2008928571428585, + "index": 37, + "input_bit": "bfd6000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "bff336db6db6db70", + "oracle_class": "finite", + "oracle_value": -1.2008928571428577, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.395983866647874e-16, + "row": 3, + "ulp_distance": 4 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 0, + "external_bits": "bfd16db6db6db6c0", + "external_class": "finite", + "external_value": -0.27232142857142705, + "index": 50, + "input_bit": "c006c00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "bfd16db6db6db6e0", + "oracle_class": "finite", + "oracle_value": -0.2723214285714288, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.523015279109153e-15, + "row": 5, + "ulp_distance": 32 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 1, + "external_bits": "c01156db6db6db6c", + "external_class": "finite", + "external_value": -4.334821428571427, + "index": 51, + "input_bit": "c01ba00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c01156db6db6db6e", + "oracle_class": "finite", + "oracle_value": -4.334821428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.0978777757534115e-16, + "row": 5, + "ulp_distance": 2 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "c020cb6db6db6db6", + "external_class": "finite", + "external_value": -8.397321428571427, + "index": 52, + "input_bit": "c025f00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c020cb6db6db6db7", + "oracle_class": "finite", + "oracle_value": -8.397321428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.1153850719067315e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 3, + "external_bits": "40308a4924924925", + "external_class": "finite", + "external_value": 16.540178571428573, + "index": 53, + "input_bit": "402bf00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "40308a4924924924", + "oracle_class": "finite", + "oracle_value": 16.54017857142857, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.147929457628373e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "4028f4924924924a", + "external_class": "finite", + "external_value": 12.477678571428573, + "index": 54, + "input_bit": "4023d00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4028f49249249249", + "oracle_class": "finite", + "oracle_value": 12.477678571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.4236276637769447e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "4020d4924924924a", + "external_class": "finite", + "external_value": 8.415178571428573, + "index": 55, + "input_bit": "4017600000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4020d49249249249", + "oracle_class": "finite", + "oracle_value": 8.415178571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.1108961911175385e-16, + "row": 5, + "ulp_distance": 1 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 6, + "external_bits": "4011692492492494", + "external_class": "finite", + "external_value": 4.352678571428573, + "index": 56, + "input_bit": "3ffc800000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4011692492492492", + "oracle_class": "finite", + "oracle_value": 4.352678571428571, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.0810659694939073e-16, + "row": 5, + "ulp_distance": 2 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "3fd2924924924940", + "external_class": "finite", + "external_value": 0.29017857142857295, + "index": 57, + "input_bit": "c002400000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "3fd2924924924920", + "oracle_class": "finite", + "oracle_value": 0.2901785714285712, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.121598954240831e-15, + "row": 5, + "ulp_distance": 32 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 8, + "external_bits": "c00e2db6db6db6d8", + "external_class": "finite", + "external_value": -3.772321428571427, + "index": 58, + "input_bit": "c019600000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c00e2db6db6db6dc", + "oracle_class": "finite", + "oracle_value": -3.772321428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.708922272492974e-16, + "row": 5, + "ulp_distance": 4 + }, + { + "absolute_difference": 1.7763568394002505e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 9, + "external_bits": "c01f56db6db6db6c", + "external_class": "finite", + "external_value": -7.834821428571427, + "index": 59, + "input_bit": "c024d00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c01f56db6db6db6e", + "oracle_class": "finite", + "oracle_value": -7.834821428571429, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.267258871941061e-16, + "row": 5, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 0, + "external_bits": "400bc92492492498", + "external_class": "finite", + "external_value": 3.4732142857142883, + "index": 60, + "input_bit": "4014c00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "400bc92492492490", + "oracle_class": "finite", + "oracle_value": 3.4732142857142847, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.022889285412997e-15, + "row": 6, + "ulp_distance": 8 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 1, + "external_bits": "4019a4924924924c", + "external_class": "finite", + "external_value": 6.410714285714288, + "index": 61, + "input_bit": "4020400000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4019a49249249248", + "oracle_class": "finite", + "oracle_value": 6.410714285714285, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 5.541837493393537e-16, + "row": 6, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "4022b24924924926", + "external_class": "finite", + "external_value": 9.348214285714288, + "index": 62, + "input_bit": "4026200000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4022b24924924924", + "oracle_class": "finite", + "oracle_value": 9.348214285714285, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.8004195990989113e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 3, + "external_bits": "4028924924924926", + "external_class": "finite", + "external_value": 12.285714285714288, + "index": 63, + "input_bit": "402c000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4028924924924924", + "oracle_class": "finite", + "oracle_value": 12.285714285714285, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.8917436920469186e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "c02b8db6db6db6da", + "external_class": "finite", + "external_value": -13.776785714285712, + "index": 64, + "input_bit": "c028200000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c02b8db6db6db6dc", + "oracle_class": "finite", + "oracle_value": -13.776785714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.578768192000364e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "c025adb6db6db6da", + "external_class": "finite", + "external_value": -10.839285714285712, + "index": 65, + "input_bit": "c022400000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c025adb6db6db6dc", + "oracle_class": "finite", + "oracle_value": -10.839285714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.2776271171800345e-16, + "row": 6, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 6, + "external_bits": "c01f9b6db6db6db4", + "external_class": "finite", + "external_value": -7.901785714285712, + "index": 66, + "input_bit": "c018c00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c01f9b6db6db6db8", + "oracle_class": "finite", + "oracle_value": -7.901785714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 4.496089627408545e-16, + "row": 6, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "c013db6db6db6db4", + "external_class": "finite", + "external_value": -4.964285714285712, + "index": 67, + "input_bit": "c00a000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c013db6db6db6db8", + "oracle_class": "finite", + "oracle_value": -4.964285714285715, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.15654554002979e-16, + "row": 6, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 8, + "external_bits": "c00036db6db6db68", + "external_class": "finite", + "external_value": -2.0267857142857117, + "index": 68, + "input_bit": "bfd4000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c00036db6db6db70", + "oracle_class": "finite", + "oracle_value": -2.0267857142857153, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 1.7528807578222758e-15, + "row": 6, + "ulp_distance": 8 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 9, + "external_bits": "3fed249249249260", + "external_class": "finite", + "external_value": 0.9107142857142883, + "index": 69, + "input_bit": "4005000000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "3fed249249249240", + "oracle_class": "finite", + "oracle_value": 0.9107142857142847, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 3.90101894142799e-15, + "row": 6, + "ulp_distance": 32 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 0, + "external_bits": "402a700000000002", + "external_class": "finite", + "external_value": 13.218750000000004, + "index": 70, + "input_bit": "402a700000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "402a700000000000", + "oracle_class": "finite", + "oracle_value": 13.21875, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.6876320974377306e-16, + "row": 7, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 1, + "external_bits": "c0175ffffffffffc", + "external_class": "finite", + "external_value": -5.8437499999999964, + "index": 71, + "input_bit": "c017600000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c017600000000000", + "oracle_class": "finite", + "oracle_value": -5.84375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.079510038589096e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 2, + "external_bits": "4010600000000004", + "external_class": "finite", + "external_value": 4.0937500000000036, + "index": 72, + "input_bit": "4010600000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4010600000000000", + "oracle_class": "finite", + "oracle_value": 4.09375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 8.67838455890198e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 3, + "external_bits": "402c100000000002", + "external_class": "finite", + "external_value": 14.031250000000004, + "index": 73, + "input_bit": "402c100000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "402c100000000000", + "oracle_class": "finite", + "oracle_value": 14.03125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.532000840125078e-16, + "row": 7, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 4, + "external_bits": "c0141ffffffffffc", + "external_class": "finite", + "external_value": -5.0312499999999964, + "index": 74, + "input_bit": "c014200000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c014200000000000", + "oracle_class": "finite", + "oracle_value": -5.03125, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.061294268423361e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 5, + "external_bits": "4013a00000000004", + "external_class": "finite", + "external_value": 4.9062500000000036, + "index": 75, + "input_bit": "4013a00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4013a00000000000", + "oracle_class": "finite", + "oracle_value": 4.90625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 7.241199854879996e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 6, + "external_bits": "c02c4ffffffffffe", + "external_class": "finite", + "external_value": -14.156249999999996, + "index": 76, + "input_bit": "c02c500000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c02c500000000000", + "oracle_class": "finite", + "oracle_value": -14.15625, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.5096432168127164e-16, + "row": 7, + "ulp_distance": 2 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 7, + "external_bits": "c010dffffffffffc", + "external_class": "finite", + "external_value": -4.2187499999999964, + "index": 77, + "input_bit": "c010e00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c010e00000000000", + "oracle_class": "finite", + "oracle_value": -4.21875, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 8.421247238638232e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 8, + "external_bits": "4016e00000000004", + "external_class": "finite", + "external_value": 5.7187500000000036, + "index": 78, + "input_bit": "4016e00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "4016e00000000000", + "oracle_class": "finite", + "oracle_value": 5.71875, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 6.21239550391344e-16, + "row": 7, + "ulp_distance": 4 + }, + { + "absolute_difference": 3.552713678800501e-15, + "array": "corrected", + "case_identifier": "trimmed_mean_of_differences__irregular__11", + "column": 9, + "external_bits": "c02aaffffffffffe", + "external_class": "finite", + "external_value": -13.343749999999996, + "index": 79, + "input_bit": "c02ab00000000000", + "mask_bit": "3ff0000000000000", + "method": 6, + "method_name": "Trimmed mean of differences", + "oracle_bits": "c02ab00000000000", + "oracle_class": "finite", + "oracle_value": -13.34375, + "probable_cause": "compiler/evaluation-order sensitivity", + "relative_difference": 2.662455215962905e-16, + "row": 7, + "ulp_distance": 2 + } + ], + "finite_nonzero_count": 64, + "maximum_absolute_difference": 5.329070518200751e-15, + "maximum_ulp_distance": 144, + "signed_zero_only_count": 0 + } + ], + "background": { + "arrays_bitwise_exact": 8, + "elements_bitwise_exact": 504, + "first_mismatch": null, + "infinity_mismatches": 0, + "logical_arrays_compared": 8, + "max_absolute_difference": 0.0, + "max_relative_difference": 0.0, + "max_ulp_distance": 0, + "nan_mismatches": 0, + "nonzero_mismatches": 0, + "signed_zero_only_mismatches": 0, + "total_elements": 504 + }, + "corrected": { + "arrays_bitwise_exact": 61, + "elements_bitwise_exact": 3757, + "first_mismatch": { + "absolute_difference": 0.0, + "array": "corrected", + "case_identifier": "median__plateaus_signed_zero__10", + "column": 4, + "external_bits": "0000000000000000", + "external_class": "finite", + "external_value": 0.0, + "index": 4, + "input_bit": "8000000000000000", + "mask_bit": "0000000000000000", + "method": 1, + "method_name": "Median", + "oracle_bits": "8000000000000000", + "oracle_class": "finite", + "oracle_value": -0.0, + "probable_cause": "signed-zero behavior", + "relative_difference": null, + "row": 0, + "ulp_distance": 9223372036854775808 + }, + "infinity_mismatches": 0, + "logical_arrays_compared": 64, + "max_absolute_difference": 5.329070518200751e-15, + "max_relative_difference": 1.6277888128322567e-14, + "max_ulp_distance": 9223372036854775808, + "nan_mismatches": 0, + "nonzero_mismatches": 128, + "signed_zero_only_mismatches": 3, + "total_elements": 3888 + }, + "mutation_noop_agreement": { + "arrays": "64/64" + } + }, + "evidence": { + "evidence_vocabulary": [ + "SOURCE_CONFIRMED", + "EXTERNAL_PROBE_CONFIRMED", + "ORACLE_CONFIRMED", + "SOFTWARE_VERIFIED" + ], + "installed_build_diagnosis": [ + "INSTALLED_BUILD_ROOT_CAUSE_CONFIRMED", + "V3_NOT_JUSTIFIED" + ], + "non_claims": [ + "No public API is frozen by this private fixture.", + "No universal, non-finite, other-version, performance, or production-adapter equivalence is claimed.", + "The installed fast-math reassociation is not normalized or emulated by portable production arithmetic." + ], + "source_hashes": { + "aggregate_comparison.json": "8c017ccecaa64a2d09b9462928ec8c87c7638d509583ad569b097ae869d7c70b", + "canonical_reference.json": "e2fa6d094acc5ec04f87901aa345244f9d22e70577d25f963a8cdb74363e457e", + "installed_build_matrix.json": "0f5889fe40aca3fbdbcd5cefe217a6a4cc492e77087a7f3d28ea8bfd3ef1752b", + "installed_build_root_cause_report.md": "eeb835e456c395337fe6fb8ef71e65f4f9ceaa8b95edc285778b52f2c5a9bbd4", + "mask_root_cause_report.md": "c0e0c34a205c07de06ab4397a01e235935de2fe36e7e431cf3613285726ba521", + "mismatch_ledger_v2.json": "49a8a763b92e24223791a9b6c3f0177910b51d86182a9661601b693bbe12b04c", + "oracle_candidate_outputs_v2.json": "7da7283019d698089d1a9cca4cec712860529fce42c2cb0752c2b136ecd1cb30", + "oracle_v2.py": "06585bf85b58392640e5c185694a2b4e47742dae8969b65a689f1bb5c717b198", + "sanitized_input_only_cases.json": "70aa072427df43bb9275e0c946322afd656ab7f7919afee403723644b04ca50e" + } + }, + "fixture": { + "array_hashes": { + "input__median__alternating_offsets__02": "357a6cfeffcac0560df03db6751fb1e35aef3070f6a027d0f71d569f9f3907bf", + "input__median__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "input__median__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "input__median__irregular__11": "90b41a79dd042fc9968e4542bcb8744ebc5ea3bcbc0215b3ca79337b059dc7e0", + "input__median__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "input__median__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "input__median__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "input__median__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "input__median__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "input__median__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "input__median__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "input__median__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "input__median__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "input__median__step__06": "d2af277d46bc5e074665ea196e664d21ad1cc891a96b4f84f9bc715dc40f0057", + "input__median__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "input__median__wide__14": "d6b0650aa9ab258fd54d7df089d829cf074e266af1b9c22b0638a5ea14311c8c", + "input__median_of_differences__alternating_offsets__02": "357a6cfeffcac0560df03db6751fb1e35aef3070f6a027d0f71d569f9f3907bf", + "input__median_of_differences__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "input__median_of_differences__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "input__median_of_differences__irregular__11": "90b41a79dd042fc9968e4542bcb8744ebc5ea3bcbc0215b3ca79337b059dc7e0", + "input__median_of_differences__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "input__median_of_differences__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "input__median_of_differences__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "input__median_of_differences__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "input__median_of_differences__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "input__median_of_differences__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "input__median_of_differences__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "input__median_of_differences__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "input__median_of_differences__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "input__median_of_differences__step__06": "d2af277d46bc5e074665ea196e664d21ad1cc891a96b4f84f9bc715dc40f0057", + "input__median_of_differences__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "input__median_of_differences__wide__14": "d6b0650aa9ab258fd54d7df089d829cf074e266af1b9c22b0638a5ea14311c8c", + "input__trimmed_mean__alternating_offsets__02": "357a6cfeffcac0560df03db6751fb1e35aef3070f6a027d0f71d569f9f3907bf", + "input__trimmed_mean__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "input__trimmed_mean__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "input__trimmed_mean__irregular__11": "90b41a79dd042fc9968e4542bcb8744ebc5ea3bcbc0215b3ca79337b059dc7e0", + "input__trimmed_mean__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "input__trimmed_mean__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "input__trimmed_mean__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "input__trimmed_mean__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "input__trimmed_mean__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "input__trimmed_mean__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "input__trimmed_mean__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "input__trimmed_mean__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "input__trimmed_mean__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "input__trimmed_mean__step__06": "d2af277d46bc5e074665ea196e664d21ad1cc891a96b4f84f9bc715dc40f0057", + "input__trimmed_mean__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "input__trimmed_mean__wide__14": "d6b0650aa9ab258fd54d7df089d829cf074e266af1b9c22b0638a5ea14311c8c", + "input__trimmed_mean_of_differences__alternating_offsets__02": "357a6cfeffcac0560df03db6751fb1e35aef3070f6a027d0f71d569f9f3907bf", + "input__trimmed_mean_of_differences__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "input__trimmed_mean_of_differences__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "input__trimmed_mean_of_differences__irregular__11": "90b41a79dd042fc9968e4542bcb8744ebc5ea3bcbc0215b3ca79337b059dc7e0", + "input__trimmed_mean_of_differences__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "input__trimmed_mean_of_differences__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "input__trimmed_mean_of_differences__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "input__trimmed_mean_of_differences__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "input__trimmed_mean_of_differences__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "input__trimmed_mean_of_differences__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "input__trimmed_mean_of_differences__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "input__trimmed_mean_of_differences__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "input__trimmed_mean_of_differences__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "input__trimmed_mean_of_differences__step__06": "d2af277d46bc5e074665ea196e664d21ad1cc891a96b4f84f9bc715dc40f0057", + "input__trimmed_mean_of_differences__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "input__trimmed_mean_of_differences__wide__14": "d6b0650aa9ab258fd54d7df089d829cf074e266af1b9c22b0638a5ea14311c8c", + "installed_background__median__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__median__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__median_of_differences__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__median_of_differences__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__trimmed_mean__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__trimmed_mean__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__trimmed_mean_of_differences__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_background__trimmed_mean_of_differences__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "installed_corrected__median__alternating_offsets__02": "61234be3d9301f80ddeb6c9a9a13123934657cba5ecb4202175a54d6dae60efe", + "installed_corrected__median__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "installed_corrected__median__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "installed_corrected__median__irregular__11": "9e7828288f37ca568f85366e21e29c8c814806eda24fe97afcf1297ca2cb172a", + "installed_corrected__median__linear__03": "06563cc7c8c90d58ecf2309509a1a315bc1b37dd854b181e927316f26181e27f", + "installed_corrected__median__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "installed_corrected__median__multimodal__09": "c2c8140cce2425b50592e5b02e802860018e1e82d84b5d44d1a2ffb5f2e88fe5", + "installed_corrected__median__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "installed_corrected__median__nonlinear__13": "4cc7882bbb8b3e92f75bacee75ed0d5dd9f4a37cae1be1bf6d3228166a761269", + "installed_corrected__median__plane__05": "7addfac9c462a047d7ab56e2199333741cf2a39679e7389841032cdb7fcc9ca0", + "installed_corrected__median__plateaus_signed_zero__10": "68df12fea4f5f3fd313bc4969b6c5ae0e9c69ad1df0fd1fd405a0eccdc97ba4f", + "installed_corrected__median__row_offsets__01": "7ab1578e2b5cd0a33868790e38a9c241949674e0fbb42000239525b2f26622cb", + "installed_corrected__median__scars__08": "aeec794980c492601fedb134023e9a0b7109b58cfb853ead18a7eed437700298", + "installed_corrected__median__step__06": "c70637dfda8c8a28434080e0bb0c95d0fd5355923aab02fd09c8e3aa83c5cc08", + "installed_corrected__median__tall__15": "979e082e8d0461e258217a631f4bb2d60a32701abd35adeadc833d1f7448eaf4", + "installed_corrected__median__wide__14": "5c02a5bdf8e3cfd840d88a81b9892413092cd3b6dfabbbccd35a0111fb6cc228", + "installed_corrected__median_of_differences__alternating_offsets__02": "94d2adc27887a0cb929a111683d05d048e1e9b5a2baedd542f6218fddfa951b4", + "installed_corrected__median_of_differences__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "installed_corrected__median_of_differences__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "installed_corrected__median_of_differences__irregular__11": "928ff5351111e14a0db920c86c2076ee3ef63f0032a9d84996d7b3ae282ca960", + "installed_corrected__median_of_differences__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "installed_corrected__median_of_differences__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "installed_corrected__median_of_differences__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "installed_corrected__median_of_differences__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "installed_corrected__median_of_differences__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "installed_corrected__median_of_differences__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "installed_corrected__median_of_differences__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "installed_corrected__median_of_differences__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "installed_corrected__median_of_differences__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "installed_corrected__median_of_differences__step__06": "85fabb79e3c47302e3406e2d06761008d6559e8c4ddc2a7125efa435d03478e4", + "installed_corrected__median_of_differences__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "installed_corrected__median_of_differences__wide__14": "b8e3f5a33d7da639d3288a249ea0019b174440c69e67f8218acfe17c98be3b05", + "installed_corrected__trimmed_mean__alternating_offsets__02": "61234be3d9301f80ddeb6c9a9a13123934657cba5ecb4202175a54d6dae60efe", + "installed_corrected__trimmed_mean__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "installed_corrected__trimmed_mean__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "installed_corrected__trimmed_mean__irregular__11": "9e7828288f37ca568f85366e21e29c8c814806eda24fe97afcf1297ca2cb172a", + "installed_corrected__trimmed_mean__linear__03": "08643ee6e1365003e650b4dca6e524edbbcae65fccaa83875d705fd59807dc20", + "installed_corrected__trimmed_mean__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "installed_corrected__trimmed_mean__multimodal__09": "bd2bd5f4e6e349d9169b2bab1091558a0fc50f178d37842d13ed05a56a80d3f4", + "installed_corrected__trimmed_mean__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "installed_corrected__trimmed_mean__nonlinear__13": "ad40ebce383b2c9bc80fb7fb841633016f5ffa7117a130769c620f1b9feba558", + "installed_corrected__trimmed_mean__plane__05": "7addfac9c462a047d7ab56e2199333741cf2a39679e7389841032cdb7fcc9ca0", + "installed_corrected__trimmed_mean__plateaus_signed_zero__10": "784dc81f68fb2e2325ada327ec49c93ffcdaaaf8498d5aac822a8a765b66ba7e", + "installed_corrected__trimmed_mean__row_offsets__01": "02778f1a7461b3b70ab6f82a9cd19893b8d49e4b2f7ff42e5b88a14599f7945c", + "installed_corrected__trimmed_mean__scars__08": "aeec794980c492601fedb134023e9a0b7109b58cfb853ead18a7eed437700298", + "installed_corrected__trimmed_mean__step__06": "334ca055c7b71e4ef33d44b98d2023362759294517bcbf44770311082e1f4e6c", + "installed_corrected__trimmed_mean__tall__15": "1d789d30ba5a12fd8d3c2b59258d15e2ccc38190a3d5f40f8a3b429e91e3501f", + "installed_corrected__trimmed_mean__wide__14": "5c02a5bdf8e3cfd840d88a81b9892413092cd3b6dfabbbccd35a0111fb6cc228", + "installed_corrected__trimmed_mean_of_differences__alternating_offsets__02": "94d2adc27887a0cb929a111683d05d048e1e9b5a2baedd542f6218fddfa951b4", + "installed_corrected__trimmed_mean_of_differences__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "installed_corrected__trimmed_mean_of_differences__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "installed_corrected__trimmed_mean_of_differences__irregular__11": "928ff5351111e14a0db920c86c2076ee3ef63f0032a9d84996d7b3ae282ca960", + "installed_corrected__trimmed_mean_of_differences__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "installed_corrected__trimmed_mean_of_differences__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "installed_corrected__trimmed_mean_of_differences__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "installed_corrected__trimmed_mean_of_differences__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "installed_corrected__trimmed_mean_of_differences__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "installed_corrected__trimmed_mean_of_differences__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "installed_corrected__trimmed_mean_of_differences__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "installed_corrected__trimmed_mean_of_differences__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "installed_corrected__trimmed_mean_of_differences__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "installed_corrected__trimmed_mean_of_differences__step__06": "85fabb79e3c47302e3406e2d06761008d6559e8c4ddc2a7125efa435d03478e4", + "installed_corrected__trimmed_mean_of_differences__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "installed_corrected__trimmed_mean_of_differences__wide__14": "b8e3f5a33d7da639d3288a249ea0019b174440c69e67f8218acfe17c98be3b05", + "mask__median__alternating_offsets__02": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__median__impulses__07": "a1e6f7825ecf367c48a842a75009c3b80f539f15fd6065f1253e57e8f99ee50f", + "mask__median__irregular__11": "f451b46961e212eb73e212ae2a17b2850d05cc6f329ad865ac6a50eff607b422", + "mask__median__linear__03": "ab4302588815a75df4757d07e679699c83c6d798879935bebdd64ac49964176e", + "mask__median__linear__12": "ec8d5aa71ca4cca07a3343d31b0836cb9b18fedc51ea47e42a2c30b1d723025e", + "mask__median__multimodal__09": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__median__nonlinear__04": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "mask__median__nonlinear__13": "1e0feb0bc85f55e91e89430145467acf4779312af547004fab993ea165f87c8a", + "mask__median__plane__05": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__median__plateaus_signed_zero__10": "3fbc16fea18639d063c1f465df6424b5d75869c15978627ea98c018113590a83", + "mask__median__row_offsets__01": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__median__step__06": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__median__tall__15": "8b871ddffd98c092f975df14ed97b80e7f6fb9e6eae968a2483bfd6097f352d0", + "mask__median_of_differences__alternating_offsets__02": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__median_of_differences__impulses__07": "a1e6f7825ecf367c48a842a75009c3b80f539f15fd6065f1253e57e8f99ee50f", + "mask__median_of_differences__irregular__11": "f451b46961e212eb73e212ae2a17b2850d05cc6f329ad865ac6a50eff607b422", + "mask__median_of_differences__linear__03": "ab4302588815a75df4757d07e679699c83c6d798879935bebdd64ac49964176e", + "mask__median_of_differences__linear__12": "ec8d5aa71ca4cca07a3343d31b0836cb9b18fedc51ea47e42a2c30b1d723025e", + "mask__median_of_differences__multimodal__09": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__median_of_differences__nonlinear__04": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "mask__median_of_differences__nonlinear__13": "1e0feb0bc85f55e91e89430145467acf4779312af547004fab993ea165f87c8a", + "mask__median_of_differences__plane__05": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__median_of_differences__plateaus_signed_zero__10": "3fbc16fea18639d063c1f465df6424b5d75869c15978627ea98c018113590a83", + "mask__median_of_differences__row_offsets__01": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__median_of_differences__step__06": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__median_of_differences__tall__15": "8b871ddffd98c092f975df14ed97b80e7f6fb9e6eae968a2483bfd6097f352d0", + "mask__trimmed_mean__alternating_offsets__02": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__trimmed_mean__impulses__07": "a1e6f7825ecf367c48a842a75009c3b80f539f15fd6065f1253e57e8f99ee50f", + "mask__trimmed_mean__irregular__11": "f451b46961e212eb73e212ae2a17b2850d05cc6f329ad865ac6a50eff607b422", + "mask__trimmed_mean__linear__03": "ab4302588815a75df4757d07e679699c83c6d798879935bebdd64ac49964176e", + "mask__trimmed_mean__linear__12": "ec8d5aa71ca4cca07a3343d31b0836cb9b18fedc51ea47e42a2c30b1d723025e", + "mask__trimmed_mean__multimodal__09": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__trimmed_mean__nonlinear__04": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "mask__trimmed_mean__nonlinear__13": "1e0feb0bc85f55e91e89430145467acf4779312af547004fab993ea165f87c8a", + "mask__trimmed_mean__plane__05": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__trimmed_mean__plateaus_signed_zero__10": "3fbc16fea18639d063c1f465df6424b5d75869c15978627ea98c018113590a83", + "mask__trimmed_mean__row_offsets__01": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__trimmed_mean__step__06": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__trimmed_mean__tall__15": "8b871ddffd98c092f975df14ed97b80e7f6fb9e6eae968a2483bfd6097f352d0", + "mask__trimmed_mean_of_differences__alternating_offsets__02": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__trimmed_mean_of_differences__impulses__07": "a1e6f7825ecf367c48a842a75009c3b80f539f15fd6065f1253e57e8f99ee50f", + "mask__trimmed_mean_of_differences__irregular__11": "f451b46961e212eb73e212ae2a17b2850d05cc6f329ad865ac6a50eff607b422", + "mask__trimmed_mean_of_differences__linear__03": "ab4302588815a75df4757d07e679699c83c6d798879935bebdd64ac49964176e", + "mask__trimmed_mean_of_differences__linear__12": "ec8d5aa71ca4cca07a3343d31b0836cb9b18fedc51ea47e42a2c30b1d723025e", + "mask__trimmed_mean_of_differences__multimodal__09": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__trimmed_mean_of_differences__nonlinear__04": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "mask__trimmed_mean_of_differences__nonlinear__13": "1e0feb0bc85f55e91e89430145467acf4779312af547004fab993ea165f87c8a", + "mask__trimmed_mean_of_differences__plane__05": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__trimmed_mean_of_differences__plateaus_signed_zero__10": "3fbc16fea18639d063c1f465df6424b5d75869c15978627ea98c018113590a83", + "mask__trimmed_mean_of_differences__row_offsets__01": "8813bb2f612042c432b6d772ff65b124167f6a65d083b3ac95184fd2e660d5f4", + "mask__trimmed_mean_of_differences__step__06": "80606141f1fde982222a459ed69c7334baadb70a37d9877341964de603e109d2", + "mask__trimmed_mean_of_differences__tall__15": "8b871ddffd98c092f975df14ed97b80e7f6fb9e6eae968a2483bfd6097f352d0", + "portable_background__median__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__median__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__median_of_differences__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__median_of_differences__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__trimmed_mean__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__trimmed_mean__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__trimmed_mean_of_differences__constant__00": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_background__trimmed_mean_of_differences__impulses__07": "b17ffde1c70ab26d31f0b40d3b4cb8faf7784f84b726036eda5a8cad2a8e13bb", + "portable_corrected__median__alternating_offsets__02": "61234be3d9301f80ddeb6c9a9a13123934657cba5ecb4202175a54d6dae60efe", + "portable_corrected__median__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "portable_corrected__median__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "portable_corrected__median__irregular__11": "9e7828288f37ca568f85366e21e29c8c814806eda24fe97afcf1297ca2cb172a", + "portable_corrected__median__linear__03": "06563cc7c8c90d58ecf2309509a1a315bc1b37dd854b181e927316f26181e27f", + "portable_corrected__median__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "portable_corrected__median__multimodal__09": "c2c8140cce2425b50592e5b02e802860018e1e82d84b5d44d1a2ffb5f2e88fe5", + "portable_corrected__median__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "portable_corrected__median__nonlinear__13": "4cc7882bbb8b3e92f75bacee75ed0d5dd9f4a37cae1be1bf6d3228166a761269", + "portable_corrected__median__plane__05": "7addfac9c462a047d7ab56e2199333741cf2a39679e7389841032cdb7fcc9ca0", + "portable_corrected__median__plateaus_signed_zero__10": "0f1ef690046cfd1b5a2df56f734dfc73c303e0956eaffbced81cb560ae7aad5b", + "portable_corrected__median__row_offsets__01": "7ab1578e2b5cd0a33868790e38a9c241949674e0fbb42000239525b2f26622cb", + "portable_corrected__median__scars__08": "aeec794980c492601fedb134023e9a0b7109b58cfb853ead18a7eed437700298", + "portable_corrected__median__step__06": "c70637dfda8c8a28434080e0bb0c95d0fd5355923aab02fd09c8e3aa83c5cc08", + "portable_corrected__median__tall__15": "979e082e8d0461e258217a631f4bb2d60a32701abd35adeadc833d1f7448eaf4", + "portable_corrected__median__wide__14": "5c02a5bdf8e3cfd840d88a81b9892413092cd3b6dfabbbccd35a0111fb6cc228", + "portable_corrected__median_of_differences__alternating_offsets__02": "94d2adc27887a0cb929a111683d05d048e1e9b5a2baedd542f6218fddfa951b4", + "portable_corrected__median_of_differences__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "portable_corrected__median_of_differences__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "portable_corrected__median_of_differences__irregular__11": "0a934ba3d3bc59d1baec577bc0c74e420fd99c79747503ecfcde45a2bb8fdcf7", + "portable_corrected__median_of_differences__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "portable_corrected__median_of_differences__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "portable_corrected__median_of_differences__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "portable_corrected__median_of_differences__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "portable_corrected__median_of_differences__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "portable_corrected__median_of_differences__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "portable_corrected__median_of_differences__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "portable_corrected__median_of_differences__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "portable_corrected__median_of_differences__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "portable_corrected__median_of_differences__step__06": "85fabb79e3c47302e3406e2d06761008d6559e8c4ddc2a7125efa435d03478e4", + "portable_corrected__median_of_differences__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "portable_corrected__median_of_differences__wide__14": "b8e3f5a33d7da639d3288a249ea0019b174440c69e67f8218acfe17c98be3b05", + "portable_corrected__trimmed_mean__alternating_offsets__02": "61234be3d9301f80ddeb6c9a9a13123934657cba5ecb4202175a54d6dae60efe", + "portable_corrected__trimmed_mean__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "portable_corrected__trimmed_mean__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "portable_corrected__trimmed_mean__irregular__11": "9e7828288f37ca568f85366e21e29c8c814806eda24fe97afcf1297ca2cb172a", + "portable_corrected__trimmed_mean__linear__03": "08643ee6e1365003e650b4dca6e524edbbcae65fccaa83875d705fd59807dc20", + "portable_corrected__trimmed_mean__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "portable_corrected__trimmed_mean__multimodal__09": "bd2bd5f4e6e349d9169b2bab1091558a0fc50f178d37842d13ed05a56a80d3f4", + "portable_corrected__trimmed_mean__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "portable_corrected__trimmed_mean__nonlinear__13": "ad40ebce383b2c9bc80fb7fb841633016f5ffa7117a130769c620f1b9feba558", + "portable_corrected__trimmed_mean__plane__05": "7addfac9c462a047d7ab56e2199333741cf2a39679e7389841032cdb7fcc9ca0", + "portable_corrected__trimmed_mean__plateaus_signed_zero__10": "784dc81f68fb2e2325ada327ec49c93ffcdaaaf8498d5aac822a8a765b66ba7e", + "portable_corrected__trimmed_mean__row_offsets__01": "02778f1a7461b3b70ab6f82a9cd19893b8d49e4b2f7ff42e5b88a14599f7945c", + "portable_corrected__trimmed_mean__scars__08": "aeec794980c492601fedb134023e9a0b7109b58cfb853ead18a7eed437700298", + "portable_corrected__trimmed_mean__step__06": "334ca055c7b71e4ef33d44b98d2023362759294517bcbf44770311082e1f4e6c", + "portable_corrected__trimmed_mean__tall__15": "1d789d30ba5a12fd8d3c2b59258d15e2ccc38190a3d5f40f8a3b429e91e3501f", + "portable_corrected__trimmed_mean__wide__14": "5c02a5bdf8e3cfd840d88a81b9892413092cd3b6dfabbbccd35a0111fb6cc228", + "portable_corrected__trimmed_mean_of_differences__alternating_offsets__02": "94d2adc27887a0cb929a111683d05d048e1e9b5a2baedd542f6218fddfa951b4", + "portable_corrected__trimmed_mean_of_differences__constant__00": "4eda4b8ccbb316e4b0cfdb08428b80234eba9e7a2b2ad75b470e1884a46d4dca", + "portable_corrected__trimmed_mean_of_differences__impulses__07": "02e66377214151ca19419b134537b31cd89a4961486073e18f52fea91a186519", + "portable_corrected__trimmed_mean_of_differences__irregular__11": "0a934ba3d3bc59d1baec577bc0c74e420fd99c79747503ecfcde45a2bb8fdcf7", + "portable_corrected__trimmed_mean_of_differences__linear__03": "a57e5975b58f59b5dec4cf97843a2a7b6e9865ab1111735ed00250ac3f465ad7", + "portable_corrected__trimmed_mean_of_differences__linear__12": "7cc8792314d79a8aceb45ec0d38e78e66b96464c4befb26d628200eb023abe47", + "portable_corrected__trimmed_mean_of_differences__multimodal__09": "2ed0e7fd37cdf7300361874e5ae013283f07b6946031417dccd720f18aa6340d", + "portable_corrected__trimmed_mean_of_differences__nonlinear__04": "249f8b503536170dd2e60319d9d8443c6e86cb2f0fddf0da032f016378dfbcdc", + "portable_corrected__trimmed_mean_of_differences__nonlinear__13": "293b812f2c82c95d50d977fb2319e65fb2ff697b5c3289e0426ad5ab4770ebfb", + "portable_corrected__trimmed_mean_of_differences__plane__05": "fb09450ac88fb3df030531ae16503d47ed04d34f0d47a5c45a0d6e1e5433572e", + "portable_corrected__trimmed_mean_of_differences__plateaus_signed_zero__10": "d58704c1572e727abd8e1dc362875a554736a61832919243e88e281756224056", + "portable_corrected__trimmed_mean_of_differences__row_offsets__01": "79ff6e8d2cbed5e71aff4bb8a8dca49a3448ff892aa6e20a750df75b37e67743", + "portable_corrected__trimmed_mean_of_differences__scars__08": "2b11105397a9b6371bd9b91513f0237db84d2d55b7403b62c59ea68bfeb53f30", + "portable_corrected__trimmed_mean_of_differences__step__06": "85fabb79e3c47302e3406e2d06761008d6559e8c4ddc2a7125efa435d03478e4", + "portable_corrected__trimmed_mean_of_differences__tall__15": "ce87de967ff114fcdf07b36b380c47e6fe96af215869876d637fdc3c79bdcec5", + "portable_corrected__trimmed_mean_of_differences__wide__14": "b8e3f5a33d7da639d3288a249ea0019b174440c69e67f8218acfe17c98be3b05" + }, + "npz_filename": "align_rows_statistics_reference.npz", + "npz_sha256": "0098e804597440419fd1eea2914ddccd7bbb5412c8691a8e3c34f0538983119e" + }, + "method_counts": { + "Median": 16, + "Median of differences": 16, + "Trimmed mean": 16, + "Trimmed mean of differences": 16 + }, + "profiles": { + "installed_gwyddion_2_71_fast_math_profile": { + "build": { + "associative_reassociation": true, + "compiler": "GCC 16.1.1", + "fast_math": true, + "lto": true + }, + "campaign": "row_shift_statistics", + "canonical_reference_sha256": "e2fa6d094acc5ec04f87901aa345244f9d22e70577d25f963a8cdb74363e457e", + "description": "Frozen installed Gwyddion executable profile; secondary external evidence.", + "gwyddion_version": "2.71", + "module_sha256": "c21d52375807ae096e34a3469c2f20c4c66ea3197479e13215a6d7b9d465b451" + }, + "portable_source_semantics": { + "candidate_output_sha256": "7da7283019d698089d1a9cca4cec712860529fce42c2cb0752c2b136ecd1cb30", + "description": "Frozen independent V2 source-semantic oracle; primary production contract.", + "method_evidence": { + "Median": "ORACLE_CONFIRMED_NUMERIC_WITH_EXPLAINED_SIGNED_ZERO", + "Median of differences": "ORACLE_MISMATCHED", + "Trimmed mean": "ORACLE_CONFIRMED_BITWISE", + "Trimmed mean of differences": "ORACLE_MISMATCHED" + }, + "oracle_source_sha256": "06585bf85b58392640e5c185694a2b4e47742dae8969b65a689f1bb5c717b198" + } + }, + "schema_version": 1 +} diff --git a/tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.npz b/tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.npz new file mode 100644 index 0000000..80c4b62 Binary files /dev/null and b/tests/validation/fixtures/gwyddion/align_rows_statistics/align_rows_statistics_reference.npz differ diff --git a/tests/validation/fixtures/gwyddion/arc_revolution/gwyddion_2_71_directional.json b/tests/validation/fixtures/gwyddion/arc_revolution/gwyddion_2_71_directional.json new file mode 100644 index 0000000..cab1966 --- /dev/null +++ b/tests/validation/fixtures/gwyddion/arc_revolution/gwyddion_2_71_directional.json @@ -0,0 +1,102 @@ +{ + "acceptance": { + "background_max_abs_error": 5e-14, + "corrected_max_abs_error": 5e-14, + "reconstruction_max_abs_error": 5e-14 + }, + "artifacts": { + "array_canonical_sha256": { + "background_both_inverted": "3adb462bfcba10d75dd4c0416424db686959c392b3885dff8824ab0097d1f55b", + "background_both_normal": "931b156e5c8c58c24642e89573105531263d4534d795a8809edfccf8d2b6c163", + "background_horizontal_inverted": "9ffd9fc84037d3230be9a4bf3a353534833e57a186abfe2e759e8a9cb15b1b55", + "background_horizontal_normal": "a4e795a383f8e495edbca5b3b6a2fd903dd6739f9515169178a749ab86cab327", + "background_vertical_inverted": "df9f61f8ed98f63d8ec20905ecb56009a7f47945bda5d4805dd055a331fd9f56", + "background_vertical_normal": "6d4d08a08c1752aca685a452d1ec143d555471905c1bdcda48bbeba940b7cc30", + "corrected_both_inverted": "aff71f2fa45bb29b35c6c4cc0412263d0095732b222376f2291f3604b676a4cc", + "corrected_both_normal": "88d4553c6b95637decc3cc7841c56cd0bec556801c4712b09767b343f108953d", + "corrected_horizontal_inverted": "abb1ce32e2a6e68e5f7175306edc51de4cd23bccbc697df68b5fa5721853ab0b", + "corrected_horizontal_normal": "35cc88b2c8e2e5aa060d4446c459e8cda6c8e6eb9c1a00f4a7209422b24c6a60", + "corrected_vertical_inverted": "25249844f021891d31b2acea23d9ca256ed64a7c81c15999fd1fcf444075ee46", + "corrected_vertical_normal": "a3251acde29315ee05883a47ed53f0730f859239d6efc3a297303234d47baf83", + "input": "69146737b4b7cb993261a33fabaff6adf3f1e47eb07f61431a48ac1b31f725fd" + }, + "npz_filename": "gwyddion_2_71_directional.npz", + "npz_sha256": "50b263b8add97950ba1ef882f96d5ee3bc35001908c47a1dbb20b9428bc3bc5e" + }, + "cases": { + "both_inverted": { + "corrected_reference_valid": true, + "direction": "both", + "inverted": true + }, + "both_normal": { + "corrected_reference_valid": true, + "direction": "both", + "inverted": false + }, + "horizontal_inverted": { + "corrected_reference_valid": false, + "direction": "horizontal", + "inverted": true, + "known_reference_defect": "Gwyddion 2.71 returns before populating the corrected result after computing and sign-restoring the background." + }, + "horizontal_normal": { + "corrected_reference_valid": true, + "direction": "horizontal", + "inverted": false + }, + "vertical_inverted": { + "corrected_reference_valid": true, + "direction": "vertical", + "inverted": true + }, + "vertical_normal": { + "corrected_reference_valid": true, + "direction": "vertical", + "inverted": false + } + }, + "field": { + "dtype": "float64", + "height_unit": "unspecified synthetic units", + "shape": [ + 5, + 7 + ], + "xreal": 5.6, + "yreal": 6.5 + }, + "fixture_id": "gwyddion-2.71-arc-revolution-directional", + "known_reference_defects": { + "horizontal_inverted_corrected_result": { + "classification": "KNOWN_REFERENCE_DEFECT", + "reference_result_untouched": true, + "sentinel": 123456789.0, + "spmkit_policy": "Preserve the validated background but return the scientifically consistent corrected field." + }, + "single_sample_processing_axis": { + "classification": "KNOWN_REFERENCE_DEFECT", + "reference_behavior": "The historical moving-sums control flow can read before its output buffer.", + "spmkit_policy": "Define a one-sample processing axis as identity." + } + }, + "parameters": { + "composition": { + "both": "horizontal followed by vertical", + "inverted": "negate input, process, negate background", + "vertical": "transpose, horizontal, transpose back" + }, + "radius_px": 2.5 + }, + "reference": { + "operation": "Revolve Arc", + "probe_output_sha256": "1f8ee0535ac3b0d93e3b330ec4f96b39436e4da1853f5d3ce9ba45e1f2d0eca3", + "probe_source": "gwyddion-2.71/arc-revolve-parity/arc_revolve_behavior_probe.c", + "probe_source_sha256": "27e92376d7955f134a6d76091775dc28fe2e1ba8246936b27e2e924d3ba765f4", + "software": "Gwyddion", + "source_file": "gwyddion-2.71/source/modules/process/arc-revolve.c", + "source_sha256": "afb19a2382b0abb46595fa3dabc126ade50ec31c91ec9c96ea2284f42d0a67ac", + "version": "2.71" + }, + "schema_version": 1 +} diff --git a/tests/validation/fixtures/gwyddion/arc_revolution/gwyddion_2_71_directional.npz b/tests/validation/fixtures/gwyddion/arc_revolution/gwyddion_2_71_directional.npz new file mode 100644 index 0000000..b7f5d81 Binary files /dev/null and b/tests/validation/fixtures/gwyddion/arc_revolution/gwyddion_2_71_directional.npz differ diff --git a/tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.json b/tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.json new file mode 100644 index 0000000..2540a8d --- /dev/null +++ b/tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.json @@ -0,0 +1,751 @@ +{ + "capability": "gwyddion_filter_flat_disc_morphology", + "cases": [ + { + "case_id": "singleton_1x1", + "input_key": "input__singleton_1x1", + "shape": [ + 1, + 1 + ], + "sizes": [ + { + "closing_key": "closing__singleton_1x1__2", + "opening_key": "opening__singleton_1x1__2", + "size_px": 2 + }, + { + "closing_key": "closing__singleton_1x1__3", + "opening_key": "opening__singleton_1x1__3", + "size_px": 3 + }, + { + "closing_key": "closing__singleton_1x1__4", + "opening_key": "opening__singleton_1x1__4", + "size_px": 4 + }, + { + "closing_key": "closing__singleton_1x1__5", + "opening_key": "opening__singleton_1x1__5", + "size_px": 5 + }, + { + "closing_key": "closing__singleton_1x1__30", + "opening_key": "opening__singleton_1x1__30", + "size_px": 30 + }, + { + "closing_key": "closing__singleton_1x1__31", + "opening_key": "opening__singleton_1x1__31", + "size_px": 31 + } + ] + }, + { + "case_id": "singleton_row_1x7", + "input_key": "input__singleton_row_1x7", + "shape": [ + 1, + 7 + ], + "sizes": [ + { + "closing_key": "closing__singleton_row_1x7__2", + "opening_key": "opening__singleton_row_1x7__2", + "size_px": 2 + }, + { + "closing_key": "closing__singleton_row_1x7__3", + "opening_key": "opening__singleton_row_1x7__3", + "size_px": 3 + }, + { + "closing_key": "closing__singleton_row_1x7__4", + "opening_key": "opening__singleton_row_1x7__4", + "size_px": 4 + }, + { + "closing_key": "closing__singleton_row_1x7__5", + "opening_key": "opening__singleton_row_1x7__5", + "size_px": 5 + }, + { + "closing_key": "closing__singleton_row_1x7__30", + "opening_key": "opening__singleton_row_1x7__30", + "size_px": 30 + }, + { + "closing_key": "closing__singleton_row_1x7__31", + "opening_key": "opening__singleton_row_1x7__31", + "size_px": 31 + } + ] + }, + { + "case_id": "singleton_column_7x1", + "input_key": "input__singleton_column_7x1", + "shape": [ + 7, + 1 + ], + "sizes": [ + { + "closing_key": "closing__singleton_column_7x1__2", + "opening_key": "opening__singleton_column_7x1__2", + "size_px": 2 + }, + { + "closing_key": "closing__singleton_column_7x1__3", + "opening_key": "opening__singleton_column_7x1__3", + "size_px": 3 + }, + { + "closing_key": "closing__singleton_column_7x1__4", + "opening_key": "opening__singleton_column_7x1__4", + "size_px": 4 + }, + { + "closing_key": "closing__singleton_column_7x1__5", + "opening_key": "opening__singleton_column_7x1__5", + "size_px": 5 + }, + { + "closing_key": "closing__singleton_column_7x1__30", + "opening_key": "opening__singleton_column_7x1__30", + "size_px": 30 + }, + { + "closing_key": "closing__singleton_column_7x1__31", + "opening_key": "opening__singleton_column_7x1__31", + "size_px": 31 + } + ] + }, + { + "case_id": "wide_large_gradient", + "input_key": "input__wide_large_gradient", + "shape": [ + 35, + 39 + ], + "sizes": [ + { + "closing_key": "closing__wide_large_gradient__2", + "opening_key": "opening__wide_large_gradient__2", + "size_px": 2 + }, + { + "closing_key": "closing__wide_large_gradient__3", + "opening_key": "opening__wide_large_gradient__3", + "size_px": 3 + }, + { + "closing_key": "closing__wide_large_gradient__4", + "opening_key": "opening__wide_large_gradient__4", + "size_px": 4 + }, + { + "closing_key": "closing__wide_large_gradient__5", + "opening_key": "opening__wide_large_gradient__5", + "size_px": 5 + }, + { + "closing_key": "closing__wide_large_gradient__30", + "opening_key": "opening__wide_large_gradient__30", + "size_px": 30 + }, + { + "closing_key": "closing__wide_large_gradient__31", + "opening_key": "opening__wide_large_gradient__31", + "size_px": 31 + } + ] + }, + { + "case_id": "tall_large_gradient", + "input_key": "input__tall_large_gradient", + "shape": [ + 39, + 35 + ], + "sizes": [ + { + "closing_key": "closing__tall_large_gradient__2", + "opening_key": "opening__tall_large_gradient__2", + "size_px": 2 + }, + { + "closing_key": "closing__tall_large_gradient__3", + "opening_key": "opening__tall_large_gradient__3", + "size_px": 3 + }, + { + "closing_key": "closing__tall_large_gradient__4", + "opening_key": "opening__tall_large_gradient__4", + "size_px": 4 + }, + { + "closing_key": "closing__tall_large_gradient__5", + "opening_key": "opening__tall_large_gradient__5", + "size_px": 5 + }, + { + "closing_key": "closing__tall_large_gradient__30", + "opening_key": "opening__tall_large_gradient__30", + "size_px": 30 + }, + { + "closing_key": "closing__tall_large_gradient__31", + "opening_key": "opening__tall_large_gradient__31", + "size_px": 31 + } + ] + }, + { + "case_id": "constant_nonzero", + "input_key": "input__constant_nonzero", + "shape": [ + 4, + 6 + ], + "sizes": [ + { + "closing_key": "closing__constant_nonzero__2", + "opening_key": "opening__constant_nonzero__2", + "size_px": 2 + }, + { + "closing_key": "closing__constant_nonzero__3", + "opening_key": "opening__constant_nonzero__3", + "size_px": 3 + }, + { + "closing_key": "closing__constant_nonzero__4", + "opening_key": "opening__constant_nonzero__4", + "size_px": 4 + }, + { + "closing_key": "closing__constant_nonzero__5", + "opening_key": "opening__constant_nonzero__5", + "size_px": 5 + }, + { + "closing_key": "closing__constant_nonzero__30", + "opening_key": "opening__constant_nonzero__30", + "size_px": 30 + }, + { + "closing_key": "closing__constant_nonzero__31", + "opening_key": "opening__constant_nonzero__31", + "size_px": 31 + } + ] + }, + { + "case_id": "signed_monotonic", + "input_key": "input__signed_monotonic", + "shape": [ + 4, + 7 + ], + "sizes": [ + { + "closing_key": "closing__signed_monotonic__2", + "opening_key": "opening__signed_monotonic__2", + "size_px": 2 + }, + { + "closing_key": "closing__signed_monotonic__3", + "opening_key": "opening__signed_monotonic__3", + "size_px": 3 + }, + { + "closing_key": "closing__signed_monotonic__4", + "opening_key": "opening__signed_monotonic__4", + "size_px": 4 + }, + { + "closing_key": "closing__signed_monotonic__5", + "opening_key": "opening__signed_monotonic__5", + "size_px": 5 + }, + { + "closing_key": "closing__signed_monotonic__30", + "opening_key": "opening__signed_monotonic__30", + "size_px": 30 + }, + { + "closing_key": "closing__signed_monotonic__31", + "opening_key": "opening__signed_monotonic__31", + "size_px": 31 + } + ] + }, + { + "case_id": "checker_step", + "input_key": "input__checker_step", + "shape": [ + 6, + 8 + ], + "sizes": [ + { + "closing_key": "closing__checker_step__2", + "opening_key": "opening__checker_step__2", + "size_px": 2 + }, + { + "closing_key": "closing__checker_step__3", + "opening_key": "opening__checker_step__3", + "size_px": 3 + }, + { + "closing_key": "closing__checker_step__4", + "opening_key": "opening__checker_step__4", + "size_px": 4 + }, + { + "closing_key": "closing__checker_step__5", + "opening_key": "opening__checker_step__5", + "size_px": 5 + }, + { + "closing_key": "closing__checker_step__30", + "opening_key": "opening__checker_step__30", + "size_px": 30 + }, + { + "closing_key": "closing__checker_step__31", + "opening_key": "opening__checker_step__31", + "size_px": 31 + } + ] + }, + { + "case_id": "positive_impulse", + "input_key": "input__positive_impulse", + "shape": [ + 7, + 7 + ], + "sizes": [ + { + "closing_key": "closing__positive_impulse__2", + "opening_key": "opening__positive_impulse__2", + "size_px": 2 + }, + { + "closing_key": "closing__positive_impulse__3", + "opening_key": "opening__positive_impulse__3", + "size_px": 3 + }, + { + "closing_key": "closing__positive_impulse__4", + "opening_key": "opening__positive_impulse__4", + "size_px": 4 + }, + { + "closing_key": "closing__positive_impulse__5", + "opening_key": "opening__positive_impulse__5", + "size_px": 5 + }, + { + "closing_key": "closing__positive_impulse__30", + "opening_key": "opening__positive_impulse__30", + "size_px": 30 + }, + { + "closing_key": "closing__positive_impulse__31", + "opening_key": "opening__positive_impulse__31", + "size_px": 31 + } + ] + }, + { + "case_id": "negative_impulse", + "input_key": "input__negative_impulse", + "shape": [ + 7, + 7 + ], + "sizes": [ + { + "closing_key": "closing__negative_impulse__2", + "opening_key": "opening__negative_impulse__2", + "size_px": 2 + }, + { + "closing_key": "closing__negative_impulse__3", + "opening_key": "opening__negative_impulse__3", + "size_px": 3 + }, + { + "closing_key": "closing__negative_impulse__4", + "opening_key": "opening__negative_impulse__4", + "size_px": 4 + }, + { + "closing_key": "closing__negative_impulse__5", + "opening_key": "opening__negative_impulse__5", + "size_px": 5 + }, + { + "closing_key": "closing__negative_impulse__30", + "opening_key": "opening__negative_impulse__30", + "size_px": 30 + }, + { + "closing_key": "closing__negative_impulse__31", + "opening_key": "opening__negative_impulse__31", + "size_px": 31 + } + ] + }, + { + "case_id": "corner_edge_large_irregular", + "input_key": "input__corner_edge_large_irregular", + "shape": [ + 37, + 37 + ], + "sizes": [ + { + "closing_key": "closing__corner_edge_large_irregular__2", + "opening_key": "opening__corner_edge_large_irregular__2", + "size_px": 2 + }, + { + "closing_key": "closing__corner_edge_large_irregular__3", + "opening_key": "opening__corner_edge_large_irregular__3", + "size_px": 3 + }, + { + "closing_key": "closing__corner_edge_large_irregular__4", + "opening_key": "opening__corner_edge_large_irregular__4", + "size_px": 4 + }, + { + "closing_key": "closing__corner_edge_large_irregular__5", + "opening_key": "opening__corner_edge_large_irregular__5", + "size_px": 5 + }, + { + "closing_key": "closing__corner_edge_large_irregular__30", + "opening_key": "opening__corner_edge_large_irregular__30", + "size_px": 30 + }, + { + "closing_key": "closing__corner_edge_large_irregular__31", + "opening_key": "opening__corner_edge_large_irregular__31", + "size_px": 31 + } + ] + }, + { + "case_id": "plateau_signed_zero_irregular", + "input_key": "input__plateau_signed_zero_irregular", + "shape": [ + 6, + 6 + ], + "sizes": [ + { + "closing_key": "closing__plateau_signed_zero_irregular__2", + "opening_key": "opening__plateau_signed_zero_irregular__2", + "size_px": 2 + }, + { + "closing_key": "closing__plateau_signed_zero_irregular__3", + "opening_key": "opening__plateau_signed_zero_irregular__3", + "size_px": 3 + }, + { + "closing_key": "closing__plateau_signed_zero_irregular__4", + "opening_key": "opening__plateau_signed_zero_irregular__4", + "size_px": 4 + }, + { + "closing_key": "closing__plateau_signed_zero_irregular__5", + "opening_key": "opening__plateau_signed_zero_irregular__5", + "size_px": 5 + }, + { + "closing_key": "closing__plateau_signed_zero_irregular__30", + "opening_key": "opening__plateau_signed_zero_irregular__30", + "size_px": 30 + }, + { + "closing_key": "closing__plateau_signed_zero_irregular__31", + "opening_key": "opening__plateau_signed_zero_irregular__31", + "size_px": 31 + } + ] + } + ], + "display_name": "Gwyddion 2.71 Filter Flat-Disc Opening and Closing", + "evidence_roles": { + "external_gwyddion_outputs": "opening__* and closing__* arrays copied bitwise from canonical normal records", + "final_bitwise_agreement": "kernels 30/30; Opening 72/72; Closing 72/72", + "independently_derived_kernel_masks": "kernel__2 through kernel__31 derived from the frozen digital-ellipse rule", + "independently_regenerated_input_fields": "input__* arrays regenerated from frozen field provenance", + "oracle_outputs": "Oracle V2 agreed bitwise with every external operation output before fixture freeze" + }, + "external_evidence": { + "canonical_reference_sha256": "907bd347cc8c213d1061b786b6efe5692c87ffd8be62db1f6de2bd9bc78acdbd", + "provenance_sha256": "c3777cdcfdd868a705ef09c63a7548a5b1c0eb0b79d053e02ac2f45e9c97e5af" + }, + "fixture": { + "array_count": 186, + "array_hashes": { + "closing__checker_step__2": "549faf4f123fed4aa75090d49a29af404e5894caea1d1ba9accb136795607e66", + "closing__checker_step__3": "00177cea6073c7c1aef8caec87d45274802ed58d657f05c41f280901fec5a914", + "closing__checker_step__30": "c7605b54aec87be38b51b424273cbc38014c12be04f78d09b6816fc3ceeaa6d2", + "closing__checker_step__31": "c7605b54aec87be38b51b424273cbc38014c12be04f78d09b6816fc3ceeaa6d2", + "closing__checker_step__4": "00177cea6073c7c1aef8caec87d45274802ed58d657f05c41f280901fec5a914", + "closing__checker_step__5": "00177cea6073c7c1aef8caec87d45274802ed58d657f05c41f280901fec5a914", + "closing__constant_nonzero__2": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "closing__constant_nonzero__3": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "closing__constant_nonzero__30": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "closing__constant_nonzero__31": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "closing__constant_nonzero__4": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "closing__constant_nonzero__5": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "closing__corner_edge_large_irregular__2": "30d788eb30c28fdc14865cd57a223de101b525ccf8e0bd4c4ef0292f8165af16", + "closing__corner_edge_large_irregular__3": "d564d7850db6827ba3fbbed2fe07735aa5668259052b776c79473bb75fa8dde0", + "closing__corner_edge_large_irregular__30": "5350879dff7b92f4157021827ac241652b34decb362228c7c164be5d3a8fc694", + "closing__corner_edge_large_irregular__31": "76151275f43aecc801213fde0071e1cca128b0b491d5a31fef4c1981468d591c", + "closing__corner_edge_large_irregular__4": "aec763989f7c5c535d4b1e1a66054952284f5c271aee69f879244b8bdf33ec23", + "closing__corner_edge_large_irregular__5": "daa3ce3051807673a70435a81a84843614be3b09dc797cd7a2bbc3ba22d5f083", + "closing__negative_impulse__2": "33e796f5927b062c3093049847baa9a1747f871ef8ecc380e119d5ce7e0235ca", + "closing__negative_impulse__3": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "closing__negative_impulse__30": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "closing__negative_impulse__31": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "closing__negative_impulse__4": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "closing__negative_impulse__5": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "closing__plateau_signed_zero_irregular__2": "35de89582f988c2c742c1cd318e43e36be7ac7c438255fb6a9b8d58a6a651509", + "closing__plateau_signed_zero_irregular__3": "8c4d247411130e8b14b12d00f3f83b398f4bf51d81dcbe44cbe2c0f372dfaff7", + "closing__plateau_signed_zero_irregular__30": "780be79a7e2f743626bcdc61491d302ab66ef7c65f911655083135fb9d698c16", + "closing__plateau_signed_zero_irregular__31": "780be79a7e2f743626bcdc61491d302ab66ef7c65f911655083135fb9d698c16", + "closing__plateau_signed_zero_irregular__4": "314a7ffcc66e2fd62d31f198bbe4232088b52204210ed70cc531d819ccb58c71", + "closing__plateau_signed_zero_irregular__5": "b6ca05886b04ffb4355809f7d12d038979484108822c7e548ad152df1511295d", + "closing__positive_impulse__2": "d2ded403737d66c677cdb6ddf4e6f23e653cf0db0ac122689ad5cd13567d93af", + "closing__positive_impulse__3": "d2ded403737d66c677cdb6ddf4e6f23e653cf0db0ac122689ad5cd13567d93af", + "closing__positive_impulse__30": "1bbb1f0181809223f299c464be224d16812e2096df00ff229dfec1c23edf4bb9", + "closing__positive_impulse__31": "1bbb1f0181809223f299c464be224d16812e2096df00ff229dfec1c23edf4bb9", + "closing__positive_impulse__4": "d2ded403737d66c677cdb6ddf4e6f23e653cf0db0ac122689ad5cd13567d93af", + "closing__positive_impulse__5": "d2ded403737d66c677cdb6ddf4e6f23e653cf0db0ac122689ad5cd13567d93af", + "closing__signed_monotonic__2": "af4ab808820ac2155947d283d6fe21071ffe7f429e86e71f68fd3208d26ba578", + "closing__signed_monotonic__3": "03ee92fda9e395956ac96b8b05157de03757720086db9786d6b2bfce348e9a6f", + "closing__signed_monotonic__30": "8959adcac1341add2c49169d03667e6ff2052cd523bfd406e33ffbe281a4c9a8", + "closing__signed_monotonic__31": "8959adcac1341add2c49169d03667e6ff2052cd523bfd406e33ffbe281a4c9a8", + "closing__signed_monotonic__4": "b8ed900f335f6793043dff898a4c67152d9afcbab032597bfa5dcc8eef50be1e", + "closing__signed_monotonic__5": "f83360854be41f1e1a04d31bbb2e293153bb5c97db606b019c6126c5a1a990a1", + "closing__singleton_1x1__2": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "closing__singleton_1x1__3": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "closing__singleton_1x1__30": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "closing__singleton_1x1__31": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "closing__singleton_1x1__4": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "closing__singleton_1x1__5": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "closing__singleton_column_7x1__2": "3b8628be65941ecee0a1db198b5f74b886f46b3533ddaa3c4f4e863163ac760d", + "closing__singleton_column_7x1__3": "638cea73109bdeefe6aaeb42b79f504d5292bf197d06c17e747fdbbe5fe1e96a", + "closing__singleton_column_7x1__30": "a6e67ec9314298513a44ebca1df1ed53e2cfe864c01726de0da46c4eca3b07f6", + "closing__singleton_column_7x1__31": "a6e67ec9314298513a44ebca1df1ed53e2cfe864c01726de0da46c4eca3b07f6", + "closing__singleton_column_7x1__4": "638cea73109bdeefe6aaeb42b79f504d5292bf197d06c17e747fdbbe5fe1e96a", + "closing__singleton_column_7x1__5": "638cea73109bdeefe6aaeb42b79f504d5292bf197d06c17e747fdbbe5fe1e96a", + "closing__singleton_row_1x7__2": "ec64dc8cdb866d776124e7b1ba4dc738dbd0ac7bf7125a57874f50f59e1fbe1f", + "closing__singleton_row_1x7__3": "5f0cf54986d51349a9e2222343234a6150c3b951f573ba216ce99177050d6ffa", + "closing__singleton_row_1x7__30": "8fb04fdba76775b3f1d4665e0d54af74642f533ef253f3c8b85cea4645d1cade", + "closing__singleton_row_1x7__31": "8fb04fdba76775b3f1d4665e0d54af74642f533ef253f3c8b85cea4645d1cade", + "closing__singleton_row_1x7__4": "ec64dc8cdb866d776124e7b1ba4dc738dbd0ac7bf7125a57874f50f59e1fbe1f", + "closing__singleton_row_1x7__5": "880142629b49de6c81553da4bd885ff4b528ef303d349c027acb5beec5f16d8e", + "closing__tall_large_gradient__2": "7f68eaee56f96fc0aa421abc8a558ac0c6862c79b186c3716b0c066db58e058b", + "closing__tall_large_gradient__3": "3802a4db20fab3c87830454b5f04c638248392ddc9067b2d373ae4bb82e96e81", + "closing__tall_large_gradient__30": "7e4fda56d5afaab125afddba11c2ff7409471e0d9951eb2310d75d2e5316f1ff", + "closing__tall_large_gradient__31": "c55d1ae466f23dbb6423c0eb6934c46d46389e3e34e2e5990ef39f2a684b7e66", + "closing__tall_large_gradient__4": "e9ad8eb63810e87e48ed7008fd30532dcce2926c0b76ce88289feda761d8192f", + "closing__tall_large_gradient__5": "188ebc037d1cd3607984fdf75c0b2b58cc42cef9684c32519f71a3a014648c11", + "closing__wide_large_gradient__2": "ee5e1d0b350d52ee11f4136d259364c1a24be9db926f3bf8420f7ce32f5c2e25", + "closing__wide_large_gradient__3": "0740e5ae87db3729b88ecee4970d4d3c73f46da29643271ebb7d4abcbcdf3cfa", + "closing__wide_large_gradient__30": "fbd533fdb7ca5ceba54f0ca6f5c34769f1f7be80fc1a136bd92d7d9264034aad", + "closing__wide_large_gradient__31": "fc293e98538b12616575d6ab5653b18ff1f8a7a75b70c8c5477c24c0fa53fe28", + "closing__wide_large_gradient__4": "216dc6378a409bc61c6625b579bc59dfdd8526cf6de74b4acbb8866be3f03234", + "closing__wide_large_gradient__5": "56551dff49fcbc35ea761879e2c6c82f78b4ba98b2ff298b64a2ac78b128113d", + "input__checker_step": "10d0e45c0cee7e54cae872e70cc1f2da9abfa23110b94ea78554c593e27e3c68", + "input__constant_nonzero": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "input__corner_edge_large_irregular": "c65221e19a3f07e0f27701f0894284efa06e6f4170f81d3c7ad0e2fd5e99ea77", + "input__negative_impulse": "2095e704f85c2fb2bda5201c43521b634e6755b9b8acae6b3c90e886bb5a2d0d", + "input__plateau_signed_zero_irregular": "56345c343c12cf83f4b20665b0c47eae34a7816cd7345933f7474454c3eb36eb", + "input__positive_impulse": "d2ded403737d66c677cdb6ddf4e6f23e653cf0db0ac122689ad5cd13567d93af", + "input__signed_monotonic": "af4ab808820ac2155947d283d6fe21071ffe7f429e86e71f68fd3208d26ba578", + "input__singleton_1x1": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input__singleton_column_7x1": "46ad2761807736956fb408501485f5da0d73240a4247578ef7458fb9aaa804f3", + "input__singleton_row_1x7": "7f069ded918cb1c64e81b0d3c624eb3255498176befd8502593e17e8684dc5ab", + "input__tall_large_gradient": "7f68eaee56f96fc0aa421abc8a558ac0c6862c79b186c3716b0c066db58e058b", + "input__wide_large_gradient": "ee5e1d0b350d52ee11f4136d259364c1a24be9db926f3bf8420f7ce32f5c2e25", + "kernel__10": "5c70caa3ba2e6af3ed08232ed54240877a2ab2c9c60346f38401270b886f4f1d", + "kernel__11": "43d0d734008f7d8563be5e615d2ffb988dc524e009a4bd81d70bb37f8a40510a", + "kernel__12": "02f34f132d062512c439053764974fe4b2838578dc283311d93dde5f838456bb", + "kernel__13": "b2ff89cf2be45056773496e642aaab203ac62a140a6820ec91297625e37dd220", + "kernel__14": "bbea56dc715cffc1bfcd65a5798e99fb369b9d1ee0b516f144d7908547b0695f", + "kernel__15": "53acff94afe7b09554bedf0bc9dd309813d92c1ca96ee2c225c27ca6e1646207", + "kernel__16": "45fe81ab93f7e7d7cd747de2b832549c9b89bf6826fc34f99ad087604dc24d40", + "kernel__17": "07548696aea5a8f66ee5cc355873e8798576a34ba1247c66bb3ded63b46ff959", + "kernel__18": "6c9b729120fa04e0ef1cb168a84bd17b6bf06e00e87b00b1eba7587fcd0da3b1", + "kernel__19": "ecf1fe17f401651a4d364d6a12c4048d95cae3df742a3ba6df7047321009e4be", + "kernel__2": "d4179f7562080b20aa5758e8f77f7de2390b33886e4cddd798baafdf0bd8181d", + "kernel__20": "f1bdfd0eb3c7100d1cfabeb9a8c8bde5af05b676f9201c8861b6add2f7577306", + "kernel__21": "83c2e4242be54d74cdd01d4bf6971aa45b57c6d323b52095c79b0e97117341f4", + "kernel__22": "44b82fdaec19762a954fd5f6ca935e16a1cf3c2f7bf99c8e905687d842b81939", + "kernel__23": "8930b0c95e940f4bddbe48da7c82e7fc4c8ff8bc384f22f90b4ca8f1ac9ee95b", + "kernel__24": "7a107d522c9c0c5dd893d12a99d52f86bcf4e2c85555f42cfa8db7e5dfd1e1db", + "kernel__25": "1000540487c0356dfdc67fa37971459086ec0f3c3b387e177a7d42ad4883c61f", + "kernel__26": "dc1d314252a2bc35b31b85739ecb8bd4e4cae4afddc02537686cbc3cfdb461ef", + "kernel__27": "528ed618156c27d796f338720c0617792b33ae51239bf64e3baae6d37165256a", + "kernel__28": "74aa89788ebbb447a13d7bf330600731df45110aa5d78f06c1ccd14b0d941836", + "kernel__29": "a641959b84f07ca1d0da938841e9ac7ef6ae32513c0b825fea9970e94ad8c22c", + "kernel__3": "3ddc8773256fdade5c945f5bef0182bd29ff8cb0880f3850e5a27d37f797be20", + "kernel__30": "4060bc6edc617d162096c1a2440821e061f61193289c1a0068e99329e2848288", + "kernel__31": "04c7b79f82b1c0e0c68b3da94e33e9f8537cd555b4bc90ee602df85f064100e3", + "kernel__4": "a73d20c787bdf5392a6fd8edfbe76dec3938661cb51fca4f48398be9bbf8b49c", + "kernel__5": "9084089098214cf292025671cdb3caf7ed7fa23d0bab7284d719ba1a96af450b", + "kernel__6": "3d350c6e17e741516f2fd0beaad2bf6721ab953c4174ec96f08798d5c632f9a3", + "kernel__7": "8ba9c1acba082ff24ad22770b59996798b8916efc0d3e39b1c9ffd5be06a9787", + "kernel__8": "0af9a49e943bae75644c5cf347b5f18701c18e8cbd410372518f0b73c3d9ce91", + "kernel__9": "3eca03815aa18f289d2576af932c27e2d0d8d09a255675df01b9e52f6287748b", + "opening__checker_step__2": "0c07891198c9bb1dce2cafdab7033ab8cd71c7c8e6d99dc166433b14a72cd69d", + "opening__checker_step__3": "ae9e571893791b924efe61ccea35a121f28e19342e937764a7f30c885c4a5cea", + "opening__checker_step__30": "ab77e9652a2393ec9753e0657dd193dc63fc01fd52c729a3430fc2365da2cab7", + "opening__checker_step__31": "ab77e9652a2393ec9753e0657dd193dc63fc01fd52c729a3430fc2365da2cab7", + "opening__checker_step__4": "0c07891198c9bb1dce2cafdab7033ab8cd71c7c8e6d99dc166433b14a72cd69d", + "opening__checker_step__5": "ae9e571893791b924efe61ccea35a121f28e19342e937764a7f30c885c4a5cea", + "opening__constant_nonzero__2": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "opening__constant_nonzero__3": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "opening__constant_nonzero__30": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "opening__constant_nonzero__31": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "opening__constant_nonzero__4": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "opening__constant_nonzero__5": "dae580a20e0027b3071e62a048ab15a1e7546ba9db97a8db66a8c637e8144302", + "opening__corner_edge_large_irregular__2": "05c2b74c9feef6179fbefdcb3c424f932a94400db35230433ba29a4e3acf624f", + "opening__corner_edge_large_irregular__3": "d6317d7b4bb1b6c99b701fa88fe44524726e05bf49328c428a174c2df73193fc", + "opening__corner_edge_large_irregular__30": "c0bac360fc2a7942309334aa50783919aa46cc7580082cdacb61147128197293", + "opening__corner_edge_large_irregular__31": "545ac6253a65d37488d01371f13ddf8a87ba684073ee25ced462899e70097a2d", + "opening__corner_edge_large_irregular__4": "5d497cce0eff2fcfdac3955342462271914e322b5726e27f6ab7bec293022c9b", + "opening__corner_edge_large_irregular__5": "8177a29e983b3de0bfc2f90ba0337a64f37efd1c5cc9aff57d5b0640c777b260", + "opening__negative_impulse__2": "2095e704f85c2fb2bda5201c43521b634e6755b9b8acae6b3c90e886bb5a2d0d", + "opening__negative_impulse__3": "2095e704f85c2fb2bda5201c43521b634e6755b9b8acae6b3c90e886bb5a2d0d", + "opening__negative_impulse__30": "c14be15f646a1a7525df6a453588f7915272720df730751ccc2369e6640918bb", + "opening__negative_impulse__31": "c14be15f646a1a7525df6a453588f7915272720df730751ccc2369e6640918bb", + "opening__negative_impulse__4": "2095e704f85c2fb2bda5201c43521b634e6755b9b8acae6b3c90e886bb5a2d0d", + "opening__negative_impulse__5": "2095e704f85c2fb2bda5201c43521b634e6755b9b8acae6b3c90e886bb5a2d0d", + "opening__plateau_signed_zero_irregular__2": "c1f17f225427e1d05943ec639e6a098532cdc59c604d6d5546e3a69046842d96", + "opening__plateau_signed_zero_irregular__3": "169f330642c47c2b5226f0d9a03ee5d7332e606ac5f86b4beee8d0e4250f9509", + "opening__plateau_signed_zero_irregular__30": "ca838e3d3ba38029761bf68da23a0136a73514928dc162b0733db65cf4452f8e", + "opening__plateau_signed_zero_irregular__31": "ca838e3d3ba38029761bf68da23a0136a73514928dc162b0733db65cf4452f8e", + "opening__plateau_signed_zero_irregular__4": "d786aa450bfe27248e22bb2dd4787b10261e2a475d125490bd783de8b31f13b4", + "opening__plateau_signed_zero_irregular__5": "049473bb0e3f3422c1b3c011d456356f70991b87b010444b799c4c0c0faa6e94", + "opening__positive_impulse__2": "335583a571361ab9904774510167583c48ab75f30813d80d6693e30ac69b6126", + "opening__positive_impulse__3": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "opening__positive_impulse__30": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "opening__positive_impulse__31": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "opening__positive_impulse__4": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "opening__positive_impulse__5": "62240ab337c97bb1aa1bb839b004903cf8cdc0e4e9de20f1a76d318eb6905ed2", + "opening__signed_monotonic__2": "af4ab808820ac2155947d283d6fe21071ffe7f429e86e71f68fd3208d26ba578", + "opening__signed_monotonic__3": "69dbaeab3157138b23bceeba6bf90063bbd9a85280660e57cb2217316afaf979", + "opening__signed_monotonic__30": "8d3e394a381329abe9629998aafe02e9fd773a30a46fbb8a9083f55276edee1e", + "opening__signed_monotonic__31": "8d3e394a381329abe9629998aafe02e9fd773a30a46fbb8a9083f55276edee1e", + "opening__signed_monotonic__4": "2dd5931263888cdec9ceb257cf6bbe509dfcde78999f74e62c27885502e126d3", + "opening__signed_monotonic__5": "cd704a6e56172f084f114a55967085576ca8aefc0a280e60213d207fae793c46", + "opening__singleton_1x1__2": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "opening__singleton_1x1__3": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "opening__singleton_1x1__30": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "opening__singleton_1x1__31": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "opening__singleton_1x1__4": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "opening__singleton_1x1__5": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "opening__singleton_column_7x1__2": "2bfc1127cd5e93cc78476d75d6d6a90e4640aa259bcd65010aba8b9730814d17", + "opening__singleton_column_7x1__3": "dbe9110f97777b6b19ad78c4d588f0dba77388aab20e34b15c5c6a19006dafa9", + "opening__singleton_column_7x1__30": "1520e3230762667c58530f3fc3ca081bf101ad1664c1c8bf3f7570cd1c72acd8", + "opening__singleton_column_7x1__31": "1520e3230762667c58530f3fc3ca081bf101ad1664c1c8bf3f7570cd1c72acd8", + "opening__singleton_column_7x1__4": "47cbefedcedc22d64d4fc36edfef2c8931d9db4de2c836281b9a85fb6878c530", + "opening__singleton_column_7x1__5": "9dd8b068fad65ebdccd934c8732464bd6428649b79add9b5c439cc6ccec1d3b0", + "opening__singleton_row_1x7__2": "3f056136013619ca2212d2b39b52746919a5d2bcc0e1b97a7e8a63e44734078c", + "opening__singleton_row_1x7__3": "a7a0701a59476221b8a4f64eefbaaf7c9be9b6f99c0cad06ace821064ca6f999", + "opening__singleton_row_1x7__30": "5676978c6bad3f9e0cf6a8569ed772ecff878f39ea67744c0450944d943840b1", + "opening__singleton_row_1x7__31": "5676978c6bad3f9e0cf6a8569ed772ecff878f39ea67744c0450944d943840b1", + "opening__singleton_row_1x7__4": "0573eb990c9972d8d0d72e341b525ae85a9119f442487c53fe94d78506ad72a9", + "opening__singleton_row_1x7__5": "0573eb990c9972d8d0d72e341b525ae85a9119f442487c53fe94d78506ad72a9", + "opening__tall_large_gradient__2": "7f68eaee56f96fc0aa421abc8a558ac0c6862c79b186c3716b0c066db58e058b", + "opening__tall_large_gradient__3": "1b7fa61d9665dc1d6e7a2c1d8f5cce9036936f654fdc49518b946bde730b4a44", + "opening__tall_large_gradient__30": "62f00af3fa414fc6117a257a2983e23c888f3bf448b9ef5887f9b289a44c88bd", + "opening__tall_large_gradient__31": "6c6913d38f16b6d30a9477134f81dcdc3d85806908757007ff32e37927e35fdf", + "opening__tall_large_gradient__4": "772e3cb4b2df37da6d478a349e461f9f8958a1643aa90e3df39f9395a56d262e", + "opening__tall_large_gradient__5": "068b5174c6d476fbcd8c7b47f3919ee9392c2f682c839c36804536ef7957f6bf", + "opening__wide_large_gradient__2": "ee5e1d0b350d52ee11f4136d259364c1a24be9db926f3bf8420f7ce32f5c2e25", + "opening__wide_large_gradient__3": "46f729c6cc1f704814ff25d33110709dd7029122e26935302ee8d7371b1bb57d", + "opening__wide_large_gradient__30": "b732b91bf8c44a662fbdc8a3b6cd19579ca57abfc91832eb017f972b7c4e0373", + "opening__wide_large_gradient__31": "136e13a491d7ed38f86d4319cd243d97f562104de4ea8b13e7079483e0641e7e", + "opening__wide_large_gradient__4": "54e2941a7f7fdc2c356d4572eef880520bd3bf8dbb883d34660ba2036508c26b", + "opening__wide_large_gradient__5": "54c94ae164daa05701ff493558cf268c764797eadaaa9199dc34229b4d26378f" + }, + "hash_definition": "dtype.str, NUL, comma-separated shape, NUL, C-order bytes", + "npz_relative_path": "flat_disc_morphology_reference.npz", + "npz_sha256": "7cf0cbbd988376c361f9bc97c0c91cc8b0c3df2110596c2be6b1f92d0af787ef" + }, + "kernel_masks": { + "10": "kernel__10", + "11": "kernel__11", + "12": "kernel__12", + "13": "kernel__13", + "14": "kernel__14", + "15": "kernel__15", + "16": "kernel__16", + "17": "kernel__17", + "18": "kernel__18", + "19": "kernel__19", + "2": "kernel__2", + "20": "kernel__20", + "21": "kernel__21", + "22": "kernel__22", + "23": "kernel__23", + "24": "kernel__24", + "25": "kernel__25", + "26": "kernel__26", + "27": "kernel__27", + "28": "kernel__28", + "29": "kernel__29", + "3": "kernel__3", + "30": "kernel__30", + "31": "kernel__31", + "4": "kernel__4", + "5": "kernel__5", + "6": "kernel__6", + "7": "kernel__7", + "8": "kernel__8", + "9": "kernel__9" + }, + "metrics": { + "closing_bitwise_exact": "72/72", + "input_mutation": 0, + "kernels_bitwise_exact": "30/30", + "max_absolute_difference": 0.0, + "max_ulp_distance": 0, + "opening_bitwise_exact": "72/72", + "signed_zero_mismatches": 0 + }, + "oracle_evidence": { + "oracle_v2_sha256": "bf4129fe4fd871dda3132d5457d45d0833d69e9acd5cf3bb765fc0f3a8d9792e", + "reduction_model_sha256": "43089668a7fe0c699093be440402c8b1b11b42dfea4eb720785241788306c543", + "reduction_trace_manifest_sha256": "354117107e0e007c5187a4290049e340a980290aa47265057b4c4fed6621a52e" + }, + "schema_version": 1, + "scope": { + "finite_nonempty_2d": true, + "full_field": true, + "mask_policy": "ignore" + }, + "sizes_exercised": [ + 2, + 3, + 4, + 5, + 30, + 31 + ] +} diff --git a/tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.npz b/tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.npz new file mode 100644 index 0000000..debbe77 Binary files /dev/null and b/tests/validation/fixtures/gwyddion/flat_disc_morphology/flat_disc_morphology_reference.npz differ diff --git a/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.json b/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.json new file mode 100644 index 0000000..f0235d9 --- /dev/null +++ b/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.json @@ -0,0 +1,50 @@ +{ + "acceptance": { + "corrected_max_abs_error": 5e-13, + "final_peak_mean_abs_error": 5e-10, + "final_peak_rms_abs_error": 5e-10 + }, + "artifacts": { + "corrected_canonical_sha256": "0cdfcb4315ed0bb231890e80cb00845d63f4264be2b7dcdb4f9ed9230f2d003d", + "input_canonical_sha256": "b323b21a248a4882b324b189c9198eb140138bf729a3f483d27887f1b51db802", + "npz_filename": "gwyddion_2_71_end_to_end.npz", + "npz_sha256": "e92fe35ceaeaf166457efacb72a310ed5e012608946a604bdffba6e9df13d058" + }, + "expected_control_flow": { + "facet_iterations": 5, + "final_peak_success": true, + "polynomial_applied": 4, + "polynomial_attempted": 4, + "polynomial_degrees": [ + 2, + 3, + 4, + 5 + ] + }, + "expected_result": { + "corrected_maximum": 6.0000618910655525, + "corrected_minimum": 0.0, + "final_peak_mean": -0.38253115055779424, + "final_peak_rms": 0.16615659800631805 + }, + "field": { + "dtype": "float64", + "height_unit": "unspecified synthetic units", + "pixel_size_x": 0.8, + "pixel_size_y": 1.3, + "shape": [ + 24, + 32 + ] + }, + "fixture_id": "gwyddion-2.71-flatten-base-end-to-end", + "reference": { + "operation": "Flatten Base", + "probe_output_sha256": "51ead932b37381b8c0cdd4b0df6b4f9e8962cf638c1cfaad96d0b178118a70a0", + "probe_source": "gwyddion-2.71/flatten-base-parity/flatten_base_end_to_end_probe.c", + "software": "Gwyddion", + "version": "2.71" + }, + "schema_version": 1 +} diff --git a/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.npz b/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.npz new file mode 100644 index 0000000..a88893d Binary files /dev/null and b/tests/validation/fixtures/gwyddion/flatten_base/gwyddion_2_71_end_to_end.npz differ diff --git a/tests/validation/fixtures/gwyddion/median_background/median_background_reference.json b/tests/validation/fixtures/gwyddion/median_background/median_background_reference.json new file mode 100644 index 0000000..8778aa4 --- /dev/null +++ b/tests/validation/fixtures/gwyddion/median_background/median_background_reference.json @@ -0,0 +1,1570 @@ +{ + "acceptance_contract": { + "background_comparison": "bitwise exact float64 equality", + "corrected_comparison": "bitwise exact float64 equality", + "future_discrepancy_rule": "any future discrepancy must first adjudicate source, probe, oracle, fixture and implementation evidence", + "input_mutation": "forbidden", + "no_acceptance_relaxation": "no acceptance relaxation may be introduced merely to satisfy tests", + "output_c_contiguous": "required", + "output_dtype": "float64", + "output_finiteness": "required for finite inputs in this fixture", + "output_shape": "identical to input", + "reconstruction": { + "absolute_tolerance": 1e-15, + "relation": "input == background + corrected", + "relative_tolerance": 0.0 + } + }, + "campaign": { + "asan_detections": 0, + "asan_executions": 36, + "asan_exit_zero": 36, + "glib_detections": 0, + "input_mutation_maximum": 0.0, + "normal_asan_stdout_identical": 36, + "normal_executions": 36, + "normal_exit_zero": 36, + "radius_inventory": { + "1": { + "active_count": 9, + "backend": "direct", + "rank": 4, + "resolution": 3 + }, + "1024": { + "active_count": 3297401, + "backend": "radixtree", + "rank": 1648700, + "resolution": 2049 + }, + "2": { + "active_count": 21, + "backend": "direct", + "rank": 10, + "resolution": 5 + }, + "20": { + "active_count": 1313, + "backend": "radixtree", + "rank": 656, + "resolution": 41 + }, + "3": { + "active_count": 37, + "backend": "radixtree", + "rank": 18, + "resolution": 7 + }, + "4": { + "active_count": 69, + "backend": "radixtree", + "rank": 34, + "resolution": 9 + } + }, + "reconstruction_maximum": 4.440892098500626e-16, + "timeouts": 0, + "total_executions": 72, + "total_logical_cases": 36 + }, + "capability": "gwyddion_median_background", + "cases": [ + { + "arrays": { + "background": "background__wide_r1", + "corrected": "corrected__wide_r1", + "input": "input__wide_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "ee5a77b1472039236347ef7ef98215d3663c51c273e4d94977355c7f77163170", + "canonical_hashes": { + "background": "ed8bfcc683183f523f966253ba59a441b5d04a3145ef5625bc2a342aa9c8a9e7", + "corrected": "aa024e640cdfdeaa1b91b8fa964deaf67c4a1ef8e9917c32c71026ebc36927c1", + "input": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a" + }, + "family": "WIDE_ASYMMETRIC", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "wide_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "ee5a77b1472039236347ef7ef98215d3663c51c273e4d94977355c7f77163170", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 7 + ], + "xres": 7, + "yres": 5 + }, + { + "arrays": { + "background": "background__wide_r2", + "corrected": "corrected__wide_r2", + "input": "input__wide_r2" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "6aef0c2c556e148c225ee9015982cf227064b69300ba4f815a1b2f7586949c4f", + "canonical_hashes": { + "background": "af4db96f382b13f356f4516f0178ac0487a4c5a7665252ef0c57c00cc0cc43f6", + "corrected": "bd35648198dec90ab5233c591109c687c0421994f7d209c99284aace73129862", + "input": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a" + }, + "family": "WIDE_ASYMMETRIC", + "kernel_active_count": 21, + "kernel_resolution": 5, + "name": "wide_r2", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "6aef0c2c556e148c225ee9015982cf227064b69300ba4f815a1b2f7586949c4f", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 2, + "rank_backend_reference": "direct", + "rank_index": 10, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 7 + ], + "xres": 7, + "yres": 5 + }, + { + "arrays": { + "background": "background__wide_r3", + "corrected": "corrected__wide_r3", + "input": "input__wide_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "94e409e5857751215cc6f58e6ef30f04a844305e26832ceab763aa30b924f62e", + "canonical_hashes": { + "background": "fb59c7348c40e18e23f4bc438e36db88c87454ed57db382c06486a6ac0cdb9f5", + "corrected": "38e571bd73d4bd79cc7ed18ebecb3ff77ef1d70ec1bba750201079daa47603e4", + "input": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a" + }, + "family": "WIDE_ASYMMETRIC", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "wide_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "94e409e5857751215cc6f58e6ef30f04a844305e26832ceab763aa30b924f62e", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 7 + ], + "xres": 7, + "yres": 5 + }, + { + "arrays": { + "background": "background__wide_r4", + "corrected": "corrected__wide_r4", + "input": "input__wide_r4" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "74f3b7a01c85452d0ebd6ea3bdf149e03f71c6759be46fc39672ed551925d380", + "canonical_hashes": { + "background": "fbcefdd158ac239a19ff9cc7671d29a95c72550f70247875ae67ba294e299de3", + "corrected": "72150de5367cc6a8e2b4288439f777bb4324656afaeef20f10e96b2a9b38e80d", + "input": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a" + }, + "family": "WIDE_ASYMMETRIC", + "kernel_active_count": 69, + "kernel_resolution": 9, + "name": "wide_r4", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "74f3b7a01c85452d0ebd6ea3bdf149e03f71c6759be46fc39672ed551925d380", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 4, + "rank_backend_reference": "radixtree", + "rank_index": 34, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 7 + ], + "xres": 7, + "yres": 5 + }, + { + "arrays": { + "background": "background__wide_r20", + "corrected": "corrected__wide_r20", + "input": "input__wide_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "e78cd9f92b7450558b91f79e1dd2f7ca956b88db081ee37c76920feee4893461", + "canonical_hashes": { + "background": "c4f959c3e789cc2d132d64bf19b5e275568ada2d1aa003b8d449c991479103f9", + "corrected": "b793073e8a658e6d0bd7f8d019fcd804056f9074c330b8d319d3b66b9d82f2a5", + "input": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a" + }, + "family": "WIDE_ASYMMETRIC", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "wide_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "e78cd9f92b7450558b91f79e1dd2f7ca956b88db081ee37c76920feee4893461", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 7 + ], + "xres": 7, + "yres": 5 + }, + { + "arrays": { + "background": "background__tall_r1", + "corrected": "corrected__tall_r1", + "input": "input__tall_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "1c3eae268263c2942688421781a36b7d79601a7f15df2e22e148314b44310186", + "canonical_hashes": { + "background": "3dd16ee6c30ad2cd3ef78c35c3b32eeb293a8724c16d506d4b62731260d875c4", + "corrected": "2096acc9eaf4969b92e9a156e8af1d08747083128ceff92b647f88a3c5236a25", + "input": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827" + }, + "family": "TALL_ASYMMETRIC", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "tall_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "1c3eae268263c2942688421781a36b7d79601a7f15df2e22e148314b44310186", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 7, + 5 + ], + "xres": 5, + "yres": 7 + }, + { + "arrays": { + "background": "background__tall_r2", + "corrected": "corrected__tall_r2", + "input": "input__tall_r2" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "c4fb1bab61dea587929df374eb586c78d2174f9b1164cb4d2f9609568cabb8e6", + "canonical_hashes": { + "background": "10070900cd33e8ced89e4b7bc99119676c21422a769383cfad4af189e6c9eb41", + "corrected": "db4cd2345bbde0d1ad8353c0b2f16eb3b8943a319175836ef5ebe62fcbd3177a", + "input": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827" + }, + "family": "TALL_ASYMMETRIC", + "kernel_active_count": 21, + "kernel_resolution": 5, + "name": "tall_r2", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "c4fb1bab61dea587929df374eb586c78d2174f9b1164cb4d2f9609568cabb8e6", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 2, + "rank_backend_reference": "direct", + "rank_index": 10, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 7, + 5 + ], + "xres": 5, + "yres": 7 + }, + { + "arrays": { + "background": "background__tall_r3", + "corrected": "corrected__tall_r3", + "input": "input__tall_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "784348f9e52f7b9ae3ec2e43735ae912863acabba8b1aa1f031ed0c362424cab", + "canonical_hashes": { + "background": "0cc64e208ea1a477acc88cfd2f0b6be19305df2550137e766b51549c63e256f6", + "corrected": "8a6493968843f6a590800f45a820f9b1349510dc4345be287efefbb8b28dfe32", + "input": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827" + }, + "family": "TALL_ASYMMETRIC", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "tall_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "784348f9e52f7b9ae3ec2e43735ae912863acabba8b1aa1f031ed0c362424cab", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 7, + 5 + ], + "xres": 5, + "yres": 7 + }, + { + "arrays": { + "background": "background__tall_r4", + "corrected": "corrected__tall_r4", + "input": "input__tall_r4" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "527a50e7e291c883fc7ef9bf8611f3ea66c8ae3ada97eecc0ba113427a5d10f4", + "canonical_hashes": { + "background": "6b5fef03b136cab88588e25d3fa51f34b6e3e2b9defae8d598d004b50a753a2f", + "corrected": "bfe4717df0982a911f39bc8f3afd0c0a1571e708dc50215a3a8aad3d6c9593f6", + "input": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827" + }, + "family": "TALL_ASYMMETRIC", + "kernel_active_count": 69, + "kernel_resolution": 9, + "name": "tall_r4", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "527a50e7e291c883fc7ef9bf8611f3ea66c8ae3ada97eecc0ba113427a5d10f4", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 4, + "rank_backend_reference": "radixtree", + "rank_index": 34, + "reference_reconstruction_maximum": 4.440892098500626e-16, + "shape": [ + 7, + 5 + ], + "xres": 5, + "yres": 7 + }, + { + "arrays": { + "background": "background__tall_r20", + "corrected": "corrected__tall_r20", + "input": "input__tall_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "36a7270e69665f030a293cd1aad36d64a2554295c5ea332da93b350fc3a0a783", + "canonical_hashes": { + "background": "6b1552c3b22919f4d934b54714d7e2af63d2a6e99b08d25bc5145f92b187a5f4", + "corrected": "fbd461fcb22ebc5122c696d084111ff4f81d5435bd3a33c7a8d4ad1eb5e8f427", + "input": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827" + }, + "family": "TALL_ASYMMETRIC", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "tall_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "36a7270e69665f030a293cd1aad36d64a2554295c5ea332da93b350fc3a0a783", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 4.440892098500626e-16, + "shape": [ + 7, + 5 + ], + "xres": 5, + "yres": 7 + }, + { + "arrays": { + "background": "background__constant_r1", + "corrected": "corrected__constant_r1", + "input": "input__constant_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "0f9e8a47b96cfeedbb1009af226a753adac5cbbc429aa6d47dc718ff1169ccbc", + "canonical_hashes": { + "background": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "corrected": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "input": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d" + }, + "family": "CONSTANT_NONZERO", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "constant_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "0f9e8a47b96cfeedbb1009af226a753adac5cbbc429aa6d47dc718ff1169ccbc", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 4 + ], + "xres": 4, + "yres": 3 + }, + { + "arrays": { + "background": "background__constant_r3", + "corrected": "corrected__constant_r3", + "input": "input__constant_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "63faa4e94c8239e10abed8150465d259ce0fdc4199160d31e43f4c8eab57aa66", + "canonical_hashes": { + "background": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "corrected": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "input": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d" + }, + "family": "CONSTANT_NONZERO", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "constant_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "63faa4e94c8239e10abed8150465d259ce0fdc4199160d31e43f4c8eab57aa66", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 4 + ], + "xres": 4, + "yres": 3 + }, + { + "arrays": { + "background": "background__constant_r20", + "corrected": "corrected__constant_r20", + "input": "input__constant_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "c22d1cf50a37185c37e73d2409e7072ffb13be2ea7cf2079f5316152041e8709", + "canonical_hashes": { + "background": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "corrected": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "input": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d" + }, + "family": "CONSTANT_NONZERO", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "constant_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "c22d1cf50a37185c37e73d2409e7072ffb13be2ea7cf2079f5316152041e8709", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 4 + ], + "xres": 4, + "yres": 3 + }, + { + "arrays": { + "background": "background__signed_r1", + "corrected": "corrected__signed_r1", + "input": "input__signed_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "5bbc8a33fdc2f4f65de201cca101313eef6c3ed46c8198b15e6da608db7eaaae", + "canonical_hashes": { + "background": "a132b970a12538921d3d9ebe1afa3c9eefea29e3ce069f432ed5bd60e850921b", + "corrected": "34c540f12aed27673d1483c377141fd0de3cffa96ab3d9b30537ed7f7dcccbc5", + "input": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b" + }, + "family": "SIGNED_EXPLICIT", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "signed_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "5bbc8a33fdc2f4f65de201cca101313eef6c3ed46c8198b15e6da608db7eaaae", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 3 + ], + "xres": 3, + "yres": 3 + }, + { + "arrays": { + "background": "background__signed_r2", + "corrected": "corrected__signed_r2", + "input": "input__signed_r2" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "676315256497615ea251205778866fe9240f7a03b9c7828e59cd2821c22966b0", + "canonical_hashes": { + "background": "16c24b162b2baa3f5d4d22e1779ab002f8af3c8fe2df12a5dfad3a0812fca745", + "corrected": "42c169eb8fb0911f6d4ea70e6a48040ff8c933f481cb77907d148cd2b9d52969", + "input": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b" + }, + "family": "SIGNED_EXPLICIT", + "kernel_active_count": 21, + "kernel_resolution": 5, + "name": "signed_r2", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "676315256497615ea251205778866fe9240f7a03b9c7828e59cd2821c22966b0", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 2, + "rank_backend_reference": "direct", + "rank_index": 10, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 3 + ], + "xres": 3, + "yres": 3 + }, + { + "arrays": { + "background": "background__signed_r3", + "corrected": "corrected__signed_r3", + "input": "input__signed_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "f2fa6d42a4520d70ee9d38fd51ef89a7792dc8b7ae8294da416b4bbffc673207", + "canonical_hashes": { + "background": "c9626a23c50d43caeb07c7008c212607a5580def6d5c33f33914d8f57226b295", + "corrected": "ab5822d7ecf9b27c5353b2bd4203b0fbb4f4c241583b273ce352c62eb09b59ea", + "input": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b" + }, + "family": "SIGNED_EXPLICIT", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "signed_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "f2fa6d42a4520d70ee9d38fd51ef89a7792dc8b7ae8294da416b4bbffc673207", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 3 + ], + "xres": 3, + "yres": 3 + }, + { + "arrays": { + "background": "background__signed_r20", + "corrected": "corrected__signed_r20", + "input": "input__signed_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "685ac6941cde1c12699c60df73786bbb2d93d28b2021403ee239e371500eae93", + "canonical_hashes": { + "background": "8c5d90f8bcf1b0e5ed1aed6ef09cf8914f39930d6775cbb95b4596cd0a48a340", + "corrected": "23dc233138657fd8a0b43948d8b9b615075262136ef8b15588713864c544e259", + "input": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b" + }, + "family": "SIGNED_EXPLICIT", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "signed_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "685ac6941cde1c12699c60df73786bbb2d93d28b2021403ee239e371500eae93", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 3, + 3 + ], + "xres": 3, + "yres": 3 + }, + { + "arrays": { + "background": "background__singleton_1x1_r1", + "corrected": "corrected__singleton_1x1_r1", + "input": "input__singleton_1x1_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "9f23d5ab27921bcb281195f1aa4242f6d567822c58db2289c72e654bb91679ec", + "canonical_hashes": { + "background": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "input": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f" + }, + "family": "SINGLETON_SCALAR", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "singleton_1x1_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "9f23d5ab27921bcb281195f1aa4242f6d567822c58db2289c72e654bb91679ec", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 1 + ], + "xres": 1, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_1x1_r3", + "corrected": "corrected__singleton_1x1_r3", + "input": "input__singleton_1x1_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "5350c8cf313880fe9d9c7d66322447619c031d76e533245b670e0fce615ddf9f", + "canonical_hashes": { + "background": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "input": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f" + }, + "family": "SINGLETON_SCALAR", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "singleton_1x1_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "5350c8cf313880fe9d9c7d66322447619c031d76e533245b670e0fce615ddf9f", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 1 + ], + "xres": 1, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_1x1_r20", + "corrected": "corrected__singleton_1x1_r20", + "input": "input__singleton_1x1_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "935a44ebec138342eba5fd54e8ae4356d3cc8a0f693b50da86735b13915b6998", + "canonical_hashes": { + "background": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "input": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f" + }, + "family": "SINGLETON_SCALAR", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "singleton_1x1_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "935a44ebec138342eba5fd54e8ae4356d3cc8a0f693b50da86735b13915b6998", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 1 + ], + "xres": 1, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_1x1_r1024", + "corrected": "corrected__singleton_1x1_r1024", + "input": "input__singleton_1x1_r1024" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "a3aa53016ac652fa2c6658a0dc08a40178cfdcde8250cc9efdea16a7234aa200", + "canonical_hashes": { + "background": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "input": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f" + }, + "family": "SINGLETON_SCALAR", + "kernel_active_count": 3297401, + "kernel_resolution": 2049, + "name": "singleton_1x1_r1024", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "a3aa53016ac652fa2c6658a0dc08a40178cfdcde8250cc9efdea16a7234aa200", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1024, + "rank_backend_reference": "radixtree", + "rank_index": 1648700, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 1 + ], + "xres": 1, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_row_r1", + "corrected": "corrected__singleton_row_r1", + "input": "input__singleton_row_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "049d6a8c610eec979a4a849aafd127cd03441dc5b09dd9716637acc4833820bd", + "canonical_hashes": { + "background": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "corrected": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "input": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57" + }, + "family": "SINGLETON_ROW", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "singleton_row_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "049d6a8c610eec979a4a849aafd127cd03441dc5b09dd9716637acc4833820bd", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 5 + ], + "xres": 5, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_row_r3", + "corrected": "corrected__singleton_row_r3", + "input": "input__singleton_row_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "5cde1b61afe9b14f6c0b067c10bb88cdd9f571631297140dfad8bb139bdf6995", + "canonical_hashes": { + "background": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "corrected": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "input": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57" + }, + "family": "SINGLETON_ROW", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "singleton_row_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "5cde1b61afe9b14f6c0b067c10bb88cdd9f571631297140dfad8bb139bdf6995", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 5 + ], + "xres": 5, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_row_r20", + "corrected": "corrected__singleton_row_r20", + "input": "input__singleton_row_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "fcc806e053dd97ba2cd197a545e8cae79c89eebcc9fce56ea158883bccc0224f", + "canonical_hashes": { + "background": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "corrected": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "input": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57" + }, + "family": "SINGLETON_ROW", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "singleton_row_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "fcc806e053dd97ba2cd197a545e8cae79c89eebcc9fce56ea158883bccc0224f", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 1, + 5 + ], + "xres": 5, + "yres": 1 + }, + { + "arrays": { + "background": "background__singleton_column_r1", + "corrected": "corrected__singleton_column_r1", + "input": "input__singleton_column_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "69bda8da85155b03afe65486f742e6c9d5a5fe15ae7cd96f6bfa5a6aa80e1420", + "canonical_hashes": { + "background": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "corrected": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "input": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9" + }, + "family": "SINGLETON_COLUMN", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "singleton_column_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "69bda8da85155b03afe65486f742e6c9d5a5fe15ae7cd96f6bfa5a6aa80e1420", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 1 + ], + "xres": 1, + "yres": 5 + }, + { + "arrays": { + "background": "background__singleton_column_r3", + "corrected": "corrected__singleton_column_r3", + "input": "input__singleton_column_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "b683797214f377eb700e8104b2c38a2138bdc34667228b70895d6fcbaa95330a", + "canonical_hashes": { + "background": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "corrected": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "input": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9" + }, + "family": "SINGLETON_COLUMN", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "singleton_column_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "b683797214f377eb700e8104b2c38a2138bdc34667228b70895d6fcbaa95330a", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 1 + ], + "xres": 1, + "yres": 5 + }, + { + "arrays": { + "background": "background__singleton_column_r20", + "corrected": "corrected__singleton_column_r20", + "input": "input__singleton_column_r20" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "df16cae389eb5a7f7a3e1b89247d564d94b0e62cde63259b781d235b8de22ed0", + "canonical_hashes": { + "background": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "corrected": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "input": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9" + }, + "family": "SINGLETON_COLUMN", + "kernel_active_count": 1313, + "kernel_resolution": 41, + "name": "singleton_column_r20", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "df16cae389eb5a7f7a3e1b89247d564d94b0e62cde63259b781d235b8de22ed0", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 20, + "rank_backend_reference": "radixtree", + "rank_index": 656, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 1 + ], + "xres": 1, + "yres": 5 + }, + { + "arrays": { + "background": "background__impulse_positive_r1", + "corrected": "corrected__impulse_positive_r1", + "input": "input__impulse_positive_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "e6cae67ec4b60be3c67305e2bb4a8cd7ea9ebf2853d28865cafc9721e82008e2", + "canonical_hashes": { + "background": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "corrected": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "input": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa" + }, + "family": "IMPULSE_POSITIVE", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "impulse_positive_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "e6cae67ec4b60be3c67305e2bb4a8cd7ea9ebf2853d28865cafc9721e82008e2", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 5 + ], + "xres": 5, + "yres": 5 + }, + { + "arrays": { + "background": "background__impulse_positive_r2", + "corrected": "corrected__impulse_positive_r2", + "input": "input__impulse_positive_r2" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "d54909e63485b8123a710f92495c51b333122ceb4c56b9581478c011b5031e3b", + "canonical_hashes": { + "background": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "corrected": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "input": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa" + }, + "family": "IMPULSE_POSITIVE", + "kernel_active_count": 21, + "kernel_resolution": 5, + "name": "impulse_positive_r2", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "d54909e63485b8123a710f92495c51b333122ceb4c56b9581478c011b5031e3b", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 2, + "rank_backend_reference": "direct", + "rank_index": 10, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 5 + ], + "xres": 5, + "yres": 5 + }, + { + "arrays": { + "background": "background__impulse_positive_r3", + "corrected": "corrected__impulse_positive_r3", + "input": "input__impulse_positive_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "33503698dbc21880eec0a1c6b2cca79af69c06fc5142d84cdff725f613fb8cf2", + "canonical_hashes": { + "background": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "corrected": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "input": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa" + }, + "family": "IMPULSE_POSITIVE", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "impulse_positive_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "33503698dbc21880eec0a1c6b2cca79af69c06fc5142d84cdff725f613fb8cf2", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 5 + ], + "xres": 5, + "yres": 5 + }, + { + "arrays": { + "background": "background__impulse_negative_r1", + "corrected": "corrected__impulse_negative_r1", + "input": "input__impulse_negative_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "136cef3c87081f62d1a58ea34ba4cec53c3ec0c3a70e546c7781dabc03672900", + "canonical_hashes": { + "background": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "corrected": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "input": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894" + }, + "family": "IMPULSE_NEGATIVE", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "impulse_negative_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "136cef3c87081f62d1a58ea34ba4cec53c3ec0c3a70e546c7781dabc03672900", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 5 + ], + "xres": 5, + "yres": 5 + }, + { + "arrays": { + "background": "background__impulse_negative_r2", + "corrected": "corrected__impulse_negative_r2", + "input": "input__impulse_negative_r2" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "13cacbc9901651a60cee59fb99f6001b7cc1923cb19077175dc2ef900320eaef", + "canonical_hashes": { + "background": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "corrected": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "input": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894" + }, + "family": "IMPULSE_NEGATIVE", + "kernel_active_count": 21, + "kernel_resolution": 5, + "name": "impulse_negative_r2", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "13cacbc9901651a60cee59fb99f6001b7cc1923cb19077175dc2ef900320eaef", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 2, + "rank_backend_reference": "direct", + "rank_index": 10, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 5 + ], + "xres": 5, + "yres": 5 + }, + { + "arrays": { + "background": "background__impulse_negative_r3", + "corrected": "corrected__impulse_negative_r3", + "input": "input__impulse_negative_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "d82063e40817af9aea7bbdee7e0ae6c456dac52d238a4eb3aacacc7117d80fbe", + "canonical_hashes": { + "background": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "corrected": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "input": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894" + }, + "family": "IMPULSE_NEGATIVE", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "impulse_negative_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "d82063e40817af9aea7bbdee7e0ae6c456dac52d238a4eb3aacacc7117d80fbe", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 5, + 5 + ], + "xres": 5, + "yres": 5 + }, + { + "arrays": { + "background": "background__monotonic_r1", + "corrected": "corrected__monotonic_r1", + "input": "input__monotonic_r1" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "322b1a1a28cb3b1ca4be0790b3bf7ca57a92fa18b75530d222608d0c2e43fdff", + "canonical_hashes": { + "background": "60754f4f2727f46508e7d407861187159d7e4487ebf92d55e2fa0f43eb0b71c4", + "corrected": "ec15989faf678e6c93b7a2770f66a8c9a18971872c4dc179df88e31b83632f1c", + "input": "516a1bf69f91e9c4f763f5ddf322d5ac3b6632b7c1712427373d3398aee70905" + }, + "family": "MONOTONIC", + "kernel_active_count": 9, + "kernel_resolution": 3, + "name": "monotonic_r1", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "322b1a1a28cb3b1ca4be0790b3bf7ca57a92fa18b75530d222608d0c2e43fdff", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 1, + "rank_backend_reference": "direct", + "rank_index": 4, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 4, + 4 + ], + "xres": 4, + "yres": 4 + }, + { + "arrays": { + "background": "background__monotonic_r2", + "corrected": "corrected__monotonic_r2", + "input": "input__monotonic_r2" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "f7b29e736c9e9e870da37aa5690ba0ef99d397ff87790d208aa6e4e0ec9720f1", + "canonical_hashes": { + "background": "60754f4f2727f46508e7d407861187159d7e4487ebf92d55e2fa0f43eb0b71c4", + "corrected": "ec15989faf678e6c93b7a2770f66a8c9a18971872c4dc179df88e31b83632f1c", + "input": "516a1bf69f91e9c4f763f5ddf322d5ac3b6632b7c1712427373d3398aee70905" + }, + "family": "MONOTONIC", + "kernel_active_count": 21, + "kernel_resolution": 5, + "name": "monotonic_r2", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "f7b29e736c9e9e870da37aa5690ba0ef99d397ff87790d208aa6e4e0ec9720f1", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 2, + "rank_backend_reference": "direct", + "rank_index": 10, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 4, + 4 + ], + "xres": 4, + "yres": 4 + }, + { + "arrays": { + "background": "background__monotonic_r3", + "corrected": "corrected__monotonic_r3", + "input": "input__monotonic_r3" + }, + "asan_exit_code": 0, + "asan_stdout_sha256": "809f2c550b73dd1f9a7f963f2ebdde1c14c7524faf94b70b587d4dda49310350", + "canonical_hashes": { + "background": "3d69ac8275fae70542a8bb19f0a68a83dbdf1a9d075da1ae3e67f0754c94a57c", + "corrected": "88fd2936e40e0605702b266a858c911967dd8229c60c1dfb66a0c3d3ee8274f6", + "input": "516a1bf69f91e9c4f763f5ddf322d5ac3b6632b7c1712427373d3398aee70905" + }, + "family": "MONOTONIC", + "kernel_active_count": 37, + "kernel_resolution": 7, + "name": "monotonic_r3", + "normal_asan_stdout_byte_identical": true, + "normal_exit_code": 0, + "normal_stdout_sha256": "809f2c550b73dd1f9a7f963f2ebdde1c14c7524faf94b70b587d4dda49310350", + "oracle_reference_background_bitwise_exact": true, + "oracle_reference_corrected_bitwise_exact": true, + "radius": 3, + "rank_backend_reference": "radixtree", + "rank_index": 18, + "reference_reconstruction_maximum": 0.0, + "shape": [ + 4, + 4 + ], + "xres": 4, + "yres": 4 + } + ], + "evidence_classification": { + "external_probe": "EXECUTABLE_EXTERNAL_REFERENCE", + "freeze_audit": "MEDIAN_BACKGROUND_ORACLE_FREEZE_APPROVED", + "independent_oracle": "INDEPENDENT_PYTHON_ORACLE", + "spmkit_implementation": "NOT_YET_IMPLEMENTED" + }, + "fixture": { + "all_finite": true, + "array_count": 108, + "arrays_per_case": 3, + "canonical_hash_algorithm": "SHA-256 of dtype.str, NUL, comma-separated shape, NUL, and C-order bytes", + "case_count": 36, + "compressed": false, + "dimension_count": 2, + "dtype": "float64", + "npz_relative_path": "tests/validation/fixtures/gwyddion/median_background/median_background_reference.npz", + "npz_sha256": "a581893e44d8887939d2335bc7eda6650a301e78e4025a8c7aa77020db442b20", + "order": "C" + }, + "operation": "median_bg", + "oracle": { + "background_bitwise_exact_percentage_maximum": 100.0, + "background_bitwise_exact_percentage_minimum": 100.0, + "background_maximum_absolute_difference": 0.0, + "background_maximum_ulp": 0, + "canonical_source_array_hash_count": 180, + "canonical_source_array_hashes": { + "input__constant_r1": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "input__constant_r20": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "input__constant_r3": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "input__impulse_negative_r1": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "input__impulse_negative_r2": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "input__impulse_negative_r3": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "input__impulse_positive_r1": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "input__impulse_positive_r2": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "input__impulse_positive_r3": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "input__monotonic_r1": "516a1bf69f91e9c4f763f5ddf322d5ac3b6632b7c1712427373d3398aee70905", + "input__monotonic_r2": "516a1bf69f91e9c4f763f5ddf322d5ac3b6632b7c1712427373d3398aee70905", + "input__monotonic_r3": "516a1bf69f91e9c4f763f5ddf322d5ac3b6632b7c1712427373d3398aee70905", + "input__signed_r1": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b", + "input__signed_r2": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b", + "input__signed_r20": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b", + "input__signed_r3": "b0c13406214e251df81d7859813566e2c43c107310f36f0906906ba3f090375b", + "input__singleton_1x1_r1": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input__singleton_1x1_r1024": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input__singleton_1x1_r20": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input__singleton_1x1_r3": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input__singleton_column_r1": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "input__singleton_column_r20": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "input__singleton_column_r3": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "input__singleton_row_r1": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "input__singleton_row_r20": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "input__singleton_row_r3": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "input__tall_r1": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827", + "input__tall_r2": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827", + "input__tall_r20": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827", + "input__tall_r3": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827", + "input__tall_r4": "a7db6ca92ecd92a9c68dfc06110cdeb0c79b46796fd9e4396ba59cf32bcc2827", + "input__wide_r1": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a", + "input__wide_r2": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a", + "input__wide_r20": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a", + "input__wide_r3": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a", + "input__wide_r4": "9eec98d0332346e0928a2bc242bd93ea9b8affe7f135040379a23a100d0f972a", + "oracle_background__constant_r1": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "oracle_background__constant_r20": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "oracle_background__constant_r3": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "oracle_background__impulse_negative_r1": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "oracle_background__impulse_negative_r2": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "oracle_background__impulse_negative_r3": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "oracle_background__impulse_positive_r1": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "oracle_background__impulse_positive_r2": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "oracle_background__impulse_positive_r3": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "oracle_background__monotonic_r1": "60754f4f2727f46508e7d407861187159d7e4487ebf92d55e2fa0f43eb0b71c4", + "oracle_background__monotonic_r2": "60754f4f2727f46508e7d407861187159d7e4487ebf92d55e2fa0f43eb0b71c4", + "oracle_background__monotonic_r3": "3d69ac8275fae70542a8bb19f0a68a83dbdf1a9d075da1ae3e67f0754c94a57c", + "oracle_background__signed_r1": "a132b970a12538921d3d9ebe1afa3c9eefea29e3ce069f432ed5bd60e850921b", + "oracle_background__signed_r2": "16c24b162b2baa3f5d4d22e1779ab002f8af3c8fe2df12a5dfad3a0812fca745", + "oracle_background__signed_r20": "8c5d90f8bcf1b0e5ed1aed6ef09cf8914f39930d6775cbb95b4596cd0a48a340", + "oracle_background__signed_r3": "c9626a23c50d43caeb07c7008c212607a5580def6d5c33f33914d8f57226b295", + "oracle_background__singleton_1x1_r1": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "oracle_background__singleton_1x1_r1024": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "oracle_background__singleton_1x1_r20": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "oracle_background__singleton_1x1_r3": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "oracle_background__singleton_column_r1": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "oracle_background__singleton_column_r20": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "oracle_background__singleton_column_r3": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "oracle_background__singleton_row_r1": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "oracle_background__singleton_row_r20": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "oracle_background__singleton_row_r3": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "oracle_background__tall_r1": "3dd16ee6c30ad2cd3ef78c35c3b32eeb293a8724c16d506d4b62731260d875c4", + "oracle_background__tall_r2": "10070900cd33e8ced89e4b7bc99119676c21422a769383cfad4af189e6c9eb41", + "oracle_background__tall_r20": "6b1552c3b22919f4d934b54714d7e2af63d2a6e99b08d25bc5145f92b187a5f4", + "oracle_background__tall_r3": "0cc64e208ea1a477acc88cfd2f0b6be19305df2550137e766b51549c63e256f6", + "oracle_background__tall_r4": "6b5fef03b136cab88588e25d3fa51f34b6e3e2b9defae8d598d004b50a753a2f", + "oracle_background__wide_r1": "ed8bfcc683183f523f966253ba59a441b5d04a3145ef5625bc2a342aa9c8a9e7", + "oracle_background__wide_r2": "af4db96f382b13f356f4516f0178ac0487a4c5a7665252ef0c57c00cc0cc43f6", + "oracle_background__wide_r20": "c4f959c3e789cc2d132d64bf19b5e275568ada2d1aa003b8d449c991479103f9", + "oracle_background__wide_r3": "fb59c7348c40e18e23f4bc438e36db88c87454ed57db382c06486a6ac0cdb9f5", + "oracle_background__wide_r4": "fbcefdd158ac239a19ff9cc7671d29a95c72550f70247875ae67ba294e299de3", + "oracle_corrected__constant_r1": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "oracle_corrected__constant_r20": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "oracle_corrected__constant_r3": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "oracle_corrected__impulse_negative_r1": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "oracle_corrected__impulse_negative_r2": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "oracle_corrected__impulse_negative_r3": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "oracle_corrected__impulse_positive_r1": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "oracle_corrected__impulse_positive_r2": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "oracle_corrected__impulse_positive_r3": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "oracle_corrected__monotonic_r1": "ec15989faf678e6c93b7a2770f66a8c9a18971872c4dc179df88e31b83632f1c", + "oracle_corrected__monotonic_r2": "ec15989faf678e6c93b7a2770f66a8c9a18971872c4dc179df88e31b83632f1c", + "oracle_corrected__monotonic_r3": "88fd2936e40e0605702b266a858c911967dd8229c60c1dfb66a0c3d3ee8274f6", + "oracle_corrected__signed_r1": "34c540f12aed27673d1483c377141fd0de3cffa96ab3d9b30537ed7f7dcccbc5", + "oracle_corrected__signed_r2": "42c169eb8fb0911f6d4ea70e6a48040ff8c933f481cb77907d148cd2b9d52969", + "oracle_corrected__signed_r20": "23dc233138657fd8a0b43948d8b9b615075262136ef8b15588713864c544e259", + "oracle_corrected__signed_r3": "ab5822d7ecf9b27c5353b2bd4203b0fbb4f4c241583b273ce352c62eb09b59ea", + "oracle_corrected__singleton_1x1_r1": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "oracle_corrected__singleton_1x1_r1024": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "oracle_corrected__singleton_1x1_r20": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "oracle_corrected__singleton_1x1_r3": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "oracle_corrected__singleton_column_r1": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "oracle_corrected__singleton_column_r20": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "oracle_corrected__singleton_column_r3": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "oracle_corrected__singleton_row_r1": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "oracle_corrected__singleton_row_r20": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "oracle_corrected__singleton_row_r3": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "oracle_corrected__tall_r1": "2096acc9eaf4969b92e9a156e8af1d08747083128ceff92b647f88a3c5236a25", + "oracle_corrected__tall_r2": "db4cd2345bbde0d1ad8353c0b2f16eb3b8943a319175836ef5ebe62fcbd3177a", + "oracle_corrected__tall_r20": "fbd461fcb22ebc5122c696d084111ff4f81d5435bd3a33c7a8d4ad1eb5e8f427", + "oracle_corrected__tall_r3": "8a6493968843f6a590800f45a820f9b1349510dc4345be287efefbb8b28dfe32", + "oracle_corrected__tall_r4": "bfe4717df0982a911f39bc8f3afd0c0a1571e708dc50215a3a8aad3d6c9593f6", + "oracle_corrected__wide_r1": "aa024e640cdfdeaa1b91b8fa964deaf67c4a1ef8e9917c32c71026ebc36927c1", + "oracle_corrected__wide_r2": "bd35648198dec90ab5233c591109c687c0421994f7d209c99284aace73129862", + "oracle_corrected__wide_r20": "b793073e8a658e6d0bd7f8d019fcd804056f9074c330b8d319d3b66b9d82f2a5", + "oracle_corrected__wide_r3": "38e571bd73d4bd79cc7ed18ebecb3ff77ef1d70ec1bba750201079daa47603e4", + "oracle_corrected__wide_r4": "72150de5367cc6a8e2b4288439f777bb4324656afaeef20f10e96b2a9b38e80d", + "reference_background__constant_r1": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "reference_background__constant_r20": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "reference_background__constant_r3": "1cf44ec5f4c9b1418956213c4c073602be40a3b9ff83253313c95b184324334d", + "reference_background__impulse_negative_r1": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "reference_background__impulse_negative_r2": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "reference_background__impulse_negative_r3": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "reference_background__impulse_positive_r1": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "reference_background__impulse_positive_r2": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "reference_background__impulse_positive_r3": "0db221a6c1a230bbb1d9c6bd2a89e3711812b8cb975c80161c7ed7b696922519", + "reference_background__monotonic_r1": "60754f4f2727f46508e7d407861187159d7e4487ebf92d55e2fa0f43eb0b71c4", + "reference_background__monotonic_r2": "60754f4f2727f46508e7d407861187159d7e4487ebf92d55e2fa0f43eb0b71c4", + "reference_background__monotonic_r3": "3d69ac8275fae70542a8bb19f0a68a83dbdf1a9d075da1ae3e67f0754c94a57c", + "reference_background__signed_r1": "a132b970a12538921d3d9ebe1afa3c9eefea29e3ce069f432ed5bd60e850921b", + "reference_background__signed_r2": "16c24b162b2baa3f5d4d22e1779ab002f8af3c8fe2df12a5dfad3a0812fca745", + "reference_background__signed_r20": "8c5d90f8bcf1b0e5ed1aed6ef09cf8914f39930d6775cbb95b4596cd0a48a340", + "reference_background__signed_r3": "c9626a23c50d43caeb07c7008c212607a5580def6d5c33f33914d8f57226b295", + "reference_background__singleton_1x1_r1": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "reference_background__singleton_1x1_r1024": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "reference_background__singleton_1x1_r20": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "reference_background__singleton_1x1_r3": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "reference_background__singleton_column_r1": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "reference_background__singleton_column_r20": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "reference_background__singleton_column_r3": "424b7adea6f6a2417030b7203ee7b7323ae361c00237a5e1393af739665399b9", + "reference_background__singleton_row_r1": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "reference_background__singleton_row_r20": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "reference_background__singleton_row_r3": "5fd3797e9aa57a4f15237d46f86a84e0b52a8669e369748b45a53bb8a859ed57", + "reference_background__tall_r1": "3dd16ee6c30ad2cd3ef78c35c3b32eeb293a8724c16d506d4b62731260d875c4", + "reference_background__tall_r2": "10070900cd33e8ced89e4b7bc99119676c21422a769383cfad4af189e6c9eb41", + "reference_background__tall_r20": "6b1552c3b22919f4d934b54714d7e2af63d2a6e99b08d25bc5145f92b187a5f4", + "reference_background__tall_r3": "0cc64e208ea1a477acc88cfd2f0b6be19305df2550137e766b51549c63e256f6", + "reference_background__tall_r4": "6b5fef03b136cab88588e25d3fa51f34b6e3e2b9defae8d598d004b50a753a2f", + "reference_background__wide_r1": "ed8bfcc683183f523f966253ba59a441b5d04a3145ef5625bc2a342aa9c8a9e7", + "reference_background__wide_r2": "af4db96f382b13f356f4516f0178ac0487a4c5a7665252ef0c57c00cc0cc43f6", + "reference_background__wide_r20": "c4f959c3e789cc2d132d64bf19b5e275568ada2d1aa003b8d449c991479103f9", + "reference_background__wide_r3": "fb59c7348c40e18e23f4bc438e36db88c87454ed57db382c06486a6ac0cdb9f5", + "reference_background__wide_r4": "fbcefdd158ac239a19ff9cc7671d29a95c72550f70247875ae67ba294e299de3", + "reference_corrected__constant_r1": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "reference_corrected__constant_r20": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "reference_corrected__constant_r3": "be1cf123657b083340a21ba2ddfe026545ed615e8f9eca5888c9371bdc63c2e7", + "reference_corrected__impulse_negative_r1": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "reference_corrected__impulse_negative_r2": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "reference_corrected__impulse_negative_r3": "21b11b545d6270bbdd4eb4fc6d6e3163dc5ec6a8d107321998116ba6582f2894", + "reference_corrected__impulse_positive_r1": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "reference_corrected__impulse_positive_r2": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "reference_corrected__impulse_positive_r3": "368ccc46b91920521792dea92d5544ad77455bbb0b83aec7908ad8416b67b1aa", + "reference_corrected__monotonic_r1": "ec15989faf678e6c93b7a2770f66a8c9a18971872c4dc179df88e31b83632f1c", + "reference_corrected__monotonic_r2": "ec15989faf678e6c93b7a2770f66a8c9a18971872c4dc179df88e31b83632f1c", + "reference_corrected__monotonic_r3": "88fd2936e40e0605702b266a858c911967dd8229c60c1dfb66a0c3d3ee8274f6", + "reference_corrected__signed_r1": "34c540f12aed27673d1483c377141fd0de3cffa96ab3d9b30537ed7f7dcccbc5", + "reference_corrected__signed_r2": "42c169eb8fb0911f6d4ea70e6a48040ff8c933f481cb77907d148cd2b9d52969", + "reference_corrected__signed_r20": "23dc233138657fd8a0b43948d8b9b615075262136ef8b15588713864c544e259", + "reference_corrected__signed_r3": "ab5822d7ecf9b27c5353b2bd4203b0fbb4f4c241583b273ce352c62eb09b59ea", + "reference_corrected__singleton_1x1_r1": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "reference_corrected__singleton_1x1_r1024": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "reference_corrected__singleton_1x1_r20": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "reference_corrected__singleton_1x1_r3": "8a39439d008124c73db4f3c41b941530a9e08999c00c2b80178156f471d722a6", + "reference_corrected__singleton_column_r1": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "reference_corrected__singleton_column_r20": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "reference_corrected__singleton_column_r3": "b90348918aa0c746039e4891c95c72882d8a271f09eadb60ffd523798b23d03d", + "reference_corrected__singleton_row_r1": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "reference_corrected__singleton_row_r20": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "reference_corrected__singleton_row_r3": "8d3f028c79a314dc26cf9da56ea9c801def54293f49340d6a041d42178d4d8e0", + "reference_corrected__tall_r1": "2096acc9eaf4969b92e9a156e8af1d08747083128ceff92b647f88a3c5236a25", + "reference_corrected__tall_r2": "db4cd2345bbde0d1ad8353c0b2f16eb3b8943a319175836ef5ebe62fcbd3177a", + "reference_corrected__tall_r20": "fbd461fcb22ebc5122c696d084111ff4f81d5435bd3a33c7a8d4ad1eb5e8f427", + "reference_corrected__tall_r3": "8a6493968843f6a590800f45a820f9b1349510dc4345be287efefbb8b28dfe32", + "reference_corrected__tall_r4": "bfe4717df0982a911f39bc8f3afd0c0a1571e708dc50215a3a8aad3d6c9593f6", + "reference_corrected__wide_r1": "aa024e640cdfdeaa1b91b8fa964deaf67c4a1ef8e9917c32c71026ebc36927c1", + "reference_corrected__wide_r2": "bd35648198dec90ab5233c591109c687c0421994f7d209c99284aace73129862", + "reference_corrected__wide_r20": "b793073e8a658e6d0bd7f8d019fcd804056f9074c330b8d319d3b66b9d82f2a5", + "reference_corrected__wide_r3": "38e571bd73d4bd79cc7ed18ebecb3ff77ef1d70ec1bba750201079daa47603e4", + "reference_corrected__wide_r4": "72150de5367cc6a8e2b4288439f777bb4324656afaeef20f10e96b2a9b38e80d" + }, + "cases": 36, + "corrected_bitwise_exact_percentage_maximum": 100.0, + "corrected_bitwise_exact_percentage_minimum": 100.0, + "corrected_maximum_absolute_difference": 0.0, + "corrected_maximum_ulp": 0, + "independence": { + "gwyddion_execution": false, + "numpy_partition_used_instead_of_internal_gwyddion_selection": true, + "reference_arrays_loaded_only_after_oracle_outputs_calculated": true, + "scipy_import": false, + "spmkit_import": false, + "subprocess_import": false, + "tolerance_selected": false + }, + "nonfinite_cases": 0, + "oracle_reconstruction_maximum": 4.440892098500626e-16, + "reference_reconstruction_maximum": 4.440892098500626e-16, + "source_arrays": 180 + }, + "reference_software": { + "name": "Gwyddion", + "version": "2.71" + }, + "schema_version": 1, + "scope": { + "assertions": [ + "evidence is limited to the frozen 36-case campaign", + "both Gwyddion rank-filter backends are represented", + "no universal equivalence is claimed", + "no SPMKit implementation is validated by this fixture alone", + "errors outside the frozen domain are not excluded" + ] + }, + "semantics": { + "active_condition": "squared ellipse expression <= radius_squared", + "active_offsets": "centred by subtracting radius", + "corrected": "input - background", + "direct_backend_condition": "kernel_active_count <= 25", + "exterior": "GWY_EXTERIOR_BORDER_EXTEND", + "exterior_interpretation": "nearest valid edge pixel", + "input_mutation": false, + "kernel_geometry": "inclusive digital ellipse over pixel centres", + "kernel_resolution": "2*radius + 1", + "offset_order": "row-major", + "pixel_centre": "kernel_index + 0.5", + "radius_default": 20, + "radius_maximum": 1024, + "radius_minimum": 1, + "radius_type": "integer", + "radixtree_backend_condition": "kernel_active_count > 25", + "rank": "kernel_active_count//2" + }, + "source_artifacts": { + "campaign_summary": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/campaign-summary.tsv", + "sha256": "7876d9cf3bc61375ecff5ca42c16789e11f4f0cc651f330ed7da57b36fc493b2" + }, + "median_bg_c": { + "path": "gwyddion-2.71/source/modules/process/median-bg.c", + "sha256": "5021fff407531459ed47aff7a47e4f5b2ce2ea7df13d04ca4405f05581258729" + }, + "oracle_log": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/oracle.log", + "sha256": "87eec8d8509d6d879ab41d748e9c21cc1ae428e6157131320c5fe7de0635d059" + }, + "oracle_provenance": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/oracle_provenance.json", + "sha256": "fe759bafbd7180394f3262da13e19275475ae569c3f79f879a51c6b8025e0d74" + }, + "oracle_report": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/oracle_report.md", + "sha256": "1a11f8caec6456de78feb9679e3fe0bec81e101058bfd73806d9953665b8dd31" + }, + "oracle_script": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/oracle.py", + "sha256": "2696798b180fcce779bbded49131d106cdc8f159c30aa1df873008f04d66084b" + }, + "oracle_source_npz": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/oracle_arrays.npz", + "sha256": "d56117cb4bbfc182d9fdfb8a9f6d2b400b5d5d8c17e051075342387b98dacc09" + }, + "oracle_summary": { + "note": "ephemeral source artifact; identity frozen by SHA-256", + "path": "frozen-evidence/median-background/oracle_summary.tsv", + "sha256": "0325066fd9a13cbc2b70d21473d2aeca783faa469a37d867fc6c11801c51b69f" + }, + "probe_c": { + "path": "gwyddion-2.71/median-background-parity/median_background_behavior_probe.c", + "sha256": "8e1956a6dbc69afcf5244098bc930f529c733bdec2768909cb7372ccea260f10" + }, + "runner": { + "path": "gwyddion-2.71/median-background-parity/run_median_background_probe_campaign.sh", + "sha256": "8c1c42dba8fc8a36bb93d1a82e60257dca3ff5044a2987b452608f99115ab594" + } + } +} diff --git a/tests/validation/fixtures/gwyddion/median_background/median_background_reference.npz b/tests/validation/fixtures/gwyddion/median_background/median_background_reference.npz new file mode 100644 index 0000000..3680a23 Binary files /dev/null and b/tests/validation/fixtures/gwyddion/median_background/median_background_reference.npz differ diff --git a/tests/validation/fixtures/gwyddion/path_level/path_level_reference.json b/tests/validation/fixtures/gwyddion/path_level/path_level_reference.json new file mode 100644 index 0000000..fd00959 --- /dev/null +++ b/tests/validation/fixtures/gwyddion/path_level/path_level_reference.json @@ -0,0 +1,4965 @@ +{ + "bases": [ + { + "base_id": "anisotropic_physical_coordinates", + "input_canonical_hash": "6a416ab164f1f7ab8e0e50ae9a44a60e20a1d809763d1ed766bbfb4d928f682a", + "input_key": "input__anisotropic_physical_coordinates", + "input_sha256": "34de16e687ce994aba2ffcd923f193dc229d66720326ce1d25adcd2cc4fb6c81", + "shape": [ + 8, + 9 + ], + "tags": [ + "anisotropic_range", + "physical_to_pixel", + "fractional" + ], + "xoffset": 2.5, + "xreal": 13.0, + "yoffset": -1.25, + "yreal": 7.0 + }, + { + "base_id": "constant_horizontal", + "input_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "input_key": "input__constant_horizontal", + "input_sha256": "6124b8b219e263c168d9ced67da10ba711273d4abe8173adc71b29292ebad730", + "shape": [ + 6, + 7 + ], + "tags": [ + "constant", + "horizontal", + "horizontal_noop_control" + ], + "xoffset": 0.0, + "xreal": 7.0, + "yoffset": 0.0, + "yreal": 6.0 + }, + { + "base_id": "constant_no_lines", + "input_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "input_key": "input__constant_no_lines", + "input_sha256": "6124b8b219e263c168d9ced67da10ba711273d4abe8173adc71b29292ebad730", + "shape": [ + 6, + 7 + ], + "tags": [ + "constant", + "no_lines" + ], + "xoffset": 0.0, + "xreal": 7.0, + "yoffset": 0.0, + "yreal": 6.0 + }, + { + "base_id": "floor_c_truncation_starts_ends", + "input_canonical_hash": "b4e18dfcabe627710e38d3a2dc0d5d60c425927c484c442c3c7fe04490a014a2", + "input_key": "input__floor_c_truncation_starts_ends", + "input_sha256": "c66afcbd6acc665c817399d3abf4229e5744ccd078c7246dc933e635f969cba5", + "shape": [ + 9, + 9 + ], + "tags": [ + "endpoint_floor", + "c_truncation", + "starts_before_ends", + "horizontal_exclusion" + ], + "xoffset": 0.0, + "xreal": 9.0, + "yoffset": 0.0, + "yreal": 9.0 + }, + { + "base_id": "irregular_outside_endpoints", + "input_canonical_hash": "5d3b9265e0c3f0c1c04a2ed6496adad7567fcdbcac5de7a42e1b7dad31a9979e", + "input_key": "input__irregular_outside_endpoints", + "input_sha256": "a1fd7497c6c860b2837f06f8c50289fd06407f307778faae65dcb43f87cdde21", + "shape": [ + 9, + 11 + ], + "tags": [ + "irregular", + "outside_endpoints", + "clamp" + ], + "xoffset": 0.0, + "xreal": 11.0, + "yoffset": 0.0, + "yreal": 9.0 + }, + { + "base_id": "line_order_a", + "input_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "input_key": "input__line_order_a", + "input_sha256": "7f6192489fe38405e1b71a4a8f2ccd3d36ec1e89679a82e58390c27c946db924", + "shape": [ + 10, + 11 + ], + "tags": [ + "line_id_order", + "same_start_end", + "overlap", + "floating_sum_order" + ], + "xoffset": 0.0, + "xreal": 11.0, + "yoffset": 0.0, + "yreal": 10.0 + }, + { + "base_id": "line_order_b_permuted", + "input_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "input_key": "input__line_order_b_permuted", + "input_sha256": "7f6192489fe38405e1b71a4a8f2ccd3d36ec1e89679a82e58390c27c946db924", + "shape": [ + 10, + 11 + ], + "tags": [ + "line_id_order", + "permuted_object_order", + "same_start_end", + "floating_sum_order" + ], + "xoffset": 0.0, + "xreal": 11.0, + "yoffset": 0.0, + "yreal": 10.0 + }, + { + "base_id": "negative_impulse_reversed", + "input_canonical_hash": "03658489207560a26e8f9384c9038edae9c588d11c22b1356cd782edd05e0f2d", + "input_key": "input__negative_impulse_reversed", + "input_sha256": "50614a2893c88b9cf749f15ba45c068d4b9c42a498450fd18327e823a2ed466e", + "shape": [ + 9, + 9 + ], + "tags": [ + "negative_impulse", + "reversed_endpoints", + "normalization" + ], + "xoffset": 0.0, + "xreal": 9.0, + "yoffset": 0.0, + "yreal": 9.0 + }, + { + "base_id": "plateau_signed_zero_partial_clamp", + "input_canonical_hash": "68173761ff74a584231639a78d02744a65601a59c3ef3fc29ac29ad7fabdbc8e", + "input_key": "input__plateau_signed_zero_partial_clamp", + "input_sha256": "cce846fe062457eb38d481a3220f087a621cac10461b7445019e932388be3c9f", + "shape": [ + 6, + 8 + ], + "tags": [ + "plateau", + "ties", + "signed_zero", + "partially_clamped", + "window_clamp" + ], + "xoffset": 0.0, + "xreal": 8.0, + "yoffset": 0.0, + "yreal": 6.0 + }, + { + "base_id": "positive_impulse_fractional", + "input_canonical_hash": "9c00590617b05138f45aaf221281c78ae41defb9816108660b68e100dee68b29", + "input_key": "input__positive_impulse_fractional", + "input_sha256": "bc9754a349f10caa54b4b9e8033801771f722c4e2edb64625037e990d43edc46", + "shape": [ + 9, + 9 + ], + "tags": [ + "positive_impulse", + "fractional_endpoints" + ], + "xoffset": 0.0, + "xreal": 9.0, + "yoffset": 0.0, + "yreal": 9.0 + }, + { + "base_id": "row_offset_duplicate_overlap", + "input_canonical_hash": "085a24234af7acd58e17ab21c025e997739175748553ce19f389faf4d0fb5c40", + "input_key": "input__row_offset_duplicate_overlap", + "input_sha256": "d9ca38fc110c7a823714db55f4cc2147d4c07f51f51a03534966ae2de4d9dac8", + "shape": [ + 8, + 9 + ], + "tags": [ + "row_offset", + "duplicate", + "overlap", + "multiplicity" + ], + "xoffset": 0.0, + "xreal": 9.0, + "yoffset": 0.0, + "yreal": 8.0 + }, + { + "base_id": "signed_gradient_positive_slope", + "input_canonical_hash": "66be0573d9152154da4b83a74c6685149caf3312eff2af2c3b7c9109d77877a8", + "input_key": "input__signed_gradient_positive_slope", + "input_sha256": "09454df9b633f36abd7d8cd81958d90689d1bd69011ceceb92638724d0b2161f", + "shape": [ + 7, + 8 + ], + "tags": [ + "signed_gradient", + "positive_slope", + "first_last_rows" + ], + "xoffset": 0.0, + "xreal": 8.0, + "yoffset": 0.0, + "yreal": 7.0 + }, + { + "base_id": "singleton_1x1_no_lines", + "input_canonical_hash": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input_key": "input__singleton_1x1_no_lines", + "input_sha256": "f52df18731eea8d020801fe2c6b3164648d9d81256a6c37964533a25999961d3", + "shape": [ + 1, + 1 + ], + "tags": [ + "singleton", + "no_lines" + ], + "xoffset": 0.0, + "xreal": 1.0, + "yoffset": 0.0, + "yreal": 1.0 + }, + { + "base_id": "singleton_column_9x1_vertical", + "input_canonical_hash": "fe3c64e8997d8cfb4c242bc6a100fd8ace0308a31e873052e6b4f22bf1bd1e6a", + "input_key": "input__singleton_column_9x1_vertical", + "input_sha256": "16c63e75e56a8d16a04a77bfa9c93fea6748b8ac932abd8a37e7943f0e2179a0", + "shape": [ + 9, + 1 + ], + "tags": [ + "singleton_column", + "vertical" + ], + "xoffset": 0.0, + "xreal": 1.0, + "yoffset": 0.0, + "yreal": 9.0 + }, + { + "base_id": "singleton_row_1x9_horizontal", + "input_canonical_hash": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "input_key": "input__singleton_row_1x9_horizontal", + "input_sha256": "d52da1d0188e4b146a5b19363211229ed0586dc2114e7da30ccf2ea66e118222", + "shape": [ + 1, + 9 + ], + "tags": [ + "singleton_row", + "horizontal", + "signed_zero", + "ties" + ], + "xoffset": 0.0, + "xreal": 9.0, + "yoffset": 0.0, + "yreal": 1.0 + }, + { + "base_id": "step_negative_slope", + "input_canonical_hash": "17b72e293ae5970773ecc3f728d5a83f6b7318a77857f5f92a7c5d324974611b", + "input_key": "input__step_negative_slope", + "input_sha256": "f0f28d2cfc1dd0a82ab57351e44a8f7f9f7b321084f29ad49c0b03233186ac83", + "shape": [ + 8, + 10 + ], + "tags": [ + "step", + "negative_slope" + ], + "xoffset": 0.0, + "xreal": 10.0, + "yoffset": 0.0, + "yreal": 8.0 + }, + { + "base_id": "tall_edge_window", + "input_canonical_hash": "d03a7dd325f7d70525564e56744303ad9751924f581710cdabc8499b679f9368", + "input_key": "input__tall_edge_window", + "input_sha256": "c5a2d6d3ebed6e18ca47962faa96a67827953ab98080c92b312471d7cda212b1", + "shape": [ + 17, + 5 + ], + "tags": [ + "tall", + "first_last_columns", + "edge_window" + ], + "xoffset": 0.0, + "xreal": 5.0, + "yoffset": 0.0, + "yreal": 17.0 + }, + { + "base_id": "wide_edge_window", + "input_canonical_hash": "ef163e701f48a3bc31df9816e555935fa7e95d79067ca791c509193c97f59b4f", + "input_key": "input__wide_edge_window", + "input_sha256": "f9311154b2c88b6b1d760f39f40efa93d25e92e06195c04a03eea10d5a1d8e6b", + "shape": [ + 5, + 17 + ], + "tags": [ + "wide", + "first_last_columns", + "edge_window" + ], + "xoffset": 0.0, + "xreal": 17.0, + "yoffset": 0.0, + "yreal": 5.0 + } + ], + "capability": "gwyddion_path_level", + "cases": [ + { + "base_id": "singleton_1x1_no_lines", + "case_id": "singleton_1x1_no_lines__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "output_key": "corrected__singleton_1x1_no_lines__t1", + "selection_count_unchanged": true, + "tags": [ + "singleton", + "no_lines" + ], + "thickness": 1 + }, + { + "base_id": "singleton_1x1_no_lines", + "case_id": "singleton_1x1_no_lines__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "output_key": "corrected__singleton_1x1_no_lines__t2", + "selection_count_unchanged": true, + "tags": [ + "singleton", + "no_lines" + ], + "thickness": 2 + }, + { + "base_id": "singleton_1x1_no_lines", + "case_id": "singleton_1x1_no_lines__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "output_key": "corrected__singleton_1x1_no_lines__t3", + "selection_count_unchanged": true, + "tags": [ + "singleton", + "no_lines" + ], + "thickness": 3 + }, + { + "base_id": "singleton_1x1_no_lines", + "case_id": "singleton_1x1_no_lines__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "output_key": "corrected__singleton_1x1_no_lines__t128", + "selection_count_unchanged": true, + "tags": [ + "singleton", + "no_lines" + ], + "thickness": 128 + }, + { + "base_id": "singleton_row_1x9_horizontal", + "case_id": "singleton_row_1x9_horizontal__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 0, + 0, + 8, + 0 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "output_key": "corrected__singleton_row_1x9_horizontal__t1", + "selection_count_unchanged": true, + "tags": [ + "singleton_row", + "horizontal", + "signed_zero", + "ties" + ], + "thickness": 1 + }, + { + "base_id": "singleton_row_1x9_horizontal", + "case_id": "singleton_row_1x9_horizontal__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 0, + 0, + 8, + 0 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "output_key": "corrected__singleton_row_1x9_horizontal__t2", + "selection_count_unchanged": true, + "tags": [ + "singleton_row", + "horizontal", + "signed_zero", + "ties" + ], + "thickness": 2 + }, + { + "base_id": "singleton_row_1x9_horizontal", + "case_id": "singleton_row_1x9_horizontal__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 0, + 0, + 8, + 0 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "output_key": "corrected__singleton_row_1x9_horizontal__t3", + "selection_count_unchanged": true, + "tags": [ + "singleton_row", + "horizontal", + "signed_zero", + "ties" + ], + "thickness": 3 + }, + { + "base_id": "singleton_row_1x9_horizontal", + "case_id": "singleton_row_1x9_horizontal__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 0, + 0, + 8, + 0 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000" + ], + "output_canonical_hash": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "output_key": "corrected__singleton_row_1x9_horizontal__t128", + "selection_count_unchanged": true, + "tags": [ + "singleton_row", + "horizontal", + "signed_zero", + "ties" + ], + "thickness": 128 + }, + { + "base_id": "singleton_column_9x1_vertical", + "case_id": "singleton_column_9x1_vertical__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "4008000000000000", + "4010000000000000", + "4014000000000000", + "4018000000000000", + "401c000000000000", + "4020000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "output_canonical_hash": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "output_key": "corrected__singleton_column_9x1_vertical__t1", + "selection_count_unchanged": true, + "tags": [ + "singleton_column", + "vertical" + ], + "thickness": 1 + }, + { + "base_id": "singleton_column_9x1_vertical", + "case_id": "singleton_column_9x1_vertical__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "4008000000000000", + "4010000000000000", + "4014000000000000", + "4018000000000000", + "401c000000000000", + "4020000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "output_canonical_hash": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "output_key": "corrected__singleton_column_9x1_vertical__t2", + "selection_count_unchanged": true, + "tags": [ + "singleton_column", + "vertical" + ], + "thickness": 2 + }, + { + "base_id": "singleton_column_9x1_vertical", + "case_id": "singleton_column_9x1_vertical__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "4008000000000000", + "4010000000000000", + "4014000000000000", + "4018000000000000", + "401c000000000000", + "4020000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "output_canonical_hash": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "output_key": "corrected__singleton_column_9x1_vertical__t3", + "selection_count_unchanged": true, + "tags": [ + "singleton_column", + "vertical" + ], + "thickness": 3 + }, + { + "base_id": "singleton_column_9x1_vertical", + "case_id": "singleton_column_9x1_vertical__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff0000000000000", + "4000000000000000", + "4008000000000000", + "4010000000000000", + "4014000000000000", + "4018000000000000", + "401c000000000000", + "4020000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000", + "3ff0000000000000" + ], + "output_canonical_hash": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "output_key": "corrected__singleton_column_9x1_vertical__t128", + "selection_count_unchanged": true, + "tags": [ + "singleton_column", + "vertical" + ], + "thickness": 128 + }, + { + "base_id": "constant_no_lines", + "case_id": "constant_no_lines__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_no_lines__t1", + "selection_count_unchanged": true, + "tags": [ + "constant", + "no_lines" + ], + "thickness": 1 + }, + { + "base_id": "constant_no_lines", + "case_id": "constant_no_lines__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_no_lines__t2", + "selection_count_unchanged": true, + "tags": [ + "constant", + "no_lines" + ], + "thickness": 2 + }, + { + "base_id": "constant_no_lines", + "case_id": "constant_no_lines__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_no_lines__t3", + "selection_count_unchanged": true, + "tags": [ + "constant", + "no_lines" + ], + "thickness": 3 + }, + { + "base_id": "constant_no_lines", + "case_id": "constant_no_lines__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 0, + "lines_hex": [], + "normalized_endpoints": [], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_no_lines__t128", + "selection_count_unchanged": true, + "tags": [ + "constant", + "no_lines" + ], + "thickness": 128 + }, + { + "base_id": "constant_horizontal", + "case_id": "constant_horizontal__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x1.8000000000000p+1", + "0x1.8000000000000p+2", + "0x1.8000000000000p+1" + ], + "normalized_endpoints": [ + 0, + 3, + 6, + 3 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_horizontal__t1", + "selection_count_unchanged": true, + "tags": [ + "constant", + "horizontal", + "horizontal_noop_control" + ], + "thickness": 1 + }, + { + "base_id": "constant_horizontal", + "case_id": "constant_horizontal__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x1.8000000000000p+1", + "0x1.8000000000000p+2", + "0x1.8000000000000p+1" + ], + "normalized_endpoints": [ + 0, + 3, + 6, + 3 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_horizontal__t2", + "selection_count_unchanged": true, + "tags": [ + "constant", + "horizontal", + "horizontal_noop_control" + ], + "thickness": 2 + }, + { + "base_id": "constant_horizontal", + "case_id": "constant_horizontal__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x1.8000000000000p+1", + "0x1.8000000000000p+2", + "0x1.8000000000000p+1" + ], + "normalized_endpoints": [ + 0, + 3, + 6, + 3 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_horizontal__t3", + "selection_count_unchanged": true, + "tags": [ + "constant", + "horizontal", + "horizontal_noop_control" + ], + "thickness": 3 + }, + { + "base_id": "constant_horizontal", + "case_id": "constant_horizontal__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x1.8000000000000p+1", + "0x1.8000000000000p+2", + "0x1.8000000000000p+1" + ], + "normalized_endpoints": [ + 0, + 3, + 6, + 3 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "output_key": "corrected__constant_horizontal__t128", + "selection_count_unchanged": true, + "tags": [ + "constant", + "horizontal", + "horizontal_noop_control" + ], + "thickness": 128 + }, + { + "base_id": "signed_gradient_positive_slope", + "case_id": "signed_gradient_positive_slope__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.8000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 6 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "401c000000000000", + "402c000000000000", + "4035000000000000", + "403c000000000000", + "4041800000000000", + "4041800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "0000000000000000" + ], + "output_canonical_hash": "98d9efe8c730be95299ccedbbfcadfa55c39c5baa5c57cf2eea805b1849b9ebc", + "output_key": "corrected__signed_gradient_positive_slope__t1", + "selection_count_unchanged": true, + "tags": [ + "signed_gradient", + "positive_slope", + "first_last_rows" + ], + "thickness": 1 + }, + { + "base_id": "signed_gradient_positive_slope", + "case_id": "signed_gradient_positive_slope__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.8000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 6 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "401c000000000000", + "402c000000000000", + "4035000000000000", + "403c000000000000", + "4041800000000000", + "4041800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "0000000000000000" + ], + "output_canonical_hash": "98d9efe8c730be95299ccedbbfcadfa55c39c5baa5c57cf2eea805b1849b9ebc", + "output_key": "corrected__signed_gradient_positive_slope__t2", + "selection_count_unchanged": true, + "tags": [ + "signed_gradient", + "positive_slope", + "first_last_rows" + ], + "thickness": 2 + }, + { + "base_id": "signed_gradient_positive_slope", + "case_id": "signed_gradient_positive_slope__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.8000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 6 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "401c000000000000", + "402c000000000000", + "4035000000000000", + "403c000000000000", + "4041800000000000", + "4045000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000" + ], + "output_canonical_hash": "7b0e9c2fafe8c614eb62559bc77e7b8bea4684a2a0503530b5988767f2b9af40", + "output_key": "corrected__signed_gradient_positive_slope__t3", + "selection_count_unchanged": true, + "tags": [ + "signed_gradient", + "positive_slope", + "first_last_rows" + ], + "thickness": 3 + }, + { + "base_id": "signed_gradient_positive_slope", + "case_id": "signed_gradient_positive_slope__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.8000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 6 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "401c000000000000", + "402c000000000000", + "4035000000000000", + "403c000000000000", + "4041800000000000", + "4045000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000", + "401c000000000000" + ], + "output_canonical_hash": "7b0e9c2fafe8c614eb62559bc77e7b8bea4684a2a0503530b5988767f2b9af40", + "output_key": "corrected__signed_gradient_positive_slope__t128", + "selection_count_unchanged": true, + "tags": [ + "signed_gradient", + "positive_slope", + "first_last_rows" + ], + "thickness": 128 + }, + { + "base_id": "row_offset_duplicate_overlap", + "case_id": "row_offset_duplicate_overlap__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 7, + 4, + 0, + 4, + 7, + 1, + 0, + 7, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4059000000000000", + "4069000000000000", + "4072c00000000000", + "4079000000000000", + "407f400000000000", + "4082c00000000000", + "4085e00000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000" + ], + "output_canonical_hash": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "output_key": "corrected__row_offset_duplicate_overlap__t1", + "selection_count_unchanged": true, + "tags": [ + "row_offset", + "duplicate", + "overlap", + "multiplicity" + ], + "thickness": 1 + }, + { + "base_id": "row_offset_duplicate_overlap", + "case_id": "row_offset_duplicate_overlap__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 7, + 4, + 0, + 4, + 7, + 1, + 0, + 7, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4059000000000000", + "4069000000000000", + "4072c00000000000", + "4079000000000000", + "407f400000000000", + "4082c00000000000", + "4085e00000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000" + ], + "output_canonical_hash": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "output_key": "corrected__row_offset_duplicate_overlap__t2", + "selection_count_unchanged": true, + "tags": [ + "row_offset", + "duplicate", + "overlap", + "multiplicity" + ], + "thickness": 2 + }, + { + "base_id": "row_offset_duplicate_overlap", + "case_id": "row_offset_duplicate_overlap__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 7, + 4, + 0, + 4, + 7, + 1, + 0, + 7, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4059000000000000", + "4069000000000000", + "4072c00000000000", + "4079000000000000", + "407f400000000000", + "4082c00000000000", + "4085e00000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000" + ], + "output_canonical_hash": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "output_key": "corrected__row_offset_duplicate_overlap__t3", + "selection_count_unchanged": true, + "tags": [ + "row_offset", + "duplicate", + "overlap", + "multiplicity" + ], + "thickness": 3 + }, + { + "base_id": "row_offset_duplicate_overlap", + "case_id": "row_offset_duplicate_overlap__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.c000000000000p+2", + "0x1.0000000000000p+0", + "0x0.0p+0", + "0x1.c000000000000p+2", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 7, + 4, + 0, + 4, + 7, + 1, + 0, + 7, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4059000000000000", + "4069000000000000", + "4072c00000000000", + "4079000000000000", + "407f400000000000", + "4082c00000000000", + "4085e00000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000", + "4059000000000000" + ], + "output_canonical_hash": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "output_key": "corrected__row_offset_duplicate_overlap__t128", + "selection_count_unchanged": true, + "tags": [ + "row_offset", + "duplicate", + "overlap", + "multiplicity" + ], + "thickness": 128 + }, + { + "base_id": "step_negative_slope", + "case_id": "step_negative_slope__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.2000000000000p+3", + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 9, + 0, + 0, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4000000000000000", + "4010000000000000", + "4018000000000000", + "4020000000000000", + "4024000000000000", + "4028000000000000", + "402c000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000" + ], + "output_canonical_hash": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "output_key": "corrected__step_negative_slope__t1", + "selection_count_unchanged": true, + "tags": [ + "step", + "negative_slope" + ], + "thickness": 1 + }, + { + "base_id": "step_negative_slope", + "case_id": "step_negative_slope__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.2000000000000p+3", + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 9, + 0, + 0, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4000000000000000", + "4010000000000000", + "4018000000000000", + "4020000000000000", + "4024000000000000", + "4028000000000000", + "402c000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000" + ], + "output_canonical_hash": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "output_key": "corrected__step_negative_slope__t2", + "selection_count_unchanged": true, + "tags": [ + "step", + "negative_slope" + ], + "thickness": 2 + }, + { + "base_id": "step_negative_slope", + "case_id": "step_negative_slope__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.2000000000000p+3", + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 9, + 0, + 0, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4000000000000000", + "4010000000000000", + "4018000000000000", + "4020000000000000", + "4024000000000000", + "4028000000000000", + "402c000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000" + ], + "output_canonical_hash": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "output_key": "corrected__step_negative_slope__t3", + "selection_count_unchanged": true, + "tags": [ + "step", + "negative_slope" + ], + "thickness": 3 + }, + { + "base_id": "step_negative_slope", + "case_id": "step_negative_slope__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.2000000000000p+3", + "0x0.0p+0", + "0x0.0p+0", + "0x1.c000000000000p+2" + ], + "normalized_endpoints": [ + 9, + 0, + 0, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4000000000000000", + "4010000000000000", + "4018000000000000", + "4020000000000000", + "4024000000000000", + "4028000000000000", + "402c000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000", + "4000000000000000" + ], + "output_canonical_hash": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "output_key": "corrected__step_negative_slope__t128", + "selection_count_unchanged": true, + "tags": [ + "step", + "negative_slope" + ], + "thickness": 128 + }, + { + "base_id": "positive_impulse_fractional", + "case_id": "positive_impulse_fractional__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.3333333333333p-1", + "0x1.0000000000000p-1", + "0x1.d99999999999ap+2", + "0x1.0666666666666p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff4000000000000", + "4004000000000000", + "400e000000000000", + "4014000000000000", + "c054f00000000000", + "c054a00000000000", + "c054500000000000", + "c054000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000", + "c056300000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000" + ], + "output_canonical_hash": "6c068627660abe9d58c50f8716d222b5619be8162566a9d2893804620eea2af6", + "output_key": "corrected__positive_impulse_fractional__t1", + "selection_count_unchanged": true, + "tags": [ + "positive_impulse", + "fractional_endpoints" + ], + "thickness": 1 + }, + { + "base_id": "positive_impulse_fractional", + "case_id": "positive_impulse_fractional__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.3333333333333p-1", + "0x1.0000000000000p-1", + "0x1.d99999999999ap+2", + "0x1.0666666666666p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff4000000000000", + "4004000000000000", + "400e000000000000", + "4049000000000000", + "4019000000000000", + "401e000000000000", + "4021800000000000", + "4024000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000", + "4047200000000000", + "c045e00000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000" + ], + "output_canonical_hash": "93c99554deb0fec35b49cebb7e78795bace6435849dd7655e915331ae3af1f93", + "output_key": "corrected__positive_impulse_fractional__t2", + "selection_count_unchanged": true, + "tags": [ + "positive_impulse", + "fractional_endpoints" + ], + "thickness": 2 + }, + { + "base_id": "positive_impulse_fractional", + "case_id": "positive_impulse_fractional__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.3333333333333p-1", + "0x1.0000000000000p-1", + "0x1.d99999999999ap+2", + "0x1.0666666666666p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff4000000000000", + "4004000000000000", + "400e000000000000", + "4041800000000000", + "4019000000000000", + "401e000000000000", + "4021800000000000", + "4024000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000", + "403f400000000000", + "c03cc00000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000" + ], + "output_canonical_hash": "e283f7dfa8b6f5e56e0b309a2355371fd74336159dcc48e4e48ceb62d2aa45b6", + "output_key": "corrected__positive_impulse_fractional__t3", + "selection_count_unchanged": true, + "tags": [ + "positive_impulse", + "fractional_endpoints" + ], + "thickness": 3 + }, + { + "base_id": "positive_impulse_fractional", + "case_id": "positive_impulse_fractional__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.3333333333333p-1", + "0x1.0000000000000p-1", + "0x1.d99999999999ap+2", + "0x1.0666666666666p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ff4000000000000", + "4004000000000000", + "400e000000000000", + "402e000000000000", + "4019000000000000", + "401e000000000000", + "4021800000000000", + "4024000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000", + "4026800000000000", + "c021800000000000", + "3ff4000000000000", + "3ff4000000000000", + "3ff4000000000000" + ], + "output_canonical_hash": "5b0046c9ba51d594ee4360a061443829afd33c490360ca259ba3e389ebdb3699", + "output_key": "corrected__positive_impulse_fractional__t128", + "selection_count_unchanged": true, + "tags": [ + "positive_impulse", + "fractional_endpoints" + ], + "thickness": 128 + }, + { + "base_id": "negative_impulse_reversed", + "case_id": "negative_impulse_reversed__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.c000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+0", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 1, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "bfe0000000000000", + "bff0000000000000", + "bff8000000000000", + "c000000000000000", + "c004000000000000", + "c008000000000000", + "c00c000000000000", + "c010000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000" + ], + "output_canonical_hash": "59520dc5ab335da577436c1c9cae0fd64d3dbcb9ea7aa3206b1fcfd51cc8efb9", + "output_key": "corrected__negative_impulse_reversed__t1", + "selection_count_unchanged": true, + "tags": [ + "negative_impulse", + "reversed_endpoints", + "normalization" + ], + "thickness": 1 + }, + { + "base_id": "negative_impulse_reversed", + "case_id": "negative_impulse_reversed__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.c000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+0", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 1, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "bfe0000000000000", + "bff0000000000000", + "bff8000000000000", + "c000000000000000", + "c004000000000000", + "c008000000000000", + "c00c000000000000", + "c010000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000" + ], + "output_canonical_hash": "59520dc5ab335da577436c1c9cae0fd64d3dbcb9ea7aa3206b1fcfd51cc8efb9", + "output_key": "corrected__negative_impulse_reversed__t2", + "selection_count_unchanged": true, + "tags": [ + "negative_impulse", + "reversed_endpoints", + "normalization" + ], + "thickness": 2 + }, + { + "base_id": "negative_impulse_reversed", + "case_id": "negative_impulse_reversed__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.c000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+0", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 1, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "bfe0000000000000", + "bff0000000000000", + "bff8000000000000", + "c000000000000000", + "c004000000000000", + "c008000000000000", + "c00c000000000000", + "c010000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000" + ], + "output_canonical_hash": "59520dc5ab335da577436c1c9cae0fd64d3dbcb9ea7aa3206b1fcfd51cc8efb9", + "output_key": "corrected__negative_impulse_reversed__t3", + "selection_count_unchanged": true, + "tags": [ + "negative_impulse", + "reversed_endpoints", + "normalization" + ], + "thickness": 3 + }, + { + "base_id": "negative_impulse_reversed", + "case_id": "negative_impulse_reversed__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.c000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+0", + "0x0.0p+0" + ], + "normalized_endpoints": [ + 1, + 0, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "bfe0000000000000", + "bff0000000000000", + "bff8000000000000", + "c000000000000000", + "c004000000000000", + "c026aaaaaaaaaaab", + "c00c000000000002", + "c010000000000001" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "bfe0000000000000", + "c021aaaaaaaaaaab", + "401f555555555555", + "bfe0000000000000" + ], + "output_canonical_hash": "bdf60bf5b78e5053cb51c565ccff49725691b19f2619c0e0e17fa6663157c08b", + "output_key": "corrected__negative_impulse_reversed__t128", + "selection_count_unchanged": true, + "tags": [ + "negative_impulse", + "reversed_endpoints", + "normalization" + ], + "thickness": 128 + }, + { + "base_id": "plateau_signed_zero_partial_clamp", + "case_id": "plateau_signed_zero_partial_clamp__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.8000000000000p+1", + "0x0.0p+0", + "0x1.4000000000000p+3", + "0x1.4000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 5 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4010000000000000", + "4010000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4010000000000000", + "0000000000000000" + ], + "output_canonical_hash": "aa07ce88e1d599f243c76465a6ce65156e805f0b85ba495dde97a34741a62f57", + "output_key": "corrected__plateau_signed_zero_partial_clamp__t1", + "selection_count_unchanged": true, + "tags": [ + "plateau", + "ties", + "signed_zero", + "partially_clamped", + "window_clamp" + ], + "thickness": 1 + }, + { + "base_id": "plateau_signed_zero_partial_clamp", + "case_id": "plateau_signed_zero_partial_clamp__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.8000000000000p+1", + "0x0.0p+0", + "0x1.4000000000000p+3", + "0x1.4000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 5 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4000000000000000", + "4000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "4000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "b2c7bd0472ccd4ce7467682ad9d69d4f8f3768892d337d923eeabf04467f3f9b", + "output_key": "corrected__plateau_signed_zero_partial_clamp__t2", + "selection_count_unchanged": true, + "tags": [ + "plateau", + "ties", + "signed_zero", + "partially_clamped", + "window_clamp" + ], + "thickness": 2 + }, + { + "base_id": "plateau_signed_zero_partial_clamp", + "case_id": "plateau_signed_zero_partial_clamp__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.8000000000000p+1", + "0x0.0p+0", + "0x1.4000000000000p+3", + "0x1.4000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 5 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff5555555555555", + "3ff5555555555555" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "3ff5555555555555", + "0000000000000000" + ], + "output_canonical_hash": "2688ef8ad5fa877e031007e52054202f5e01175efeb3b9be707fcaf05fa274a9", + "output_key": "corrected__plateau_signed_zero_partial_clamp__t3", + "selection_count_unchanged": true, + "tags": [ + "plateau", + "ties", + "signed_zero", + "partially_clamped", + "window_clamp" + ], + "thickness": 3 + }, + { + "base_id": "plateau_signed_zero_partial_clamp", + "case_id": "plateau_signed_zero_partial_clamp__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.8000000000000p+1", + "0x0.0p+0", + "0x1.4000000000000p+3", + "0x1.4000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 7, + 5 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "bfe0000000000000", + "bfe0000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "bfe0000000000000", + "0000000000000000", + "3fe0000000000000", + "0000000000000000" + ], + "output_canonical_hash": "08c4c42bec90733a5ff5be313afeaf1923fffbc97f796ca4b7f7df0b7de531db", + "output_key": "corrected__plateau_signed_zero_partial_clamp__t128", + "selection_count_unchanged": true, + "tags": [ + "plateau", + "ties", + "signed_zero", + "partially_clamped", + "window_clamp" + ], + "thickness": 128 + }, + { + "base_id": "irregular_outside_endpoints", + "case_id": "irregular_outside_endpoints__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.4000000000000p+2", + "-0x1.0000000000000p+1", + "0x1.e000000000000p+3", + "0x1.8000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 10, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c041800000000000", + "c04f800000000000", + "c055000000000000", + "c056c00000000000", + "c056c00000000000", + "c055000000000000", + "c05c400000000000", + "c05c400000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c041800000000000", + "c03c000000000000", + "c035000000000000", + "c01c000000000000", + "0000000000000000", + "401c000000000000", + "c03d000000000000", + "0000000000000000" + ], + "output_canonical_hash": "91800c865bcbac917f8b230b5539c17fe80d99b9f1637002eb9a473802ba9dd8", + "output_key": "corrected__irregular_outside_endpoints__t1", + "selection_count_unchanged": true, + "tags": [ + "irregular", + "outside_endpoints", + "clamp" + ], + "thickness": 1 + }, + { + "base_id": "irregular_outside_endpoints", + "case_id": "irregular_outside_endpoints__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.4000000000000p+2", + "-0x1.0000000000000p+1", + "0x1.e000000000000p+3", + "0x1.8000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 10, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c024000000000000", + "c02a000000000000", + "c03e800000000000", + "c041000000000000", + "c03e800000000000", + "c034000000000000", + "c038000000000000", + "c038000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c024000000000000", + "c008000000000000", + "c031800000000000", + "c00c000000000000", + "400c000000000000", + "4025000000000000", + "c010000000000000", + "0000000000000000" + ], + "output_canonical_hash": "3728f123d94ae7a56bf848d0a9598794f0bad0c1922b8bd2b2d7f2fbf7138474", + "output_key": "corrected__irregular_outside_endpoints__t2", + "selection_count_unchanged": true, + "tags": [ + "irregular", + "outside_endpoints", + "clamp" + ], + "thickness": 2 + }, + { + "base_id": "irregular_outside_endpoints", + "case_id": "irregular_outside_endpoints__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.4000000000000p+2", + "-0x1.0000000000000p+1", + "0x1.e000000000000p+3", + "0x1.8000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 10, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c019555555555555", + "c016aaaaaaaaaaaa", + "c028aaaaaaaaaaaa", + "c013ffffffffffff", + "4022aaaaaaaaaaac", + "4030555555555556", + "4030000000000001", + "c017fffffffffffc" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c019555555555555", + "3fe5555555555555", + "c01aaaaaaaaaaaab", + "401d555555555555", + "402caaaaaaaaaaab", + "401c000000000000", + "bfd5555555555555", + "c036000000000000" + ], + "output_canonical_hash": "adedd8d927e2807952907de66c79167ebdb95fbff3677293aa57cf3fb2ddbd12", + "output_key": "corrected__irregular_outside_endpoints__t3", + "selection_count_unchanged": true, + "tags": [ + "irregular", + "outside_endpoints", + "clamp" + ], + "thickness": 3 + }, + { + "base_id": "irregular_outside_endpoints", + "case_id": "irregular_outside_endpoints__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "-0x1.4000000000000p+2", + "-0x1.0000000000000p+1", + "0x1.e000000000000p+3", + "0x1.8000000000000p+3" + ], + "normalized_endpoints": [ + 0, + 0, + 10, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3ffa2e8ba2e8ba2f", + "bfe45d1745d1745e", + "c00745d1745d1746", + "bff45d1745d1745d", + "3fd745d1745d1748", + "bffe8ba2e8ba2e8c", + "bfd1745d1745d174", + "3ff5d1745d1745d2" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3ffa2e8ba2e8ba2f", + "c0022e8ba2e8ba2f", + "c0022e8ba2e8ba2f", + "3ffa2e8ba2e8ba2f", + "3ffa2e8ba2e8ba2f", + "c0022e8ba2e8ba2f", + "3ffa2e8ba2e8ba2f", + "3ffa2e8ba2e8ba2f" + ], + "output_canonical_hash": "685f5bbfbea8354a39680e76ac8f95d455948387eab02c8fe9bc0d89adc63d58", + "output_key": "corrected__irregular_outside_endpoints__t128", + "selection_count_unchanged": true, + "tags": [ + "irregular", + "outside_endpoints", + "clamp" + ], + "thickness": 128 + }, + { + "base_id": "wide_edge_window", + "case_id": "wide_edge_window__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 4 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4040c00000000000", + "4042000000000000", + "4043400000000000", + "4044800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4040c00000000000", + "4004000000000000", + "4004000000000000", + "4004000000000000" + ], + "output_canonical_hash": "1e68b58906514cebde1408fd722b3f3e63d3507a33ea2f8ec761cfe2a076864c", + "output_key": "corrected__wide_edge_window__t1", + "selection_count_unchanged": true, + "tags": [ + "wide", + "first_last_columns", + "edge_window" + ], + "thickness": 1 + }, + { + "base_id": "wide_edge_window", + "case_id": "wide_edge_window__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 4 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4032000000000000", + "4034800000000000", + "4037000000000000", + "4039800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4032000000000000", + "4004000000000000", + "4004000000000000", + "4004000000000000" + ], + "output_canonical_hash": "4a154ac77147d3967cdbcce6f6a633ccbf307c4c2956d5fab4183f1073a811db", + "output_key": "corrected__wide_edge_window__t2", + "selection_count_unchanged": true, + "tags": [ + "wide", + "first_last_columns", + "edge_window" + ], + "thickness": 2 + }, + { + "base_id": "wide_edge_window", + "case_id": "wide_edge_window__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 4 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4032000000000000", + "4034800000000000", + "4037000000000000", + "4039800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4032000000000000", + "4004000000000000", + "4004000000000000", + "4004000000000000" + ], + "output_canonical_hash": "4a154ac77147d3967cdbcce6f6a633ccbf307c4c2956d5fab4183f1073a811db", + "output_key": "corrected__wide_edge_window__t3", + "selection_count_unchanged": true, + "tags": [ + "wide", + "first_last_columns", + "edge_window" + ], + "thickness": 3 + }, + { + "base_id": "wide_edge_window", + "case_id": "wide_edge_window__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + "0x1.0000000000000p+2" + ], + "normalized_endpoints": [ + 0, + 0, + 0, + 4 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "40114b4b4b4b4b4b", + "401b4b4b4b4b4b4b", + "4022a5a5a5a5a5a6", + "402a969696969697" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "40114b4b4b4b4b4b", + "4004000000000000", + "4004000000000000", + "400fc3c3c3c3c3c4" + ], + "output_canonical_hash": "0d76a9c4f4c70739ba5254748a41602d81351fec2a8f1fb3b9c50090b06fa1a5", + "output_key": "corrected__wide_edge_window__t128", + "selection_count_unchanged": true, + "tags": [ + "wide", + "first_last_columns", + "edge_window" + ], + "thickness": 128 + }, + { + "base_id": "tall_edge_window", + "case_id": "tall_edge_window__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+4" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 16 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c030c00000000000", + "c032800000000000", + "c034400000000000", + "c036000000000000", + "c037c00000000000", + "c039800000000000", + "c03b400000000000", + "c03d000000000000", + "c03ec00000000000", + "c040400000000000", + "c041200000000000", + "c042000000000000", + "c042e00000000000", + "c043c00000000000", + "c044a00000000000", + "c045800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c030c00000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000" + ], + "output_canonical_hash": "743048c754fd6d005556ab0a551fe1db5082889247da7fbb3bd987a41ae3d72c", + "output_key": "corrected__tall_edge_window__t1", + "selection_count_unchanged": true, + "tags": [ + "tall", + "first_last_columns", + "edge_window" + ], + "thickness": 1 + }, + { + "base_id": "tall_edge_window", + "case_id": "tall_edge_window__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+4" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 16 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c030c00000000000", + "c032800000000000", + "c034400000000000", + "c036000000000000", + "c037c00000000000", + "c039800000000000", + "c03b400000000000", + "c03d000000000000", + "c03ec00000000000", + "c040400000000000", + "c041200000000000", + "c042000000000000", + "c042e00000000000", + "c043c00000000000", + "c044a00000000000", + "c045800000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c030c00000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000" + ], + "output_canonical_hash": "743048c754fd6d005556ab0a551fe1db5082889247da7fbb3bd987a41ae3d72c", + "output_key": "corrected__tall_edge_window__t2", + "selection_count_unchanged": true, + "tags": [ + "tall", + "first_last_columns", + "edge_window" + ], + "thickness": 2 + }, + { + "base_id": "tall_edge_window", + "case_id": "tall_edge_window__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+4" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 16 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c022800000000000", + "c026000000000000", + "c029800000000000", + "c02d000000000000", + "c030400000000000", + "c032000000000000", + "c033c00000000000", + "c035800000000000", + "c037400000000000", + "c039000000000000", + "c03ac00000000000", + "c03c800000000000", + "c03e400000000000", + "c040000000000000", + "c040e00000000000", + "c041c00000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c022800000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000" + ], + "output_canonical_hash": "6fcc09b9955d42781dec3ec9b71c4a1c8918b10709079c6ed4972346ff27722f", + "output_key": "corrected__tall_edge_window__t3", + "selection_count_unchanged": true, + "tags": [ + "tall", + "first_last_columns", + "edge_window" + ], + "thickness": 3 + }, + { + "base_id": "tall_edge_window", + "case_id": "tall_edge_window__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.0000000000000p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+4" + ], + "normalized_endpoints": [ + 4, + 0, + 4, + 16 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "c013000000000000", + "c01a000000000000", + "c020800000000000", + "c024000000000000", + "c027800000000000", + "c02b000000000000", + "c02e800000000000", + "c031000000000000", + "c032c00000000000", + "c034800000000000", + "c036400000000000", + "c038000000000000", + "c039c00000000000", + "c03b800000000000", + "c03d400000000000", + "c040666666666666" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "c013000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "bffc000000000000", + "c00c666666666666" + ], + "output_canonical_hash": "6690a460d28cfe4c05a3e3e2105f26443f8ab4a5805bc2c871b3e2979da63e73", + "output_key": "corrected__tall_edge_window__t128", + "selection_count_unchanged": true, + "tags": [ + "tall", + "first_last_columns", + "edge_window" + ], + "thickness": 128 + }, + { + "base_id": "anisotropic_physical_coordinates", + "case_id": "anisotropic_physical_coordinates__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.7333333333333p+0", + "0x1.8f5c28f5c28f6p-1", + "0x1.73d70a3d70a3dp+3", + "0x1.8b851eb851eb8p+2" + ], + "normalized_endpoints": [ + 1, + 0, + 8, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4008000000000000", + "4018000000000000", + "4022000000000000", + "4028000000000000", + "402e000000000000", + "4032000000000000", + "4032000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "0000000000000000" + ], + "output_canonical_hash": "65883b8732e68b57d3e323a6e0867b96e3579a41fcfa1f6c01a24f64471136c0", + "output_key": "corrected__anisotropic_physical_coordinates__t1", + "selection_count_unchanged": true, + "tags": [ + "anisotropic_range", + "physical_to_pixel", + "fractional" + ], + "thickness": 1 + }, + { + "base_id": "anisotropic_physical_coordinates", + "case_id": "anisotropic_physical_coordinates__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.7333333333333p+0", + "0x1.8f5c28f5c28f6p-1", + "0x1.73d70a3d70a3dp+3", + "0x1.8b851eb851eb8p+2" + ], + "normalized_endpoints": [ + 1, + 0, + 8, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4008000000000000", + "4018000000000000", + "4022000000000000", + "4028000000000000", + "402e000000000000", + "4032000000000000", + "4032000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "0000000000000000" + ], + "output_canonical_hash": "65883b8732e68b57d3e323a6e0867b96e3579a41fcfa1f6c01a24f64471136c0", + "output_key": "corrected__anisotropic_physical_coordinates__t2", + "selection_count_unchanged": true, + "tags": [ + "anisotropic_range", + "physical_to_pixel", + "fractional" + ], + "thickness": 2 + }, + { + "base_id": "anisotropic_physical_coordinates", + "case_id": "anisotropic_physical_coordinates__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.7333333333333p+0", + "0x1.8f5c28f5c28f6p-1", + "0x1.73d70a3d70a3dp+3", + "0x1.8b851eb851eb8p+2" + ], + "normalized_endpoints": [ + 1, + 0, + 8, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4008000000000000", + "4018000000000000", + "4022000000000000", + "4028000000000000", + "402e000000000000", + "4032000000000000", + "4035000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000" + ], + "output_canonical_hash": "4e9fbfa79553687bf0beca278d8b62caec8e63d52df889e6aa098ed926893ba2", + "output_key": "corrected__anisotropic_physical_coordinates__t3", + "selection_count_unchanged": true, + "tags": [ + "anisotropic_range", + "physical_to_pixel", + "fractional" + ], + "thickness": 3 + }, + { + "base_id": "anisotropic_physical_coordinates", + "case_id": "anisotropic_physical_coordinates__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 1, + "lines_hex": [ + "0x1.7333333333333p+0", + "0x1.8f5c28f5c28f6p-1", + "0x1.73d70a3d70a3dp+3", + "0x1.8b851eb851eb8p+2" + ], + "normalized_endpoints": [ + 1, + 0, + 8, + 7 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4008000000000000", + "4018000000000000", + "4022000000000000", + "4028000000000000", + "402e000000000000", + "4032000000000000", + "4035000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000", + "4008000000000000" + ], + "output_canonical_hash": "4e9fbfa79553687bf0beca278d8b62caec8e63d52df889e6aa098ed926893ba2", + "output_key": "corrected__anisotropic_physical_coordinates__t128", + "selection_count_unchanged": true, + "tags": [ + "anisotropic_range", + "physical_to_pixel", + "fractional" + ], + "thickness": 128 + }, + { + "base_id": "floor_c_truncation_starts_ends", + "case_id": "floor_c_truncation_starts_ends__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.799999999999ap+2", + "0x1.999999999999ap-4", + "0x1.999999999999ap-3", + "0x1.3333333333333p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+2", + "0x1.3333333333333p+0", + "0x1.0666666666666p+2", + "0x1.f333333333334p+2", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 5, + 0, + 0, + 4, + 0, + 4, + 8, + 4, + 1, + 4, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4026000000000000", + "4036000000000000", + "4040800000000000", + "4046000000000000", + "404b800000000000", + "4050800000000000", + "4053400000000000", + "4056000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000" + ], + "output_canonical_hash": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "output_key": "corrected__floor_c_truncation_starts_ends__t1", + "selection_count_unchanged": true, + "tags": [ + "endpoint_floor", + "c_truncation", + "starts_before_ends", + "horizontal_exclusion" + ], + "thickness": 1 + }, + { + "base_id": "floor_c_truncation_starts_ends", + "case_id": "floor_c_truncation_starts_ends__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.799999999999ap+2", + "0x1.999999999999ap-4", + "0x1.999999999999ap-3", + "0x1.3333333333333p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+2", + "0x1.3333333333333p+0", + "0x1.0666666666666p+2", + "0x1.f333333333334p+2", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 5, + 0, + 0, + 4, + 0, + 4, + 8, + 4, + 1, + 4, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4026000000000000", + "4036000000000000", + "4040800000000000", + "4046000000000000", + "404b800000000000", + "4050800000000000", + "4053400000000000", + "4056000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000" + ], + "output_canonical_hash": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "output_key": "corrected__floor_c_truncation_starts_ends__t2", + "selection_count_unchanged": true, + "tags": [ + "endpoint_floor", + "c_truncation", + "starts_before_ends", + "horizontal_exclusion" + ], + "thickness": 2 + }, + { + "base_id": "floor_c_truncation_starts_ends", + "case_id": "floor_c_truncation_starts_ends__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.799999999999ap+2", + "0x1.999999999999ap-4", + "0x1.999999999999ap-3", + "0x1.3333333333333p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+2", + "0x1.3333333333333p+0", + "0x1.0666666666666p+2", + "0x1.f333333333334p+2", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 5, + 0, + 0, + 4, + 0, + 4, + 8, + 4, + 1, + 4, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4026000000000000", + "4036000000000000", + "4040800000000000", + "4046000000000000", + "404b800000000000", + "4050800000000000", + "4053400000000000", + "4056000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000" + ], + "output_canonical_hash": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "output_key": "corrected__floor_c_truncation_starts_ends__t3", + "selection_count_unchanged": true, + "tags": [ + "endpoint_floor", + "c_truncation", + "starts_before_ends", + "horizontal_exclusion" + ], + "thickness": 3 + }, + { + "base_id": "floor_c_truncation_starts_ends", + "case_id": "floor_c_truncation_starts_ends__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.799999999999ap+2", + "0x1.999999999999ap-4", + "0x1.999999999999ap-3", + "0x1.3333333333333p+2", + "0x0.0p+0", + "0x1.0000000000000p+2", + "0x1.0000000000000p+3", + "0x1.0000000000000p+2", + "0x1.3333333333333p+0", + "0x1.0666666666666p+2", + "0x1.f333333333334p+2", + "0x1.0000000000000p+3" + ], + "normalized_endpoints": [ + 5, + 0, + 0, + 4, + 0, + 4, + 8, + 4, + 1, + 4, + 7, + 8 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "4026000000000000", + "4036000000000000", + "4040800000000000", + "4046000000000000", + "404b800000000000", + "4050800000000000", + "4053400000000000", + "4056000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000", + "4026000000000000" + ], + "output_canonical_hash": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "output_key": "corrected__floor_c_truncation_starts_ends__t128", + "selection_count_unchanged": true, + "tags": [ + "endpoint_floor", + "c_truncation", + "starts_before_ends", + "horizontal_exclusion" + ], + "thickness": 128 + }, + { + "base_id": "line_order_a", + "case_id": "line_order_a__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3", + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9, + 8, + 0, + 8, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "output_key": "corrected__line_order_a__t1", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "same_start_end", + "overlap", + "floating_sum_order" + ], + "thickness": 1 + }, + { + "base_id": "line_order_a", + "case_id": "line_order_a__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3", + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9, + 8, + 0, + 8, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "output_key": "corrected__line_order_a__t2", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "same_start_end", + "overlap", + "floating_sum_order" + ], + "thickness": 2 + }, + { + "base_id": "line_order_a", + "case_id": "line_order_a__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3", + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9, + 8, + 0, + 8, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "output_key": "corrected__line_order_a__t3", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "same_start_end", + "overlap", + "floating_sum_order" + ], + "thickness": 3 + }, + { + "base_id": "line_order_a", + "case_id": "line_order_a__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3", + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9, + 8, + 0, + 8, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "output_key": "corrected__line_order_a__t128", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "same_start_end", + "overlap", + "floating_sum_order" + ], + "thickness": 128 + }, + { + "base_id": "line_order_b_permuted", + "case_id": "line_order_b_permuted__t1", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3", + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 8, + 0, + 8, + 9, + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3fd5555555555555", + "3fe5555555555555", + "3ff0000000000000", + "3ff5555555555555", + "3ffaaaaaaaaaaaaa", + "3fffffffffffffff", + "4002aaaaaaaaaaaa", + "4005555555555555", + "4008000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555", + "3fd5555555555555" + ], + "output_canonical_hash": "8deb39b674d4f29c27a333036f49f9cebc630c935082d4c3de99e93f3e6c0f78", + "output_key": "corrected__line_order_b_permuted__t1", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "permuted_object_order", + "same_start_end", + "floating_sum_order" + ], + "thickness": 1 + }, + { + "base_id": "line_order_b_permuted", + "case_id": "line_order_b_permuted__t2", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3", + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 8, + 0, + 8, + 9, + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3fc5555555555555", + "3fd5555555555555", + "3fe0000000000000", + "3fe5555555555555", + "3feaaaaaaaaaaaaa", + "3fefffffffffffff", + "3ff2aaaaaaaaaaaa", + "3ff5555555555555", + "3ff8000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555", + "3fc5555555555555" + ], + "output_canonical_hash": "8349941775d84b6000b8d76a068c59c94005493cfcd9ab661723fb5f6b5fbd24", + "output_key": "corrected__line_order_b_permuted__t2", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "permuted_object_order", + "same_start_end", + "floating_sum_order" + ], + "thickness": 2 + }, + { + "base_id": "line_order_b_permuted", + "case_id": "line_order_b_permuted__t3", + "data_changed_signals": 1, + "external_mutation_of_data_field": true, + "external_no_op": false, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3", + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 8, + 0, + 8, + 9, + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "3fbc71c71c71c71c", + "3fcc71c71c71c71c", + "3fd5555555555555", + "3fdc71c71c71c71c", + "3fe1c71c71c71c72", + "3fe5555555555556", + "3fe8e38e38e38e3a", + "3fec71c71c71c71e", + "3ff0000000000001" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c", + "3fbc71c71c71c71c" + ], + "output_canonical_hash": "85561a2c0e1710ff158f7fa914fed201a96ee1fd03bda3929d6529b4cc6c725a", + "output_key": "corrected__line_order_b_permuted__t3", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "permuted_object_order", + "same_start_end", + "floating_sum_order" + ], + "thickness": 3 + }, + { + "base_id": "line_order_b_permuted", + "case_id": "line_order_b_permuted__t128", + "data_changed_signals": 1, + "external_mutation_of_data_field": false, + "external_no_op": true, + "line_count": 3, + "lines_hex": [ + "0x1.0000000000000p+3", + "0x0.0p+0", + "0x1.0000000000000p+3", + "0x1.2000000000000p+3", + "0x1.0000000000000p+1", + "0x0.0p+0", + "0x1.0000000000000p+1", + "0x1.2000000000000p+3", + "0x1.4000000000000p+2", + "0x0.0p+0", + "0x1.4000000000000p+2", + "0x1.2000000000000p+3" + ], + "normalized_endpoints": [ + 8, + 0, + 8, + 9, + 2, + 0, + 2, + 9, + 5, + 0, + 5, + 9 + ], + "oracle_cumulative_correction_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "oracle_row_differences_bits": [ + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + "0000000000000000" + ], + "output_canonical_hash": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "output_key": "corrected__line_order_b_permuted__t128", + "selection_count_unchanged": true, + "tags": [ + "line_id_order", + "permuted_object_order", + "same_start_end", + "floating_sum_order" + ], + "thickness": 128 + } + ], + "evidence": { + "external_executed_records": "canonical_reference.json", + "external_oracle_agreement": { + "exact_cases": 72, + "exact_elements": 4652, + "finite_nonzero_mismatches": 0, + "line_order_discriminator_external": true, + "line_order_discriminator_oracle": true, + "max_absolute_difference": 0.0, + "max_ulp_distance": 0, + "mutation_agreement": 72, + "no_op_agreement": 72, + "normalized_endpoint_agreement": 72, + "oracle_input_mutation_maximum": 0, + "schema_version": 1, + "signed_zero_mismatches": 0, + "total_cases": 72, + "total_elements": 4652 + }, + "independent_oracle_outputs": "comparison_ledger.json: 72/72 bitwise exact", + "independent_regeneration": "regenerated_input_report.json: 72/72", + "source_hashes": { + "external_artifacts": { + "build/asan_compile.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "build/asan_compile.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "build/normal_compile.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "build/normal_compile.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "campaign.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "campaign.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "canonical_reference.json": "5dcbd07836de0d6cd856dbfe620f7c24edded25a993c17472746b09e80902d84", + "case_definitions.json": "b133d6ab04a2c75cbaa276abed1be899f0ec914a6e12737ddb96c6e11c7f933d", + "cases/anisotropic_physical_coordinates__t1.ini": "afeffb0651e955cd314e1b2f7d4faf7ef93ce235ad51867924a16d13f0d1bcf1", + "cases/anisotropic_physical_coordinates__t128.ini": "eafbf974a7e5d6b15c2703cb5395f927799a3072e5314548e93528c01a2fd592", + "cases/anisotropic_physical_coordinates__t2.ini": "31bc9cc8a04ed75f9d9a62ce1cb8cc0deb068088b995702b95720b8bcaaffb29", + "cases/anisotropic_physical_coordinates__t3.ini": "ffce1f95486bd574a1adca818a1f15d14cee11d948ae61da8a69776896fd1abd", + "cases/constant_horizontal__t1.ini": "b3f96726abaf641889b16ccc770842a354e3853601da6e014b838283019f43fa", + "cases/constant_horizontal__t128.ini": "cf993d45a873e3003d008095df2d186056ee65d6110798ee305022fcd23612e8", + "cases/constant_horizontal__t2.ini": "67c8b3c950afea142e6eb621cdf66e11c08f51dbb94049588cd8b0addc9bd0a5", + "cases/constant_horizontal__t3.ini": "448eb1e988f5db1b8fbf315b26c1bbb5bb5e8cf5d9d337b9c9c0a3ce1b383ee8", + "cases/constant_no_lines__t1.ini": "00da4a12b37237b3e702c3f4d4712600f195fcea1cba390cf2e77db73f5003f6", + "cases/constant_no_lines__t128.ini": "d8919ea8a00ef4b8ff5afc7517587fa6453a588a7ba1f5e582c37298ac7ab298", + "cases/constant_no_lines__t2.ini": "adfc019941c3a7365dc05f41a8bd124eb2d6664b3de12211d9b1fc5d6ab53568", + "cases/constant_no_lines__t3.ini": "f3aab9d2490a2383cbfea1f0dc7efa3c50a974a894a5c6d09932177a47df88e6", + "cases/floor_c_truncation_starts_ends__t1.ini": "243719896c089a04e315f920e238914c302f3aa98153f41293aff1e39db215d8", + "cases/floor_c_truncation_starts_ends__t128.ini": "00ae6f0a119d05637828858f7bc9e686764b3f5a6dd5d37a599ae0f3ff5fadb9", + "cases/floor_c_truncation_starts_ends__t2.ini": "08c96406a73cc24b564b5ed836f7eaa005694e1e7d2ed96b4d8a5abb018182ae", + "cases/floor_c_truncation_starts_ends__t3.ini": "f4d1e2169492f373c16103c02d1ddbbd46e3f415a997caeebfe64a576334d315", + "cases/irregular_outside_endpoints__t1.ini": "a2f3ebcc8391e2c4f46f28a3a019cb78e2c236368c441f7f716a6567228a8377", + "cases/irregular_outside_endpoints__t128.ini": "425e6e661d48ff51aaaf5bb9691f2b8ed0ef9f684476f5ee8ade73bbb59e27cf", + "cases/irregular_outside_endpoints__t2.ini": "e0a63a25e8400450670a2e75e934fb7693e2af62a7ad3c3da205203e535f135f", + "cases/irregular_outside_endpoints__t3.ini": "0b6e1c561d31e94438c51d3f7fbd3b916ae44f9be2f7a7dd39a2f8fbad9d6a6f", + "cases/line_order_a__t1.ini": "bce8488351f47d16cbe8a3a451e12ede7b34aaf5c214f87931d4581a1c5b4eaa", + "cases/line_order_a__t128.ini": "ba1e3651cf3d794e869e6129d9873ab01cc14168f2525c32f898f2ef70c8f27c", + "cases/line_order_a__t2.ini": "8895a60770f348f9cfabed99c0b69ae1e76c3293dd8345ef015c07ab5a1e68e6", + "cases/line_order_a__t3.ini": "3f001f62306340ca31354185415eb1381f488ab6cbf07c1b16c195bbd55a44db", + "cases/line_order_b_permuted__t1.ini": "a9b3252e97a571518f72f93831b3bbbad40a865cd3c6f94eb77cda15b8acb503", + "cases/line_order_b_permuted__t128.ini": "450efbaf6e39c8ec2a21b14daaa11d1f52501e282008b99fbd3f8eb986041a12", + "cases/line_order_b_permuted__t2.ini": "5da01acad5692b29ef7bdc2386b8ce538d396de3114718741bf3730f631b6cb6", + "cases/line_order_b_permuted__t3.ini": "8c9a1a642bbd5a3bad084bd243e68566dac55bbe35a9f261db3865a078c308a9", + "cases/negative_impulse_reversed__t1.ini": "dad1e60b819354f5e022d75c0ea0ea74c46cf6dc6ad0a0d05c61cd5a5944255b", + "cases/negative_impulse_reversed__t128.ini": "d9fee15053724f6f44251dcc52654beef98527e7b1459a2f9b46573da1d0d220", + "cases/negative_impulse_reversed__t2.ini": "7cc4e82a5004f6fd79b07a48753cfd5b43372df490e30b0fc43954cf8127ce23", + "cases/negative_impulse_reversed__t3.ini": "6cc9d3848992c440d36c53e4b755e0762732845d761f7b5bace1ae644decea48", + "cases/plateau_signed_zero_partial_clamp__t1.ini": "09f1052921ec0ed95b5c713556bfcdb99de4fd473da29a153fe8bbd2d539d1e8", + "cases/plateau_signed_zero_partial_clamp__t128.ini": "c1a81ffd5fcec7bb671cb16e16bda12aa840c64d6fe2aa1e5b6f198f81e35f1f", + "cases/plateau_signed_zero_partial_clamp__t2.ini": "ddb9b7c3bffebae3e37e13b040f315da6177b10f75b790d64341b52f49551676", + "cases/plateau_signed_zero_partial_clamp__t3.ini": "378f8036042f0414478852b406e32c0c3590eb3565b3eaa9c9a6133037b33e78", + "cases/positive_impulse_fractional__t1.ini": "acef0ebdc8e7188f42cb3924ad8757cf3016a4de7fe968b46394edb82b6dc974", + "cases/positive_impulse_fractional__t128.ini": "6f2469efe5bec71452489d59cd596ca494bd72745a22a6f23b2d061fc165a2e0", + "cases/positive_impulse_fractional__t2.ini": "53f45bf596ca7abbd9732046cf9dae4e3402616b12e6af4b9fa78de5405a30ad", + "cases/positive_impulse_fractional__t3.ini": "c3e2a0e5e161ee17794173370a11a414200c51a6ffb1fe2f641043196a2cd1e5", + "cases/row_offset_duplicate_overlap__t1.ini": "16f78567c8c6a47cd215dc85c0da0efc12ef3a6a62ff9e7842fa891523d36c25", + "cases/row_offset_duplicate_overlap__t128.ini": "2e66e3da8ee4d4199f029c60546337dc58c8267fcc782e912604314ab86de1f8", + "cases/row_offset_duplicate_overlap__t2.ini": "7a3cf0664572f17f40f57723fae88b10e50855a8dfae496674f51c78cf644701", + "cases/row_offset_duplicate_overlap__t3.ini": "12fc3185939b4a81d1d79542e6add3f390ec47fc14334df321ddf2dc1ccfbf2b", + "cases/signed_gradient_positive_slope__t1.ini": "f882823f414417afdb283e39e47ec922949a2f7afb232b8db8f9c4ab10b499c0", + "cases/signed_gradient_positive_slope__t128.ini": "38b388380a86a72e6804f1adf174228e958b89a011d3f4bfb809d27a4889c47b", + "cases/signed_gradient_positive_slope__t2.ini": "4ff52d1eaf06e94100610311034c33a915f563ab56b038503dad4ea034bd532e", + "cases/signed_gradient_positive_slope__t3.ini": "52601b0fc9dcfc3091da243545211b673dbacf87ea93cf3c2013a28a12e24341", + "cases/singleton_1x1_no_lines__t1.ini": "705fe80d58e6ee6f1ae9dc34fe6a7db5c4d10471db9f80f35b45f48b6e267d5c", + "cases/singleton_1x1_no_lines__t128.ini": "1c440db00ee86b806a7771a370535a1a490278e2b16777eb8b463bc41b2ac303", + "cases/singleton_1x1_no_lines__t2.ini": "006d758bf8c286943af75614473dd0641bb8c434ae9ae3f8aa691d31085fc409", + "cases/singleton_1x1_no_lines__t3.ini": "d70b16d124464138394c6adb4b0d7a17b911e3fa89f732692db2f63f270f9e11", + "cases/singleton_column_9x1_vertical__t1.ini": "e4c39202647b34baf415336b6a5fb15946b779ff99913da9b2c2c81cf604dccb", + "cases/singleton_column_9x1_vertical__t128.ini": "2f8a849760ef4d66eeeb060097164c3d9d2935a10ccba6fe754cfe643ceb9451", + "cases/singleton_column_9x1_vertical__t2.ini": "465c29c28ce563b3ee27696238399c919d7ac22a1cb573ee6c3d2f724fbc4a8b", + "cases/singleton_column_9x1_vertical__t3.ini": "7e829e7b19afb5c0348612e0d26120412413315119c3cc39e365136e218c83e8", + "cases/singleton_row_1x9_horizontal__t1.ini": "edb792b382e11428183cd2d18765d478e32a6dc61052d6246c0cea1dac3ebca0", + "cases/singleton_row_1x9_horizontal__t128.ini": "8c970ad1d29e56235a619f2b43c52a74d09f76ac6c98ffbe6f6aa1cb7d1b6d99", + "cases/singleton_row_1x9_horizontal__t2.ini": "905bed56f93f5ad4618794870005aca01f1c31bbcc5511cc7efba91b6bd60e62", + "cases/singleton_row_1x9_horizontal__t3.ini": "c0b167ff1b0711da58567cfb605155db7dad1759967e39ac1f9e0dcd806105e8", + "cases/step_negative_slope__t1.ini": "51a4bb4c2fefda5fda160dae20f11de3db1d12a94bc6e382974e31a92af25e42", + "cases/step_negative_slope__t128.ini": "2889917cb160eda58b031390e5bcb1831e5e6d5cbd4ff92cdbc35c946b01c863", + "cases/step_negative_slope__t2.ini": "3902965d669613084b53e6f76d53b3a6841e8518fbeb4544a0ecb52977e690a4", + "cases/step_negative_slope__t3.ini": "4266d291a9cec4c193e8154eb5f336357f038d2b41794af3bbffbbcd6b8650da", + "cases/tall_edge_window__t1.ini": "507045c9241eba005dcf12b7fd2e0f0cd040a6cb608e93d4de5657e913b1c0ae", + "cases/tall_edge_window__t128.ini": "f98e41ae17a0717637fc50eb17dcbc6bd27b2c263176d89919a06c9ccc96d516", + "cases/tall_edge_window__t2.ini": "956e1f398a7e53c1754f2954a64c8be306abfaf6e8d90e4149598b7c9172abe7", + "cases/tall_edge_window__t3.ini": "c16070880f30eadde78b1ddbca2323b7e9b622ed675a8d214be9233f8e3acb46", + "cases/wide_edge_window__t1.ini": "d9ad6680ddf0a1242126abbacfec353433e6ecb3b138808fd1dbe1dfdd53f420", + "cases/wide_edge_window__t128.ini": "900dd890d2914fd6f05712a6be798b5a7db8aa92336b3331ebbec1607fc055c2", + "cases/wide_edge_window__t2.ini": "72aa85e676eb9934ee573fd2aacb4d64b60d98e4d755aa96d7a8b099635933ba", + "cases/wide_edge_window__t3.ini": "1fe2f2be7bc2e89a4080198e80a059f1e917a5b26659bd012ba7db8a2c89db71", + "debugger_symbol_map.gdb": "26885933d3365af23f5ff7ff492336750a4139aa8dc40f8a6d0ab90b47268fab", + "debugger_symbol_map.log": "4359c571dbdb519a649bd9f073f272c02c2d609bd86792c1d67476aad8e061b4", + "debugger_symbol_mapping.json": "ed2a084bd91b10a6185c1a8e41ae0a360a1e208eaf54da58efc90c2c14bc76cb", + "debugger_trace.gdb": "67173ccee0b95e63ff010758959316dfde2bd51f8c37523c7ffd38cc322c8ff1", + "debugger_trace.log": "44924d5065b785d1d2664f7124f86411b858d275f827f2e8f5a3f00f7ef083ff", + "diagnostics/asan_harness_probe.stderr": "2b8775a9f736ed171b0ae6abf80b437e792466248400a352b432369730624fac", + "diagnostics/asan_harness_probe.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/manifest_verification/final_verify.log": "013ea7a0ba15684ca8489bb2329e1c42d6cb7f7c742f0d01c895496ac2306f2f", + "diagnostics/manifest_verification/initial_verify.log": "013ea7a0ba15684ca8489bb2329e1c42d6cb7f7c742f0d01c895496ac2306f2f", + "diagnostics/partial_attempt/build/asan_compile.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/build/asan_compile.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/build/normal_compile.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/build/normal_compile.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/campaign.stderr": "b0c6d60140c0c85eaa8e1a1e699932b72b1cf910c246c425a1e65a91c4d2326b", + "diagnostics/partial_attempt/campaign.stdout": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/case_definitions.json": "b133d6ab04a2c75cbaa276abed1be899f0ec914a6e12737ddb96c6e11c7f933d", + "diagnostics/partial_attempt/cases/anisotropic_physical_coordinates__t1.ini": "afeffb0651e955cd314e1b2f7d4faf7ef93ce235ad51867924a16d13f0d1bcf1", + "diagnostics/partial_attempt/cases/anisotropic_physical_coordinates__t128.ini": "eafbf974a7e5d6b15c2703cb5395f927799a3072e5314548e93528c01a2fd592", + "diagnostics/partial_attempt/cases/anisotropic_physical_coordinates__t2.ini": "31bc9cc8a04ed75f9d9a62ce1cb8cc0deb068088b995702b95720b8bcaaffb29", + "diagnostics/partial_attempt/cases/anisotropic_physical_coordinates__t3.ini": "ffce1f95486bd574a1adca818a1f15d14cee11d948ae61da8a69776896fd1abd", + "diagnostics/partial_attempt/cases/constant_horizontal__t1.ini": "b3f96726abaf641889b16ccc770842a354e3853601da6e014b838283019f43fa", + "diagnostics/partial_attempt/cases/constant_horizontal__t128.ini": "cf993d45a873e3003d008095df2d186056ee65d6110798ee305022fcd23612e8", + "diagnostics/partial_attempt/cases/constant_horizontal__t2.ini": "67c8b3c950afea142e6eb621cdf66e11c08f51dbb94049588cd8b0addc9bd0a5", + "diagnostics/partial_attempt/cases/constant_horizontal__t3.ini": "448eb1e988f5db1b8fbf315b26c1bbb5bb5e8cf5d9d337b9c9c0a3ce1b383ee8", + "diagnostics/partial_attempt/cases/constant_no_lines__t1.ini": "00da4a12b37237b3e702c3f4d4712600f195fcea1cba390cf2e77db73f5003f6", + "diagnostics/partial_attempt/cases/constant_no_lines__t128.ini": "d8919ea8a00ef4b8ff5afc7517587fa6453a588a7ba1f5e582c37298ac7ab298", + "diagnostics/partial_attempt/cases/constant_no_lines__t2.ini": "adfc019941c3a7365dc05f41a8bd124eb2d6664b3de12211d9b1fc5d6ab53568", + "diagnostics/partial_attempt/cases/constant_no_lines__t3.ini": "f3aab9d2490a2383cbfea1f0dc7efa3c50a974a894a5c6d09932177a47df88e6", + "diagnostics/partial_attempt/cases/floor_c_truncation_starts_ends__t1.ini": "243719896c089a04e315f920e238914c302f3aa98153f41293aff1e39db215d8", + "diagnostics/partial_attempt/cases/floor_c_truncation_starts_ends__t128.ini": "00ae6f0a119d05637828858f7bc9e686764b3f5a6dd5d37a599ae0f3ff5fadb9", + "diagnostics/partial_attempt/cases/floor_c_truncation_starts_ends__t2.ini": "08c96406a73cc24b564b5ed836f7eaa005694e1e7d2ed96b4d8a5abb018182ae", + "diagnostics/partial_attempt/cases/floor_c_truncation_starts_ends__t3.ini": "f4d1e2169492f373c16103c02d1ddbbd46e3f415a997caeebfe64a576334d315", + "diagnostics/partial_attempt/cases/irregular_outside_endpoints__t1.ini": "a2f3ebcc8391e2c4f46f28a3a019cb78e2c236368c441f7f716a6567228a8377", + "diagnostics/partial_attempt/cases/irregular_outside_endpoints__t128.ini": "425e6e661d48ff51aaaf5bb9691f2b8ed0ef9f684476f5ee8ade73bbb59e27cf", + "diagnostics/partial_attempt/cases/irregular_outside_endpoints__t2.ini": "e0a63a25e8400450670a2e75e934fb7693e2af62a7ad3c3da205203e535f135f", + "diagnostics/partial_attempt/cases/irregular_outside_endpoints__t3.ini": "0b6e1c561d31e94438c51d3f7fbd3b916ae44f9be2f7a7dd39a2f8fbad9d6a6f", + "diagnostics/partial_attempt/cases/line_order_a__t1.ini": "bce8488351f47d16cbe8a3a451e12ede7b34aaf5c214f87931d4581a1c5b4eaa", + "diagnostics/partial_attempt/cases/line_order_a__t128.ini": "ba1e3651cf3d794e869e6129d9873ab01cc14168f2525c32f898f2ef70c8f27c", + "diagnostics/partial_attempt/cases/line_order_a__t2.ini": "8895a60770f348f9cfabed99c0b69ae1e76c3293dd8345ef015c07ab5a1e68e6", + "diagnostics/partial_attempt/cases/line_order_a__t3.ini": "3f001f62306340ca31354185415eb1381f488ab6cbf07c1b16c195bbd55a44db", + "diagnostics/partial_attempt/cases/line_order_b_permuted__t1.ini": "a9b3252e97a571518f72f93831b3bbbad40a865cd3c6f94eb77cda15b8acb503", + "diagnostics/partial_attempt/cases/line_order_b_permuted__t128.ini": "450efbaf6e39c8ec2a21b14daaa11d1f52501e282008b99fbd3f8eb986041a12", + "diagnostics/partial_attempt/cases/line_order_b_permuted__t2.ini": "5da01acad5692b29ef7bdc2386b8ce538d396de3114718741bf3730f631b6cb6", + "diagnostics/partial_attempt/cases/line_order_b_permuted__t3.ini": "8c9a1a642bbd5a3bad084bd243e68566dac55bbe35a9f261db3865a078c308a9", + "diagnostics/partial_attempt/cases/negative_impulse_reversed__t1.ini": "dad1e60b819354f5e022d75c0ea0ea74c46cf6dc6ad0a0d05c61cd5a5944255b", + "diagnostics/partial_attempt/cases/negative_impulse_reversed__t128.ini": "d9fee15053724f6f44251dcc52654beef98527e7b1459a2f9b46573da1d0d220", + "diagnostics/partial_attempt/cases/negative_impulse_reversed__t2.ini": "7cc4e82a5004f6fd79b07a48753cfd5b43372df490e30b0fc43954cf8127ce23", + "diagnostics/partial_attempt/cases/negative_impulse_reversed__t3.ini": "6cc9d3848992c440d36c53e4b755e0762732845d761f7b5bace1ae644decea48", + "diagnostics/partial_attempt/cases/plateau_signed_zero_partial_clamp__t1.ini": "09f1052921ec0ed95b5c713556bfcdb99de4fd473da29a153fe8bbd2d539d1e8", + "diagnostics/partial_attempt/cases/plateau_signed_zero_partial_clamp__t128.ini": "c1a81ffd5fcec7bb671cb16e16bda12aa840c64d6fe2aa1e5b6f198f81e35f1f", + "diagnostics/partial_attempt/cases/plateau_signed_zero_partial_clamp__t2.ini": "ddb9b7c3bffebae3e37e13b040f315da6177b10f75b790d64341b52f49551676", + "diagnostics/partial_attempt/cases/plateau_signed_zero_partial_clamp__t3.ini": "378f8036042f0414478852b406e32c0c3590eb3565b3eaa9c9a6133037b33e78", + "diagnostics/partial_attempt/cases/positive_impulse_fractional__t1.ini": "acef0ebdc8e7188f42cb3924ad8757cf3016a4de7fe968b46394edb82b6dc974", + "diagnostics/partial_attempt/cases/positive_impulse_fractional__t128.ini": "6f2469efe5bec71452489d59cd596ca494bd72745a22a6f23b2d061fc165a2e0", + "diagnostics/partial_attempt/cases/positive_impulse_fractional__t2.ini": "53f45bf596ca7abbd9732046cf9dae4e3402616b12e6af4b9fa78de5405a30ad", + "diagnostics/partial_attempt/cases/positive_impulse_fractional__t3.ini": "c3e2a0e5e161ee17794173370a11a414200c51a6ffb1fe2f641043196a2cd1e5", + "diagnostics/partial_attempt/cases/row_offset_duplicate_overlap__t1.ini": "16f78567c8c6a47cd215dc85c0da0efc12ef3a6a62ff9e7842fa891523d36c25", + "diagnostics/partial_attempt/cases/row_offset_duplicate_overlap__t128.ini": "2e66e3da8ee4d4199f029c60546337dc58c8267fcc782e912604314ab86de1f8", + "diagnostics/partial_attempt/cases/row_offset_duplicate_overlap__t2.ini": "7a3cf0664572f17f40f57723fae88b10e50855a8dfae496674f51c78cf644701", + "diagnostics/partial_attempt/cases/row_offset_duplicate_overlap__t3.ini": "12fc3185939b4a81d1d79542e6add3f390ec47fc14334df321ddf2dc1ccfbf2b", + "diagnostics/partial_attempt/cases/signed_gradient_positive_slope__t1.ini": "f882823f414417afdb283e39e47ec922949a2f7afb232b8db8f9c4ab10b499c0", + "diagnostics/partial_attempt/cases/signed_gradient_positive_slope__t128.ini": "38b388380a86a72e6804f1adf174228e958b89a011d3f4bfb809d27a4889c47b", + "diagnostics/partial_attempt/cases/signed_gradient_positive_slope__t2.ini": "4ff52d1eaf06e94100610311034c33a915f563ab56b038503dad4ea034bd532e", + "diagnostics/partial_attempt/cases/signed_gradient_positive_slope__t3.ini": "52601b0fc9dcfc3091da243545211b673dbacf87ea93cf3c2013a28a12e24341", + "diagnostics/partial_attempt/cases/singleton_1x1_no_lines__t1.ini": "705fe80d58e6ee6f1ae9dc34fe6a7db5c4d10471db9f80f35b45f48b6e267d5c", + "diagnostics/partial_attempt/cases/singleton_1x1_no_lines__t128.ini": "1c440db00ee86b806a7771a370535a1a490278e2b16777eb8b463bc41b2ac303", + "diagnostics/partial_attempt/cases/singleton_1x1_no_lines__t2.ini": "006d758bf8c286943af75614473dd0641bb8c434ae9ae3f8aa691d31085fc409", + "diagnostics/partial_attempt/cases/singleton_1x1_no_lines__t3.ini": "d70b16d124464138394c6adb4b0d7a17b911e3fa89f732692db2f63f270f9e11", + "diagnostics/partial_attempt/cases/singleton_column_9x1_vertical__t1.ini": "e4c39202647b34baf415336b6a5fb15946b779ff99913da9b2c2c81cf604dccb", + "diagnostics/partial_attempt/cases/singleton_column_9x1_vertical__t128.ini": "2f8a849760ef4d66eeeb060097164c3d9d2935a10ccba6fe754cfe643ceb9451", + "diagnostics/partial_attempt/cases/singleton_column_9x1_vertical__t2.ini": "465c29c28ce563b3ee27696238399c919d7ac22a1cb573ee6c3d2f724fbc4a8b", + "diagnostics/partial_attempt/cases/singleton_column_9x1_vertical__t3.ini": "7e829e7b19afb5c0348612e0d26120412413315119c3cc39e365136e218c83e8", + "diagnostics/partial_attempt/cases/singleton_row_1x9_horizontal__t1.ini": "edb792b382e11428183cd2d18765d478e32a6dc61052d6246c0cea1dac3ebca0", + "diagnostics/partial_attempt/cases/singleton_row_1x9_horizontal__t128.ini": "8c970ad1d29e56235a619f2b43c52a74d09f76ac6c98ffbe6f6aa1cb7d1b6d99", + "diagnostics/partial_attempt/cases/singleton_row_1x9_horizontal__t2.ini": "905bed56f93f5ad4618794870005aca01f1c31bbcc5511cc7efba91b6bd60e62", + "diagnostics/partial_attempt/cases/singleton_row_1x9_horizontal__t3.ini": "c0b167ff1b0711da58567cfb605155db7dad1759967e39ac1f9e0dcd806105e8", + "diagnostics/partial_attempt/cases/step_negative_slope__t1.ini": "51a4bb4c2fefda5fda160dae20f11de3db1d12a94bc6e382974e31a92af25e42", + "diagnostics/partial_attempt/cases/step_negative_slope__t128.ini": "2889917cb160eda58b031390e5bcb1831e5e6d5cbd4ff92cdbc35c946b01c863", + "diagnostics/partial_attempt/cases/step_negative_slope__t2.ini": "3902965d669613084b53e6f76d53b3a6841e8518fbeb4544a0ecb52977e690a4", + "diagnostics/partial_attempt/cases/step_negative_slope__t3.ini": "4266d291a9cec4c193e8154eb5f336357f038d2b41794af3bbffbbcd6b8650da", + "diagnostics/partial_attempt/cases/tall_edge_window__t1.ini": "507045c9241eba005dcf12b7fd2e0f0cd040a6cb608e93d4de5657e913b1c0ae", + "diagnostics/partial_attempt/cases/tall_edge_window__t128.ini": "f98e41ae17a0717637fc50eb17dcbc6bd27b2c263176d89919a06c9ccc96d516", + "diagnostics/partial_attempt/cases/tall_edge_window__t2.ini": "956e1f398a7e53c1754f2954a64c8be306abfaf6e8d90e4149598b7c9172abe7", + "diagnostics/partial_attempt/cases/tall_edge_window__t3.ini": "c16070880f30eadde78b1ddbca2323b7e9b622ed675a8d214be9233f8e3acb46", + "diagnostics/partial_attempt/cases/wide_edge_window__t1.ini": "d9ad6680ddf0a1242126abbacfec353433e6ecb3b138808fd1dbe1dfdd53f420", + "diagnostics/partial_attempt/cases/wide_edge_window__t128.ini": "900dd890d2914fd6f05712a6be798b5a7db8aa92336b3331ebbec1607fc055c2", + "diagnostics/partial_attempt/cases/wide_edge_window__t2.ini": "72aa85e676eb9934ee573fd2aacb4d64b60d98e4d755aa96d7a8b099635933ba", + "diagnostics/partial_attempt/cases/wide_edge_window__t3.ini": "1fe2f2be7bc2e89a4080198e80a059f1e917a5b26659bd012ba7db8a2c89db71", + "diagnostics/partial_attempt/execution/xvfb.stderr": "8c1f239fee83cab6bc55f538764f9d775d4d6a84614fcf64468213888242479a", + "diagnostics/partial_attempt/identity.json": "4de945de9fb37e7c7545b93cd99d0ab4f9bc4b868358aceb7f870c4298aaa72f", + "diagnostics/partial_attempt/pathlevel_harness": "23be2b8e47f125001d28c5da5b3ce08b9830953dd3e0b44cd568778ff42aaedc", + "diagnostics/partial_attempt/pathlevel_harness_asan": "3bd261fd0865fa6cc4da038c8c4880518325596fd3a612d62385674063430149", + "diagnostics/partial_attempt/raw/asan/singleton_1x1_no_lines__t1.jsonl": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/asan/singleton_1x1_no_lines__t1.stderr": "42c2dc0696f4a9d3794fc6e4ff63b169187caae3ceda164df96da71467688302", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t1.jsonl": "66fc7aee07c798976f73d3c02fb548e49028ad7e7f6abf29c8030cfc7941f769", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t128.jsonl": "3efa4fabe260b9e66d500907f93de64b4588aa2087255b91ff7de57d7f34263d", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t2.jsonl": "404138bf058b8c3a20ce8d2a29668110ba5e61c32f43d93404cd97a620f9b557", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t3.jsonl": "c30b6124bb7e3759d63cdeb2394252b639fefc0628b88affae953b883de7d9d5", + "diagnostics/partial_attempt/raw/normal_repeat_1/anisotropic_physical_coordinates__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t1.jsonl": "535a241c5ecda16a314077fe31f5141291af7254b1d88dacbeced8fadeb011aa", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t128.jsonl": "6777a1a29438762a2a6a7a45c0d380932260153ec06cee73f489049b9d637e55", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t2.jsonl": "dd6aa64c6793656912579b1b8347fdd77108c93b0d44d386ec61b84a6f345add", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t3.jsonl": "7774997731b79e6e8c6b24aaeedccfbf8b316149d69790bdd0511c8e37ba135c", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t1.jsonl": "77003691bfd3aa5d7a94927da76d60f61e7c44cc8b16d66bd658fc7109eac64f", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t128.jsonl": "6558b2021d0ad386476b9110d184cbc22dedf5f5067056c35f1c6618ddd9e308", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t2.jsonl": "cb4865c48dd5790e798435e0316efe41d126fc3e9ab627f8aeb052de4cf18a21", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t3.jsonl": "f9a6f9bee105e071e10cfbb7c653c33ece183ae1c94ab50e1f664e150abda64d", + "diagnostics/partial_attempt/raw/normal_repeat_1/constant_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t1.jsonl": "36b143cdb980cab5e8071ea868d9b1f166c7cdad5dd0a52393e4edff84fa4f4a", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t128.jsonl": "c58f964b7f0a8eda2f5d80b782468b0a4100dbd175950249ec07fab9440e52f8", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t2.jsonl": "07712df1d4a00a650e04cd3bdb57e8c69fbda7075d2f8e568bacfee6d310dcbb", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t3.jsonl": "9cb882679721faef074484528f0f8e3231ead67cc30e6fa1b238a8c91d467441", + "diagnostics/partial_attempt/raw/normal_repeat_1/floor_c_truncation_starts_ends__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t1.jsonl": "c255b97d53f57c2c77db702bb095567dbcb68edc054b3627464cf54c5abb9bee", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t128.jsonl": "40b135f2915e413607c58e18bf34af7be5e361776575eef66adbda214219eb1f", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t2.jsonl": "bb29a722e508d7ab9cfb72ae5febd1c838cc9d6c801ebd21641c15cb6dfa1050", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t3.jsonl": "1f81f86278d44c3de4bf2a282859c24983def31a98592aa544b4ecc5dbe0a500", + "diagnostics/partial_attempt/raw/normal_repeat_1/irregular_outside_endpoints__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t1.jsonl": "28670e869a53fadc1ba83d8d616e6e41cc5553a8a7a2d22b65d1787b423410bb", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t128.jsonl": "6f9a24a117d7ed0ccc522de216e81c7366a6136612376676b4837853f84a4977", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t2.jsonl": "396b6f93a669928fcec52d2e9d9e975e9a9522793a2215137a973b858951252b", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t3.jsonl": "53b8fee46e78d2cb9d7bde1044f155c2ac848b2d71538079b9b795577e2e33b4", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_a__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t1.jsonl": "1b453d0198d8ff937e3706d860e2e12ac892ebddf227ddee2015bfb1242571c6", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t128.jsonl": "b6a40f9d746249999d27917a9bcbcfcf79024297bc82c6ed1b3e4b04c63a5ab9", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t2.jsonl": "abf9b94cab9cb091db9b517b061a23b281ea7cdcd07f92f1c09fc008a18d8214", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t3.jsonl": "099788f9100ed2c641f992863f7dd3a6c81f5b231d3e78dd2e863067ce4b9065", + "diagnostics/partial_attempt/raw/normal_repeat_1/line_order_b_permuted__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t1.jsonl": "0cd351c18af4e73f1997fd655a47ede75cc79283bbf97d9ef4f4066db2c3ba35", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t128.jsonl": "be2b21e6d6a1c2880906fcad50d957b0f27318349c4228648303967b0bcaba14", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t2.jsonl": "ae0ba650690b01e7bf6dfb97459f494bc10e1ee3b9c8f3db7afe84298c0fbbb4", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t3.jsonl": "0243715fd70d485949a67b955233890bf70e897f31291b6e4a02cad401461b0f", + "diagnostics/partial_attempt/raw/normal_repeat_1/negative_impulse_reversed__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t1.jsonl": "46ccd9715870233ba0d105ab31e6d84c3ff6714cd455a9a2ca867595b24772e3", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t128.jsonl": "443f0f61ff8a16356da13a58cf8f18ddffe64405f9ba85185200892ae5997225", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t2.jsonl": "b1dd38123d64d122a3c7502eb48ee7b60f6167b440af65a32a2dbe0d860cd36d", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t3.jsonl": "bd4247365e2c8033f51344b6a99a5a1650cd1119e83c658f695ed00b2e74cf4b", + "diagnostics/partial_attempt/raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t1.jsonl": "9d723351aea63732e2d0ee573ed7f351b120fe93621e3d51848f36237862ae7f", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t128.jsonl": "7cdb3d0f26e6e7f93e2d7356c7c934fe78d3fb6a5beab70608b6e402a4c69469", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t2.jsonl": "b597da1ff6853ccb8fe3e0ece556376928c91de025a9965e3cc7ae707ef0a713", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t3.jsonl": "0dc4f0c8e820990ceae418950823b5679da88e313f38763c36108e35a24ded9c", + "diagnostics/partial_attempt/raw/normal_repeat_1/positive_impulse_fractional__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t1.jsonl": "d5eecd56cd15d130ff99cf49805bd02a85bce77ac94db5e27f87807393750c30", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t128.jsonl": "5c81dc66c795754545fdfb74cd20f29c77729e89622ee281e036b24933a069f3", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t2.jsonl": "8f02ad5d5c5004c91dd93d0d66d4fe88dda07bcc4debf4ae08a5ed1cba665a85", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t3.jsonl": "ce256c127432b3373b0ac21b02903abd4d52021749132346f329dd80441a92d6", + "diagnostics/partial_attempt/raw/normal_repeat_1/row_offset_duplicate_overlap__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t1.jsonl": "b31e19897620beb8f1558e7d0c9823480ec5428f585dabb58afe0b1f80c21c75", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t128.jsonl": "3e36b2cfa144d46d97dffda93e5a5bcc53230f4f529ac0f5bbba213990f8a0a7", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t2.jsonl": "1cc65fe29df674f7033923f868c96274137ea852d8cd5816d43d995c32f86367", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t3.jsonl": "704952d3917ab78aad6f57c27210b042a3e8009b4e0674daa7c6825f15d40252", + "diagnostics/partial_attempt/raw/normal_repeat_1/signed_gradient_positive_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t1.jsonl": "756488cc3248d3dd6718206ebf1df55893af4c03e9fe311154053850e73fea31", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t128.jsonl": "6fc04d31ed9495ff92db78486a1700704285c3593754ce77f987e1518fb499a5", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t2.jsonl": "d9f779f7789ad702ab5662fef352a4ae69c01b5215ef397e30fb2d33baf3fe28", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t3.jsonl": "df1dafe9003ed907a08f5c309df38e073453211be313727bf4baeb32aff53fac", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_1x1_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t1.jsonl": "a001a1bc5de31de376b1511ef3d22d697eaef8ac2f4952fd95df3d1a6daf3fbc", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t128.jsonl": "9a137ab721715e85a068ac2de3d1751b5859d11cc8cb9ca6dd7e82033b904afa", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t2.jsonl": "ab5d901c07b821189eb7e43cd55ceb1879f9ae73af19c19c6cdde4dba5423c27", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t3.jsonl": "2e009c43502ae1979ab071cab49645d6d901200170f0011d50c1a148c8bca67b", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_column_9x1_vertical__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t1.jsonl": "4b3e866c5c653902f67f2e956f1fb6d21431a0be903c456dc044d94206b6578c", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t128.jsonl": "0cc09261440a582eb7e0099fc3cd6ae25f9f28be3e142de06cafc77b521e8f41", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t2.jsonl": "20536c1bae6847a545cc5c915df82df41d64a19d032fca99c00ccf50df47601d", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t3.jsonl": "9c341f78f8b425ec6b2777e40c4facda68104512041b21b177dc651fc1d4a6aa", + "diagnostics/partial_attempt/raw/normal_repeat_1/singleton_row_1x9_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t1.jsonl": "f78a39b04faca06cf606c71bf18b5e9550fddc12c8032f86e3a770916643cd6a", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t128.jsonl": "dd1003615bacce43a62223fdd6b749eba1d13c7967806a3de29c6bd22f5b254a", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t2.jsonl": "980022d97ae76b491998e50f197dce03a13d0945c756b39e44f5f067e7fdc320", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t3.jsonl": "d7e89e7b6ce2efb5bb7f21ccab09c2498fb36787988caac126d7909cd32a4b26", + "diagnostics/partial_attempt/raw/normal_repeat_1/step_negative_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t1.jsonl": "70fc151e0962d51ca44773eaa6fbbce0c27bda43f78ee2371d4c733508138502", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t128.jsonl": "723f91f5498f32b1d5f82285082d915fe6f6ddab320bb4361401c136f1d1fbbf", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t2.jsonl": "708369fb3cd60cc461db38d793b376e92c55807aef24fd934e6e93556ca33db5", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t3.jsonl": "39d763f011bb91d88831085274d3e029135558fcba7f035e0704f3dba898c98d", + "diagnostics/partial_attempt/raw/normal_repeat_1/tall_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t1.jsonl": "8ab9fdb5927e915ef40fcda1b95d89df245422a6b8d8039a3a3f0904b9c72a53", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t128.jsonl": "16144ef175f87574c5d7195cd2950bcff706602a7d0685077c8966bd2c7d1115", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t2.jsonl": "4d7cf4fa5e14406cb3af440b804b6605f796dc1eef92ad090094134a4b1065f5", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t3.jsonl": "e89e6d4f0a7fabd5463e2184ae24754e8ba2203d6f05e70f81bc65162e9e693b", + "diagnostics/partial_attempt/raw/normal_repeat_1/wide_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t1.jsonl": "66fc7aee07c798976f73d3c02fb548e49028ad7e7f6abf29c8030cfc7941f769", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t128.jsonl": "3efa4fabe260b9e66d500907f93de64b4588aa2087255b91ff7de57d7f34263d", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t2.jsonl": "404138bf058b8c3a20ce8d2a29668110ba5e61c32f43d93404cd97a620f9b557", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t3.jsonl": "c30b6124bb7e3759d63cdeb2394252b639fefc0628b88affae953b883de7d9d5", + "diagnostics/partial_attempt/raw/normal_repeat_2/anisotropic_physical_coordinates__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t1.jsonl": "535a241c5ecda16a314077fe31f5141291af7254b1d88dacbeced8fadeb011aa", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t128.jsonl": "6777a1a29438762a2a6a7a45c0d380932260153ec06cee73f489049b9d637e55", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t2.jsonl": "dd6aa64c6793656912579b1b8347fdd77108c93b0d44d386ec61b84a6f345add", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t3.jsonl": "7774997731b79e6e8c6b24aaeedccfbf8b316149d69790bdd0511c8e37ba135c", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t1.jsonl": "77003691bfd3aa5d7a94927da76d60f61e7c44cc8b16d66bd658fc7109eac64f", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t128.jsonl": "6558b2021d0ad386476b9110d184cbc22dedf5f5067056c35f1c6618ddd9e308", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t2.jsonl": "cb4865c48dd5790e798435e0316efe41d126fc3e9ab627f8aeb052de4cf18a21", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t3.jsonl": "f9a6f9bee105e071e10cfbb7c653c33ece183ae1c94ab50e1f664e150abda64d", + "diagnostics/partial_attempt/raw/normal_repeat_2/constant_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t1.jsonl": "36b143cdb980cab5e8071ea868d9b1f166c7cdad5dd0a52393e4edff84fa4f4a", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t128.jsonl": "c58f964b7f0a8eda2f5d80b782468b0a4100dbd175950249ec07fab9440e52f8", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t2.jsonl": "07712df1d4a00a650e04cd3bdb57e8c69fbda7075d2f8e568bacfee6d310dcbb", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t3.jsonl": "9cb882679721faef074484528f0f8e3231ead67cc30e6fa1b238a8c91d467441", + "diagnostics/partial_attempt/raw/normal_repeat_2/floor_c_truncation_starts_ends__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t1.jsonl": "c255b97d53f57c2c77db702bb095567dbcb68edc054b3627464cf54c5abb9bee", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t128.jsonl": "40b135f2915e413607c58e18bf34af7be5e361776575eef66adbda214219eb1f", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t2.jsonl": "bb29a722e508d7ab9cfb72ae5febd1c838cc9d6c801ebd21641c15cb6dfa1050", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t3.jsonl": "1f81f86278d44c3de4bf2a282859c24983def31a98592aa544b4ecc5dbe0a500", + "diagnostics/partial_attempt/raw/normal_repeat_2/irregular_outside_endpoints__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t1.jsonl": "28670e869a53fadc1ba83d8d616e6e41cc5553a8a7a2d22b65d1787b423410bb", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t128.jsonl": "6f9a24a117d7ed0ccc522de216e81c7366a6136612376676b4837853f84a4977", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t2.jsonl": "396b6f93a669928fcec52d2e9d9e975e9a9522793a2215137a973b858951252b", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t3.jsonl": "53b8fee46e78d2cb9d7bde1044f155c2ac848b2d71538079b9b795577e2e33b4", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_a__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t1.jsonl": "1b453d0198d8ff937e3706d860e2e12ac892ebddf227ddee2015bfb1242571c6", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t128.jsonl": "b6a40f9d746249999d27917a9bcbcfcf79024297bc82c6ed1b3e4b04c63a5ab9", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t2.jsonl": "abf9b94cab9cb091db9b517b061a23b281ea7cdcd07f92f1c09fc008a18d8214", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t3.jsonl": "099788f9100ed2c641f992863f7dd3a6c81f5b231d3e78dd2e863067ce4b9065", + "diagnostics/partial_attempt/raw/normal_repeat_2/line_order_b_permuted__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t1.jsonl": "0cd351c18af4e73f1997fd655a47ede75cc79283bbf97d9ef4f4066db2c3ba35", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t128.jsonl": "be2b21e6d6a1c2880906fcad50d957b0f27318349c4228648303967b0bcaba14", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t2.jsonl": "ae0ba650690b01e7bf6dfb97459f494bc10e1ee3b9c8f3db7afe84298c0fbbb4", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t3.jsonl": "0243715fd70d485949a67b955233890bf70e897f31291b6e4a02cad401461b0f", + "diagnostics/partial_attempt/raw/normal_repeat_2/negative_impulse_reversed__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t1.jsonl": "46ccd9715870233ba0d105ab31e6d84c3ff6714cd455a9a2ca867595b24772e3", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t128.jsonl": "443f0f61ff8a16356da13a58cf8f18ddffe64405f9ba85185200892ae5997225", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t2.jsonl": "b1dd38123d64d122a3c7502eb48ee7b60f6167b440af65a32a2dbe0d860cd36d", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t3.jsonl": "bd4247365e2c8033f51344b6a99a5a1650cd1119e83c658f695ed00b2e74cf4b", + "diagnostics/partial_attempt/raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t1.jsonl": "9d723351aea63732e2d0ee573ed7f351b120fe93621e3d51848f36237862ae7f", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t128.jsonl": "7cdb3d0f26e6e7f93e2d7356c7c934fe78d3fb6a5beab70608b6e402a4c69469", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t2.jsonl": "b597da1ff6853ccb8fe3e0ece556376928c91de025a9965e3cc7ae707ef0a713", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t3.jsonl": "0dc4f0c8e820990ceae418950823b5679da88e313f38763c36108e35a24ded9c", + "diagnostics/partial_attempt/raw/normal_repeat_2/positive_impulse_fractional__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t1.jsonl": "d5eecd56cd15d130ff99cf49805bd02a85bce77ac94db5e27f87807393750c30", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t128.jsonl": "5c81dc66c795754545fdfb74cd20f29c77729e89622ee281e036b24933a069f3", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t2.jsonl": "8f02ad5d5c5004c91dd93d0d66d4fe88dda07bcc4debf4ae08a5ed1cba665a85", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t3.jsonl": "ce256c127432b3373b0ac21b02903abd4d52021749132346f329dd80441a92d6", + "diagnostics/partial_attempt/raw/normal_repeat_2/row_offset_duplicate_overlap__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t1.jsonl": "b31e19897620beb8f1558e7d0c9823480ec5428f585dabb58afe0b1f80c21c75", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t128.jsonl": "3e36b2cfa144d46d97dffda93e5a5bcc53230f4f529ac0f5bbba213990f8a0a7", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t2.jsonl": "1cc65fe29df674f7033923f868c96274137ea852d8cd5816d43d995c32f86367", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t3.jsonl": "704952d3917ab78aad6f57c27210b042a3e8009b4e0674daa7c6825f15d40252", + "diagnostics/partial_attempt/raw/normal_repeat_2/signed_gradient_positive_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t1.jsonl": "756488cc3248d3dd6718206ebf1df55893af4c03e9fe311154053850e73fea31", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t128.jsonl": "6fc04d31ed9495ff92db78486a1700704285c3593754ce77f987e1518fb499a5", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t2.jsonl": "d9f779f7789ad702ab5662fef352a4ae69c01b5215ef397e30fb2d33baf3fe28", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t3.jsonl": "df1dafe9003ed907a08f5c309df38e073453211be313727bf4baeb32aff53fac", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_1x1_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t1.jsonl": "a001a1bc5de31de376b1511ef3d22d697eaef8ac2f4952fd95df3d1a6daf3fbc", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t128.jsonl": "9a137ab721715e85a068ac2de3d1751b5859d11cc8cb9ca6dd7e82033b904afa", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t2.jsonl": "ab5d901c07b821189eb7e43cd55ceb1879f9ae73af19c19c6cdde4dba5423c27", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t3.jsonl": "2e009c43502ae1979ab071cab49645d6d901200170f0011d50c1a148c8bca67b", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_column_9x1_vertical__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t1.jsonl": "4b3e866c5c653902f67f2e956f1fb6d21431a0be903c456dc044d94206b6578c", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t128.jsonl": "0cc09261440a582eb7e0099fc3cd6ae25f9f28be3e142de06cafc77b521e8f41", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t2.jsonl": "20536c1bae6847a545cc5c915df82df41d64a19d032fca99c00ccf50df47601d", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t3.jsonl": "9c341f78f8b425ec6b2777e40c4facda68104512041b21b177dc651fc1d4a6aa", + "diagnostics/partial_attempt/raw/normal_repeat_2/singleton_row_1x9_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t1.jsonl": "f78a39b04faca06cf606c71bf18b5e9550fddc12c8032f86e3a770916643cd6a", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t128.jsonl": "dd1003615bacce43a62223fdd6b749eba1d13c7967806a3de29c6bd22f5b254a", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t2.jsonl": "980022d97ae76b491998e50f197dce03a13d0945c756b39e44f5f067e7fdc320", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t3.jsonl": "d7e89e7b6ce2efb5bb7f21ccab09c2498fb36787988caac126d7909cd32a4b26", + "diagnostics/partial_attempt/raw/normal_repeat_2/step_negative_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t1.jsonl": "70fc151e0962d51ca44773eaa6fbbce0c27bda43f78ee2371d4c733508138502", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t128.jsonl": "723f91f5498f32b1d5f82285082d915fe6f6ddab320bb4361401c136f1d1fbbf", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t2.jsonl": "708369fb3cd60cc461db38d793b376e92c55807aef24fd934e6e93556ca33db5", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t3.jsonl": "39d763f011bb91d88831085274d3e029135558fcba7f035e0704f3dba898c98d", + "diagnostics/partial_attempt/raw/normal_repeat_2/tall_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t1.jsonl": "8ab9fdb5927e915ef40fcda1b95d89df245422a6b8d8039a3a3f0904b9c72a53", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t128.jsonl": "16144ef175f87574c5d7195cd2950bcff706602a7d0685077c8966bd2c7d1115", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t2.jsonl": "4d7cf4fa5e14406cb3af440b804b6605f796dc1eef92ad090094134a4b1065f5", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t3.jsonl": "e89e6d4f0a7fabd5463e2184ae24754e8ba2203d6f05e70f81bc65162e9e693b", + "diagnostics/partial_attempt/raw/normal_repeat_2/wide_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "execution/xvfb.stderr": "feed30a9679ec98db430a2141afeccb0fd00b95f8d2827a58e3297c793fec185", + "execution_records.json": "ea13871bc393117c0cdf4f4de640a39438a0721c7ea01dbaffc76c6b07e514b0", + "execution_trace.json": "69c2b96ad974c479acfb946ebaadf8abbaa5f81b99265ddc0725e203d817a489", + "harness_compile.log": "d3791a72246328d0afc2911db00f6077c0964e85aee5450d8d630b505c3e62cb", + "identity.json": "4de945de9fb37e7c7545b93cd99d0ab4f9bc4b868358aceb7f870c4298aaa72f", + "linked_library_identity.json": "7748f78e8ee2fb2d95a88e966a6cb60806d9bbb04cb7298d6e1b906f48614a19", + "pathlevel_harness": "23be2b8e47f125001d28c5da5b3ce08b9830953dd3e0b44cd568778ff42aaedc", + "pathlevel_harness.c": "467a275b834e3dcc4a4ee406ef13be053a68733c105392fc391ccfe3bcfadffd", + "pathlevel_harness_asan": "3bd261fd0865fa6cc4da038c8c4880518325596fd3a612d62385674063430149", + "prototype_compile.log": "3da82dc7be198dcdc7651ec390fc3e3b228155a22d66b56165c3b2b1ac2326ff", + "prototype_gdb.log": "1c6ed59e97712c374f3841c691a8c263ff1685593f4141351d3df4c75a322282", + "prototype_stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prototype_stdout.log": "ea808d13cf135f9cea525ebd0bf3674ed455d4f79d735beda9ed261bdbaf783e", + "provenance.json": "23c204722e19f6592595e3b278ebd2cf4adb8b2c2a771430551a6bfc857be704", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t1.jsonl": "66fc7aee07c798976f73d3c02fb548e49028ad7e7f6abf29c8030cfc7941f769", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t128.jsonl": "3efa4fabe260b9e66d500907f93de64b4588aa2087255b91ff7de57d7f34263d", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t2.jsonl": "404138bf058b8c3a20ce8d2a29668110ba5e61c32f43d93404cd97a620f9b557", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t3.jsonl": "c30b6124bb7e3759d63cdeb2394252b639fefc0628b88affae953b883de7d9d5", + "raw/normal_repeat_1/anisotropic_physical_coordinates__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_horizontal__t1.jsonl": "535a241c5ecda16a314077fe31f5141291af7254b1d88dacbeced8fadeb011aa", + "raw/normal_repeat_1/constant_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_horizontal__t128.jsonl": "6777a1a29438762a2a6a7a45c0d380932260153ec06cee73f489049b9d637e55", + "raw/normal_repeat_1/constant_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_horizontal__t2.jsonl": "dd6aa64c6793656912579b1b8347fdd77108c93b0d44d386ec61b84a6f345add", + "raw/normal_repeat_1/constant_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_horizontal__t3.jsonl": "7774997731b79e6e8c6b24aaeedccfbf8b316149d69790bdd0511c8e37ba135c", + "raw/normal_repeat_1/constant_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_no_lines__t1.jsonl": "77003691bfd3aa5d7a94927da76d60f61e7c44cc8b16d66bd658fc7109eac64f", + "raw/normal_repeat_1/constant_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_no_lines__t128.jsonl": "6558b2021d0ad386476b9110d184cbc22dedf5f5067056c35f1c6618ddd9e308", + "raw/normal_repeat_1/constant_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_no_lines__t2.jsonl": "cb4865c48dd5790e798435e0316efe41d126fc3e9ab627f8aeb052de4cf18a21", + "raw/normal_repeat_1/constant_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/constant_no_lines__t3.jsonl": "f9a6f9bee105e071e10cfbb7c653c33ece183ae1c94ab50e1f664e150abda64d", + "raw/normal_repeat_1/constant_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t1.jsonl": "36b143cdb980cab5e8071ea868d9b1f166c7cdad5dd0a52393e4edff84fa4f4a", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t128.jsonl": "c58f964b7f0a8eda2f5d80b782468b0a4100dbd175950249ec07fab9440e52f8", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t2.jsonl": "07712df1d4a00a650e04cd3bdb57e8c69fbda7075d2f8e568bacfee6d310dcbb", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t3.jsonl": "9cb882679721faef074484528f0f8e3231ead67cc30e6fa1b238a8c91d467441", + "raw/normal_repeat_1/floor_c_truncation_starts_ends__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/irregular_outside_endpoints__t1.jsonl": "c255b97d53f57c2c77db702bb095567dbcb68edc054b3627464cf54c5abb9bee", + "raw/normal_repeat_1/irregular_outside_endpoints__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/irregular_outside_endpoints__t128.jsonl": "40b135f2915e413607c58e18bf34af7be5e361776575eef66adbda214219eb1f", + "raw/normal_repeat_1/irregular_outside_endpoints__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/irregular_outside_endpoints__t2.jsonl": "bb29a722e508d7ab9cfb72ae5febd1c838cc9d6c801ebd21641c15cb6dfa1050", + "raw/normal_repeat_1/irregular_outside_endpoints__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/irregular_outside_endpoints__t3.jsonl": "1f81f86278d44c3de4bf2a282859c24983def31a98592aa544b4ecc5dbe0a500", + "raw/normal_repeat_1/irregular_outside_endpoints__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_a__t1.jsonl": "28670e869a53fadc1ba83d8d616e6e41cc5553a8a7a2d22b65d1787b423410bb", + "raw/normal_repeat_1/line_order_a__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_a__t128.jsonl": "6f9a24a117d7ed0ccc522de216e81c7366a6136612376676b4837853f84a4977", + "raw/normal_repeat_1/line_order_a__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_a__t2.jsonl": "396b6f93a669928fcec52d2e9d9e975e9a9522793a2215137a973b858951252b", + "raw/normal_repeat_1/line_order_a__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_a__t3.jsonl": "53b8fee46e78d2cb9d7bde1044f155c2ac848b2d71538079b9b795577e2e33b4", + "raw/normal_repeat_1/line_order_a__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_b_permuted__t1.jsonl": "1b453d0198d8ff937e3706d860e2e12ac892ebddf227ddee2015bfb1242571c6", + "raw/normal_repeat_1/line_order_b_permuted__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_b_permuted__t128.jsonl": "b6a40f9d746249999d27917a9bcbcfcf79024297bc82c6ed1b3e4b04c63a5ab9", + "raw/normal_repeat_1/line_order_b_permuted__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_b_permuted__t2.jsonl": "abf9b94cab9cb091db9b517b061a23b281ea7cdcd07f92f1c09fc008a18d8214", + "raw/normal_repeat_1/line_order_b_permuted__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/line_order_b_permuted__t3.jsonl": "099788f9100ed2c641f992863f7dd3a6c81f5b231d3e78dd2e863067ce4b9065", + "raw/normal_repeat_1/line_order_b_permuted__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/negative_impulse_reversed__t1.jsonl": "0cd351c18af4e73f1997fd655a47ede75cc79283bbf97d9ef4f4066db2c3ba35", + "raw/normal_repeat_1/negative_impulse_reversed__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/negative_impulse_reversed__t128.jsonl": "be2b21e6d6a1c2880906fcad50d957b0f27318349c4228648303967b0bcaba14", + "raw/normal_repeat_1/negative_impulse_reversed__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/negative_impulse_reversed__t2.jsonl": "ae0ba650690b01e7bf6dfb97459f494bc10e1ee3b9c8f3db7afe84298c0fbbb4", + "raw/normal_repeat_1/negative_impulse_reversed__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/negative_impulse_reversed__t3.jsonl": "0243715fd70d485949a67b955233890bf70e897f31291b6e4a02cad401461b0f", + "raw/normal_repeat_1/negative_impulse_reversed__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t1.jsonl": "46ccd9715870233ba0d105ab31e6d84c3ff6714cd455a9a2ca867595b24772e3", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t128.jsonl": "443f0f61ff8a16356da13a58cf8f18ddffe64405f9ba85185200892ae5997225", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t2.jsonl": "b1dd38123d64d122a3c7502eb48ee7b60f6167b440af65a32a2dbe0d860cd36d", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t3.jsonl": "bd4247365e2c8033f51344b6a99a5a1650cd1119e83c658f695ed00b2e74cf4b", + "raw/normal_repeat_1/plateau_signed_zero_partial_clamp__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/positive_impulse_fractional__t1.jsonl": "9d723351aea63732e2d0ee573ed7f351b120fe93621e3d51848f36237862ae7f", + "raw/normal_repeat_1/positive_impulse_fractional__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/positive_impulse_fractional__t128.jsonl": "7cdb3d0f26e6e7f93e2d7356c7c934fe78d3fb6a5beab70608b6e402a4c69469", + "raw/normal_repeat_1/positive_impulse_fractional__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/positive_impulse_fractional__t2.jsonl": "b597da1ff6853ccb8fe3e0ece556376928c91de025a9965e3cc7ae707ef0a713", + "raw/normal_repeat_1/positive_impulse_fractional__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/positive_impulse_fractional__t3.jsonl": "0dc4f0c8e820990ceae418950823b5679da88e313f38763c36108e35a24ded9c", + "raw/normal_repeat_1/positive_impulse_fractional__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t1.jsonl": "d5eecd56cd15d130ff99cf49805bd02a85bce77ac94db5e27f87807393750c30", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t128.jsonl": "5c81dc66c795754545fdfb74cd20f29c77729e89622ee281e036b24933a069f3", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t2.jsonl": "8f02ad5d5c5004c91dd93d0d66d4fe88dda07bcc4debf4ae08a5ed1cba665a85", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t3.jsonl": "ce256c127432b3373b0ac21b02903abd4d52021749132346f329dd80441a92d6", + "raw/normal_repeat_1/row_offset_duplicate_overlap__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/signed_gradient_positive_slope__t1.jsonl": "b31e19897620beb8f1558e7d0c9823480ec5428f585dabb58afe0b1f80c21c75", + "raw/normal_repeat_1/signed_gradient_positive_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/signed_gradient_positive_slope__t128.jsonl": "3e36b2cfa144d46d97dffda93e5a5bcc53230f4f529ac0f5bbba213990f8a0a7", + "raw/normal_repeat_1/signed_gradient_positive_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/signed_gradient_positive_slope__t2.jsonl": "1cc65fe29df674f7033923f868c96274137ea852d8cd5816d43d995c32f86367", + "raw/normal_repeat_1/signed_gradient_positive_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/signed_gradient_positive_slope__t3.jsonl": "704952d3917ab78aad6f57c27210b042a3e8009b4e0674daa7c6825f15d40252", + "raw/normal_repeat_1/signed_gradient_positive_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_1x1_no_lines__t1.jsonl": "756488cc3248d3dd6718206ebf1df55893af4c03e9fe311154053850e73fea31", + "raw/normal_repeat_1/singleton_1x1_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_1x1_no_lines__t128.jsonl": "6fc04d31ed9495ff92db78486a1700704285c3593754ce77f987e1518fb499a5", + "raw/normal_repeat_1/singleton_1x1_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_1x1_no_lines__t2.jsonl": "d9f779f7789ad702ab5662fef352a4ae69c01b5215ef397e30fb2d33baf3fe28", + "raw/normal_repeat_1/singleton_1x1_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_1x1_no_lines__t3.jsonl": "df1dafe9003ed907a08f5c309df38e073453211be313727bf4baeb32aff53fac", + "raw/normal_repeat_1/singleton_1x1_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t1.jsonl": "a001a1bc5de31de376b1511ef3d22d697eaef8ac2f4952fd95df3d1a6daf3fbc", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t128.jsonl": "9a137ab721715e85a068ac2de3d1751b5859d11cc8cb9ca6dd7e82033b904afa", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t2.jsonl": "ab5d901c07b821189eb7e43cd55ceb1879f9ae73af19c19c6cdde4dba5423c27", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t3.jsonl": "2e009c43502ae1979ab071cab49645d6d901200170f0011d50c1a148c8bca67b", + "raw/normal_repeat_1/singleton_column_9x1_vertical__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t1.jsonl": "4b3e866c5c653902f67f2e956f1fb6d21431a0be903c456dc044d94206b6578c", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t128.jsonl": "0cc09261440a582eb7e0099fc3cd6ae25f9f28be3e142de06cafc77b521e8f41", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t2.jsonl": "20536c1bae6847a545cc5c915df82df41d64a19d032fca99c00ccf50df47601d", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t3.jsonl": "9c341f78f8b425ec6b2777e40c4facda68104512041b21b177dc651fc1d4a6aa", + "raw/normal_repeat_1/singleton_row_1x9_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/step_negative_slope__t1.jsonl": "f78a39b04faca06cf606c71bf18b5e9550fddc12c8032f86e3a770916643cd6a", + "raw/normal_repeat_1/step_negative_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/step_negative_slope__t128.jsonl": "dd1003615bacce43a62223fdd6b749eba1d13c7967806a3de29c6bd22f5b254a", + "raw/normal_repeat_1/step_negative_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/step_negative_slope__t2.jsonl": "980022d97ae76b491998e50f197dce03a13d0945c756b39e44f5f067e7fdc320", + "raw/normal_repeat_1/step_negative_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/step_negative_slope__t3.jsonl": "d7e89e7b6ce2efb5bb7f21ccab09c2498fb36787988caac126d7909cd32a4b26", + "raw/normal_repeat_1/step_negative_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/tall_edge_window__t1.jsonl": "70fc151e0962d51ca44773eaa6fbbce0c27bda43f78ee2371d4c733508138502", + "raw/normal_repeat_1/tall_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/tall_edge_window__t128.jsonl": "723f91f5498f32b1d5f82285082d915fe6f6ddab320bb4361401c136f1d1fbbf", + "raw/normal_repeat_1/tall_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/tall_edge_window__t2.jsonl": "708369fb3cd60cc461db38d793b376e92c55807aef24fd934e6e93556ca33db5", + "raw/normal_repeat_1/tall_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/tall_edge_window__t3.jsonl": "39d763f011bb91d88831085274d3e029135558fcba7f035e0704f3dba898c98d", + "raw/normal_repeat_1/tall_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/wide_edge_window__t1.jsonl": "8ab9fdb5927e915ef40fcda1b95d89df245422a6b8d8039a3a3f0904b9c72a53", + "raw/normal_repeat_1/wide_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/wide_edge_window__t128.jsonl": "16144ef175f87574c5d7195cd2950bcff706602a7d0685077c8966bd2c7d1115", + "raw/normal_repeat_1/wide_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/wide_edge_window__t2.jsonl": "4d7cf4fa5e14406cb3af440b804b6605f796dc1eef92ad090094134a4b1065f5", + "raw/normal_repeat_1/wide_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_1/wide_edge_window__t3.jsonl": "e89e6d4f0a7fabd5463e2184ae24754e8ba2203d6f05e70f81bc65162e9e693b", + "raw/normal_repeat_1/wide_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t1.jsonl": "66fc7aee07c798976f73d3c02fb548e49028ad7e7f6abf29c8030cfc7941f769", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t128.jsonl": "3efa4fabe260b9e66d500907f93de64b4588aa2087255b91ff7de57d7f34263d", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t2.jsonl": "404138bf058b8c3a20ce8d2a29668110ba5e61c32f43d93404cd97a620f9b557", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t3.jsonl": "c30b6124bb7e3759d63cdeb2394252b639fefc0628b88affae953b883de7d9d5", + "raw/normal_repeat_2/anisotropic_physical_coordinates__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_horizontal__t1.jsonl": "535a241c5ecda16a314077fe31f5141291af7254b1d88dacbeced8fadeb011aa", + "raw/normal_repeat_2/constant_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_horizontal__t128.jsonl": "6777a1a29438762a2a6a7a45c0d380932260153ec06cee73f489049b9d637e55", + "raw/normal_repeat_2/constant_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_horizontal__t2.jsonl": "dd6aa64c6793656912579b1b8347fdd77108c93b0d44d386ec61b84a6f345add", + "raw/normal_repeat_2/constant_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_horizontal__t3.jsonl": "7774997731b79e6e8c6b24aaeedccfbf8b316149d69790bdd0511c8e37ba135c", + "raw/normal_repeat_2/constant_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_no_lines__t1.jsonl": "77003691bfd3aa5d7a94927da76d60f61e7c44cc8b16d66bd658fc7109eac64f", + "raw/normal_repeat_2/constant_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_no_lines__t128.jsonl": "6558b2021d0ad386476b9110d184cbc22dedf5f5067056c35f1c6618ddd9e308", + "raw/normal_repeat_2/constant_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_no_lines__t2.jsonl": "cb4865c48dd5790e798435e0316efe41d126fc3e9ab627f8aeb052de4cf18a21", + "raw/normal_repeat_2/constant_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/constant_no_lines__t3.jsonl": "f9a6f9bee105e071e10cfbb7c653c33ece183ae1c94ab50e1f664e150abda64d", + "raw/normal_repeat_2/constant_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t1.jsonl": "36b143cdb980cab5e8071ea868d9b1f166c7cdad5dd0a52393e4edff84fa4f4a", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t128.jsonl": "c58f964b7f0a8eda2f5d80b782468b0a4100dbd175950249ec07fab9440e52f8", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t2.jsonl": "07712df1d4a00a650e04cd3bdb57e8c69fbda7075d2f8e568bacfee6d310dcbb", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t3.jsonl": "9cb882679721faef074484528f0f8e3231ead67cc30e6fa1b238a8c91d467441", + "raw/normal_repeat_2/floor_c_truncation_starts_ends__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/irregular_outside_endpoints__t1.jsonl": "c255b97d53f57c2c77db702bb095567dbcb68edc054b3627464cf54c5abb9bee", + "raw/normal_repeat_2/irregular_outside_endpoints__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/irregular_outside_endpoints__t128.jsonl": "40b135f2915e413607c58e18bf34af7be5e361776575eef66adbda214219eb1f", + "raw/normal_repeat_2/irregular_outside_endpoints__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/irregular_outside_endpoints__t2.jsonl": "bb29a722e508d7ab9cfb72ae5febd1c838cc9d6c801ebd21641c15cb6dfa1050", + "raw/normal_repeat_2/irregular_outside_endpoints__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/irregular_outside_endpoints__t3.jsonl": "1f81f86278d44c3de4bf2a282859c24983def31a98592aa544b4ecc5dbe0a500", + "raw/normal_repeat_2/irregular_outside_endpoints__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_a__t1.jsonl": "28670e869a53fadc1ba83d8d616e6e41cc5553a8a7a2d22b65d1787b423410bb", + "raw/normal_repeat_2/line_order_a__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_a__t128.jsonl": "6f9a24a117d7ed0ccc522de216e81c7366a6136612376676b4837853f84a4977", + "raw/normal_repeat_2/line_order_a__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_a__t2.jsonl": "396b6f93a669928fcec52d2e9d9e975e9a9522793a2215137a973b858951252b", + "raw/normal_repeat_2/line_order_a__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_a__t3.jsonl": "53b8fee46e78d2cb9d7bde1044f155c2ac848b2d71538079b9b795577e2e33b4", + "raw/normal_repeat_2/line_order_a__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_b_permuted__t1.jsonl": "1b453d0198d8ff937e3706d860e2e12ac892ebddf227ddee2015bfb1242571c6", + "raw/normal_repeat_2/line_order_b_permuted__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_b_permuted__t128.jsonl": "b6a40f9d746249999d27917a9bcbcfcf79024297bc82c6ed1b3e4b04c63a5ab9", + "raw/normal_repeat_2/line_order_b_permuted__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_b_permuted__t2.jsonl": "abf9b94cab9cb091db9b517b061a23b281ea7cdcd07f92f1c09fc008a18d8214", + "raw/normal_repeat_2/line_order_b_permuted__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/line_order_b_permuted__t3.jsonl": "099788f9100ed2c641f992863f7dd3a6c81f5b231d3e78dd2e863067ce4b9065", + "raw/normal_repeat_2/line_order_b_permuted__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/negative_impulse_reversed__t1.jsonl": "0cd351c18af4e73f1997fd655a47ede75cc79283bbf97d9ef4f4066db2c3ba35", + "raw/normal_repeat_2/negative_impulse_reversed__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/negative_impulse_reversed__t128.jsonl": "be2b21e6d6a1c2880906fcad50d957b0f27318349c4228648303967b0bcaba14", + "raw/normal_repeat_2/negative_impulse_reversed__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/negative_impulse_reversed__t2.jsonl": "ae0ba650690b01e7bf6dfb97459f494bc10e1ee3b9c8f3db7afe84298c0fbbb4", + "raw/normal_repeat_2/negative_impulse_reversed__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/negative_impulse_reversed__t3.jsonl": "0243715fd70d485949a67b955233890bf70e897f31291b6e4a02cad401461b0f", + "raw/normal_repeat_2/negative_impulse_reversed__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t1.jsonl": "46ccd9715870233ba0d105ab31e6d84c3ff6714cd455a9a2ca867595b24772e3", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t128.jsonl": "443f0f61ff8a16356da13a58cf8f18ddffe64405f9ba85185200892ae5997225", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t2.jsonl": "b1dd38123d64d122a3c7502eb48ee7b60f6167b440af65a32a2dbe0d860cd36d", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t3.jsonl": "bd4247365e2c8033f51344b6a99a5a1650cd1119e83c658f695ed00b2e74cf4b", + "raw/normal_repeat_2/plateau_signed_zero_partial_clamp__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/positive_impulse_fractional__t1.jsonl": "9d723351aea63732e2d0ee573ed7f351b120fe93621e3d51848f36237862ae7f", + "raw/normal_repeat_2/positive_impulse_fractional__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/positive_impulse_fractional__t128.jsonl": "7cdb3d0f26e6e7f93e2d7356c7c934fe78d3fb6a5beab70608b6e402a4c69469", + "raw/normal_repeat_2/positive_impulse_fractional__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/positive_impulse_fractional__t2.jsonl": "b597da1ff6853ccb8fe3e0ece556376928c91de025a9965e3cc7ae707ef0a713", + "raw/normal_repeat_2/positive_impulse_fractional__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/positive_impulse_fractional__t3.jsonl": "0dc4f0c8e820990ceae418950823b5679da88e313f38763c36108e35a24ded9c", + "raw/normal_repeat_2/positive_impulse_fractional__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t1.jsonl": "d5eecd56cd15d130ff99cf49805bd02a85bce77ac94db5e27f87807393750c30", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t128.jsonl": "5c81dc66c795754545fdfb74cd20f29c77729e89622ee281e036b24933a069f3", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t2.jsonl": "8f02ad5d5c5004c91dd93d0d66d4fe88dda07bcc4debf4ae08a5ed1cba665a85", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t3.jsonl": "ce256c127432b3373b0ac21b02903abd4d52021749132346f329dd80441a92d6", + "raw/normal_repeat_2/row_offset_duplicate_overlap__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/signed_gradient_positive_slope__t1.jsonl": "b31e19897620beb8f1558e7d0c9823480ec5428f585dabb58afe0b1f80c21c75", + "raw/normal_repeat_2/signed_gradient_positive_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/signed_gradient_positive_slope__t128.jsonl": "3e36b2cfa144d46d97dffda93e5a5bcc53230f4f529ac0f5bbba213990f8a0a7", + "raw/normal_repeat_2/signed_gradient_positive_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/signed_gradient_positive_slope__t2.jsonl": "1cc65fe29df674f7033923f868c96274137ea852d8cd5816d43d995c32f86367", + "raw/normal_repeat_2/signed_gradient_positive_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/signed_gradient_positive_slope__t3.jsonl": "704952d3917ab78aad6f57c27210b042a3e8009b4e0674daa7c6825f15d40252", + "raw/normal_repeat_2/signed_gradient_positive_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_1x1_no_lines__t1.jsonl": "756488cc3248d3dd6718206ebf1df55893af4c03e9fe311154053850e73fea31", + "raw/normal_repeat_2/singleton_1x1_no_lines__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_1x1_no_lines__t128.jsonl": "6fc04d31ed9495ff92db78486a1700704285c3593754ce77f987e1518fb499a5", + "raw/normal_repeat_2/singleton_1x1_no_lines__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_1x1_no_lines__t2.jsonl": "d9f779f7789ad702ab5662fef352a4ae69c01b5215ef397e30fb2d33baf3fe28", + "raw/normal_repeat_2/singleton_1x1_no_lines__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_1x1_no_lines__t3.jsonl": "df1dafe9003ed907a08f5c309df38e073453211be313727bf4baeb32aff53fac", + "raw/normal_repeat_2/singleton_1x1_no_lines__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t1.jsonl": "a001a1bc5de31de376b1511ef3d22d697eaef8ac2f4952fd95df3d1a6daf3fbc", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t128.jsonl": "9a137ab721715e85a068ac2de3d1751b5859d11cc8cb9ca6dd7e82033b904afa", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t2.jsonl": "ab5d901c07b821189eb7e43cd55ceb1879f9ae73af19c19c6cdde4dba5423c27", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t3.jsonl": "2e009c43502ae1979ab071cab49645d6d901200170f0011d50c1a148c8bca67b", + "raw/normal_repeat_2/singleton_column_9x1_vertical__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t1.jsonl": "4b3e866c5c653902f67f2e956f1fb6d21431a0be903c456dc044d94206b6578c", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t128.jsonl": "0cc09261440a582eb7e0099fc3cd6ae25f9f28be3e142de06cafc77b521e8f41", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t2.jsonl": "20536c1bae6847a545cc5c915df82df41d64a19d032fca99c00ccf50df47601d", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t3.jsonl": "9c341f78f8b425ec6b2777e40c4facda68104512041b21b177dc651fc1d4a6aa", + "raw/normal_repeat_2/singleton_row_1x9_horizontal__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/step_negative_slope__t1.jsonl": "f78a39b04faca06cf606c71bf18b5e9550fddc12c8032f86e3a770916643cd6a", + "raw/normal_repeat_2/step_negative_slope__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/step_negative_slope__t128.jsonl": "dd1003615bacce43a62223fdd6b749eba1d13c7967806a3de29c6bd22f5b254a", + "raw/normal_repeat_2/step_negative_slope__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/step_negative_slope__t2.jsonl": "980022d97ae76b491998e50f197dce03a13d0945c756b39e44f5f067e7fdc320", + "raw/normal_repeat_2/step_negative_slope__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/step_negative_slope__t3.jsonl": "d7e89e7b6ce2efb5bb7f21ccab09c2498fb36787988caac126d7909cd32a4b26", + "raw/normal_repeat_2/step_negative_slope__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/tall_edge_window__t1.jsonl": "70fc151e0962d51ca44773eaa6fbbce0c27bda43f78ee2371d4c733508138502", + "raw/normal_repeat_2/tall_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/tall_edge_window__t128.jsonl": "723f91f5498f32b1d5f82285082d915fe6f6ddab320bb4361401c136f1d1fbbf", + "raw/normal_repeat_2/tall_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/tall_edge_window__t2.jsonl": "708369fb3cd60cc461db38d793b376e92c55807aef24fd934e6e93556ca33db5", + "raw/normal_repeat_2/tall_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/tall_edge_window__t3.jsonl": "39d763f011bb91d88831085274d3e029135558fcba7f035e0704f3dba898c98d", + "raw/normal_repeat_2/tall_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/wide_edge_window__t1.jsonl": "8ab9fdb5927e915ef40fcda1b95d89df245422a6b8d8039a3a3f0904b9c72a53", + "raw/normal_repeat_2/wide_edge_window__t1.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/wide_edge_window__t128.jsonl": "16144ef175f87574c5d7195cd2950bcff706602a7d0685077c8966bd2c7d1115", + "raw/normal_repeat_2/wide_edge_window__t128.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/wide_edge_window__t2.jsonl": "4d7cf4fa5e14406cb3af440b804b6605f796dc1eef92ad090094134a4b1065f5", + "raw/normal_repeat_2/wide_edge_window__t2.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "raw/normal_repeat_2/wide_edge_window__t3.jsonl": "e89e6d4f0a7fabd5463e2184ae24754e8ba2203d6f05e70f81bc65162e9e693b", + "raw/normal_repeat_2/wide_edge_window__t3.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "run_pathlevel_campaign.py": "4076c930032c20317192b757e8a08d57b33a0f10268eeb21d0533ee4c90f09be", + "sha256_check.log": "e61fb7692f5d879bf91ac10d8ecd676ce287b2c5a1b3081c495ac7d3255f8729", + "sha256_verify.log": "fab42f59f562cc4a988344055becde6afa66620c0d9b9ee16853776cf0b7af58", + "smoke.err": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "smoke.ini": "7a8696b9b091b8e74e77a47900f01558bf3c49e9f59d62358bdfd245aa73d673", + "smoke.out": "f04372236f40e2e5d21f9472f3b88213a2a0dd189f66a319faaee3b136a2e1cb", + "smoke_1.err": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "smoke_1.out": "f04372236f40e2e5d21f9472f3b88213a2a0dd189f66a319faaee3b136a2e1cb", + "smoke_2.err": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "smoke_2.out": "f04372236f40e2e5d21f9472f3b88213a2a0dd189f66a319faaee3b136a2e1cb", + "summary.json": "e6f6e663977b8af8c7a7da9cfbcdf995b7efae179fa27b5df9201a0ccab46f32", + "xvfb_test.log": "1796b1ab4d88c0a0b35915bdda742a7fac9d7dcd9cc317e46ee861c1689fdcb1" + }, + "external_sha256sums": "8aa6a3e68403d3b67fedc63c725b81bfa4b9f391621681da9decf152c3cdc8d8", + "oracle_artifacts": { + "compare_external_reference.py": "3240c45edf7295a34b9b71774157cb910eafd4e99b47f2ee1d47ada718bb87d8", + "comparison_execution.log": "49a9b02fa0c7edfad9b9098465f06eb3f2129b349a20c8e93200e2289129ff6e", + "comparison_ledger.json": "ff6aa434f193263b21fe13a8a76e795565b0838ea6288900eb79c26f056436a8", + "comparison_summary.json": "3b25d20b1e236d886716ad50003cb9ce2530a10e8b141d5bc3303ec57313f851", + "mismatch_classifications.json": "d8b549a7f55003ff8baf35a65980680dc5e80fcca6776d3e62265a8fb0b8a44f", + "oracle_freeze.json": "a9624b1c02afe9701f3d19e061e9eb7cbe50104167e90b9d476f719b8b6c0585", + "oracle_self_tests.py": "63c72fb346936b7bd613e74438fc1bb9d7c5e02cb574888564240634f16e730c", + "path_level_oracle.py": "9b4ec65ba67777c211ab08c73d91bf523ee5082f04ce0424ea4f1fe569ae58f1", + "provenance.json": "0ea5364ade3a2070eb9ae1b9602b4d90ec97716d32a635f37e5cb4f53a6e5445", + "regenerate_path_level_cases.py": "c8a0410436018e3ef1c0ef9dc008cec454213c2b65949d2857d8fabc2752670c", + "regenerated_input_report.json": "b080368d26def8bcc40da259b0be4b9fd2fc9f49fd6ae158849de5dcc5acc31e", + "source_semantics.json": "0ffa339e51b24ee5b4f82d5cbe4e59846306488c1cf3a7780cddb96a214d49fe", + "summary.json": "7788598126f483e00d9115fdbc0c6dbc12cfe6fc77c68e1fb16effb8f4894b58", + "verify_regenerated_inputs.py": "132f7994ea2c4aa31ba21f83bdf96e92c3fbffe2cc32ac743ab36c16c70f5625" + }, + "oracle_sha256sums": "efd796028956bbb2d771589d0b92a77239f7ad3b548b818e1a88fff427fafd42" + } + }, + "fixture": { + "array_count": 90, + "array_hashes": { + "corrected__anisotropic_physical_coordinates__t1": "65883b8732e68b57d3e323a6e0867b96e3579a41fcfa1f6c01a24f64471136c0", + "corrected__anisotropic_physical_coordinates__t128": "4e9fbfa79553687bf0beca278d8b62caec8e63d52df889e6aa098ed926893ba2", + "corrected__anisotropic_physical_coordinates__t2": "65883b8732e68b57d3e323a6e0867b96e3579a41fcfa1f6c01a24f64471136c0", + "corrected__anisotropic_physical_coordinates__t3": "4e9fbfa79553687bf0beca278d8b62caec8e63d52df889e6aa098ed926893ba2", + "corrected__constant_horizontal__t1": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_horizontal__t128": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_horizontal__t2": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_horizontal__t3": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_no_lines__t1": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_no_lines__t128": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_no_lines__t2": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__constant_no_lines__t3": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "corrected__floor_c_truncation_starts_ends__t1": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "corrected__floor_c_truncation_starts_ends__t128": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "corrected__floor_c_truncation_starts_ends__t2": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "corrected__floor_c_truncation_starts_ends__t3": "3df3fd5c6238e7362e1cce67aff6d2796526d8afa6bb905eb6f3c8fba275a7c4", + "corrected__irregular_outside_endpoints__t1": "91800c865bcbac917f8b230b5539c17fe80d99b9f1637002eb9a473802ba9dd8", + "corrected__irregular_outside_endpoints__t128": "685f5bbfbea8354a39680e76ac8f95d455948387eab02c8fe9bc0d89adc63d58", + "corrected__irregular_outside_endpoints__t2": "3728f123d94ae7a56bf848d0a9598794f0bad0c1922b8bd2b2d7f2fbf7138474", + "corrected__irregular_outside_endpoints__t3": "adedd8d927e2807952907de66c79167ebdb95fbff3677293aa57cf3fb2ddbd12", + "corrected__line_order_a__t1": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "corrected__line_order_a__t128": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "corrected__line_order_a__t2": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "corrected__line_order_a__t3": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "corrected__line_order_b_permuted__t1": "8deb39b674d4f29c27a333036f49f9cebc630c935082d4c3de99e93f3e6c0f78", + "corrected__line_order_b_permuted__t128": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "corrected__line_order_b_permuted__t2": "8349941775d84b6000b8d76a068c59c94005493cfcd9ab661723fb5f6b5fbd24", + "corrected__line_order_b_permuted__t3": "85561a2c0e1710ff158f7fa914fed201a96ee1fd03bda3929d6529b4cc6c725a", + "corrected__negative_impulse_reversed__t1": "59520dc5ab335da577436c1c9cae0fd64d3dbcb9ea7aa3206b1fcfd51cc8efb9", + "corrected__negative_impulse_reversed__t128": "bdf60bf5b78e5053cb51c565ccff49725691b19f2619c0e0e17fa6663157c08b", + "corrected__negative_impulse_reversed__t2": "59520dc5ab335da577436c1c9cae0fd64d3dbcb9ea7aa3206b1fcfd51cc8efb9", + "corrected__negative_impulse_reversed__t3": "59520dc5ab335da577436c1c9cae0fd64d3dbcb9ea7aa3206b1fcfd51cc8efb9", + "corrected__plateau_signed_zero_partial_clamp__t1": "aa07ce88e1d599f243c76465a6ce65156e805f0b85ba495dde97a34741a62f57", + "corrected__plateau_signed_zero_partial_clamp__t128": "08c4c42bec90733a5ff5be313afeaf1923fffbc97f796ca4b7f7df0b7de531db", + "corrected__plateau_signed_zero_partial_clamp__t2": "b2c7bd0472ccd4ce7467682ad9d69d4f8f3768892d337d923eeabf04467f3f9b", + "corrected__plateau_signed_zero_partial_clamp__t3": "2688ef8ad5fa877e031007e52054202f5e01175efeb3b9be707fcaf05fa274a9", + "corrected__positive_impulse_fractional__t1": "6c068627660abe9d58c50f8716d222b5619be8162566a9d2893804620eea2af6", + "corrected__positive_impulse_fractional__t128": "5b0046c9ba51d594ee4360a061443829afd33c490360ca259ba3e389ebdb3699", + "corrected__positive_impulse_fractional__t2": "93c99554deb0fec35b49cebb7e78795bace6435849dd7655e915331ae3af1f93", + "corrected__positive_impulse_fractional__t3": "e283f7dfa8b6f5e56e0b309a2355371fd74336159dcc48e4e48ceb62d2aa45b6", + "corrected__row_offset_duplicate_overlap__t1": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "corrected__row_offset_duplicate_overlap__t128": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "corrected__row_offset_duplicate_overlap__t2": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "corrected__row_offset_duplicate_overlap__t3": "68f17161caacadee25c2f0b16f545cc636cefdc9676428d146008476442115ec", + "corrected__signed_gradient_positive_slope__t1": "98d9efe8c730be95299ccedbbfcadfa55c39c5baa5c57cf2eea805b1849b9ebc", + "corrected__signed_gradient_positive_slope__t128": "7b0e9c2fafe8c614eb62559bc77e7b8bea4684a2a0503530b5988767f2b9af40", + "corrected__signed_gradient_positive_slope__t2": "98d9efe8c730be95299ccedbbfcadfa55c39c5baa5c57cf2eea805b1849b9ebc", + "corrected__signed_gradient_positive_slope__t3": "7b0e9c2fafe8c614eb62559bc77e7b8bea4684a2a0503530b5988767f2b9af40", + "corrected__singleton_1x1_no_lines__t1": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected__singleton_1x1_no_lines__t128": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected__singleton_1x1_no_lines__t2": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected__singleton_1x1_no_lines__t3": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "corrected__singleton_column_9x1_vertical__t1": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "corrected__singleton_column_9x1_vertical__t128": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "corrected__singleton_column_9x1_vertical__t2": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "corrected__singleton_column_9x1_vertical__t3": "200d94bc383cca5c1db3e0f689f0254aafc0f249143664c14f6d62b07cc85ac6", + "corrected__singleton_row_1x9_horizontal__t1": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "corrected__singleton_row_1x9_horizontal__t128": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "corrected__singleton_row_1x9_horizontal__t2": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "corrected__singleton_row_1x9_horizontal__t3": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "corrected__step_negative_slope__t1": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "corrected__step_negative_slope__t128": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "corrected__step_negative_slope__t2": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "corrected__step_negative_slope__t3": "a16ab03c4869d6d035d7fa6c7804b3719dfe56b73e893d5d96c8cbc85ba3443d", + "corrected__tall_edge_window__t1": "743048c754fd6d005556ab0a551fe1db5082889247da7fbb3bd987a41ae3d72c", + "corrected__tall_edge_window__t128": "6690a460d28cfe4c05a3e3e2105f26443f8ab4a5805bc2c871b3e2979da63e73", + "corrected__tall_edge_window__t2": "743048c754fd6d005556ab0a551fe1db5082889247da7fbb3bd987a41ae3d72c", + "corrected__tall_edge_window__t3": "6fcc09b9955d42781dec3ec9b71c4a1c8918b10709079c6ed4972346ff27722f", + "corrected__wide_edge_window__t1": "1e68b58906514cebde1408fd722b3f3e63d3507a33ea2f8ec761cfe2a076864c", + "corrected__wide_edge_window__t128": "0d76a9c4f4c70739ba5254748a41602d81351fec2a8f1fb3b9c50090b06fa1a5", + "corrected__wide_edge_window__t2": "4a154ac77147d3967cdbcce6f6a633ccbf307c4c2956d5fab4183f1073a811db", + "corrected__wide_edge_window__t3": "4a154ac77147d3967cdbcce6f6a633ccbf307c4c2956d5fab4183f1073a811db", + "input__anisotropic_physical_coordinates": "6a416ab164f1f7ab8e0e50ae9a44a60e20a1d809763d1ed766bbfb4d928f682a", + "input__constant_horizontal": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "input__constant_no_lines": "c1c6ef99dd2253f1ab8064f12a78776b7cf4f8d3c4cf1da3176b07845e034a4f", + "input__floor_c_truncation_starts_ends": "b4e18dfcabe627710e38d3a2dc0d5d60c425927c484c442c3c7fe04490a014a2", + "input__irregular_outside_endpoints": "5d3b9265e0c3f0c1c04a2ed6496adad7567fcdbcac5de7a42e1b7dad31a9979e", + "input__line_order_a": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "input__line_order_b_permuted": "9c0f061314c5119cbb5a4386c777dfe3529082eb13bbbc439fdcda87ec35137b", + "input__negative_impulse_reversed": "03658489207560a26e8f9384c9038edae9c588d11c22b1356cd782edd05e0f2d", + "input__plateau_signed_zero_partial_clamp": "68173761ff74a584231639a78d02744a65601a59c3ef3fc29ac29ad7fabdbc8e", + "input__positive_impulse_fractional": "9c00590617b05138f45aaf221281c78ae41defb9816108660b68e100dee68b29", + "input__row_offset_duplicate_overlap": "085a24234af7acd58e17ab21c025e997739175748553ce19f389faf4d0fb5c40", + "input__signed_gradient_positive_slope": "66be0573d9152154da4b83a74c6685149caf3312eff2af2c3b7c9109d77877a8", + "input__singleton_1x1_no_lines": "532e03267162c2ccaef12fb31a3af6be53b459bbcd1c7f0765badbe16f738c3f", + "input__singleton_column_9x1_vertical": "fe3c64e8997d8cfb4c242bc6a100fd8ace0308a31e873052e6b4f22bf1bd1e6a", + "input__singleton_row_1x9_horizontal": "0edc83a0f575bc974f946fc5d7e19df0ee907dce03cdd817f274f77962e5344e", + "input__step_negative_slope": "17b72e293ae5970773ecc3f728d5a83f6b7318a77857f5f92a7c5d324974611b", + "input__tall_edge_window": "d03a7dd325f7d70525564e56744303ad9751924f581710cdabc8499b679f9368", + "input__wide_edge_window": "ef163e701f48a3bc31df9816e555935fa7e95d79067ca791c509193c97f59b4f" + }, + "canonical_hash_algorithm": "dtype.str NUL comma-separated shape NUL C-order bytes", + "npz_sha256": "1544c94bbf6efdb896598ff818e73c79c3ab3997b07115198755116cd4e27126", + "relative_npz": "path_level_reference.npz" + }, + "line_order_discriminator": { + "first_case": "line_order_a__t1", + "outputs_differ": true, + "second_case": "line_order_b_permuted__t1" + }, + "metrics": { + "base_families": 18, + "deterministic_repeat_pairs": "72/72", + "exact_elements": "4652/4652", + "external_oracle_arrays": "72/72 bitwise", + "finite_nonzero_mismatches": 0, + "fresh_external_executions": 144, + "logical_cases": 72, + "max_absolute_difference": 0.0, + "max_ulp_distance": 0, + "mutation_classification": "72/72", + "no_op_classification": "72/72", + "normalized_endpoints": "72/72", + "oracle_input_mutation_maximum": 0, + "signed_zero_mismatches": 0 + }, + "reference_software": { + "name": "Gwyddion", + "version": "2.71" + }, + "schema_version": 1, + "thicknesses": [ + 1, + 2, + 3, + 128 + ] +} diff --git a/tests/validation/fixtures/gwyddion/path_level/path_level_reference.npz b/tests/validation/fixtures/gwyddion/path_level/path_level_reference.npz new file mode 100644 index 0000000..8f289c2 Binary files /dev/null and b/tests/validation/fixtures/gwyddion/path_level/path_level_reference.npz differ diff --git a/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.json b/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.json new file mode 100644 index 0000000..b8b8a29 --- /dev/null +++ b/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.json @@ -0,0 +1,485 @@ +{ + "schema_version": 2, + "reference_software": "Gwyddion", + "reference_version": "2.71", + "operation": "sphere-revolve", + "spmkit_method": "gwyddion_sphere_revolution", + "generated_at": "2026-08-02T01:59:23.826557+00:00", + "branch": "feat/gwyddion-leveling-parity", + "head": "c693fc97e94a5829f57c8b2acf8f522f4f05fb4f", + "case_order": [ + "wide_r1", + "wide_r2_5", + "wide_r4", + "wide_r1000", + "tall_r2_5", + "tall_r1000", + "constant_r1", + "constant_r2_5", + "signed_r1", + "signed_r2_5" + ], + "acceptance": { + "background_atol": 5e-14, + "corrected_atol": 5e-14, + "reconstruction_atol": 5e-14, + "rtol": 0.0 + }, + "evidence_classes": { + "normal_route": "direct external reference", + "inverted_background": "derived external reference", + "inverted_corrected": "safe deliberate divergence" + }, + "source_and_artifact_hashes": { + "sphere_revolve_c": "4218cd4e303634c610e9be5f18656d12715c68df95a9b30930b33232b3d8cbe9", + "probe_c": "97248b51df742937ed5dc0a975b8b1ca08b1b6eeb5add95eda4526118337b188", + "runner_sh": "d673393126833277bda41c77403f1dbaf5dc965d6d8b63ee73994238bec8f7a7", + "oracle_py": "f1598e5f7cd0e173ec72ea928e270038ac8ab5f4c61d57b31a60373e50d40e4b", + "precision_audit_py": "58f1acd0d3c3d644c93adcc7c754889e883726976341695e974d4c09a34f72e4", + "normative_spec_md": "3db2e7ea0c965dfa9bcd803ade6d3dae2fc38b216a132c05df36d52a9256b1c8", + "kernel_py": "ef00ec0033de3966ff1ab7642fdba3f24e0d9030550e5c7995fb55318eb75a38", + "core_test_py": "d0a46f20d6260327f80d68ddae8bfa5360def162b6ca6db77511baf5df5e6ef5" + }, + "npz_sha256": "a0713ec37da9d8865717eaf29d876a3c616d0e3e374b217d40f4d02b30288256", + "canonical_array_hashes": { + "input__wide_r1": "69146737b4b7cb993261a33fabaff6adf3f1e47eb07f61431a48ac1b31f725fd", + "direct_background__wide_r1": "119858aaa90729cac657572c2a188d6da3039a1d7e51b1c0a3715620cbc2609f", + "direct_corrected__wide_r1": "e3a6738890370f7564f8bc70bbf7ff92e1a3a79e4ce43df467b6c8e519b5377e", + "negated_input__wide_r1": "d2211b505a1a4de29c21d15c53f10d1f2a3c93f4f4c60df039edaccaa5d0b8ab", + "direct_negated_background__wide_r1": "918eaa4348b9e202f52207444b63649ed3842c2b53246ba44515211fe7191df3", + "direct_negated_corrected__wide_r1": "63ba46649b09a9b0a878f7cae78e7a28888eaac9a8179dbc83a6c9f2ade96615", + "derived_inverted_background__wide_r1": "8c6002c171f33dda2eec6ddc3e9c32529d79db216a49d2e18120893a65b2c0e4", + "safe_inverted_corrected__wide_r1": "470527380b8f8d9a1b3eebc89ed6172859893dd6336292c6b7a2dbc71b43fbd3", + "input__wide_r2_5": "69146737b4b7cb993261a33fabaff6adf3f1e47eb07f61431a48ac1b31f725fd", + "direct_background__wide_r2_5": "647cdbd3a59f8402b3a035cfc95b2761857fc6f1d12698873a1baf7baf12e781", + "direct_corrected__wide_r2_5": "a1c3a4dfa3af93867b8c09bab8863a360bdfe11261f489473778e6c287a2ba8a", + "negated_input__wide_r2_5": "d2211b505a1a4de29c21d15c53f10d1f2a3c93f4f4c60df039edaccaa5d0b8ab", + "direct_negated_background__wide_r2_5": "5b127b1e0d3915b0c7be45a51184e3480434372569640f9619cc59f973a703c1", + "direct_negated_corrected__wide_r2_5": "67f689918a9bfe5e37dd6d5a31eb73f57189875378523fba37da20ab870ec6c9", + "derived_inverted_background__wide_r2_5": "8c097d290cc5f026a6166d1cd02e7969000c3b5ef02331f5d61a5f0020932e90", + "safe_inverted_corrected__wide_r2_5": "9e6037fe4d5b7fd93ab88b324afe82e884543338ba5f064824f3280028b37041", + "input__wide_r4": "69146737b4b7cb993261a33fabaff6adf3f1e47eb07f61431a48ac1b31f725fd", + "direct_background__wide_r4": "b7cebb17fe54bd7d9db06cc430c14980e7e4d44d375aff041272d96b3690be94", + "direct_corrected__wide_r4": "30c5edf90cbf0cc9828d4b7de4cb05922f7c80fadd74c0fff7161a57c7a23632", + "negated_input__wide_r4": "d2211b505a1a4de29c21d15c53f10d1f2a3c93f4f4c60df039edaccaa5d0b8ab", + "direct_negated_background__wide_r4": "56180d83a286b67f17aa60547a1c80a805ecd1131ef59080df41c474da3cda72", + "direct_negated_corrected__wide_r4": "696949157f2bb11c57057b3380759a2d10699e7fb3babf605e7ff95f8cd72853", + "derived_inverted_background__wide_r4": "d928831acd34aa4fd87bf48d37a8f3fc54bacab2ab2f0df696511537aadd60ed", + "safe_inverted_corrected__wide_r4": "71d6cfcfbcb00ac883c006bf3326bdc31a27af5f441441b1a8eb70db948e4bfb", + "input__wide_r1000": "69146737b4b7cb993261a33fabaff6adf3f1e47eb07f61431a48ac1b31f725fd", + "direct_background__wide_r1000": "aaad7ed8d28a89d2c3ddcb0ea9a523cb2f587b81405ad24221b02bcdf0a3e218", + "direct_corrected__wide_r1000": "cbe37ea045576e23a8edb245b92aad4d7de176570ce5335bedd4733a373932a9", + "negated_input__wide_r1000": "d2211b505a1a4de29c21d15c53f10d1f2a3c93f4f4c60df039edaccaa5d0b8ab", + "direct_negated_background__wide_r1000": "15d9fe68194a00adfa0e8b72754ca2296cd0323f18ecb0c1ebd59ff800a8bc83", + "direct_negated_corrected__wide_r1000": "5ba0ed1dec2f5572fc47664049f5833e39339789f7f6137fea936dc6d205dcc8", + "derived_inverted_background__wide_r1000": "2d7981d21aade5a65c7815be10c2862a6254cad412e117800abb4af3127d70ee", + "safe_inverted_corrected__wide_r1000": "2370363919119f1adbe2900a6584cea0f6c333e6dcd3063309442e7c79235812", + "input__tall_r2_5": "3a837baf13a1cb0bea03702753db0d2b667893ee9003d21b28a07bd1bbd3a132", + "direct_background__tall_r2_5": "be5c9715e05c682598b6f94a76142a644ffc437216286b6f6e8860d339350b7e", + "direct_corrected__tall_r2_5": "77e9b4e6cb3bfe1567f88c7c3f5cc25bc6314b305ded3dda19e1614dcd3bfb08", + "negated_input__tall_r2_5": "007e29d4098488b51ee87aa3a00786dce3919b58ebb5cedb6efb8dcc597862c9", + "direct_negated_background__tall_r2_5": "88abc8164c05d9a31c556a99aca0c681adbfc0626453d416b894cefdf7de8d0e", + "direct_negated_corrected__tall_r2_5": "cca9cf55cc415101c85a532f9e84b8cb701e559cbb304db2a161c0960806aa6f", + "derived_inverted_background__tall_r2_5": "b86fcbe1697bd5398350e385e8982c9cfb23e6e42b36276179e04f447ffe8830", + "safe_inverted_corrected__tall_r2_5": "8645ac63b1a9f4eb53931501ca68fab23717d9ae79132832498e4a77f8912e4c", + "input__tall_r1000": "3a837baf13a1cb0bea03702753db0d2b667893ee9003d21b28a07bd1bbd3a132", + "direct_background__tall_r1000": "a94c005361b3f0ea87f8e19ed26776ab79adbeed63dd0a279fcddf15dad42d56", + "direct_corrected__tall_r1000": "78d4da412c0679a8d0dbc8dfc5890f59f62160dc6f38a8c48246c5afd73eed77", + "negated_input__tall_r1000": "007e29d4098488b51ee87aa3a00786dce3919b58ebb5cedb6efb8dcc597862c9", + "direct_negated_background__tall_r1000": "194cd005221041d281c2a9540091b646eba4a4efc15d73775994c79b6fd283cf", + "direct_negated_corrected__tall_r1000": "7955d363e6d608bf21e52ba3e1dd486e2128823508285955ddba0619a2c95776", + "derived_inverted_background__tall_r1000": "f5c91313e76e58115530d065ac9ce1a5cb6d088b6a40c589acda6eedf3c20b2c", + "safe_inverted_corrected__tall_r1000": "739b3e3bccae514ab39ff603f36084af7acf211a6a0e1377d24ff15f3078aa41", + "input__constant_r1": "d32ea6dde7d52e807e1204be1be53d453ed891e71d9b7dd1345a94ee2907c6ed", + "direct_background__constant_r1": "d32ea6dde7d52e807e1204be1be53d453ed891e71d9b7dd1345a94ee2907c6ed", + "direct_corrected__constant_r1": "737f4c9a20fb6c951e5eafcb1fd7757796eff40c5c26ed5d81553aac100f5b47", + "negated_input__constant_r1": "858c6b006727b712ee48e328f67bc5c5bed8cfaff7cf5d9eb4bef3b0e7eecc82", + "direct_negated_background__constant_r1": "c492bf11d8ec721daae6bf83d888a283715408712ba6e5f40edee693fa05c7d5", + "direct_negated_corrected__constant_r1": "acd6960f3e73d5c21ba808b9fbdbdd90fdc1bfc38c2177d435aae82c790f9a0b", + "derived_inverted_background__constant_r1": "a2c6c9c0b75b5c307d548aea99671f656eee00ac723849c09b90f8d652b36a4c", + "safe_inverted_corrected__constant_r1": "f1ec68565055de1c7e54a960fc7e8fdce650dc5afe8f5e91c581acc63fd04815", + "input__constant_r2_5": "d32ea6dde7d52e807e1204be1be53d453ed891e71d9b7dd1345a94ee2907c6ed", + "direct_background__constant_r2_5": "d32ea6dde7d52e807e1204be1be53d453ed891e71d9b7dd1345a94ee2907c6ed", + "direct_corrected__constant_r2_5": "737f4c9a20fb6c951e5eafcb1fd7757796eff40c5c26ed5d81553aac100f5b47", + "negated_input__constant_r2_5": "858c6b006727b712ee48e328f67bc5c5bed8cfaff7cf5d9eb4bef3b0e7eecc82", + "direct_negated_background__constant_r2_5": "858c6b006727b712ee48e328f67bc5c5bed8cfaff7cf5d9eb4bef3b0e7eecc82", + "direct_negated_corrected__constant_r2_5": "737f4c9a20fb6c951e5eafcb1fd7757796eff40c5c26ed5d81553aac100f5b47", + "derived_inverted_background__constant_r2_5": "d32ea6dde7d52e807e1204be1be53d453ed891e71d9b7dd1345a94ee2907c6ed", + "safe_inverted_corrected__constant_r2_5": "737f4c9a20fb6c951e5eafcb1fd7757796eff40c5c26ed5d81553aac100f5b47", + "input__signed_r1": "c5d49a60375fbb0f0bd983aa6e15f3b0ae4aca85c28a9e4c612611cd0a9e4eea", + "direct_background__signed_r1": "bc5b006f2703d632ddd05cb532e6369dd451a3010390c1c842844a4cbffaed87", + "direct_corrected__signed_r1": "8d30b75f1ad6cbb6d578018a2cb07253fd8a9781b9ea84e1c4e5b05e055ad69f", + "negated_input__signed_r1": "18c1108964b720fb0059c59f858ffe2ab057b5fa65122f8ffd00dd1704b53f90", + "direct_negated_background__signed_r1": "f87fe6aeb2a2349c236f7385d397521bc7b3b0685b435eef080c08db1bb5bbe2", + "direct_negated_corrected__signed_r1": "30f4c167e4db33122f486c327acb3afceb473a6cf95f7bedc0fa4709f113bfb7", + "derived_inverted_background__signed_r1": "fb8cea53a478af8ac307a4a7c4e096951aba0232ee5b3a26f15cb588895dacd0", + "safe_inverted_corrected__signed_r1": "25258b05b2dc1d72723fdc024709831ca4f1b583c8d5ffb84ee7da556e2b18c6", + "input__signed_r2_5": "c5d49a60375fbb0f0bd983aa6e15f3b0ae4aca85c28a9e4c612611cd0a9e4eea", + "direct_background__signed_r2_5": "a5dcc566f0ce17f4512757dfa6b20be5506d71d70f22a2bff313273663a84790", + "direct_corrected__signed_r2_5": "c710507228e274f51cddbeeda3a60aab800b10dad2f2b0f87383230b97c42503", + "negated_input__signed_r2_5": "18c1108964b720fb0059c59f858ffe2ab057b5fa65122f8ffd00dd1704b53f90", + "direct_negated_background__signed_r2_5": "3321dacf524ad9b13efaa2a952068d1dc4e9113507260411d166b50b6881328a", + "direct_negated_corrected__signed_r2_5": "1a82f3c4fea7ac5ba986af6de8820a2898814565524a960a8bdaa9c108d013be", + "derived_inverted_background__signed_r2_5": "0b642ee63eb39e0b3f2fae3bc7461237e3eafee3c971335edea8ca94bffe380e", + "safe_inverted_corrected__signed_r2_5": "9073b6b0d8a1cd520bf01fe4132957ad11421e9da1ae4c9ab44ed644d8352c61" + }, + "cases": { + "wide_r1": { + "pair_id": "wide_r1", + "original_case": "wide_r1_normal", + "negated_case": "wide_r1_negated_normal", + "family": "WIDE_ASYMMETRIC", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 7, + "yres": 5, + "radius": 1.0, + "q_original": 1.1162423626132756, + "q_negated": 1.1162423626132756, + "sphere_size": 1, + "sphere_resolution": 3, + "local_filter_size": 0, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 8.881784197001252e-16 + }, + "wide_r2_5": { + "pair_id": "wide_r2_5", + "original_case": "wide_r2_5_normal", + "negated_case": "wide_r2_5_negated_normal", + "family": "WIDE_ASYMMETRIC", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 7, + "yres": 5, + "radius": 2.5, + "q_original": 1.1162423626132756, + "q_negated": 1.1162423626132756, + "sphere_size": 3, + "sphere_resolution": 7, + "local_filter_size": 1, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 4.440892098500626e-16 + }, + "wide_r4": { + "pair_id": "wide_r4", + "original_case": "wide_r4_normal", + "negated_case": "wide_r4_negated_normal", + "family": "WIDE_ASYMMETRIC", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 7, + "yres": 5, + "radius": 4.0, + "q_original": 1.1162423626132756, + "q_negated": 1.1162423626132756, + "sphere_size": 4, + "sphere_resolution": 9, + "local_filter_size": 2, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 4.440892098500626e-16 + }, + "wide_r1000": { + "pair_id": "wide_r1000", + "original_case": "wide_r1000_normal", + "negated_case": "wide_r1000_negated_normal", + "family": "WIDE_ASYMMETRIC", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 7, + "yres": 5, + "radius": 1000.0, + "q_original": 1.1162423626132756, + "q_negated": 1.1162423626132756, + "sphere_size": 7, + "sphere_resolution": 15, + "local_filter_size": 3, + "very_flat": true, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 4.440892098500626e-16 + }, + "tall_r2_5": { + "pair_id": "tall_r2_5", + "original_case": "tall_r2_5_normal", + "negated_case": "tall_r2_5_negated_normal", + "family": "TALL_ASYMMETRIC", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 5, + "yres": 7, + "radius": 2.5, + "q_original": 1.0649069433390794, + "q_negated": 1.0649069433390794, + "sphere_size": 3, + "sphere_resolution": 7, + "local_filter_size": 1, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 4.440892098500626e-16 + }, + "tall_r1000": { + "pair_id": "tall_r1000", + "original_case": "tall_r1000_normal", + "negated_case": "tall_r1000_negated_normal", + "family": "TALL_ASYMMETRIC", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 5, + "yres": 7, + "radius": 1000.0, + "q_original": 1.0649069433390794, + "q_negated": 1.0649069433390794, + "sphere_size": 5, + "sphere_resolution": 11, + "local_filter_size": 2, + "very_flat": true, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 4.440892098500626e-16 + }, + "constant_r1": { + "pair_id": "constant_r1", + "original_case": "constant_r1_normal", + "negated_case": "constant_r1_negated_normal", + "family": "CONSTANT_ZERO_RMS", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 4, + "yres": 3, + "radius": 1.0, + "q_original": 0.0, + "q_negated": 0.0, + "sphere_size": 1, + "sphere_resolution": 3, + "local_filter_size": 0, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 0.0 + }, + "constant_r2_5": { + "pair_id": "constant_r2_5", + "original_case": "constant_r2_5_normal", + "negated_case": "constant_r2_5_negated_normal", + "family": "CONSTANT_ZERO_RMS", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 4, + "yres": 3, + "radius": 2.5, + "q_original": 0.0, + "q_negated": 0.0, + "sphere_size": 3, + "sphere_resolution": 7, + "local_filter_size": 1, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 0.0 + }, + "signed_r1": { + "pair_id": "signed_r1", + "original_case": "signed_r1_normal", + "negated_case": "signed_r1_negated_normal", + "family": "SIGNED_MICRO_GRID", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 3, + "yres": 3, + "radius": 1.0, + "q_original": 3.569417423157558, + "q_negated": 3.569417423157558, + "sphere_size": 1, + "sphere_resolution": 3, + "local_filter_size": 0, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 0.0 + }, + "signed_r2_5": { + "pair_id": "signed_r2_5", + "original_case": "signed_r2_5_normal", + "negated_case": "signed_r2_5_negated_normal", + "family": "SIGNED_MICRO_GRID", + "input_variant_original": "original", + "input_variant_negated": "negated", + "xres": 3, + "yres": 3, + "radius": 2.5, + "q_original": 3.569417423157558, + "q_negated": 3.569417423157558, + "sphere_size": 3, + "sphere_resolution": 7, + "local_filter_size": 1, + "very_flat": false, + "direct_execution_status": "exit_code_0_success", + "reconstruction_max_abs": 4.440892098500626e-16 + } + }, + "inverted_reference_failures": [ + { + "case": "wide_r1_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 1.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "wide_r2_5_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 2.5, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "wide_r4_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 4.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "wide_r1000_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 1000.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "tall_r2_5_inverted", + "family": "TALL_ASYMMETRIC", + "radius": 2.5, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "tall_r1000_inverted", + "family": "TALL_ASYMMETRIC", + "radius": 1000.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "constant_r1_inverted", + "family": "CONSTANT_ZERO_RMS", + "radius": 1.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "constant_r2_5_inverted", + "family": "CONSTANT_ZERO_RMS", + "radius": 2.5, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "signed_r1_inverted", + "family": "SIGNED_MICRO_GRID", + "radius": 1.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "signed_r2_5_inverted", + "family": "SIGNED_MICRO_GRID", + "radius": 2.5, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "wide_r1_negated_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 1.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "wide_r2_5_negated_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 2.5, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "wide_r4_negated_inverted", + "family": "WIDE_ASYMMETRIC", + "radius": 4.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "tall_r2_5_negated_inverted", + "family": "TALL_ASYMMETRIC", + "radius": 2.5, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + }, + { + "case": "constant_r1_negated_inverted", + "family": "CONSTANT_ZERO_RMS", + "radius": 1.0, + "normal_exit_code": 139, + "asan_exit_code": 134, + "execute_started": true, + "execute_returned": false, + "failure_site": "sphere-revolve.c:328 gwy_data_field_subtract_fields", + "arrays_available": false + } + ] +} \ No newline at end of file diff --git a/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.npz b/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.npz new file mode 100644 index 0000000..132d9a9 Binary files /dev/null and b/tests/validation/fixtures/gwyddion/sphere_revolution/sphere_revolution_reference.npz differ diff --git a/tests/validation/test_arc_revolution_vs_gwyddion.py b/tests/validation/test_arc_revolution_vs_gwyddion.py new file mode 100644 index 0000000..73b884a --- /dev/null +++ b/tests/validation/test_arc_revolution_vs_gwyddion.py @@ -0,0 +1,258 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np +import pytest + +from spmkit.core.analysis._gwyddion_arc_revolution import ( + _gwyddion_arc_background, + _gwyddion_arc_corrected, +) + +_FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" / "gwyddion" / "arc_revolution" +_METADATA_PATH = _FIXTURE_DIR / "gwyddion_2_71_directional.json" +_METADATA = json.loads(_METADATA_PATH.read_text(encoding="utf-8")) +_CASE_NAMES = tuple(_METADATA["cases"]) + + +def _canonical_array_sha256(array: np.ndarray) -> str: + canonical = np.ascontiguousarray( + array, + dtype=np.float64, + ) + + digest = hashlib.sha256() + digest.update(str(canonical.dtype).encode("ascii")) + digest.update(b"\0") + digest.update(",".join(str(value) for value in canonical.shape).encode("ascii")) + digest.update(b"\0") + digest.update(canonical.tobytes(order="C")) + return digest.hexdigest() + + +def _load_fixture() -> dict[str, np.ndarray]: + npz_path = _FIXTURE_DIR / _METADATA["artifacts"]["npz_filename"] + + assert hashlib.sha256(npz_path.read_bytes()).hexdigest() == ( + _METADATA["artifacts"]["npz_sha256"] + ) + + with np.load(npz_path) as fixture: + arrays = { + name: np.asarray( + fixture[name], + dtype=np.float64, + ) + for name in fixture.files + } + + expected_hashes = _METADATA["artifacts"]["array_canonical_sha256"] + + assert set(arrays) == set(expected_hashes) + + for name, array in arrays.items(): + assert _canonical_array_sha256(array) == expected_hashes[name] + + return arrays + + +@pytest.mark.parametrize("case_name", _CASE_NAMES) +def test_arc_background_matches_gwyddion_2_71( + case_name: str, +) -> None: + fixture = _load_fixture() + case = _METADATA["cases"][case_name] + acceptance = _METADATA["acceptance"] + + input_field = fixture["input"].copy() + original_input = input_field.copy() + + result = _gwyddion_arc_background( + input_field, + _METADATA["parameters"]["radius_px"], + direction=case["direction"], + inverted=case["inverted"], + ) + + np.testing.assert_allclose( + result, + fixture[f"background_{case_name}"], + atol=acceptance["background_max_abs_error"], + rtol=0.0, + ) + np.testing.assert_array_equal( + input_field, + original_input, + ) + + assert result.dtype == np.float64 + assert result.flags.c_contiguous + assert not result.flags.writeable + + +@pytest.mark.parametrize( + "case_name", + [name for name, case in _METADATA["cases"].items() if case["corrected_reference_valid"]], +) +def test_arc_corrected_matches_valid_gwyddion_2_71_results( + case_name: str, +) -> None: + fixture = _load_fixture() + case = _METADATA["cases"][case_name] + acceptance = _METADATA["acceptance"] + + input_field = fixture["input"].copy() + + background = _gwyddion_arc_background( + input_field, + _METADATA["parameters"]["radius_px"], + direction=case["direction"], + inverted=case["inverted"], + ) + corrected = _gwyddion_arc_corrected( + input_field, + _METADATA["parameters"]["radius_px"], + direction=case["direction"], + inverted=case["inverted"], + ) + + np.testing.assert_allclose( + corrected, + fixture[f"corrected_{case_name}"], + atol=acceptance["corrected_max_abs_error"], + rtol=0.0, + ) + np.testing.assert_allclose( + corrected + background, + input_field, + atol=acceptance["reconstruction_max_abs_error"], + rtol=0.0, + ) + + assert not corrected.flags.writeable + + +def test_horizontal_inverted_reference_defect_is_preserved_and_repaired() -> None: + fixture = _load_fixture() + defect = _METADATA["known_reference_defects"]["horizontal_inverted_corrected_result"] + acceptance = _METADATA["acceptance"] + + input_field = fixture["input"].copy() + reference_corrected = fixture["corrected_horizontal_inverted"] + + assert defect["classification"] == "KNOWN_REFERENCE_DEFECT" + assert defect["reference_result_untouched"] is True + assert np.all(reference_corrected == defect["sentinel"]) + + background = _gwyddion_arc_background( + input_field, + _METADATA["parameters"]["radius_px"], + direction="horizontal", + inverted=True, + ) + corrected = _gwyddion_arc_corrected( + input_field, + _METADATA["parameters"]["radius_px"], + direction="horizontal", + inverted=True, + ) + + np.testing.assert_allclose( + background, + fixture["background_horizontal_inverted"], + atol=acceptance["background_max_abs_error"], + rtol=0.0, + ) + np.testing.assert_allclose( + corrected + background, + input_field, + atol=acceptance["reconstruction_max_abs_error"], + rtol=0.0, + ) + + assert not np.any(corrected == defect["sentinel"]) + assert not background.flags.writeable + assert not corrected.flags.writeable + + +@pytest.mark.parametrize("case_name", _CASE_NAMES) +def test_public_arc_result_matches_gwyddion_2_71( + case_name: str, +) -> None: + from spmkit.core.analysis import ( + analyze_gwyddion_arc_revolution_background, + ) + from spmkit.core.models import SPMChannel + + fixture = _load_fixture() + case = _METADATA["cases"][case_name] + field = _METADATA["field"] + acceptance = _METADATA["acceptance"] + + input_field = fixture["input"].copy() + original_input = input_field.copy() + + channel = SPMChannel( + name="Gwyddion 2.71 frozen Revolve Arc field", + data=input_field, + unit="V", + x_range=float(field["xreal"]), + y_range=float(field["yreal"]), + direction="forward", + group="external-validation", + metadata={ + "fixture_id": _METADATA["fixture_id"], + "reference": "Gwyddion 2.71", + }, + ) + + result = analyze_gwyddion_arc_revolution_background( + channel, + _METADATA["parameters"]["radius_px"], + direction=case["direction"], + inverted=case["inverted"], + ) + + np.testing.assert_allclose( + result.background.data, + fixture[f"background_{case_name}"], + atol=acceptance["background_max_abs_error"], + rtol=0.0, + ) + + if case["corrected_reference_valid"]: + np.testing.assert_allclose( + result.corrected.data, + fixture[f"corrected_{case_name}"], + atol=acceptance["corrected_max_abs_error"], + rtol=0.0, + ) + else: + defect = _METADATA["known_reference_defects"]["horizontal_inverted_corrected_result"] + assert np.all(fixture[f"corrected_{case_name}"] == defect["sentinel"]) + assert not np.any(result.corrected.data == defect["sentinel"]) + + np.testing.assert_allclose( + result.corrected.data + result.background.data, + input_field, + atol=acceptance["reconstruction_max_abs_error"], + rtol=0.0, + ) + np.testing.assert_array_equal( + input_field, + original_input, + ) + + assert result.method == "gwyddion_arc_revolution" + assert result.parameters == { + "radius_px": 2.5, + "direction": case["direction"], + "inverted": case["inverted"], + } + assert result.background.unit == "V" + assert result.corrected.unit == "V" + assert not result.background.data.flags.writeable + assert not result.corrected.data.flags.writeable diff --git a/tests/validation/test_flat_disc_morphology_fixture_integrity.py b/tests/validation/test_flat_disc_morphology_fixture_integrity.py new file mode 100644 index 0000000..ccfc455 --- /dev/null +++ b/tests/validation/test_flat_disc_morphology_fixture_integrity.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parent / "fixtures/gwyddion/flat_disc_morphology" + + +def _hash(array: np.ndarray) -> str: + value = np.ascontiguousarray(array) + digest = hashlib.sha256() + digest.update(value.dtype.str.encode("ascii")) + digest.update(b"\0") + digest.update(",".join(str(item) for item in value.shape).encode("ascii")) + digest.update(b"\0") + digest.update(value.tobytes(order="C")) + return digest.hexdigest() + + +def test_flat_disc_fixture_integrity() -> None: + manifest = json.loads((ROOT / "flat_disc_morphology_reference.json").read_text()) + assert manifest["schema_version"] == 1 + assert len(manifest["cases"]) == 12 + assert manifest["sizes_exercised"] == [2, 3, 4, 5, 30, 31] + assert len(manifest["kernel_masks"]) == 30 + assert manifest["metrics"]["opening_bitwise_exact"] == "72/72" + assert manifest["metrics"]["closing_bitwise_exact"] == "72/72" + with np.load(ROOT / "flat_disc_morphology_reference.npz", allow_pickle=False) as data: + assert len(data.files) == 186 + assert set(data.files) == set(manifest["fixture"]["array_hashes"]) + for name in data.files: + array = data[name] + assert array.ndim == 2 and array.flags.c_contiguous + assert np.isfinite(array).all() + assert _hash(array) == manifest["fixture"]["array_hashes"][name] + assert data["input__singleton_row_1x7"].view(np.uint64)[0, 0] == 1 << 63 diff --git a/tests/validation/test_flatten_base_vs_gwyddion.py b/tests/validation/test_flatten_base_vs_gwyddion.py new file mode 100644 index 0000000..e936e92 --- /dev/null +++ b/tests/validation/test_flatten_base_vs_gwyddion.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np +import pytest + +from spmkit.core.analysis import _flatten_base + +_FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" / "gwyddion" / "flatten_base" +_METADATA_PATH = _FIXTURE_DIR / "gwyddion_2_71_end_to_end.json" + + +def test_flatten_base_matches_gwyddion_2_71_end_to_end() -> None: + metadata = json.loads(_METADATA_PATH.read_text()) + npz_path = _FIXTURE_DIR / metadata["artifacts"]["npz_filename"] + + assert hashlib.sha256(npz_path.read_bytes()).hexdigest() == ( + metadata["artifacts"]["npz_sha256"] + ) + + with np.load(npz_path) as fixture: + input_field = np.asarray( + fixture["input"], + dtype=float, + ) + expected_corrected = np.asarray( + fixture["corrected"], + dtype=float, + ) + + original_input = input_field.copy() + field = metadata["field"] + expected_flow = metadata["expected_control_flow"] + expected_result = metadata["expected_result"] + acceptance = metadata["acceptance"] + + result = _flatten_base._run_flatten_base( + input_field, + pixel_size_x=float(field["pixel_size_x"]), + pixel_size_y=float(field["pixel_size_y"]), + ) + + attempted_degrees = tuple(iteration.degree for iteration in result.polynomial_stage.iterations) + applied_degrees = tuple( + iteration.degree for iteration in result.polynomial_stage.iterations if iteration.applied + ) + expected_degrees = tuple(expected_flow["polynomial_degrees"]) + + assert len(result.facet_stage.iterations) == (expected_flow["facet_iterations"]) + assert attempted_degrees == expected_degrees + assert applied_degrees == expected_degrees + assert result.final_peak.success is (expected_flow["final_peak_success"]) + + assert result.final_peak.mean == pytest.approx( + expected_result["final_peak_mean"], + abs=acceptance["final_peak_mean_abs_error"], + rel=0.0, + ) + assert result.final_peak.rms == pytest.approx( + expected_result["final_peak_rms"], + abs=acceptance["final_peak_rms_abs_error"], + rel=0.0, + ) + + np.testing.assert_allclose( + result.corrected, + expected_corrected, + atol=acceptance["corrected_max_abs_error"], + rtol=0.0, + ) + + assert float(np.min(result.corrected)) == pytest.approx( + expected_result["corrected_minimum"], + abs=acceptance["corrected_max_abs_error"], + rel=0.0, + ) + assert float(np.max(result.corrected)) == pytest.approx( + expected_result["corrected_maximum"], + abs=acceptance["corrected_max_abs_error"], + rel=0.0, + ) + + np.testing.assert_array_equal( + input_field, + original_input, + ) + np.testing.assert_allclose( + result.corrected + result.background, + input_field, + atol=5e-14, + rtol=0.0, + ) + + assert not result.corrected.flags.writeable + assert not result.background.flags.writeable diff --git a/tests/validation/test_gwyddion_align_rows_statistics_fixture_integrity.py b/tests/validation/test_gwyddion_align_rows_statistics_fixture_integrity.py new file mode 100644 index 0000000..6b7b345 --- /dev/null +++ b/tests/validation/test_gwyddion_align_rows_statistics_fixture_integrity.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + +import numpy as np + +ROOT = Path(__file__).resolve().parent / "fixtures/gwyddion/align_rows_statistics" +MANIFEST_SHA256 = "be6f6b91f063c57f882e57f8aa685f1845c6ccfc41b82365f0c648e7ac98781f" +NPZ_SHA256 = "0098e804597440419fd1eea2914ddccd7bbb5412c8691a8e3c34f0538983119e" + + +def _digest(file_path: Path) -> str: + return hashlib.sha256(file_path.read_bytes()).hexdigest() + + +def _array_hash(array: np.ndarray) -> str: + value = np.ascontiguousarray(array, dtype=np.float64) + digest = hashlib.sha256() + digest.update(value.dtype.str.encode("ascii")) + digest.update(b"\0") + digest.update(",".join(str(item) for item in value.shape).encode("ascii")) + digest.update(b"\0") + digest.update(value.tobytes(order="C")) + return digest.hexdigest() + + +def _bits(array: np.ndarray) -> np.ndarray: + return np.ascontiguousarray(array, dtype=np.float64).view(np.uint64) + + +def _subtraction_in_order(input_data: np.ndarray, corrected: np.ndarray) -> np.ndarray: + result = np.empty_like(input_data, order="C") + for row in range(input_data.shape[0]): + for column in range(input_data.shape[1]): + result[row, column] = input_data[row, column] - corrected[row, column] + return result + + +def _load() -> tuple[dict[str, Any], dict[str, np.ndarray]]: + manifest = json.loads((ROOT / "align_rows_statistics_reference.json").read_text()) + with np.load(ROOT / "align_rows_statistics_reference.npz", allow_pickle=False) as archive: + arrays = {name: archive[name].copy(order="C") for name in archive.files} + return manifest, arrays + + +def test_fixture_hashes_inventory_and_deterministic_loading() -> None: + assert _digest(ROOT / "align_rows_statistics_reference.json") == MANIFEST_SHA256 + assert _digest(ROOT / "align_rows_statistics_reference.npz") == NPZ_SHA256 + manifest, first = _load() + _, second = _load() + assert manifest["schema_version"] == 1 + assert manifest["capability"] == "gwyddion_align_rows_statistics" + assert manifest["case_count"] == 64 + assert manifest["method_counts"] == { + "Median": 16, + "Median of differences": 16, + "Trimmed mean": 16, + "Trimmed mean of differences": 16, + } + cases = manifest["cases"] + assert len(cases) == 64 + assert len({case["case_identifier"] for case in cases}) == 64 + assert set(first) == set(manifest["fixture"]["array_hashes"]) + assert set(first) == set(second) + for name, array in first.items(): + assert array.dtype == np.float64 and array.flags.c_contiguous + assert array.ndim == 2 and np.isfinite(array).all() + assert _array_hash(array) == manifest["fixture"]["array_hashes"][name] + assert np.array_equal(_bits(array), _bits(second[name])) + + +def test_profile_identity_exception_scope_and_background_relations() -> None: + manifest, arrays = _load() + assert manifest["profiles"]["portable_source_semantics"]["candidate_output_sha256"] == ( + "7da7283019d698089d1a9cca4cec712860529fce42c2cb0752c2b136ecd1cb30" + ) + assert manifest["profiles"]["installed_gwyddion_2_71_fast_math_profile"][ + "canonical_reference_sha256" + ] == ("e2fa6d094acc5ec04f87901aa345244f9d22e70577d25f963a8cdb74363e457e") + assert manifest["profiles"]["installed_gwyddion_2_71_fast_math_profile"]["module_sha256"] == ( + "c21d52375807ae096e34a3469c2f20c4c66ea3197479e13215a6d7b9d465b451" + ) + assert manifest["evidence"]["installed_build_diagnosis"] == [ + "INSTALLED_BUILD_ROOT_CAUSE_CONFIRMED", + "V3_NOT_JUSTIFIED", + ] + + mismatching_cases: set[str] = set() + finite_nonzero = signed_zero = nan = infinity = exact_arrays = exact_elements = 0 + background_arrays = background_elements = mutation_matches = 0 + for case in manifest["cases"]: + input_data = arrays[case["input_key"]] + portable = arrays[case["portable_corrected_key"]] + installed = arrays[case["installed_corrected_key"]] + assert input_data.shape == (case["rows"], case["columns"]) + assert _bits(input_data).ravel().tolist() == [ + int(value, 16) for value in case["input_bits"] + ] + if case["mask_key"] is None: + assert case["mask_bits"] is None + else: + assert _bits(arrays[case["mask_key"]]).ravel().tolist() == [ + int(value, 16) for value in case["mask_bits"] + ] + portable_bits = _bits(portable) + installed_bits = _bits(installed) + differing = portable_bits != installed_bits + exact_elements += int((~differing).sum()) + exact_arrays += int(not differing.any()) + if differing.any(): + mismatching_cases.add(case["case_identifier"]) + for row, column in np.argwhere(differing): + left = portable[row, column] + right = installed[row, column] + if np.isnan(left) or np.isnan(right): + nan += 1 + elif np.isinf(left) or np.isinf(right): + infinity += 1 + elif left == right == 0.0: + signed_zero += 1 + else: + finite_nonzero += 1 + portable_mutated = bool((_bits(portable) != _bits(input_data)).any()) + installed_mutated = bool((_bits(installed) != _bits(input_data)).any()) + mutation_matches += int(portable_mutated == installed_mutated == case["installed_mutated"]) + if case["extract_background_request"]: + portable_background = arrays[case["portable_background_key"]] + installed_background = arrays[case["installed_background_key"]] + assert np.array_equal(_bits(portable_background), _bits(installed_background)) + assert np.array_equal( + _bits(portable_background), _bits(_subtraction_in_order(input_data, portable)) + ) + assert np.array_equal( + _bits(installed_background), _bits(_subtraction_in_order(input_data, installed)) + ) + background_arrays += 1 + background_elements += portable_background.size + assert exact_arrays == 61 + assert exact_elements == 3757 + assert finite_nonzero == 128 + assert signed_zero == 3 + assert nan == infinity == 0 + assert mismatching_cases == { + "median__plateaus_signed_zero__10", + "median_of_differences__irregular__11", + "trimmed_mean_of_differences__irregular__11", + } + assert background_arrays == 8 and background_elements == 504 + assert mutation_matches == 64 + exceptions = manifest["comparison_metrics"]["authorized_exceptions"] + assert {item["case_identifier"] for item in exceptions} == mismatching_cases + assert sum(item["finite_nonzero_count"] for item in exceptions) == 128 + assert sum(item["signed_zero_only_count"] for item in exceptions) == 3 + assert ( + manifest["comparison_metrics"]["corrected"]["max_absolute_difference"] + == 5.329070518200751e-15 + ) diff --git a/tests/validation/test_median_background_fixture_integrity.py b/tests/validation/test_median_background_fixture_integrity.py new file mode 100644 index 0000000..c785a78 --- /dev/null +++ b/tests/validation/test_median_background_fixture_integrity.py @@ -0,0 +1,301 @@ +"""Integrity checks for frozen Gwyddion 2.71 Median Background evidence.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np + +_FIXTURE_DIR = Path(__file__).parent / "fixtures" / "gwyddion" / "median_background" +_NPZ_PATH = _FIXTURE_DIR / "median_background_reference.npz" +_MANIFEST_PATH = _FIXTURE_DIR / "median_background_reference.json" +_EXPECTED_CASES = [ + "wide_r1", + "wide_r2", + "wide_r3", + "wide_r4", + "wide_r20", + "tall_r1", + "tall_r2", + "tall_r3", + "tall_r4", + "tall_r20", + "constant_r1", + "constant_r3", + "constant_r20", + "signed_r1", + "signed_r2", + "signed_r3", + "signed_r20", + "singleton_1x1_r1", + "singleton_1x1_r3", + "singleton_1x1_r20", + "singleton_1x1_r1024", + "singleton_row_r1", + "singleton_row_r3", + "singleton_row_r20", + "singleton_column_r1", + "singleton_column_r3", + "singleton_column_r20", + "impulse_positive_r1", + "impulse_positive_r2", + "impulse_positive_r3", + "impulse_negative_r1", + "impulse_negative_r2", + "impulse_negative_r3", + "monotonic_r1", + "monotonic_r2", + "monotonic_r3", +] +_EXPECTED_RADIUS_INVENTORY = { + "1": {"active_count": 9, "backend": "direct", "rank": 4, "resolution": 3}, + "2": {"active_count": 21, "backend": "direct", "rank": 10, "resolution": 5}, + "3": {"active_count": 37, "backend": "radixtree", "rank": 18, "resolution": 7}, + "4": {"active_count": 69, "backend": "radixtree", "rank": 34, "resolution": 9}, + "20": {"active_count": 1313, "backend": "radixtree", "rank": 656, "resolution": 41}, + "1024": { + "active_count": 3297401, + "backend": "radixtree", + "rank": 1648700, + "resolution": 2049, + }, +} + + +def _sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical_array_hash(array: np.ndarray) -> str: + contiguous = np.ascontiguousarray(array) + digest = hashlib.sha256() + digest.update(contiguous.dtype.str.encode("ascii")) + digest.update(b"\0") + digest.update(",".join(str(value) for value in contiguous.shape).encode("ascii")) + digest.update(b"\0") + digest.update(contiguous.tobytes(order="C")) + return digest.hexdigest() + + +def _load_manifest() -> dict[str, object]: + with _MANIFEST_PATH.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def _load_arrays_read_only() -> dict[str, np.ndarray]: + arrays: dict[str, np.ndarray] = {} + with np.load(_NPZ_PATH, allow_pickle=False) as archive: + for name in archive.files: + array = np.ascontiguousarray(archive[name]).copy(order="C") + array.setflags(write=False) + arrays[name] = array + return arrays + + +def _cases(manifest: dict[str, object]) -> list[dict[str, object]]: + return manifest["cases"] + + +def test_manifest_schema_and_identity() -> None: + manifest = _load_manifest() + + assert manifest["schema_version"] == 1 + assert manifest["capability"] == "gwyddion_median_background" + assert manifest["operation"] == "median_bg" + assert manifest["reference_software"] == {"name": "Gwyddion", "version": "2.71"} + assert manifest["fixture"]["case_count"] == 36 + assert manifest["oracle"]["canonical_source_array_hash_count"] == 180 + assert len(manifest["oracle"]["canonical_source_array_hashes"]) == 180 + assert "manifest_self_hash" not in manifest["fixture"] + serialized = json.dumps(manifest, sort_keys=True) + forbidden_markers = ( + "/" + "tmp/", + "/" + "home/", + "." + "reference", + "_" + "_" + "pycache__", + ) + assert not any(marker in serialized for marker in forbidden_markers) + + +def test_case_order_is_exact() -> None: + manifest = _load_manifest() + + assert [case["name"] for case in _cases(manifest)] == _EXPECTED_CASES + + +def test_fixture_exists_and_hash_matches_manifest() -> None: + manifest = _load_manifest() + + assert _NPZ_PATH.is_file() + assert _sha256_file(_NPZ_PATH) == manifest["fixture"]["npz_sha256"] + + +def test_fixture_contains_exactly_108_arrays() -> None: + arrays = _load_arrays_read_only() + + assert len(arrays) == 108 + + +def test_fixture_contains_exactly_three_arrays_per_case() -> None: + manifest = _load_manifest() + arrays = _load_arrays_read_only() + + for case in _cases(manifest): + names = set(case["arrays"].values()) + assert len(names) == 3 + assert names <= arrays.keys() + + +def test_fixture_array_names_are_exact() -> None: + arrays = _load_arrays_read_only() + expected_names = { + f"{role}__{case}" + for case in _EXPECTED_CASES + for role in ("input", "background", "corrected") + } + + assert set(arrays) == expected_names + + +def test_fixture_array_dtypes_are_float64() -> None: + arrays = _load_arrays_read_only() + + assert all(array.dtype == np.float64 for array in arrays.values()) + + +def test_fixture_arrays_are_two_dimensional() -> None: + arrays = _load_arrays_read_only() + + assert all(array.ndim == 2 for array in arrays.values()) + + +def test_fixture_arrays_are_c_contiguous() -> None: + arrays = _load_arrays_read_only() + + assert all(array.flags.c_contiguous for array in arrays.values()) + + +def test_fixture_arrays_are_finite() -> None: + arrays = _load_arrays_read_only() + + assert all(np.isfinite(array).all() for array in arrays.values()) + + +def test_fixture_shapes_match_manifest() -> None: + manifest = _load_manifest() + arrays = _load_arrays_read_only() + + for case in _cases(manifest): + expected_shape = tuple(case["shape"]) + for name in case["arrays"].values(): + assert arrays[name].shape == expected_shape + + +def test_fixture_canonical_hashes_match_manifest() -> None: + manifest = _load_manifest() + arrays = _load_arrays_read_only() + + for case in _cases(manifest): + for role, name in case["arrays"].items(): + assert _canonical_array_hash(arrays[name]) == case["canonical_hashes"][role] + + +def test_fixture_helper_returns_read_only_arrays() -> None: + arrays = _load_arrays_read_only() + + assert all(not array.flags.writeable for array in arrays.values()) + + +def test_background_and_corrected_shapes_match_input() -> None: + manifest = _load_manifest() + arrays = _load_arrays_read_only() + + for case in _cases(manifest): + input_array = arrays[case["arrays"]["input"]] + background = arrays[case["arrays"]["background"]] + corrected = arrays[case["arrays"]["corrected"]] + assert background.shape == input_array.shape + assert corrected.shape == input_array.shape + + +def test_fixture_reconstruction_contract() -> None: + manifest = _load_manifest() + arrays = _load_arrays_read_only() + reconstruction = manifest["acceptance_contract"]["reconstruction"] + + for case in _cases(manifest): + input_array = arrays[case["arrays"]["input"]] + background = arrays[case["arrays"]["background"]] + corrected = arrays[case["arrays"]["corrected"]] + np.testing.assert_allclose( + input_array, + background + corrected, + atol=reconstruction["absolute_tolerance"], + rtol=reconstruction["relative_tolerance"], + ) + + +def test_input_does_not_share_memory_with_outputs() -> None: + manifest = _load_manifest() + arrays = _load_arrays_read_only() + + for case in _cases(manifest): + input_array = arrays[case["arrays"]["input"]] + background = arrays[case["arrays"]["background"]] + corrected = arrays[case["arrays"]["corrected"]] + assert not np.shares_memory(input_array, background) + assert not np.shares_memory(input_array, corrected) + + +def test_radius_and_backend_inventory_is_exact() -> None: + manifest = _load_manifest() + + assert manifest["campaign"]["radius_inventory"] == _EXPECTED_RADIUS_INVENTORY + assert {case["rank_backend_reference"] for case in _cases(manifest)} == { + "direct", + "radixtree", + } + + +def test_evidence_classification_is_exact() -> None: + manifest = _load_manifest() + + assert manifest["evidence_classification"] == { + "external_probe": "EXECUTABLE_EXTERNAL_REFERENCE", + "freeze_audit": "MEDIAN_BACKGROUND_ORACLE_FREEZE_APPROVED", + "independent_oracle": "INDEPENDENT_PYTHON_ORACLE", + "spmkit_implementation": "NOT_YET_IMPLEMENTED", + } + + +def test_acceptance_contract_is_exact() -> None: + manifest = _load_manifest() + contract = manifest["acceptance_contract"] + + assert contract["background_comparison"] == "bitwise exact float64 equality" + assert contract["corrected_comparison"] == "bitwise exact float64 equality" + assert contract["output_dtype"] == "float64" + assert contract["output_shape"] == "identical to input" + assert contract["output_c_contiguous"] == "required" + assert contract["output_finiteness"] == "required for finite inputs in this fixture" + assert contract["input_mutation"] == "forbidden" + assert contract["reconstruction"] == { + "absolute_tolerance": 1e-15, + "relation": "input == background + corrected", + "relative_tolerance": 0.0, + } + assert contract["no_acceptance_relaxation"] == ( + "no acceptance relaxation may be introduced merely to satisfy tests" + ) + + +def test_fixture_contains_no_oracle_or_reference_arrays() -> None: + arrays = _load_arrays_read_only() + + assert all(not name.startswith(("oracle_", "reference_")) for name in arrays) diff --git a/tests/validation/test_path_level_fixture_integrity.py b/tests/validation/test_path_level_fixture_integrity.py new file mode 100644 index 0000000..4db1995 --- /dev/null +++ b/tests/validation/test_path_level_fixture_integrity.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parent / "fixtures/gwyddion/path_level" + + +def _canonical_hash(array: np.ndarray) -> str: + value = np.ascontiguousarray(array) + digest = hashlib.sha256() + digest.update(value.dtype.str.encode("ascii")) + digest.update(b"\0") + digest.update(",".join(str(item) for item in value.shape).encode("ascii")) + digest.update(b"\0") + digest.update(value.tobytes(order="C")) + return digest.hexdigest() + + +def test_path_level_fixture_integrity() -> None: + manifest = json.loads((ROOT / "path_level_reference.json").read_text()) + serialized = json.dumps(manifest, sort_keys=True) + forbidden_markers = ( + "/" + "tmp/", + "/" + "home/", + "." + "reference", + "_" + "_" + "pycache__", + ) + assert not any(marker in serialized for marker in forbidden_markers) + assert manifest["schema_version"] == 1 + assert manifest["capability"] == "gwyddion_path_level" + assert len(manifest["bases"]) == 18 + assert len(manifest["cases"]) == 72 + assert manifest["thicknesses"] == [1, 2, 3, 128] + assert manifest["metrics"]["exact_elements"] == "4652/4652" + assert manifest["metrics"]["external_oracle_arrays"] == "72/72 bitwise" + assert manifest["line_order_discriminator"]["outputs_differ"] is True + assert len({case["case_id"] for case in manifest["cases"]}) == 72 + assert all(len(case["lines_hex"]) == 4 * case["line_count"] for case in manifest["cases"]) + assert all( + len(case["normalized_endpoints"]) == 4 * case["line_count"] for case in manifest["cases"] + ) + external_artifacts = manifest["evidence"]["source_hashes"]["external_artifacts"] + assert external_artifacts["canonical_reference.json"] == ( + "5dcbd07836de0d6cd856dbfe620f7c24edded25a993c17472746b09e80902d84" + ) + assert manifest["evidence"]["source_hashes"]["oracle_artifacts"]["path_level_oracle.py"] == ( + "9b4ec65ba67777c211ab08c73d91bf523ee5082f04ce0424ea4f1fe569ae58f1" + ) + with np.load(ROOT / "path_level_reference.npz", allow_pickle=False) as archive: + assert len(archive.files) == 90 + assert set(archive.files) == set(manifest["fixture"]["array_hashes"]) + for name in archive.files: + array = archive[name] + assert array.dtype == np.float64 + assert array.ndim == 2 and array.flags.c_contiguous + assert np.isfinite(array).all() + assert _canonical_hash(array) == manifest["fixture"]["array_hashes"][name] + assert archive["input__singleton_row_1x9_horizontal"].view(np.uint64)[0, 0] == 1 << 63 + for base in manifest["bases"]: + assert base["input_key"] in archive.files + assert list(archive[base["input_key"]].shape) == base["shape"] + for case in manifest["cases"]: + assert case["output_key"] in archive.files + assert archive[case["output_key"]].shape == archive[f"input__{case['base_id']}"].shape diff --git a/tests/validation/test_sphere_revolution_vs_gwyddion.py b/tests/validation/test_sphere_revolution_vs_gwyddion.py new file mode 100644 index 0000000..603782b --- /dev/null +++ b/tests/validation/test_sphere_revolution_vs_gwyddion.py @@ -0,0 +1,306 @@ +"""Validation tests for SPMKit Gwyddion Sphere Revolution against frozen 2.71 fixtures.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import numpy as np + +from spmkit.core.analysis import ( + BackgroundResult, + analyze_gwyddion_sphere_revolution_background, +) +from spmkit.core.analysis._gwyddion_sphere_revolution import ( + _gwyddion_sphere_background, + _gwyddion_sphere_result, +) +from spmkit.core.models import SPMChannel + +_FIXTURE_DIR = Path(__file__).parent / "fixtures" / "gwyddion" / "sphere_revolution" +_NPZ_PATH = _FIXTURE_DIR / "sphere_revolution_reference.npz" +_JSON_PATH = _FIXTURE_DIR / "sphere_revolution_reference.json" + +with _JSON_PATH.open("r", encoding="utf-8") as _f: + _METADATA = json.load(_f) + +_ACCEPTANCE_ATOL = float(_METADATA["acceptance"]["background_atol"]) +_ACCEPTANCE_RTOL = float(_METADATA["acceptance"]["rtol"]) + + +def _canonical_array_sha256(array: np.ndarray) -> str: + canonical = np.ascontiguousarray(array, dtype=np.float64) + digest = hashlib.sha256() + digest.update(str(canonical.dtype).encode("ascii")) + digest.update(b"\0") + digest.update(",".join(str(value) for value in canonical.shape).encode("ascii")) + digest.update(b"\0") + digest.update(canonical.tobytes(order="C")) + return digest.hexdigest() + + +def test_sphere_fixture_metadata_and_artifact_hashes() -> None: + assert _METADATA["schema_version"] == 2 + assert _METADATA["reference_software"] == "Gwyddion" + assert _METADATA["reference_version"] == "2.71" + assert _METADATA["operation"] == "sphere-revolve" + assert _METADATA["spmkit_method"] == "gwyddion_sphere_revolution" + assert _METADATA["branch"] == "feat/gwyddion-leveling-parity" + assert _METADATA["head"] == "c693fc97e94a5829f57c8b2acf8f522f4f05fb4f" + assert len(_METADATA["case_order"]) == 10 + + npz_sha256 = hashlib.sha256(_NPZ_PATH.read_bytes()).hexdigest() + assert npz_sha256 == _METADATA["npz_sha256"] + + source_hashes = _METADATA["source_and_artifact_hashes"] + expected_sources = { + "sphere_revolve_c": "4218cd4e303634c610e9be5f18656d12715c68df95a9b30930b33232b3d8cbe9", + "probe_c": "97248b51df742937ed5dc0a975b8b1ca08b1b6eeb5add95eda4526118337b188", + "runner_sh": "d673393126833277bda41c77403f1dbaf5dc965d6d8b63ee73994238bec8f7a7", + "oracle_py": "f1598e5f7cd0e173ec72ea928e270038ac8ab5f4c61d57b31a60373e50d40e4b", + "precision_audit_py": "58f1acd0d3c3d644c93adcc7c754889e883726976341695e974d4c09a34f72e4", + "normative_spec_md": "3db2e7ea0c965dfa9bcd803ade6d3dae2fc38b216a132c05df36d52a9256b1c8", + "kernel_py": "ef00ec0033de3966ff1ab7642fdba3f24e0d9030550e5c7995fb55318eb75a38", + "core_test_py": "d0a46f20d6260327f80d68ddae8bfa5360def162b6ca6db77511baf5df5e6ef5", + } + for k, v in expected_sources.items(): + assert source_hashes[k] == v + + +def test_sphere_fixture_canonical_array_hashes() -> None: + expected_hashes = _METADATA["canonical_array_hashes"] + npz_data = np.load(_NPZ_PATH) + + assert len(npz_data.files) == 80 + assert len(expected_hashes) == 80 + assert set(npz_data.files) == set(expected_hashes.keys()) + + for array_name in npz_data.files: + array = npz_data[array_name] + assert array.dtype == np.float64 + assert array.flags.c_contiguous + assert np.all(np.isfinite(array)) + assert _canonical_array_sha256(array) == expected_hashes[array_name] + + +def test_gwyddion_sphere_direct_normal_background() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + + inp = npz_data[f"input__{pair_id}"] + expected_bg = npz_data[f"direct_background__{pair_id}"] + + inp_copy = inp.copy() + bg = _gwyddion_sphere_background(inp, radius) + + assert bg.dtype == np.float64 + assert bg.flags.c_contiguous + assert not bg.flags.writeable + np.testing.assert_array_equal(inp, inp_copy) + + np.testing.assert_allclose( + bg, + expected_bg, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + ) + + +def test_gwyddion_sphere_direct_normal_corrected() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + + inp = npz_data[f"input__{pair_id}"] + expected_corr = npz_data[f"direct_corrected__{pair_id}"] + + inp_copy = inp.copy() + _, corr = _gwyddion_sphere_result(inp, radius, inverted=False) + + assert corr.dtype == np.float64 + assert corr.flags.c_contiguous + assert not corr.flags.writeable + np.testing.assert_array_equal(inp, inp_copy) + + np.testing.assert_allclose( + corr, + expected_corr, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + ) + + +def test_gwyddion_sphere_normal_on_negated_input() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + + neg_inp = npz_data[f"negated_input__{pair_id}"] + expected_neg_bg = npz_data[f"direct_negated_background__{pair_id}"] + + neg_bg = _gwyddion_sphere_background(neg_inp, radius) + + np.testing.assert_allclose( + neg_bg, + expected_neg_bg, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + ) + + +def test_gwyddion_sphere_derived_inverted_background() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + + inp = npz_data[f"input__{pair_id}"] + expected_inv_bg = npz_data[f"derived_inverted_background__{pair_id}"] + + inv_bg, _ = _gwyddion_sphere_result(inp, radius, inverted=True) + + assert inv_bg.dtype == np.float64 + assert inv_bg.flags.c_contiguous + assert not inv_bg.flags.writeable + + np.testing.assert_allclose( + inv_bg, + expected_inv_bg, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + ) + + +def test_gwyddion_sphere_safe_inverted_corrected() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + + inp = npz_data[f"input__{pair_id}"] + expected_safe_corr = npz_data[f"safe_inverted_corrected__{pair_id}"] + + _, inv_corr = _gwyddion_sphere_result(inp, radius, inverted=True) + + assert inv_corr.dtype == np.float64 + assert inv_corr.flags.c_contiguous + assert not inv_corr.flags.writeable + + np.testing.assert_allclose( + inv_corr, + expected_safe_corr, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + ) + + +def test_gwyddion_sphere_reconstruction() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + + inp = npz_data[f"input__{pair_id}"] + neg_inp = npz_data[f"negated_input__{pair_id}"] + + # 1. Normal route + bg, corr = _gwyddion_sphere_result(inp, radius, inverted=False) + np.testing.assert_allclose(corr + bg, inp, atol=_ACCEPTANCE_ATOL, rtol=0.0) + + # 2. Negated normal route + neg_bg, neg_corr = _gwyddion_sphere_result(neg_inp, radius, inverted=False) + np.testing.assert_allclose(neg_corr + neg_bg, neg_inp, atol=_ACCEPTANCE_ATOL, rtol=0.0) + + # 3. Inverted route + inv_bg, inv_corr = _gwyddion_sphere_result(inp, radius, inverted=True) + np.testing.assert_allclose(inv_corr + inv_bg, inp, atol=_ACCEPTANCE_ATOL, rtol=0.0) + + +def test_gwyddion_sphere_frozen_inverted_failure_evidence() -> None: + inv_failures = _METADATA["inverted_reference_failures"] + + assert len(inv_failures) == 15 + + for failure in inv_failures: + assert failure["arrays_available"] is False + assert failure["execute_started"] is True + assert failure["execute_returned"] is False + assert failure["normal_exit_code"] != 0 + assert failure["asan_exit_code"] != 0 + + +def test_gwyddion_sphere_public_analyze_against_fixture() -> None: + npz_data = np.load(_NPZ_PATH) + cases = _METADATA["cases"] + + for pair_id in _METADATA["case_order"]: + case_info = cases[pair_id] + radius = float(case_info["radius"]) + inp = npz_data[f"input__{pair_id}"] + + channel = SPMChannel( + name=f"Channel_{pair_id}", + data=inp, + unit="nm", + x_range=1.0e-6, + y_range=1.0e-6, + metadata={"pair_id": pair_id}, + ) + + for inv in (False, True): + res = analyze_gwyddion_sphere_revolution_background( + channel, + radius, + inverted=inv, + ) + + assert isinstance(res, BackgroundResult) + assert res.method == "gwyddion_sphere_revolution" + assert res.parameters == {"radius_px": radius, "inverted": inv} + + assert res.background.unit == "nm" + assert res.background.x_range == 1.0e-6 + assert res.background.y_range == 1.0e-6 + assert res.background.metadata == {"pair_id": pair_id} + + assert res.corrected.unit == "nm" + assert res.corrected.x_range == 1.0e-6 + assert res.corrected.y_range == 1.0e-6 + assert res.corrected.metadata == {"pair_id": pair_id} + + if not inv: + expected_bg = npz_data[f"direct_background__{pair_id}"] + expected_corr = npz_data[f"direct_corrected__{pair_id}"] + else: + expected_bg = npz_data[f"derived_inverted_background__{pair_id}"] + expected_corr = npz_data[f"safe_inverted_corrected__{pair_id}"] + + np.testing.assert_allclose( + res.background.data, + expected_bg, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + ) + np.testing.assert_allclose( + res.corrected.data, + expected_corr, + atol=_ACCEPTANCE_ATOL, + rtol=_ACCEPTANCE_RTOL, + )