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