emhass 0.11.2__py3-none-any.whl → 0.11.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
emhass/optimization.py CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- import logging
4
+ import bz2
5
5
  import copy
6
+ import logging
6
7
  import pathlib
7
- import bz2
8
8
  import pickle as cPickle
9
+ from math import ceil
9
10
  from typing import Optional, Tuple
10
- import pandas as pd
11
+
11
12
  import numpy as np
13
+ import pandas as pd
12
14
  import pulp as plp
13
- from pulp import PULP_CBC_CMD, COIN_CMD, GLPK_CMD
14
- from math import ceil
15
+ from pulp import COIN_CMD, GLPK_CMD, PULP_CBC_CMD
15
16
 
16
17
 
17
18
  class Optimization:
@@ -32,10 +33,18 @@ class Optimization:
32
33
 
33
34
  """
34
35
 
35
- def __init__(self, retrieve_hass_conf: dict, optim_conf: dict, plant_conf: dict,
36
- var_load_cost: str, var_prod_price: str,
37
- costfun: str, emhass_conf: dict, logger: logging.Logger,
38
- opt_time_delta: Optional[int] = 24) -> None:
36
+ def __init__(
37
+ self,
38
+ retrieve_hass_conf: dict,
39
+ optim_conf: dict,
40
+ plant_conf: dict,
41
+ var_load_cost: str,
42
+ var_prod_price: str,
43
+ costfun: str,
44
+ emhass_conf: dict,
45
+ logger: logging.Logger,
46
+ opt_time_delta: Optional[int] = 24,
47
+ ) -> None:
39
48
  r"""
40
49
  Define constructor for Optimization class.
41
50
 
@@ -66,40 +75,55 @@ class Optimization:
66
75
  self.retrieve_hass_conf = retrieve_hass_conf
67
76
  self.optim_conf = optim_conf
68
77
  self.plant_conf = plant_conf
69
- self.freq = self.retrieve_hass_conf['optimization_time_step']
70
- self.time_zone = self.retrieve_hass_conf['time_zone']
71
- self.timeStep = self.freq.seconds/3600 # in hours
72
- self.time_delta = pd.to_timedelta(opt_time_delta, "hours") # The period of optimization
73
- self.var_PV = self.retrieve_hass_conf['sensor_power_photovoltaics']
74
- self.var_load = self.retrieve_hass_conf['sensor_power_load_no_var_loads']
75
- self.var_load_new = self.var_load+'_positive'
78
+ self.freq = self.retrieve_hass_conf["optimization_time_step"]
79
+ self.time_zone = self.retrieve_hass_conf["time_zone"]
80
+ self.timeStep = self.freq.seconds / 3600 # in hours
81
+ self.time_delta = pd.to_timedelta(
82
+ opt_time_delta, "hours"
83
+ ) # The period of optimization
84
+ self.var_PV = self.retrieve_hass_conf["sensor_power_photovoltaics"]
85
+ self.var_load = self.retrieve_hass_conf["sensor_power_load_no_var_loads"]
86
+ self.var_load_new = self.var_load + "_positive"
76
87
  self.costfun = costfun
77
88
  # self.emhass_conf = emhass_conf
78
89
  self.logger = logger
79
90
  self.var_load_cost = var_load_cost
80
91
  self.var_prod_price = var_prod_price
81
92
  self.optim_status = None
82
- if 'lp_solver' in optim_conf.keys():
83
- self.lp_solver = optim_conf['lp_solver']
93
+ if "lp_solver" in optim_conf.keys():
94
+ self.lp_solver = optim_conf["lp_solver"]
84
95
  else:
85
- self.lp_solver = 'default'
86
- if 'lp_solver_path' in optim_conf.keys():
87
- self.lp_solver_path = optim_conf['lp_solver_path']
96
+ self.lp_solver = "default"
97
+ if "lp_solver_path" in optim_conf.keys():
98
+ self.lp_solver_path = optim_conf["lp_solver_path"]
88
99
  else:
89
- self.lp_solver_path = 'empty'
90
- if self.lp_solver != 'COIN_CMD' and self.lp_solver_path != 'empty':
91
- self.logger.error("Use COIN_CMD solver name if you want to set a path for the LP solver")
92
- if self.lp_solver == 'COIN_CMD' and self.lp_solver_path == 'empty': #if COIN_CMD but lp_solver_path is empty
93
- self.logger.warning("lp_solver=COIN_CMD but lp_solver_path=empty, attempting to use lp_solver_path=/usr/bin/cbc")
94
- self.lp_solver_path = '/usr/bin/cbc'
95
-
96
- def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: np.array,
97
- unit_load_cost: np.array, unit_prod_price: np.array,
98
- soc_init: Optional[float] = None, soc_final: Optional[float] = None,
99
- def_total_hours: Optional[list] = None,
100
- def_start_timestep: Optional[list] = None,
101
- def_end_timestep: Optional[list] = None,
102
- debug: Optional[bool] = False) -> pd.DataFrame:
100
+ self.lp_solver_path = "empty"
101
+ if self.lp_solver != "COIN_CMD" and self.lp_solver_path != "empty":
102
+ self.logger.error(
103
+ "Use COIN_CMD solver name if you want to set a path for the LP solver"
104
+ )
105
+ if (
106
+ self.lp_solver == "COIN_CMD" and self.lp_solver_path == "empty"
107
+ ): # if COIN_CMD but lp_solver_path is empty
108
+ self.logger.warning(
109
+ "lp_solver=COIN_CMD but lp_solver_path=empty, attempting to use lp_solver_path=/usr/bin/cbc"
110
+ )
111
+ self.lp_solver_path = "/usr/bin/cbc"
112
+
113
+ def perform_optimization(
114
+ self,
115
+ data_opt: pd.DataFrame,
116
+ P_PV: np.array,
117
+ P_load: np.array,
118
+ unit_load_cost: np.array,
119
+ unit_prod_price: np.array,
120
+ soc_init: Optional[float] = None,
121
+ soc_final: Optional[float] = None,
122
+ def_total_hours: Optional[list] = None,
123
+ def_start_timestep: Optional[list] = None,
124
+ def_end_timestep: Optional[list] = None,
125
+ debug: Optional[bool] = False,
126
+ ) -> pd.DataFrame:
103
127
  r"""
104
128
  Perform the actual optimization using linear programming (LP).
105
129
 
@@ -138,24 +162,26 @@ class Optimization:
138
162
 
139
163
  """
140
164
  # Prepare some data in the case of a battery
141
- if self.optim_conf['set_use_battery']:
165
+ if self.optim_conf["set_use_battery"]:
142
166
  if soc_init is None:
143
167
  if soc_final is not None:
144
168
  soc_init = soc_final
145
169
  else:
146
- soc_init = self.plant_conf['battery_target_state_of_charge']
170
+ soc_init = self.plant_conf["battery_target_state_of_charge"]
147
171
  if soc_final is None:
148
172
  if soc_init is not None:
149
173
  soc_final = soc_init
150
174
  else:
151
- soc_final = self.plant_conf['battery_target_state_of_charge']
175
+ soc_final = self.plant_conf["battery_target_state_of_charge"]
152
176
  if def_total_hours is None:
153
- def_total_hours = self.optim_conf['operating_hours_of_each_deferrable_load']
177
+ def_total_hours = self.optim_conf["operating_hours_of_each_deferrable_load"]
154
178
  if def_start_timestep is None:
155
- def_start_timestep = self.optim_conf['start_timesteps_of_each_deferrable_load']
179
+ def_start_timestep = self.optim_conf[
180
+ "start_timesteps_of_each_deferrable_load"
181
+ ]
156
182
  if def_end_timestep is None:
157
- def_end_timestep = self.optim_conf['end_timesteps_of_each_deferrable_load']
158
- type_self_conso = 'bigm' # maxmin
183
+ def_end_timestep = self.optim_conf["end_timesteps_of_each_deferrable_load"]
184
+ type_self_conso = "bigm" # maxmin
159
185
 
160
186
  #### The LP problem using Pulp ####
161
187
  opt_model = plp.LpProblem("LP_Model", plp.LpMaximize)
@@ -165,398 +191,698 @@ class Optimization:
165
191
  M = 10e10
166
192
 
167
193
  ## Add decision variables
168
- P_grid_neg = {(i):plp.LpVariable(cat='Continuous',
169
- lowBound=-self.plant_conf['maximum_power_to_grid'], upBound=0,
170
- name="P_grid_neg{}".format(i)) for i in set_I}
171
- P_grid_pos = {(i):plp.LpVariable(cat='Continuous',
172
- lowBound=0, upBound=self.plant_conf['maximum_power_from_grid'],
173
- name="P_grid_pos{}".format(i)) for i in set_I}
194
+ P_grid_neg = {
195
+ (i): plp.LpVariable(
196
+ cat="Continuous",
197
+ lowBound=-self.plant_conf["maximum_power_to_grid"],
198
+ upBound=0,
199
+ name="P_grid_neg{}".format(i),
200
+ )
201
+ for i in set_I
202
+ }
203
+ P_grid_pos = {
204
+ (i): plp.LpVariable(
205
+ cat="Continuous",
206
+ lowBound=0,
207
+ upBound=self.plant_conf["maximum_power_from_grid"],
208
+ name="P_grid_pos{}".format(i),
209
+ )
210
+ for i in set_I
211
+ }
174
212
  P_deferrable = []
175
213
  P_def_bin1 = []
176
- for k in range(self.optim_conf['number_of_deferrable_loads']):
177
- if type(self.optim_conf['nominal_power_of_deferrable_loads'][k]) == list:
178
- upBound = np.max(self.optim_conf['nominal_power_of_deferrable_loads'][k])
214
+ for k in range(self.optim_conf["number_of_deferrable_loads"]):
215
+ if type(self.optim_conf["nominal_power_of_deferrable_loads"][k]) == list:
216
+ upBound = np.max(
217
+ self.optim_conf["nominal_power_of_deferrable_loads"][k]
218
+ )
179
219
  else:
180
- upBound = self.optim_conf['nominal_power_of_deferrable_loads'][k]
181
- if self.optim_conf['treat_deferrable_load_as_semi_cont'][k]:
182
- P_deferrable.append({(i):plp.LpVariable(cat='Continuous',
183
- name="P_deferrable{}_{}".format(k, i)) for i in set_I})
220
+ upBound = self.optim_conf["nominal_power_of_deferrable_loads"][k]
221
+ if self.optim_conf["treat_deferrable_load_as_semi_cont"][k]:
222
+ P_deferrable.append(
223
+ {
224
+ (i): plp.LpVariable(
225
+ cat="Continuous", name="P_deferrable{}_{}".format(k, i)
226
+ )
227
+ for i in set_I
228
+ }
229
+ )
184
230
  else:
