battery_optimizer.uk.battery_optimization#

Classes

UKBatteryOptimizer(*args, request_id[, ...])

class battery_optimizer.uk.battery_optimization.UKBatteryOptimizer(*args, request_id, country=Country.UK, user_id, request_creation_time, elastic_filter=False, already_auctioned_as_constants=True, asset, verbose=False, epsilon=1e-06, commercial_objective=UKCommercialObjective(commercial_columns=['revenue_PV', 'dc_high_revenue', 'dc_low_revenue', 'commodity_revenue', 'third_party_revenue', 'third_party_cost', 'commodity_cost']), multiplier_buy_sell_exceed_limit=10.0, upper_bound_risk_increment=None, imbalance=Imbalance(imbalance_cost_per_MWh=1000000.0, min_imbalance_cost_per_MWh=100000.0, imbalance_cost_shaping=True), solve_optimization_problem_max_seconds=90.0, intraday_strategy=IntradayStrategy(name=<IntradayStrategyName.vwap: 'vwap'>, delivery_length=<IntradayDeliveryLength.half_hour: 'HalfHour'>, tradeable_time_window_minutes=NaT), intraday_result_aggregation=IntradayResultAggregation(name=<IntradayResultAggregationName.minmax: 'minmax'>, no_buckets=1), objective=UKObjective(name=<ObjectiveName.pnl: 'pnl'>, frozen_window_plus_buffer=Timedelta('0 days 01:30:00'), melmil_optimization_horizon=Timedelta('0 days 01:00:00')), solver_settings=SolverSettings(gapRel=0.01), do_input_for_intraday_bucketing_correct_bucket=True, elastic_soe_constraints=False, global_max_charging_power_kw=None, global_max_discharging_power_kw=None, feature_flags=UKFeatureFlags(return_pickled_optimizer=False, da_ds_rounding_method='da_rerun'), use_case=UseCase.BESS, iterative_commodity_and_ds=True, keep_n2ex_constant_hourly=True, default_penalty_for_discharge_during_dc=1000000.0, apply_penalty_for_discharge_during_dc=False, balancing_mechanism=BalancingMechanism(activation_time_minutes=Timedelta('0 days 00:32:00'), activation_ratio=1.0))#

Bases: UKRequest, BatteryOptimizer

Parameters:
add_basepoint_change_after_end_of_dc_constraint(problem, variables)#

Purpose: This method aims to update the optimization problem by adding constraints related to basepoint changes immediately after the end of Dynamic Containment (DC) operations. The constraints ensure that power basepoint changes after DC operations adhere to a specified shape (trapezium) based on the marketed DC power of the preceding settlement period.

In the worst case we ended DC with no charge/discharge action and then in the SP right after DC we want to charge. In this SP we have to follow the baseline ramping rules, so the maximum power/energy that can be delivered is a trapezium: power starts at 0 and increases following the ramp rate up to the maximum marketed volume in the previous SP and then stays at that plateau.

Parameters: * problem: The optimization problem to which constraints are being added. * variables: DataFrame containing all the variables associated with the optimization problem.

Return: * None. The function modifies the problem in-place.

Key components: 1. Calculating the limit after DCL ends: This section computes the allowable change in charge after the end of DCL operations. The constraint is based on the maximum charging power, the end of DCL variable, and the trapezium-shaped basepoint change limit, considering the marketed DC power of the preceding settlement period.

2. Calculating the limit after DCH ends: Similarly, this section computes the allowable change in discharge after the end of DCH operations. The constraint is based on the maximum discharging power, the end of DCH variable, and the trapezium-shaped basepoint change limit, considering the marketed DC power of the preceding settlement period.

add_basepoint_change_after_initial_dc_constraint(problem, variables)#
add_basepoint_change_before_start_of_dc_constraint(problem, variables)#

Purpose: This method aims to update the optimization problem by adding constraints related to the basepoint changes before the start of Dynamic Containment (DC) operations. It ensures that the changes in the basepoint during the start of DC adhere to a triangle ramp rate shape. It means that it will prevent the model from charging or discharging too much the battery just before start dcl and dch.

