Skip to content

odys.optimization.formulations.generator

The generator formulation: power, status, startup and shutdown variables, their constraints, the generators' power injection and their cost in the profit.

The variables, constraints and objective terms are written out in the Generators formulation page.

See also Generator for the domain model and entity_arrays for the parameter arrays.

Model formulation of generators.

GeneratorFormulation

Bases: VariableFormulation[GeneratorVariables]

Generators: power limits, available capacity, startup and shutdown, minimum up and down time, ramping.

Source code in src/odys/optimization/formulations/generator.py
 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
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
class GeneratorFormulation(VariableFormulation[GeneratorVariables]):
    """Generators: power limits, available capacity, startup and shutdown, minimum up and down time, ramping."""

    dimension = GENERATOR
    entity_type = Generator
    power_name: ClassVar[str] = f"{GENERATOR}_power"
    status_name: ClassVar[str] = f"{GENERATOR}_status"
    startup_name: ClassVar[str] = f"{GENERATOR}_startup"
    shutdown_name: ClassVar[str] = f"{GENERATOR}_shutdown"

    def __init__(self, generators: Sequence[Generator], context: ModelContext) -> None:
        """Initialize with the generators of the system.

        Args:
            generators: The generators, at least one.
            context: The shared indexing of the problem.
        """
        super().__init__(generators, context)
        self.arrays = vectorize(GeneratorArrays, generators, self.coordinates)
        self.commitment_arrays = vectorize(
            CommitmentArrays,
            [generator.commitment for generator in generators],
            self.coordinates,
        )
        self.states = generator_state_arrays(generators, self.coordinates, context)
        self.commitment = CommitmentFormulation(self.coordinates, self.states.commitment, context)
        self.available_capacity = context.profiles(
            AvailableCapacityProfile,
            generators,
            self.coordinates,
            lambda profile: profile.values,
            default=np.inf,
        )

    @classmethod
    def build(cls, inputs: FormulationInputs) -> Self | None:
        """Return the formulation of the generators, or None if there are none."""
        generators = inputs.of_type(Generator)
        return cls(generators, inputs.context) if generators else None

    def _create_variables(self, model: linopy.Model) -> GeneratorVariables:
        """Add power (MW, non-negative) and the binary status, startup and shutdown indicators."""
        coords = self.context.variable_coords(self.coordinates)
        return GeneratorVariables(
            power=model.add_variables(name=self.power_name, coords=coords, lower=0.0),
            commitment=self.commitment.create_variables(model),
        )

    @constraint
    def _get_generator_max_power_constraint(self) -> ModelConstraint:
        """Generator power limit constraint.

        This constraint ensures that each generator's power output does not
        exceed its nominal power capacity.
        """
        return ModelConstraint(
            constraint=self.variables.power - self.variables.commitment.status * self.arrays.nominal_power <= 0,
            name="generator_max_power_constraint",
        )

    @constraint
    def _get_generator_status_constraint(self) -> ModelConstraint:
        """When on, output is at least a small fraction (1e-5) of nominal power, so `status` = 1 means it produces."""
        epsilon = 1e-5 * self.arrays.nominal_power
        return ModelConstraint(
            constraint=self.variables.power >= self.variables.commitment.status * epsilon,
            name="generator_status_constraint",
        )

    @constraint
    def _get_generator_startup_lower_bound_constraint(self) -> ModelConstraint:
        """Startup indicator active when turning generator on, from its initial status at t=0."""
        return self.commitment.startup_lower_bound_constraint(self.variables.commitment)

    @constraint
    def _get_generator_startup_upper_bound_1_constraint(self) -> ModelConstraint:
        """Startup indicator bounded by current status."""
        return self.commitment.startup_upper_bound_1_constraint(self.variables.commitment)

    @constraint
    def _get_generator_startup_upper_bound_2_constraint(self) -> ModelConstraint:
        """Startup indicator bounded by previous status."""
        return self.commitment.startup_upper_bound_2_constraint(self.variables.commitment)

    @constraint
    def _get_generator_shutdown_lower_bound_constraint(self) -> ModelConstraint:
        """Shutdown indicator active when turning generator off, from its initial status at t=0."""
        return self.commitment.shutdown_lower_bound_constraint(self.variables.commitment)

    @constraint
    def _get_generator_shutdown_upper_bound_1_constraint(self) -> ModelConstraint:
        """Shutdown indicator bounded by previous status."""
        return self.commitment.shutdown_upper_bound_1_constraint(self.variables.commitment)

    @constraint
    def _get_generator_shutdown_upper_bound_2_constraint(self) -> ModelConstraint:
        """Shutdown indicator bounded by current status."""
        return self.commitment.shutdown_upper_bound_2_constraint(self.variables.commitment)

    @constraint
    def _get_min_uptime_constraint(self) -> list[ModelConstraint]:
        """Once started, a generator stays on for its minimum up time, counting time on before the horizon."""
        return self.commitment.min_uptime_constraints(self.variables.commitment)

    @constraint
    def _get_min_downtime_constraint(self) -> list[ModelConstraint]:
        """Once stopped, a generator stays off for its minimum down time, counting time off before the horizon."""
        return self.commitment.min_downtime_constraints(self.variables.commitment)

    @constraint
    def _get_initial_min_uptime_constraint(self) -> list[ModelConstraint]:
        """A generator on for less than its minimum up time before the horizon does not shut down at t=0."""
        return self.commitment.initial_min_uptime_constraint(self.variables.commitment)

    @constraint
    def _get_initial_min_downtime_constraint(self) -> list[ModelConstraint]:
        """A generator off for less than its minimum down time before the horizon does not start at t=0."""
        return self.commitment.initial_min_downtime_constraint(self.variables.commitment)

    @constraint
    def _get_min_power_constraint(self) -> ModelConstraint:
        """When on, output is at least `min_power`."""
        return ModelConstraint(
            constraint=self.variables.power >= self.arrays.min_power * self.variables.commitment.status,
            name="generator_min_power_constraint",
        )

    @constraint
    def _get_max_ramp_up_constraint(self) -> ModelConstraint:
        """Output rises by at most `ramp_up` per hour times the step length, from the initial power at t=0.

        Unlimited when unset.
        """
        max_ramp_up = (self.arrays.ramp_up * self.context.timestep_hours).fillna(self.arrays.nominal_power)
        ramp = self.variables.power - self._previous_power()
        return ModelConstraint(
            constraint=ramp.to_constraint("<=", max_ramp_up),
            name="generator_max_ramp_up_constraint",
        )

    @constraint
    def _get_max_ramp_down_constraint(self) -> ModelConstraint:
        """Output falls by at most `ramp_down` per hour times the step length, from the initial power at t=0.

        Unlimited when unset.
        """
        max_ramp_down = (self.arrays.ramp_down * self.context.timestep_hours).fillna(self.arrays.nominal_power)
        ramp = self._previous_power() - self.variables.power
        return ModelConstraint(
            constraint=ramp.to_constraint("<=", max_ramp_down),
            name="generator_max_ramp_down_constraint",
        )

    @constraint
    def _get_final_power_constraint(self) -> list[ModelConstraint]:
        """Output at the last step equals the final power, for the generators that have a final state."""
        has_final_power = self.states.final_power.notnull()
        if not has_final_power.any():
            return []
        last_step = {TIME: self.context.time.labels[-1]}
        with_final_power = {GENERATOR: has_final_power}
        expression = self.variables.power.sel(last_step).sel(with_final_power) - self.states.final_power.sel(
            with_final_power,
        )
        return [
            ModelConstraint(
                constraint=expression.to_constraint("=", 0),
                name="generator_final_power_constraint",
            ),
        ]

    @constraint
    def _get_available_capacity_constraint(self) -> ModelConstraint:
        """Power is capped by the scenario's available capacity profile (unbounded without one)."""
        return ModelConstraint(
            constraint=self.variables.power <= self.available_capacity,
            name="available_capacity_constraint",
        )

    def _previous_power(self) -> linopy.LinearExpression:
        """Return the power one step earlier, the initial power at t=0."""
        return time_shifted(self.variables.power, 1, outside_value=self.states.initial_power)

    def power_injection(self) -> linopy.LinearExpression:
        """Return the total generator output, per scenario and time."""
        injection: linopy.LinearExpression = self.variables.power.sum(GENERATOR)
        return injection

    def profit(self) -> linopy.LinearExpression:
        """Return minus the generation cost per scenario: variable cost per MWh plus startup and shutdown costs."""
        cost = self.variables.power * self.context.timestep_hours * self.arrays.variable_cost + self.commitment.costs(
            self.variables.commitment,
            self.commitment_arrays,
        )
        profit: linopy.LinearExpression = -cost.sum([TIME, GENERATOR])
        return profit

    def dispatch(self, solution: xr.Dataset) -> GeneratorDispatch:
        """Return the power, status, startups and shutdowns of the generators."""
        data = xr.Dataset(
            {
                "power": solution[self.power_name],
                "status": solution[self.status_name],
                "startup": solution[self.startup_name],
                "shutdown": solution[self.shutdown_name],
            },
        )
        return GeneratorDispatch(data, GENERATOR)