185
- P_deferrable.append({(i):plp.LpVariable(cat='Continuous',
186
- lowBound=0, upBound=upBound,
187
- name="P_deferrable{}_{}".format(k, i)) for i in set_I})
188
- P_def_bin1.append({(i):plp.LpVariable(cat='Binary',
189
- name="P_def{}_bin1_{}".format(k, i)) for i in set_I})
231
+ P_deferrable.append(
232
+ {
233
+ (i): plp.LpVariable(
234
+ cat="Continuous",
235
+ lowBound=0,
236
+ upBound=upBound,
237
+ name="P_deferrable{}_{}".format(k, i),
238
+ )
239
+ for i in set_I
240
+ }
241
+ )
242
+ P_def_bin1.append(
243
+ {
244
+ (i): plp.LpVariable(
245
+ cat="Binary", name="P_def{}_bin1_{}".format(k, i)
246
+ )
247
+ for i in set_I
248
+ }
249
+ )
190
250
  P_def_start = []
191
251
  P_def_bin2 = []
192
- for k in range(self.optim_conf['number_of_deferrable_loads']):
193
- P_def_start.append({(i):plp.LpVariable(cat='Binary',
194
- name="P_def{}_start_{}".format(k, i)) for i in set_I})
195
- P_def_bin2.append({(i):plp.LpVariable(cat='Binary',
196
- name="P_def{}_bin2_{}".format(k, i)) for i in set_I})
197
- D = {(i):plp.LpVariable(cat='Binary',
198
- name="D_{}".format(i)) for i in set_I}
199
- E = {(i):plp.LpVariable(cat='Binary',
200
- name="E_{}".format(i)) for i in set_I}
201
- if self.optim_conf['set_use_battery']:
202
- P_sto_pos = {(i):plp.LpVariable(cat='Continuous',
203
- lowBound=0, upBound=self.plant_conf['battery_discharge_power_max'],
204
- name="P_sto_pos_{0}".format(i)) for i in set_I}
205
- P_sto_neg = {(i):plp.LpVariable(cat='Continuous',
206
- lowBound=-self.plant_conf['battery_charge_power_max'], upBound=0,
207
- name="P_sto_neg_{0}".format(i)) for i in set_I}
252
+ for k in range(self.optim_conf["number_of_deferrable_loads"]):
253
+ P_def_start.append(
254
+ {
255
+ (i): plp.LpVariable(
256
+ cat="Binary", name="P_def{}_start_{}".format(k, i)
257
+ )
258
+ for i in set_I
259
+ }
260
+ )
261
+ P_def_bin2.append(
262
+ {
263
+ (i): plp.LpVariable(
264
+ cat="Binary", name="P_def{}_bin2_{}".format(k, i)
265
+ )
266
+ for i in set_I
267
+ }
268
+ )
269
+ D = {(i): plp.LpVariable(cat="Binary", name="D_{}".format(i)) for i in set_I}
270
+ E = {(i): plp.LpVariable(cat="Binary", name="E_{}".format(i)) for i in set_I}
271
+ if self.optim_conf["set_use_battery"]:
272
+ P_sto_pos = {
273
+ (i): plp.LpVariable(
274
+ cat="Continuous",
275
+ lowBound=0,
276
+ upBound=self.plant_conf["battery_discharge_power_max"],
277
+ name="P_sto_pos_{0}".format(i),
278
+ )
279
+ for i in set_I
280
+ }
281
+ P_sto_neg = {
282
+ (i): plp.LpVariable(
283
+ cat="Continuous",
284
+ lowBound=-self.plant_conf["battery_charge_power_max"],
285
+ upBound=0,
286
+ name="P_sto_neg_{0}".format(i),
287
+ )
288
+ for i in set_I
289
+ }
208
290
  else:
209
- P_sto_pos = {(i):i*0 for i in set_I}
210
- P_sto_neg = {(i):i*0 for i in set_I}
211
-
212
- if self.costfun == 'self-consumption':
213
- SC = {(i):plp.LpVariable(cat='Continuous',
214
- name="SC_{}".format(i)) for i in set_I}
215
- if self.plant_conf['inverter_is_hybrid']:
216
- P_hybrid_inverter = {(i):plp.LpVariable(cat='Continuous',
217
- name="P_hybrid_inverter{}".format(i)) for i in set_I}
218
- P_PV_curtailment = {(i):plp.LpVariable(cat='Continuous', lowBound=0,
219
- name="P_PV_curtailment{}".format(i)) for i in set_I}
220
-
291
+ P_sto_pos = {(i): i * 0 for i in set_I}
292
+ P_sto_neg = {(i): i * 0 for i in set_I}
293
+
294
+ if self.costfun == "self-consumption":
295
+ SC = {
296
+ (i): plp.LpVariable(cat="Continuous", name="SC_{}".format(i))
297
+ for i in set_I
298
+ }
299
+ if self.plant_conf["inverter_is_hybrid"]:
300
+ P_hybrid_inverter = {
301
+ (i): plp.LpVariable(
302
+ cat="Continuous", name="P_hybrid_inverter{}".format(i)
303
+ )
304
+ for i in set_I
305
+ }
306
+ P_PV_curtailment = {
307
+ (i): plp.LpVariable(
308
+ cat="Continuous", lowBound=0, name="P_PV_curtailment{}".format(i)
309
+ )
310
+ for i in set_I
311
+ }
312
+
221
313
  ## Define objective
222
- P_def_sum= []
314
+ P_def_sum = []
223
315
  for i in set_I:
224
- P_def_sum.append(plp.lpSum(P_deferrable[k][i] for k in range(self.optim_conf['number_of_deferrable_loads'])))
225
- if self.costfun == 'profit':
226
- if self.optim_conf['set_total_pv_sell']:
227
- objective = plp.lpSum(-0.001*self.timeStep*(unit_load_cost[i]*(P_load[i] + P_def_sum[i]) + \
228
- unit_prod_price[i]*P_grid_neg[i]) for i in set_I)
316
+ P_def_sum.append(
317
+ plp.lpSum(
318
+ P_deferrable[k][i]
319
+ for k in range(self.optim_conf["number_of_deferrable_loads"])
320
+ )
321
+ )
322
+ if self.costfun == "profit":
323
+ if self.optim_conf["set_total_pv_sell"]:
324
+ objective = plp.lpSum(
325
+ -0.001
326
+ * self.timeStep
327
+ * (
328
+ unit_load_cost[i] * (P_load[i] + P_def_sum[i])
329
+ + unit_prod_price[i] * P_grid_neg[i]
330
+ )
331
+ for i in set_I
332
+ )
229
333
  else:
230
- objective = plp.lpSum(-0.001*self.timeStep*(unit_load_cost[i]*P_grid_pos[i] + \
231
- unit_prod_price[i]*P_grid_neg[i]) for i in set_I)
232
- elif self.costfun == 'cost':
233
- if self.optim_conf['set_total_pv_sell']:
234
- objective = plp.lpSum(-0.001*self.timeStep*unit_load_cost[i]*(P_load[i] + P_def_sum[i]) for i in set_I)
334
+ objective = plp.lpSum(
335
+ -0.001
336
+ * self.timeStep
337
+ * (
338
+ unit_load_cost[i] * P_grid_pos[i]
339
+ + unit_prod_price[i] * P_grid_neg[i]
340
+ )
341
+ for i in set_I
342
+ )
343
+ elif self.costfun == "cost":
344
+ if self.optim_conf["set_total_pv_sell"]:
345
+ objective = plp.lpSum(
346
+ -0.001
347
+ * self.timeStep
348
+ * unit_load_cost[i]
349
+ * (P_load[i] + P_def_sum[i])
350
+ for i in set_I
351
+ )
235
352
  else:
236
- objective = plp.lpSum(-0.001*self.timeStep*unit_load_cost[i]*P_grid_pos[i] for i in set_I)
237
- elif self.costfun == 'self-consumption':
238
- if type_self_conso == 'bigm':
353
+ objective = plp.lpSum(
354
+ -0.001 * self.timeStep * unit_load_cost[i] * P_grid_pos[i]
355
+ for i in set_I
356
+ )
357
+ elif self.costfun == "self-consumption":
358
+ if type_self_conso == "bigm":
239
359
  bigm = 1e3
240
- objective = plp.lpSum(-0.001*self.timeStep*(bigm*unit_load_cost[i]*P_grid_pos[i] + \
241
- unit_prod_price[i]*P_grid_neg[i]) for i in set_I)
242
- elif type_self_conso == 'maxmin':
243
- objective = plp.lpSum(0.001*self.timeStep*unit_load_cost[i]*SC[i] for i in set_I)
360
+ objective = plp.lpSum(
361
+ -0.001
362
+ * self.timeStep
363
+ * (
364
+ bigm * unit_load_cost[i] * P_grid_pos[i]
365
+ + unit_prod_price[i] * P_grid_neg[i]
366
+ )
367
+ for i in set_I
368
+ )
369
+ elif type_self_conso == "maxmin":
370
+ objective = plp.lpSum(
371
+ 0.001 * self.timeStep * unit_load_cost[i] * SC[i] for i in set_I
372
+ )
244
373
  else:
245
374
  self.logger.error("Not a valid option for type_self_conso parameter")
246
375
  else:
247
376
  self.logger.error("The cost function specified type is not valid")
248
377
  # Add more terms to the objective function in the case of battery use
249
- if self.optim_conf['set_use_battery']:
250
- objective = objective + plp.lpSum(-0.001*self.timeStep*(
251
- self.optim_conf['weight_battery_discharge']*P_sto_pos[i] + \
252
- self.optim_conf['weight_battery_charge']*P_sto_neg[i]) for i in set_I)
378
+ if self.optim_conf["set_use_battery"]:
379
+ objective = objective + plp.lpSum(
380
+ -0.001
381
+ * self.timeStep
382
+ * (
383
+ self.optim_conf["weight_battery_discharge"] * P_sto_pos[i]
384
+ + self.optim_conf["weight_battery_charge"] * P_sto_neg[i]
385
+ )
386
+ for i in set_I
387
+ )
253
388
 
254
389
  # Add term penalizing each startup where configured
