nirs4all.pipeline.dagml_bridge module
nirs4all pipeline → dag-ml DSL frontend (migration spike, compile-only).
First slice of the #1 migration gap (dag-ml/docs/migration-nirs4all/): dag-ml’s
compiler ingests serialized nirs4all-style compat DSL JSON, but nirs4all has no
serializer from a live pipeline (operator instances) to that DSL. This module
provides it for the linear transform → y_processing → splitter → model shape —
the parity gate-zero slice (baseline_vertical_slice).
Scope (deliberately narrow for the spike):
Supported: bare transformer/splitter instances →
{"class", "params"}(dag-ml infers transform-vs-splitter from the class; splitters become campaign controller calls, not graph nodes);{"y_processing": op}→y_transform;{"model": op}→model.Not yet: branch / merge / tag / exclude / generators / augmentation / multi-source — these raise
NotImplementedErrornaming the offending step, to be filled in againstdag-ml/docs/design/DSL_NIRS4ALL_PARITY.md.
dag-ml is a CORE dependency since the ADR-17 cutover (the default engine); the import is still guarded so this module imports cleanly even if a broken wheel lacks the native backend. This is compile-only (DSL lowering); execution via host controllers is a later migration phase.
- nirs4all.pipeline.dagml_bridge.compile_with_dagml(pipeline: list[Any], dsl_id: str = 'nirs4all-pipeline') Any[source]
Lower a nirs4all pipeline and compile it to a dag-ml
CompiledPipelineArtifact.- Raises:
ImportError – if the dag-ml core dependency is somehow missing (
pip install nirs4all).NotImplementedError – if the pipeline uses an unsupported construct.
- nirs4all.pipeline.dagml_bridge.constrained_operator_survivor_sequences(generator_node: dict[str, Any]) list[list[Any]][source]
The PRUNED survivor SEQUENCES of a constrained operator generator (ADR-17 1a + 1b-cartesian).
Expands the constrained
_or_-pick /_cartesian_node through nirs4all’s ownexpand_spec— the SAME constraint source of truth both engines use (and the engine-agnostic survivor lock intest_generators_conformance_extra) — and normalizes each variant to a LIST of operator instances (a single-op variant becomes a one-element list). dag-ml’s native operator-SELECT later compiles ONE survivor branch per sequence and, applying pick +_mutex_/_requires_/_exclude_during its own sequence-build (prune_sequences_by_constraints), produces the byte-identical pruned set, so the per-variantvariant_labelfingerprints agree by content (seeoperator_choice_variant_label()). The survivor ORDER followsexpand_spec(legacyOrStrategy/CartesianStrategyinput order), which matches dag-ml’s stable enumerate-retain order — so the{variant_label → config_name}map (built in expand order) and dag-ml’s reports align by content, not position.
- nirs4all.pipeline.dagml_bridge.is_grid_param_generator_spec(grid_map: Any) bool[source]
True ONLY for a
_grid_over MODEL params the native dag-mlGridgenerator can lower at parity.A step-level
{"_grid_": {param: [values], …}, "model": M}lowers to one native dag-mlGridparam generator (_grid_param_generator()). dag-ml’sGridis a flat Cartesian product of each param’s PLAIN value list — itsPipelineDslGeneratorValueis an untagged literal, so it does NOT recursively expand a nested generator value. nirs4all’sGridStrategyDOES expand a nested-generator value (n_components: {"_range_": …}) BEFORE the product, so only a grid whose every value is a list (or bare scalar) of PLAIN, finite, JSON-native scalars — with NO nested dict (a nested generator / object value) and NO nested-generator list element — is representable natively and produces the identical variant set. ALL of:a non-empty dict of
str → values(the bare_grid_param-map, NOcount/_seed_modifier — those reach here only when the caller already split the keyword off, and a modifier changes the variant set vs.expand_spec, so it is excluded by the caller); ANDevery value is a list of plain scalars (or a single plain scalar), each scalar finite and JSON-native (
bool/int/float/str/None) — NO dict, NO list-of-dicts (a nested generator the nativeGridcannot expand), NO numpy / non-finite / non-JSON value; ANDthe param insertion order is already alphabetical (
list(grid_map) == sorted(grid_map)): dag-ml’sGridnests params inBTreeMap(sorted) order while nirs4all nests in INSERTION order, so a non-alphabetical grid would produce a different variant ENUMERATION order thanexpand_spec— keep it on the Python path so the per-variant config map stays aligned.
Any other shape (a nested-generator value, a modifier-bearing or non-alphabetical grid, a non-list non-scalar value, a non-JSON / non-finite scalar) →
False(stays on the correct Pythonexpand_specpath, still dag-ml-native via that route). When in doubt, this returnsFalse.
- nirs4all.pipeline.dagml_bridge.is_param_generator_spec(spec: Any) bool[source]
True ONLY for the exact
_range_/_log_range_list forms lowered natively at proven parity.Conservative by design: a single key (
_range_or_log_range_) whose value is a list of exactly three numbers —{"_range_": [a, b, s]}or{"_log_range_": [a, b, n]}. dag-ml’s native log_range now round-trips throughbuild_execution_plan(the float-label fingerprint drift is fixed bycanonical_generator_numberat value generation, dag-ml2a77a7f), and its base-10 geometric expansion matches nirs4all’s_log_range_exactly, so the list form is native. Everything else still falls back to the correct Pythonexpand_specpath:_grid_— value-level lowering is not proven equivalent to step-level grid expansion;the dict
{"from"/"to"/...}form, a wrong-length list, or any modifier key (count/_seed_) — would change the variant set versusexpand_spec.
Other keys in the dict (e.g. a
modelsibling) are handled by the caller, not here.
- nirs4all.pipeline.dagml_bridge.lower_constrained_operator_pipeline(steps: list[Any], dsl_id: str = 'nirs4all-pipeline') dict[str, Any][source]
Lower a CONSTRAINED operator-generator pipeline to ONE NATIVE Generator DSL step (ADR-17 item 5 slice B).
stepsis the splitter-free pipeline[<constrained generator>, …downstream…](the predicate_is_constrained_operator_generator()admits exactly one constrained_or_-pick /_cartesian_generator plus one concrete downstream model, optionally ay_processing). The generator lowers to ONE canonical{"kind": "generator", …}step that carries its operator OPTIONS as branches/stages, itspick/arrange/then_*/countmodifiers, its lowered operator-contentconstraints, and the downstream model (+ anyy_processing) as thetail— and dag-ml PRUNES (the CATCH-22 fix). dag-ml applies pick + the_mutex_/_requires_/_exclude_prune to the SAME survivor setexpand_specproduced (proven byte-identical in ADR-17 1a), appends thetailso every survivor terminates in the model, and stamps each per-variant report with the multi-opvariant_labelfingerprint of[<survivor ops>, <tail>]— the EXACT sub-sequenceoperator_sequence_variant_label()recomputes host-side.This REPLACES the previous survivor PRE-EXPANSION (
constrained_operator_survivor_sequences→expand_spec→ one_or_branch per survivor): with the nativetailfield onPipelineDslGeneratorStepandcompile_operator_variant_modelsreaching the prune for a model-terminated generator, the host no longer pre-prunes — it hands the constraints + pick to dag-ml. The{variant_label → config_name}map (built indagml.resultfromexpand_spec) still aligns because dag-ml prunes the SAME survivor set, in the SAME legacy order. A non-bare /None/ non-routable choice is rejected upstream by the predicate; an option still lowers each transform via the strict bare-operator path so a stray non-JSON param fails loud (→ Python expand), and a constraint ref that names no option (a malformed constraint) likewise demotes.
- nirs4all.pipeline.dagml_bridge.operator_choice_variant_label(choice: Any, downstream_steps: list[Any]) str[source]
The AUTHORITATIVE
variant_label(hex sha256) for ONE bare-operator_or_choice + its tail.Maps a per-variant dag-ml report back to its operator-choice config (#23 Phase 7). The label is a cross-language CONTENT fingerprint dag-ml stamps on every operator-SELECT report; the host MUST call the dag-ml PyO3 helper (
dag_ml.canonical_operator_variant_label) — NOT a pure-Pythonjson.dumps+ sha256 — because Python’s float formatting diverges from Rust’s (1e-05/1e-7/ 1-ULP shortest decimals, all common NIRS params), which would SILENTLY mis-key the map. Sharing the one Rust codepath makes the host label byte-identical to the report label by construction.The fingerprint is computed over the FULL LOWERED choice sub-sequence — dag-ml’s
lower_operator_variant_modelactivates the chosen_or_branch PLUS every downstream step the branch flows into (the concrete model, anyy_processing), sochoice.stepsis the choice’s transform step FOLLOWED by the downstream steps. The host therefore buildssteps_json=[<choice transform>] + [<lowered downstream steps>]in thePipelineDslSteptagged shape ({"kind", "id", "operator", "params"}) — lowering each downstream step from its RAW form via_label_step_from_raw()(NOT through_step_to_dsl()’sdefault=reprparams, which would stringify a non-JSON downstream param BEFORE strict checking). Theidis ignored by the canonicalization (it reads onlykind/ operator class / params), but the OPERATOR SHAPE matters: a bare-operator transform choice keeps the object operator{"class": FQN}, while a model keeps the BARE-STRING operator FQN — exactly what dag-ml’s lowering produces.downstream_stepsis the lowered pipeline tail after the generator (no splitter, noNone— already split off by the caller).The ENTIRE label payload is STRICTLY sanitized (chosen branch + downstream model + y_processing): every step’s operator + params is recursively coerced (numpy
.item()/.tolist()) and any NaN / Inf / non-str-key / non-JSON value is REJECTED via_strict_json_safe()(NOTdefault=repr, which would emit"np.int64(5)"strings that break byte-identity, and NOTallow_nan=True, which would pass a non-finite the PyO3 helper then rejects mid-run). ANY failure to construct or fingerprint the label — a non-JSON / non-finite param OR the PyO3 helper raising — is converted toDagMlUnsupportedso the inner Python-expand fallback fires instead of crashing.A MULTI-STEP list choice (
[SNV, Detrend], ADR-17 item 5 slice D) is its own flat operator SEQUENCE, so the label is over[<each transform>, <downstream>]—_operator_choice_operators()unpacks the choice into its operator list, matching dag-ml’s multi-transform branch survivor.
- nirs4all.pipeline.dagml_bridge.operator_sequence_variant_label(operators: list[Any], downstream_steps: list[Any]) str[source]
The AUTHORITATIVE
variant_label(hex sha256) for a MULTI-OPERATOR survivor sequence + its tail.The constrained-generator generalization of
operator_choice_variant_label(): a constrained_or_-pick /_cartesian_survivor is a SEQUENCE of operators ([SNV, Detrend]), not a single choice. dag-ml lowers each survivor branch to[<transform op0>, …, <transform opN>] + downstreamand fingerprints THAT whole sub-sequence (operator_variant_label(choice.steps)), so the host builds the SAMEsteps_json=[<each transform>, <lowered downstream>]and fingerprints it via the SAME Rust canonicalization (dag_ml.canonical_operator_variant_label) — byte-identical to the report label by construction. Each operator’s params are STRICTLY sanitized (_canonical_label_params(), NaN/Inf/non-JSON/non-str-key REJECTED →DagMlUnsupported→ the inner Python-expand fallback); the downstream tail is lowered from its RAW form (_label_step_from_raw()). A single-elementoperatorsreproduces the flat-single label exactly.
- nirs4all.pipeline.dagml_bridge.step_has_native_grid(step: Any) bool[source]
True iff a model
stepcarries a step-level_grid_the native dag-mlGridcan lower at parity.A clean native grid is
{"_grid_": <native param-map>, "model": M[, plain params]}: a step-level_grid_whose param-map passesis_grid_param_generator_spec()AND NO variant-set-changing grid modifier sibling (count/_seed_) — those would subsample/reseed the grid, which the nativeGriddoes not reproduce, so a modifier-bearing grid stays on the Python expand path. The single recognition point shared by the bridge lowering (_step_to_dsl()), the generation-kind classifier, and the plain-param application, so the three never disagree on which grids go native.