battery_optimizer.battery_optimization#
Classes
|
- class battery_optimizer.battery_optimization.BatteryOptimizer(*args, request_id, country, user_id, request_creation_time, elastic_filter=False, already_auctioned_as_constants=True, asset, verbose=False, epsilon=1e-06, commercial_objective=CommercialObjective(commercial_columns=['commodity_revenue', '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, intraday_result_aggregation=IntradayResultAggregation(name=<IntradayResultAggregationName.minmax: 'minmax'>, no_buckets=1), objective=Objective(name=<ObjectiveName.pnl: 'pnl'>), 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=FeatureFlags(return_pickled_optimizer=False))#
-
- Parameters:
request_id (str)
country (Country)
request_creation_time (datetime)
elastic_filter (bool)
already_auctioned_as_constants (bool)
asset (Asset)
verbose (bool)
epsilon (float)
commercial_objective (CommercialObjective)
multiplier_buy_sell_exceed_limit (Annotated[float, Ge(ge=0.0)])
upper_bound_risk_increment (float | dict | UpperBoundRiskIncrement | None)
imbalance (Imbalance)
solve_optimization_problem_max_seconds (float)
intraday_strategy (IntradayStrategy)
intraday_result_aggregation (IntradayResultAggregation)
objective (Objective)
solver_settings (SolverSettings)
do_input_for_intraday_bucketing_correct_bucket (bool)
elastic_soe_constraints (bool)
global_max_charging_power_kw (float | None)
global_max_discharging_power_kw (float | None)
feature_flags (FeatureFlags)
- add_constant_power_per_efa_block_constraint(problem, variables, variable_name, market_in_question)#
- add_constraints(problem, variables, variables_global)#
- add_daily_cycle_limit_constraint(problem, variables, variables_special)#
- static add_if_condition_boolean_flag_for_volume(problem, variables, boolean_variable, volume_variable)#
- static add_logical_and(problem, variables, output_variable, input_variables)#
- static add_logical_or(problem, variables, output_variable, input_variables)#
- static add_pairwise_power_equality_per_efa_block(rows, problem, capacity_variable, efa_block)#
- add_slack_variables_to_constraint(problem, constraint_name, penalty=1000000.0)#
Elasticify an existing constraint by adding non-negative slack variable(s) and penalizing their usage in the objective.
This converts a hard (inviolable) constraint into a soft one whose violation is discouraged instead of forbidden, enabling the solver to return near-feasible solutions in scenarios where the original model would be infeasible. The method supports all three PuLP constraint senses (==, <=, >=):
Equality: adds two slacks (positive and negative) so that either side deviations are captured:
base_expr + s_pos - s_neg == rhs.Less-than-or-equal: adds one slack capturing excess above the RHS:
base_expr - s <= rhs.Greater-than-or-equal: adds one slack capturing shortfall below the RHS:
base_expr + s >= rhs.
- Rationale:
We rebuild the constraint rather than mutating its left-hand side in place. Earlier versions used
constraint.addInPlace(...)which mutated a sharedLpAffineExpressionreferenced by both the stored constraint object and cached SoE expressions. That aliasing caused slack variables to “leak” into expressions that should represent the raw (non-elastic) battery state of energy. By constructing a freshLpAffineExpressionwe preserve clean separation of model components.- Side effects:
Replaces
problem.constraints[constraint_name]with a new constraint object.Registers created slack variable(s) in
self._slack_variablesfor later inspection (e.g. viaget_infeasible_constraints()).Augments the objective with a penalty term
penalty * sum(slacks)(sign adjusted for maximize vs minimize problems) so that any violation worsens the objective.
- Parameters:
problem (pulp.LpProblem) – The MILP problem containing the constraint to be elasticified.
constraint_name (str) – Name (key) of the target constraint as stored in
problem.constraints. Must exist. IMPORTANT: Generate this viaget_pulp_conforming_constraint_name()before adding the original constraint and again when passing it here. PuLP sanitizes names (spaces, pluses, dashes -> underscores). If you pass the unsanitized original string the lookup may fail becauseproblem.constraintsholds the transformed version.penalty (float, optional) – Scalar penalty applied per unit of slack (default
1e6). Choose a value larger than typical objective coefficients so the solver prefers feasibility restoration over profit.
- Raises:
KeyError – If
constraint_nameis not found inproblem.constraints.ValueError – If the constraint sense is unexpected or the problem optimization sense is unknown.
Notes
Name conformity: Always use
get_pulp_conforming_constraint_name()to deriveconstraint_name. Failing to do so can raise aKeyErrorsince PuLP silently rewrites illegal characters.Penalty is added incrementally only for newly introduced slack variables; repeated calls on the same constraint are idempotent (no duplicate slacks or penalties).
For equality constraints two slacks are required to remain linear and non-negative.
High penalties may cause numerical scaling issues; tune relative to price/imbalance magnitudes. See usage in
create_asset_state_target_soe_constraintswhere penalty is tied to max imbalance costs.
Examples
>>> optimizer.add_slack_variables_to_constraint(problem, 'Constrain target SoE to expected target 50 in settlement period 2025-10-15 12:00:00') After solving, inspect slacks: >>> optimizer.get_infeasible_constraints(top=5) {'Constrain target SoE ...': 'target_soc_something >= 50 (slack=2.0)'}
- property commit_sha#
- compute_intraday_pnl_for_buckets(direction, timestamp, sell_adjustment=None, problem_solved=False)#
This function returns a LPAffineExpression or a float for a given set of intraday price buckets, depending on whether the problem was already solved or not.
- Args:
direction (str): direction of the intraday deals; either ‘buy’ or ‘sell’ timestamp (_type_): _description_ problem_solved (bool, optional): was the original problem already solved. Defaults to False.
- Returns:
pulp.pulp.LpAffineExpression | float: returns a LpAffineExpression if not solved yet, otherwise a float
- Parameters:
- Return type:
- create_asset_state_target_soe_constraints(problem, variables)#
- create_battery_max_power_constraints(problem, variables)#
- create_boolean_variables_and_constraints(problem, variables)#
- create_constraints_for_net_charging_discharging(problem, variables)#
- abstractmethod create_country_specific_constraints(problem, variables)#
- create_grid_connection_constraints(problem, variables)#
- create_intraday_variable_hierarchy(problem, variables)#
- create_soe_constraints(problem, variables)#
- abstractmethod create_wholesale_duration_constraints(problem, variables)#
- evaluate_problem_variables(delta=False)#
- evaluate_problem_variables_melmil(delta=False)#
- evaluate_pulp_variable(variable, delta=False)#
- get_absolute_value(variable)#
- get_country_specific_objective(problem, variables, variables_global)#
- get_imbalance_penalty(variables)#
- get_imbalance_penalty_cost(variables, inverted=False)#
- Return type:
Series
- get_infeasible_constraints(top=None, eps=1e-06)#
- get_infeasible_solution_timeout_message()#
- get_objective(problem, variables, variables_global)#
- abstractmethod get_objective_pnl(variables, problem_solved=False, markets=None)#
- get_optimization_problem()#
Purpose: This method formulates and returns an optimization problem for day-ahead trading of a battery in the UK energy market. The optimization problem aims to maximize net revenues from energy and Dynamic Containment (DC) market participation while considering battery parameters, market data, and operational constraints.
Note: The BatteryOptimizer class inherits from Request, which sets a number of object attributes to the required input parameters. Therefore, the BatteryOptimizer object already knows all required input parameters for setting up the corresponding optimization problem. Refer to the Request class to further understand the (nested) structure of the input parameters.
Parameters (not passed as input arguments but already attached to self): * asset: The main parameter that links to all other optimization parameters * asset.battery_parameters: An object representing the battery, including its parameters like capacity, efficiency, maximum charge/discharge rates, etc. * asset.battery_initial_conditions: Initial state of the battery at the beginning of the optimization timeframe, e.g. initial SoE * asset.battery_marketed: Battery volumes already marketed in same timeframe as requested optimization timeframe * asset.battery_availabilities: Parameter that allows trader to finetune what timeperiods the battery is available for trading * asset.PV_forecast: Optional on-site PV forecast to be considered in optimization routine (limits available grid export capacity) * asset.battery_commercials: Static commerical parameters such as grid costs * asset.price_forecast: Price forecasts for markets the battery is being optimized for * asset.strategy_optimization: Parameters that define e.g. what markets to optimize for and what timeperiod to optimize
Return: * Returns a formulated optimization problem (problem) ready to be solved.
Key components: 1. Initialization:
The method initializes a new linear programming problem using the pulp library.
- Variable Generation:
The method calls the get_variables function to obtain decision variables required for the optimization problem.
- Constraint Addition:
A series of constraints are added to the optimization problem: Simple constraints: * define helpful booleans used in the more complex constains: e.g. boolean that turn 1 if we are doing dcl or dch, turn 1 if dcl, turn 1 if dch, turn 1 if dch and dcl, turn 1 if in SP before dc, turn 1 if in SP after dc etc * define helpful booleans used in the more complex constains: that turns 1 if we are charging/discharging during DC during SP but not SP+1 or SP-1 (in which case we are charging “standalone”) * soc of battery >= soc_min * soc of battery <= soc_max * dcl <= max discharge of the battery * dch <= max charge of the battery * the volume offered for DC must be constant across a given EFA block * epex30min and n2ex1h are rounded down to 100kW (Note: DC variables are integer values signifying whole units of MW) * the volume offered for n2exh must be constant over a 1h period * use technical maximum of the PV production for the gird max discharge constraint * when offering dch + dcl together one must keep 12% of the power to do basepoint management (so both cannot be offered simultaneously with full power) 12% comes from ensuring that we can charge/discharge at least 3min of contracted power over the SP so 3/30 = 10%. This is rounded to 12% to keep buffer. * total charge <= grid charge constraint * total discharge <= grid discharge constraint * PV cannot be curtailed, either its production flows in the battery or in the grid. Via a parameter allow_pv_to_battery it is possible to allow or not to use the PV production to charge the battery.
More complex constraints defined as functions: * add_power_swing_constraint: Ensures power changes between periods do not exceed the specified threshold (power_swing_kW). * add_soc_during_dc_constraint: Ensures the SoC lies within specific bounds during DCL and DCH operations. * add_basepoint_change_before_start_of_dc_constraint: Adds constraints related to power basepoint changes at the start of DC operations. * add_basepoint_change_during_dc_constraint: Adds constraints related to power basepoint changes during DC operations. * add_basepoint_change_after_end_of_dc_constraint: Adds constraints related to power basepoint changes after the end of DC operations.
4. Objective Function: The method calls the get_objective function to obtain the objective function for the optimization problem. This objective function is designed to maximize net revenues from energy and DC market participation.
5. Finalizing the Optimization Problem: The objective function is added to the optimization problem, and the problem is returned, ready for solving.
This method provides a structured way to formulate an optimization problem for day-ahead trading of a battery in the UK energy market. The optimization considers a wide range of constraints, including battery operational limits, market parameters, and specific UK market rules related to Dynamic Containment.
- get_penalized_objective_pnl(problem, variables, variables_global)#
- get_pulp_conforming_constraint_name(constraint_name)#
- get_scip_iis_message(scip_executable_name='scip', timeout_seconds=5)#
Attempt to compute an IIS (Irreducible Infeasible Subsystem) using the SCIP CLI.
Workflow: 1. Serialize the current pulp problem to an MPS file (SCIP can ingest MPS). 2. Invoke the external scip binary with a command sequence to read the file, compute IIS, and display it. 3. Capture stdout and return it so it can be embedded into the infeasible solution error message.
Requirements / Behaviour: * If SCIP is not installed / not found on PATH -> return a meaningful message (do NOT raise, we already raise outside). * Avoid leaving artefacts on disk: use a NamedTemporaryFile and delete after use. * Return full stdout including newlines. Stderr is appended (SCIP sometimes writes informational messages there). * Guard against large output by truncating only if extremely large (> 200k chars) to avoid excessive log payload. * Any unexpected runtime error returns a message explaining failure rather than propagating.
Rationale: Keeping the MPS file in a temporary location avoids polluting the working directory while still using the standard CLI interface. In-memory only would require embedding SCP’s C libraries or py-scipo bindings; given project scope, a short-lived temp file is pragmatic and portable.
- 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.
- abstractmethod get_target_soc(variables)#
- get_variables()#
- get_variables_buckets()#
- get_variables_buckets_bucket()#
- get_variables_buckets_vwap()#
- get_variables_global()#
- 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, /)#
This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that’s what pydantic-core passes when calling it.
- Args:
self: The BaseModel instance. context: The context.
- Parameters:
self (BaseModel)
context (Any)
- Return type:
None
- print_infeasible_constraints(top=None, eps=1e-06)#
- property problem#
- set_already_auctioned_markets_to_constants(problem, variables)#
- set_markets_not_requested_to_zero(problem, variables)#
- property socs#
- solve_optimization_problem()#
- validate_objective(objective)#
- property variables#
- property variables_buckets#
- property variables_global#