255
- if ('set_deferrable_startup_penalty' in self.optim_conf and self.optim_conf['set_deferrable_startup_penalty']):
256
- for k in range(self.optim_conf['number_of_deferrable_loads']):
257
- if (len(self.optim_conf['set_deferrable_startup_penalty']) > k and self.optim_conf['set_deferrable_startup_penalty'][k]):
390
+ if (
391
+ "set_deferrable_startup_penalty" in self.optim_conf
392
+ and self.optim_conf["set_deferrable_startup_penalty"]
393
+ ):
394
+ for k in range(self.optim_conf["number_of_deferrable_loads"]):
395
+ if (
396
+ len(self.optim_conf["set_deferrable_startup_penalty"]) > k
397
+ and self.optim_conf["set_deferrable_startup_penalty"][k]
398
+ ):
258
399
  objective = objective + plp.lpSum(
259
- -0.001 * self.timeStep * self.optim_conf['set_deferrable_startup_penalty'][k] * P_def_start[k][i] *\
260
- unit_load_cost[i] * self.optim_conf['nominal_power_of_deferrable_loads'][k]
261
- for i in set_I)
400
+ -0.001
401
+ * self.timeStep
402
+ * self.optim_conf["set_deferrable_startup_penalty"][k]
403
+ * P_def_start[k][i]
404
+ * unit_load_cost[i]
405
+ * self.optim_conf["nominal_power_of_deferrable_loads"][k]
406
+ for i in set_I
407
+ )
262
408
 
263
409
  opt_model.setObjective(objective)
264
410
 
265
411
  ## Setting constraints
266
412
  # The main constraint: power balance
267
- if self.plant_conf['inverter_is_hybrid']:
268
- constraints = {"constraint_main1_{}".format(i) :
269
- plp.LpConstraint(
270
- e = P_hybrid_inverter[i] - P_def_sum[i] - P_load[i] + P_grid_neg[i] + P_grid_pos[i] ,
271
- sense = plp.LpConstraintEQ,
272
- rhs = 0)
273
- for i in set_I}
413
+ if self.plant_conf["inverter_is_hybrid"]:
414
+ constraints = {
415
+ "constraint_main1_{}".format(i): plp.LpConstraint(
416
+ e=P_hybrid_inverter[i]
417
+ - P_def_sum[i]
418
+ - P_load[i]
419
+ + P_grid_neg[i]
420
+ + P_grid_pos[i],
421
+ sense=plp.LpConstraintEQ,
422
+ rhs=0,
423
+ )
424
+ for i in set_I
425
+ }
274
426
  else:
275
- if self.plant_conf['compute_curtailment']:
276
- constraints = {"constraint_main2_{}".format(i) :
277
- plp.LpConstraint(
278
- e = P_PV[i] - P_PV_curtailment[i] - P_def_sum[i] - P_load[i] + P_grid_neg[i] + P_grid_pos[i] + P_sto_pos[i] + P_sto_neg[i],
279
- sense = plp.LpConstraintEQ,
280
- rhs = 0)
281
- for i in set_I}
427
+ if self.plant_conf["compute_curtailment"]:
428
+ constraints = {
429
+ "constraint_main2_{}".format(i): plp.LpConstraint(
430
+ e=P_PV[i]
431
+ - P_PV_curtailment[i]
432
+ - P_def_sum[i]
433
+ - P_load[i]
434
+ + P_grid_neg[i]
435
+ + P_grid_pos[i]
436
+ + P_sto_pos[i]
437
+ + P_sto_neg[i],
438
+ sense=plp.LpConstraintEQ,
439
+ rhs=0,
440
+ )
441
+ for i in set_I
442
+ }
282
443
  else:
283
- constraints = {"constraint_main3_{}".format(i) :
284
- plp.LpConstraint(
285
- e = P_PV[i] - P_def_sum[i] - P_load[i] + P_grid_neg[i] + P_grid_pos[i] + P_sto_pos[i] + P_sto_neg[i],
286
- sense = plp.LpConstraintEQ,
287
- rhs = 0)
288
- for i in set_I}
289
-
444
+ constraints = {
445
+ "constraint_main3_{}".format(i): plp.LpConstraint(
446
+ e=P_PV[i]
447
+ - P_def_sum[i]
448
+ - P_load[i]
449
+ + P_grid_neg[i]
450
+ + P_grid_pos[i]
451
+ + P_sto_pos[i]
452
+ + P_sto_neg[i],
453
+ sense=plp.LpConstraintEQ,
454
+ rhs=0,
455
+ )
456
+ for i in set_I
457
+ }
458
+
290
459
  # Constraint for hybrid inverter and curtailment cases
291
- if type(self.plant_conf['pv_module_model']) == list:
460
+ if type(self.plant_conf["pv_module_model"]) == list:
292
461
  P_nom_inverter = 0.0
293
- for i in range(len(self.plant_conf['pv_inverter_model'])):
294
- if type(self.plant_conf['pv_inverter_model'][i]) == str:
295
- cec_inverters = bz2.BZ2File(pathlib.Path(__file__).parent / 'data/cec_inverters.pbz2', "rb")
462
+ for i in range(len(self.plant_conf["pv_inverter_model"])):
463
+ if type(self.plant_conf["pv_inverter_model"][i]) == str:
464
+ cec_inverters = bz2.BZ2File(
465
+ pathlib.Path(__file__).parent / "data/cec_inverters.pbz2", "rb"
466
+ )
296
467
  cec_inverters = cPickle.load(cec_inverters)
297
- inverter = cec_inverters[self.plant_conf['pv_inverter_model'][i]]
468
+ inverter = cec_inverters[self.plant_conf["pv_inverter_model"][i]]
298
469
  P_nom_inverter += inverter.Paco
299
470
  else:
300
- P_nom_inverter += self.plant_conf['pv_inverter_model'][i]
471
+ P_nom_inverter += self.plant_conf["pv_inverter_model"][i]
301
472
  else:
302
- if type(self.plant_conf['pv_inverter_model'][i]) == str:
303
- cec_inverters = bz2.BZ2File(pathlib.Path(__file__).parent / 'data/cec_inverters.pbz2', "rb")
473
+ if type(self.plant_conf["pv_inverter_model"][i]) == str:
474
+ cec_inverters = bz2.BZ2File(
475
+ pathlib.Path(__file__).parent / "data/cec_inverters.pbz2", "rb"
476
+ )
304
477
  cec_inverters = cPickle.load(cec_inverters)
305
- inverter = cec_inverters[self.plant_conf['pv_inverter_model']]
478
+ inverter = cec_inverters[self.plant_conf["pv_inverter_model"]]
306
479
  P_nom_inverter = inverter.Paco
307
480
  else:
308
- P_nom_inverter = self.plant_conf['pv_inverter_model']
309
- if self.plant_conf['inverter_is_hybrid']:
310
- constraints.update({"constraint_hybrid_inverter1_{}".format(i) :
311
- plp.LpConstraint(
312
- e = P_PV[i] - P_PV_curtailment[i] + P_sto_pos[i] + P_sto_neg[i] - P_nom_inverter,
313
- sense = plp.LpConstraintLE,
314
- rhs = 0)
315
- for i in set_I})
316
- constraints.update({"constraint_hybrid_inverter2_{}".format(i) :
317
- plp.LpConstraint(
318
- e = P_PV[i] - P_PV_curtailment[i] + P_sto_pos[i] + P_sto_neg[i] - P_hybrid_inverter[i],
319
- sense = plp.LpConstraintEQ,
320
- rhs = 0)
321
- for i in set_I})
481
+ P_nom_inverter = self.plant_conf["pv_inverter_model"]
482
+ if self.plant_conf["inverter_is_hybrid"]:
483
+ constraints.update(
484
+ {
485
+ "constraint_hybrid_inverter1_{}".format(i): plp.LpConstraint(
486
+ e=P_PV[i]
487
+ - P_PV_curtailment[i]
488
+ + P_sto_pos[i]
489
+ + P_sto_neg[i]
490
+ - P_nom_inverter,
491
+ sense=plp.LpConstraintLE,
492
+ rhs=0,
493
+ )
494
+ for i in set_I
495
+ }
496
+ )
497
+ constraints.update(
498
+ {
499
+ "constraint_hybrid_inverter2_{}".format(i): plp.LpConstraint(
500
+ e=P_PV[i]
501
+ - P_PV_curtailment[i]
502
+ + P_sto_pos[i]
503
+ + P_sto_neg[i]
504
+ - P_hybrid_inverter[i],
505
+ sense=plp.LpConstraintEQ,
506
+ rhs=0,
507
+ )
508
+ for i in set_I
509
+ }
510
+ )
322
511
  else:
323
- if self.plant_conf['compute_curtailment']:
324
- constraints.update({"constraint_curtailment_{}".format(i) :
325
- plp.LpConstraint(
326
- e = P_PV_curtailment[i] - max(P_PV[i],0),
327
- sense = plp.LpConstraintLE,
328
- rhs = 0)
329
- for i in set_I})
512
+ if self.plant_conf["compute_curtailment"]:
513
+ constraints.update(
514
+ {
515
+ "constraint_curtailment_{}".format(i): plp.LpConstraint(
516
+ e=P_PV_curtailment[i] - max(P_PV[i], 0),
517
+ sense=plp.LpConstraintLE,
518
+ rhs=0,
519
+ )
520
+ for i in set_I
521
+ }
522
+ )
330
523
 
331
524
  # Two special constraints just for a self-consumption cost function
332
- if self.costfun == 'self-consumption':
333
- if type_self_conso == 'maxmin': # maxmin linear problem
334
- constraints.update({"constraint_selfcons_PV1_{}".format(i) :
335
- plp.LpConstraint(
336
- e = SC[i] - P_PV[i],
337
- sense = plp.LpConstraintLE,
338
- rhs = 0)
339
- for i in set_I})
340
- constraints.update({"constraint_selfcons_PV2_{}".format(i) :
341
- plp.LpConstraint(
342
- e = SC[i] - P_load[i] - P_def_sum[i],
343
- sense = plp.LpConstraintLE,
344
- rhs = 0)
345
- for i in set_I})
525
+ if self.costfun == "self-consumption":
526
+ if type_self_conso == "maxmin": # maxmin linear problem
527
+ constraints.update(
528
+ {
529
+ "constraint_selfcons_PV1_{}".format(i): plp.LpConstraint(
530
+ e=SC[i] - P_PV[i], sense=plp.LpConstraintLE, rhs=0
531
+ )
532
+ for i in set_I
533
+ }
534
+ )
535
+ constraints.update(
536
+ {
537
+ "constraint_selfcons_PV2_{}".format(i): plp.LpConstraint(
538
+ e=SC[i] - P_load[i] - P_def_sum[i],
539
+ sense=plp.LpConstraintLE,
540
+ rhs=0,
541
+ )
542
+ for i in set_I
543
+ }
544
+ )
346
545
 
347
546
  # Avoid injecting and consuming from grid at the same time
