Skip to content

Optimization API

OptimizationResults

Optimization results handling for energy system models.

This module provides classes for handling and analyzing optimization results from energy system models.

OptimizationResults

Container for optimization results and metadata.

This class wraps the solver results and provides convenient access to solution data, solver status, and termination conditions.

Source code in src/odys/optimization/optimization_results.py
 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
 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
class OptimizationResults:
    """Container for optimization results and metadata.

    This class wraps the solver results and provides convenient access
    to solution data, solver status, and termination conditions.
    """

    def __init__(
        self,
        solver_status: SolverStatus,
        termination_condition: TerminationCondition,
        milp_model: EnergyMILPModel,
    ) -> None:
        """Initialize the optimization results object.

        Args:
            solver_status: Solving status
            termination_condition: Termination condition
            milp_model: Solved EnergyMILPModel Model
        """
        self._solver_status = solver_status
        self._termination_condition = termination_condition
        self._milp_model = milp_model

    @cached_property
    def solver_status(self) -> str:
        """Get the solver status.

        Returns:
            The solver status indicating whether the solve was successful.

        """
        return self._solver_status.value

    @cached_property
    def termination_condition(self) -> str:
        """Get the termination condition.

        Returns:
            The termination condition indicating how the solver finished.

        """
        return self._termination_condition.value

    @cached_property
    def _solution(self) -> xr.Dataset:
        self._validate_terminated_successfully()
        return self._milp_model.linopy_model.solution

    def to_dataframe(self) -> pd.DataFrame:
        """Convert optimization results to a pandas DataFrame.

        Returns:
            DataFrame containing all solution variables with units, variables,
            and time periods as multi-level index columns.
        """
        dfs = []
        for variable in ModelVariable:
            variable_name = variable.var_name
            # Skip if there is this variable is not populated (eg skip battery varialbes if no batteries in the system)
            if variable_name not in self._milp_model.linopy_model.variables.labels:
                continue
            var_solution = self._solution[variable_name]
            df = (
                var_solution
                .to_series()
                .reset_index()
                .rename(columns={variable.asset_dimension: "unit", variable_name: "value"})
                .assign(variable=variable_name)
            )
            dfs.append(df)

        return (
            pd
            .concat(dfs, ignore_index=True)
            .set_index([
                ModelDimension.Scenarios,
                "unit",
                "variable",
                ModelDimension.Time,
            ])
            .sort_index()
            .pipe(self._drop_single_scenario_level)
        )

    def _drop_single_scenario_level(self, df: pd.DataFrame) -> pd.DataFrame:
        scenario_values = df.index.get_level_values(ModelDimension.Scenarios).to_numpy()
        if (scenario_values == scenario_values[0]).all():
            return df.droplevel(ModelDimension.Scenarios)
        return df

    def _validate_terminated_successfully(self) -> None:
        if self._solver_status != SolverStatus.ok:
            msg = f"No solution available. Optimization Termination Condition: {self.termination_condition}."
            raise ValueError(msg)

    @cached_property
    def batteries(self) -> BatteryResults:
        """Get battery results."""
        self._validate_terminated_successfully()
        if self._milp_model.parameters.batteries is None:
            msg = "This model does not contain battery results"
            raise ValueError(msg)
        return BatteryResults(
            net_power=self._get_variable_results(ModelVariable.BATTERY_POWER_NET),
            state_of_charge=self._get_variable_results(ModelVariable.BATTERY_SOC),
        )

    @cached_property
    def markets(self) -> MarketResults:
        """Get market results."""
        self._validate_terminated_successfully()
        if self._milp_model.parameters.markets is None:
            msg = "This model does not contain market results"
            raise ValueError(msg)
        return MarketResults(
            sell_volume=self._get_variable_results(ModelVariable.MARKET_SELL),
            buy_volume=self._get_variable_results(ModelVariable.MARKET_BUY),
        )

    @cached_property
    def generators(self) -> GeneratorResults:
        """Get generator results."""
        self._validate_terminated_successfully()
        if self._milp_model.parameters.generators is None:
            msg = "This model does not contain generator results"
            raise ValueError(msg)

        return GeneratorResults(
            power=self._get_variable_results(ModelVariable.GENERATOR_POWER),
            status=self._get_variable_results(ModelVariable.GENERATOR_STATUS),
            startup=self._get_variable_results(ModelVariable.GENERATOR_STARTUP),
            shutdown=self._get_variable_results(ModelVariable.GENERATOR_SHUTDOWN),
        )

    def _get_variable_results(self, variable: ModelVariable) -> pd.DataFrame:
        var_timeseries = self._solution[variable.var_name].to_series()
        return var_timeseries.unstack().pipe(self._drop_single_scenario_level)  # noqa: PD010

