Skip to content

odys.domain.validation

System-level validation rules. They run automatically when an EnergySystem is created, and check that every profile references an entity of the system, every load and market has its required profile in every scenario, every profile fits the horizon, and supply can meet demand. The rules treat entities alike through their capability queries (max_supply, min_demand, max_energy_supply, validate_horizon), so a new asset type only needs to override them and be listed as supported. Rules about a single entity or profile run when that object is built.

Energy system input validation.

This module holds the rules that span entities and scenarios. Rules about a single entity or profile live on that object and run when it is built. The rules here treat every entity alike through its capability queries (max_supply, min_demand, max_energy_supply, validate_horizon) and every profile through its type's entity_types and required flags and its number_of_steps and operating_values. A new asset type is covered by overriding those queries (that the optimizer models it is checked against its formulations, in odys.optimization.formulations); a new profile type by adding it to AnyProfile and PROFILE_TYPES in profiles.py. Conditions are checked through their own queries (validate_horizon, validate_operating_conditions) and required per asset through each initial condition type's required_for; a new initial condition type is added to INITIAL_CONDITION_TYPES in conditions.py.

validate_conditions_fit_the_run(condition_set, scenarios, horizon)

Validate every condition against the horizon and what its asset faces in each scenario.

Conditions are reached through their validate_horizon and validate_operating_conditions queries, like entities through theirs.

Parameters:

Name Type Description Default
condition_set ConditionSet

The initial and final conditions of the run.

required
scenarios Sequence[Scenario]

The scenarios of the problem, whose profiles have one value per timestep.

required
horizon Horizon

The time grid of the optimization.

required

Raises:

Type Description
OdysValidationError

If a condition does not fit the horizon or cannot be met in a scenario.

Source code in src/odys/domain/validation.py
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
def validate_conditions_fit_the_run(
    condition_set: ConditionSet,
    scenarios: Sequence[Scenario],
    horizon: Horizon,
) -> None:
    """Validate every condition against the horizon and what its asset faces in each scenario.

    Conditions are reached through their `validate_horizon` and
    `validate_operating_conditions` queries, like entities through theirs.

    Args:
        condition_set: The initial and final conditions of the run.
        scenarios: The scenarios of the problem, whose profiles have one value per timestep.
        horizon: The time grid of the optimization.

    Raises:
        OdysValidationError: If a condition does not fit the horizon or cannot be met in a scenario.

    """
    for condition in condition_set.conditions:
        condition.validate_horizon(horizon)
        for scenario in scenarios:
            try:
                condition.validate_operating_conditions(_operating_conditions(scenario, condition.entity, horizon))
            except OdysValidationError as err:
                msg = f"Scenario '{scenario.name}': {err}"
                raise OdysValidationError(msg) from err

validate_conditions_reference_system_assets(condition_set, portfolio)

Validate that each condition references an asset of the energy system.

A condition whose asset is missing from the portfolio, or differs from the portfolio's asset of that name, would describe an asset that is not being optimized.

Parameters:

Name Type Description Default
condition_set ConditionSet

The initial and final conditions of the run.

required
portfolio AssetPortfolio

The asset portfolio.

required

Raises:

Type Description
OdysValidationError

If a condition's asset is not the portfolio's asset of that name.

Source code in src/odys/domain/validation.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
def validate_conditions_reference_system_assets(condition_set: ConditionSet, portfolio: AssetPortfolio) -> None:
    """Validate that each condition references an asset of the energy system.

    A condition whose asset is missing from the portfolio, or differs from the
    portfolio's asset of that name, would describe an asset that is not being optimized.

    Args:
        condition_set: The initial and final conditions of the run.
        portfolio: The asset portfolio.

    Raises:
        OdysValidationError: If a condition's asset is not the portfolio's asset of that name.

    """
    for condition in condition_set.conditions:
        system_asset = portfolio.assets.get(condition.entity.name)
        if system_asset is None:
            msg = (
                f"The {type(condition).__name__} for '{condition.entity.name}' references an asset "
                "that is not in the energy system."
            )
            raise OdysValidationError(msg)
        if system_asset != condition.entity:
            msg = (
                f"The {type(condition).__name__} for '{condition.entity.name}' does not reference "
                "the asset of that name in the energy system."
            )
            raise OdysValidationError(msg)

