Skip to content

Data model

The classes of each layer and how they relate. Each section links to the API reference of its packages; the overview shows how the layers depend on each other, and sequence diagrams shows how they interact during optimize().

Domain

The user-facing layer: pydantic models validated at construction. It imports nothing else from odys and no numerical library. API reference: odys.domain.

classDiagram
    class EnergyEntity {
        <<abstract>>
        +name: str
        +max_supply(conditions)
        +min_demand(conditions)
        +max_energy_supply(conditions)
        +validate_horizon(conditions)
    }
    class Asset {
        <<abstract>>
    }
    class Generator {
        +nominal_power
        +variable_cost
        +min_power
        +ramp_up
        +ramp_down
        +commitment: Commitment
    }
    class Commitment {
        <<value object>>
        +min_up_time
        +min_down_time
        +startup_cost
        +shutdown_cost
        +validate_horizon(horizon, owner)
        +time_coupled_fields() tuple~str~
    }
    class Battery {
        <<value object>>
        +capacity
        +max_charge_power
        +max_discharge_power
        +efficiency_charging
        +efficiency_discharging
        +soc_min
        +soc_max
        +degradation_cost
        +self_discharge_rate
    }
    class BatteryStorage {
        +battery: Battery
    }
    class ElectricVehicle {
        +battery: Battery
        +trips: tuple~Trip~
    }
    class Trip {
        <<value object>>
        +start_time
        +end_time
        +energy_consumption
        +min_soc_at_departure
    }
    class Charger {
        +max_power
    }
    class FixedLoad
    class FlexibleLoad {
        +min_load
        +max_load
        +value_of_consumption
        +ramp_up
        +ramp_down
        +commitment: Commitment or None
    }
    class EnergyMarket {
        +max_trading_volume_per_step
        +allowed_trade_direction: AllowedTradeDirection
        +stage_fixed: bool
    }
    class AllowedTradeDirection {
        <<StrEnum>>
    }
    class AssetPortfolio {
        +assets: tuple~Asset~
        +get_asset(name) Asset
        +assets_of(asset_type) tuple
    }
    class Horizon {
        <<value object>>
        +timestep: timedelta
        +number_of_steps: int
        +hours_per_step: float
        +is_whole_steps(duration) bool
        +steps_in(duration) int
    }
    class OperatingConditions {
        <<value object>>
        +horizon: Horizon
        +profile_values
    }
    class Profile {
        <<abstract>>
        +entity_types: ClassVar
        +required: ClassVar
        +entity: EnergyEntity
        +number_of_steps: int
        +operating_values: tuple~float~
    }
    class SeriesProfile {
        <<abstract>>
        +values: tuple~float~
    }
    class LoadProfile {
        +load: FixedLoad
    }
    class FlexibleLoadProfile {
        +load: FlexibleLoad
        +lower_bound: tuple~float~
        +upper_bound: tuple~float~
    }
    class AvailableCapacityProfile {
        +generator: Generator
    }
    class PriceProfile {
        +market: EnergyMarket
    }
    class Scenario {
        +name: str
        +probability: float
        +profiles: tuple~Profile~
        +profiles_of(kind)
        +profile_for(kind, entity)
    }
    class ScenarioSet {
        +scenarios: tuple~Scenario~
        +names
    }
    class Condition {
        <<abstract>>
        +entity_types: ClassVar
        +entity: Asset
    }
    class InitialCondition {
        <<abstract>>
        +required_for(entity) str or None
    }
    class FinalCondition {
        <<abstract>>
    }
    class GeneratorInitialState {
        +generator: Generator
        +power: float
        +time_in_state: timedelta or None
        +is_on: bool
    }
    class GeneratorFinalState {
        +generator: Generator
        +power: float
    }
    class FlexibleLoadInitialState {
        +load: FlexibleLoad
        +power: float
        +time_in_state: timedelta or None
        +is_on: bool
    }
    class StorageInitialState {
        +storage: BatteryStorage
        +soc: float
    }
    class StorageFinalState {
        +storage: BatteryStorage
        +soc: float
    }
    class ElectricVehicleInitialState {
        +vehicle: ElectricVehicle
        +soc: float
    }
    class ElectricVehicleFinalState {
        +vehicle: ElectricVehicle
        +soc: float
    }
    class ConditionSet {
        +initial: tuple~InitialCondition~
        +final: tuple~FinalCondition~
        +conditions
        +get(kind, entity)
    }
    class ObjectiveTerm {
        <<abstract>>
        +weight: float
    }
    class ProfitTerm
    class CVaRTerm {
        +confidence_level: float
    }
    class Objective {
        +terms: tuple~ObjectiveTerm~
        +term_of(term_type)
    }

    EnergyEntity <|-- Asset
    EnergyEntity <|-- EnergyMarket
    Asset <|-- Generator
    Asset <|-- BatteryStorage
    Asset <|-- ElectricVehicle
    Asset <|-- Charger
    Asset <|-- FixedLoad
    Asset <|-- FlexibleLoad
    Generator *-- Commitment
    FlexibleLoad o-- Commitment
    BatteryStorage *-- Battery
    ElectricVehicle *-- Battery
    ElectricVehicle *-- "0..*" Trip
    EnergyMarket --> AllowedTradeDirection
    AssetPortfolio o-- "0..*" Asset
    Profile <|-- SeriesProfile
    Profile <|-- FlexibleLoadProfile
    SeriesProfile <|-- LoadProfile
    SeriesProfile <|-- AvailableCapacityProfile
    SeriesProfile <|-- PriceProfile
    LoadProfile --> FixedLoad
    FlexibleLoadProfile --> FlexibleLoad
    AvailableCapacityProfile --> Generator
    PriceProfile --> EnergyMarket
    Scenario *-- "0..*" Profile
    ScenarioSet *-- "1..*" Scenario
    OperatingConditions --> Horizon
    Condition <|-- InitialCondition
    Condition <|-- FinalCondition
    InitialCondition <|-- GeneratorInitialState
    InitialCondition <|-- StorageInitialState
    InitialCondition <|-- ElectricVehicleInitialState
    InitialCondition <|-- FlexibleLoadInitialState
    FinalCondition <|-- GeneratorFinalState
    FinalCondition <|-- StorageFinalState
    FinalCondition <|-- ElectricVehicleFinalState
    GeneratorInitialState --> Generator
    GeneratorFinalState --> Generator
    FlexibleLoadInitialState --> FlexibleLoad
    StorageInitialState --> BatteryStorage
    StorageFinalState --> BatteryStorage
    ElectricVehicleInitialState --> ElectricVehicle
    ElectricVehicleFinalState --> ElectricVehicle
    ConditionSet *-- "0..*" Condition
    ObjectiveTerm <|-- ProfitTerm
    ObjectiveTerm <|-- CVaRTerm
    Objective *-- "1..*" ObjectiveTerm