Why? Because starting with this SP, the battery must follow the ramping rules for DC. By forcing a triangle shape it means that we consider that the baseline will start from 0, increase up to a maximum until minute 15 and then go back to 0 to start DC. The computation of the baseline is done live based on the real state of the asset and the target SoC computed on the previous day optimization. By using a triangle shape we bring the chances really high that the baseline will be able to charge/discharge exactly as expected by the optimization.

Parameters:
  • problem: The optimization problem to which constraints are being added.

  • variables: DataFrame containing all the variables associated with the optimization problem.

Return:
  • None. The function modifies the problem in-place.

Key components: 1. Calculating the limit for DCL start: This section computes the allowable change in charge for the start of DCL operations. It uses the maximum charging power, the start of DCL variable, and the triangle-shaped basepoint change limit to determine the permissible charging power change before DCL begins.

2. Calculating the limit for DCH start: Similarly, this section computes the allowable change in discharge for the start of DCH operations. It uses the maximum discharging power, the start of DCH variable, and the triangle-shaped basepoint change limit to determine the permissible discharging power change before DCH begins.

add_basepoint_change_during_dc_constraint(problem, variables)#

Purpose: This method updates the optimization problem by adding constraints related to basepoint changes during Dynamic Containment (DC) operations. The constraints ensure that changes in power basepoints adhere to specified shapes (trapezium or triangle) based on the marketed DC power in the surrounding settlement periods.

When we charge or discharge standalone (so only in SP and not in SP-1 or SP+1) we allow to charge based on a triangle shape: basepoint will go from 0 and increase following the DC ramp rate up to minute 15 and then go down.

When we are not in a standalone scenario then we allow a trapezium shape: the basepoint will go from 0 to the marketed volume (or the opposite). And in the next SP it can go to 0. This doesn’t guarantee, but increases the probability that live a baseline will be computable that: * charges/discharges as much energy as expected by the optimization * while fulfilling the baseline ramping rules for DC.

Parameters:
  • problem: The optimization problem to which constraints are being added.

  • variables: DataFrame containing all the variables associated with the optimization problem.

Return:
  • None. The function modifies the problem in-place.

Key components: 1. Iterating over Charge and Discharge Scenarios: The method processes two scenarios: charging (associated with DCL) and discharging (associated with DCH). For each scenario, constraints are added based on the marketed DC power in the current, previous, and next settlement periods.

2. Upper Bound Based on Current Settlement Period: This constraint ensures that the change in power (either charge or discharge) in the current settlement period doesn’t exceed * the maximum change dictated by the trapezium shape * and the marketed DC power in the same period.

3. Upper Bound Based on Previous Settlement Period: Similar to the above, but the constraint ensures that the change in power (either charge or discharge) in the current settlement period doesn’t exceed * the maximum change dictated by the trapezium shape * and the marketed DC power in the previous period.

4. Upper Bound for Standalone Basepoint Changes: This set of constraints specifically caters to standalone basepoint changes: charging in SP but NOT in SP-1 or SP+1 / discharging in SP but NOT in SP-1 or SP+1. Constraints ensure that the change in power in the current settlement period doesn’t exceed the maximum change dictated by the triangle shape and the marketed DC power in the surrounding (previous and next) settlement periods.

add_constant_n2ex_power_per_hour_constraint(problem, variables, n2ex_variable)#
add_constraint_via_expression_series_to_problem(problem, variables, name_str, expression_series, relation_symbol, const_value=0, is_elastic=False)#

this is an alternative method to add constraints to problem. This method offers a convienient/standard way to add some complicated constraints to a problem.

This applys to situations, when you need to add some equations with one side a constant value and the other side as a pd.Series For instance, it can be commonly used for power/energy balance equation.

add_constraints(problem, variables, variables_global)#
add_daily_cycle_limit_constraint(problem, variables, variables_special)#
static add_pairwise_constant_n2ex_power_per_hour_constraint(rows, problem, n2ex_variable)#
add_power_swing_constraint(problem, variables, power_swing_kW)#

Purpose: This method updates the optimization problem by adding constraints related to power swings. Power swing constraints are implemented to ensure that the difference between certain power-related variables doesn’t exceed a specified threshold (power_swing_kW).

Parameters:
  • problem: The optimization problem to which constraints are being added.

  • variables: DataFrame containing all the variables associated with the optimization problem.

  • power_swing_kW: The threshold for the allowed power swing.