validate_energy_system_inputs(portfolio, scenario_set, markets, horizon, condition_set)

Run all cross-domain validation checks on the energy system.

Parameters:

Name Type Description Default
portfolio AssetPortfolio

The asset portfolio to validate against.

required
scenario_set ScenarioSet

The scenarios of the problem.

required
markets Sequence[EnergyMarket]

Normalized sequence of energy markets.

required
horizon Horizon

The time grid of the optimization.

required
condition_set ConditionSet

The initial and final conditions of the run.

required

Raises:

Type Description
OdysValidationError

If any validation check fails.

Source code in src/odys/domain/validation.py
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
def validate_energy_system_inputs(
    portfolio: AssetPortfolio,
    scenario_set: ScenarioSet,
    markets: Sequence[EnergyMarket],
    horizon: Horizon,
    condition_set: ConditionSet,
) -> None:
    """Run all cross-domain validation checks on the energy system.

    Args:
        portfolio: The asset portfolio to validate against.
        scenario_set: The scenarios of the problem.
        markets: Normalized sequence of energy markets.
        horizon: The time grid of the optimization.
        condition_set: The initial and final conditions of the run.

    Raises:
        OdysValidationError: If any validation check fails.

    """
    entities: tuple[EnergyEntity, ...] = (*portfolio.assets.values(), *markets)
    for entity in entities:
        entity.validate_horizon(horizon)

    validate_conditions_reference_system_assets(condition_set, portfolio)
    validate_required_initial_conditions_present(condition_set, portfolio)

    scenarios = scenario_set.scenarios
    validate_profiles_reference_system_entities(scenarios, portfolio, markets)
    validate_required_profiles_present(scenarios, entities)
    validate_profile_lengths(scenarios, horizon)
    validate_conditions_fit_the_run(condition_set, scenarios, horizon)

    for scenario in scenarios:
        validate_has_load_or_market(scenario, markets)
        validate_enough_power_to_meet_demand(scenario, entities, horizon)
        if not markets:
            validate_enough_energy_to_meet_demand(scenario, entities, horizon)

validate_enough_energy_to_meet_demand(scenario, entities, horizon)

Validate that the energy available over the horizon can meet the minimum energy demand.

A coarser, horizon-level counterpart to validate_enough_power_to_meet_demand: a system can pass every per-timestep power check and still be infeasible over the horizon, for example when a battery is too small for a long horizon. Every entity's max_energy_supply is an optimistic bound (storage is emptied once, generators run at their available capacity throughout), so this check can miss infeasibility but does not raise false positives.

Parameters:

Name Type Description Default
scenario Scenario

Scenario to check.

required
entities Sequence[EnergyEntity]

All assets and markets of the energy system.

required
horizon Horizon

The time grid of the optimization.

required

Raises:

Type Description
OdysValidationError

If total minimum energy demand exceeds total available energy.

Source code in src/odys/domain/validation.py
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
def validate_enough_energy_to_meet_demand(
    scenario: Scenario,
    entities: Sequence[EnergyEntity],
    horizon: Horizon,
) -> None:
    """Validate that the energy available over the horizon can meet the minimum energy demand.

    A coarser, horizon-level counterpart to `validate_enough_power_to_meet_demand`:
    a system can pass every per-timestep power check and still be infeasible
    over the horizon, for example when a battery is too small for a long
    horizon. Every entity's `max_energy_supply` is an optimistic bound
    (storage is emptied once, generators run at their available capacity
    throughout), so this check can miss infeasibility but does not raise
    false positives.

    Args:
        scenario: Scenario to check.
        entities: All assets and markets of the energy system.
        horizon: The time grid of the optimization.

    Raises:
        OdysValidationError: If total minimum energy demand exceeds total available energy.

    """
    conditions = [(entity, _operating_conditions(scenario, entity, horizon)) for entity in entities]
    total_energy_demand = sum(sum(entity.min_demand(c)) for entity, c in conditions) * horizon.hours_per_step
    total_energy_supply = sum(entity.max_energy_supply(c) for entity, c in conditions)
    if total_energy_demand > total_energy_supply:
        msg = (
            f"Infeasible problem in scenario '{scenario.name}': total energy demand "
            f"({total_energy_demand}) over the horizon exceeds total available energy "
            f"({total_energy_supply})."
        )
        raise OdysValidationError(msg)