348
- constraints.update({"constraint_pgridpos_{}".format(i) :
349
- plp.LpConstraint(
350
- e = P_grid_pos[i] - self.plant_conf['maximum_power_from_grid']*D[i],
351
- sense = plp.LpConstraintLE,
352
- rhs = 0)
353
- for i in set_I})
354
- constraints.update({"constraint_pgridneg_{}".format(i) :
355
- plp.LpConstraint(
356
- e = -P_grid_neg[i] - self.plant_conf['maximum_power_to_grid']*(1-D[i]),
357
- sense = plp.LpConstraintLE,
358
- rhs = 0)
359
- for i in set_I})
547
+ constraints.update(
548
+ {
549
+ "constraint_pgridpos_{}".format(i): plp.LpConstraint(
550
+ e=P_grid_pos[i] - self.plant_conf["maximum_power_from_grid"] * D[i],
551
+ sense=plp.LpConstraintLE,
552
+ rhs=0,
553
+ )
554
+ for i in set_I
555
+ }
556
+ )
557
+ constraints.update(
558
+ {
559
+ "constraint_pgridneg_{}".format(i): plp.LpConstraint(
560
+ e=-P_grid_neg[i]
561
+ - self.plant_conf["maximum_power_to_grid"] * (1 - D[i]),
562
+ sense=plp.LpConstraintLE,
563
+ rhs=0,
564
+ )
565
+ for i in set_I
566
+ }
567
+ )
360
568
 
361
569
  # Treat deferrable loads constraints
362
570
  predicted_temps = {}
363
- for k in range(self.optim_conf['number_of_deferrable_loads']):
364
-
365
- if type(self.optim_conf['nominal_power_of_deferrable_loads'][k]) == list:
571
+ for k in range(self.optim_conf["number_of_deferrable_loads"]):
572
+ if type(self.optim_conf["nominal_power_of_deferrable_loads"][k]) == list:
366
573
  # Constraint for sequence of deferrable
367
574
  # WARNING: This is experimental, formulation seems correct but feasibility problems.
368
575
  # Probably uncomptabile with other constraints
369
- power_sequence = self.optim_conf['nominal_power_of_deferrable_loads'][k]
576
+ power_sequence = self.optim_conf["nominal_power_of_deferrable_loads"][k]
370
577
  sequence_length = len(power_sequence)
578
+
371
579
  def create_matrix(input_list, n):
372
580
  matrix = []
373
581
  for i in range(n + 1):
374
582
  row = [0] * i + input_list + [0] * (n - i)
375
- matrix.append(row[:n*2])
583
+ matrix.append(row[: n * 2])
376
584
  return matrix
377
- matrix = create_matrix(power_sequence, n-sequence_length)
378
- y = plp.LpVariable.dicts(f"y{k}", (i for i in range(len(matrix))), cat='Binary')
379
- constraints.update({f"single_value_constraint_{k}" :
380
- plp.LpConstraint(
381
- e = plp.lpSum(y[i] for i in range(len(matrix))) - 1,
382
- sense = plp.LpConstraintEQ,
383
- rhs = 0)
384
- })
385
- constraints.update({f"pdef{k}_sumconstraint_{i}" :
386
- plp.LpConstraint(
387
- e = plp.lpSum(P_deferrable[k][i] for i in set_I) - np.sum(power_sequence),
388
- sense = plp.LpConstraintEQ,
389
- rhs = 0)
390
- })
391
- constraints.update({f"pdef{k}_positive_constraint_{i}" :
392
- plp.LpConstraint(
393
- e = P_deferrable[k][i],
394
- sense = plp.LpConstraintGE,
395
- rhs = 0)
396
- for i in set_I})
585
+
586
+ matrix = create_matrix(power_sequence, n - sequence_length)
587
+ y = plp.LpVariable.dicts(
588
+ f"y{k}", (i for i in range(len(matrix))), cat="Binary"
589
+ )
590
+ constraints.update(
591
+ {
592
+ f"single_value_constraint_{k}": plp.LpConstraint(
593
+ e=plp.lpSum(y[i] for i in range(len(matrix))) - 1,
594
+ sense=plp.LpConstraintEQ,
595
+ rhs=0,
596
+ )
597
+ }
598
+ )
599
+ constraints.update(
600
+ {
601
+ f"pdef{k}_sumconstraint_{i}": plp.LpConstraint(
602
+ e=plp.lpSum(P_deferrable[k][i] for i in set_I)
603
+ - np.sum(power_sequence),
604
+ sense=plp.LpConstraintEQ,
605
+ rhs=0,
606
+ )
607
+ }
608
+ )
609
+ constraints.update(
610
+ {
611
+ f"pdef{k}_positive_constraint_{i}": plp.LpConstraint(
612
+ e=P_deferrable[k][i], sense=plp.LpConstraintGE, rhs=0
613
+ )
614
+ for i in set_I
615
+ }
616
+ )
397
617
  for num, mat in enumerate(matrix):
398
- constraints.update({f"pdef{k}_value_constraint_{num}_{i}" :
399
- plp.LpConstraint(
400
- e = P_deferrable[k][i] - mat[i]*y[num],
401
- sense = plp.LpConstraintEQ,
402
- rhs = 0)
403
- for i in set_I})
404
-
618
+ constraints.update(
619
+ {
620
+ f"pdef{k}_value_constraint_{num}_{i}": plp.LpConstraint(
621
+ e=P_deferrable[k][i] - mat[i] * y[num],
622
+ sense=plp.LpConstraintEQ,
623
+ rhs=0,
624
+ )
625
+ for i in set_I
626
+ }
627
+ )
628
+
405
629
  elif "def_load_config" in self.optim_conf.keys():
406
630
  if "thermal_config" in self.optim_conf["def_load_config"][k]:
407
631
  # Special case of a thermal deferrable load
408
- def_load_config = self.optim_conf['def_load_config'][k]
409
- if def_load_config and 'thermal_config' in def_load_config:
632
+ def_load_config = self.optim_conf["def_load_config"][k]
633
+ if def_load_config and "thermal_config" in def_load_config:
410
634
  hc = def_load_config["thermal_config"]
411
635
  start_temperature = hc["start_temperature"]
412
636
  cooling_constant = hc["cooling_constant"]
413
637
  heating_rate = hc["heating_rate"]
414
638
  overshoot_temperature = hc["overshoot_temperature"]
415
- outdoor_temperature_forecast = data_opt['outdoor_temperature_forecast']
639
+ outdoor_temperature_forecast = data_opt[
640
+ "outdoor_temperature_forecast"
641
+ ]
416
642
  desired_temperatures = hc["desired_temperatures"]
417
- sense = hc.get('sense', 'heat')
643
+ sense = hc.get("sense", "heat")
418
644
  predicted_temp = [start_temperature]
419
645
  for I in set_I:
420
646
  if I == 0:
421
647
  continue
422
648
  predicted_temp.append(
423
- predicted_temp[I-1]
424
- + (P_deferrable[k][I-1] * (heating_rate * self.timeStep / self.optim_conf['nominal_power_of_deferrable_loads'][k]))
425
- - (cooling_constant * (predicted_temp[I-1] - outdoor_temperature_forecast[I-1])))
426
- if len(desired_temperatures) > I and desired_temperatures[I]:
427
- constraints.update({"constraint_defload{}_temperature_{}".format(k, I):
428
- plp.LpConstraint(
429
- e = predicted_temp[I],
430
- sense = plp.LpConstraintGE if sense == 'heat' else plp.LpConstraintLE,
431
- rhs = desired_temperatures[I],
649
+ predicted_temp[I - 1]
650
+ + (
651
+ P_deferrable[k][I - 1]
652
+ * (
653
+ heating_rate
654
+ * self.timeStep
655
+ / self.optim_conf[
656
+ "nominal_power_of_deferrable_loads"
657
+ ][k]
658
+ )
659
+ )
660
+ - (
661
+ cooling_constant
662
+ * (
663
+ predicted_temp[I - 1]
664
+ - outdoor_temperature_forecast[I - 1]
432
665
  )
433
- })
434
- constraints.update({"constraint_defload{}_overshoot_temp_{}".format(k, I):
435
- plp.LpConstraint(
436
- e = predicted_temp[I],
437
- sense = plp.LpConstraintLE if sense == 'heat' else plp.LpConstraintGE,
438
- rhs = overshoot_temperature,
666
+ )
439
667
  )
440
- for I in set_I})
668
+ if (
669
+ len(desired_temperatures) > I
670
+ and desired_temperatures[I]
671
+ ):
672
+ constraints.update(
673
+ {
674
+ "constraint_defload{}_temperature_{}".format(
675
+ k, I
676
+ ): plp.LpConstraint(
677
+ e=predicted_temp[I],
678
+ sense=plp.LpConstraintGE
679
+ if sense == "heat"
680
+ else plp.LpConstraintLE,
681
+ rhs=desired_temperatures[I],
682
+ )
683
+ }
684
+ )
685
+ constraints.update(
686
+ {
687
+ "constraint_defload{}_overshoot_temp_{}".format(
688
+ k, I
689
+ ): plp.LpConstraint(
690
+ e=predicted_temp[I],
691
+ sense=plp.LpConstraintLE
692
+ if sense == "heat"
693
+ else plp.LpConstraintGE,
694
+ rhs=overshoot_temperature,
695
+ )
696
+ for I in set_I
697
+ }
698
+ )
441
699
  predicted_temps[k] = predicted_temp
442
-
700
+
443
701
  else:
444
-
445
702
  if def_total_hours[k] > 0:
446
703
  # Total time of deferrable load
447
- constraints.update({"constraint_defload{}_energy".format(k) :
448
- plp.LpConstraint(
449
- e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in set_I),
450
- sense = plp.LpConstraintEQ,
451
- rhs = def_total_hours[k]*self.optim_conf['nominal_power_of_deferrable_loads'][k])
452
- })
453
-
704
+ constraints.update(
705
+ {
706
+ "constraint_defload{}_energy".format(k): plp.LpConstraint(
707
+ e=plp.lpSum(
708
+ P_deferrable[k][i] * self.timeStep for i in set_I
709
+ ),
710
+ sense=plp.LpConstraintEQ,
711
+ rhs=def_total_hours[k]
712
+ * self.optim_conf["nominal_power_of_deferrable_loads"][
713
+ k
714
+ ],
715
+ )
716
+ }
717
+ )
718
+
454
719
  # Ensure deferrable loads consume energy between def_start_timestep & def_end_timestep
455
- self.logger.debug("Deferrable load {}: Proposed optimization window: {} --> {}".format(
456
- k, def_start_timestep[k], def_end_timestep[k]))
720
+ self.logger.debug(
721
+ "Deferrable load {}: Proposed optimization window: {} --> {}".format(
722
+ k, def_start_timestep[k], def_end_timestep[k]
723
+ )
724
+ )
457
725
  def_start, def_end, warning = Optimization.validate_def_timewindow(
458
- def_start_timestep[k], def_end_timestep[k], ceil(def_total_hours[k]/self.timeStep), n)
459
- if warning is not None:
726
+ def_start_timestep[k],
727
+ def_end_timestep[k],
728
+ ceil(def_total_hours[k] / self.timeStep),
729
+ n,
730
+ )
731
+ if warning is not None:
460
732
  self.logger.warning("Deferrable load {} : {}".format(k, warning))
