emhass 0.9.1__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 +206 -5
- emhass/forecast.py +13 -7
- emhass/optimization.py +261 -82
- emhass/retrieve_hass.py +55 -7
- emhass/utils.py +59 -102
- emhass/web_server.py +32 -7
- {emhass-0.9.1.dist-info → emhass-0.10.0.dist-info}/METADATA +111 -8
- {emhass-0.9.1.dist-info → emhass-0.10.0.dist-info}/RECORD +12 -12
- {emhass-0.9.1.dist-info → emhass-0.10.0.dist-info}/LICENSE +0 -0
- {emhass-0.9.1.dist-info → emhass-0.10.0.dist-info}/WHEEL +0 -0
- {emhass-0.9.1.dist-info → emhass-0.10.0.dist-info}/entry_points.txt +0 -0
- {emhass-0.9.1.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,
|
emhass/retrieve_hass.py
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
|
4
4
|
import json
|
5
5
|
import copy
|
6
|
+
import os
|
7
|
+
import pathlib
|
6
8
|
import datetime
|
7
9
|
import logging
|
8
10
|
from typing import Optional
|
@@ -61,7 +63,7 @@ class RetrieveHass:
|
|
61
63
|
self.freq = freq
|
62
64
|
self.time_zone = time_zone
|
63
65
|
self.params = params
|
64
|
-
|
66
|
+
self.emhass_conf = emhass_conf
|
65
67
|
self.logger = logger
|
66
68
|
self.get_data_from_file = get_data_from_file
|
67
69
|
|
@@ -304,9 +306,11 @@ class RetrieveHass:
|
|
304
306
|
}
|
305
307
|
return data
|
306
308
|
|
309
|
+
|
307
310
|
def post_data(self, data_df: pd.DataFrame, idx: int, entity_id: str, unit_of_measurement: str,
|
308
311
|
friendly_name: str, type_var: str, from_mlforecaster: Optional[bool] = False,
|
309
|
-
publish_prefix: Optional[str] = ""
|
312
|
+
publish_prefix: Optional[str] = "", save_entities: Optional[bool] = False,
|
313
|
+
logger_levels: Optional[str] = "info", dont_post: Optional[bool] = False) -> None:
|
310
314
|
r"""
|
311
315
|
Post passed data to hass.
|
312
316
|
|
@@ -326,6 +330,12 @@ class RetrieveHass:
|
|
326
330
|
:type type_var: str
|
327
331
|
:param publish_prefix: A common prefix for all published data entity_id.
|
328
332
|
:type publish_prefix: str, optional
|
333
|
+
:param save_entities: if entity data should be saved in data_path/entities
|
334
|
+
:type save_entities: bool, optional
|
335
|
+
:param logger_levels: set logger level, info or debug, to output
|
336
|
+
:type logger_levels: str, optional
|
337
|
+
:param dont_post: dont post to HA
|
338
|
+
:type dont_post: bool, optional
|
329
339
|
|
330
340
|
"""
|
331
341
|
# Add a possible prefix to the entity ID
|
@@ -340,10 +350,12 @@ class RetrieveHass:
|
|
340
350
|
headers = {
|
341
351
|
"Authorization": "Bearer " + self.long_lived_token,
|
342
352
|
"content-type": "application/json",
|
343
|
-
}
|
353
|
+
}
|
344
354
|
# Preparing the data dict to be published
|
345
355
|
if type_var == "cost_fun":
|
346
|
-
|
356
|
+
if isinstance(data_df.iloc[0],pd.Series): #if Series extract
|
357
|
+
data_df = data_df.iloc[:, 0]
|
358
|
+
state = np.round(data_df.sum(), 2)
|
347
359
|
elif type_var == "unit_load_cost" or type_var == "unit_prod_price":
|
348
360
|
state = np.round(data_df.loc[data_df.index[idx]], 4)
|
349
361
|
elif type_var == "optim_status":
|
@@ -398,18 +410,54 @@ class RetrieveHass:
|
|
398
410
|
},
|
399
411
|
}
|
400
412
|
# Actually post the data
|
401
|
-
if self.get_data_from_file:
|
413
|
+
if self.get_data_from_file or dont_post:
|
402
414
|
class response:
|
403
415
|
pass
|
404
416
|
response.status_code = 200
|
405
417
|
response.ok = True
|
406
418
|
else:
|
407
419
|
response = post(url, headers=headers, data=json.dumps(data))
|
420
|
+
|
408
421
|
# Treating the response status and posting them on the logger
|
409
422
|
if response.ok:
|
410
|
-
|
423
|
+
|
424
|
+
if logger_levels == "DEBUG":
|
425
|
+
self.logger.debug("Successfully posted to " + entity_id + " = " + str(state))
|
426
|
+
else:
|
427
|
+
self.logger.info("Successfully posted to " + entity_id + " = " + str(state))
|
428
|
+
|
429
|
+
# If save entities is set, save entity data to /data_path/entities
|
430
|
+
if (save_entities):
|
431
|
+
entities_path = self.emhass_conf['data_path'] / "entities"
|
432
|
+
|
433
|
+
# Clarify folder exists
|
434
|
+
pathlib.Path(entities_path).mkdir(parents=True, exist_ok=True)
|
435
|
+
|
436
|
+
# Save entity data to json file
|
437
|
+
result = data_df.to_json(index="timestamp", orient='index', date_unit='s', date_format='iso')
|
438
|
+
parsed = json.loads(result)
|
439
|
+
with open(entities_path / (entity_id + ".json"), "w") as file:
|
440
|
+
json.dump(parsed, file, indent=4)
|
441
|
+
|
442
|
+
# Save the required metadata to json file
|
443
|
+
if os.path.isfile(entities_path / "metadata.json"):
|
444
|
+
with open(entities_path / "metadata.json", "r") as file:
|
445
|
+
metadata = json.load(file)
|
446
|
+
else:
|
447
|
+
metadata = {}
|
448
|
+
with open(entities_path / "metadata.json", "w") as file:
|
449
|
+
# Save entity metadata, key = entity_id
|
450
|
+
metadata[entity_id] = {'name': data_df.name, 'unit_of_measurement': unit_of_measurement,'friendly_name': friendly_name,'type_var': type_var, 'freq': int(self.freq.seconds / 60)}
|
451
|
+
|
452
|
+
# Find lowest frequency to set for continual loop freq
|
453
|
+
if metadata.get("lowest_freq",None) == None or metadata["lowest_freq"] > int(self.freq.seconds / 60):
|
454
|
+
metadata["lowest_freq"] = int(self.freq.seconds / 60)
|
455
|
+
json.dump(metadata,file, indent=4)
|
456
|
+
|
457
|
+
self.logger.debug("Saved " + entity_id + " to json file")
|
458
|
+
|
411
459
|
else:
|
412
|
-
self.logger.
|
460
|
+
self.logger.warning(
|
413
461
|
"The status code for received curl command response is: "
|
414
462
|
+ str(response.status_code)
|
415
463
|
)
|