Constraints
Constraints define the feasible region of a linear program. They are how you control what is and isn't possible in a simulation.
The assets and site in energypylinear
apply built-in constraints to the linear program. In addition, you can define your own custom constraints.
A custom constraint allows you to control what can and cannot happen in an energypylinear
simulation.
Custom Constraint
The epl.Constraint
represents a single custom constraint.
A custom constraint has a left hand side, a right hand side and a sense.
class Constraint(pydantic.BaseModel):
"""A custom constraint.
Made of a left-hand side (LHS), a right-hand side (RHS) and a sense (<=, ==, >=).
Attributes:
lhs: The left-hand side of the constraint.
rhs: The right-hand side of the constraint.
sense: The constraint sense and a sense (<=, ==, >=).
interval_aggregation: How to aggregate terms across intervals.
None will result in one constraint per interval.
"sum" will result in one constraint per simulation.
Example of a custom constraint with the `energypylinear` Pydantic models to represent:
- the constraint,
- the constraint terms on the left and right sides,
- the sense of the constraint,
- whether to aggregate across the interval (time) dimension.
```python
import energypylinear as epl
epl.Battery(
power_mw=1,
capacity_mwh=2,
efficiency_pct=0.98,
electricity_prices=np.random.normal(0.0, 1000, 48 * 7),
constraints=[
epl.Constraint(
lhs=[
epl.ConstraintTerm(
asset_type="battery", variable="electric_charge_mwh"
),
epl.ConstraintTerm(
asset_type="battery", variable="electric_discharge_mwh"
),
],
rhs=cycle_limit_mwh,
sense="le",
interval_aggregation="sum",
)
],
)
```
Constraints can also be set with dictionaries:
```python
import energypylinear as epl
epl.Battery(
power_mw=1,
capacity_mwh=2,
efficiency_pct=0.98,
electricity_prices=np.random.normal(0.0, 1000, 48 * 7),
constraints=[
{
"lhs": [
{"asset_type": "battery", "variable": "electric_charge_mwh"},
{"asset_type": "battery", "variable": "electric_discharge_mwh"},
],
"rhs": cycle_limit_mwh,
"sense": "le",
"interval_aggregation": "sum",
},
],
)
"""
lhs: float | ConstraintTerm | dict | list[float | ConstraintTerm | dict]
rhs: float | ConstraintTerm | dict | list[float | ConstraintTerm | dict]
sense: typing.Literal["le", "eq", "ge"]
interval_aggregation: typing.Literal["sum"] | None = None
It also has an option for configuring how the constraint is aggregated over the simulation intervals.
Constraint Terms
Both the left and right hand sides of a custom constraint are lists of constraint terms. A constraint term can be either a constant, an epl.ConstraintTerm
or a dictionary.
The epl.ConstraintTerm
represents a single term in a constraint:
@dataclasses.dataclass
class ConstraintTerm:
"""A term in a constraint.
The sum of terms creates the two sides of a constraint,
the left-hand side (LHS) and right-hand side (RHS).
Examples:
```python
# site import power electricity cost
ConstraintTerm(
variable="import_power_mwh",
asset_type="site",
interval_data="electricity_prices"
)
# site export power electricity revenue
ConstraintTerm(
variable="import_power_mwh",
asset_type="site",
interval_data="electricity_prices",
coefficient=-1
)
# battery cycle cost
ConstraintTerm(
variable="electric_charge_mwh",
asset_type="battery",
coefficient=0.25
)
```
Attributes:
variable: The linear program variable. This will be an
attribute of a OneInterval object, like `import_power_mwh`
or `gas_consumption_mwh`.
asset_type: The type of asset, such as `battery` or `chp`.
`*` will include all assets.
interval_data: The interval data variable, such as
`electricity_prices` or `gas_prices`.
asset_name: The name of a specific asset.
coefficient: A constant multipler for the term.
"""
variable: str
asset_type: str | None = None
interval_data: str | None = None
asset_name: str | None = None
coefficient: float = 1.0
Examples
Limiting Battery Cycles
The example below shows how to optimize a battery with a custom constraint on battery cycles.
We define battery cycles as the sum of the total battery charge and discharge, and constrain it to be less than or equal to 15 cycles of 2 MWh per cycle:
import energypylinear as epl
import numpy as np
np.random.seed(42)
cycle_limit_mwh = 30
asset = epl.Battery(
power_mw=1,
capacity_mwh=2,
efficiency_pct=0.98,
electricity_prices=np.random.normal(0.0, 1000, 48 * 7),
constraints=[
epl.Constraint(
lhs=[
epl.ConstraintTerm(
asset_type="battery", variable="electric_charge_mwh"
),
epl.ConstraintTerm(
asset_type="battery", variable="electric_discharge_mwh"
),
],
rhs=cycle_limit_mwh,
sense="le",
interval_aggregation="sum",
)
],
)
simulation = asset.optimize(verbose=3)
total_cycles = simulation.results.sum()[
["battery-electric_charge_mwh", "battery-electric_discharge_mwh"]
].sum()
print(f"{total_cycles=}")
After simulation we can see our total cycles are constrained to an upper limit of 30 (with a small floating point error):
Constraining Total Generation
The example below shows how to use a custom constraint to constrain the total generation in a site.
We define a site with a solar and electric generator asset, with the available solar power increasing with time:
import energypylinear as epl
import numpy as np
np.random.seed(42)
idx_len = 4
generator_size = 100
solar_gen = [10.0, 20, 30, 40]
site = epl.Site(
assets=[
epl.RenewableGenerator(
electric_generation_mwh=solar_gen,
name="solar",
electric_generation_lower_bound_pct=0.0,
),
epl.CHP(electric_power_max_mw=generator_size, electric_efficiency_pct=0.5),
],
electricity_prices=np.full(idx_len, 400),
gas_prices=10,
constraints=[
{
"lhs": {"variable": "electric_generation_mwh", "asset_type": "*"},
"rhs": 25,
"sense": "le",
}
],
)
simulation = site.optimize(verbose=3)
print(
simulation.results[
[
"chp-electric_generation_mwh",
"solar-electric_generation_mwh",
"total-electric_generation_mwh",
]
]
)
As solar generation becomes available, the CHP electric generation decreases to keep the total site electric generation at 25 MWh: