Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions client/client.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,20 @@ components:
maximum: 2
default: 0
description: Charging and discharging priority 0..2 compared to other batteries. 2 = highest priority.

prc_dpl_soc_high:
type: number
minimum: 0
description: |
Price in €/h for higher battery life depletion when sitting at high SOC.
A linear cost model is applied, starting at 80% SOC, applying full price at 100% SOC
The battery capacity is assumed to be s_capacity or s_max if s_capacity is not specified
prc_dpl_soc_low:
type: number
minimum: 0
description: |
Price in €/h for higher battery life depletion when sitting at low SOC.
A linear cost model is applied, starting at 20% SOC, applying full price at 0% SOC
The battery capacity is assumed to be s_capacity or s_max if s_capacity is not specified
TimeSeries:
type: object
required:
Expand Down
6 changes: 5 additions & 1 deletion src/optimizer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def handle_validation_error(error):
'c_max': fields.Float(required=True, description='Maximum charge power (W)'),
'd_max': fields.Float(required=True, description='Maximum discharge power (W)'),
'p_a': fields.Float(required=True, description='Monetary value per Wh at end of the optimization horizon'),
'c_priority': fields.Integer(required=False, description='Charging and discharging priority compared to other batteries. 2 = highest priority.')
'c_priority': fields.Integer(required=False, description='Charging and discharging priority compared to other batteries. 2 = highest priority.'),
'prc_dpl_soc_high': fields.Float(required=False, description='Price in €/h for higher battery life depletion when sitting at high SOC.'),
'prc_dpl_soc_low': fields.Float(required=False, description='Price in €/h for higher battery life depletion when sitting at low SOC.')
})

time_series_model = api.model('TimeSeries', {
Expand Down Expand Up @@ -168,6 +170,8 @@ def post(self):
d_max=bat_data['d_max'],
p_a=bat_data['p_a'],
c_priority=bat_data.get('c_priority', 0),
prc_dpl_soc_high=bat_data.get('prc_dpl_soc_high', 0.0),
prc_dpl_soc_low=bat_data.get('prc_dpl_soc_low', 0.0)
))

# Parse time series data
Expand Down
34 changes: 34 additions & 0 deletions src/optimizer/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class BatteryConfig:
p_demand: Optional[List[float]] = None # Minimum charge demand (Wh)
s_goal: Optional[List[float]] = None # Goal state of charge (Wh)
c_priority: int = 0
prc_dpl_soc_high: float = 0.0
prc_dpl_soc_low: float = 0.0


@dataclass
Expand Down Expand Up @@ -213,6 +215,14 @@ def _setup_variables(self):
for t in self.time_steps
]

# Cost variable to charge for battery depletion at very high and very low battery SOC state
self.variables['cst_bat_dpl'] = {}
for i, bat in enumerate(self.batteries):
self.variables['cst_bat_dpl'][i] = [
pulp.LpVariable(f"cst_bat_dpl_{i}_{t}", lowBound=0)
for t in self.time_steps
]

def _setup_target_function(self):
"""
Gather all target function contributions and instantiate the objective
Expand Down Expand Up @@ -254,6 +264,11 @@ def _setup_target_function(self):
if self.is_grid_demand_rate_active:
objective += - self.grid.prc_p_exc_imp * self.variables['p_max_imp_exc']

# cost for depleting battery life by sitting at very high or very low SOC
for i, bat in enumerate(self.batteries):
for t in self.time_steps:
objective += - self.variables['cst_bat_dpl'][i][t]

############################################################################
# Penalties for exceeding battery SOC limits at start
for i, bat in enumerate(self.batteries):
Expand Down Expand Up @@ -480,6 +495,25 @@ def _add_battery_constraints(self):
# Charge constraint
self.problem += self.variables['c'][i][t] <= self.M * (1 - self.variables['z_cd'][i][t])

# cost for depleting battery life by sitting at very high or very low SOC
# figure out battery capacity, avoid div 0 in case s_amx is set to 0
bat_capacity = self.batteries[i].s_max
if self.batteries[i].s_capacity is not None and self.batteries[i].s_capacity > 0.0:
bat_capacity = self.batteries[i].s_capacity
bat_capacity = max(bat_capacity, 1.0)
for t in self.time_steps:
# scale the prices to the time step width
prc_dpl_soc_high = self.batteries[i].prc_dpl_soc_high / 3600.0 * self.time_series.dt[t]
prc_dpl_soc_low = self.batteries[i].prc_dpl_soc_low / 3600.0 * self.time_series.dt[t]
# for all SOCs
self.problem += self.variables['cst_bat_dpl'][i][t] >= 0
# for high SOCs, linear cost ramp starting at 80% SOC, full cost at 100%
self.problem += self.variables['cst_bat_dpl'][i][t] \
- ((prc_dpl_soc_high / (0.2 * bat_capacity)) * (self.variables['s'][i][t] - 0.8 * bat_capacity)) >= 0
# for low SOCs, linear cost ramp starting at 20% SOC, full cost at 0%
self.problem += self.variables['cst_bat_dpl'][i][t] \
- ((prc_dpl_soc_low) * (1.0 - (self.variables['s'][i][t] / (0.2 * bat_capacity)))) >= 0

def solve(self) -> Dict:
"""
Creates the MILP model if none exists and solves the optimization problem.
Expand Down
Loading
Loading