__init__(generators, context)

Initialize with the generators of the system.

Parameters:

Name Type Description Default
generators Sequence[Generator]

The generators, at least one.

required
context ModelContext

The shared indexing of the problem.

required
Source code in src/odys/optimization/formulations/generator.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def __init__(self, generators: Sequence[Generator], context: ModelContext) -> None:
    """Initialize with the generators of the system.

    Args:
        generators: The generators, at least one.
        context: The shared indexing of the problem.
    """
    super().__init__(generators, context)
    self.arrays = vectorize(GeneratorArrays, generators, self.coordinates)
    self.commitment_arrays = vectorize(
        CommitmentArrays,
        [generator.commitment for generator in generators],
        self.coordinates,
    )
    self.states = generator_state_arrays(generators, self.coordinates, context)
    self.commitment = CommitmentFormulation(self.coordinates, self.states.commitment, context)
    self.available_capacity = context.profiles(
        AvailableCapacityProfile,
        generators,
        self.coordinates,
        lambda profile: profile.values,
        default=np.inf,
    )

build(inputs) classmethod

Return the formulation of the generators, or None if there are none.

Source code in src/odys/optimization/formulations/generator.py
70
71
72
73
74
@classmethod
def build(cls, inputs: FormulationInputs) -> Self | None:
    """Return the formulation of the generators, or None if there are none."""
    generators = inputs.of_type(Generator)
    return cls(generators, inputs.context) if generators else None

