Day-ahead FCR optimization#
Here we will walk through an example of optimizing a battery energy storage system (BESS) for frequency containment reserve (FCR)
in the German market using the DEBatteryOptimizer.
In [1]: from battery_optimizer.de.battery_optimization import DEBatteryOptimizer
In [2]: problem = DEBatteryOptimizer(**request);
For the purpose of this tutorial, we have prepared a sample optimization request that describes the following asset and grid connection:
In [3]: request['asset']['battery_parameters']
Out[3]:
{'min_soe': 0.0,
'max_soe': 1.0,
'charging_efficiency': 0.96,
'discharging_efficiency': 0.96,
'max_charging_power_kw': 15000.0,
'max_discharging_power_kw': 15000.0,
'capacity_kwh': 30000.0,
'max_daily_cycle': [{'date': '2023-01-10T00:00:00', 'max_daily_cycle': 2.0}],
'daily_cycle_limit_applies': True,
'daily_cycle_limit_penalty': True,
'grid_connection_export_kw': 15000.0,
'grid_connection_import_kw': 15000.0,
'power_swing_limit': {'applies': False, 'value_kw': 0.0}}
While our optimizer sets sensible FCRParameters defaults, for this example we will explicitly define them as follows:
In [4]: request['asset']['strategy_optimization']['fcr_parameters'] = {
...: 'delivery_duration_sec': 900,
...: 'delivery_duration_buffer_perc': 4. / 3.,
...: 'max_marketable_power_factor': 1.25,
...: 'prequalified_power_kW': 15_000.,
...: }
...:
In this example, we assume no prior market commitments:
In [5]: problem.asset.battery_marketed.sum()
Out[5]:
bought_intraday_kw 0.0
sold_intraday_kw 0.0
sold_epexIDA1_kw 0.0
bought_epexIDA1_kw 0.0
sold_epexDA_kw 0.0
bought_epexDA_kw 0.0
fcr_kw 0.0
afrr_capacity_pos_kw 0.0
afrr_capacity_neg_kw 0.0
afrr_energy_pos_kw 0.0
afrr_energy_neg_kw 0.0
dtype: float64
To keep things simple, let us set a constant price for FCR:
In [6]: price_forecast = problem.asset.price_forecast.copy()
In [7]: price_forecast['fcr'] = 100.
In [8]: request['asset']['price_forecast'] = price_forecast.reset_index().to_dict('records')
Note that we assume this equals 100. Euro / MW / settlement period as is described here: fcr.
For ancillary services such as FCR, it is impossible to know for certain how the state of energy (SoE) of our battery will
evolve over time, as this depends on the actual activation of the reserve capacity we provide to the grid operator.
The optimizer allows you to provide, optionally, your expectation of activation in the form of energy throughput profiles,
FlexMarketsEnergyThroughput.
For this simple example, let us assume zero expected throughput:
In [9]: throughput = problem.asset.flex_markets_energy_throughput.copy()
In [10]: throughput[['fcr_discharge_MWh_per_MW', 'fcr_charge_MWh_per_MW']] = 0.
In [11]: request['asset']['flex_markets_energy_throughput'] = throughput.reset_index().to_dict('records')
Let us indicate that we only want to optimize for FCR:
In [12]: request['asset']['strategy_optimization']['markets'] = ['fcr']
Lastly, let us set the initial SoE of our asset at the beginning of the optimization horizon to 50%:
In [13]: request['asset']['battery_initial_conditions']['initial_soe'] = 0.5
With that, we can re-initialize the optimizer with our updated request and run the optimization:
In [14]: problem = DEBatteryOptimizer(**request)
In [15]: problem.solve_optimization_problem()
In [16]: result = DEResult.create_dataframe_from_solved_problem(problem)
And these are the FCR positions the optimizer proposes:
In [17]: fig, (price_axis, power_axis) = plt.subplots(2, 1, figsize=(12, 8), sharex=True, gridspec_kw={'height_ratios': [1, 3]});
In [18]: fig.subplots_adjust(hspace=0.05);
In [19]: price_axis.plot(price_forecast.index, price_forecast['fcr'], label='FCR Price / EUR/MW', color='tab:grey');
In [20]: price_axis.set_ylabel('Price (EUR/MW)', fontsize='small');
In [21]: power_axis.bar(result.index, result['fcr_volume_MW'], width=0.01, label='FCR / MW', color='tab:blue', alpha=0.6);
In [22]: power_axis.bar(result.index, -result['fcr_volume_MW'], width=0.01, label='FCR / MW', color='tab:blue', alpha=0.6);
In [23]: power_axis.axhline(0, color='black', linewidth=1.5, linestyle='--');
In [24]: power_axis.set_ylabel('Power (MW)', fontsize='small');
In [25]: energy_axis = power_axis.twinx();
In [26]: energy_axis.plot(result.index, result['soe_target_kwh'] / 1000., label='State of Charge / MWh', color='tab:red');
In [27]: energy_axis.set_ylabel('State of Charge (MWh)', fontsize='small');
In [28]: fig.autofmt_xdate();
In [29]: plt.tight_layout();
In [30]: plt.show();
| fcr_volume_MW | soe_target_kwh | |
|---|---|---|
| time_from | ||
| 2023-01-09 23:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-09 23:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-09 23:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-09 23:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 00:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 00:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 00:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 00:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 01:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 01:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 01:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 01:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 02:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 02:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 02:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 02:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 03:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 03:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 03:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 03:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 04:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 04:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 04:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 04:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 05:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 05:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 05:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 05:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 06:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 06:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 06:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 06:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 07:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 07:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 07:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 07:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 08:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 08:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 08:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 08:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 09:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 09:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 09:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 09:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 10:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 10:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 10:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 10:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 11:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 11:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 11:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 11:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 12:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 12:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 12:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 12:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 13:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 13:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 13:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 13:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 14:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 14:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 14:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 14:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 15:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 15:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 15:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 15:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 16:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 16:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 16:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 16:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 17:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 17:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 17:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 17:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 18:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 18:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 18:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 18:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 19:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 19:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 19:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 19:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 20:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 20:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 20:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 20:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 21:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 21:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 21:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 21:45:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 22:00:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 22:15:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 22:30:00+00:00 | 12.0 | 15000.0 |
| 2023-01-10 22:45:00+00:00 | 12.0 | 15000.0 |