461
- self.logger.debug("Deferrable load {}: Validated optimization window: {} --> {}".format(
462
- k, def_start, def_end))
463
- if def_start > 0:
464
- constraints.update({"constraint_defload{}_start_timestep".format(k) :
465
- plp.LpConstraint(
466
- e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in range(0, def_start)),
467
- sense = plp.LpConstraintEQ,
468
- rhs = 0)
469
- })
470
- if def_end > 0:
471
- constraints.update({"constraint_defload{}_end_timestep".format(k) :
472
- plp.LpConstraint(
473
- e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in range(def_end, n)),
474
- sense = plp.LpConstraintEQ,
475
- rhs = 0)
476
- })
477
-
733
+ self.logger.debug(
734
+ "Deferrable load {}: Validated optimization window: {} --> {}".format(
735
+ k, def_start, def_end
736
+ )
737
+ )
738
+ if def_start > 0:
739
+ constraints.update(
740
+ {
741
+ "constraint_defload{}_start_timestep".format(
742
+ k
743
+ ): plp.LpConstraint(
744
+ e=plp.lpSum(
745
+ P_deferrable[k][i] * self.timeStep
746
+ for i in range(0, def_start)
747
+ ),
748
+ sense=plp.LpConstraintEQ,
749
+ rhs=0,
750
+ )
751
+ }
752
+ )
753
+ if def_end > 0:
754
+ constraints.update(
755
+ {
756
+ "constraint_defload{}_end_timestep".format(k): plp.LpConstraint(
757
+ e=plp.lpSum(
758
+ P_deferrable[k][i] * self.timeStep
759
+ for i in range(def_end, n)
760
+ ),
761
+ sense=plp.LpConstraintEQ,
762
+ rhs=0,
763
+ )
764
+ }
765
+ )
766
+
478
767
  # Treat the number of starts for a deferrable load (new method considering current state)
479
768
  current_state = 0
480
- if ("def_current_state" in self.optim_conf and len(self.optim_conf["def_current_state"]) > k):
769
+ if (
770
+ "def_current_state" in self.optim_conf
771
+ and len(self.optim_conf["def_current_state"]) > k
772
+ ):
481
773
  current_state = 1 if self.optim_conf["def_current_state"][k] else 0
482
774
  # P_deferrable < P_def_bin2 * 1 million
483
775
  # P_deferrable must be zero if P_def_bin2 is zero
484
- constraints.update({"constraint_pdef{}_start1_{}".format(k, i):
485
- plp.LpConstraint(
486
- e=P_deferrable[k][i] - P_def_bin2[k][i] * M,
487
- sense=plp.LpConstraintLE,
488
- rhs=0)
489
- for i in set_I})
776
+ constraints.update(
777
+ {
778
+ "constraint_pdef{}_start1_{}".format(k, i): plp.LpConstraint(
779
+ e=P_deferrable[k][i] - P_def_bin2[k][i] * M,
780
+ sense=plp.LpConstraintLE,
781
+ rhs=0,
782
+ )
783
+ for i in set_I
784
+ }
785
+ )
490
786
  # P_deferrable - P_def_bin2 <= 0
491
787
  # P_def_bin2 must be zero if P_deferrable is zero
492
- constraints.update({"constraint_pdef{}_start1a_{}".format(k, i):
493
- plp.LpConstraint(
494
- e=P_def_bin2[k][i] - P_deferrable[k][i],
495
- sense=plp.LpConstraintLE,
496
- rhs=0)
497
- for i in set_I})
788
+ constraints.update(
789
+ {
790
+ "constraint_pdef{}_start1a_{}".format(k, i): plp.LpConstraint(
791
+ e=P_def_bin2[k][i] - P_deferrable[k][i],
792
+ sense=plp.LpConstraintLE,
793
+ rhs=0,
794
+ )
795
+ for i in set_I
796
+ }
797
+ )
498
798
  # P_def_start + P_def_bin2[i-1] >= P_def_bin2[i]
499
799
  # If load is on this cycle (P_def_bin2[i] is 1) then P_def_start must be 1 OR P_def_bin2[i-1] must be 1
500
800
  # For first timestep, use current state if provided by caller.
501
- constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
502
- plp.LpConstraint(
503
- e=P_def_start[k][i]
504
- - P_def_bin2[k][i]
505
- + (P_def_bin2[k][i - 1] if i - 1 >= 0 else current_state),
506
- sense=plp.LpConstraintGE,
507
- rhs=0)
508
- for i in set_I})
801
+ constraints.update(
802
+ {
803
+ "constraint_pdef{}_start2_{}".format(k, i): plp.LpConstraint(
804
+ e=P_def_start[k][i]
805
+ - P_def_bin2[k][i]
806
+ + (P_def_bin2[k][i - 1] if i - 1 >= 0 else current_state),
807
+ sense=plp.LpConstraintGE,
808
+ rhs=0,
809
+ )
810
+ for i in set_I
811
+ }
812
+ )
509
813
  # P_def_bin2[i-1] + P_def_start <= 1
510
814
  # If load started this cycle (P_def_start[i] is 1) then P_def_bin2[i-1] must be 0
511
- constraints.update({"constraint_pdef{}_start3_{}".format(k, i):
512
- plp.LpConstraint(
513
- e=(P_def_bin2[k][i-1] if i-1 >= 0 else 0) + P_def_start[k][i],
514
- sense=plp.LpConstraintLE,
515
- rhs=1)
516
- for i in set_I})
517
-
815
+ constraints.update(
816
+ {
817
+ "constraint_pdef{}_start3_{}".format(k, i): plp.LpConstraint(
818
+ e=(P_def_bin2[k][i - 1] if i - 1 >= 0 else 0)
819
+ + P_def_start[k][i],
820
+ sense=plp.LpConstraintLE,
821
+ rhs=1,
822
+ )
823
+ for i in set_I
824
+ }
825
+ )
826
+
518
827
  # Treat deferrable as a fixed value variable with just one startup
519
- if self.optim_conf['set_deferrable_load_single_constant'][k]:
828
+ if self.optim_conf["set_deferrable_load_single_constant"][k]:
520
829
  # P_def_start[i] must be 1 for exactly 1 value of i
521
- constraints.update({"constraint_pdef{}_start4".format(k) :
522
- plp.LpConstraint(
523
- e = plp.lpSum(P_def_start[k][i] for i in set_I),
524
- sense = plp.LpConstraintEQ,
525
- rhs = 1)
526
- })
830
+ constraints.update(
831
+ {
832
+ "constraint_pdef{}_start4".format(k): plp.LpConstraint(
833
+ e=plp.lpSum(P_def_start[k][i] for i in set_I),
834
+ sense=plp.LpConstraintEQ,
835
+ rhs=1,
836
+ )
837
+ }
838
+ )
527
839
  # P_def_bin2 must be 1 for exactly the correct number of timesteps.
528
- constraints.update({"constraint_pdef{}_start5".format(k) :
529
- plp.LpConstraint(
530
- e = plp.lpSum(P_def_bin2[k][i] for i in set_I),
531
- sense = plp.LpConstraintEQ,
532
- rhs = def_total_hours[k]/self.timeStep)
533
- })
534
-
840
+ constraints.update(
841
+ {
842
+ "constraint_pdef{}_start5".format(k): plp.LpConstraint(
843
+ e=plp.lpSum(P_def_bin2[k][i] for i in set_I),
844
+ sense=plp.LpConstraintEQ,
845
+ rhs=def_total_hours[k] / self.timeStep,
846
+ )
847
+ }
848
+ )
849
+
535
850
  # Treat deferrable load as a semi-continuous variable
536
- if self.optim_conf['treat_deferrable_load_as_semi_cont'][k]:
537
- constraints.update({"constraint_pdef{}_semicont1_{}".format(k, i) :
538
- plp.LpConstraint(
539
- e=P_deferrable[k][i] - self.optim_conf['nominal_power_of_deferrable_loads'][k]*P_def_bin1[k][i],
540
- sense=plp.LpConstraintGE,
541
- rhs=0)
542
- for i in set_I})
543
- constraints.update({"constraint_pdef{}_semicont2_{}".format(k, i) :
544
- plp.LpConstraint(
545
- e=P_deferrable[k][i] - self.optim_conf['nominal_power_of_deferrable_loads'][k]*P_def_bin1[k][i],
546
- sense=plp.LpConstraintLE,
547
- rhs=0)
548
- for i in set_I})
549
-
550
-
851
+ if self.optim_conf["treat_deferrable_load_as_semi_cont"][k]:
852
+ constraints.update(
853
+ {
854
+ "constraint_pdef{}_semicont1_{}".format(k, i): plp.LpConstraint(
855
+ e=P_deferrable[k][i]
856
+ - self.optim_conf["nominal_power_of_deferrable_loads"][k]
857
+ * P_def_bin1[k][i],
858
+ sense=plp.LpConstraintGE,
859
+ rhs=0,
860
+ )
861
+ for i in set_I
862
+ }
863
+ )
864
+ constraints.update(
865
+ {
866
+ "constraint_pdef{}_semicont2_{}".format(k, i): plp.LpConstraint(
867
+ e=P_deferrable[k][i]
868
+ - self.optim_conf["nominal_power_of_deferrable_loads"][k]
869
+ * P_def_bin1[k][i],
870
+ sense=plp.LpConstraintLE,
871
+ rhs=0,
872
+ )
873
+ for i in set_I
874
+ }
875
+ )
876
+
551
877
  # Treat the number of starts for a deferrable load (old method, kept here just in case)
552
878
  # if self.optim_conf['set_deferrable_load_single_constant'][k]:
553
- # constraints.update({"constraint_pdef{}_start1_{}".format(k, i) :
879
+ # constraints.update({"constraint_pdef{}_start1_{}".format(k, i) :
554
880
  # plp.LpConstraint(
555
881
  # e=P_deferrable[k][i] - P_def_bin2[k][i]*M,
556
882
  # sense=plp.LpConstraintLE,
557
883
  # rhs=0)
558
884
  # for i in set_I})
559
- # constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
885
+ # constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
560
886
  # plp.LpConstraint(
561
887
  # e=P_def_start[k][i] - P_def_bin2[k][i] + (P_def_bin2[k][i-1] if i-1 >= 0 else 0),
562
888
  # sense=plp.LpConstraintGE,
@@ -570,85 +896,184 @@ class Optimization:
570
896
  # })
571
897
 
572
898
  # The battery constraints
573
- if self.optim_conf['set_use_battery']:
899
+ if self.optim_conf["set_use_battery"]:
574
900
  # Optional constraints to avoid charging the battery from the grid
