Skip to content
Merged
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
51 changes: 36 additions & 15 deletions src/optimizer/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,12 +408,22 @@ def _add_energy_balance_constraints(self):
== e_grid_exp
+ self.time_series.gt[t])

# Constraints (4)-(5): Grid flow direction
# Constraints (4)-(5): Grid flow direction. Export/import have natural
# per-step caps (export: solar plus discharge-to-grid capacity; import:
# demand plus grid-charge capacity) that tighten the LP relaxation vs
# the global big-M without excluding any integer point. When a grid
# limit is active, the opposite side's unbounded excess variable enters
# the energy balance, so only then fall back to the global big-M.
cap_d_exp = sum(b.d_max for b in self.batteries if b.discharge_to_grid)
cap_c_imp = sum(b.c_max for b in self.batteries if b.charge_from_grid)
for t in self.time_steps:
dth = self.time_series.dt[t] / 3600.
m_exp = self.time_series.ft[t] + cap_d_exp * dth if self.grid.p_max_imp is None else self.M
m_imp = self.time_series.gt[t] + cap_c_imp * dth if self.grid.p_max_exp is None else self.M
# Export constraint
self.problem += self.variables['e'][t] <= self.M * self.variables['y'][t]
self.problem += self.variables['e'][t] <= m_exp * self.variables['y'][t]
# Import constraint
self.problem += self.variables['n'][t] <= self.M * (1 - self.variables['y'][t])
self.problem += self.variables['n'][t] <= m_imp * (1 - self.variables['y'][t])

# limit regular grid import power
if self.grid.p_max_imp is not None:
Expand All @@ -422,15 +432,17 @@ def _add_energy_balance_constraints(self):
for t in self.time_steps:
self.problem += self.variables['n'][t] <= self.grid.p_max_imp * self.time_series.dt[t] / 3600
self.problem += (self.grid.p_max_imp * self.time_series.dt[t] / 3600 - self.variables['n'][t]
<= self.M * self.variables['z_imp_lim'][t])
<= self.grid.p_max_imp * self.time_series.dt[t] / 3600
* self.variables['z_imp_lim'][t])
self.problem += (self.variables['e_imp_lim_exc'][t]
<= self.M * (1 - self.variables['z_imp_lim'][t]))
else:
# limit the actual import power
for t in self.time_steps:
self.problem += self.variables['n'][t] <= self.grid.p_max_imp * self.time_series.dt[t] / 3600
self.problem += (self.grid.p_max_imp * self.time_series.dt[t] / 3600 - self.variables['n'][t]
<= self.M * self.variables['z_imp_lim'][t])
<= self.grid.p_max_imp * self.time_series.dt[t] / 3600
* self.variables['z_imp_lim'][t])
self.problem += (self.variables['e_imp_lim_exc'][t]
<= self.M * (1 - self.variables['z_imp_lim'][t]))

Expand All @@ -439,7 +451,8 @@ def _add_energy_balance_constraints(self):
for t in self.time_steps:
self.problem += self.variables['e'][t] <= self.grid.p_max_exp * self.time_series.dt[t] / 3600
self.problem += (self.grid.p_max_exp * self.time_series.dt[t] / 3600 - self.variables['e'][t]
<= self.M * self.variables['z_exp_lim'][t])
<= self.grid.p_max_exp * self.time_series.dt[t] / 3600
* self.variables['z_exp_lim'][t])
self.problem += (self.variables['e_exp_lim_exc'][t]
<= self.M * (1 - self.variables['z_exp_lim'][t]))

Expand Down Expand Up @@ -514,13 +527,14 @@ def _add_battery_constraints(self):

# introduce z_p_demand to become only 1 if s_max is almost reached and the
# charging with c_min would stop before the end of the time slot
# s is bounded below by 0, so s_max is the largest the left side can get
self.problem += ((bat.s_max - self.variables['s'][i][t])
<= (1 - self.variables['z_p_demand'][i][t]) * self.M
<= (1 - self.variables['z_p_demand'][i][t]) * bat.s_max
+ bat.c_min * self.time_series.dt[t] / 3600.)

