Parity Strategy

nirs4all-lite needs parity gates because it is intended to become the portable core behind multiple host-language APIs.

Tiers

  1. Upstream native vs upstream binding: each upstream project proves its own binding parity first, especially nirs4all-methods.

  2. nirs4all-lite native vs binding: lite pipelines produce identical results across Rust, Python, R, MATLAB/Octave, and WASM within declared tolerance.

  3. nirs4all-lite vs full Python nirs4all: equivalent pipelines match the current Python library before the lite binding can replace any core path.

Fixture policy

Fixtures must record:

  • dataset identity and provenance;

  • pipeline descriptor;

  • upstream versions or commit SHAs;

  • numeric tolerance and dtype;

  • platform/runtime notes when relevant.

Golden outputs should be generated from the owning upstream implementation, not from host-language rewrites.

Pipeline definition fixtures must use the full Python nirs4all JSON/YAML syntax. The parser-level contract accepts JSON/YAML paths, JSON/YAML text, direct step lists, pipeline, and steps in every target binding, but canonical fixtures should keep the same pipeline envelope used in nirs4all/examples/pipeline_samples. Execution parity is a separate gate: a definition that parses is not considered validated until native lite and every target binding match the full Python nirs4all result for the portable operator subset.

External operators follow the same rule. A language binding can expose operator metadata before execution support exists, but an operator cannot be marked execute-local, execute-remote, or parity-validated until a fixture proves that the host-language adapter delegates to the same upstream behavior.

Initial parity targets

  • PLS pipelines backed by nirs4all-methods.

  • Format load/record conversion backed by nirs4all-formats.

  • Dataset materialization backed by nirs4all-io.

  • Catalog resolution backed by nirs4all-datasets.

  • DAG execution descriptors backed by dag-ml and dag-ml-data.

  • External operator adapters once the relevant executor can plan or call them.

Current execution gates

The initial numeric gates compare the JavaScript/WASM, Python, Rust, R, and MATLAB/Octave bindings against full Python nirs4all on four shared JSON/YAML pipeline fixtures:

  • SNV + PLS;

  • Savitzky-Golay + PLS;

  • Kennard-Stone + SNV + PLS;

  • Kennard-Stone + SNV + Savitzky-Golay + PLS component sweep.

The Python oracle is generated by scripts/parity/generate_python_oracle.py into tests/parity/expected/portable_python_oracle.json. It uses the real Python nirs4all operators for Kennard-Stone, SNV, and Savitzky-Golay, plus sklearn.cross_decomposition.PLSRegression. The WASM, Python, Rust, R, and MATLAB/Octave tests then execute the same fixture descriptors through their runPortablePipeline(), run_portable_pipeline(), run_portable_pipeline_with_library(), nirs4all_run_portable_pipeline(), and nirs4all.runPortablePipeline() APIs and compare split indices, targets, RMSE, predictions, and selected sweep component within the declared tolerances. The WASM gate also reuses the serialized selected model through predictPortablePipeline() and checks that predictions on the held-out rows match the selected run output.

The shared parser contract also has regression coverage for Savitzky-Golay defaults and explicit boundary modes. mode defaults to interp to match full Python nirs4all, while explicit methods-backed SciPy modes (mirror, constant, nearest, wrap, interp) and cval are preserved across Python, Rust, JavaScript/WASM, R, and MATLAB/Octave.

Regenerate the oracle from a workspace that has full Python nirs4all installed:

PYTHONPATH=/home/delete/nirs4all/nirs4all \
  /home/delete/nirs4all/nirs4all/.venv/bin/python \
  scripts/parity/generate_python_oracle.py

Run the WASM parity gate after building/staging nirs4all-methods JS/WASM. Set NIRS4ALL_METHODS_JS_DIST when the methods checkout is not a sibling of this repository. Set NIRS4ALL_LITE_REQUIRE_METHODS_PARITY=1 to make a missing methods artefact fail instead of skip:

NIRS4ALL_METHODS_JS_DIST=/path/to/nirs4all-methods/bindings/js/dist \
NIRS4ALL_LITE_REQUIRE_METHODS_PARITY=1 \
npm test --prefix bindings/wasm

Run the strict Python parity gate with nirs4all-methods importable and a local libn4m build:

PYTHONPATH=bindings/python/src:/path/to/nirs4all-methods/bindings/python/src \
PLS4ALL_LIB_PATH=/path/to/libn4m.so \
NIRS4ALL_LITE_REQUIRE_METHODS_PARITY=1 \
python -m unittest bindings/python/tests/test_execution_parity.py -v

Run the strict Rust parity gate with a local libn4m build:

NIRS4ALL_METHODS_LIB=/path/to/libn4m.so \
LD_LIBRARY_PATH=/path/to/libn4m-directory \
NIRS4ALL_LITE_REQUIRE_METHODS_PARITY=1 \
cargo test -p nirs4all rust_binding_execution_matches_full_python_nirs4all_oracle -- --nocapture

Run the strict R parity gate after installing an n4m R binding that exposes the portable preprocessing and splitter surface:

NIRS4ALL_LITE_PARITY_ORACLE=$PWD/tests/parity/expected/portable_python_oracle.json \
NIRS4ALL_LITE_PARITY_FIXTURES=$PWD/bindings/r/inst/extdata \
NIRS4ALL_LITE_REQUIRE_METHODS_PARITY=1 \
Rscript bindings/r/tests/parity.R

Run the strict MATLAB/Octave parity gate after building the nirs4all-methods +pls4all MEX shims:

NIRS4ALL_LITE_PARITY_ORACLE=$PWD/tests/parity/expected/portable_python_oracle.json \
NIRS4ALL_LITE_PARITY_FIXTURES=$PWD/tests/parity/fixtures \
NIRS4ALL_LITE_REQUIRE_METHODS_PARITY=1 \
octave --quiet --eval "addpath('bindings/matlab/tests'); parity"