Battery
The epl.Battery
asset is suitable for modelling an electric battery, such as a lithium-ion battery.
Assumptions
The battery charge rate is defined by power_mw
, which defines both the maximum rate of charge and discharge. discharge_power_mw
can be used to define a different rate of maximum discharge. Both the charge and discharge power are independent of the battery state of charge.
The battery storage capacity is defined by capacity_mwh
. This should be the capacity after taking into account any battery depth of discharge limits.
An efficiency penalty is applied to the battery charge energy, based on the efficiency_pct
parameter. No electricity is lost when discharging or during storage. The efficiency is independent of the battery state of charge.
initial_charge_mwh
and final_charge_mwh
control the battery state of charge at the start and end of the simulation. These can cause infeasible simulations if the battery is unable to charge or discharge enough to meet these constraints.
Use
You can optimize a single battery with epl.Battery
:
import energypylinear as epl
asset = epl.Battery(
power_mw=2,
discharge_power_mw=2,
capacity_mwh=4,
efficiency_pct=0.9,
electricity_prices=[100.0, 50, 200, -100, 0, 200, 100, -100],
freq_mins=60,
initial_charge_mwh=1,
final_charge_mwh=3,
name="battery"
)
simulation = asset.optimize()
assert all(
simulation.results.columns
== [
"site-import_power_mwh",
"site-export_power_mwh",
"site-electricity_prices",
"site-electricity_carbon_intensities",
"site-high_temperature_load_mwh",
"site-low_temperature_load_mwh",
"site-low_temperature_generation_mwh",
"site-gas_prices",
"site-electric_load_mwh",
"spill-electric_generation_mwh",
"spill-electric_load_mwh",
"spill-high_temperature_generation_mwh",
"spill-low_temperature_generation_mwh",
"spill-high_temperature_load_mwh",
"spill-low_temperature_load_mwh",
"spill-gas_consumption_mwh",
"battery-electric_charge_mwh",
"battery-electric_charge_binary",
"battery-electric_discharge_mwh",
"battery-electric_discharge_binary",
"battery-electric_loss_mwh",
"battery-electric_initial_charge_mwh",
"battery-electric_final_charge_mwh",
"total-electric_generation_mwh",
"total-electric_load_mwh",
"total-high_temperature_generation_mwh",
"total-low_temperature_generation_mwh",
"total-high_temperature_load_mwh",
"total-low_temperature_load_mwh",
"total-gas_consumption_mwh",
"total-electric_charge_mwh",
"total-electric_discharge_mwh",
"total-spills_mwh",
"total-electric_loss_mwh",
"site-electricity_balance_mwh",
]
)
Validation
A natural response when you get access to something someone else built is to wonder - does this work correctly?
This section will give you confidence in the implementation of the battery asset.
Price Dispatch Behaviour
Let's optimize a battery using a sequence of five prices.
We expect that the battery will charge when prices are low, and will discharge when prices are high.
In energypylinear
, a positive site electricity balance is importing, and a negative site electricity balance is exporting.
import energypylinear as epl
asset = epl.Battery(
electricity_prices=[10, -50, 200, -50, 200],
)
simulation = asset.optimize(verbose=3)
print(simulation.results[["site-electricity_prices", "site-electricity_balance_mwh"]])
site-electricity_prices site-electricity_balance_mwh
0 10.0 0.444444
1 -50.0 2.000000
2 200.0 -2.000000
3 -50.0 2.000000
4 200.0 -2.000000
As expected, the battery charges (with a site that is positive) when prices are low and discharges (with a negative site electricity balance) when prices are high.
Now let's change the prices and see how the dispatch changes:
import energypylinear as epl
asset = epl.Battery(
electricity_prices=[200, -50, -50, 200, 220],
)
simulation = asset.optimize(verbose=3)
print(simulation.results[["site-electricity_prices", "site-electricity_balance_mwh"]])
site-electricity_prices site-electricity_balance_mwh
0 200.0 0.0
1 -50.0 2.0
2 -50.0 2.0
3 200.0 -1.6
4 220.0 -2.0
As expected, the battery continues to charge during low electricity price intervals, and discharge when electricity prices are high.
Battery Energy Balance
Let's return to our original set of prices and check the energy balance of the battery:
import pandas as pd
import energypylinear as epl
pd.set_option("display.max_columns", 30)
pd.set_option("display.width", 400)
asset = epl.Battery(
electricity_prices=[10, -50, 200, -50, 200],
)
simulation = asset.optimize(verbose=3)
checks = epl.check_results(simulation.results, verbose=3)
balance = checks["electricity-balance"]
print(balance)
input accumulation output balance import generation export load charge discharge loss spills soc
0 0.444444 -0.444444 0.0 True 0.444444 0.0 0.0 0.0 0.444444 0.0 0.044444 0.0 0.0
1 2.000000 -2.000000 0.0 True 2.000000 0.0 0.0 0.0 2.000000 0.0 0.200000 0.0 0.0
2 0.000000 2.000000 2.0 True 0.000000 0.0 2.0 0.0 0.000000 2.0 0.000000 0.0 0.0
3 2.000000 -2.000000 0.0 True 2.000000 0.0 0.0 0.0 2.000000 0.0 0.200000 0.0 0.0
4 0.000000 2.000000 2.0 True 0.000000 0.0 2.0 0.0 0.000000 2.0 0.000000 0.0 0.0
In the first interval, we charge the battery with 0.444444 MWh
- 0.4 MWh
goes into increasing the battery state of charge from 0.0 MWh
to 0.4 MWh
, with the balance 0.044444 MWh
going to battery losses.
Battery Efficiency
We can validate the performance of the battery efficiency by checking the losses across different battery efficiencies:
import numpy as np
import pandas as pd
import energypylinear as epl
np.random.seed(42)
prices = np.random.uniform(-100, 100, 12) + 100
out = []
for efficiency_pct in [1.0, 0.9, 0.8]:
asset = epl.Battery(
power_mw=4,
capacity_mwh=10,
efficiency_pct=efficiency_pct,
electricity_prices=prices,
)
simulation = asset.optimize(
objective="price",
verbose=3
)
results = simulation.results
out.append(
{
"eff_pct": efficiency_pct,
"charge_mwh": results["battery-electric_charge_mwh"].sum(),
"discharge_mwh": results["battery-electric_discharge_mwh"].sum(),
"loss_mwh": results["battery-electric_loss_mwh"].sum(),
"prices_$_mwh": results["site-electricity_prices"].mean(),
"import_mwh": results["site-import_power_mwh"].sum(),
"objective": (results["site-import_power_mwh"] - results["site-export_power_mwh"] * results["site-electricity_prices"]).sum(),
}
)
print(pd.DataFrame(out))
eff_pct charge_mwh discharge_mwh loss_mwh prices_$_mwh import_mwh objective
0 1.0 18.000000 18.0 0.000000 103.197695 18.000000 -3018.344310
1 0.9 19.111111 17.2 1.911111 103.197695 19.111111 -2893.086854
2 0.8 20.000000 16.0 4.000000 103.197695 20.000000 -2719.962419
From the above we observe the following as efficiency decreases:
- a reduction in the amount discharged
discharge_mwh
, - an increase in battery losses
loss_mwh
, - a increase in the objective function, which is an increase in cost.
State of Charge & Power Ratings
We can demonstrate the state of charge and battery power settings by first optimizing a battery and showing it's plot:
import numpy as np
import energypylinear as epl
np.random.seed(42)
electricity_prices = np.random.normal(100, 10, 10).tolist()
asset = epl.Battery(power_mw=2, capacity_mwh=4, electricity_prices=electricity_prices)
results = asset.optimize()
asset.plot(results, path="./docs/docs/static/battery.png")
Takeaways:
- the battery state of charge is constrained between 0 and 4 MWh,
- the battery power rating is constrained between -2 and 2 MW,
- battery SOC starts empty and ends empty.
import numpy as np
import energypylinear as epl
np.random.seed(42)
asset = epl.Battery(
power_mw=4,
capacity_mwh=8,
electricity_prices=np.random.normal(100, 10, 10),
initial_charge_mwh=1.0,
final_charge_mwh=3.0
)
results = asset.optimize()
asset.plot(results, path="./docs/docs/static/battery-fast.png")
Takeaways:
- battery SOC starts at 1 MWh and ends at 3 MWh.