Parameters and optimization

OptimizationProblem.assemble builds one ModelContext and every formulation whose entity type is present (FORMULATIONS order), then every objective term the objective holds (OBJECTIVE_TERM_FORMULATIONS). Each formulation vectorizes its entities into its EntityArrays subclass with vectorize, reads scenario profiles (context.profiles) and the run's initial and final conditions (context.conditions) through the context, owns its variables, constraints, power injection, profit and results view, and names its own entity axis. build_model adds variables, every ConstraintGroup's constraints and PowerBalance, and maximizes the sum of the objective-term expressions. API reference: odys.parameters, odys.optimization.

classDiagram
    class ModelContext {
        +horizon: Horizon
        +scenario_set: ScenarioSet
        +condition_set: ConditionSet
        +time: Coordinates
        +scenarios: Coordinates
        +timestep_hours: float
        +probabilities
        +variable_coords(coordinates)
        +profiles(kind, entities, coordinates, value)
        +conditions(kind, entities, coordinates, value)
    }
    class Coordinates {
        +dimension: str
        +labels: tuple~str~
        +of_entities(dimension, entities)
    }
    class ModelDimension {
        <<StrEnum>>
        +Scenarios
        +Time
    }
    class EntityArrays {
        <<abstract>>
    }
    class ConstraintGroup {
        +collect_constraints()
        +add_to_model(model)
    }
    class VariableOwner~V~ {
        <<abstract>>
        +variables: V
        +add_variables(model)
    }
    class FormulationInputs {
        +entities
        +context: ModelContext
        +built
        +of_type(entity_type)
        +formulation_of(formulation_type)
    }
    class Formulation {
        <<abstract>>
        +dimension: ClassVar~str~
        +entity_type: ClassVar
        +build(inputs)
        +add_variables(model)
        +power_injection()
        +profit()
        +dispatch(solution) Dispatch
    }
    class VariableFormulation~V~ {
        <<abstract>>
    }
    class GeneratorFormulation
    class EnergyMarketFormulation
    class FixedLoadFormulation
    class FlexibleLoadFormulation
    class BatteryStorageFormulation
    class ElectricVehicleFormulation
    class ChargingFormulation
    class StorageFormulation {
        +create_variables(model)
        +power_injection(variables)
        +dispatch_data(solution)
    }
    class CommitmentFormulation {
        +create_variables(model) CommitmentVariables
        +previous_status(variables)
        +costs(variables, arrays)
    }
    class PowerBalance
    class ObjectiveTermInputs {
        +objective: Objective
        +context: ModelContext
        +formulations
    }
    class ObjectiveTermFormulation {
        <<abstract>>
        +build(inputs)
        +add_variables(model)
        +expression()
    }
    class ProfitTermFormulation
    class CVaRTermFormulation
    class OptimizationProblem {
        +context: ModelContext
        +formulations: tuple~Formulation~
        +objective_terms: tuple~ObjectiveTermFormulation~
        +assemble(entities, horizon, scenario_set, condition_set, objective)
        +formulation_of(formulation_type)
        +dispatches(solution)
    }

    ModelContext --> Coordinates
    Coordinates ..> ModelDimension
    ConstraintGroup <|-- Formulation
    ConstraintGroup <|-- ObjectiveTermFormulation
    ConstraintGroup <|-- PowerBalance
    Formulation <|-- VariableFormulation
    VariableOwner <|-- VariableFormulation
    Formulation <|-- FixedLoadFormulation
    VariableFormulation <|-- GeneratorFormulation
    VariableFormulation <|-- EnergyMarketFormulation
    VariableFormulation <|-- FlexibleLoadFormulation
    VariableFormulation <|-- BatteryStorageFormulation
    VariableFormulation <|-- ElectricVehicleFormulation
    VariableFormulation <|-- ChargingFormulation
    BatteryStorageFormulation *-- StorageFormulation
    ElectricVehicleFormulation *-- StorageFormulation
    GeneratorFormulation *-- CommitmentFormulation
    FlexibleLoadFormulation *-- CommitmentFormulation
    ChargingFormulation --> ElectricVehicleFormulation
    Formulation --> ModelContext
    Formulation ..> EntityArrays : vectorize
    Formulation ..> FormulationInputs : build
    PowerBalance o-- "1..*" Formulation
    ObjectiveTermFormulation <|-- ProfitTermFormulation
    ObjectiveTermFormulation <|-- CVaRTermFormulation
    VariableOwner <|-- CVaRTermFormulation
    ObjectiveTermFormulation ..> ObjectiveTermInputs : build
    OptimizationProblem *-- ModelContext
    OptimizationProblem *-- "1..*" Formulation
    OptimizationProblem *-- "1..*" ObjectiveTermFormulation

