emhass 0.9.0__py3-none-any.whl → 0.10.0__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/command_line.py +392 -286
- emhass/forecast.py +21 -21
- emhass/machine_learning_regressor.py +14 -53
- emhass/optimization.py +261 -82
- emhass/retrieve_hass.py +76 -101
- emhass/utils.py +70 -128
- emhass/web_server.py +32 -7
- {emhass-0.9.0.dist-info → emhass-0.10.0.dist-info}/METADATA +125 -19
- {emhass-0.9.0.dist-info → emhass-0.10.0.dist-info}/RECORD +13 -13
- {emhass-0.9.0.dist-info → emhass-0.10.0.dist-info}/LICENSE +0 -0
- {emhass-0.9.0.dist-info → emhass-0.10.0.dist-info}/WHEEL +0 -0
- {emhass-0.9.0.dist-info → emhass-0.10.0.dist-info}/entry_points.txt +0 -0
- {emhass-0.9.0.dist-info → emhass-0.10.0.dist-info}/top_level.txt +0 -0
emhass/optimization.py
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
|
4
4
|
import logging
|
5
5
|
import copy
|
6
|
+
import pathlib
|
7
|
+
import bz2
|
8
|
+
import pickle as cPickle
|
6
9
|
from typing import Optional, Tuple
|
7
10
|
import pandas as pd
|
8
11
|
import numpy as np
|
@@ -89,7 +92,7 @@ class Optimization:
|
|
89
92
|
if self.lp_solver == 'COIN_CMD' and self.lp_solver_path == 'empty': #if COIN_CMD but lp_solver_path is empty
|
90
93
|
self.logger.warning("lp_solver=COIN_CMD but lp_solver_path=empty, attempting to use lp_solver_path=/usr/bin/cbc")
|
91
94
|
self.lp_solver_path = '/usr/bin/cbc'
|
92
|
-
|
95
|
+
|
93
96
|
def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: np.array,
|
94
97
|
unit_load_cost: np.array, unit_prod_price: np.array,
|
95
98
|
soc_init: Optional[float] = None, soc_final: Optional[float] = None,
|
@@ -153,14 +156,14 @@ class Optimization:
|
|
153
156
|
if def_end_timestep is None:
|
154
157
|
def_end_timestep = self.optim_conf['def_end_timestep']
|
155
158
|
type_self_conso = 'bigm' # maxmin
|
156
|
-
|
159
|
+
|
157
160
|
#### The LP problem using Pulp ####
|
158
161
|
opt_model = plp.LpProblem("LP_Model", plp.LpMaximize)
|
159
|
-
|
162
|
+
|
160
163
|
n = len(data_opt.index)
|
161
164
|
set_I = range(n)
|
162
165
|
M = 10e10
|
163
|
-
|
166
|
+
|
164
167
|
## Add decision variables
|
165
168
|
P_grid_neg = {(i):plp.LpVariable(cat='Continuous',
|
166
169
|
lowBound=-self.plant_conf['P_to_grid_max'], upBound=0,
|
@@ -171,12 +174,16 @@ class Optimization:
|
|
171
174
|
P_deferrable = []
|
172
175
|
P_def_bin1 = []
|
173
176
|
for k in range(self.optim_conf['num_def_loads']):
|
177
|
+
if type(self.optim_conf['P_deferrable_nom'][k]) == list:
|
178
|
+
upBound = np.max(self.optim_conf['P_deferrable_nom'][k])
|
179
|
+
else:
|
180
|
+
upBound = self.optim_conf['P_deferrable_nom'][k]
|
174
181
|
if self.optim_conf['treat_def_as_semi_cont'][k]:
|
175
182
|
P_deferrable.append({(i):plp.LpVariable(cat='Continuous',
|
176
183
|
name="P_deferrable{}_{}".format(k, i)) for i in set_I})
|
177
184
|
else:
|
178
185
|
P_deferrable.append({(i):plp.LpVariable(cat='Continuous',
|
179
|
-
lowBound=0, upBound=
|
186
|
+
lowBound=0, upBound=upBound,
|
180
187
|
name="P_deferrable{}_{}".format(k, i)) for i in set_I})
|
181
188
|
P_def_bin1.append({(i):plp.LpVariable(cat='Binary',
|
182
189
|
name="P_def{}_bin1_{}".format(k, i)) for i in set_I})
|
@@ -201,11 +208,16 @@ class Optimization:
|
|
201
208
|
else:
|
202
209
|
P_sto_pos = {(i):i*0 for i in set_I}
|
203
210
|
P_sto_neg = {(i):i*0 for i in set_I}
|
204
|
-
|
211
|
+
|
205
212
|
if self.costfun == 'self-consumption':
|
206
213
|
SC = {(i):plp.LpVariable(cat='Continuous',
|
207
214
|
name="SC_{}".format(i)) for i in set_I}
|
208
|
-
|
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
|
+
|
209
221
|
## Define objective
|
210
222
|
P_def_sum= []
|
211
223
|
for i in set_I:
|
@@ -238,17 +250,116 @@ class Optimization:
|
|
238
250
|
objective = objective + plp.lpSum(-0.001*self.timeStep*(
|
239
251
|
self.optim_conf['weight_battery_discharge']*P_sto_pos[i] + \
|
240
252
|
self.optim_conf['weight_battery_charge']*P_sto_neg[i]) for i in set_I)
|
253
|
+
|
254
|
+
# Add term penalizing each startup where configured
|
255
|
+
if ("def_start_penalty" in self.optim_conf and self.optim_conf["def_start_penalty"]):
|
256
|
+
for k in range(self.optim_conf["num_def_loads"]):
|
257
|
+
if (len(self.optim_conf["def_start_penalty"]) > k and self.optim_conf["def_start_penalty"][k]):
|
258
|
+
objective = objective + plp.lpSum(
|
259
|
+
-0.001 * self.timeStep * self.optim_conf["def_start_penalty"][k] * P_def_start[k][i] *\
|
260
|
+
unit_load_cost[i] * self.optim_conf['P_deferrable_nom'][k]
|
261
|
+
for i in set_I)
|
262
|
+
|
241
263
|
opt_model.setObjective(objective)
|
242
|
-
|
264
|
+
|
243
265
|
## Setting constraints
|
244
266
|
# The main constraint: power balance
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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}
|
274
|
+
else:
|
275
|
+
constraints = {"constraint_main1_{}".format(i) :
|
276
|
+
plp.LpConstraint(
|
277
|
+
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],
|
278
|
+
sense = plp.LpConstraintEQ,
|
279
|
+
rhs = 0)
|
280
|
+
for i in set_I}
|
281
|
+
|
282
|
+
# Constraint for hybrid inverter and curtailment cases
|
283
|
+
if type(self.plant_conf['module_model']) == list:
|
284
|
+
P_nom_inverter = 0.0
|
285
|
+
for i in range(len(self.plant_conf['inverter_model'])):
|
286
|
+
if type(self.plant_conf['inverter_model'][i]) == str:
|
287
|
+
cec_inverters = bz2.BZ2File(pathlib.Path(__file__).parent / 'data/cec_inverters.pbz2', "rb")
|
288
|
+
cec_inverters = cPickle.load(cec_inverters)
|
289
|
+
inverter = cec_inverters[self.plant_conf['inverter_model'][i]]
|
290
|
+
P_nom_inverter += inverter.Paco
|
291
|
+
else:
|
292
|
+
P_nom_inverter += self.plant_conf['inverter_model'][i]
|
293
|
+
else:
|
294
|
+
if type(self.plant_conf['inverter_model'][i]) == str:
|
295
|
+
cec_inverters = bz2.BZ2File(pathlib.Path(__file__).parent / 'data/cec_inverters.pbz2', "rb")
|
296
|
+
cec_inverters = cPickle.load(cec_inverters)
|
297
|
+
inverter = cec_inverters[self.plant_conf['inverter_model']]
|
298
|
+
P_nom_inverter = inverter.Paco
|
299
|
+
else:
|
300
|
+
P_nom_inverter = self.plant_conf['inverter_model']
|
301
|
+
if self.plant_conf['inverter_is_hybrid']:
|
302
|
+
constraints.update({"constraint_hybrid_inverter1_{}".format(i) :
|
303
|
+
plp.LpConstraint(
|
304
|
+
e = P_PV[i] - P_PV_curtailment[i] + P_sto_pos[i] + P_sto_neg[i] - P_nom_inverter,
|
305
|
+
sense = plp.LpConstraintLE,
|
306
|
+
rhs = 0)
|
307
|
+
for i in set_I})
|
308
|
+
constraints.update({"constraint_hybrid_inverter2_{}".format(i) :
|
309
|
+
plp.LpConstraint(
|
310
|
+
e = P_PV[i] - P_PV_curtailment[i] + P_sto_pos[i] + P_sto_neg[i] - P_hybrid_inverter[i],
|
311
|
+
sense = plp.LpConstraintEQ,
|
312
|
+
rhs = 0)
|
313
|
+
for i in set_I})
|
314
|
+
else:
|
315
|
+
constraints.update({"constraint_curtailment_{}".format(i) :
|
316
|
+
plp.LpConstraint(
|
317
|
+
e = P_PV[i] - P_PV_curtailment[i] - P_nom_inverter,
|
318
|
+
sense = plp.LpConstraintLE,
|
319
|
+
rhs = 0)
|
320
|
+
for i in set_I})
|
251
321
|
|
322
|
+
# Constraint for sequence of deferrable
|
323
|
+
# WARNING: This is experimental, formulation seems correct but feasibility problems.
|
324
|
+
# Probably uncomptabile with other constraints
|
325
|
+
for k in range(self.optim_conf['num_def_loads']):
|
326
|
+
if type(self.optim_conf['P_deferrable_nom'][k]) == list:
|
327
|
+
power_sequence = self.optim_conf['P_deferrable_nom'][k]
|
328
|
+
sequence_length = len(power_sequence)
|
329
|
+
def create_matrix(input_list, n):
|
330
|
+
matrix = []
|
331
|
+
for i in range(n + 1):
|
332
|
+
row = [0] * i + input_list + [0] * (n - i)
|
333
|
+
matrix.append(row[:n*2])
|
334
|
+
return matrix
|
335
|
+
matrix = create_matrix(power_sequence, n-sequence_length)
|
336
|
+
y = plp.LpVariable.dicts(f"y{k}", (i for i in range(len(matrix))), cat='Binary')
|
337
|
+
constraints.update({f"single_value_constraint_{k}" :
|
338
|
+
plp.LpConstraint(
|
339
|
+
e = plp.lpSum(y[i] for i in range(len(matrix))) - 1,
|
340
|
+
sense = plp.LpConstraintEQ,
|
341
|
+
rhs = 0)
|
342
|
+
})
|
343
|
+
constraints.update({f"pdef{k}_sumconstraint_{i}" :
|
344
|
+
plp.LpConstraint(
|
345
|
+
e = plp.lpSum(P_deferrable[k][i] for i in set_I) - np.sum(power_sequence),
|
346
|
+
sense = plp.LpConstraintEQ,
|
347
|
+
rhs = 0)
|
348
|
+
})
|
349
|
+
constraints.update({f"pdef{k}_positive_constraint_{i}" :
|
350
|
+
plp.LpConstraint(
|
351
|
+
e = P_deferrable[k][i],
|
352
|
+
sense = plp.LpConstraintGE,
|
353
|
+
rhs = 0)
|
354
|
+
for i in set_I})
|
355
|
+
for num, mat in enumerate(matrix):
|
356
|
+
constraints.update({f"pdef{k}_value_constraint_{num}_{i}" :
|
357
|
+
plp.LpConstraint(
|
358
|
+
e = P_deferrable[k][i] - mat[i]*y[num],
|
359
|
+
sense = plp.LpConstraintEQ,
|
360
|
+
rhs = 0)
|
361
|
+
for i in set_I})
|
362
|
+
|
252
363
|
# Two special constraints just for a self-consumption cost function
|
253
364
|
if self.costfun == 'self-consumption':
|
254
365
|
if type_self_conso == 'maxmin': # maxmin linear problem
|
@@ -264,7 +375,7 @@ class Optimization:
|
|
264
375
|
sense = plp.LpConstraintLE,
|
265
376
|
rhs = 0)
|
266
377
|
for i in set_I})
|
267
|
-
|
378
|
+
|
268
379
|
# Avoid injecting and consuming from grid at the same time
|
269
380
|
constraints.update({"constraint_pgridpos_{}".format(i) :
|
270
381
|
plp.LpConstraint(
|
@@ -278,79 +389,145 @@ class Optimization:
|
|
278
389
|
sense = plp.LpConstraintLE,
|
279
390
|
rhs = 0)
|
280
391
|
for i in set_I})
|
281
|
-
|
392
|
+
|
282
393
|
# Treat deferrable loads constraints
|
283
394
|
for k in range(self.optim_conf['num_def_loads']):
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
rhs = def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k])
|
290
|
-
})
|
291
|
-
# Ensure deferrable loads consume energy between def_start_timestep & def_end_timestep
|
292
|
-
self.logger.debug("Deferrable load {}: Proposed optimization window: {} --> {}".format(k, def_start_timestep[k], def_end_timestep[k]))
|
293
|
-
def_start, def_end, warning = Optimization.validate_def_timewindow(def_start_timestep[k], def_end_timestep[k], ceil(def_total_hours[k]/self.timeStep), n)
|
294
|
-
if warning is not None:
|
295
|
-
self.logger.warning("Deferrable load {} : {}".format(k, warning))
|
296
|
-
self.logger.debug("Deferrable load {}: Validated optimization window: {} --> {}".format(k, def_start, def_end))
|
297
|
-
if def_start > 0:
|
298
|
-
constraints.update({"constraint_defload{}_start_timestep".format(k) :
|
395
|
+
if type(self.optim_conf['P_deferrable_nom'][k]) == list:
|
396
|
+
continue
|
397
|
+
else:
|
398
|
+
# Total time of deferrable load
|
399
|
+
constraints.update({"constraint_defload{}_energy".format(k) :
|
299
400
|
plp.LpConstraint(
|
300
|
-
e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in
|
401
|
+
e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in set_I),
|
301
402
|
sense = plp.LpConstraintEQ,
|
302
|
-
rhs =
|
403
|
+
rhs = def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k])
|
303
404
|
})
|
304
|
-
|
305
|
-
|
405
|
+
# Ensure deferrable loads consume energy between def_start_timestep & def_end_timestep
|
406
|
+
self.logger.debug("Deferrable load {}: Proposed optimization window: {} --> {}".format(
|
407
|
+
k, def_start_timestep[k], def_end_timestep[k]))
|
408
|
+
def_start, def_end, warning = Optimization.validate_def_timewindow(
|
409
|
+
def_start_timestep[k], def_end_timestep[k], ceil(def_total_hours[k]/self.timeStep), n)
|
410
|
+
if warning is not None:
|
411
|
+
self.logger.warning("Deferrable load {} : {}".format(k, warning))
|
412
|
+
self.logger.debug("Deferrable load {}: Validated optimization window: {} --> {}".format(
|
413
|
+
k, def_start, def_end))
|
414
|
+
if def_start > 0:
|
415
|
+
constraints.update({"constraint_defload{}_start_timestep".format(k) :
|
416
|
+
plp.LpConstraint(
|
417
|
+
e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in range(0, def_start)),
|
418
|
+
sense = plp.LpConstraintEQ,
|
419
|
+
rhs = 0)
|
420
|
+
})
|
421
|
+
if def_end > 0:
|
422
|
+
constraints.update({"constraint_defload{}_end_timestep".format(k) :
|
423
|
+
plp.LpConstraint(
|
424
|
+
e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in range(def_end, n)),
|
425
|
+
sense = plp.LpConstraintEQ,
|
426
|
+
rhs = 0)
|
427
|
+
})
|
428
|
+
# Treat deferrable load as a semi-continuous variable
|
429
|
+
if self.optim_conf['treat_def_as_semi_cont'][k]:
|
430
|
+
constraints.update({"constraint_pdef{}_semicont1_{}".format(k, i) :
|
431
|
+
plp.LpConstraint(
|
432
|
+
e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i],
|
433
|
+
sense=plp.LpConstraintGE,
|
434
|
+
rhs=0)
|
435
|
+
for i in set_I})
|
436
|
+
constraints.update({"constraint_pdef{}_semicont2_{}".format(k, i) :
|
437
|
+
plp.LpConstraint(
|
438
|
+
e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i],
|
439
|
+
sense=plp.LpConstraintLE,
|
440
|
+
rhs=0)
|
441
|
+
for i in set_I})
|
442
|
+
# Treat the number of starts for a deferrable load
|
443
|
+
if self.optim_conf['set_def_constant'][k]:
|
444
|
+
constraints.update({"constraint_pdef{}_start1_{}".format(k, i) :
|
445
|
+
plp.LpConstraint(
|
446
|
+
e=P_deferrable[k][i] - P_def_bin2[k][i]*M,
|
447
|
+
sense=plp.LpConstraintLE,
|
448
|
+
rhs=0)
|
449
|
+
for i in set_I})
|
450
|
+
constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
|
451
|
+
plp.LpConstraint(
|
452
|
+
e=P_def_start[k][i] - P_def_bin2[k][i] + (P_def_bin2[k][i-1] if i-1 >= 0 else 0),
|
453
|
+
sense=plp.LpConstraintGE,
|
454
|
+
rhs=0)
|
455
|
+
for i in set_I})
|
456
|
+
constraints.update({"constraint_pdef{}_start3".format(k) :
|
306
457
|
plp.LpConstraint(
|
307
|
-
e = plp.lpSum(
|
458
|
+
e = plp.lpSum(P_def_start[k][i] for i in set_I),
|
308
459
|
sense = plp.LpConstraintEQ,
|
309
|
-
rhs =
|
460
|
+
rhs = 1)
|
310
461
|
})
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
462
|
+
# Treat deferrable load as a semi-continuous variable
|
463
|
+
if self.optim_conf['treat_def_as_semi_cont'][k]:
|
464
|
+
constraints.update({"constraint_pdef{}_semicont1_{}".format(k, i) :
|
465
|
+
plp.LpConstraint(
|
466
|
+
e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i],
|
467
|
+
sense=plp.LpConstraintGE,
|
468
|
+
rhs=0)
|
469
|
+
for i in set_I})
|
470
|
+
constraints.update({"constraint_pdef{}_semicont2_{}".format(k, i) :
|
471
|
+
plp.LpConstraint(
|
472
|
+
e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i],
|
473
|
+
sense=plp.LpConstraintLE,
|
474
|
+
rhs=0)
|
475
|
+
for i in set_I})
|
476
|
+
# Treat the number of starts for a deferrable load
|
477
|
+
current_state = 0
|
478
|
+
if ("def_current_state" in self.optim_conf and len(self.optim_conf["def_current_state"]) > k):
|
479
|
+
current_state = 1 if self.optim_conf["def_current_state"][k] else 0
|
480
|
+
# P_deferrable < P_def_bin2 * 1 million
|
481
|
+
# P_deferrable must be zero if P_def_bin2 is zero
|
482
|
+
constraints.update({"constraint_pdef{}_start1_{}".format(k, i):
|
321
483
|
plp.LpConstraint(
|
322
|
-
e=P_deferrable[k][i] -
|
484
|
+
e=P_deferrable[k][i] - P_def_bin2[k][i] * M,
|
323
485
|
sense=plp.LpConstraintLE,
|
324
486
|
rhs=0)
|
325
487
|
for i in set_I})
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
constraints.update({"constraint_pdef{}_start1_{}".format(k, i) :
|
488
|
+
# P_deferrable - P_def_bin2 <= 0
|
489
|
+
# P_def_bin2 must be zero if P_deferrable is zero
|
490
|
+
constraints.update({"constraint_pdef{}_start1a_{}".format(k, i):
|
330
491
|
plp.LpConstraint(
|
331
|
-
e=
|
492
|
+
e=P_def_bin2[k][i] - P_deferrable[k][i],
|
332
493
|
sense=plp.LpConstraintLE,
|
333
494
|
rhs=0)
|
334
495
|
for i in set_I})
|
496
|
+
# P_def_start + P_def_bin2[i-1] >= P_def_bin2[i]
|
497
|
+
# 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
|
498
|
+
# For first timestep, use current state if provided by caller.
|
335
499
|
constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
|
336
500
|
plp.LpConstraint(
|
337
|
-
e=P_def_start[k][i]
|
501
|
+
e=P_def_start[k][i]
|
502
|
+
- P_def_bin2[k][i]
|
503
|
+
+ (P_def_bin2[k][i - 1] if i - 1 >= 0 else current_state),
|
338
504
|
sense=plp.LpConstraintGE,
|
339
505
|
rhs=0)
|
340
506
|
for i in set_I})
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
507
|
+
# P_def_bin2[i-1] + P_def_start <= 1
|
508
|
+
# If load started this cycle (P_def_start[i] is 1) then P_def_bin2[i-1] must be 0
|
509
|
+
constraints.update({"constraint_pdef{}_start3_{}".format(k, i):
|
510
|
+
plp.LpConstraint(
|
511
|
+
e=(P_def_bin2[k][i-1] if i-1 >= 0 else 0) + P_def_start[k][i],
|
512
|
+
sense=plp.LpConstraintLE,
|
513
|
+
rhs=1)
|
514
|
+
for i in set_I})
|
515
|
+
if self.optim_conf['set_def_constant'][k]:
|
516
|
+
# P_def_start[i] must be 1 for exactly 1 value of i
|
517
|
+
constraints.update({"constraint_pdef{}_start4".format(k) :
|
518
|
+
plp.LpConstraint(
|
519
|
+
e = plp.lpSum(P_def_start[k][i] for i in set_I),
|
520
|
+
sense = plp.LpConstraintEQ,
|
521
|
+
rhs = 1)
|
522
|
+
})
|
523
|
+
# P_def_bin2 must be 1 for exactly the correct number of timesteps.
|
524
|
+
constraints.update({"constraint_pdef{}_start5".format(k) :
|
525
|
+
plp.LpConstraint(
|
526
|
+
e = plp.lpSum(P_def_bin2[k][i] for i in set_I),
|
527
|
+
sense = plp.LpConstraintEQ,
|
528
|
+
rhs = def_total_hours[k]/self.timeStep)
|
529
|
+
})
|
530
|
+
|
354
531
|
# The battery constraints
|
355
532
|
if self.optim_conf['set_use_battery']:
|
356
533
|
# Optional constraints to avoid charging the battery from the grid
|
@@ -423,7 +600,7 @@ class Optimization:
|
|
423
600
|
rhs=(soc_init - soc_final)*self.plant_conf['Enom']/self.timeStep)
|
424
601
|
})
|
425
602
|
opt_model.constraints = constraints
|
426
|
-
|
603
|
+
|
427
604
|
## Finally, we call the solver to solve our optimization model:
|
428
605
|
# solving with default solver CBC
|
429
606
|
if self.lp_solver == 'PULP_CBC_CMD':
|
@@ -435,7 +612,7 @@ class Optimization:
|
|
435
612
|
else:
|
436
613
|
self.logger.warning("Solver %s unknown, using default", self.lp_solver)
|
437
614
|
opt_model.solve()
|
438
|
-
|
615
|
+
|
439
616
|
# The status of the solution is printed to the screen
|
440
617
|
self.optim_status = plp.LpStatus[opt_model.status]
|
441
618
|
self.logger.info("Status: " + self.optim_status)
|
@@ -444,7 +621,7 @@ class Optimization:
|
|
444
621
|
return
|
445
622
|
else:
|
446
623
|
self.logger.info("Total value of the Cost function = %.02f", plp.value(opt_model.objective))
|
447
|
-
|
624
|
+
|
448
625
|
# Build results Dataframe
|
449
626
|
opt_tp = pd.DataFrame()
|
450
627
|
opt_tp["P_PV"] = [P_PV[i] for i in set_I]
|
@@ -465,8 +642,11 @@ class Optimization:
|
|
465
642
|
SOC_opt.append(SOCinit - SOC_opt_delta[i])
|
466
643
|
SOCinit = SOC_opt[i]
|
467
644
|
opt_tp["SOC_opt"] = SOC_opt
|
645
|
+
if self.plant_conf['inverter_is_hybrid']:
|
646
|
+
opt_tp["P_hybrid_inverter"] = [P_hybrid_inverter[i].varValue for i in set_I]
|
647
|
+
opt_tp["P_PV_curtailment"] = [P_PV_curtailment[i].varValue for i in set_I]
|
468
648
|
opt_tp.index = data_opt.index
|
469
|
-
|
649
|
+
|
470
650
|
# Lets compute the optimal cost function
|
471
651
|
P_def_sum_tp = []
|
472
652
|
for i in set_I:
|
@@ -479,7 +659,7 @@ class Optimization:
|
|
479
659
|
else:
|
480
660
|
opt_tp["cost_profit"] = [-0.001*self.timeStep*(unit_load_cost[i]*P_grid_pos[i].varValue + \
|
481
661
|
unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
|
482
|
-
|
662
|
+
|
483
663
|
if self.costfun == 'profit':
|
484
664
|
if self.optim_conf['set_total_pv_sell']:
|
485
665
|
opt_tp["cost_fun_profit"] = [-0.001*self.timeStep*(unit_load_cost[i]*(P_load[i] + P_def_sum_tp[i]) + \
|
@@ -500,17 +680,16 @@ class Optimization:
|
|
500
680
|
unit_prod_price[i]*P_grid_neg[i].varValue) for i in set_I]
|
501
681
|
else:
|
502
682
|
self.logger.error("The cost function specified type is not valid")
|
503
|
-
|
683
|
+
|
504
684
|
# Add the optimization status
|
505
685
|
opt_tp["optim_status"] = self.optim_status
|
506
|
-
|
686
|
+
|
507
687
|
# Debug variables
|
508
688
|
if debug:
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
689
|
+
for k in range(self.optim_conf["num_def_loads"]):
|
690
|
+
opt_tp[f"P_def_start_{k}"] = [P_def_start[k][i].varValue for i in set_I]
|
691
|
+
opt_tp[f"P_def_bin2_{k}"] = [P_def_bin2[k][i].varValue for i in set_I]
|
692
|
+
|
514
693
|
return opt_tp
|
515
694
|
|
516
695
|
def perform_perfect_forecast_optim(self, df_input_data: pd.DataFrame, days_list: pd.date_range) -> pd.DataFrame:
|
@@ -548,9 +727,9 @@ class Optimization:
|
|
548
727
|
self.opt_res = opt_tp
|
549
728
|
else:
|
550
729
|
self.opt_res = pd.concat([self.opt_res, opt_tp], axis=0)
|
551
|
-
|
730
|
+
|
552
731
|
return self.opt_res
|
553
|
-
|
732
|
+
|
554
733
|
def perform_dayahead_forecast_optim(self, df_input_data: pd.DataFrame,
|
555
734
|
P_PV: pd.Series, P_load: pd.Series) -> pd.DataFrame:
|
556
735
|
r"""
|
@@ -577,7 +756,7 @@ class Optimization:
|
|
577
756
|
P_load.values.ravel(),
|
578
757
|
unit_load_cost, unit_prod_price)
|
579
758
|
return self.opt_res
|
580
|
-
|
759
|
+
|
581
760
|
def perform_naive_mpc_optim(self, df_input_data: pd.DataFrame, P_PV: pd.Series, P_load: pd.Series,
|
582
761
|
prediction_horizon: int, soc_init: Optional[float] = None, soc_final: Optional[float] = None,
|
583
762
|
def_total_hours: Optional[list] = None,
|