Native PLS HPO within each outer fold

Set finetune_params.scope="fold" with native_profile="n4m.pls_role_pipeline.v1" to select PLS parameters separately inside each outer training fold. DAG-ML schedules independent native studies, and Methods performs fitting and prediction. The SDK materializes the declared splitter’s sample-ID memberships; it does not run a Python optimizer or compute the training scores.

import nirs4all
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import KFold

pipeline = [
    KFold(n_splits=3, shuffle=True, random_state=17),
    {
        "model": PLSRegression(n_components=1, scale=True),
        "finetune_params": {
            "engine": "n4m",
            "scope": "fold",
            "approach": "grouped",
            "sampler": "random",
            "pruner": "none",
            "n_trials": 4,
            "seed": 6,
            "metric": "rmse",
            "direction": "minimize",
            "model_params": {
                "n_components": ["int", 1, 3],
                "scale": [False, True],
            },
        },
        "refit_params": {"n_components": 2, "scale": False},
    },
]

with nirs4all.run(
    pipeline,
    {"X": X, "y": y, "sample_ids": sample_ids, "target_names": ["response"]},
    engine="native",
    native_profile="n4m.pls_role_pipeline.v1",
    save_charts=False,
    methods_library_path=library_path,
) as result:
    outer_cv_rmse = result.cv_best_score
    studies = result.tuning_scope_results
    refit_search_params = result.tuning_best_params
    refit_inner_rmse = result.tuning_best_value
    resume_package = result.tuning_resume_package
    archive = result.export("fold-tuned-pls.n4a")

For each outer fold, the study sees only its outer-train sample IDs. The same declared splitter is applied to that pool to build the inner folds. Inner OOF RMSE selects the local winner. A fresh model with those parameters is fitted on all outer-train rows and predicts outer-validation rows. Those external predictions produce cv_best_score; outer-validation targets never select that fold’s winner.

REFIT runs a separate study over the full declared training universe. Its inner OOF winner supplies tuning_best_params and tuning_best_value. refit_params then overrides the winning recipe for the final fitted model. The exported RAW artifact contains that final REFIT state. No outer-fold model weights are reused. In the example, inspection reports two components and scale=False, even if the REFIT search selected different parameters.

n_trials is the total budget for each study, including resumed history. Three outer folds plus REFIT therefore run four studies. With a budget of four, that is up to sixteen trials, each evaluated on its own inner folds. The local budgets, native checkpoints, parameter identities and lineage are kept separately; they are not one campaign-wide search history.

Splitters and group identity

The fold profile accepts exact sklearn KFold and GroupKFold declarations. Shuffling requires an explicit random_state. Inner membership uses the same configuration applied to the scoped pool, preserving sklearn’s actual splits rather than replacing them with a modulo-based split.

For grouped data, supply a complete group vector and use GroupKFold directly:

from sklearn.model_selection import GroupKFold

pipeline[0] = GroupKFold(n_splits=3)
dataset = {
    "X": X,
    "y": y,
    "sample_ids": sample_ids,
    "groups": group_ids,
    "target_names": ["response"],
}

Run that pipeline and dataset with the same options above. All rows of a group remain on the same fold side, including within each outer-train study. Each scoped pool must contain enough groups and training rows for the declared splitter and PLS component count. Missing or partial groups are refused. Declaring groups with KFold is also refused. This profile adds no separate inner_cv setting, resampling splitter, or approach="individual" mode.

Study results and resume

tuning_scope_results returns an independent snapshot containing outer_scopes and refit_scope. Each scope records its phase, outer fold ID (null for REFIT), signed inner fold set, effective parameter fingerprint, winner identity, typed winner_params and complete resume_state. Winner parameters contain only searched keys. Fixed constructor, train and refit settings remain in the signed recipe. Public scale values are booleans; the native categorical ledger uses index 0 for False and 1 for True. Phase JSON is FIT_CV or REFIT; scope ID suffixes retain lowercase fit_cv/refit. The package’s required root coordinator relations table attests group/origin/exclusion identity without carrying X/y buffers.

For metadata inspection through DAG-ML, use dag_ml.methods_hpo_fold_state_from_package(resume_package). It returns a native-validated MethodsFoldHpoState whose to_dict() snapshots are independent. The reader validates the containing package without FIT or HPO.

To continue all studies from two trials to four, retain the complete result.tuning_resume_package, set n_trials=4, and pass the package as finetune_params.resume_package. The containing Package V2 is the resume authority; a standalone N4MOPT checkpoint is insufficient. Changing features, targets, fold membership, groups, train/refit controls or study scope invalidates it. Exchanging histories between scopes is refused even when a containing package fingerprint is recomputed.

Inspect and replay the saved final model

inspection = nirs4all.inspect_portable_predictor_archive_v2(
    archive, methods_library_path=library_path,
)
assert inspection["models"][0]["model_params"] == {
    "n_components": 2, "scale": False,
}
prediction = nirs4all.predict(
    archive,
    {"X": heldout_X, "sample_ids": heldout_ids},
    engine="native",
    methods_library_path=library_path,
)
assert prediction.metadata["training_performed"] is False

Inspection and prediction hydrate the saved final state without FIT or HPO. They require matching installed native packages and no training arrays or heldout targets. Omitting scope, or setting scope="campaign", retains the existing campaign behavior. Calls without the explicit native profile retain the historical N4MM lane. This feature does not add warm-start, a new model family, or the separate multimodal N-D profile. The existing SNV → Savitzky–Golay → PLS recipe remains available through this explicit profile; every inner, outer and REFIT fit preserves its declared preprocessing steps.

The required scientific gate is NIRS4ALL_REQUIRE_NATIVE_PLS_FOLD_HPO=1, with NIRS4ALL_CORE_LIVE_METHODS_LIBRARY selecting the exact Methods library. NIRS4ALL_NATIVE_PLS_INSTALLED_PYTHON selects the installed-wheel Python for detached -I replay when the parent test process uses a different environment.