Skip to content

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.
    """

    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
    # a constraint term for site import power electricity cost
    ConstraintTerm(
        variable="import_power_mwh",
        asset_type="site",
        interval_data="electricity_prices"
    )

    # a constraint term for site export power electricity revenue
    ConstraintTerm(
        variable="import_power_mwh",
        asset_type="site",
        interval_data="electricity_prices",
        coefficient=-1
    )

    # a constraint term for 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):

total_cycles=30.000000002

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:

   chp-electric_generation_mwh  solar-electric_generation_mwh  total-electric_generation_mwh
0                         15.0                           10.0                           25.0
1                          5.0                           20.0                           25.0
2                          0.0                           25.0                           25.0
3                          0.0                           25.0                           25.0