dispatch(solution)

Return the power, status, startups and shutdowns of the generators.

Source code in src/odys/optimization/formulations/generator.py
233
234
235
236
237
238
239
240
241
242
243
def dispatch(self, solution: xr.Dataset) -> GeneratorDispatch:
    """Return the power, status, startups and shutdowns of the generators."""
    data = xr.Dataset(
        {
            "power": solution[self.power_name],
            "status": solution[self.status_name],
            "startup": solution[self.startup_name],
            "shutdown": solution[self.shutdown_name],
        },
    )
    return GeneratorDispatch(data, GENERATOR)

power_injection()

Return the total generator output, per scenario and time.

Source code in src/odys/optimization/formulations/generator.py
219
220
221
222
def power_injection(self) -> linopy.LinearExpression:
    """Return the total generator output, per scenario and time."""
    injection: linopy.LinearExpression = self.variables.power.sum(GENERATOR)
    return injection

profit()

Return minus the generation cost per scenario: variable cost per MWh plus startup and shutdown costs.

Source code in src/odys/optimization/formulations/generator.py
224
225
226
227
228
229
230
231
def profit(self) -> linopy.LinearExpression:
    """Return minus the generation cost per scenario: variable cost per MWh plus startup and shutdown costs."""
    cost = self.variables.power * self.context.timestep_hours * self.arrays.variable_cost + self.commitment.costs(
        self.variables.commitment,
        self.commitment_arrays,
    )
    profit: linopy.LinearExpression = -cost.sum([TIME, GENERATOR])
    return profit

GeneratorVariables

Bases: BaseModel

Decision variables of the generators.

Source code in src/odys/optimization/formulations/generator.py
27
28
29
30
31
32
33
class GeneratorVariables(BaseModel):
    """Decision variables of the generators."""

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

    power: linopy.Variable
    commitment: CommitmentVariables