Skip to content

odys.optimization.formulations.base

Internal implementation detail. The Formulation base class.

Base class of the model formulation of one entity type.

Formulation

Bases: ConstraintGroup, ABC

The optimization model of every entity of one type, vectorized along that type's dimension.

A formulation owns, for its entity type: the parameter arrays, the decision variables (created in add_variables), the @constraint methods (inherited discovery from ConstraintGroup), the net power it injects into the power balance, its profit per scenario and its dispatch results. It is built only when the system has at least one entity of its type, so its methods never check for absence. It holds its variables once added, so it belongs to a single model.

Each subclass names its own axis (dimension) and the entity type it models (entity_type); its entities' names are the coordinates along that axis.

Source code in src/odys/optimization/formulations/base.py
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
class Formulation(ConstraintGroup, ABC):
    """The optimization model of every entity of one type, vectorized along that type's dimension.

    A formulation owns, for its entity type: the parameter arrays, the decision
    variables (created in `add_variables`), the `@constraint` methods (inherited
    discovery from `ConstraintGroup`), the net power it injects into the power
    balance, its profit per scenario and its dispatch results. It is built only when the system has
    at least one entity of its type, so its methods never check for absence.
    It holds its variables once added, so it belongs to a single model.

    Each subclass names its own axis (`dimension`) and the entity type it models
    (`entity_type`); its entities' names are the coordinates along that axis.
    """

    dimension: ClassVar[str]
    entity_type: ClassVar[type[EnergyEntity]]

    def __init__(self, entities: Sequence[EnergyEntity], context: ModelContext) -> None:
        """Initialize with this type's entities and the shared indexing of the problem.

        Args:
            entities: The entities of this formulation's type, at least one, in order.
            context: The time and scenario coordinates, step length and profiles of the problem.
        """
        self.context = context
        self.coordinates = Coordinates.of_entities(self.dimension, entities)

    @classmethod
    @abstractmethod
    def build(cls, inputs: "FormulationInputs") -> Self | None:
        """Return the formulation of this type's entities, or None if the system has none.

        Args:
            inputs: Every entity of the system, the shared indexing, and the formulations built before this one.
        """

    def add_variables(self, model: linopy.Model) -> None:
        """Add this formulation's decision variables to the model. The default adds none.

        Args:
            model: The linopy model to add the variables to.
        """

    @abstractmethod
    def power_injection(self) -> linopy.LinearExpression | xr.DataArray | None:
        """Return the net power these entities inject into the bus, per scenario and time, in MW.

        Supply is positive and demand negative. A constant array (fixed demand) has no
        decision variable. None means the entity type is outside the power balance (chargers).
        """

    def profit(self) -> linopy.LinearExpression | None:
        """Return the profit of these entities per scenario, with energy terms scaled by the step length.

        The default is None: the entity type adds nothing to the objective.
        """
        return None

    @abstractmethod
    def dispatch(self, solution: xr.Dataset) -> Dispatch | None:
        """Return the dispatch results of these entities, read from the solution of the model.

        None means the entity type has no results (fixed loads).

        Args:
            solution: The solution of the model this formulation was built into, by linopy variable name.
        """

__init__(entities, context)

Initialize with this type's entities and the shared indexing of the problem.

Parameters:

Name Type Description Default
entities Sequence[EnergyEntity]

The entities of this formulation's type, at least one, in order.

required
context ModelContext

The time and scenario coordinates, step length and profiles of the problem.

required
Source code in src/odys/optimization/formulations/base.py
43
44
45
46
47
48
49
50
51
def __init__(self, entities: Sequence[EnergyEntity], context: ModelContext) -> None:
    """Initialize with this type's entities and the shared indexing of the problem.

    Args:
        entities: The entities of this formulation's type, at least one, in order.
        context: The time and scenario coordinates, step length and profiles of the problem.
    """
    self.context = context
    self.coordinates = Coordinates.of_entities(self.dimension, entities)

add_variables(model)

Add this formulation's decision variables to the model. The default adds none.

Parameters:

Name Type Description Default
model Model

The linopy model to add the variables to.

