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