Source code for openstef_foundation_models.presets.forecasting_workflow

# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project <openstef@lfenergy.org>
#
# SPDX-License-Identifier: MPL-2.0

"""Presets for building foundation-model forecasting workflows from config.

A :class:`ForecastingWorkflowConfig` declares the model family, the checkpoint
that backs it, the requested quantiles/horizons, the target column, and the
columns to keep; :func:`create_forecasting_workflow` turns it into a
:class:`~openstef_models.workflows.custom_forecasting_workflow.CustomForecastingWorkflow`
with feature-selection preprocessing and quantile-sorting postprocessing. Every
selected non-target column is forwarded to the model as a known covariate.

The checkpoint defaults to the published OpenSTEF Chronos-2 model on the
HuggingFace Hub, so the minimal config is just::

    workflow = create_forecasting_workflow(ForecastingWorkflowConfig())

Pick a different size or variant through :class:`~openstef_foundation_models.models.catalog.Chronos2`,
or pass a :class:`~openstef_foundation_models.models.checkpoint.LocalCheckpoint`
to run a file already on disk.
"""

from typing import Literal, assert_never

from pydantic import Field

from openstef_core.base_model import BaseConfig
from openstef_core.mixins import TransformPipeline
from openstef_core.types import LeadTime, Q, Quantile
from openstef_foundation_models.inference.backend import InferenceBackend
from openstef_foundation_models.inference.provider_selection import DefaultProviderPolicy
from openstef_foundation_models.inference.providers import ExecutionProvider, SessionOptionsConfig
from openstef_foundation_models.models.catalog import Chronos2
from openstef_foundation_models.models.checkpoint import CheckpointRef
from openstef_foundation_models.models.forecasting.chronos2_forecaster import (
    Chronos2Forecaster,
    Chronos2HyperParams,
)
from openstef_models.mixins import ModelIdentifier
from openstef_models.models import ForecastingModel
from openstef_models.transforms.general import Selector
from openstef_models.transforms.postprocessing import QuantileSorter
from openstef_models.utils.feature_selection import FeatureSelection
from openstef_models.workflows.custom_forecasting_workflow import (
    CustomForecastingWorkflow,
    ForecastingCallback,
)


[docs] class OnnxBackendConfig(BaseConfig): """Compute configuration for an ONNX Runtime inference backend. Holds only *how* to run the model (execution providers, session options), not *which* weights: the checkpoint is supplied to :meth:`build` by the caller, so the same compute settings can run different checkpoints. """ kind: Literal["onnx"] = Field(default="onnx", description="Discriminator tag for backend type.") providers: list[ExecutionProvider] | None = Field( default=None, description="Ordered execution providers to try. ``None`` lets :attr:`policy` pick a host-appropriate " "chain from the checkpoint metadata (graceful). An explicit list is used exactly as given (strict: a " "missing accelerator raises).", ) policy: DefaultProviderPolicy = Field( default=DefaultProviderPolicy(), description="Selection policy used when :attr:`providers` is None. Maps the checkpoint's precision and " "static-shape-ness plus the host to an ordered provider chain. Replace it (e.g. a subclass overriding " "select) to target hardware the default does not cover.", ) session_options: SessionOptionsConfig | None = Field( default=None, description="Optional ONNX Runtime session options.", )
[docs] def build(self, checkpoint: CheckpointRef) -> InferenceBackend: """Resolve *checkpoint* and build the ONNX Runtime backend. Importing the backend raises ``MissingExtraError`` if ONNX Runtime is not installed. Args: checkpoint: The checkpoint (weights + metadata) to load and run. Returns: A ready-to-run backend wrapping the resolved checkpoint. """ from openstef_foundation_models.inference.onnx_backend import OnnxBackend # noqa: PLC0415 resolved = checkpoint.resolve() return OnnxBackend.from_checkpoint( resolved, providers=self.providers, session_options=self.session_options, policy=self.policy, )
#: A backend configuration. Currently ONNX-only; kept as a named type so the #: workflow config and factory can grow to a discriminated union of backends #: without changing their public signatures. BackendConfig = OnnxBackendConfig
[docs] class ForecastingWorkflowConfig(BaseConfig): """Declarative configuration for a foundation-model forecasting workflow. Selects a model family and the checkpoint that backs it, the requested quantiles and horizons, the target column, and the columns to keep before forecasting. Every kept non-target column is treated as a known covariate, so weather forecasts condition the prediction. Compute settings (execution providers, session options) live on the nested :attr:`backend` config. """ model: Literal["chronos2"] = Field(default="chronos2", description="Foundation model family to use.") checkpoint: CheckpointRef = Field( default=Chronos2.BASE.checkpoint(), description="Checkpoint (weights + metadata) to load and run. Defaults to the published OpenSTEF " "Chronos-2 dynamic ONNX checkpoint on the HuggingFace Hub. Pick a size and variant with " "`Chronos2.<SIZE>.checkpoint(...)`, or pass a LocalCheckpoint to run a file already on disk.", ) quantiles: list[Quantile] = Field( default=[Q(0.5)], min_length=1, description="Quantile levels to predict.", ) horizons: list[LeadTime] = Field( default=[LeadTime.from_string("PT48H")], min_length=1, description="Forecast horizons to predict.", ) target_column: str = Field(default="load", description="Name of the target column to forecast.") selected_features: FeatureSelection = Field( default_factory=lambda: FeatureSelection.ALL, description="Columns to keep before forecasting. Defaults to all columns. Every kept non-target column " "is forwarded to the model as a known covariate. Must include the target column.", ) backend: BackendConfig = Field( default_factory=OnnxBackendConfig, description="Inference backend (compute) configuration: execution providers and session options.", ) chronos2_hyperparams: Chronos2HyperParams = Field( default_factory=Chronos2HyperParams, description="Hyperparameters for the Chronos-2 forecaster.", ) model_id: ModelIdentifier = Field(default="chronos2", description="Unique identifier for the workflow model.") run_name: str | None = Field(default=None, description="Optional name for this workflow run.") experiment_tags: dict[str, str] = Field( default_factory=dict, description="Optional metadata tags for experiment tracking.", )
[docs] def create_forecasting_workflow(config: ForecastingWorkflowConfig) -> CustomForecastingWorkflow: """Build a foundation-model forecasting workflow from a declarative config. Resolves the checkpoint (lazily importing the inference runtime), composes the forecaster for the selected model family on the configured backend, and wraps it in a workflow with feature-selection preprocessing and quantile-sorting postprocessing. There is no training step: the model is zero-shot, so :meth:`CustomForecastingWorkflow.fit` only fits the feature selector. Args: config: The workflow configuration. Returns: A ready-to-use workflow composing the configured backend. """ match config.model: case "chronos2": model_backend = config.backend.build(config.checkpoint) forecaster = Chronos2Forecaster( backend=model_backend, quantiles=config.quantiles, horizons=config.horizons, hyperparams=config.chronos2_hyperparams, ) case _: assert_never(config.model) callbacks: list[ForecastingCallback] = [] return CustomForecastingWorkflow( model=ForecastingModel( preprocessing=TransformPipeline(transforms=[Selector(selection=config.selected_features)]), forecaster=forecaster, postprocessing=TransformPipeline(transforms=[QuantileSorter()]), target_column=config.target_column, ), model_id=config.model_id, run_name=config.run_name, callbacks=callbacks, experiment_tags=config.experiment_tags, )
__all__ = [ "BackendConfig", "ForecastingWorkflowConfig", "OnnxBackendConfig", "create_forecasting_workflow", ]