required
Source code in src/odys/optimization/formulations/base.py
62
63
64
65
66
67
def add_variables(self, model: linopy.Model) -> None:
    """Add this formulation's decision variables to the model. The default adds none.

    Args:
        model: The linopy model to add the variables to.
    """

build(inputs) abstractmethod classmethod

Return the formulation of this type's entities, or None if the system has none.

Parameters:

Name Type Description Default
inputs FormulationInputs

Every entity of the system, the shared indexing, and the formulations built before this one.

required
Source code in src/odys/optimization/formulations/base.py
53
54
55
56
57
58
59
60
@classmethod
@abstractmethod
def build(cls, inputs: "FormulationInputs") -> Self | None:
    """Return the formulation of this type's entities, or None if the system has none.

    Args:
        inputs: Every entity of the system, the shared indexing, and the formulations built before this one.
    """

dispatch(solution) abstractmethod

Return the dispatch results of these entities, read from the solution of the model.

None means the entity type has no results (fixed loads).

Parameters:

Name Type Description Default
solution Dataset

The solution of the model this formulation was built into, by linopy variable name.

required
Source code in src/odys/optimization/formulations/base.py
84
85
86
87
88
89
90
91
92
@abstractmethod
def dispatch(self, solution: xr.Dataset) -> Dispatch | None:
    """Return the dispatch results of these entities, read from the solution of the model.

    None means the entity type has no results (fixed loads).

    Args:
        solution: The solution of the model this formulation was built into, by linopy variable name.
    """

power_injection() abstractmethod

Return the net power these entities inject into the bus, per scenario and time, in MW.

Supply is positive and demand negative. A constant array (fixed demand) has no decision variable. None means the entity type is outside the power balance (chargers).

Source code in src/odys/optimization/formulations/base.py
69
70
71
72
73
74
75
@abstractmethod
def power_injection(self) -> linopy.LinearExpression | xr.DataArray | None:
    """Return the net power these entities inject into the bus, per scenario and time, in MW.

    Supply is positive and demand negative. A constant array (fixed demand) has no
    decision variable. None means the entity type is outside the power balance (chargers).
    """

profit()

Return the profit of these entities per scenario, with energy terms scaled by the step length.

The default is None: the entity type adds nothing to the objective.

Source code in src/odys/optimization/formulations/base.py
77
78
79
80
81
82
def profit(self) -> linopy.LinearExpression | None:
    """Return the profit of these entities per scenario, with energy terms scaled by the step length.

    The default is None: the entity type adds nothing to the objective.
    """
    return None

FormulationInputs

Bases: BaseModel

What a formulation is built from: the system's entities, the shared indexing, and earlier formulations.

Formulations are built in FORMULATIONS order, so a formulation that depends on another (charging on electric vehicles) finds it in built.

Source code in src/odys/optimization/formulations/base.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class FormulationInputs(BaseModel):
    """What a formulation is built from: the system's entities, the shared indexing, and earlier formulations.

    Formulations are built in `FORMULATIONS` order, so a formulation that depends
    on another (charging on electric vehicles) finds it in `built`.
    """

    model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)

    entities: tuple[EnergyEntity, ...]
    context: ModelContext
    built: tuple[Formulation, ...] = ()

    def of_type(self, kind: type[EntityT]) -> list[EntityT]:
        """Return the entities of the given type, in order."""
        return [entity for entity in self.entities if isinstance(entity, kind)]

    def formulation_of(self, kind: type[FormulationT]) -> FormulationT | None:
        """Return an already built formulation of the given type, if any."""
        return next((formulation for formulation in self.built if isinstance(formulation, kind)), None)

formulation_of(kind)

Return an already built formulation of the given type, if any.

Source code in src/odys/optimization/formulations/base.py
112
113
114
def formulation_of(self, kind: type[FormulationT]) -> FormulationT | None:
    """Return an already built formulation of the given type, if any."""
    return next((formulation for formulation in self.built if isinstance(formulation, kind)), None)

of_type(kind)

Return the entities of the given type, in order.

