diff --git a/docs/docs/how-to/battery-degradation.md b/docs/docs/how-to/battery-degradation.md new file mode 100644 index 0000000..4e055ce --- /dev/null +++ b/docs/docs/how-to/battery-degradation.md @@ -0,0 +1,189 @@ +Battery degradation is where battery performance reduces with time or battery use. + +The performance of the battery is defined by the parameters of power (MW), capacity (MWh) and efficiency (%). + +`energypylinear` does not model battery degradation within a single simulation - degradation can be handled by splitting up the battery lifetime into multiple simulations. + +## Modelling a Single Year in Monthly Chunks + +To handle battery degradation over a year, we will split the year into 12 months and run a simulation for each month: + + +```python +import numpy as np +import pandas as pd + +import energypylinear as epl + +np.random.seed(42) +days = 35 +dataset = pd.DataFrame({ + "timestamp": pd.date_range("2021-01-01", periods=days * 24, freq="h"), + "prices": np.random.normal(-1000, 1000, days * 24) + 100 +}) +battery_params = { + "power_mw": 4, + "capacity_mwh": 10, + "efficiency_pct": 0.9, + "freq_mins": 60 +} + +results = [] +objs = [] +for month, group in dataset.groupby(dataset['timestamp'].dt.month): + print(f"Month {month}") + battery = epl.Battery(electricity_prices=group['prices'], **battery_params) + simulation = battery.optimize(verbose=3) + results.append(simulation.results) + objs.append(simulation.status.objective) + +year = pd.concat(results) +assert year.shape[0] == days * 24 +account = epl.get_accounts(year, verbose=3) +np.testing.assert_allclose(account.profit, -1 * sum(objs)) +print(account) +``` + +``` +Month 1 +Month 2 + +``` + +The results above do not include any battery degradation - battery parameters are the same at the start of each month. + +## Modelling Degradation + +To model degradation, we need to take a view on how our battery parameters change over time. + +For our simulation, we will model: + +- battery power decays by 0.1 MW for each 150 MWh of battery charge, +- battery capacity decays by 0.1 MWh for each 150 MWh of battery charge, +- battery efficiency decays by 0.1% over 30 days. + + +```python +def get_battery_params(cumulative_charge_mwh: float = 0, cumulative_days: float = 0) -> dict: + """Get degraded battery parameters based on usage and time.""" + power_decay_mw_per_mwh = 0.1 / 150 + capacity_decay_mwh_per_mwh = 0.1 / 150 + efficiency_decay_pct_per_day = 0.1 / 30 + return { + "power_mw": 4 - power_decay_mw_per_mwh * cumulative_charge_mwh, + "capacity_mwh": 10 - capacity_decay_mwh_per_mwh * cumulative_charge_mwh, + "efficiency_pct": 0.9 - efficiency_decay_pct_per_day * cumulative_days, + "freq_mins": 60 + } +``` + +For a fresh battery, our battery parameters are: + + +```python +print(get_battery_params()) +``` + +``` +{'power_mw': 4.0, 'capacity_mwh': 10.0, 'efficiency_pct': 0.9, 'freq_mins': 60} +``` + +For a battery that has been charged with 300 MWh over 60 days, our battery parameters are: + + +```python +print(get_battery_params(cumulative_charge_mwh=300, cumulative_days=60)) +``` + +``` +{'power_mw': 3.8, 'capacity_mwh': 9.8, 'efficiency_pct': 0.7, 'freq_mins': 60} +``` + +## Modelling a Single Year in Monthly Chunks with Degradation + +We can include our battery degradation model in our simulation by keeping track of our battery usage and updating the battery parameters at the start of each month: + + +```python +import collections + +results = [] +cumulative = collections.defaultdict(float) +for month, group in dataset.groupby(dataset['timestamp'].dt.month): + battery_params = get_battery_params( + cumulative_charge_mwh=cumulative['charge_mwh'], + cumulative_days=cumulative['days'] + ) + print(f"Month: {month}, Battery Params: {battery_params}") + battery = epl.Battery(electricity_prices=group['prices'], **battery_params) + simulation = battery.optimize(verbose=3) + results.append(simulation.results) + cumulative['charge_mwh'] += simulation.results['battery-electric_charge_mwh'].sum() + cumulative['days'] += group.shape[0] / 24 + +year = pd.concat(results) +assert year.shape[0] == days * 24 +account = epl.get_accounts(year, verbose=3) +print(account) +``` + +``` +Month: 1, Battery Params: {'power_mw': 4.0, 'capacity_mwh': 10.0, 'efficiency_pct': 0.9, 'freq_mins': 60} +Month: 2, Battery Params: {'power_mw': 3.0663703705399996, 'capacity_mwh': 9.06637037054, 'efficiency_pct': 0.7966666666666666, 'freq_mins': 60} + +``` + +## Full Example + +```python +import collections + +import numpy as np +import pandas as pd + +import energypylinear as epl + +def get_battery_params(cumulative_charge_mwh: float = 0, cumulative_days: float = 0) -> dict: + """Get degraded battery parameters based on usage and time.""" + power_decay_mw_per_mwh = 0.1 / 150 + capacity_decay_mwh_per_mwh = 0.1 / 150 + efficiency_decay_pct_per_day = 0.1 / 30 + return { + "power_mw": 4 - power_decay_mw_per_mwh * cumulative_charge_mwh, + "capacity_mwh": 10 - capacity_decay_mwh_per_mwh * cumulative_charge_mwh, + "efficiency_pct": 0.9 - efficiency_decay_pct_per_day * cumulative_days, + "freq_mins": 60 + } + +np.random.seed(42) +days = 35 +dataset = pd.DataFrame({ + "timestamp": pd.date_range("2021-01-01", periods=days * 24, freq="h"), + "prices": np.random.normal(-1000, 1000, days * 24) + 100 +}) + +results = [] +cumulative = collections.defaultdict(float) +for month, group in dataset.groupby(dataset['timestamp'].dt.month): + battery_params = get_battery_params( + cumulative_charge_mwh=cumulative['charge_mwh'], + cumulative_days=cumulative['days'] + ) + print(f"Month: {month}, Battery Params: {battery_params}") + battery = epl.Battery(electricity_prices=group['prices'], **battery_params) + simulation = battery.optimize(verbose=3) + results.append(simulation.results) + cumulative['charge_mwh'] += simulation.results['battery-electric_charge_mwh'].sum() + cumulative['days'] += group.shape[0] / 24 + +year = pd.concat(results) +assert year.shape[0] == days * 24 +account = epl.get_accounts(year, verbose=3) +print(account) +``` + +``` +Month: 1, Battery Params: {'power_mw': 4.0, 'capacity_mwh': 10.0, 'efficiency_pct': 0.9, 'freq_mins': 60} +Month: 2, Battery Params: {'power_mw': 3.0663703705399996, 'capacity_mwh': 9.06637037054, 'efficiency_pct': 0.7966666666666666, 'freq_mins': 60} + +``` diff --git a/docs/docs/how-to/price-carbon.md b/docs/docs/how-to/price-carbon.md index 3e72cb9..61183fb 100644 --- a/docs/docs/how-to/price-carbon.md +++ b/docs/docs/how-to/price-carbon.md @@ -1,4 +1,4 @@ -`energypylinear` has the ability to optimize for both price and carbon as optimization objectives. +`energypylinear` can optimize for both price and carbon as optimization objectives. This ability comes from two things - an objective function, which can be either for price or carbon, along with accounting of both price and carbon emissions.