Architecture Overview
This page and the rest of architecture/ describe what is actually built — the components,
their responsibilities, and the local design rationale recorded next to each. The transferable
why — the design principles that govern these
decisions, and the engineering hypotheses they
serve — lives in the Design Philosophy section.
The architecture prioritises developer velocity, idempotent re-runs, and strict training–serving symmetry — one execution path from research to production, so nothing gets rewritten on the way to production. The primary aim is to develop novel, ambitious, state-of-the-art ML approaches to forecasting. We are simultaneously building a "test-harness" production service so that ML research runs in a production-like environment from day one.
The aim is to manage the entire data pipeline in Dagster: download data, validate data, train ML models, run inference, perform backtests. MLflow tracks every experiment. Re-running a backtest should be as easy as clicking a button in Dagster. Swapping a new model into production should require minimal friction.
The system is designed as a modular monorepo using uv workspaces, with Dagster orchestrating the data pipeline and MLflow tracking experiments. The measured performance engineering behind these choices — storage formats, lazy evaluation, memory bounds, and Polars' row-index ceiling — lives on Performance and Scale.
Core Components
- Environment & Modularity:
uvworkspace (Monorepo). Python 3.14. Individual components must be pip-installable with expressive type hints. - Data Processing: Polars. Chosen for extreme speed and its native
join_asoffunctionality to guarantee no future-data leakage during feature engineering.- Centralized Data Preparation: All data entering ML models passes through a centralized preparation step to enforce strict data contracts, handle missing entities, and ensure consistency between training and inference.
- Storage: Delta Lake on cloud object storage for both power data and NWP weather data. Delta Lake provides ACID transactions, time-travel, and efficient partitioning. The same technology is also how forecasts are delivered to NGED. Each table's physical layout is owned by the
delta_storepackage (ascontractsowns its logical shape); the layouts were chosen by measurement rather than assumption, and the measured numbers are on Performance and Scale → Storage formats. - Orchestration: Dagster. Manages the pipeline via Software-Defined Assets (SDAs). Partitioned by NWP init time, not substation, allowing models to train globally across all substations (if they want to). Dagster is responsible for detecting bad data.
- Every asset says whether the live service needs it, as a
layertag valuedproductionorresearch(defs/_tags.py). The fourproductionassets —power_time_series_and_metadata,h3_grid_weights,ecmwf_ensandlive_forecasts— are everything the deployment runs to produce forecasts. Theresearchassets are everything else: the cross-validation assets, pluspromotable_model_runsandpromoted_model, which need an MLflow tracking server the deployment does not reach. The tag says whether the service needs an asset, not where the asset runs: theresearchassets all run on a researcher's laptop today, and some may move to the cloud later, but never onto the VM that serves the forecasts. Filter either side in the Dagster UI's selection box, or on the command line, withtag:layer=production/tag:layer=research.
- Every asset says whether the live service needs it, as a
- Configuration Management: plain YAML for model configs,
pydanticfor validating them, andpydantic-settingsfor environment-derived settings. A model YAML inconf/model/names itsBaseForecastersubclass as a_target_import path, whichcontracts.config_schemas.import_classresolves;register_experiment_jobapplies the run'sconfig_overrideson top and constructs that class'sCONFIG_CLASS, so pydantic validates every hyperparameter at registration time — its name as well as its value. The resolved config is logged to MLflow. - Experiment Tracking: MLflow.
- Visualisation: Altair for plotting, Marimo for interactive data exploration and web apps.
The Universal Model Interface
All forecasting models subclass BaseForecaster (defined in ml_core), which provides a common train / predict / save / load interface. The model wrapper encapsulates the model weights and all translation logic, keeping Dagster assets completely agnostic to the underlying implementation. Each subclass of BaseForecaster is responsible for defining:
- Feature engineering: Each subclass carries a
feature_engineer: ClassVar[FeatureEngineer]strategy (composition, not inheritance) that owns the full preparation pipeline — from raw inputs (observed power, gridded NWP, time-series metadata) to anAllFeaturesframe. The defaultTabularFeatureEngineerdoes the nearest-cell NWP spatial join then runs the tabular feature pipeline. A future model that needs a different data view (e.g. a CNN wanting a spatial NWP crop per time series) overridesfeature_engineerwith a differentFeatureEngineersubclass without touchingBaseForecasteror any other model.FeatureEngineerandTabularFeatureEngineerlive inpackages/ml_core/src/ml_core/features/. - Input translation: Transforms the canonical
AllFeaturesPolars LazyFrame into the required model shape. - Output translation: Converts native model outputs into the strict
PowerForecastschema. - Persistence: Each subclass owns its own save/load format.
XGBoostForecasterwrites one.ubjfile pertime_series_idplus ameta.jsoncontaining the full serialisedXGBoostConfig. (This may change later. We may switch to saving models using native MLflow flavors (e.g.,mlflow.xgboost.log_model), which serialize the raw model object directly.) - Identity: Model name, version, and optional MLflow experiment ID travel with the config, so every
PowerForecastrow is self-describing.