575
- if self.optim_conf['set_nocharge_from_grid']:
576
- constraints.update({"constraint_nocharge_from_grid_{}".format(i) :
577
- plp.LpConstraint(
578
- e = P_sto_neg[i] + P_PV[i],
579
- sense = plp.LpConstraintGE,
580
- rhs = 0)
581
- for i in set_I})
901
+ if self.optim_conf["set_nocharge_from_grid"]:
902
+ constraints.update(
903
+ {
904
+ "constraint_nocharge_from_grid_{}".format(i): plp.LpConstraint(
905
+ e=P_sto_neg[i] + P_PV[i], sense=plp.LpConstraintGE, rhs=0
906
+ )
907
+ for i in set_I
908
+ }
909
+ )
582
910
  # Optional constraints to avoid discharging the battery to the grid
583
- if self.optim_conf['set_nodischarge_to_grid']:
584
- constraints.update({"constraint_nodischarge_to_grid_{}".format(i) :
585
- plp.LpConstraint(
586
- e = P_grid_neg[i] + P_PV[i],
587
- sense = plp.LpConstraintGE,
588
- rhs = 0)
589
- for i in set_I})
911
+ if self.optim_conf["set_nodischarge_to_grid"]:
912
+ constraints.update(
913
+ {
914
+ "constraint_nodischarge_to_grid_{}".format(i): plp.LpConstraint(
915
+ e=P_grid_neg[i] + P_PV[i], sense=plp.LpConstraintGE, rhs=0
916
+ )
917
+ for i in set_I
918
+ }
919
+ )
590
920
  # Limitation of power dynamics in power per unit of time
591
- if self.optim_conf['set_battery_dynamic']:
592
- constraints.update({"constraint_pos_batt_dynamic_max_{}".format(i) :
593
- plp.LpConstraint(e = P_sto_pos[i+1] - P_sto_pos[i],
594
- sense = plp.LpConstraintLE,
595
- rhs = self.timeStep*self.optim_conf['battery_dynamic_max']*self.plant_conf['battery_discharge_power_max'])
596
- for i in range(n-1)})
597
- constraints.update({"constraint_pos_batt_dynamic_min_{}".format(i) :
598
- plp.LpConstraint(e = P_sto_pos[i+1] - P_sto_pos[i],
599
- sense = plp.LpConstraintGE,
600
- rhs = self.timeStep*self.optim_conf['battery_dynamic_min']*self.plant_conf['battery_discharge_power_max'])
601
- for i in range(n-1)})
602
- constraints.update({"constraint_neg_batt_dynamic_max_{}".format(i) :
603
- plp.LpConstraint(e = P_sto_neg[i+1] - P_sto_neg[i],
604
- sense = plp.LpConstraintLE,
605
- rhs = self.timeStep*self.optim_conf['battery_dynamic_max']*self.plant_conf['battery_charge_power_max'])
606
- for i in range(n-1)})
607
- constraints.update({"constraint_neg_batt_dynamic_min_{}".format(i) :
608
- plp.LpConstraint(e = P_sto_neg[i+1] - P_sto_neg[i],
609
- sense = plp.LpConstraintGE,
610
- rhs = self.timeStep*self.optim_conf['battery_dynamic_min']*self.plant_conf['battery_charge_power_max'])
611
- for i in range(n-1)})
921
+ if self.optim_conf["set_battery_dynamic"]:
922
+ constraints.update(
923
+ {
924
+ "constraint_pos_batt_dynamic_max_{}".format(
925
+ i
926
+ ): plp.LpConstraint(
927
+ e=P_sto_pos[i + 1] - P_sto_pos[i],
928
+ sense=plp.LpConstraintLE,
929
+ rhs=self.timeStep
930
+ * self.optim_conf["battery_dynamic_max"]
931
+ * self.plant_conf["battery_discharge_power_max"],
932
+ )
933
+ for i in range(n - 1)
934
+ }
935
+ )
936
+ constraints.update(
937
+ {
938
+ "constraint_pos_batt_dynamic_min_{}".format(
939
+ i
940
+ ): plp.LpConstraint(
941
+ e=P_sto_pos[i + 1] - P_sto_pos[i],
942
+ sense=plp.LpConstraintGE,
943
+ rhs=self.timeStep
944
+ * self.optim_conf["battery_dynamic_min"]
945
+ * self.plant_conf["battery_discharge_power_max"],
946
+ )
947
+ for i in range(n - 1)
948
+ }
949
+ )
950
+ constraints.update(
951
+ {
952
+ "constraint_neg_batt_dynamic_max_{}".format(
953
+ i
954
+ ): plp.LpConstraint(
955
+ e=P_sto_neg[i + 1] - P_sto_neg[i],
956
+ sense=plp.LpConstraintLE,
957
+ rhs=self.timeStep
958
+ * self.optim_conf["battery_dynamic_max"]
959
+ * self.plant_conf["battery_charge_power_max"],
960
+ )
961
+ for i in range(n - 1)
962
+ }
963
+ )
964
+ constraints.update(
965
+ {
966
+ "constraint_neg_batt_dynamic_min_{}".format(
967
+ i
968
+ ): plp.LpConstraint(
969
+ e=P_sto_neg[i + 1] - P_sto_neg[i],
970
+ sense=plp.LpConstraintGE,
971
+ rhs=self.timeStep
972
+ * self.optim_conf["battery_dynamic_min"]
973
+ * self.plant_conf["battery_charge_power_max"],
974
+ )
975
+ for i in range(n - 1)
976
+ }
977
+ )
612
978
  # Then the classic battery constraints
613
- constraints.update({"constraint_pstopos_{}".format(i) :
614
- plp.LpConstraint(
615
- e=P_sto_pos[i] - self.plant_conf['battery_discharge_efficiency']*self.plant_conf['battery_discharge_power_max']*E[i],
616
- sense=plp.LpConstraintLE,
617
- rhs=0)
618
- for i in set_I})
619
- constraints.update({"constraint_pstoneg_{}".format(i) :
620
- plp.LpConstraint(
621
- e=-P_sto_neg[i] - (1/self.plant_conf['battery_charge_efficiency'])*self.plant_conf['battery_charge_power_max']*(1-E[i]),
622
- sense=plp.LpConstraintLE,
623
- rhs=0)
624
- for i in set_I})
625
- constraints.update({"constraint_socmax_{}".format(i) :
626
- plp.LpConstraint(
627
- e=-plp.lpSum(P_sto_pos[j]*(1/self.plant_conf['battery_discharge_efficiency']) + self.plant_conf['battery_charge_efficiency']*P_sto_neg[j] for j in range(i)),
628
- sense=plp.LpConstraintLE,
629
- rhs=(self.plant_conf['battery_nominal_energy_capacity']/self.timeStep)*(self.plant_conf['battery_maximum_state_of_charge'] - soc_init))
630
- for i in set_I})
631
- constraints.update({"constraint_socmin_{}".format(i) :
632
- plp.LpConstraint(
633
- e=plp.lpSum(P_sto_pos[j]*(1/self.plant_conf['battery_discharge_efficiency']) + self.plant_conf['battery_charge_efficiency']*P_sto_neg[j] for j in range(i)),
634
- sense=plp.LpConstraintLE,
635
- rhs=(self.plant_conf['battery_nominal_energy_capacity']/self.timeStep)*(soc_init - self.plant_conf['battery_minimum_state_of_charge']))
636
- for i in set_I})
637
- constraints.update({"constraint_socfinal_{}".format(0) :
638
- plp.LpConstraint(
639
- e=plp.lpSum(P_sto_pos[i]*(1/self.plant_conf['battery_discharge_efficiency']) + self.plant_conf['battery_charge_efficiency']*P_sto_neg[i] for i in set_I),
640
- sense=plp.LpConstraintEQ,
641
- rhs=(soc_init - soc_final)*self.plant_conf['battery_nominal_energy_capacity']/self.timeStep)
642
- })
979
+ constraints.update(
980
+ {
981
+ "constraint_pstopos_{}".format(i): plp.LpConstraint(
982
+ e=P_sto_pos[i]
983
+ - self.plant_conf["battery_discharge_efficiency"]
984
+ * self.plant_conf["battery_discharge_power_max"]
985
+ * E[i],
986
+ sense=plp.LpConstraintLE,
987
+ rhs=0,
988
+ )
989
+ for i in set_I
990
+ }
991
+ )
992
+ constraints.update(
993
+ {
994
+ "constraint_pstoneg_{}".format(i): plp.LpConstraint(
995
+ e=-P_sto_neg[i]
996
+ - (1 / self.plant_conf["battery_charge_efficiency"])
997
+ * self.plant_conf["battery_charge_power_max"]
998
+ * (1 - E[i]),
999
+ sense=plp.LpConstraintLE,
1000
+ rhs=0,
1001
+ )
1002
+ for i in set_I
1003
+ }
1004
+ )
1005
+ constraints.update(
1006
+ {
1007
+ "constraint_socmax_{}".format(i): plp.LpConstraint(
1008
+ e=-plp.lpSum(
1009
+ P_sto_pos[j]
1010
+ * (1 / self.plant_conf["battery_discharge_efficiency"])
1011
+ + self.plant_conf["battery_charge_efficiency"]
1012
+ * P_sto_neg[j]
1013
+ for j in range(i)
1014
+ ),
1015
+ sense=plp.LpConstraintLE,
1016
+ rhs=(
1017
+ self.plant_conf["battery_nominal_energy_capacity"]
1018
+ / self.timeStep
1019
+ )
1020
+ * (
1021
+ self.plant_conf["battery_maximum_state_of_charge"]
1022
+ - soc_init
1023
+ ),
1024
+ )
1025
+ for i in set_I
1026
+ }
1027
+ )
1028
+ constraints.update(
1029
+ {
1030
+ "constraint_socmin_{}".format(i): plp.LpConstraint(
1031
+ e=plp.lpSum(
1032
+ P_sto_pos[j]
1033
+ * (1 / self.plant_conf["battery_discharge_efficiency"])
1034
+ + self.plant_conf["battery_charge_efficiency"]
1035
+ * P_sto_neg[j]
1036
+ for j in range(i)
1037
+ ),
1038
+ sense=plp.LpConstraintLE,
1039
+ rhs=(
1040
+ self.plant_conf["battery_nominal_energy_capacity"]
1041
+ / self.timeStep
1042
+ )
1043
+ * (
1044
+ soc_init
1045
+ - self.plant_conf["battery_minimum_state_of_charge"]
1046
+ ),
1047
+ )
1048
+ for i in set_I
1049
+ }
1050
+ )
1051
+ constraints.update(
1052
+ {
1053
+ "constraint_socfinal_{}".format(0): plp.LpConstraint(
1054
+ e=plp.lpSum(
1055
+ P_sto_pos[i]
1056
+ * (1 / self.plant_conf["battery_discharge_efficiency"])
1057
+ + self.plant_conf["battery_charge_efficiency"]
1058
+ * P_sto_neg[i]
1059
+ for i in set_I
1060
+ ),
1061
+ sense=plp.LpConstraintEQ,
1062
+ rhs=(soc_init - soc_final)
1063
+ * self.plant_conf["battery_nominal_energy_capacity"]
1064
+ / self.timeStep,
1065
+ )
1066
+ }
1067
+ )
643
1068
  opt_model.constraints = constraints