Return:
  • None. The function modifies the problem in-place.

Key components: 1. Power Swing for DC both:

This section calculates the power swing when both DCL and DCH are active. The difference between contracted quantities of DCL and DCH shouldn’t exceed the power_swing_kW value.

  1. Power Swing when doing DCL and Charging:

    This section calculates the power swing when the system is doing DCL and charging. The difference between DCL contracted quantity and the charging power shouldn’t exceed the power_swing_kW value.

  2. Power Swing when doing DCH and Discharging:

    This section calculates the power swing when the system is doing DCH and discharging. The difference between DCH contracted quantity and the discharging power shouldn’t exceed the power_swing_kW value.

  3. Power Swing for DC both with Charging and Discharging:

    This section calculates the power swing when both DCL and DCH are active, taking into account both charging and discharging activities.

5. Power Swing for DCL only: This section calculates the power swing when only DCL is active.

The method effectively ensures that the power swings in various scenarios are within acceptable limits as defined by the power_swing_kW value.

add_soc_during_dynamic_service_constraint(problem, variables, soc_name, soc_df)#

Purpose: This method aims to update the optimization problem by adding constraints related to the State of Charge (SoC) during Dynamic Containment (DC) operations, ensuring that the SoC lies within specific bounds during DCL (Dynamic Containment Low) and DCH (Dynamic Containment High) operations.

Parameters:
  • problem: The optimization problem to which constraints are being added.

  • variables: DataFrame containing all the variables associated with the optimization problem.

  • soc_df: DataFrame containing the state of charge (SoC) values.

Return:
  • None. The function modifies the problem in-place.

Key components: 1. Constraints for DCL and DCH: This section computes the SoC values when either DCL or DCH is active.

  • The SoC should be greater than the minimum physical SoC value and enough to deliver DCL during 15min during DCL operations

  • and the SoC less than the maximum physical SoC value and enough to deliver DCH during 15min during DCH operations

  1. Lookahead Constraints:
    • These constraints ensure that the SoC at the end of a settlement period is sufficient to cater to DCL/DCH operations in the next period.

    This involves a one-step lookahead in the soc_df DataFrame.

  2. Initial SoE Constraints:
    • These constraints ensure that the DCL/DCH operations in the first timestep of the simulation do not exceed the initial state of energy (SoE) of the battery.

create_PV_constraints(problem, variables)#
create_battery_max_power_constraints(problem, variables)#
create_boolean_variables_and_constraints(problem, variables)#
create_country_specific_constraints(problem, variables)#
create_dynamic_service_unavailability_constraints(problem, variables)#
create_dynamic_services_headroom_constraints(problem, variables)#
create_grid_connection_constraints(problem, variables)#
create_soc_scenario_maximal_mil_boa(soc, variables)#
create_soc_scenarios_maximal_mel_boa(soc, variables)#
create_soe_constraints(problem, variables)#
create_wholesale_duration_constraints(problem, variables)#
property custom_lower_bounds: dict[str, dict]#
property custom_upper_bounds: dict[str, dict]#
property custom_variables: dict#
get_country_specific_objective(problem, variables, variables_global)#
get_dynamic_services_energy_throughput(timestamp, variables, side)#
get_dynamic_services_energy_throughput_high(timestamp, variables)#
get_dynamic_services_energy_throughput_low(timestamp, variables)#
get_dynamic_services_high_energy_throughput_buffer(timestamp, variables)#
Parameters:
  • timestamp (Timestamp)

  • variables (DataFrame)

Return type:

LpAffineExpression

get_dynamic_services_high_energy_throughput_buffer_v1(timestamp, variables)#

Compute the dynamic services high energy throughput buffer. We used to support a V2 of this buffer, hence the V1 in the name.

Let SPi be the current settlement period within its EFA block (1..8). Define:

ds_throughput_low(j) = expected DSL energy throughput in SPj / discharging_efficiency / capacity_kWh(SPj) ds_throughput_high(j) = expected DSH energy throughput in SPj * charging_efficiency / capacity_kWh(SPj) ds_rev_high(j) = maximal total DSL volume in SPj * charging_efficiency / capacity_kWh(SPj)