Solving and results

solve takes a linopy.Model and returns a SolveOutcome with an odys-owned status. Whether a solution exists is has_solution, not the status: linopy reports ok for a time limit reached before the first feasible solution. OptimalDispatchResults builds the dispatches from the outcome only when there is a solution, squeezes a single scenario once, and serves one typed property per entity type. API reference: odys.solvers, odys.results, odys.energy_system.

classDiagram
    class EnergySystem {
        +portfolio: AssetPortfolio
        +markets
        +scenarios
        +initial_conditions
        +final_conditions
        +objective: Objective
        +condition_set: ConditionSet
        +build_problem() OptimizationProblem
        +optimize(solver_config) OptimalDispatchResults
    }
    class SolverName {
        <<StrEnum>>
        +HIGHS
        +GUROBI
        +CPLEX
        +SCIP
    }
    class SolverConfig {
        +solver_name: SolverName
        +time_limit
        +mip_rel_gap
        +solver_options
    }
    class SolveStatus {
        <<StrEnum>>
        +OK
        +WARNING
        +ERROR
        +ABORTED
        +UNKNOWN
    }
    class SolveOutcome {
        +status: SolveStatus
        +termination_condition: str
        +objective_value: float or None
        +solution: Dataset
        +has_solution: bool
    }
    class Dispatch {
        <<abstract>>
        +label: ClassVar~str~
        +dimension: str
        +to_dataset()
        +to_dataframe()
    }
    class GeneratorDispatch {
        +power
        +status
        +startup
        +shutdown
    }
    class BatteryDispatch {
        +net_power
        +soc
        +charge_mode
    }
    class BatteryStorageDispatch
    class ElectricVehicleDispatch
    class ChargerDispatch {
        +assignment
        +power
    }
    class MarketDispatch {
        +sell_volume
        +buy_volume
        +net_volume
    }
    class FlexibleLoadDispatch {
        +consumption
        +status
        +startup
        +shutdown
    }
    class OptimalDispatchResults {
        +solver_status: SolveStatus
        +termination_condition: str
        +objective_value: float
        +generators: GeneratorDispatch
        +battery_storages: BatteryStorageDispatch
        +electric_vehicles: ElectricVehicleDispatch
        +chargers: ChargerDispatch
        +markets: MarketDispatch
        +flexible_loads: FlexibleLoadDispatch
        +to_dataset()
    }

    EnergySystem ..> OptimizationProblem : build_problem
    EnergySystem ..> SolverConfig : optimize
    EnergySystem ..> OptimalDispatchResults : optimize
    SolverConfig --> SolverName
    SolveOutcome --> SolveStatus
    OptimalDispatchResults ..> SolveOutcome : reads
    OptimalDispatchResults o-- "0..*" Dispatch
    Dispatch <|-- GeneratorDispatch
    Dispatch <|-- BatteryDispatch
    BatteryDispatch <|-- BatteryStorageDispatch
    BatteryDispatch <|-- ElectricVehicleDispatch
    Dispatch <|-- ChargerDispatch
    Dispatch <|-- MarketDispatch
    Dispatch <|-- FlexibleLoadDispatch

Errors

The errors odys raises itself derive from OdysError, and all four are exported from odys; field types and ranges are pydantic's, so they raise pydantic.ValidationError. Input that breaks an odys rule is rejected with OdysValidationError when it is built (an entity, profile, condition or the EnergySystem), a missing solver or a run without a solution raises OdysSolverError, and reading a results container the system has no assets for raises OdysNoResultsError. API reference: odys.domain.exceptions.

classDiagram
    class OdysError
    class OdysValidationError
    class OdysSolverError
    class OdysNoResultsError

    OdysError <|-- OdysValidationError
    OdysError <|-- OdysSolverError
    OdysError <|-- OdysNoResultsError