batteries cached property

Get battery results.

generators cached property

Get generator results.

markets cached property

Get market results.

solver_status cached property

Get the solver status.

Returns:

Type Description
str

The solver status indicating whether the solve was successful.

termination_condition cached property

Get the termination condition.

Returns:

Type Description
str

The termination condition indicating how the solver finished.

__init__(solver_status, termination_condition, milp_model)

Initialize the optimization results object.

Parameters:

Name Type Description Default
solver_status SolverStatus

Solving status

required
termination_condition TerminationCondition

Termination condition

required
milp_model EnergyMILPModel

Solved EnergyMILPModel Model

required
Source code in src/odys/optimization/optimization_results.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(
    self,
    solver_status: SolverStatus,
    termination_condition: TerminationCondition,
    milp_model: EnergyMILPModel,
) -> None:
    """Initialize the optimization results object.

    Args:
        solver_status: Solving status
        termination_condition: Termination condition
        milp_model: Solved EnergyMILPModel Model
    """
    self._solver_status = solver_status
    self._termination_condition = termination_condition
    self._milp_model = milp_model

to_dataframe()

Convert optimization results to a pandas DataFrame.

Returns:

Type Description
DataFrame

DataFrame containing all solution variables with units, variables,

DataFrame

and time periods as multi-level index columns.

Source code in src/odys/optimization/optimization_results.py
 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
def to_dataframe(self) -> pd.DataFrame:
    """Convert optimization results to a pandas DataFrame.

    Returns:
        DataFrame containing all solution variables with units, variables,
        and time periods as multi-level index columns.
    """
    dfs = []
    for variable in ModelVariable:
        variable_name = variable.var_name
        # Skip if there is this variable is not populated (eg skip battery varialbes if no batteries in the system)
        if variable_name not in self._milp_model.linopy_model.variables.labels:
            continue
        var_solution = self._solution[variable_name]
        df = (
            var_solution
            .to_series()
            .reset_index()
            .rename(columns={variable.asset_dimension: "unit", variable_name: "value"})
            .assign(variable=variable_name)
        )
        dfs.append(df)

    return (
        pd
        .concat(dfs, ignore_index=True)
        .set_index([
            ModelDimension.Scenarios,
            "unit",
            "variable",
            ModelDimension.Time,
        ])
        .sort_index()
        .pipe(self._drop_single_scenario_level)
    )

Result Containers

Containers for storing per-asset optimization results.

BatteryResults

Bases: BaseModel

Class to store battery results.

Source code in src/odys/optimization/result_containers.py
18
19
20
21
22
23
24
class BatteryResults(BaseModel):
    """Class to store battery results."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    net_power: pd.DataFrame
    state_of_charge: pd.DataFrame

GeneratorResults

Bases: BaseModel

Class to store generator results.

Source code in src/odys/optimization/result_containers.py
 7
 8
 9
10
11
12
13
14
15
class GeneratorResults(BaseModel):
    """Class to store generator results."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    power: pd.DataFrame
    status: pd.DataFrame
    startup: pd.DataFrame
    shutdown: pd.DataFrame

MarketResults

Bases: BaseModel

Class to store market results.

Source code in src/odys/optimization/result_containers.py
27
28
29
30
31
32
33
class MarketResults(BaseModel):
    """Class to store market results."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    sell_volume: pd.DataFrame
    buy_volume: pd.DataFrame