644
1069
 
645
1070
  ## Finally, we call the solver to solve our optimization model:
646
1071
  # solving with default solver CBC
647
- if self.lp_solver == 'PULP_CBC_CMD':
1072
+ if self.lp_solver == "PULP_CBC_CMD":
648
1073
  opt_model.solve(PULP_CBC_CMD(msg=0))
649
- elif self.lp_solver == 'GLPK_CMD':
1074
+ elif self.lp_solver == "GLPK_CMD":
650
1075
  opt_model.solve(GLPK_CMD(msg=0))
651
- elif self.lp_solver == 'COIN_CMD':
1076
+ elif self.lp_solver == "COIN_CMD":
652
1077
  opt_model.solve(COIN_CMD(msg=0, path=self.lp_solver_path))
653
1078
  else:
654
1079
  self.logger.warning("Solver %s unknown, using default", self.lp_solver)
@@ -661,65 +1086,133 @@ class Optimization:
661
1086
  self.logger.warning("Cost function cannot be evaluated")
662
1087
  return
663
1088
  else:
664
- self.logger.info("Total value of the Cost function = %.02f", plp.value(opt_model.objective))
1089
+ self.logger.info(
1090
+ "Total value of the Cost function = %.02f",
1091
+ plp.value(opt_model.objective),
1092
+ )
665
1093
 
666
1094
  # Build results Dataframe
667
1095
  opt_tp = pd.DataFrame()
668
1096
  opt_tp["P_PV"] = [P_PV[i] for i in set_I]
669
1097
  opt_tp["P_Load"] = [P_load[i] for i in set_I]
670
- for k in range(self.optim_conf['number_of_deferrable_loads']):
671
- opt_tp["P_deferrable{}".format(k)] = [P_deferrable[k][i].varValue for i in set_I]
1098
+ for k in range(self.optim_conf["number_of_deferrable_loads"]):
1099
+ opt_tp["P_deferrable{}".format(k)] = [
1100
+ P_deferrable[k][i].varValue for i in set_I
1101
+ ]
672
1102
  opt_tp["P_grid_pos"] = [P_grid_pos[i].varValue for i in set_I]
673
1103
  opt_tp["P_grid_neg"] = [P_grid_neg[i].varValue for i in set_I]
674
- opt_tp["P_grid"] = [P_grid_pos[i].varValue + P_grid_neg[i].varValue for i in set_I]
675
- if self.optim_conf['set_use_battery']:
676
- opt_tp["P_batt"] = [P_sto_pos[i].varValue + P_sto_neg[i].varValue for i in set_I]
677
- SOC_opt_delta = [(P_sto_pos[i].varValue*(1/self.plant_conf['battery_discharge_efficiency']) + \
678
- self.plant_conf['battery_charge_efficiency']*P_sto_neg[i].varValue)*(
679
- self.timeStep/(self.plant_conf['battery_nominal_energy_capacity'])) for i in set_I]
1104
+ opt_tp["P_grid"] = [
1105
+ P_grid_pos[i].varValue + P_grid_neg[i].varValue for i in set_I
1106
+ ]
1107
+ if self.optim_conf["set_use_battery"]:
1108
+ opt_tp["P_batt"] = [
1109
+ P_sto_pos[i].varValue + P_sto_neg[i].varValue for i in set_I
1110
+ ]
1111
+ SOC_opt_delta = [
1112
+ (
1113
+ P_sto_pos[i].varValue
1114
+ * (1 / self.plant_conf["battery_discharge_efficiency"])
1115
+ + self.plant_conf["battery_charge_efficiency"]
1116
+ * P_sto_neg[i].varValue
1117
+ )
1118
+ * (self.timeStep / (self.plant_conf["battery_nominal_energy_capacity"]))
1119
+ for i in set_I
1120
+ ]
680
1121
  SOCinit = copy.copy(soc_init)
681
1122
  SOC_opt = []
682
1123
  for i in set_I:
683
1124
  SOC_opt.append(SOCinit - SOC_opt_delta[i])
684
1125
  SOCinit = SOC_opt[i]
685
1126
  opt_tp["SOC_opt"] = SOC_opt
686
- if self.plant_conf['inverter_is_hybrid']:
1127
+ if self.plant_conf["inverter_is_hybrid"]:
687
1128
  opt_tp["P_hybrid_inverter"] = [P_hybrid_inverter[i].varValue for i in set_I]
688
- if self.plant_conf['compute_curtailment']:
1129
+ if self.plant_conf["compute_curtailment"]:
689
1130
  opt_tp["P_PV_curtailment"] = [P_PV_curtailment[i].varValue for i in set_I]
690
1131
  opt_tp.index = data_opt.index
691
1132
 
692
1133
  # Lets compute the optimal cost function
693
1134
  P_def_sum_tp = []
694
1135
  for i in set_I:
695
- P_def_sum_tp.append(sum(P_deferrable[k][i].varValue for k in range(self.optim_conf['number_of_deferrable_loads'])))
1136
+ P_def_sum_tp.append(
1137
+ sum(
1138
+ P_deferrable[k][i].varValue
1139
+ for k in range(self.optim_conf["number_of_deferrable_loads"])
1140
+ )
1141
+ )
696
1142
  opt_tp["unit_load_cost"] = [unit_load_cost[i] for i in set_I]
697
1143
  opt_tp["unit_prod_price"] = [unit_prod_price[i] for i in set_I]