Cases:
SP4..SP8: Buffer = [
  • (ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) +

(ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

] SP1: Buffer = [

rev_factor * ds_rev_high(i-1) - (ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) + (ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

] SP2: Buffer = [

rev_factor * ds_rev_high(i-2) - (ds_throughput_high(i-4) + ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) + (ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

] SP3: Buffer = [

rev_factor * ds_rev_high(i-3) - (ds_throughput_high(i-5) + ds_throughput_high(i-4) + ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) + (ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

]

Return type:

LpAffineExpression

get_dynamic_services_low_energy_throughput_buffer(timestamp, variables)#
Parameters:
  • timestamp (Timestamp)

  • variables (DataFrame)

Return type:

LpAffineExpression

get_dynamic_services_low_energy_throughput_buffer_v1(timestamp, variables)#

Compute the dynamic services low energy throughput buffer. We used to support a V2 of this buffer, hence the V1 in the name.

Let SPi be the current settlement period within its EFA block (1..8). Define:

ds_throughput_low(j) = expected DSL energy throughput in SPj / discharging_efficiency / capacity_kWh(SPj) ds_throughput_high(j) = expected DSH energy throughput in SPj * charging_efficiency / capacity_kWh(SPj) ds_rev_low(j) = maximal total DSL volume in SPj / discharging_efficiency / capacity_kWh(SPj)

Cases:
SP4..SP8: Buffer = [

(ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) - (ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

] SP1: Buffer = [

rev_factor * ds_rev_low(i-1) + (ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) - (ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

] SP2: Buffer = [

rev_factor * ds_rev_low(i-2) + (ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) - (ds_throughput_low(i-4) + ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

] SP3: Buffer = [

rev_factor * ds_rev_low(i-3) + (ds_throughput_high(i-3) + ds_throughput_high(i-2) + ds_throughput_high(i-1)) - (ds_throughput_low(i-5) + ds_throughput_low(i-4) + ds_throughput_low(i-3) + ds_throughput_low(i-2) + ds_throughput_low(i-1))

]

Return type:

LpAffineExpression

get_dynamic_services_rev(timestamp, variables, side)#
get_dynamic_services_rev_high(timestamp, variables)#
get_dynamic_services_rev_low(timestamp, variables)#
get_objective_mel(variables)#
get_objective_mil(variables)#
get_objective_pnl(variables, problem_solved=False, markets=None)#

Purpose: This method formulates the objective function for the optimization problem, which balances various revenue streams and costs associated with battery operations. This includes revenues from energy markets and Dynamic Containment (DC), as well as costs associated with charging from the grid and penalties for discharging during DC.

Parameters:
  • variables: DataFrame containing all the variables associated with the optimization problem.

  • default_penalty_for_discharge_during_dc (this has been moved to the Request class): Default penalty value applied for discharging during DC if no other penalty is specified. The default value is set to a huge number.

This penalty is a simple solution to prevent the algorithm to overoptimize and cycle during DC with charging/discharging. During DC the ramp rate rule applies and it is difficult to deliver exactly the volumes required. An improvement could be considered by looking at having a threshold so that above a certain profit then it is worth cycling during dc. The charging is not penalized additionally. It costs to charge so the model would only charge if it makes sense to discharge right after DC. * markets: Iterable of market names that we want to compute the objective for - may differ from self.asset.strategy_optimization.markets

e.g. when doing iterative DC optimization (i.e. DC + commodity followed by DC-only).

.
Return:
  • Returns a DataFrame (objective) containing the objective function terms for each time period in the optimization.

Key components: 1. Adjusting for Customer Perspective: If the optimization perspective is from a customer’s standpoint, then the energy cost is adjusted to the higher of the market energy price or the contractual energy cost.

2. Revenue from PV: Revenues from Photovoltaic (PV) generation are considered as PV exported to the grid times * N2EX prices + DUOS_disch (when optimizing on N2EX) * EPEX 30min prices + DUOS_dischs (when optimizing on EPEX) Are disreguarded (considered as 0) when optimizing on DC only

3. Revenues from DC and Energy Markets from the battery: The method calculates revenues from participating in DC as well as revenues from selling energy in various energy markets. The revenue in the energy market is calculated as volume * (market price + DUOs_disch)

4. Cost of Charging from the Grid: The costs associated with charging the battery from the grid are calculated based on energy costs and any additional commercial costs. The commercial costs are defined as (BSUoS+ CfD+ CM_Levy) * TLM * LLF + (‘TNuOS_charg’+ ‘AAHEDC’+ ‘Mgmt_Fee’+ ‘Imbal_risk’+ ‘RCRC’+ ‘Elexon’) * LLF + (‘RO’+ ‘FiT’+ ‘DUoS_ch’)

5. Penalty for Discharging during DC: A penalty is applied if the battery discharges during DC operations. If no specific penalty is provided, a default value is used. This is to prevent discharging during DC. Otherwise the code would over optimize and use the power left for state of energy management to cycle the battery. As it is challenging during DC to reach the exact energy volume to charge/discharge requested by the optimization, the fastest approach was to prevent discharging. A smarter one could be to penalize discharging not by a randomly super high number but only a smaller one e.g. 100p/MWh in which case it would discharge only if “it is worth the effort”.

All the revenue streams and costs are aggregated to form the final objective function. The objective aims to maximize net revenues (revenues minus costs). This method provides a comprehensive objective function for the optimization problem, ensuring the economic operation of the battery while adhering to operational and market constraints.

get_penalized_objective_melmil(side, variables)#
get_soc_definitions(variables)#

In this functions the target soc level and the soc scenarios for MEL/MIL are defined.

For MEL/MIL, these are given as follows: For every settlement period sp, a BOA with variable MEL and MIL with maximum delivery time of the given delivery time is assumed. The BOA is simulated at the start settlement period resulting in the largest possible SoE-change ΔSoE(BOA,sp). The shift of the SoE-level due to the BOA remains constant for the frozen period.

After the frozen period, a possible recovery is taken into account. The maximum value to which the SoE level can be recovered is determined by the available power of each settlement period. For MEL, the maximum available power is defined by the difference of the maximum charging Power and the blocked power of dynamic services high, blocked buffer power of the dynamic services low and the blocked power by wholesale markets. The blocked power of the wholesale market is defined by the difference of sold energy and bought energy. For MIL, the maximum available power is defined by the difference of the maximum discharging Power and the blocked power of dynamic services low, blocked buffer power of the dynamic services high and the blocked power by wholesale markets. The blocked power of the wholesale market is defined by the difference of bought energy and sold energy.

The possible recovered energy of each settlement period accumulates with the previous ones. Considering both the sold and bought energy in the recovery process allows for unwinding later incoming trades resulting in higher MEL/MIL values. The final simulated SoE level throughout the entire optimization horizon is calculated by the original SoE level minus the BOA-SoE change, adding possible recovery after the frozen period. All simulated SoE levels must comply with existing (future) SoE restrictions. These restrictions ensure, that the calculated MEL/MIL values align with all future SoE constraints, based on the assumption of energy recovery whenever possible.

get_sp_minus_n(timestamp, n)#
get_target_soc(variables)#

This is a general method to get the SoC of each problem, self.initial_soe is the initial SoC (the SoC at the beginnging of the first timestamp). The energy delta is calculated based on the energy transferred into/out of the battery. Depends on the specific use cases, some parameters might be 0, hence, irrelavant.

get_target_soc_maximal_mel_start_of_sp(target_soc, sp_index, possible_recovered_mel)#
get_target_soc_maximal_mil_start_of_sp(target_soc, sp_index, possible_recovered)#
get_variables()#
get_variables_melmil()#
initialize_country_specific_attributes()#
model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'ignore', 'frozen': False, 'json_encoders': {<class 'datetime.datetime'>: <function BaseModel.<lambda>>, <class 'pandas._libs.tslibs.timedeltas.Timedelta'>: <function BaseModel.<lambda>>, <class 'pandas._libs.tslibs.timestamps.Timestamp'>: <function BaseModel.<lambda>>, <class 'pandas.core.frame.DataFrame'>: <function BaseModel.<lambda>>, <class 'pandas.core.series.Series'>: <function BaseModel.<lambda>>}, 'validate_default': True}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_post_init(context, /)#

We need to both initialize private attributes and call the user-defined model_post_init method.

Parameters:
Return type:

None

property variables_melmil#