# introduce z_s_max_reached to become only 1 if s_max is fully reached
self.problem += ((bat.s_max - self.variables['s'][i][t])
<= (1 - self.variables['z_s_max_reached'][i][t]) * self.M)
<= (1 - self.variables['z_s_max_reached'][i][t]) * bat.s_max)

# set a "soft" constraint to reach the requested charging rate if possible.
# deactivate it if s_max is already reached
Expand All @@ -532,34 +546,41 @@ def _add_battery_constraints(self):

if bat.c_min > 0:
# set constraints to exclude charging between 0 and c_min if z_p_demand is not 1.
self.problem += (self.variables['c'][i][t] + self.variables['z_p_demand'][i][t] * self.M
self.problem += (self.variables['c'][i][t]
+ bat.c_min * self.time_series.dt[t] / 3600. * self.variables['z_p_demand'][i][t]
>= bat.c_min * self.time_series.dt[t] / 3600. * self.variables['z_c'][i][t])
self.problem += (self.variables['c'][i][t] <= self.M * self.variables['z_c'][i][t])
self.problem += (self.variables['c'][i][t] <= bat.c_max * self.time_series.dt[t] / 3600.
* self.variables['z_c'][i][t])

# Constraint (7): Minimum charge power limits if there is no charge demand
elif bat.c_min > 0:
for t in self.time_steps:
# Lower bound: either 0 or at least c_min
self.problem += (self.variables['c'][i][t] >= bat.c_min * self.time_series.dt[t] / 3600.
* self.variables['z_c'][i][t])
self.problem += (self.variables['c'][i][t] <= self.M * self.variables['z_c'][i][t])
self.problem += (self.variables['c'][i][t] <= bat.c_max * self.time_series.dt[t] / 3600.
* self.variables['z_c'][i][t])

# control battery charging from grid
if not bat.charge_from_grid:
for t in self.time_steps:
self.problem += (self.variables['c'][i][t] <= self.M * self.variables['y'][t])
self.problem += (self.variables['c'][i][t] <= bat.c_max * self.time_series.dt[t] / 3600.
* self.variables['y'][t])

# control battery discharging to grid
if not bat.discharge_to_grid:
for t in self.time_steps:
self.problem += (self.variables['d'][i][t] <= self.M * (1 - self.variables['y'][t]))
self.problem += (self.variables['d'][i][t] <= bat.d_max * self.time_series.dt[t] / 3600.
* (1 - self.variables['y'][t]))

# lock charging against discharging
for t in self.time_steps:
# Discharge constraint
self.problem += self.variables['d'][i][t] <= self.M * self.variables['z_cd'][i][t]
self.problem += (self.variables['d'][i][t] <= bat.d_max * self.time_series.dt[t] / 3600.
* self.variables['z_cd'][i][t])
# Charge constraint
self.problem += self.variables['c'][i][t] <= self.M * (1 - self.variables['z_cd'][i][t])
self.problem += (self.variables['c'][i][t] <= bat.c_max * self.time_series.dt[t] / 3600.
* (1 - self.variables['z_cd'][i][t]))

def solve(self) -> Dict:
"""
Expand Down
2 changes: 1 addition & 1 deletion test_cases/013-grid-export-limit-hit.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
},
"expected_response": {
"status": "Optimal",
"objective_value": 17.529475918803602,
"objective_value": 17.5372629588039,
"limit_violations": {
"grid_import_limit_exceeded": false,
"grid_export_limit_hit": true
Expand Down
2 changes: 1 addition & 1 deletion test_cases/019-unexpected-charge-spikes.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@
},
"expected_response": {
"status": "Optimal",
"objective_value": 1.2393735662576777,
"objective_value": 1.2481766551776774,
"limit_violations": {
"grid_import_limit_exceeded": false,
"grid_export_limit_hit": false
Expand Down
Loading