validate_enough_power_to_meet_demand(scenario, entities, horizon)

Validate that the maximum supply can meet the minimum demand at every timestep.

Sums, per timestep, every entity's max_supply (generation, storage and EV discharge, market volume) and every entity's min_demand (fixed loads, flexible loads after their maximum decrease).

Parameters:

Name Type Description Default
scenario Scenario

Scenario to check.

required
entities Sequence[EnergyEntity]

All assets and markets of the energy system.

required
horizon Horizon

The time grid of the optimization.

required

Raises:

Type Description
OdysValidationError

If the minimum demand exceeds the maximum supply at any timestep.

Source code in src/odys/domain/validation.py
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
def validate_enough_power_to_meet_demand(
    scenario: Scenario,
    entities: Sequence[EnergyEntity],
    horizon: Horizon,
) -> None:
    """Validate that the maximum supply can meet the minimum demand at every timestep.

    Sums, per timestep, every entity's `max_supply` (generation, storage and
    EV discharge, market volume) and every entity's `min_demand` (fixed loads,
    flexible loads after their maximum decrease).

    Args:
        scenario: Scenario to check.
        entities: All assets and markets of the energy system.
        horizon: The time grid of the optimization.

    Raises:
        OdysValidationError: If the minimum demand exceeds the maximum supply at any timestep.

    """
    conditions = [(entity, _operating_conditions(scenario, entity, horizon)) for entity in entities]
    max_supply = _sum_per_timestep([entity.max_supply(c) for entity, c in conditions], horizon)
    min_demand = _sum_per_timestep([entity.min_demand(c) for entity, c in conditions], horizon)
    for t, (supply_t, demand_t) in enumerate(zip(max_supply, min_demand, strict=True)):
        if demand_t > supply_t:
            msg = (
                f"Infeasible problem in scenario '{scenario.name}' at time index {t}: "
                f"minimum demand = {demand_t}, but maximum supply "
                f"(generation + storage and EV discharge + market volume) = {supply_t}."
            )
            raise OdysValidationError(msg)

validate_has_load_or_market(scenario, markets)

Validate that the scenario has a load to serve or a market to trade in.

Parameters:

Name Type Description Default
scenario Scenario

Scenario to check.

required
markets Sequence[EnergyMarket]

The energy markets.

required

Raises:

Type Description
OdysValidationError

If the scenario has no load profile and there is no market.

Source code in src/odys/domain/validation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def validate_has_load_or_market(scenario: Scenario, markets: Sequence[EnergyMarket]) -> None:
    """Validate that the scenario has a load to serve or a market to trade in.

    Args:
        scenario: Scenario to check.
        markets: The energy markets.

    Raises:
        OdysValidationError: If the scenario has no load profile and there is no market.

    """
    if not scenario.profiles_of(LoadProfile) and not scenario.profiles_of(FlexibleLoadProfile) and not markets:
        msg = "Load profile is empty, there is nothing to balance."
        raise OdysValidationError(msg)

validate_profile_lengths(scenarios, horizon)

Validate that every profile has one value per timestep.

Parameters:

Name Type Description Default
scenarios Sequence[Scenario]

Scenarios whose profiles to check.

required
horizon Horizon

The time grid of the optimization.

required

Raises:

Type Description
OdysValidationError

If a profile's length differs from the number of timesteps.

Source code in src/odys/domain/validation.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def validate_profile_lengths(scenarios: Sequence[Scenario], horizon: Horizon) -> None:
    """Validate that every profile has one value per timestep.

    Args:
        scenarios: Scenarios whose profiles to check.
        horizon: The time grid of the optimization.

    Raises:
        OdysValidationError: If a profile's length differs from the number of timesteps.

    """
    for scenario in scenarios:
        for profile in scenario.profiles:
            if profile.number_of_steps != horizon.number_of_steps:
                msg = (
                    f"Scenario '{scenario.name}': the {type(profile).__name__} for '{profile.entity.name}' "
                    f"has {profile.number_of_steps} values, but the horizon has {horizon.number_of_steps} steps."
                )
                raise OdysValidationError(msg)