698
- if self.optim_conf['set_total_pv_sell']:
699
- opt_tp["cost_profit"] = [-0.001*self.timeStep*(unit_load_cost[i]*(P_load[i] + P_def_sum_tp[i]) + \
700
- unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
1144
+ if self.optim_conf["set_total_pv_sell"]:
1145
+ opt_tp["cost_profit"] = [
1146
+ -0.001
1147
+ * self.timeStep
1148
+ * (
1149
+ unit_load_cost[i] * (P_load[i] + P_def_sum_tp[i])
1150
+ + unit_prod_price[i] * P_grid_neg[i].varValue
1151
+ )
1152
+ for i in set_I
1153
+ ]
701
1154
  else:
702
- opt_tp["cost_profit"] = [-0.001*self.timeStep*(unit_load_cost[i]*P_grid_pos[i].varValue + \
703
- unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
1155
+ opt_tp["cost_profit"] = [
1156
+ -0.001
1157
+ * self.timeStep
1158
+ * (
1159
+ unit_load_cost[i] * P_grid_pos[i].varValue
1160
+ + unit_prod_price[i] * P_grid_neg[i].varValue
1161
+ )
1162
+ for i in set_I
1163
+ ]
704
1164
 
705
- if self.costfun == 'profit':
706
- if self.optim_conf['set_total_pv_sell']:
707
- opt_tp["cost_fun_profit"] = [-0.001*self.timeStep*(unit_load_cost[i]*(P_load[i] + P_def_sum_tp[i]) + \
708
- unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
1165
+ if self.costfun == "profit":
1166
+ if self.optim_conf["set_total_pv_sell"]:
1167
+ opt_tp["cost_fun_profit"] = [
1168
+ -0.001
1169
+ * self.timeStep
1170
+ * (
1171
+ unit_load_cost[i] * (P_load[i] + P_def_sum_tp[i])
1172
+ + unit_prod_price[i] * P_grid_neg[i].varValue
1173
+ )
1174
+ for i in set_I
1175
+ ]
709
1176
  else:
710
- opt_tp["cost_fun_profit"] = [-0.001*self.timeStep*(unit_load_cost[i]*P_grid_pos[i].varValue + \
711
- unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
712
- elif self.costfun == 'cost':
713
- if self.optim_conf['set_total_pv_sell']:
714
- opt_tp["cost_fun_cost"] = [-0.001*self.timeStep*unit_load_cost[i]*(P_load[i] + P_def_sum_tp[i]) for i in set_I]
1177
+ opt_tp["cost_fun_profit"] = [
1178
+ -0.001
1179
+ * self.timeStep
1180
+ * (
1181
+ unit_load_cost[i] * P_grid_pos[i].varValue
1182
+ + unit_prod_price[i] * P_grid_neg[i].varValue
1183
+ )
1184
+ for i in set_I
1185
+ ]
1186
+ elif self.costfun == "cost":
1187
+ if self.optim_conf["set_total_pv_sell"]:
1188
+ opt_tp["cost_fun_cost"] = [
1189
+ -0.001
1190
+ * self.timeStep
1191
+ * unit_load_cost[i]
1192
+ * (P_load[i] + P_def_sum_tp[i])
1193
+ for i in set_I
1194
+ ]
715
1195
  else:
716
- opt_tp["cost_fun_cost"] = [-0.001*self.timeStep*unit_load_cost[i]*P_grid_pos[i].varValue for i in set_I]
717
- elif self.costfun == 'self-consumption':
718
- if type_self_conso == 'maxmin':
719
- opt_tp["cost_fun_selfcons"] = [-0.001*self.timeStep*unit_load_cost[i]*SC[i].varValue for i in set_I]
720
- elif type_self_conso == 'bigm':
721
- opt_tp["cost_fun_selfcons"] = [-0.001*self.timeStep*(unit_load_cost[i]*P_grid_pos[i].varValue + \
722
- unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
1196
+ opt_tp["cost_fun_cost"] = [
1197
+ -0.001 * self.timeStep * unit_load_cost[i] * P_grid_pos[i].varValue
1198
+ for i in set_I
1199
+ ]
1200
+ elif self.costfun == "self-consumption":
1201
+ if type_self_conso == "maxmin":
1202
+ opt_tp["cost_fun_selfcons"] = [
1203
+ -0.001 * self.timeStep * unit_load_cost[i] * SC[i].varValue
1204
+ for i in set_I
1205
+ ]
1206
+ elif type_self_conso == "bigm":
1207
+ opt_tp["cost_fun_selfcons"] = [
1208
+ -0.001
1209
+ * self.timeStep
1210
+ * (
1211
+ unit_load_cost[i] * P_grid_pos[i].varValue
1212
+ + unit_prod_price[i] * P_grid_neg[i].varValue
1213
+ )
1214
+ for i in set_I
1215
+ ]
723
1216
  else:
724
1217
  self.logger.error("The cost function specified type is not valid")
725
1218
 
@@ -728,16 +1221,31 @@ class Optimization:
728
1221
 
729
1222
  # Debug variables
730
1223
  if debug:
731
- for k in range(self.optim_conf['number_of_deferrable_loads']):
1224
+ for k in range(self.optim_conf["number_of_deferrable_loads"]):
732
1225
  opt_tp[f"P_def_start_{k}"] = [P_def_start[k][i].varValue for i in set_I]
733
1226
  opt_tp[f"P_def_bin2_{k}"] = [P_def_bin2[k][i].varValue for i in set_I]
734
1227
  for i, predicted_temp in predicted_temps.items():
735
- opt_tp[f"predicted_temp_heater{i}"] = pd.Series([round(pt.value(), 2) if isinstance(pt, plp.LpAffineExpression) else pt for pt in predicted_temp], index=opt_tp.index)
736
- opt_tp[f"target_temp_heater{i}"] = pd.Series(self.optim_conf["def_load_config"][i]['thermal_config']["desired_temperatures"], index=opt_tp.index)
1228
+ opt_tp[f"predicted_temp_heater{i}"] = pd.Series(
1229
+ [
1230
+ round(pt.value(), 2)
1231
+ if isinstance(pt, plp.LpAffineExpression)
1232
+ else pt
1233
+ for pt in predicted_temp
1234
+ ],
1235
+ index=opt_tp.index,
1236
+ )
1237
+ opt_tp[f"target_temp_heater{i}"] = pd.Series(
1238
+ self.optim_conf["def_load_config"][i]["thermal_config"][
1239
+ "desired_temperatures"
1240
+ ],
1241
+ index=opt_tp.index,
1242
+ )
737
1243
 
738
1244
  return opt_tp
739
1245
 
740
- def perform_perfect_forecast_optim(self, df_input_data: pd.DataFrame, days_list: pd.date_range) -> pd.DataFrame:
1246
+ def perform_perfect_forecast_optim(
1247
+ self, df_input_data: pd.DataFrame, days_list: pd.date_range
1248
+ ) -> pd.DataFrame:
741
1249
  r"""
742
1250
  Perform an optimization on historical data (perfectly known PV production).
743
1251
 
@@ -753,21 +1261,33 @@ class Optimization:
753
1261
 
754
1262
  """
755
1263
  self.logger.info("Perform optimization for perfect forecast scenario")
756
- self.days_list_tz = days_list.tz_convert(self.time_zone).round(self.freq)[:-1] # Converted to tz and without the current day (today)
1264
+ self.days_list_tz = days_list.tz_convert(self.time_zone).round(self.freq)[
1265
+ :-1
1266
+ ] # Converted to tz and without the current day (today)
757
1267
  self.opt_res = pd.DataFrame()
758
1268
  for day in self.days_list_tz:
759
- self.logger.info("Solving for day: "+str(day.day)+"-"+str(day.month)+"-"+str(day.year))
1269
+ self.logger.info(
1270
+ "Solving for day: "
1271
+ + str(day.day)
1272
+ + "-"
1273
+ + str(day.month)
1274
+ + "-"
1275
+ + str(day.year)
1276
+ )
760
1277
  # Prepare data
761
1278
  day_start = day.isoformat()
762
- day_end = (day+self.time_delta-self.freq).isoformat()
763
- data_tp = df_input_data.copy().loc[pd.date_range(start=day_start, end=day_end, freq=self.freq)]
1279
+ day_end = (day + self.time_delta - self.freq).isoformat()
1280
+ data_tp = df_input_data.copy().loc[
1281
+ pd.date_range(start=day_start, end=day_end, freq=self.freq)
1282
+ ]
764
1283
  P_PV = data_tp[self.var_PV].values
765
1284
  P_load = data_tp[self.var_load_new].values
766
- unit_load_cost = data_tp[self.var_load_cost].values # €/kWh
767
- unit_prod_price = data_tp[self.var_prod_price].values # €/kWh
1285
+ unit_load_cost = data_tp[self.var_load_cost].values # €/kWh
1286
+ unit_prod_price = data_tp[self.var_prod_price].values # €/kWh
768
1287
  # Call optimization function
769
- opt_tp = self.perform_optimization(data_tp, P_PV, P_load,
770
- unit_load_cost, unit_prod_price)
1288
+ opt_tp = self.perform_optimization(
1289
+ data_tp, P_PV, P_load, unit_load_cost, unit_prod_price
1290
+ )
771
1291
  if len(self.opt_res) == 0:
772
1292
  self.opt_res = opt_tp
773
1293
  else:
@@ -775,8 +1295,9 @@ class Optimization:
775
1295
 
776
1296
  return self.opt_res
777
1297
 
778
- def perform_dayahead_forecast_optim(self, df_input_data: pd.DataFrame,
779
- P_PV: pd.Series, P_load: pd.Series) -> pd.DataFrame:
1298
+ def perform_dayahead_forecast_optim(
1299
+ self, df_input_data: pd.DataFrame, P_PV: pd.Series, P_load: pd.Series
1300
+ ) -> pd.DataFrame:
780
1301
  r"""
781
1302
  Perform a day-ahead optimization task using real forecast data. \
782
1303
  This type of optimization is intented to be launched once a day.
@@ -794,19 +1315,30 @@ class Optimization:
794
1315
 
795
1316
  """
796
1317
  self.logger.info("Perform optimization for the day-ahead")
797
- unit_load_cost = df_input_data[self.var_load_cost].values # €/kWh
798
- unit_prod_price = df_input_data[self.var_prod_price].values # €/kWh
1318
+ unit_load_cost = df_input_data[self.var_load_cost].values # €/kWh
1319
+ unit_prod_price = df_input_data[self.var_prod_price].values # €/kWh
799
1320
  # Call optimization function
800
- self.opt_res = self.perform_optimization(df_input_data, P_PV.values.ravel(),
801
- P_load.values.ravel(),
802
- unit_load_cost, unit_prod_price)
1321
+ self.opt_res = self.perform_optimization(
1322
+ df_input_data,
1323
+ P_PV.values.ravel(),
1324
+ P_load.values.ravel(),
1325
+ unit_load_cost,
1326
+ unit_prod_price,
1327
+ )
803
1328
  return self.opt_res
804
1329
 
805
- def perform_naive_mpc_optim(self, df_input_data: pd.DataFrame, P_PV: pd.Series, P_load: pd.Series,
806
- prediction_horizon: int, soc_init: Optional[float] = None, soc_final: Optional[float] = None,
807
- def_total_hours: Optional[list] = None,
808
- def_start_timestep: Optional[list] = None,
809
- def_end_timestep: Optional[list] = None) -> pd.DataFrame:
1330
+ def perform_naive_mpc_optim(
1331
+ self,
1332
+ df_input_data: pd.DataFrame,
1333
+ P_PV: pd.Series,
1334
+ P_load: pd.Series,
1335
+ prediction_horizon: int,
1336
+ soc_init: Optional[float] = None,
1337
+ soc_final: Optional[float] = None,
1338
+ def_total_hours: Optional[list] = None,
1339
+ def_start_timestep: Optional[list] = None,
1340
+ def_end_timestep: Optional[list] = None,
1341
+ ) -> pd.DataFrame:
810
1342
  r"""
811
1343
  Perform a naive approach to a Model Predictive Control (MPC). \
812
1344
  This implementaion is naive because we are not using the formal formulation \
@@ -843,24 +1375,38 @@ class Optimization:
843
1375
  """
844
1376
  self.logger.info("Perform an iteration of a naive MPC controller")
845
1377
  if prediction_horizon < 5:
846
- self.logger.error("Set the MPC prediction horizon to at least 5 times the optimization time step")
1378
+ self.logger.error(
1379
+ "Set the MPC prediction horizon to at least 5 times the optimization time step"
1380
+ )
847
1381
  return pd.DataFrame()
848
1382
  else:
849
- df_input_data = copy.deepcopy(df_input_data)[df_input_data.index[0]:df_input_data.index[prediction_horizon-1]]
850
- unit_load_cost = df_input_data[self.var_load_cost].values # €/kWh
851
- unit_prod_price = df_input_data[self.var_prod_price].values # €/kWh
1383
+ df_input_data = copy.deepcopy(df_input_data)[
1384
+ df_input_data.index[0] : df_input_data.index[prediction_horizon - 1]
1385
+ ]
1386
+ unit_load_cost = df_input_data[self.var_load_cost].values # €/kWh
1387
+ unit_prod_price = df_input_data[self.var_prod_price].values # €/kWh
852
1388
  # Call optimization function
853
- self.opt_res = self.perform_optimization(df_input_data, P_PV.values.ravel(), P_load.values.ravel(),
854
- unit_load_cost, unit_prod_price, soc_init=soc_init,
855
- soc_final=soc_final, def_total_hours=def_total_hours,
856
- def_start_timestep=def_start_timestep, def_end_timestep=def_end_timestep)
1389
+ self.opt_res = self.perform_optimization(
1390
+ df_input_data,
1391
+ P_PV.values.ravel(),
1392
+ P_load.values.ravel(),
1393
+ unit_load_cost,
1394
+ unit_prod_price,
1395
+ soc_init=soc_init,
1396
+ soc_final=soc_final,
1397
+ def_total_hours=def_total_hours,
1398
+ def_start_timestep=def_start_timestep,
1399
+ def_end_timestep=def_end_timestep,
1400
+ )
857
1401
  return self.opt_res
858
1402
 
859
1403
  @staticmethod
860
- def validate_def_timewindow(start: int, end: int, min_steps: int, window: int) -> Tuple[int,int,str]:
1404
+ def validate_def_timewindow(
1405
+ start: int, end: int, min_steps: int, window: int
1406
+ ) -> Tuple[int, int, str]:
861
1407
  r"""
862
1408
  Helper function to validate (and if necessary: correct) the defined optimization window of a deferrable load.
863
-
1409
+
864
1410
  :param start: Start timestep of the optimization window of the deferrable load
865
1411
  :type start: int
866
1412
  :param end: End timestep of the optimization window of the deferrable load
@@ -887,7 +1433,7 @@ class Optimization:
887
1433
  end_validated = max(0, min(window, end))
888
1434
  if end_validated > 0:
889
1435
  # If the available timeframe is shorter than the number of timesteps needed to meet the hours to operate (def_total_hours), issue a warning.
890
- if (end_validated-start_validated) < min_steps:
1436
+ if (end_validated - start_validated) < min_steps:
891
1437
  warning = "Available timeframe is shorter than the specified number of hours to operate. Optimization will fail."
892
1438
  else:
893
1439
  warning = "Invalid timeframe for deferrable load (start timestep is not <= end timestep). Continuing optimization without timewindow constraint."