Contracts API
Contracts
Defines the "data contracts": the schemas defining the precise shape of each data source and its semantics.
Dependency Isolation
This package is designed to be extremely lightweight. It defines the shape of the data using Patito and Polars, but it does not contain any ML-specific logic or heavy dependencies like MLflow. This ensures that any component in the system (e.g., a data ingestion script or a dashboard) can import these schemas without bringing in the entire ML stack.
Key Data Contracts
PowerTimeSeries: Half-hourly power observations (MW or MVA) pertime_series_id, as received from NGED.TimeSeriesMetadata: Substation and customer meter metadata, including lat/lon, H3 index, and asset type (primary substation, GSP, BSP, solar PV, wind, BESS, etc.).Nwp: ECMWF ENS NWP weather data in physical units (Float32), on disk and in memory alike. The on-disk copy is rounded to a 13-bit significand and laid out for compression and row-group pruning bydelta_store.nwp.AllFeatures: The final joined dataset passed to ML models. Primary key is(time_series_id, power_fcst_init_time, valid_time[, ensemble_member]). Includes NWP weather variables, power lag/rolling features, static metadata columns, and datetime features.PowerForecast: ML model output schema. Power values are in the range [−1, +1] (normalised). Includespower_fcst_model_name,power_fcst_model_version,power_fcst_init_time,nwp_init_time,valid_time,time_series_id, andensemble_member.
Design Principles
- Column naming: Prefer
snake_case, except for acronyms or SI units. Capitalise "DER" (distributed energy resource) and use uppercase for "MW" (megawatts). - Semantic checks: Range validation should be generous — the aim is to catch physically impossible values (e.g., 1 GW from a 1 MW solar farm), not possible-but-unlikely values.
- No lookahead bias:
AllFeaturescarriespower_fcst_init_time(when we make the forecast) as a distinct field fromnwp_init_time(when the NWP model ran). Power lag features are nullified bynullify_leaky_lags()when the lag is shorter than or equal to the forecast lead time.
contracts.common
Attributes
UTC_DATETIME_DTYPE = pl.Datetime(time_unit='us', time_zone='UTC')
module-attribute
DELIVERY_QUANTILES = (0.01, 0.02, 0.05, 0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.9, 0.95, 0.98, 0.99)
module-attribute
The thirteen quantile levels agreed with NGED for the delivery tables.
Deliberately tail-heavy: NGED is far more interested in the tails than the shoulders. This
tuple is the single source of truth for every quantile-indexed artefact — the pinball-loss
metric_param labels today, and the percentile columns of the delivery-table
representations (Representations 2 and 3) when those land in v0.5.
Functions:
quantile_label(quantile)
Return the canonical p{level} label for a quantile, e.g. 0.05 → "p5".
The label format matches the percentile column names agreed with NGED for the delivery
tables (see DELIVERY_QUANTILES).
Source code in packages/contracts/src/contracts/common.py
39 40 41 42 43 44 45 | |
validate_schema(model, df)
Validates that the schema of a Polars DataFrame or LazyFrame matches the schema defined in a
Patito model, raising DataFrameValidationError on failure. On LazyFrames, this function doesn't
materialize any data, it just calls collect_schema().
Source code in packages/contracts/src/contracts/common.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
contracts.power_schemas
Data schemas for the NGED substation forecast project.
Attributes
LIST_OF_TIME_SERIES_TYPES = ('BESS', 'Biofuel', 'CHP', 'Data Centre', 'Disaggregated Demand', 'Energy from Waste', 'EV Charging', 'Geothermal', 'Hydro', 'Hydrogen Electrolysis', 'Industrial Demand', 'Mixed (Demand)', 'Mixed (Generation)', 'Other (Demand)', 'Other (Generation)', 'Other (Storage)', 'Peaking Plant', 'PV', 'Rail', 'Raw Flow', 'Synchronous Condenser', 'Wind')
module-attribute
All time-series type values used in NGED data.
Types present in the V1 trial area: BESS, Biofuel, Disaggregated Demand, Other (Generation), PV, Raw Flow, Wind.
Notes:
- BESS: Battery energy storage system.
- Disaggregated Demand: In the trial area, exclusively associated with "Primary" substations. All "Primary" substations in the trial area have their TimeSeriesType set to "Disaggregated Demand". Indicates that NGED have already removed metered generation connected to that primary.
- Raw Flow: Used for BSP and GSP substations.
FoldId = str
module-attribute
Fold identifier for PowerForecast.fold_id.
A CV fold's id is a short label defined in conf/cv/default.yaml (e.g.
"mid_2025_to_mid_2026"); fold identity is config-driven, never hard-coded here. "live" is
the reserved sentinel for a production forecast that belongs to no CV fold.
Classes
PowerTimeSeries
Bases: Model
Source code in packages/contracts/src/contracts/power_schemas.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
Attributes
time_series_id = _get_time_series_id_dtype()
class-attribute
instance-attribute
time = pt.Field(dtype=UTC_DATETIME_DTYPE, description='End time of the 30-minute observation period (all NGED data is already half-hourly).')
class-attribute
instance-attribute
power = pt.Field(dtype=(pl.Float32), ge=(-1000), le=1000, description='Average power (MW or MVA) over the preceding 30-minute period. Unit defined in TimeSeriesMetadata.')
class-attribute
instance-attribute
columns_to_sort_by = ('time_series_id', 'time')
class-attribute
Methods:
validate(dataframe, columns=None, allow_missing_columns=False, allow_superfluous_columns=False, drop_superfluous_columns=False)
classmethod
Validate the given dataframe, ensuring time is at :00 or :30 and uniqueness.
Source code in packages/contracts/src/contracts/power_schemas.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
TimeSeriesMetadata
Bases: Model
Source code in packages/contracts/src/contracts/power_schemas.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
Attributes
time_series_id = _get_time_series_id_dtype(unique=True)
class-attribute
instance-attribute
time_series_name = pt.Field(dtype=(pl.String), description='Human-readable name for the substation or asset.', examples=['ALFORD 33 11kV S STN', 'BAMBERS FARM WIND GENERATION MABLETHORPE 33kV S ST', 'Leverton Solar Park'])
class-attribute
instance-attribute
time_series_type = pt.Field(dtype=(pl.Enum(LIST_OF_TIME_SERIES_TYPES)), description='Asset category (e.g. ‘PV’, ‘Wind’, ‘Disaggregated Demand’). See LIST_OF_TIME_SERIES_TYPES.')
class-attribute
instance-attribute
units = pt.Field(dtype=(pl.Enum(['MW', 'MVA'])), description='Power unit for this time series: ‘MW’ (active power) or ‘MVA’ (apparent power).')
class-attribute
instance-attribute
licence_area = pt.Field(dtype=(pl.Enum(['EMids'])), description='NGED licence area (for the trail area, this is always ‘EMids’).')
class-attribute
instance-attribute
substation_number = pt.Field(dtype=(pl.Int32), gt=0, lt=1000000, description='Perhaps surprisingly, each customer meter in the NGED trial area has its own substation_number (not one per physical substation).')
class-attribute
instance-attribute
substation_type = pt.Field(dtype=(pl.Enum(['BSP', 'EHV Customer', 'GSP', 'HV Customer', 'Primary'])), description='Substation voltage level / role: BSP, EHV Customer, GSP, HV Customer, or Primary. HV = high voltage. EHV = extra high voltage.')
class-attribute
instance-attribute
latitude = pt.Field(dtype=(pl.Float32), ge=49, le=61, description="Latitude in decimal degrees. For customer time series, gives the location of the substation, not the customer's site.")
class-attribute
instance-attribute
longitude = pt.Field(dtype=(pl.Float32), ge=(-9), le=2, description="Longitude in decimal degrees. For customer time series, gives the location of the substation, not the customer's site.")
class-attribute
instance-attribute
information = pt.Field(dtype=(pl.String), allow_missing=True, description='Free-text NGED notes field; always null in the V1 trial area.')
class-attribute
instance-attribute
area_wkt = pt.Field(dtype=(pl.String), allow_missing=True, description='WKT polygon for the asset’s area. In the trial, only Primary substations have this. NGED don’t have polygons for customer sites (though they hope to add that in future). For customer sites, where present, refers to the area covered by the generator itself.')
class-attribute
instance-attribute
area_center_lat = pt.Field(dtype=(pl.Float32), allow_missing=True, description='Centroid latitude of the area polygon. For customer sites, the area, where present, refers to the area covered by the generator itself.')
class-attribute
instance-attribute
area_center_lon = pt.Field(dtype=(pl.Float32), allow_missing=True, description='Centroid longitude of the area polygon. For customer sites, the area, where present, refers to the area covered by the generator itself.')
class-attribute
instance-attribute
h3_res_5 = pt.Field(dtype=(pl.UInt64), description='H3 discrete spatial index at resolution 5.')
class-attribute
instance-attribute
PowerForecast
Bases: Model
Forecast data schema for deterministic ensemble forecasts.
Internal vs delivered schema (Milestone 1 report Table 1, p.28): the columns
experiment_name, fold_id, and ml_flow_experiment_id are INTERNAL-ONLY —
they exist on this schema and the internal power_forecasts Delta table to support
cross-validation and the leaderboard, but they are NOT part of the power_forecast
table delivered to NGED.
Source code in packages/contracts/src/contracts/power_schemas.py
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | |
Attributes
valid_time = pt.Field(dtype=UTC_DATETIME_DTYPE, constraints=(pl.col('valid_time') > pl.col('power_fcst_init_time')), description='The target time this forecast is valid for. Constrained to be strictly after power_fcst_init_time: a row targeting a valid time at or before its own initialisation is an undeliverable hindcast row — at power_fcst_init_time that valid time is already observed. The live service only forecasts strictly future valid times and bulk-mode feature engineering drops hindcast rows at source, so a constraint violation here indicates a pipeline regression.')
class-attribute
instance-attribute
time_series_id = _get_time_series_id_dtype()
class-attribute
instance-attribute
ensemble_member = pt.Field(dtype=(pl.Int8), description='Ensemble member index. 0 is the control NWP ensemble member.')
class-attribute
instance-attribute
ml_flow_experiment_id = pt.Field(dtype=(pl.Int32), allow_missing=True, description='MLflow experiment ID; links to the MLflow experiment that produced this forecast.')
class-attribute
instance-attribute
nwp_init_time = pt.Field(dtype=UTC_DATETIME_DTYPE, allow_missing=True, description='The datetime that the underlying weather forecast was initialised. Null for models that do not use NWP (e.g. persistence baselines).')
class-attribute
instance-attribute
power_fcst_model_name = pt.Field(dtype=(pl.String), description='Identifier for our ML-based power forecasting model. Model-family identity set by the BaseForecaster subclass (MODEL_NAME).')
class-attribute
instance-attribute
experiment_name = pt.Field(dtype=(pl.String), description='Per-experiment key identifying the experiment that produced this forecast. Distinct from `power_fcst_model_name`, which is the model-family identity (`MODEL_NAME`); do not overload that with experiment identity. Forecasts are partitioned in Delta by (experiment_name, fold_id). INTERNAL-ONLY: projected out of the `power_forecast` table delivered to NGED.')
class-attribute
instance-attribute
power_fcst_model_version = pt.Field(dtype=(pl.Int16), description='Model version integer, bumped with each breaking change to the model implementation.')
class-attribute
instance-attribute
power_fcst_init_time = pt.Field(dtype=UTC_DATETIME_DTYPE, description='The datetime that the power forecast was initialised. This might be called `t0` in some other OCF projects.')
class-attribute
instance-attribute
power_fcst = pt.Field(dtype=(pl.Float32), description='The power forecast itself in units of MW (active power) or MVA (apparent power). The unit is defined in the `TimeSeriesMetadata` for this `time_series_id`. Positive values mean "power sent to NGED\'s grid", and negative values mean "power drawn from NGED\'s grid". Rows read back from the internal `power_forecasts` Delta table carry reduced precision: values are rounded to a 13-bit significand at write time (max relative error 2^-13 ≈ 1.2e-4, far below forecast error) to aid compression; see `delta_store.power_forecasts`.')
class-attribute
instance-attribute
fold_id = pt.Field(dtype=(pl.String), description="Identifies the source of this forecast row. For cross-validation runs, the value is the fold's label from conf/cv/default.yaml (e.g. 'mid_2025_to_mid_2026'). 'live' means a production forecast with no associated CV fold. All forecasts — CV and live — live in the same Delta table; filter on this column to select the population you need.")
class-attribute
instance-attribute
EffectiveCapacity
Bases: Model
Effective capacity of each time series at each half-hourly timestep.
Delivered to NGED as effective_capacity Delta table (Table 4 in the Milestone 1 report).
This table is backward-looking only — it does not cover the forecast period.
v0.1 implementation: one row per time_series_id, time set to the end of
the available observation history, effective_capacity_mw = P99 of abs(power) over
the full observed history. This is a static scalar per series.
Planned upgrade (v0.7): replace the P99 scalar with a time-varying capacity estimate
(see docs/techniques/convex-optimisation.md and
docs/techniques/differentiable-physics.md for the candidate estimation methods),
giving one row per (time_series_id, time) half-hourly timestep. This schema is unchanged;
the effective_capacity asset body changes and the metrics pipeline swaps its
time_series_id-only NMAE-denominator join for a temporal as-of join. Do not pre-densify
the v0.1 scalar into one row per half-hour — densifying a constant buys nothing, and the as-of
join handles sparse capacity rows naturally.
Source code in packages/contracts/src/contracts/power_schemas.py
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 | |
Attributes
time_series_id = _get_time_series_id_dtype()
class-attribute
instance-attribute
time = pt.Field(dtype=UTC_DATETIME_DTYPE, description='The half-hourly timestep this capacity estimate applies to. In v0.1, this is the end of the available observation history for that series.')
class-attribute
instance-attribute
effective_capacity_mw = pt.Field(dtype=(pl.Float32), gt=0, description="OCF's estimate of the effective capacity (MW) of this asset at this timestep. For generators: absorbs PV panel degradation, partial inverter trips, etc., but ignores ANM curtailment — a wind farm ANM-capped at 5 MW with 10 MW physical capability has effective_capacity_mw = 10. For substations: the 99th percentile of observed load over a rolling time window, under normal running arrangement only. 'Switched' power (Table 5) should be added or subtracted when a switching event is in effect.")
class-attribute
instance-attribute
contracts.weather_schemas
Attributes
WeatherFeature = Literal['temperature_2m', 'dew_point_temperature_2m', 'wind_speed_10m', 'wind_direction_10m', 'wind_speed_100m', 'wind_direction_100m', 'pressure_surface', 'pressure_reduced_to_mean_sea_level', 'geopotential_height_500hpa', 'downward_long_wave_radiation_flux_surface', 'downward_short_wave_radiation_flux_surface', 'precipitation_surface', 'categorical_precipitation_type_surface']
module-attribute
NWP_MODEL_ID_DTYPE = pl.Enum([(model.name) for model in NwpModelId])
module-attribute
Classes
NwpModelId
Bases: StrEnum
Source code in packages/contracts/src/contracts/weather_schemas.py
34 35 | |
Attributes
ECMWF_ENS_0_25_degree = auto()
class-attribute
instance-attribute
NwpMetaData
Bases: Model
Metadata about numerical weather prediction models.
Source code in packages/contracts/src/contracts/weather_schemas.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
Attributes
nwp_model_id = pt.Field(dtype=NWP_MODEL_ID_DTYPE, description="Primary key for joining with NWP data (e.g. 'ECMWF_ENS_0_25_degree').", unique=True)
class-attribute
instance-attribute
provider = pt.Field(dtype=(pl.Enum(['ECMWF'])), description="NWP data provider (currently always 'ECMWF').")
class-attribute
instance-attribute
h3_resolution = pt.Field(dtype=(pl.Int8), description="H3 spatial resolution used for this NWP model's grid.")
class-attribute
instance-attribute
is_ensemble = pt.Field(description='Whether this NWP model produces ensemble forecasts.')
class-attribute
instance-attribute
Methods:
load(csv_path=None)
classmethod
Load NWP metadata from a static CSV file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
csv_path
|
str | Path | None
|
Path to the metadata CSV; defaults to |
None
|
Source code in packages/contracts/src/contracts/weather_schemas.py
62 63 64 65 66 67 68 69 70 71 72 73 74 | |
Nwp
Bases: Model
Weather data schema for NWP forecasts: gridded ECMWF ENS ensemble weather, one row per (nwp_model_id, init_time, valid_time, ensemble_member, h3_index).
Stored on disk as plain Float32, rounded to a significand-bit budget by
delta_store.nwp.write_nwp — see docs/architecture/overview.md for the physical format
and measured numbers.
validate is the fatal ingest gate; assess_nwp_quality reports the tolerated scattered nulls
(the known upstream ECMWF ENS corruption) as a non-fatal check. Which null patterns are fatal
versus tolerated, and why, is documented at
https://openclimatefix.github.io/nged-substation-forecast/architecture/ecmwf-ens-known-issues/.
Source code in packages/contracts/src/contracts/weather_schemas.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
Attributes
nwp_model_id = pt.Field(dtype=NWP_MODEL_ID_DTYPE, description="The primary key for joining with NwpMetaData (e.g. 'ECMWF_ENS_0_25_degree').")
class-attribute
instance-attribute
init_time = pt.Field(dtype=UTC_DATETIME_DTYPE, description='When the NWP model run was initialised.')
class-attribute
instance-attribute
valid_time = pt.Field(dtype=UTC_DATETIME_DTYPE, description="The time for which this NWP value is valid. Most variables (temperature, wind, pressure, geopotential) are instantaneous — they describe conditions at this moment. Precipitation and radiation (downward_long_wave_radiation_flux_surface, downward_short_wave_radiation_flux_surface, precipitation_surface) are period-ending rates: each value represents the average rate over the period that ends at valid_time (i.e. the preceding forecast step interval). Dynamical.org de-accumulates these from ECMWF's raw cumulative fields before we receive them.")
class-attribute
instance-attribute
ensemble_member = pt.Field(dtype=(pl.UInt8), description='Ensemble member index (0-based).')
class-attribute
instance-attribute
h3_index = pt.Field(dtype=(pl.UInt64), description='H3 cell index. The H3 resolution for the nwp_model_id is stored in NwpMetaData.')
class-attribute
instance-attribute
temperature_2m = pt.Field(dtype=(pl.Float32), description='Air temperature at 2 m above ground. Unit: degrees C.', ge=(-100), le=100)
class-attribute
instance-attribute
dew_point_temperature_2m = pt.Field(dtype=(pl.Float32), description='Dew-point temperature at 2 m. Unit: degrees C.', ge=(-100), le=100)
class-attribute
instance-attribute
wind_speed_10m = pt.Field(dtype=(pl.Float32), description='Wind speed at 10 m. Unit: meters per second.', ge=0, le=200)
class-attribute
instance-attribute
wind_direction_10m = pt.Field(dtype=(pl.Float32), description='Wind direction at 10 m. The angle where the wind is coming from. Degrees. 0° is North; 90° is East.', ge=0, le=360)
class-attribute
instance-attribute
wind_speed_100m = pt.Field(dtype=(pl.Float32), description='Wind speed at 100 m. Unit: meters per second.', ge=0, le=200)
class-attribute
instance-attribute
wind_direction_100m = pt.Field(dtype=(pl.Float32), description='Wind direction at 100 m. The angle where the wind is coming from. Degrees. 0° is North; 90° is East.', ge=0, le=360)
class-attribute
instance-attribute
pressure_surface = pt.Field(dtype=(pl.Float32), description='Surface pressure. Unit: Pa.', ge=0, le=200000)
class-attribute
instance-attribute
pressure_reduced_to_mean_sea_level = pt.Field(dtype=(pl.Float32), description='Mean sea-level pressure. Unit: Pa.', ge=0, le=200000)
class-attribute
instance-attribute
geopotential_height_500hpa = pt.Field(dtype=(pl.Float32), description='Geopotential height of the 500 hPa pressure surface. Unit: m.', ge=0, le=10000)
class-attribute
instance-attribute
downward_long_wave_radiation_flux_surface = pt.Field(dtype=(pl.Float32), description='Downward long-wave radiation flux at surface. Note that this variable is all-null for lead time 0. Unit: W m-2.', ge=0, le=1500)
class-attribute
instance-attribute
downward_short_wave_radiation_flux_surface = pt.Field(dtype=(pl.Float32), description='Downward short-wave (solar) radiation flux at surface. Note that this variable is all-null for lead time 0. Unit: W m-2.', ge=0, le=1500)
class-attribute
instance-attribute
precipitation_surface = pt.Field(dtype=(pl.Float32), description='Total precipitation rate at surface, de-accumulated by Dynamical. Note that this variable is all-null for lead time 0. Unit: kg m-2 s-1.', ge=0, le=0.01)
class-attribute
instance-attribute
categorical_precipitation_type_surface = pt.Field(dtype=(pl.UInt8), description="This field is always NaN for init_times on and before 2024-11-12, and populated from the 2024-11-13 00Z run onwards (confirmed by direct inspection of the source data). Derived from ECMWF's `ptype` field. See https://codes.ecmwf.int/grib/param-db/260015 0=No precipitation; 1=Rain; 2=Thunderstorm; 3=Freezing rain; 4=Mixed/ice; 5=Snow; 6=Wet snow; 7=Mixture of rain and snow; 8=Ice pellets; 9=Graupel; 10=Hail; 11=Drizzle; 12=Freezing drizzle; 13=Hail (less than 5 mm); 14=Hail (greater than or equal to 5 mm); 15-191=Reserved; 192-254=Reserved for local use; 255=Missing")
class-attribute
instance-attribute
categorical_var_names = frozenset({'categorical_precipitation_type_surface'})
class-attribute
deaccumulated_var_names = frozenset({'precipitation_surface', 'downward_short_wave_radiation_flux_surface', 'downward_long_wave_radiation_flux_surface'})
class-attribute
The variables Dynamical.org de-accumulates from ECMWF's cumulative source fields to rates.
They share a de-accumulation step whose known upstream corruption leaves scattered per-pixel
nulls beyond lead-0 (and all three are legitimately null at lead-0). Those scattered nulls are
tolerated at ingest and reported by :func:assess_nwp_quality; only a whole-slice gap is fatal.
See https://openclimatefix.github.io/nged-substation-forecast/architecture/ecmwf-ens-known-issues/.
Methods:
all_weather_var_names()
classmethod
All meteorological variable field names (continuous + categorical).
Source code in packages/contracts/src/contracts/weather_schemas.py
251 252 253 254 | |
continuous_var_names()
classmethod
Meteorological variable field names suitable for linear interpolation.
Source code in packages/contracts/src/contracts/weather_schemas.py
256 257 258 259 | |
validate(dataframe, columns=None, allow_missing_columns=False, allow_superfluous_columns=False, drop_superfluous_columns=False)
classmethod
Validate the frame: rejecting whole-slice nulls in de-accumulated variables (scattered nulls are tolerated), enforcing uniqueness, and the ptype-introduction invariant.
Source code in packages/contracts/src/contracts/weather_schemas.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | |
scan_delta(path=None, storage_options=None)
classmethod
Lazily scan the NWP Delta table, typed and cast to this contract's dtypes.
The table stores physical-unit Float32 directly (see delta_store.nwp.write_nwp),
so no rescale step is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path | None
|
Path or URI of the |
None
|
storage_options
|
ObjectStoreOptions | None
|
delta-rs object-store options (credentials/endpoint) for a remote
|
None
|
Source code in packages/contracts/src/contracts/weather_schemas.py
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
NwpQualityReport
dataclass
Non-fatal data-quality summary for one NWP run.
Carries the tolerated scattered nulls in the de-accumulated variables beyond lead-0 — the
known upstream ECMWF ENS corruption. A structural whole-slice gap is rejected by
:meth:Nwp.validate before this runs, so a validated frame only ever has scatter here.
Source code in packages/contracts/src/contracts/weather_schemas.py
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | |
Attributes
scattered
instance-attribute
One row per affected (variable, init_time, ensemble_member, valid_time) slice, with
n_null and n_total cell counts. Empty when the run is clean.
n_null_cells
property
Total scattered null cells across all affected slices.
n_affected_slices
property
Number of (variable, member, valid_time) slices carrying at least one scattered null.
is_healthy
property
True when the run has no unexpected nulls.
affected_variables
property
The de-accumulated variables carrying scattered nulls, sorted.
Methods:
__init__(scattered)
Functions:
assess_nwp_quality(dataframe)
Summarise the tolerated-but-noteworthy nulls in a validated NWP run.
Reports the scattered per-pixel nulls in the de-accumulated variables (precipitation/radiation)
beyond lead-0 — the known upstream ECMWF ENS corruption that :meth:Nwp.validate deliberately
tolerates. Pure and Dagster-free (unit-testable in isolation); the ecmwf_ens asset wraps the
result into a WARN AssetCheckResult. See
https://openclimatefix.github.io/nged-substation-forecast/architecture/ecmwf-ens-known-issues/.
Source code in packages/contracts/src/contracts/weather_schemas.py
454 455 456 457 458 459 460 461 462 463 464 465 466 | |
contracts.geo_schemas
Classes
H3GridWeights
Bases: Model
Schema for the pre-computed H3 grid weights.
This contract defines the mapping between H3 hexagons and a regular latitude/longitude grid.
It is used to ensure type safety when passing spatial mapping data from generic geospatial
utilities (like packages/geo) to dataset-specific ingestion pipelines (like packages/dynamical_data).
Source code in packages/contracts/src/contracts/geo_schemas.py
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
Attributes
h3_index = pt.Field(dtype=(pl.UInt64), description='H3 cell index.')
class-attribute
instance-attribute
nwp_lat = pt.Field(dtype=(pl.Float32), ge=(-90), le=90, description='Latitude of the NWP grid box centre (decimal degrees).')
class-attribute
instance-attribute
nwp_lon = pt.Field(dtype=(pl.Float32), ge=(-180), le=180, description='Longitude of the NWP grid box centre (decimal degrees).')
class-attribute
instance-attribute
proportion = pt.Field(dtype=(pl.Float32), ge=0, le=1, description='Fraction of the H3 hexagon that overlaps this NWP grid box (0 to 1).')
class-attribute
instance-attribute
contracts.ml_schemas
Attributes
TimeFeature = Literal['local_time_of_day_sin', 'local_time_of_day_cos', 'local_time_of_year_sin', 'local_time_of_year_cos', 'local_day_of_week_sin', 'local_day_of_week_cos', 'local_day_of_week', 'local_utc_offset']
module-attribute
SafeInputBaseColumn = Literal['time_series_id', 'time_series_type', 'nwp_lead_time_hours', 'ensemble_member', 'power_fcst_init_time', 'nwp_init_time']
module-attribute
HORIZON_SLICES = ('all', 'intraday', 'day_ahead', 'short_medium_range', 'extended_range')
module-attribute
Horizon slice labels matching the four forecast ranges from the project report.
Bands are left-closed intervals of lead time (valid_time − power_fcst_init_time), as
implemented by ml_core.metrics:
"all": aggregates over all horizons and is always computed"intraday": [0 h, 6 h)"day_ahead": [6 h, 36 h)"short_medium_range": [36 h, 168 h) — day 2 to day 7"extended_range": [168 h, ∞) — day 8 onwards (unbounded above; in practice forecasts stop at the 14-day NWP horizon)
METRIC_NAMES = ('mae', 'nmae', 'rmse', 'mbe', 'crps', 'spread_skill_ratio', 'pinball_loss', 'mean_pinball_loss', 'picp', 'interval_width')
module-attribute
Metric names currently implemented.
Full definitions, equations, and design rationale: https://openclimatefix.github.io/nged-substation-forecast/techniques/evaluation-metrics/
Deterministic (scored on the per-run ensemble mean):
"mae": mean absolute error (MW)"nmae": normalised MAE (dimensionless; normalised by full-history effective capacity)"rmse": root mean squared error (MW)"mbe": mean bias error (MW; positive = over-prediction)
Probabilistic (scored on the ensemble members before the mean collapse):
"crps": fair (finite-ensemble-unbiased) continuous ranked probability score (MW). The only metric here that is comparable across models with different ensemble sizes."spread_skill_ratio": Fortin-corrected RMS ensemble spread ÷ RMSE of the ensemble mean (dimensionless; 1.0 = well-calibrated, < 1 = underdispersed/overconfident)."pinball_loss": quantile loss (MW) at the quantile named bymetric_param."mean_pinball_loss": unweighted mean of"pinball_loss"over the thirteenDELIVERY_QUANTILES(MW). Tail-heavy by construction, matching NGED's priorities."picp": prediction-interval coverage probability of the band named bymetric_param(dimensionless fraction). The calibrated reference for empirical quantiles from a finite ensemble sits below the nominal coverage — e.g. ≈ 0.769, not 0.8, forp10_p90at 51 members."interval_width": mean width (MW) of the band named bymetric_param— the sharpness companion to"picp"(coverage is only impressive if the band is also narrow).
QUANTILE_METRIC_PARAMS = tuple((quantile_label(q)) for q in DELIVERY_QUANTILES)
module-attribute
metric_param labels for quantile-indexed metrics (pinball loss), e.g. "p1" … "p99".
Derived from DELIVERY_QUANTILES so that evaluation is always scored at exactly the quantile
levels the product delivers.
BAND_METRIC_PARAMS = tuple(f'{quantile_label(q)}_{quantile_label(1 - q)}' for q in DELIVERY_QUANTILES if q < 0.5)
module-attribute
metric_param labels for the symmetric prediction-interval bands (PICP, interval width).
One band per symmetric pair of DELIVERY_QUANTILES: "p1_p99", "p2_p98", "p5_p95",
"p10_p90", "p20_p80", "p35_p65". Together they trace a coverage curve from the 30%
band to the 98% band.
METRIC_PARAMS = ('all', *QUANTILE_METRIC_PARAMS, *BAND_METRIC_PARAMS)
module-attribute
Parameter values for parametric metrics.
"all": all scalar metrics with no extra parameter dimension (MAE, NMAE, RMSE, MBE, CRPS, spread-skill ratio, mean pinball loss).- Quantile labels (
"p1"…"p99", fromQUANTILE_METRIC_PARAMS): pinball loss. - Band labels (
"p1_p99"…"p35_p65", fromBAND_METRIC_PARAMS): PICP and interval width.
EVALUATION_SCOPES = ('leaderboard', 'production_monitoring', 'ad_hoc')
module-attribute
Evaluation scopes that coexist in the forecast_metrics table.
"leaderboard": CV-fold leaderboard metrics"production_monitoring": live trailing-window monitoring"ad_hoc": one-off analyses (no MLflow run)
EvalScopeType = Literal['leaderboard', 'ad_hoc']
module-attribute
Type annotation for the evaluation scopes currently accepted by the metrics asset config.
Distinct from EVALUATION_SCOPES (the runtime tuple driving the Polars Enum): that tuple
includes "production_monitoring" for future use; this Literal reflects only the scopes
the asset actually handles today. Expand to match EVALUATION_SCOPES when Phase 8 lands.
TIME_SERIES_TYPE_SLICES = ('all', *LIST_OF_TIME_SERIES_TYPES)
module-attribute
Values for the time_series_type metric slice.
Every time_series_type plus the sentinel "all" for the across-everything aggregate.
Classes
AllFeatures
Bases: Model
Final joined dataset ready for the ML model.
Weather features are kept in their physical units (e.g., degrees Celsius, m/s) to ensure precision during interpolation and feature engineering.
DYNAMIC FEATURES: In addition to the explicitly defined columns below, the pipeline supports dynamically generated features. You can request these in your model config:
power_lag_{hours}h: The power value shifted by X hours (e.g.,power_lag_24h).temperature_2m_rolling_mean_{hours}h: Rolling average of temperature over X hours (e.g.,temperature_2m_rolling_mean_6h).
Note: Dynamic features are not explicitly typed as Patito fields below. This is intentional to allow infinite parameterization (e.g., any lag hour) without the overhead of metaprogramming or defining hundreds of static fields. The pipeline dynamically asserts their presence during feature engineering.
Source code in packages/contracts/src/contracts/ml_schemas.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
Attributes
valid_time = pt.Field(dtype=UTC_DATETIME_DTYPE)
class-attribute
instance-attribute
time_series_id = _get_time_series_id_dtype()
class-attribute
instance-attribute
time_series_type = pt.Field(dtype=(pl.Enum(LIST_OF_TIME_SERIES_TYPES)))
class-attribute
instance-attribute
ensemble_member = pt.Field(dtype=(pl.UInt8), allow_missing=True)
class-attribute
instance-attribute
power_fcst_init_time = pt.Field(dtype=UTC_DATETIME_DTYPE, description="When OCF's power forecast model was initialised. This might be called `t0` in other OCF projects.")
class-attribute
instance-attribute
nwp_init_time = pt.Field(dtype=UTC_DATETIME_DTYPE, allow_missing=True, description='When the NWP run was initialised.')
class-attribute
instance-attribute
power = pt.Field(dtype=(pl.Float32))
class-attribute
instance-attribute
nwp_lead_time_hours = pt.Field(dtype=(pl.Float32), allow_missing=True)
class-attribute
instance-attribute
temperature_2m = _FEATURE_DTYPE
class-attribute
instance-attribute
dew_point_temperature_2m = _FEATURE_DTYPE
class-attribute
instance-attribute
wind_speed_10m = _FEATURE_DTYPE
class-attribute
instance-attribute
wind_direction_10m = _FEATURE_DTYPE
class-attribute
instance-attribute
wind_speed_100m = _FEATURE_DTYPE
class-attribute
instance-attribute
wind_direction_100m = _FEATURE_DTYPE
class-attribute
instance-attribute
pressure_surface = _FEATURE_DTYPE
class-attribute
instance-attribute
pressure_reduced_to_mean_sea_level = _FEATURE_DTYPE
class-attribute
instance-attribute
geopotential_height_500hpa = _FEATURE_DTYPE
class-attribute
instance-attribute
downward_long_wave_radiation_flux_surface = _FEATURE_DTYPE
class-attribute
instance-attribute
downward_short_wave_radiation_flux_surface = _FEATURE_DTYPE
class-attribute
instance-attribute
precipitation_surface = _FEATURE_DTYPE
class-attribute
instance-attribute
categorical_precipitation_type_surface = _FEATURE_DTYPE
class-attribute
instance-attribute
windchill = _FEATURE_DTYPE
class-attribute
instance-attribute
local_utc_offset = pt.Field(dtype=(pl.Int8), allow_missing=True)
class-attribute
instance-attribute
local_time_of_day_sin = _FEATURE_DTYPE
class-attribute
instance-attribute
local_time_of_day_cos = _FEATURE_DTYPE
class-attribute
instance-attribute
local_time_of_year_sin = _FEATURE_DTYPE
class-attribute
instance-attribute
local_time_of_year_cos = _FEATURE_DTYPE
class-attribute
instance-attribute
local_day_of_week_sin = _FEATURE_DTYPE
class-attribute
instance-attribute
local_day_of_week_cos = _FEATURE_DTYPE
class-attribute
instance-attribute
local_day_of_week = pt.Field(dtype=(pl.Enum(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'])))
class-attribute
instance-attribute
Methods:
validate(dataframe, columns=None, allow_missing_columns=False, allow_superfluous_columns=False, drop_superfluous_columns=False)
classmethod
Validate the given dataframe, ensuring uniqueness of the primary key.
This custom validation step is crucial for preventing weather forecast leakage and ensuring data integrity. We enforce uniqueness on the combination of:
time_series_id: Identifies the specific substation/time series.power_fcst_init_time: The initialization time of the power forecast.valid_time: The target time for which the forecast is made.ensemble_member: The weather forecast ensemble member (if present).
If ensemble_member is present in the dataframe columns, we include it in the
primary key check to support ensemble forecasts. If it is not present (e.g.,
for deterministic forecasts or when ensemble members have been aggregated),
we only check uniqueness across the remaining three columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataframe
|
DataFrame
|
The Polars DataFrame to validate. |
required |
columns
|
Sequence[str] | None
|
Optional list of columns to validate against. |
None
|
allow_missing_columns
|
bool
|
Whether to allow missing columns. |
False
|
allow_superfluous_columns
|
bool
|
Whether to allow extra columns. |
False
|
drop_superfluous_columns
|
bool
|
Whether to drop extra columns. |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame[Self]
|
A validated Patito DataFrame. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If duplicate entries are found for the primary key. |
Source code in packages/contracts/src/contracts/ml_schemas.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
Metrics
Bases: Model
Evaluation metrics for power forecasts — tall format.
One row per (time_series_id, power_fcst_model_name, fold_id, horizon_slice, metric_name,
metric_param). metric_param encodes the extra parameter dimension for metrics that have
one, or "all" for scalar metrics with no extra dimension. Examples:
| time_series_id | fold_id | horizon_slice | metric_name | metric_param | metric_value |
|---|---|---|---|---|---|
| 1 | 1 | all | mae | all | 5.2 |
| 1 | 1 | day_ahead | rmse | all | 7.1 |
| 1 | 1 | day_ahead | pinball_loss | p10 | 2.1 |
| 1 | 1 | day_ahead | pinball_loss | p50 | 3.4 |
| 1 | 1 | day_ahead | mean_pinball_loss | all | 2.4 |
| 1 | 1 | day_ahead | picp | p10_p90 | 0.78 |
Primary key: (time_series_id, power_fcst_model_name, fold_id, horizon_slice, metric_name,
metric_param).
Source code in packages/contracts/src/contracts/ml_schemas.py
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | |
Attributes
time_series_id = _get_time_series_id_dtype()
class-attribute
instance-attribute
power_fcst_model_name = pt.Field(dtype=(pl.String), description='Identifier for the ML-based power forecasting model family.')
class-attribute
instance-attribute
fold_id = pt.Field(dtype=(pl.String), description="CV fold year (e.g. '2022'), or 'live' for production forecasts. Matches PowerForecast.fold_id.")
class-attribute
instance-attribute
horizon_slice = pt.Field(dtype=(pl.Enum(HORIZON_SLICES)), description="'all' aggregates over all forecast horizons. Other values select the HORIZON_SLICES bands.")
class-attribute
instance-attribute
metric_name = pt.Field(dtype=(pl.Enum(METRIC_NAMES)), description="The name of the metric (e.g. 'mae', 'rmse').")
class-attribute
instance-attribute
metric_param = pt.Field(dtype=(pl.Enum(METRIC_PARAMS)), description="Extra parameter dimension for parametric metrics. 'all' for scalar metrics (MAE, NMAE, RMSE, MBE, CRPS, spread-skill ratio, mean pinball loss). For pinball loss: a DELIVERY_QUANTILES label ('p1' … 'p99'). For PICP and interval width: a symmetric band label ('p1_p99' … 'p35_p65').")
class-attribute
instance-attribute
metric_value = pt.Field(dtype=(pl.Float32), description='The computed metric value.')
class-attribute
instance-attribute
evaluation_scope = pt.Field(dtype=(pl.Enum(EVALUATION_SCOPES)), allow_missing=True, description='Which evaluation produced this row, so leaderboard, live-monitoring, and one-off metrics coexist in one table and stay separable.')
class-attribute
instance-attribute
The columns from evaluation_scope and below are populated by the metrics Dagster asset.
They are allow_missing so that the pure compute_metrics() helper can emit the core
metric rows and have the asset enrich them with scope/window provenance before the frame is
written to the forecast_metrics Delta table.
time_series_type = pt.Field(dtype=(pl.Enum(TIME_SERIES_TYPE_SLICES)), allow_missing=True, description='The time-series category for this row. Null when metadata is unavailable for a series.')
class-attribute
instance-attribute
window_start = pt.Field(dtype=UTC_DATETIME_DTYPE, allow_missing=True, description="Inclusive start of the valid_time window this row covers. For a CV fold this is the fold's val_start; for monitoring it is the trailing-window start.")
class-attribute
instance-attribute
window_end = pt.Field(dtype=UTC_DATETIME_DTYPE, allow_missing=True, description='End of the valid_time window this row covers (fold val_end or window end).')
class-attribute
instance-attribute
window_label = pt.Field(dtype=(pl.String), allow_missing=True, description="Human label for the window, e.g. '2025', '24h', '7d', 'full_fold'.")
class-attribute
instance-attribute
computed_at = pt.Field(dtype=UTC_DATETIME_DTYPE, allow_missing=True, description='When this metric row was written (provenance; orders the append-only monitoring series and distinguishes recomputations).')
class-attribute
instance-attribute
mlflow_run_id = pt.Field(dtype=(pl.String), allow_missing=True, description="Convenience cross-link to the MLflow run this metric row belongs to. Null for 'ad_hoc' rows, which have no MLflow run.")
class-attribute
instance-attribute
experiment_name = pt.Field(dtype=(pl.String), allow_missing=True, description='Experiment that produced these forecasts. Delta partition key alongside fold_id. Populated by the metrics Dagster asset after compute_metrics() returns; allow_missing so compute_metrics() itself need not produce it.')
class-attribute
instance-attribute
EligibleTimeSeries
Bases: Model
The canonical per-fold population of eligible time_series_ids.
Written by the eligible_time_series Dagster asset (one Delta partition per fold_id)
and read by trained_cv_model and cv_power_forecasts. Eligibility is a function of
data coverage and the fold dates only — never the model or experiment config — so every
experiment trains and scores a fold on the identical population, which is what makes
leaderboard comparisons apples-to-apples.
One row per eligible (fold_id, time_series_id).
Source code in packages/contracts/src/contracts/ml_schemas.py
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 | |
Attributes
fold_id = pt.Field(dtype=(pl.String), description="The CV fold this eligibility row belongs to (e.g. 'mid_2025_to_mid_2026'); the Delta partition key.")
class-attribute
instance-attribute
time_series_id = _get_time_series_id_dtype()
class-attribute
instance-attribute
Functions:
contracts.hydra_schemas
Hydra configuration schemas for the NGED substation forecast project.
Attributes
Classes
CvFoldConfig
Bases: BaseModel
Configuration for a single expanding-window CV fold.
leaderboard distinguishes the epoch-pinned leaderboard folds (the apples-to-apples
evaluation protocol) from optional non-leaderboard dev folds such as smoke_test: a
leaderboard=False fold runs through the identical pipeline but never feeds the leaderboard.
min_training_months overrides CvConfig.min_training_months for this fold alone (None
falls back to the config-level value). A short dev fold sets it to its train length so
eligibility does not demand the leaderboard's longer history.
Source code in packages/contracts/src/contracts/hydra_schemas.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
Attributes
fold_id
instance-attribute
train_start
instance-attribute
train_end
instance-attribute
val_start
instance-attribute
val_end
instance-attribute
leaderboard = True
class-attribute
instance-attribute
min_training_months = Field(default=None, ge=1)
class-attribute
instance-attribute
CvConfig
Bases: BaseModel
Configuration for expanding-window cross-validation.
The folds list defines the evaluation protocol shared by all experiments on the leaderboard. All models must be evaluated against the same folds to ensure apples-to-apples comparison.
min_training_months controls which time series are eligible for each fold: a time series is only included if it has at least this many months of data before val_start (and data through val_end).
Source code in packages/contracts/src/contracts/hydra_schemas.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
Attributes
folds
instance-attribute
min_training_months = Field(default=6, ge=1)
class-attribute
instance-attribute
fold_ids
property
The fold ids in declaration order (e.g. ["2022", "2023", ...]).
Used to build the cv_experiment_folds partitions and to expand experiment
registration into per-fold partition keys — always read from config, never hard-coded.
leaderboard_fold_ids
property
The fold ids of the leaderboard folds only, in declaration order.
Non-leaderboard dev folds (e.g. smoke_test) are excluded. Used to expand the
full_cv / register_only run modes and to scope leaderboard metrics.
Methods:
get_fold(fold_id)
Return the fold with the given fold_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fold_id
|
str
|
The fold identifier to look up (e.g. |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no fold with that id exists in the config. |
Source code in packages/contracts/src/contracts/hydra_schemas.py
66 67 68 69 70 71 72 73 74 75 76 77 78 | |
Functions:
load_cv_config(path)
Load and validate the cross-validation config from a YAML file.
The CV folds are the leaderboard's evaluation protocol and must be read from
conf/cv/default.yaml (never hard-coded) so every experiment and asset shares one
canonical definition.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
Path to the CV config YAML (e.g. |
required |
Returns:
| Type | Description |
|---|---|
CvConfig
|
The validated |
Source code in packages/contracts/src/contracts/hydra_schemas.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
contracts.settings
Attributes
url_adapter = TypeAdapter(AnyHttpUrl)
module-attribute
PROJECT_ROOT = _find_project_root(Path(__file__))
module-attribute
The repo root — anchor for the repo-relative defaults below (conf/, metadata/,
data/, .env).
Resolved by :func:_find_project_root; see its docstring for the per-install-mode behaviour
and the caveat for wheels installed outside a workspace checkout.
Classes
DataQualitySettings
Bases: BaseSettings
Settings for data quality thresholds in substation flow processing.
These thresholds are used to identify problematic telemetry data:
-
stuck_std_threshold: When the rolling standard deviation falls below this value (acrossstuck_window_periods), the sensor is likely stuck. We replace such values with null to preserve the temporal grid. A value of 0.01 MW was chosen because substations with normal operation typically have much higher variability. -
max_mw_threshold: Active power above this value is considered physically unrealistic for primary substations in the NGED portfolio. A threshold of 150 MW was chosen because typical primary substations operate in the tens of MW range, and values exceeding 100 MW are extremely rare anomalies. -
min_mw_threshold: Active power below this value is potentially erroneous (negative values can occur at times of high renewable generation). A threshold of -50.0 MW was chosen to allow for reverse power flow during high renewable generation periods while still catching implausible extreme negative values.
Centralizing these in Settings allows them to be configurable per environment (dev/staging/prod) while preventing logic drift between asset checks and data cleaning steps. All code that references these thresholds should import them from here, not define them locally.
Source code in packages/contracts/src/contracts/settings.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
Attributes
stuck_std_threshold = 0.01
class-attribute
instance-attribute
stuck_window_periods = 48
class-attribute
instance-attribute
max_mw_threshold = 150.0
class-attribute
instance-attribute
min_mw_threshold = -50.0
class-attribute
instance-attribute
Settings
Bases: BaseSettings
Configuration settings for the NGED substation forecast project.
Source code in packages/contracts/src/contracts/settings.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
Attributes
mlflow_tracking_uri = Field(default='sqlite:///mlflow.db', description='MLflow tracking URI. Centralized here for environment-specific configuration (e.g., local SQLite for development, remote server for production). The application entrypoint should read this setting and set the MLFLOW_TRACKING_URI environment variable accordingly, which MLflow will automatically pick up.')
class-attribute
instance-attribute
data_quality = Field(default_factory=DataQualitySettings, description='Configurable thresholds for data quality checks.')
class-attribute
instance-attribute
nged_s3_bucket_url = Field(..., description='NGED S3 bucket URL. Typically stored in the .env file.')
class-attribute
instance-attribute
nged_s3_bucket_access_key = Field(..., description='Access key for the NGED S3 bucket. Typically stored in the .env file.')
class-attribute
instance-attribute
nged_s3_bucket_secret = Field(..., description='Secret key for the NGED S3 bucket. Typically stored in the .env file.')
class-attribute
instance-attribute
cv_config_path = Field(default=(PROJECT_ROOT / 'conf' / 'cv' / 'default.yaml'), description='Path to the canonical cross-validation fold definitions. These folds are the shared leaderboard evaluation protocol; read them from here, never hard-coded.')
class-attribute
instance-attribute
nwp_metadata_csv_path = Field(default=(PROJECT_ROOT / 'metadata' / 'nwp_metadata.csv'), description='Static CSV of per-NWP-model metadata (H3 resolution, provider, ensemble flag), checked into the repo and read by NwpMetaData.load. A code-relative resource (like cv_config_path), so it stays a local Path even when the data tables are a remote URI.')
class-attribute
instance-attribute
data_path_internal = Field(default=(str(PROJECT_ROOT / 'data')), description="Root of OCF's own working data tables (NWP, power, forecast_metrics, …). A local path by default; may be a remote URI (e.g. 's3://bucket/data') so the data tables live on S3. Every *_data_path below that is left unset derives from this root, except the NGED-facing delivery tables, which derive from data_path_delivery instead.")
class-attribute
instance-attribute
data_path_delivery = Field(default=(str(PROJECT_ROOT / 'data')), description='Root of the NGED-facing delivery tables (power_forecasts, effective_capacity, …). Defaults to the same local path as data_path_internal, so local dev sees one directory and the split is invisible; on AWS this points at the separate, NGED-facing bucket.')
class-attribute
instance-attribute
local_artifacts_path = Field(default=(str(PROJECT_ROOT / 'data')), description='Root of the always-local artifacts (model cache, production model). Kept separate from the data-table roots because these back local-filesystem-only libraries and must stay local even when the data tables live on S3.')
class-attribute
instance-attribute
data_store_endpoint_url = Field(default='', description='S3-compatible endpoint URL for the data tables (e.g. a MinIO/dev endpoint). Empty on AWS, where the endpoint is inferred. When set, the store is also allowed to use plain HTTP (dev endpoints rarely have TLS).')
class-attribute
instance-attribute
data_store_access_key_id = Field(default='', description='Access key for the data-table object store; empty on AWS (IAM).')
class-attribute
instance-attribute
data_store_secret_access_key = Field(default='', description='Secret key for the data-table object store; empty on AWS (IAM).')
class-attribute
instance-attribute
data_store_region = Field(default='', description='Region for the data-table object store; empty to auto-discover.')
class-attribute
instance-attribute
storage_options
property
delta-rs / polars / obstore storage_options for the managed data tables.
Empty on AWS — object_store auto-discovers the Fargate task's IAM-role credentials and
region — and empty for a local data-path root (delta-rs ignores it there). Populated from
the data_store_* settings only for a dev/MinIO/S3-compatible endpoint. The aws_*
keys are the shared object_store aliases understood by delta-rs, polars cloud IO, and
obstore alike, so one value feeds every IO site. Returned as an ObjectStoreOptions
TypedDict so ty checks each key here, where they are authored; widen it to a plain
dict at each IO boundary with typeddict_to_dict.
nged_data_path = ''
class-attribute
instance-attribute
Directory holding the NGED power_time_series Delta table and metadata parquet.
nwp_data_path = ''
class-attribute
instance-attribute
Delta table of NWP weather data.
power_forecasts_data_path = ''
class-attribute
instance-attribute
Delta table of power forecasts (partitioned by experiment_name, fold_id).
An NGED-facing delivery table — derives from data_path_delivery, not data_path_internal.
forecast_metrics_data_path = ''
class-attribute
instance-attribute
Delta table of forecast evaluation metrics.
power_time_series_data_path = ''
class-attribute
instance-attribute
Delta table of half-hourly power observations (under nged_data_path).
metadata_path = ''
class-attribute
instance-attribute
Parquet file of per-series substation metadata (under nged_data_path).
eligible_time_series_data_path = Field(default='', description='Delta table of the canonical per-fold eligible time_series_id population, written by the eligible_time_series asset (partitioned by fold_id) and read by trained_cv_model and cv_power_forecasts so every experiment scores a fold on the identical, experiment-independent population.')
class-attribute
instance-attribute
effective_capacity_data_path = Field(default='', description='Delta table of per-series effective capacity (v0.1: full-history P99 of |power|), written by the effective_capacity asset and read by the metrics asset as the NMAE denominator. An NGED-facing delivery table — derives from data_path_delivery, not data_path_internal.')
class-attribute
instance-attribute
h3_grid_weights_path = ''
class-attribute
instance-attribute
Parquet file of fractional H3 cell overlap with the GB boundary.
model_cache_base_path = Field(default='', description='Root of the local-disk model cache, keyed by MLflow run ID, used by BaseForecaster.load_from_mlflow. Currently only the CV pipeline (cv_power_forecasts) reads a model back through this cache; production inference does not use it for v0.1 (the model is baked directly into the container image instead).')
class-attribute
instance-attribute
production_model_path = Field(default='', description="Directory holding the current production model, written by the promoted_model asset (ml_core._production_helpers.fetch_model_artifacts) and read by live_forecasts via a plain BaseForecaster disk load — no MLflow at inference time. Later COPY'd into the container image at build time (issue #222).")
class-attribute
instance-attribute
sentry_dsn = Field(default='', description='Sentry.io project DSN. Empty (the default) disables Sentry entirely — every Sentry code path becomes a no-op — so laptops and CI need no Sentry config. Set it in the .env file to enable error telemetry. Passed explicitly to sentry_sdk.init as the single source of truth (the SDK would otherwise also auto-read the SENTRY_DSN env var).')
class-attribute
instance-attribute
sentry_environment = Field(default='local', description="Sentry environment tag, the dimension that separates deployments in Sentry's UI, alerts, and cron monitors. The always-on production box sets 'production'; each developer overrides the 'local' fallback with '<name>-laptop' (e.g. 'jacks-laptop') so laptop telemetry is cleanly filterable and the production-scoped missed-check-in alert never fires for a laptop.")
class-attribute
instance-attribute
sentry_traces_sample_rate = Field(default=0.0, description='Fraction of transactions sampled for Sentry performance tracing (off by default).')
class-attribute
instance-attribute
sentry_monitor_forecasts = Field(default=False, description="Emit the live_forecasts success heartbeat to Sentry's cron monitor (the missed-check-in alarm). True only on the always-on production deployment; left False on laptops so an intermittently-run laptop never registers a monitor environment that Sentry would then mark as missed.")
class-attribute
instance-attribute
model_config = SettingsConfigDict(env_file=(PROJECT_ROOT / '.env'), extra='forbid', env_file_encoding='utf-8', env_prefix='')
class-attribute
instance-attribute
Methods:
get_nged_s3_store()
Returns an initialized obstore.store.S3Store instance for the NGED bucket.
Source code in packages/contracts/src/contracts/settings.py
112 113 114 115 116 117 118 119 120 | |
validate_url(v)
classmethod
Validate that the S3 bucket URL is a valid URL.
Source code in packages/contracts/src/contracts/settings.py
372 373 374 375 376 377 | |
Functions:
get_settings()
cached
Return the shared, lazily-constructed Settings singleton.
Prefer this over constructing Settings() at module import time. Settings has required
fields (the nged_s3_bucket_* credentials) with no defaults, so instantiation reads .env
and raises ValidationError when those are absent. Deferring construction to first use
keeps library modules (e.g. the contracts schemas) importable without live credentials —
so type-checkers, doc builders, and tests that only touch in-memory frames don't need a
populated .env — while still failing fast the moment settings are actually needed.
Cached with lru_cache so every caller shares one instance (matching the previous
module-level singletons). Call get_settings.cache_clear() in a test that needs to
re-read the environment after changing it.
Source code in packages/contracts/src/contracts/settings.py
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | |