Complex terms
In energypylinear
you can use custom objective functions to define a custom set of incentives and costs in your linear program.
The objective function will often be made up of simple terms, which are the product of a single linear variable (one per interval), interval data and a coefficient.
Sites will however often have more complicated costs and revenues, that involve taking the minimum or maximum of a collection of variables.
A complex custom objective term allows you to construct an objective function with a complex set of costs and revenues.
Complex Objective Function Terms
energypylinear
uses complex terms to include these more complicated incentives and costs in the objective function:
@dataclasses.dataclass
class FunctionTermTwoVariables:
"""A function term for constraining two variables.
Will add `i` terms to the objective function, where `i` is
the number of intervals in the simulation.
Will also add constraints to the linear program.
Attributes:
function: The function to apply to the two variables.
a: Left hand side variable.
b: Right hand side variable.
M: Big-M constant used in the constraints.
interval_data: The interval data variable, such as
`electricity_prices` or `gas_prices`.
coefficient: A constant multipler for the term.
type: The type of the term.
"""
function: typing.Literal["max_two_variables", "min_two_variables"]
a: Term | float
b: Term | float
M: float
interval_data: str | None = None
coefficient: float = 1.0
type: typing.Literal["complex"] = "complex"
@dataclasses.dataclass
class FunctionTermManyVariables:
"""A function term for constraining many variables.
This will add 1 term to the objective function.
Will also add constraints to the linear program.
Attributes:
function: Function to apply to the many variables.
variables: Linear program variables to apply the function over.
M: Big-M constant used in the constraints.
interval_data: The interval data variable, such as
`electricity_prices` or `gas_prices`.
constant: A constant to include in the function alongside
the linear program variables.
coefficient: A constant multipler for the term.
type: The type of the term.
"""
function: typing.Literal["max_many_variables", "min_many_variables"]
variables: Term
M: float
constant: float = 0.0
coefficient: float = 1.0
type: typing.Literal["complex"] = "complex"
Currently the library includes four complex terms, which allow adding minimum or maximum constraints on collections of linear program variables and floats:
Function | Number of Linear Variables | Number of Floats | Terms Added to Objective Function |
---|---|---|---|
min_two_variables |
1 or 2 | 0 or 1 | Interval index length |
max_two_variables |
1 or 2 | 0 or 1 | Interval index length |
max_many_variables |
Interval index length | 0 or 1 | 1 |
min_many_variables |
Interval index length | 0 or 1 | 1 |
Examples
Maximum Demand Charge
A common incentive for many sites is a maximum demand charge, where a site will incur a cost based on the maximum site import over a length of time (commonly a month).
We can model this using the max_many_variables
function term, which will add a single term to the objective function that is the maximum of many linear program variables and a user supplied constant.
We can demonstrate this by using an example of a site with a variable electric load, with a peak of 50 MW.
We can first optimize the site with an objective function that does not include a demand charge:
import energypylinear as epl
electric_load_mwh = [30.0, 50.0, 10.0]
electricity_prices = [0.0, 0.0, 0.0]
gas_prices = 20
site = epl.Site(
assets=[
epl.CHP(
electric_efficiency_pct=1.0,
electric_power_max_mw=50,
electric_power_min_mw=0,
)
],
gas_prices=gas_prices,
electricity_prices=electricity_prices,
electric_load_mwh=electric_load_mwh,
)
no_demand_charge_simulation = site.optimize(
verbose=3,
objective={
"terms": [
{
"asset_type": "site",
"variable": "import_power_mwh",
"interval_data": "electricity_prices",
},
{
"asset_type": "site",
"variable": "export_power_mwh",
"interval_data": "electricity_prices",
"coefficient": -1,
},
{
"asset_type": "*",
"variable": "gas_consumption_mwh",
"interval_data": "gas_prices",
},
]
},
)
As expected for a site with low electricity prices, this CHP does not generate electricity in any interval:
Let's now optimize the site with a demand charge.
This demand charge has a minimum of 40 MW, and a rate of 200 $/MWh:
demand_charge_simulation = site.optimize(
verbose=3,
objective={
"terms": [
{
"asset_type": "site",
"variable": "import_power_mwh",
"interval_data": "electricity_prices",
},
{
"asset_type": "site",
"variable": "export_power_mwh",
"interval_data": "electricity_prices",
"coefficient": -1,
},
{
"asset_type": "*",
"variable": "gas_consumption_mwh",
"interval_data": "gas_prices",
},
{
"function": "max_many_variables",
"variables": {
"asset_type": "site",
"variable": "import_power_mwh",
},
"constant": 40,
"coefficient": 200,
"M": max(electric_load_mwh) * 10
},
]
},
)
Now we see that the CHP generator has generated in the one interval that had a demand higher than our demand charge minimum of 40:
print(
demand_charge_simulation.results[
["site-electric_load_mwh", "chp-electric_generation_mwh"]
]
)
If we re-run this simulation with a lower demand charge minimum, our CHP generator is now incentivized to generate in other intervals:
demand_charge_simulation = site.optimize(
verbose=3,
objective={
"terms": [
{
"asset_type": "site",
"variable": "import_power_mwh",
"interval_data": "electricity_prices",
},
{
"asset_type": "site",
"variable": "export_power_mwh",
"interval_data": "electricity_prices",
"coefficient": -1,
},
{
"asset_type": "*",
"variable": "gas_consumption_mwh",
"interval_data": "gas_prices",
},
{
"function": "max_many_variables",
"variables": {
"asset_type": "site",
"variable": "import_power_mwh",
},
"constant": 20,
"coefficient": 200,
"M": max(electric_load_mwh) * 10,
},
]
},
)
print(
demand_charge_simulation.results[
["site-electric_load_mwh", "chp-electric_generation_mwh"]
]
)
Minimum Export Incentive
Above we looked at a function term that took the maximum across many linear program variables at once using the max_many_variables
function term, which results in one term being added to the objective function.
Another type of function term included in energypylinear
is the min_two_variables
function term, which adds one term to the objective function for each interval in the linear program.
The term will represent the minimum of either a linear program variable and another linear program variable, or a linear program variable and a user supplied constant.
To demonstrate this we can look at a site where we want to incentivize a minimum export of 15 MWh or greater in each interval. The site will receive the maximum benefit when exporting 15 MW or more, and less benefit when exporting less than 15 MWh. There is no incentive to export more than 15 MWh.
Let's first setup a site with a CHP system:
import energypylinear as epl
electric_load_mwh = [30.0, 50.0, 10.0]
electricity_prices = [0.0, 0.0, 0.0]
gas_prices = 20
site = epl.Site(
assets=[
epl.CHP(
electric_efficiency_pct=1.0,
electric_power_max_mw=50,
electric_power_min_mw=0,
)
],
gas_prices=gas_prices,
electricity_prices=electricity_prices,
electric_load_mwh=electric_load_mwh,
)
Let's optimize the site without a minimum export incentive:
no_export_incentive_simulation = site.optimize(
verbose=3,
objective={
"terms": [
{
"asset_type": "site",
"variable": "import_power_mwh",
"interval_data": "electricity_prices",
},
{
"asset_type": "site",
"variable": "export_power_mwh",
"interval_data": "electricity_prices",
"coefficient": -1,
},
{
"asset_type": "*",
"variable": "gas_consumption_mwh",
"interval_data": "gas_prices",
},
]
},
)
print(no_export_incentive_simulation.results['chp-electric_generation_mwh'])
As expected, our CHP system doesn't generate:
Let's now add a minimum export incentive using the min_two_variables
function term:
no_export_incentive_simulation = site.optimize(
verbose=3,
objective={
"terms": [
{
"asset_type": "site",
"variable": "import_power_mwh",
"interval_data": "electricity_prices",
},
{
"asset_type": "site",
"variable": "export_power_mwh",
"interval_data": "electricity_prices",
"coefficient": -1,
},
{
"asset_type": "*",
"variable": "gas_consumption_mwh",
"interval_data": "gas_prices",
},
{
"function": "min_two_variables",
"a": {
"asset_type": "site",
"variable": "export_power_mwh",
},
"b": 15,
"coefficient": -200,
"M": max(electric_load_mwh) * 10
},
]
},
)
print(
no_export_incentive_simulation.results[
[
"site-electric_load_mwh",
"site-export_power_mwh",
"chp-electric_generation_mwh",
]
]
)
As expected, our CHP system generates to export a minimum of 15 MWh where possible:
site-electric_load_mwh site-export_power_mwh chp-electric_generation_mwh
0 30.0 15.0 45.0
1 50.0 0.0 0.0
2 10.0 15.0 25.0
Our asset does not generate in the second interval because the site demand is too high to allow the asset to export any electricity.