Source code in src/odys/optimization/formulations/base.py
108
109
110
def of_type(self, kind: type[EntityT]) -> list[EntityT]:
    """Return the entities of the given type, in order."""
    return [entity for entity in self.entities if isinstance(entity, kind)]

VariableFormulation

Bases: VariableOwner[VariablesT], Formulation

An entity formulation with decision variables, held in a typed, frozen variables object (see VariableOwner).

Source code in src/odys/optimization/formulations/base.py
117
118
class VariableFormulation(VariableOwner[VariablesT], Formulation):
    """An entity formulation with decision variables, held in a typed, frozen variables object (see `VariableOwner`)."""

per_scenario_profit(formulations)

Return the profit per scenario: every formulation's profit, summed over time and entities.

Scenario probabilities are not applied; the expected-profit and CVaR terms do that.

Parameters:

Name Type Description Default
formulations Sequence[Formulation]

The formulations of the problem, whose variables are already added.

required

Raises:

Type Description
OdysError

If no formulation contributes a profit term.

Source code in src/odys/optimization/formulations/base.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def per_scenario_profit(formulations: Sequence[Formulation]) -> linopy.LinearExpression:
    """Return the profit per scenario: every formulation's profit, summed over time and entities.

    Scenario probabilities are not applied; the expected-profit and CVaR terms do that.

    Args:
        formulations: The formulations of the problem, whose variables are already added.

    Raises:
        OdysError: If no formulation contributes a profit term.
    """
    profits = [profit for formulation in formulations if (profit := formulation.profit()) is not None]
    if not profits:
        msg = "The problem has no entity type that contributes a profit term."
        raise OdysError(msg)
    total: linopy.LinearExpression = reduce(add, profits)
    return total

time_shifted(variable, steps, outside_value=0)

Return the variable moved steps timesteps along time, with a constant where the source step is outside.

steps=1 gives each step the previous step's value, and outside_value at the first step: the value just before the horizon (0 by default, as if everything was off or empty). steps=-1 gives the next step's value, and outside_value at the last step. The explicit constant keeps the constraints the same under linopy's legacy and v1 semantics, where an absent slot would otherwise drop the term. The constant is added aligned by dimension name, so an array along an entity dimension lands on that dimension whatever the variable's order.

Parameters:

Name Type Description Default
variable Variable

The variable to shift; it must have the time dimension.

required
steps int

How many timesteps to move it; positive looks back, negative looks ahead.

required
outside_value float | DataArray

The value at the steps shifted in from outside the horizon, a scalar or an array along an entity dimension of the variable.

0

Returns:

Type Description
LinearExpression

A linear expression over the variable's coordinates, outside_value at the steps shifted in from outside.

Source code in src/odys/optimization/formulations/base.py
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
def time_shifted(
    variable: linopy.Variable,
    steps: int,
    outside_value: float | xr.DataArray = 0,
) -> linopy.LinearExpression:
    """Return the variable moved `steps` timesteps along time, with a constant where the source step is outside.

    `steps=1` gives each step the previous step's value, and `outside_value` at the first step: the value just
    before the horizon (0 by default, as if everything was off or empty). `steps=-1` gives the next step's value,
    and `outside_value` at the last step. The explicit constant keeps the constraints the same under linopy's
    legacy and v1 semantics, where an absent slot would otherwise drop the term. The constant is added aligned by
    dimension name, so an array along an entity dimension lands on that dimension whatever the variable's order.

    Args:
        variable: The variable to shift; it must have the time dimension.
        steps: How many timesteps to move it; positive looks back, negative looks ahead.
        outside_value: The value at the steps shifted in from outside the horizon, a scalar or an array along an
            entity dimension of the variable.

    Returns:
        A linear expression over the variable's coordinates, `outside_value` at the steps shifted in from outside.
    """
    time = variable.coords[ModelDimension.Time]
    shifted_in = xr.ones_like(time).shift({ModelDimension.Time: steps}).isnull()
    shifted: linopy.LinearExpression = (
        variable
        .to_linexpr()
        .shift({ModelDimension.Time: steps})
        .fillna(
            0,
        )
        + shifted_in * outside_value
    )
    return shifted