pub(crate) use std::cell::RefCell;
pub(crate) use std::collections::{BTreeMap, BTreeSet};
pub(crate) use std::fs;
pub(crate) use std::io::Read;
pub(crate) use std::path::{Path, PathBuf};
pub(crate) use serde::{Deserialize, Serialize};
pub(crate) use sha2::{Digest, Sha256};
pub(crate) use crate::aggregation::{
aggregate_observation_predictions, aggregate_sample_predictions_by_unit,
reduce_predictions_across_branches, reduce_proba_mean_across_branches,
AggregatedPredictionBlock, AggregationControllerInput, AggregationControllerOutput,
AggregationControllerResult, AggregationControllerTask, ObservationPredictionBlock,
PredictionUnitId,
};
pub(crate) use crate::bundle::{
build_aggregated_prediction_cache_payload, build_prediction_cache_payload,
bundle_prediction_requirement_key, validate_prediction_cache_payload_matches_record,
BundlePredictionCachePayload, BundlePredictionCachePayloadSet, BundlePredictionCacheRecord,
BundlePredictionRequirement, ExecutionBundle, RefitArtifactRecord, ReplayPhaseRequest,
};
pub(crate) use crate::campaign::stable_json_fingerprint;
pub(crate) use crate::controller::{capabilities_support_fit_influence, ControllerCapability};
pub(crate) use crate::data::{
DataBinding, DataRequestPartition, ExternalDataPlanEnvelope, RepresentationCompatibilityReport,
RepresentationPlan, RepresentationReplayManifest,
};
pub(crate) use crate::error::{DagMlError, Result};
pub(crate) use crate::fold::{FoldAssignment, FoldPartitionMode, FoldSet};
pub(crate) use crate::generation::{
enumerate_variants, GenerationChoice, OperatorVariantModel, VariantPlan,
};
pub(crate) use crate::graph::{EdgeSpec, PortKind};
pub(crate) use crate::ids::{
ArtifactId, BranchId, BundleId, ControllerId, FoldId, LineageId, NodeId, RunId, SampleId,
VariantId,
};
pub(crate) use crate::metrics::{
cross_fold_validation_reports, reassemble_merge_targets, score_regression_aggregated_block,
score_regression_prediction_block, OofAverageBlock, RegressionMetricKind,
RegressionMetricReport, RegressionTargetBlock, RegressionTargetRecord, ScoreSet,
SCORE_SET_SCHEMA_VERSION,
};
pub(crate) use crate::oof::{
PredictionBlock, PredictionPartition, StackingOofRefitContract, StackingOofRefitDecision,
StackingOofRefitPolicy,
};
pub(crate) use crate::phase::Phase;
pub(crate) use crate::plan::{prune_plan_to_active, CampaignSpec, ExecutionPlan, NodePlan};
pub(crate) use crate::policy::{
AggregationPolicy, FitInfluencePolicy, PredictionLevel, ShapeDelta, ShapeDeltaKind,
};
pub(crate) use crate::relation::SampleRelationSet;
pub(crate) use crate::rng::SeedContext;
pub(crate) use crate::selection::{
select_candidate, CandidateScore, SelectionMetric, SelectionPolicy,
};
mod artifact;
mod dataview;
mod merge;
mod oof;
mod prediction_store;
mod scheduler;
mod scoring;
mod task;
pub use artifact::*;
pub use dataview::*;
pub(crate) use merge::*;
pub use oof::*;
pub use prediction_store::*;
pub use scheduler::*;
pub(crate) use scoring::*;
pub use task::*;
pub struct BundleReplayExecution<'a> {
pub plan: &'a ExecutionPlan,
pub bundle: &'a ExecutionBundle,
pub replay_request: &'a ReplayPhaseRequest,
pub prediction_cache_store: Option<&'a dyn RuntimePredictionCacheStore>,
pub controllers: &'a RuntimeControllerRegistry,
pub data_provider: &'a dyn RuntimeDataProvider,
pub artifact_store: &'a dyn RuntimeArtifactStore,
pub data_envelopes: &'a BTreeMap<String, ExternalDataPlanEnvelope>,
}
#[derive(Default)]
pub struct RuntimeControllerRegistry {
controllers: BTreeMap<ControllerId, Box<dyn RuntimeController>>,
}
impl RuntimeControllerRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, controller: Box<dyn RuntimeController>) -> Result<()> {
let id = controller.controller_id().clone();
if self.controllers.insert(id.clone(), controller).is_some() {
return Err(DagMlError::RuntimeValidation(format!(
"duplicate runtime controller `{id}`"
)));
}
Ok(())
}
pub fn get(&self, controller_id: &ControllerId) -> Option<&dyn RuntimeController> {
self.controllers.get(controller_id).map(Box::as_ref)
}
}
pub fn dispatch_custom_observation_aggregation(
plan: &ExecutionPlan,
controllers: &RuntimeControllerRegistry,
task_id: impl Into<String>,
block: ObservationPredictionBlock,
relations: SampleRelationSet,
policy: AggregationPolicy,
requested_sample_order: Vec<SampleId>,
) -> Result<PredictionBlock> {
let controller_id = custom_aggregation_controller_id(&policy)?;
ensure_aggregation_controller_capability(plan, controller_id)?;
let task = AggregationControllerTask {
schema_version: crate::aggregation::AGGREGATION_CONTROLLER_TASK_SCHEMA_VERSION,
task_id: task_id.into(),
controller_id: controller_id.clone(),
policy,
reduction_plan: None,
input: AggregationControllerInput::ObservationToSample {
block,
relations,
requested_sample_order,
},
};
let result = dispatch_custom_aggregation_task(controllers, &task)?;
match result.output {
AggregationControllerOutput::Sample { block } => Ok(block),
AggregationControllerOutput::Unit { .. } => Err(DagMlError::RuntimeValidation(format!(
"aggregation controller task `{}` returned unit output for observation input",
task.task_id
))),
}
}
pub fn dispatch_custom_sample_aggregation(
plan: &ExecutionPlan,
controllers: &RuntimeControllerRegistry,
task_id: impl Into<String>,
block: PredictionBlock,
relations: SampleRelationSet,
policy: AggregationPolicy,
requested_unit_order: Vec<PredictionUnitId>,
) -> Result<AggregatedPredictionBlock> {
let controller_id = custom_aggregation_controller_id(&policy)?;
ensure_aggregation_controller_capability(plan, controller_id)?;
let task = AggregationControllerTask {
schema_version: crate::aggregation::AGGREGATION_CONTROLLER_TASK_SCHEMA_VERSION,
task_id: task_id.into(),
controller_id: controller_id.clone(),
policy,
reduction_plan: None,
input: AggregationControllerInput::SampleToUnit {
block,
relations,
requested_unit_order,
},
};
let result = dispatch_custom_aggregation_task(controllers, &task)?;
match result.output {
AggregationControllerOutput::Unit { block } => Ok(block),
AggregationControllerOutput::Sample { .. } => Err(DagMlError::RuntimeValidation(format!(
"aggregation controller task `{}` returned sample output for sample input",
task.task_id
))),
}
}
pub fn dispatch_custom_aggregation_task(
controllers: &RuntimeControllerRegistry,
task: &AggregationControllerTask,
) -> Result<AggregationControllerResult> {
task.validate()?;
let controller = controllers.get(&task.controller_id).ok_or_else(|| {
DagMlError::RuntimeValidation(format!(
"aggregation runtime controller `{}` is not registered",
task.controller_id
))
})?;
let result = controller.invoke_aggregation(task)?;
result.validate_for_task(task)?;
Ok(result)
}
pub(crate) fn custom_aggregation_controller_id(
policy: &AggregationPolicy,
) -> Result<&ControllerId> {
policy.validate()?;
policy
.custom_controller
.as_ref()
.map(|controller| &controller.controller_id)
.ok_or_else(|| {
DagMlError::RuntimeValidation(
"custom aggregation dispatch requires a custom_controller policy".to_string(),
)
})
}
pub(crate) fn ensure_aggregation_controller_capability(
plan: &ExecutionPlan,
controller_id: &ControllerId,
) -> Result<()> {
let manifest = plan
.controller_manifests
.get(controller_id)
.ok_or_else(|| {
DagMlError::Planning(format!(
"missing aggregation controller manifest `{controller_id}`"
))
})?;
if !manifest
.capabilities
.contains(&ControllerCapability::AggregatesPredictions)
{
return Err(DagMlError::Planning(format!(
"aggregation controller `{controller_id}` must declare aggregates_predictions"
)));
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct RunContext {
pub run_id: RunId,
pub root_seed: Option<u64>,
pub variant_id: Option<VariantId>,
pub prediction_store: InMemoryPredictionStore,
pub aggregated_prediction_store: InMemoryAggregatedPredictionStore,
pub lineage: InMemoryLineageRecorder,
pub score_collector: Vec<RegressionMetricReport>,
pub regression_target_records: Vec<RegressionTargetRecord>,
pub oof_average_blocks: Vec<OofAverageBlock>,
}
impl RunContext {
pub fn new(run_id: RunId, root_seed: Option<u64>) -> Self {
Self {
run_id,
root_seed,
variant_id: None,
prediction_store: InMemoryPredictionStore::new(),
aggregated_prediction_store: InMemoryAggregatedPredictionStore::new(),
lineage: InMemoryLineageRecorder::new(),
score_collector: Vec::new(),
regression_target_records: Vec::new(),
oof_average_blocks: Vec::new(),
}
}
pub fn collect_cross_fold_validation_scores(
&mut self,
partition_mode: FoldPartitionMode,
) -> Result<()> {
let outcome = cross_fold_validation_reports(
self.prediction_store.blocks(),
&self.regression_target_records,
SCORE_METRICS,
partition_mode,
)?;
self.score_collector.extend(outcome.reports);
self.oof_average_blocks.extend(outcome.oof_averages);
Ok(())
}
pub fn build_score_set(
&self,
plan_id: impl Into<String>,
selection_metric: Option<String>,
) -> Option<ScoreSet> {
if self.score_collector.is_empty() {
return None;
}
Some(ScoreSet {
schema_version: SCORE_SET_SCHEMA_VERSION,
plan_id: plan_id.into(),
selection_metric,
reports: self.score_collector.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct VariantSelection {
pub selected_variant_id: VariantId,
pub validation_reports: Vec<RegressionMetricReport>,
pub variant_validation_predictions: Vec<VariantValidationPredictions>,
}
#[derive(Clone, Debug)]
pub struct VariantValidationPredictions {
pub variant_id: VariantId,
pub variant_label: Option<String>,
pub predictions: Vec<PredictionBlock>,
pub regression_targets: Vec<RegressionTargetBlock>,
pub oof_average: Option<OofAverageBlock>,
}
pub fn select_best_variant_by_cv<F>(
plan: &ExecutionPlan,
run_id: &RunId,
root_seed: Option<u64>,
selection_metric: RegressionMetricKind,
mut run_single_variant_fit_cv: F,
) -> Result<Option<VariantSelection>>
where
F: FnMut(&ExecutionPlan, &mut RunContext) -> Result<()>,
{
plan.validate()?;
if plan.variants.is_empty() {
return Err(DagMlError::RuntimeValidation(
"cannot select a variant for a plan with no variants".to_string(),
));
}
score_and_rank_variants_by_cv(
&plan.variants,
run_id,
root_seed,
selection_metric,
plan_oof_partition_mode(plan),
|variant| {
Ok(ExecutionPlan {
variants: vec![variant.clone()],
..plan.clone()
})
},
|_variant| Ok(None),
&mut run_single_variant_fit_cv,
)
}
pub fn select_best_operator_variant_by_cv<F>(
union_plan: &ExecutionPlan,
model: &OperatorVariantModel,
run_id: &RunId,
root_seed: Option<u64>,
selection_metric: RegressionMetricKind,
mut run_single_variant_fit_cv: F,
) -> Result<Option<VariantSelection>>
where
F: FnMut(&ExecutionPlan, &mut RunContext) -> Result<()>,
{
union_plan.validate()?;
model.validate()?;
let variants = enumerate_variants(&model.generation_spec(), root_seed)?;
if variants.is_empty() {
return Err(DagMlError::RuntimeValidation(format!(
"operator variant model `{}` produced no variants",
model.generator_id
)));
}
let all_choice_nodes = model
.active_nodes
.values()
.flatten()
.cloned()
.collect::<BTreeSet<NodeId>>();
score_and_rank_variants_by_cv(
&variants,
run_id,
root_seed,
selection_metric,
plan_oof_partition_mode(union_plan),
|variant| {
let active_subsequence = operator_variant_active_subsequence(model, variant)?;
let active_nodes = model.active_nodes.get(active_subsequence).ok_or_else(|| {
DagMlError::RuntimeValidation(format!(
"operator variant model `{}` has no active-node set for `{active_subsequence}`",
model.generator_id
))
})?;
prune_plan_to_active(union_plan, active_nodes, &all_choice_nodes, variant)
},
|variant| {
let active_subsequence = operator_variant_active_subsequence(model, variant)?;
Ok(model.variant_labels.get(active_subsequence).cloned())
},
&mut run_single_variant_fit_cv,
)
}
fn operator_variant_active_subsequence<'a>(
model: &OperatorVariantModel,
variant: &'a VariantPlan,
) -> Result<&'a str> {
let dimension_name = &model.dimension.name;
let choice = variant.choices.get(dimension_name).ok_or_else(|| {
DagMlError::RuntimeValidation(format!(
"operator variant `{}` is missing the operator dimension `{dimension_name}`",
variant.variant_id
))
})?;
choice.active_subsequence.as_deref().ok_or_else(|| {
DagMlError::RuntimeValidation(format!(
"operator variant `{}` choice `{}` has no active_subsequence",
variant.variant_id, choice.label
))
})
}
pub fn select_best_operator_variant_from_models<F>(
union_plan: &ExecutionPlan,
models: &[OperatorVariantModel],
run_id: &RunId,
root_seed: Option<u64>,
selection_metric: RegressionMetricKind,
run_single_variant_fit_cv: F,
) -> Result<Option<VariantSelection>>
where
F: FnMut(&ExecutionPlan, &mut RunContext) -> Result<()>,
{
match models {
[] => Ok(None),
[model] => select_best_operator_variant_by_cv(
union_plan,
model,
run_id,
root_seed,
selection_metric,
run_single_variant_fit_cv,
),
_ => Err(DagMlError::RuntimeValidation(format!(
"operator-SELECT does not support {} operator generators in one pipeline; this phase scopes to a flat single operator generator (generators: {})",
models.len(),
models
.iter()
.map(|model| model.generator_id.to_string())
.collect::<Vec<_>>()
.join(", ")
))),
}
}
#[allow(clippy::too_many_arguments)]
fn score_and_rank_variants_by_cv<M, L, F>(
variants: &[VariantPlan],
run_id: &RunId,
root_seed: Option<u64>,
selection_metric: RegressionMetricKind,
partition_mode: FoldPartitionMode,
mut make_variant_plan: M,
mut resolve_variant_label: L,
run_single_variant_fit_cv: &mut F,
) -> Result<Option<VariantSelection>>
where
M: FnMut(&VariantPlan) -> Result<ExecutionPlan>,
L: FnMut(&VariantPlan) -> Result<Option<String>>,
F: FnMut(&ExecutionPlan, &mut RunContext) -> Result<()>,
{
if variants.is_empty() {
return Err(DagMlError::RuntimeValidation(
"cannot select a variant for a plan with no variants".to_string(),
));
}
let mut candidates: Vec<CandidateScore> = Vec::with_capacity(variants.len());
let mut variant_validation_reports: Vec<RegressionMetricReport> = Vec::new();
let mut variant_validation_predictions: Vec<VariantValidationPredictions> = Vec::new();
let mut any_scores_seen = false;
for variant in variants {
let variant_plan = make_variant_plan(variant)?;
let variant_label = resolve_variant_label(variant)?;
let mut ctx = RunContext::new(run_id.clone(), root_seed);
ctx.variant_id = Some(variant.variant_id.clone());
run_single_variant_fit_cv(&variant_plan, &mut ctx)?;
ctx.collect_cross_fold_validation_scores(partition_mode)?;
if !ctx.score_collector.is_empty() {
any_scores_seen = true;
}
let captured = capture_variant_validation_predictions(
&variant.variant_id,
variant_label.clone(),
&ctx,
);
if !captured.predictions.is_empty() || captured.oof_average.is_some() {
variant_validation_predictions.push(captured);
}
let avg_reports = ctx
.score_collector
.iter()
.filter(|report| {
report.partition == PredictionPartition::Validation
&& report
.fold_id
.as_ref()
.is_some_and(|fold| fold.as_str() == "avg")
})
.collect::<Vec<_>>();
match avg_reports.as_slice() {
[] => {}
[report] => candidates.push(
(*report)
.clone()
.into_candidate_score(variant.variant_id.as_str())?,
),
_ => {
return Err(DagMlError::RuntimeValidation(format!(
"variant `{}` produced {} cross-fold OOF averages (multiple prediction producers); native SELECT needs a single score target",
variant.variant_id,
avg_reports.len()
)));
}
}
for mut report in ctx.score_collector {
if report.partition != PredictionPartition::Validation {
continue;
}
report.variant_id = Some(variant.variant_id.clone());
report.variant_label = variant_label.clone();
variant_validation_reports.push(report);
}
}
if candidates.is_empty() {
if any_scores_seen {
return Err(DagMlError::RuntimeValidation(
"variants produced scores but no cross-fold OOF average; cannot rank — need >=2 folds or an explicit score target".to_string(),
));
}
return Ok(None);
}
if candidates.len() != variants.len() {
return Err(DagMlError::RuntimeValidation(format!(
"native variant SELECT scored only {} of {} variants; cannot rank variants fairly",
candidates.len(),
variants.len()
)));
}
let policy = SelectionPolicy {
id: format!("select:variant:{}", selection_metric.name()),
metric: SelectionMetric {
name: selection_metric.name().to_string(),
objective: selection_metric.objective(),
},
required_metric_level: None,
require_finite: true,
evaluation_scope: None,
refit_slot_plan: None,
stacking_fit_contract: None,
reduction_id: None,
};
let decision = select_candidate(&policy, &candidates)?;
let selected_variant_id = VariantId::new(decision.selected_candidate_id).map_err(|error| {
DagMlError::RuntimeValidation(format!("selected variant id is invalid: {error}"))
})?;
Ok(Some(VariantSelection {
selected_variant_id,
validation_reports: variant_validation_reports,
variant_validation_predictions,
}))
}
fn capture_variant_validation_predictions(
variant_id: &VariantId,
variant_label: Option<String>,
ctx: &RunContext,
) -> VariantValidationPredictions {
let mut predictions = Vec::new();
let mut regression_targets = Vec::new();
for block in ctx.prediction_store.blocks() {
if block.partition != PredictionPartition::Validation {
continue;
}
let Some(record) = ctx.regression_target_records.iter().find(|record| {
record.producer_node == block.producer_node
&& record.partition == PredictionPartition::Validation
&& record.fold_id == block.fold_id
}) else {
continue;
};
predictions.push(block.clone());
regression_targets.push(target_block_aligned_to_samples(
&block.sample_ids,
&record.block,
));
}
VariantValidationPredictions {
variant_id: variant_id.clone(),
variant_label,
predictions,
regression_targets,
oof_average: ctx.oof_average_blocks.first().cloned(),
}
}
fn target_block_aligned_to_samples(
sample_ids: &[SampleId],
targets: &RegressionTargetBlock,
) -> RegressionTargetBlock {
let value_by_sample: BTreeMap<&SampleId, &Vec<f64>> = targets
.unit_ids
.iter()
.zip(&targets.values)
.filter_map(|(unit_id, row)| match unit_id {
PredictionUnitId::Sample(sample_id) => Some((sample_id, row)),
_ => None,
})
.collect();
if sample_ids
.iter()
.any(|sample_id| !value_by_sample.contains_key(sample_id))
{
return targets.clone();
}
RegressionTargetBlock {
level: PredictionLevel::Sample,
unit_ids: sample_ids
.iter()
.cloned()
.map(PredictionUnitId::Sample)
.collect(),
values: sample_ids
.iter()
.map(|sample_id| value_by_sample[sample_id].clone())
.collect(),
target_names: targets.target_names.clone(),
}
}
#[cfg(test)]
mod explain_contract_tests {
use super::*;
fn block(method: &str) -> ExplanationBlock {
ExplanationBlock {
producer_node: NodeId::new("model:base").unwrap(),
method: method.to_string(),
target_name: Some("y".to_string()),
payload: serde_json::json!({"feature_importance": [0.5, 0.3, 0.2]}),
}
}
#[test]
fn validates_well_formed_explanation() {
assert!(block("shap").validate().is_ok());
}
#[test]
fn rejects_empty_method() {
assert!(block(" ").validate().is_err());
}
#[test]
fn rejects_empty_target_name() {
let mut b = block("shap");
b.target_name = Some(String::new());
assert!(b.validate().is_err());
}
#[test]
fn round_trips_through_json() {
let b = block("permutation_importance");
let json = serde_json::to_string(&b).expect("serialize");
let parsed: ExplanationBlock = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, b);
let mut without = block("shap");
without.target_name = None;
let json = serde_json::to_string(&without).expect("serialize");
assert!(!json.contains("target_name"));
}
}
#[cfg(test)]
mod tests;