Source: Orchestration & Configuration¶
Overview¶
The orchestration subsystem is the user-facing entry layer that transforms a YAML experiment config and CLI flags into a rooted, reproducible run_dir. Users enter via the commodity-hindcast Click CLI (or the Makefile dev wrappers); the CLI resolves the config into an ExperimentConfig (a pydantic-settings model), gates execution behind preflight path checks, and then delegates to stage modules. The run/ sub-package provides the three lower-level building blocks consumed by stages/run_hindcast.py: a walk-forward fold runner (runner.py), the fold-generation and per-fold fit/predict protocol (experiment_protocol.py), and the preflight gate library (preflight.py).
Modules¶
cli.py (~637 lines)¶
Purpose: Click entrypoint that exposes end-to-end pipeline workflows and stage-level re-run commands.
Public surface:
cli(click.Group,cli.py:161) — root entrypoint; invoking with no subcommand prints help.run(click.Group,cli.py:171) — sub-group for multi-stage workflows.run features(cli.py:188) —run_features_cmd: buildsfit.parquet+pred.parquet.run hindcast(cli.py:212) —run_hindcast_cmd: walk-forward hindcast (FIT + POSTPROCESS + EVALUATE + DELIVER).run fit-production(cli.py:337) —run_fit_production_cmd: production model fit only.run forecast(cli.py:477) —run_forecast_cmd: point-in-time forecast against an existingrun_dir.run forecast-features(cli.py:381) —run_forecast_features_cmd: materialise forecast indices + build forecast features.run forecast-predict(cli.py:427) —run_forecast_predict_cmd: predict + postprocess + deliver using pre-built forecast features.run all(cli.py:269) —run_all_cmd: features → hindcast → forecast → optional export in one call.run export(cli.py:516) —run_export_cmd: model-output export from an existingrun_dir.postprocess(cli.py:535) — stage-level re-run: aggregate + bias correction + conformal CIs.evaluate(cli.py:552) — stage-level re-run: metrics + optional plots.plots(cli.py:581) — stage-level re-run: regenerate PNGs only, with--only/--skipfilter flags.predict(cli.py:609) — stage-level re-run: generate forecast using the production model.deliver(cli.py:625) — stage-level re-run: client-facing CSVs.
Key functions:
run_features_cmd()(cli.py:188) — callspreflight_paths_for_features, thenbuild_features.run_hindcast_cmd()(cli.py:212) — delegates entirely tostages.run_hindcast.run.run_forecast_cmd()(cli.py:477) — delegates tostages.run_forecast.run; requires--run-dir,--season-year,--init-date.run_all_cmd()(cli.py:269) — sequential four-stage orchestrator; logs per-stage elapsed time._prepare_config()(cli.py:112) — setsCOMMODITY_HINDCAST_CONFIGenv var then instantiatesExperimentConfig()viaBaseSettingsso the env/YAML source chain fires correctly (callingmodel_validatewould bypasssettings_customise_sources)._resolve_experiment_config_path()(cli.py:87) — accepts a bare stem (corn_usa) and resolves it toconfigs/corn_usa.yamlunder the package root._validate_export_options()(cli.py:128) — enforcesPIPELINE_RUN_IDandMODEL_INGESTION_PATHenv vars when--exportis set.
Key type: AnyPathParam (cli.py:36) — custom click.ParamType that validates paths via cloudpathlib so s3:// URIs accepted at the CLI don't fail the built-in click.Path existence check (added in PR #345).
External dependencies: click, cloudpathlib.AnyPath, loguru
Notes: INPUT_DATA_DIR is mandatory; require_input_data_dir() raises RuntimeError when unset. --config defaults to configs/corn_usa.yaml. Stage commands (postprocess, evaluate, plots, predict, deliver) operate on an existing run_dir argument and do not need --config.
config.py (~902 lines)¶
Purpose: All pydantic(-settings) config classes for the experiment; the single source of truth for every path, commodity parameter, model choice, and output schema option.
Public surface:
ExperimentConfig(config.py:611) — rootBaseSettingsclass; resolvesINPUT_DATA_DIR→data_rootand fills derived paths.CommodityConfig(config.py:274) — season calendar, builder registry, feature/target columns, climo/weather windows, delivery unit.ModelConfig(config.py:491) — detrend strategy, regression estimator, fit-aggregation level, sample weights.ExperimentProtocolConfig(config.py:483) — CV strategy string,test_years, production cumulative threshold.BiasCorrectorConfig(config.py:528) —kind(none|coverage), lookback window.PostprocessConfig(config.py:545) —bias_corrector, orderedconformalisetuple (primary mode first).ForecastConfig(config.py:579) —raw_obs_filepath,materialised_climo_filepath,residual_mode, runtime-injectedinit_date.DeliveryConfig(config.py:432) —model_public_name,ci_levels,enforce_ci_narrowing,drop_frozen_tail.BaseBuilderConfig(config.py:168) — typed base for builder sub-configs.Builderdiscriminated union (config.py:250) —YieldsBuilder | StressBuilder | ClimoBuilder | WeatherBuilder | NDVIBuilder, discriminated ontype.
Key functions / validators:
require_input_data_dir()(config.py:50) — mandatory env-var read; raisesRuntimeErrorwhenINPUT_DATA_DIRis unset.resolve_data_path()(config.py:82) — anchors relative paths atdata_root; passes throughs3://URIs unchanged.resolve_nested_config()(config.py:124) —model_validatorhelper that loads a nested YAML or passes through an inline dict/model instance.ExperimentConfig._fill_defaults_from_data_root()(config.py:795) — fillsraw_dir,features_dir,models_dir,preds_dir,run_dir_basefromdata_rootwhen unset.ExperimentConfig._resolve_data_paths()(config.py:815) — walks everyResolvablePathfield via_iter_resolvable_fieldsand resolves againstdata_root.ExperimentConfig.settings_customise_sources()(config.py:861) — resolution order:CliSettingsSource> env >YamlConfigSettingsSource> init defaults.ExperimentConfig.init_dates_for()(config.py:706) — returns the singleforecast.init_datein forecast mode, or the fullhindcast_init_season_doysgrid otherwise.experiment_config_to_yaml_safe_dict()(config.py:888) — serialises the config forconfig_resolved.yamland MLflow; handlesCloudPath/Pathvia a JSON fallback.
Notes: ExperimentConfig uses SettingsConfigDict(cli_parse_args=False) (config.py:630) to prevent pydantic-settings from consuming sys.argv — Click handles CLI flags independently (see issue #264). Builder type discriminators are auto-injected from the YAML dict key by _inject_builder_type_from_key (config.py:354). run_dir_base and all derived output paths are set at load time; stages/run_hindcast.py::_create_run_root mutates models_dir and preds_dir in-place to point inside the fresh run_root.
run/runner.py (~139 lines)¶
Purpose: Walk-forward hindcast orchestrator; loops over folds and accumulates per-fold predictions in memory before emitting a single parquet write per fold.
Public surface:
run_walk_forward()(runner.py:27) — accepts(config, data_fold_generator), callsrun_experimentper fold, then calls_predict_fold_rollingand writesyear_data.parquet+walk_forward_preds.parquetonce per fold.
Key design note: Walk-forward deliberately bypasses stages.run_predict.run_predict because that writer has blind-overwrite persistence — sequential calls would destroy earlier init_date rows. Instead, _predict_fold_rolling (runner.py:86) accumulates positional masks on an in-memory copy of year_data (county-major order, matching upstream pred.parquet) and emits one write per fold. The county-major row order must be preserved verbatim because the manifest oracle hashes raw bytes.
External dependencies: loguru, numpy, pandas, treefera_market_insights.shared.utils.dataframes.write_dataframe
run/experiment_protocol.py (~148 lines)¶
Purpose: Defines the per-fold fit/predict protocol (run_experiment) and the expanding-window fold generator (ExpandingFoldGenerator).
Public surface:
run_experiment()(experiment_protocol.py:22) — fits detrender + imputer + regressor for one fold label, predicts on train data, persiststrain_preds.parquetunderpreds_dir/<experiment_key>/<fold_label>/.AbstractFoldGenerator(experiment_protocol.py:92) — ABC with abstractgenerate_folds()yielding(fold_label, train_data, test_data, year_data, references_fold)tuples.ExpandingFoldGenerator(experiment_protocol.py:110) — concrete walk-forward implementation; iteratesExperimentProtocolConfig.test_years, slicingfit_df[year < test_year]for training andpred_df[year == test_year]for prediction. Also yields areferences_fold: dict[str, DataFrame]keyed byspec.namefor per-fold reference-data lookup.
Notes: Test-time predictions are NOT produced or persisted by run_experiment; downstream consumers derive the test slice on demand from walk_forward_preds at the latest init_date of the fold year (via HindcastSlice.load_test_slice). The references_by_harvest parameter to ExpandingFoldGenerator.__init__ (experiment_protocol.py:119) accepts an empty dict as the "no reference data" sentinel — folds still yield but references_fold is empty.
run/preflight.py (~208 lines)¶
Purpose: Path-existence gate library; provides one Check-producing function per pipeline stage. run_preflight raises SystemExit on any critical failure.
Public surface:
Check(preflight.py:21) — dataclass:name,passed,message,critical.check_path_exists()(preflight.py:30) — wrapsAnyPath.exists()(cloud-safe); returns a criticalCheck.run_preflight()(preflight.py:42) — iterates checks, logs results, raisesSystemExiton the first critical failure.preflight_paths_for_features()(preflight.py:110) — checks allResolvablePathinputs, skipping stressfilepathwhenassemble_stress_from_indicesis set (that parquet is produced during this stage).preflight_paths_for_hindcast()(preflight.py:59) — checkscheck_data_existsentries plusfit.parquetandpred.parquetunderfeatures_dir.preflight_paths_for_resolvable_inputs()(preflight.py:77) — mechanically walks_iter_resolvable_fields(config)so adding a newResolvablePathfield automatically extends preflight coverage.preflight_paths_for_forecast_features()(preflight.py:132) — resolvable inputs + canonicalpred.parquet.preflight_paths_for_forecast_predict()(preflight.py:147) — resolvable inputs + per-(season_year, init_date)forecast features parquet + production model artefacts (detrender.pkl,feature_fill_values.parquet).preflight_paths_for_forecast()(preflight.py:183) — union of the two forecast sub-stage check sets.preflight_paths_for_export()(preflight.py:205) — resolvable inputs only.
Notes: _skip_stress_filepath_preflight() (preflight.py:93) uses duck-typing on the owner model to avoid a circular import with commodity_hindcast.config.
Makefile¶
Purpose: Developer convenience wrapper around the commodity-hindcast CLI; anchors all invocations at the repo root.
Key variables:
REPO_ROOT— resolved viagit rev-parse --show-toplevel;cd $(REPO_ROOT)is prepended to every CLI invocation so relative config paths resolve correctly.CONF_FLAG— expands to--config <path>whenEXPERIMENT_KEYis set (e.g.make hindcast EXPERIMENT_KEY=corn_usa);unexport EXPERIMENT_KEYprevents it leaking into the subprocess env where pydantic-settings might misinterpret it.INIT_DATE/SEASON_YEAR— default to current UTC date/year viadate -u; can be overridden per invocation.
Targets: sync, lint, format, test, features, features-force, hindcast, fit-production, forecast, postprocess, evaluate, plots, deliver, explore, help
Notes: forecast requires RUN_DIR to be set explicitly (no default) — callers must run make hindcast or make fit-production first to produce one.
Cross-references to wiki pages¶
- ExperimentConfig (P4)
- CommodityConfig (P4)
- Pipeline: preflight (P5)
- Pipeline: hindcast (P5)
- Pipeline: forecast (P5)
Relationships to other subsystems¶
run_features_cmdcallsfeatures/run.build_featuresandrun.preflight.preflight_paths_for_features.run_hindcast_cmddelegates tostages/run_hindcast.run, which in turn callsrun/runner.run_walk_forwardandrun/experiment_protocol.run_experiment.run_forecast_cmddelegates tostages/run_forecast.run.stages/run_hindcast._create_run_rootcreates the timestampedrun_dirunderconfig.run_dir_base, mutatesconfig.models_dirandconfig.preds_dirin place, and writesconfig_resolved.yaml.ExperimentConfigis consumed by every stage module and all builder/model code as the single config authority.ExperimentConfig.data_rootis sourced fromINPUT_DATA_DIR; relativeResolvablePathfields in builder configs resolve against it viaresolve_data_path.- YAML configs live in
configs/<commodity>_<iso3>.yaml(e.g.configs/corn_usa.yaml); the resolved snapshot is written to<run_dir>/config_resolved.yamlfor reproducibility.