emhass 0.12.4__py3-none-any.whl → 0.12.5__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-0.12.4.dist-info → emhass-0.12.5.dist-info}/METADATA +34 -17
- {emhass-0.12.4.dist-info → emhass-0.12.5.dist-info}/RECORD +5 -17
- emhass/__init__.py +0 -0
- emhass/command_line.py +0 -1748
- emhass/data/emhass_inverters.csv +0 -8
- emhass/data/emhass_modules.csv +0 -6
- emhass/forecast.py +0 -1348
- emhass/img/emhass_icon.png +0 -0
- emhass/machine_learning_forecaster.py +0 -397
- emhass/machine_learning_regressor.py +0 -275
- emhass/optimization.py +0 -1504
- emhass/retrieve_hass.py +0 -670
- emhass/utils.py +0 -1678
- emhass/web_server.py +0 -756
- {emhass-0.12.4.dist-info → emhass-0.12.5.dist-info}/WHEEL +0 -0
- {emhass-0.12.4.dist-info → emhass-0.12.5.dist-info}/entry_points.txt +0 -0
- {emhass-0.12.4.dist-info → emhass-0.12.5.dist-info}/licenses/LICENSE +0 -0
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
|