validate_profiles_reference_system_entities(scenarios, portfolio, markets)

Validate that each profile references an entity of the energy system.

Assets and markets are looked up separately, since a market may share a name with an asset. A profile whose entity is missing from the system, or differs from the system's entity of that name (for example a copy with other parameters), would describe an entity that is not being optimized.

Parameters:

Name Type Description Default
scenarios Sequence[Scenario]

Scenarios whose profiles to check.

required
portfolio AssetPortfolio

The asset portfolio.

required
markets Sequence[EnergyMarket]

The energy markets.

required

Raises:

Type Description
OdysValidationError

If a profile's entity is not the system's entity of that name.

Source code in src/odys/domain/validation.py
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
def validate_profiles_reference_system_entities(
    scenarios: Sequence[Scenario],
    portfolio: AssetPortfolio,
    markets: Sequence[EnergyMarket],
) -> None:
    """Validate that each profile references an entity of the energy system.

    Assets and markets are looked up separately, since a market may share a
    name with an asset. A profile whose entity is missing from the system, or
    differs from the system's entity of that name (for example a copy with
    other parameters), would describe an entity that is not being optimized.

    Args:
        scenarios: Scenarios whose profiles to check.
        portfolio: The asset portfolio.
        markets: The energy markets.

    Raises:
        OdysValidationError: If a profile's entity is not the system's entity of that name.

    """
    markets_by_name: Mapping[str, EnergyEntity] = {market.name: market for market in markets}
    for scenario in scenarios:
        for profile in scenario.profiles:
            system_entities = portfolio.assets if isinstance(profile.entity, Asset) else markets_by_name
            _validate_profile_references(scenario, profile, system_entities.get(profile.entity.name))

validate_required_initial_conditions_present(condition_set, portfolio)

Validate that every asset whose results depend on its starting state has an initial condition.

Parameters:

Name Type Description Default
condition_set ConditionSet

The initial and final conditions of the run.

required
portfolio AssetPortfolio

The asset portfolio.

required

Raises:

Type Description
OdysValidationError

If an asset lacks an initial condition it requires.

Source code in src/odys/domain/validation.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def validate_required_initial_conditions_present(condition_set: ConditionSet, portfolio: AssetPortfolio) -> None:
    """Validate that every asset whose results depend on its starting state has an initial condition.

    Args:
        condition_set: The initial and final conditions of the run.
        portfolio: The asset portfolio.

    Raises:
        OdysValidationError: If an asset lacks an initial condition it requires.

    """
    for kind in INITIAL_CONDITION_TYPES:
        for asset in portfolio.assets.values():
            if not isinstance(asset, kind.entity_types) or condition_set.get(kind, asset) is not None:
                continue
            reason = kind.required_for(asset)
            if reason is not None:
                msg = f"{type(asset).__name__} '{asset.name}' needs a {kind.__name__} because {reason}."
                raise OdysValidationError(msg)

validate_required_profiles_present(scenarios, entities)

Validate that every scenario has each required profile for every entity that needs one.

Parameters:

Name Type Description Default
scenarios Sequence[Scenario]

Scenarios to check.

required
entities Sequence[EnergyEntity]

All assets and markets of the energy system.

required

Raises:

Type Description
OdysValidationError

If a scenario lacks a required profile.

Source code in src/odys/domain/validation.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def validate_required_profiles_present(scenarios: Sequence[Scenario], entities: Sequence[EnergyEntity]) -> None:
    """Validate that every scenario has each required profile for every entity that needs one.

    Args:
        scenarios: Scenarios to check.
        entities: All assets and markets of the energy system.

    Raises:
        OdysValidationError: If a scenario lacks a required profile.

    """
    required_profile_types = [profile_type for profile_type in PROFILE_TYPES if profile_type.required]
    for scenario in scenarios:
        for profile_type in required_profile_types:
            missing = _names_missing_a_profile(scenario, profile_type, entities)
            if missing:
                msg = f"Scenario '{scenario.name}' is missing a {profile_type.__name__} for: {missing}"
                raise OdysValidationError(msg)