nirs4all model → n4m/pls4all estimator mapping (G3 prerequisite)

Status: authoritative mapping table for map-model-classes (RELEASE_READINESS.md G3, line 401). This is the prerequisite the release audit names for G3 — drop-in integration of n4m into nirs4all.

Scope: every model class exported by nirs4all/nirs4all/operators/models/sklearn/__init__.py __all__, mapped to its pls4all.sklearn (or full-n4m) counterpart, with a per-class verdict. Every verdict below is backed by code that was read; file:line citations are inline. Citations against nirs4all-methods are repo-relative to that repo; citations against nirs4all are repo-relative to the nirs4all repo.


1. Dependency surface

There are two distinct PyPI distributions under nirs4all-methods, and they do not overlap in what they expose:

dist (PyPI)

import package

what it exposes

model fits?

pls4all (slim subset)

pls4all, pls4all.sklearn

the PLS models, classifiers, selectors, AOM/POP selection, calibration transfer, diagnostics

YES — all PLS *_fit + MethodResult + Model/.n4a plumbing

nirs4all-methods (full)

n4m, n4m.sklearn

preprocessing, filters, augmenters, splitters, interval generators, transfer (DS/PDS)

NO model fits

  • All the model estimators (PLSRegression, OPLSRegression, PCR, SparsePLSRegression, PLSDAClassifier, MBPLSRegression, DIPLSRegression, the _in_sample.py family, …) live in pls4all, not n4m. They are wired in pls4all/sklearn/__init__.py:50-148 and the fit FFI lives in pls4all/_methods.py and pls4all/_model.py.

  • The full n4m package’s FFI (bindings/python/src/n4m/_ffi_decls.py) declares only n4m_pp_* (preprocessing), n4m_filter_*, n4m_interval_generator_*, and augmenter *_fit/*_is_fitted symbols. It has no n4m_method_result_*, no n4m_pls_fit/n4m_model_*, and no per-model *_fit. n4m/python.py is entirely preprocessing/filter/augmenter transforms (n4m/python.py:134 transform(), :441, :479, :631, …).

⚠️ LOAD-BEARING FLAG: the FULL n4m package has no method-result FFI plumbing

A “swap nirs4all models onto the full n4m package” is not possible today, even for the drop-in rows below. The PLS model surface (MethodResult, Model, *_fit) exists only in the slim pls4all package. The full n4m Python binding would need the method-result / model FFI subsystem built first before any model class could be served from it. Every “drop-in”/”adapter-needed” verdict below therefore assumes the target is pls4all.sklearn. If the integration is intended to ride on the full n4m package (so preprocessing + models come from one import), that FFI port is an additional, currently-missing prerequisite. This is consistent with RELEASE_READINESS.md G3 consolidate-pypi-packaging (line 415): “models live in dist pls4all, preprocessing in dist nirs4all-methods; a clean drop-in needs both”.

nirs4all currently has ZERO n4m/pls4all references (greenfield). Verified by grepping the whole nirs4all tree for import n4m / from n4m / import pls4all / from pls4all: 0 matches (the only pls4all-shaped hits are the unrelated internal pls4all_core token). So the swap is config/instance-level (nirs4all resolves models by class-path string or instance), not a rewrite — matching RELEASE_READINESS.md G3 line 397-398.

2. Reproducibility caveat (stochastic methods are NOT bit-reproducible)

n4m’s stochastic kernels (bagging, boosting, random-subspace, GPR, and the randomized selectors CARS/RandomFrog/GA/PSO/VISSA/IRIV/…) seed a PCG64 stream with explicit integer seeds. nirs4all seeds the numpy global RNG. These are different generators, so any stochastic swap will not bit-reproduce nirs4all’s previous outputs — only statistical equivalence can be claimed. Verified: PRODUCTION_AUDIT.md:372 (“nirs4all’s numpy RNG ≠ n4m’s PCG64”) and benchmarks/cross_binding/donor_ops.py declares value_parity=False (“stochastic — nirs4all RNG differs”) for these. RELEASE_READINESS.md seeding-reproducibility-note (line 428-430) tracks the same caveat. The deterministic PLS/PCR/SIMPLS/OPLS/sparse rows are unaffected (they match the oracle at ~1e-15).


4. Mapping table (one row per nirs4all model class)

Constructor-arg mapping uses the convention nirs4all → n4m/pls4all. “—” = no equivalent arg / not applicable.

nirs4all class

n4m/pls4all target

constructor-arg mapping (nirs4all → n4m)

verdict

notes

PLSDA

pls4all.sklearn.PLSDAClassifier

n_components n_components; (nirs4all has only n_components)

adapter-needed

nirs4all PLSDA exposes predict_proba (plsda.py:55); n4m PLSDAClassifier exposes only predict/decision_function (_classification.py:119-132), no predict_proba/classes_-proba. Shim must add proba (column-stack for binary, raw for multiclass) → see follow-ups. Both one-hot Y + argmax decode.

IKPLS

pls4all.sklearn.PLSRegression

n_components→n_components; center→center_x(+center_y); scale→scale_x(+scale_y); algorithm(1/2)→ — (n4m IKPLS variant via solver); backend→

adapter-needed

IKPLS is the Dayal–MacGregor fast kernel; n4m has no algorithm=1/2 IKPLS knob — closest is PLSRegression(solver='kernel'/'wide-kernel'). Numerically PLS1/PLS2-equivalent but not the same code path; backend='jax' has no n4m analogue. Constructor: _init_ at ikpls.py:97-104.

OPLS

pls4all.sklearn.OPLSRegression

n_components(=#ortho)→n_components; pls_components→ — (n4m OPLS folds predictive+orthogonal into one n_components); scale→scale_x/scale_y; backend→

adapter-needed

RELEASE_READINESS lists OPLS as “1:1 confirmed” (line 402). Verdict adapter-needed because nirs4all splits n_components(ortho) + pls_components(predictive) (opls.py:93) while n4m OPLSRegression takes a single n_components + Deflation.ORTHOGONAL (_regression.py:155-187); the two-knob→one-knob translation is the shim’s job.

OPLSDA

pls4all.sklearn.OPLSDAClassifier

n_components(ortho)→n_components; pls_components→ —; scale→scale_x

adapter-needed

nirs4all OPLSDA exposes predict_proba (oplsda.py:159, delegating to inner PLSDA); n4m OPLSDAClassifier has no predict_proba (_classification.py:135). Same two-knob→one-knob issue as OPLS plus the missing-proba gap.

PCR

pls4all.sklearn.PCR

n_components→n_components (nirs4all has only n_components)

drop-in

nirs4all PCR = PCA(k)+LinearRegression (pcr.py:38-41); n4m PCR = Algorithm.PCR + Solver.SVD with lock-step center/scale (_regression.py:190-224). Same fit/predict contract, full predict-on-new-X. Note: nirs4all default k=10, n4m default k=2 (shim passes k through). nirs4all PCR has no _estimator_type (defaults via RegressorMixin).

MBPLS

pls4all.sklearn.MBPLSRegression

n_components→n_components; standardize→(n4m centers internally); method('NIPALS')→(n4m mb forces NIPALS); max_tol→tol; requires block_sizes; backend→

adapter-needed

RELEASE_READINESS “1:1 confirmed” (line 402). Adapter-needed because n4m MBPLSRegression mandates block_sizes=[k1,k2,…] (_method_result_regressors.py:157-167) — nirs4all MBPLS infers/accepts a list-of-blocks X instead (mbpls.py:522). Shim must derive block_sizes. n4m mb_pls fully predicts on new X (coef in original space, pls.h:1316-1326).

DiPLS

no equivalent (semantic mismatch)pls4all.sklearn.DIPLSRegression is a different method

lags,cv_splits,tol,max_iter → ✗ no analogue

keep-native

Name collision / wrong method. nirs4all DiPLS = Dynamic PLS with time lags via trendfitter (dipls.py:73-100, tags dynamic/time-series). pls4all DIPLSRegression = Domain-Invariant PLS (Nikzad-Langerodi), needs X_target (_method_result_regressors.py:101-128). These are not the same algorithm — do NOT map. RELEASE_READINESS line 402 lists “DiPLS 1:1 confirmed” — that claim is incorrect (see §6).

SparsePLS

pls4all.sklearn.SparsePLSRegression or SparseSimplsRegression

n_components→n_components; alpha→sparsity_lambda; max_iter→max_iter; tol→tol; scale→scale_x/scale_y; backend→

adapter-needed

RELEASE_READINESS “1:1 confirmed” (line 402). nirs4all alpha (regularization strength, sparsepls.py:432-440) maps to n4m sparsity_lambda (soft-threshold magnitude, _regression.py:227-265) — scale/semantics differ, so the shim must translate and parity-check, not pass straight through. Both predict on new X.

LWPLS

pls4all.sklearn.LWPLSRegression (in-sample-only)

n_components→n_components; lambda_in_similarity→(no n4m arg — n4m takes n_neighbors); scale→; backend→; batch_size→

blocked

Genuinely local. n4m lw_pls_fit exports only neighbor_indices + per-sample local fits, no global coefficients (pls.h:1342-1346); the wrapper raises NotImplementedError on predict-on-new-X (_in_sample.py:87-101). nirs4all LWPLS does predict on new X (lazy per-query local fit, lwpls.py:1021). C-ABI gap: needs a new C entry-point that accepts a separate predict-X (refit-per-query). Cannot drop in today.

SIMPLS

pls4all.sklearn.PLSRegression(solver='simpls')

n_components→n_components; center→center_x(+center_y); scale→scale_x(+scale_y); backend→

adapter-needed

Mappable-by-param (RELEASE_READINESS line 403). PLSRegression default solver is already 'simpls' (_regression.py:131). Adapter-needed only for the arg-name translation (center/scalecenter_x/scale_x/center_y/scale_y); full predict-on-new-X.

IntervalPLS

no regressor equivalent (n4m IntervalSelector is selection-only)

n_intervals,interval_width,cv,scoring,mode,combination_method → ✗

keep-native

nirs4all IntervalPLS is a full regressor that selects intervals via CV then fits+predicts PLS on them (ipls.py:1166-1205). n4m’s IntervalSelector (_selection.py) is a SelectorMixin — it transforms/masks columns but has no predict. No regressor counterpart → keep-native (or compose IntervalSelector + PLSRegression in a Pipeline as a future port). RELEASE_READINESS line 404 lists “IntervalPLS(regressor)” under “No n4m equivalent”.

RobustPLS

pls4all.sklearn.RobustPLSRegression (currently in-sample-only wrapper)

n_components→n_components; weighting('huber'/'tukey')→(n4m default = PRM Fair-weight; legacy Huber via cfg); c→huber_k; max_iter→max_irls_iter; tol,scale,center,backend→

adapter-needed

NOT blocked. The C ABI n4m_robust_pls_fit exports coefficients, x_mean, y_mean (pls.h:773-777) — the _in_sample.py wrapper (line 146) is merely conservative and refuses predict-on-new-X. A pure-Python wrapper fix (read coef like _method_result_regressors.py) restores predict-on-new-X with no C work. nirs4all RobustPLS predicts on new X (robust_pls.py:780). See §6 (stale docstring) and follow-ups.

RecursivePLS

pls4all.sklearn.RecursivePLSRegression (in-sample-only) — semantic mismatch

forgetting_factor→(no n4m arg — n4m takes window_size); scale,center,backend→

blocked

Different algorithm + no coef. nirs4all RecursivePLS = exponentially-weighted online RPLS with a forgetting_factor, predicts on new X (recursive_pls.py:555-568, :788). n4m recursive_pls_run = fixed window_size moving-window evaluator, exports only predictions + in_window mask, no coefficients (pls.h:728-731); wrapper raises NotImplementedError (_in_sample.py:200-214). Neither the semantics nor predict-on-new-X line up. C-ABI gap: would need a forgetting-factor RPLS that exports a global coef. Keep-native is the pragmatic call until then.

KOPLS

no equivalent (n4m has KernelPLS, not Kernel-OPLS)

n_components,n_ortho_components,kernel,gamma,degree,coef0,center,scale,backend → ✗

keep-native

n4m KernelPLSRegression is kernel-PLS (Rosipal–Trejo), not Kernel-OPLS (orthogonal kernel decomposition). No n4m K-OPLS kernel. kopls.py:646-667.

KernelPLS (from nlpls.py)

pls4all.sklearn.KernelPLSRegression (in-sample-only)

n_components→n_components; kernel→kernel_type(int enum); gamma→gamma; degree→degree; coef0→coef0; center_kernel→; scale_y→; backend→

blocked

Mappable-by-param (RELEASE_READINESS line 403) but in-sample-only: n4m kernel_pls_fit exports alpha+y_mean but not the kernel-centering statistics needed for K(X_new, X_train), so the wrapper raises NotImplementedError on new X (_method_result_regressors.py:311-380, esp. 367-380; pls.h:869-873). nirs4all KernelPLS predicts on new X (nlpls.py:747). C-ABI gap: export kernel-centering means.

NLPLS

pls4all.sklearn.KernelPLSRegression (in-sample-only)

same as KernelPLS above (kernel→kernel_type, etc.)

blocked

NLPLS and KernelPLS are the same nlpls.py family (NLPLS/KernelPLS/KPLS aliases, nlpls.py:53 re-export). Same in-sample-only kernel-centering gap as the KernelPLS row.

KPLS (alias from nlpls.py)

pls4all.sklearn.KernelPLSRegression (in-sample-only)

same as KernelPLS

blocked

Alias of the nlpls.py KernelPLS (nlpls.py:53). Same kernel-centering C-ABI gap.

OKLMPLS

no equivalent

featurizer,lambda_dyn,lambda_reg_y,max_iter,tol,warm_start_pls,standardize,backend,random_state → ✗

keep-native

Koopman-latent-map dynamic PLS with a pluggable featurizer (oklmpls.py:766-789); no n4m analogue. RELEASE_READINESS line 404.

FCKPLS

no equivalent

alphas,sigmas,kernel_size,mode,kernel_type,standardize,backend → ✗

keep-native

Fractional convolutional-kernel PLS (fckpls.py:595-614); no n4m analogue. RELEASE_READINESS line 404. (Also exports FractionalPLS, FractionalConvFeaturizer — keep-native.)

AOMPLSRegressor

no equivalent (paper companion)

bank/operator args → ✗

keep-native

Vendored aom-nirs (aom_pls.py:20 re-exports _aom_nirs.pls.estimators). pls4all only ships AOM selection result handles (aom_global_select/aom_per_component_select), not the sklearn AOM-PLS regressor.

POPPLSRegressor

no equivalent (paper companion)

bank args → ✗

keep-native

Vendored aom-nirs per-component AOM-PLS (pop_pls.py:17).

AOMPLSAomlibRegressor

no equivalent

n_components,selection,cv,one_se,preprocessing,osc_n_components,asls_*,center,external_folds, X_val/y_val → ✗

keep-native

Wraps the external aompls C++ backend (aom_pls_aomlib.py:149-209). Only model with X_val/y_val in its fit signature (aom_pls_aomlib.py:211-217, currently unused). No n4m analogue.

AOMPLSClassifier

no equivalent (paper companion)

keep-native

Vendored aom-nirs AOM-PLS-DA (aom_pls_classifier.py:9).

POPPLSClassifier

no equivalent (paper companion)

keep-native

Vendored aom-nirs POP-PLS-DA (pop_pls_classifier.py:5).

AOMRidge family (AOMRidgeRegressor, AOMRidgeClassifier, AOMRidgeBlender, AOMRidgeAutoSelector, AOMRidgePLS, AOMRidgePLSCV, AOMMultiKernelRidge, AOMKernelizer, AOMMultiBranchMKL, AOMLocalRidge)

partial equivalent: native n4m.aom_ridge_blender for strict-linear compact/wide Ridge OOF blending; broader family still paper companion

keep-native except covered blender subset

Vendored aom-nirs.ridge (aom_ridge.py:16-27) still contains many non-covered AOMRidge variants.

FastAOM family (FastAOMPLSRidge, FastAOMConfig, SingleChainPLSRidge, HardAOMChainPLSRidge, SoftAOMChainPLSRidge, SparseMultiKernelRidge)

no equivalent (paper companion)

keep-native

All vendored aom-nirs.fast (aom_fast.py:18-47). No n4m counterpart.

TabPFNNIRSRegressor

no equivalent

n_estimators,max_features,sg_*,osc_n_components,random_state,device,model_path → ✗

keep-native

Foundation-model regressor (tabpfn_nirs.py:103-123); no n4m analogue. Also note: no _estimator_type declared.

The model __init__.py __all__ also exports a large set of AOM operators and banks (LinearSpectralOperator, SavitzkyGolayOperator, WhittakerOperator, ComposedOperator, default_bank, compact_bank, …) and FastAOM internals. These are not estimators (no fit/predict model contract) — they are preprocessing operators / config helpers and are out of scope for the model mapping. (Some have n4m preprocessing counterparts in the n4m package, but that is the preprocessing-parity surface, not model G3.)


5. Verdict counts

Tally over the 42 estimator names in nirs4all’s operators/models/sklearn/__init__.py __all__ (operators/banks excluded; KernelPLS/NLPLS/KPLS are one underlying method exported under 3 names; the AOMRidge family is 10 names, FastAOM 6 names):

verdict

count

classes

drop-in

1

PCR

adapter-needed

8

PLSDA, IKPLS, OPLS, OPLSDA, MBPLS, SparsePLS, SIMPLS, RobustPLS

blocked

5

LWPLS, RecursivePLS, KernelPLS, NLPLS, KPLS

keep-native

28

DiPLS, IntervalPLS, KOPLS, OKLMPLS, FCKPLS (+FractionalPLS,FractionalConvFeaturizer), AOMPLSRegressor, POPPLSRegressor, AOMPLSAomlibRegressor, AOMPLSClassifier, POPPLSClassifier, AOMRidge family (10), FastAOM family (6), TabPFNNIRSRegressor

(1 + 8 + 5 + 28 = 42, matching the estimator names in __all__; the additional __all__ entries beyond these 42 are AOM operators/banks, not estimators — see the note under the table.)

RobustPLS is in adapter-needed, not blocked, because its C ABI already exports coefficients (pls.h:773-777) — the only work is a pure-Python wrapper change (RELEASE_READINESS.md in-sample-predict-coef-export, line 410-414).

6. nirs4all classes with NO n4m equivalent (keep-native)

  • DiPLS (dynamic/time-series — not the domain-invariant DIPLSRegression; name collision)

  • IntervalPLS (as a regressor; n4m only has the selector)

  • KOPLS (Kernel-OPLS)

  • OKLMPLS (Koopman-latent dynamic PLS)

  • FCKPLS (+ FractionalPLS, FractionalConvFeaturizer)

  • AOMPLSRegressor, POPPLSRegressor, AOMPLSAomlibRegressor, AOMPLSClassifier, POPPLSClassifier

  • AOMRidge family (10 classes)

  • FastAOM family (6 classes)

  • TabPFNNIRSRegressor


7. Concrete follow-up tasks (per class)

7a. Requires NEW n4m / C-ABI work (cannot be done in pure Python)

  • LWPLS — needs a C-ABI predict-on-new-X. lw_pls_fit is local (neighbor_indices + per-sample fits, no global coef, pls.h:1342-1346). Add a C entry-point that takes (X_train, y_train, X_new) and runs the per-query local fit at predict time. (in-sample-predict-coef-export / the “genuinely local” half of RELEASE_READINESS line 413.)

  • KernelPLS / NLPLS / KPLS — needs kernel-centering export. kernel_pls_fit exports alpha+y_mean but not the training-row/global-mean kernel-centering statistics (pls.h:869-873, wrapper _method_result_regressors.py:367-380). Add those to the C result so K(X_new, X_train) can be centered consistently.

  • RecursivePLS — needs a forgetting-factor RPLS that exports a global coef (n4m’s recursive_pls_run is window-based and prediction-only, pls.h:728-731). Different algorithm; either port nirs4all’s EW-RPLS into the C core or keep native.

  • PLSDA / OPLSDA — predict_proba on the n4m classifier wrappers. n4m PLSDAClassifier/OPLSDAClassifier expose only decision_function/predict (_classification.py:119-132, :135). nirs4all exposes predict_proba (plsda.py:55, oplsda.py:159). Either add predict_proba+classes_-proba to the n4m wrapper (pure-Python, no C work — it’s a column-stack/raw-score transform) or have the shim synthesize it. (RELEASE_READINESS plsda-predict-proba, line 421.) This one is pure-Python despite living in this subsection because it touches the n4m wrapper.

7b. Pure-Python shim work (no n4m engine change)

  • RobustPLS — read coefficients in the wrapper. n4m_robust_pls_fit already exports coefficients/x_mean/y_mean (pls.h:773-777); promote RobustPLSRegression from the in-sample contract (_in_sample.py:146) to a _MethodResultRegressor (the pattern in _method_result_regressors.py). Also fix the stale docstring at _method_result_regressors.py:12-16 that wrongly says robust_pls_fit/ridge_pls_fit/continuum_regression_fit “expose only predictions” — the C ABI exports coefficients for all three (pls.h:773-804). RidgePLS and ContinuumRegression are the same pure-Python fix (not in nirs4all’s __all__, but relevant if added).

  • PCR — straight delegating shim. Translate n_components only; pass through. (drop-in.)

  • SIMPLS — arg-name shim. Map center→center_x/center_y, scale→scale_x/scale_y; target PLSRegression(solver='simpls').

  • OPLS / OPLSDA — two-knob→one-knob shim. Collapse n_components(ortho)+pls_components(predictive) into the single n4m n_components; choose a convention and parity-check.

  • MBPLS — derive block_sizes. nirs4all accepts list-of-blocks X; the n4m target mandates block_sizes (_method_result_regressors.py:157-167). Shim computes block sizes from the block list / config.

  • SparsePLS — translate alphasparsity_lambda and parity-check (scales differ).

  • IKPLS — map center/scale and pick a solver ('kernel'/'wide-kernel'); document that algorithm=1/2 and backend='jax' have no n4m analogue.

7c. Packaging / cross-cutting

  • Build the method-result FFI in the full n4m package (prerequisite for any swap onto n4m rather than pls4all) — see §1 flag. Until then, all drop-in/adapter targets must import from pls4all.sklearn.

  • Packaging decision (consolidate-pypi-packaging, RELEASE_READINESS line 415): one dist re-exporting both n4m+pls4all, or two pinned dists — each bundles its own libn4m; verified not a double-free risk, only doubled payload.

  • Reproducibility note + statistical-equivalence test for stochastic swaps (PCG64 vs numpy global RNG; §2).

  • X_val/y_val plumbing note: only AOMPLSAomlibRegressor carries X_val/y_val in fit today (and ignores them); n4m model fit(X, y) has no such kwarg and would silently drop them. No action needed unless a mapped class starts consuming them (fit-val-kwarg-support, RELEASE_READINESS line 431).

  • _estimator_type gap: PCR and TabPFNNIRSRegressor (and OPLSDA on the nirs4all side) don’t declare _estimator_type; the delegating shim should set it explicitly to keep StackingRegressor/StackingClassifier introspection working.


8. Correctness concerns surfaced while building this map

  1. DiPLS is a name collision, not a mapping. RELEASE_READINESS.md line 402 lists “DiPLS” under “1:1 confirmed”, but nirs4all DiPLS is Dynamic PLS (time lags, via trendfitter, dipls.py:73) while pls4all DIPLSRegression is Domain-Invariant PLS (Nikzad-Langerodi, needs X_target, _method_result_regressors.py:101). Mapping them would silently swap one algorithm for an unrelated one. → keep-native (or rename to avoid future confusion).

  2. Stale wrapper docstring overstates the in-sample limitation. pls4all/sklearn/_method_result_regressors.py:12-16 claims robust_pls_fit, ridge_pls_fit, and continuum_regression_fit “expose only predictions … not coefficients”. The C ABI contradicts this: all three export coefficients/x_mean/y_mean (pls.h:773-804). The corresponding _in_sample.py wrappers are therefore unnecessarily blocked; this directly affects the RobustPLS verdict (adapter-needed, not blocked).

  3. The full n4m package cannot serve any model today (no method-result FFI). Every drop-in/adapter target resolves to pls4all.sklearn. A single-import “preprocessing + models from n4m” story needs the FFI port first. (§1 flag.)