-
Notifications
You must be signed in to change notification settings - Fork 465
/
run_time_series.py
365 lines (279 loc) · 14.5 KB
/
run_time_series.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# -*- coding: utf-8 -*-
# Copyright (c) 2016-2023 by University of Kassel and Fraunhofer Institute for Energy Economics
# and Energy System Technology (IEE), Kassel. All rights reserved.
import tempfile
from collections.abc import Iterable
import tqdm
import pandapower as pp
from pandapower import LoadflowNotConverged, OPFNotConverged
from pandapower.control.run_control import ControllerNotConverged, prepare_run_ctrl, \
run_control, NetCalculationNotConverged
from pandapower.control.util.diagnostic import control_diagnostic
from pandapower.timeseries.output_writer import OutputWriter
try:
import pandaplan.core.pplog as pplog
except ImportError:
import logging as pplog
logger = pplog.getLogger(__name__)
logger.setLevel(level=pplog.WARNING)
def init_default_outputwriter(net, time_steps, **kwargs):
"""
Initializes the output writer. If output_writer is None, default output_writer is created
INPUT:
**net** - The pandapower format network
**time_steps** (list) - time steps to be calculated
"""
output_writer = kwargs.get("output_writer", None)
if output_writer is not None:
# write the output_writer to net
logger.warning("deprecated: output_writer should not be given to run_timeseries(). "
"This overwrites the stored one in net.output_writer.")
net.output_writer.iat[0, 0] = output_writer
if "output_writer" not in net or net.output_writer.iat[0, 0] is None:
# create a default output writer for this net
ow = OutputWriter(net, time_steps, output_path=tempfile.gettempdir())
logger.info("No output writer specified. Using default:")
logger.info(ow)
def init_output_writer(net, time_steps):
# init output writer before time series calculation
output_writer = net.output_writer.iat[0, 0]
output_writer.time_steps = time_steps
output_writer.init_all(net)
#
# def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█'):
# """
# Call in a loop to create terminal progress bar.
# the code is mentioned in : https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
# """
# percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
# filled_length = int(length * iteration // total)
# bar = fill * filled_length + '-' * (length - filled_length)
# # logger.info('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix))
# print('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), end="")
# # Print New Line on Complete
# if iteration == total:
# print("\n")
def controller_not_converged(time_step, ts_variables):
logger.error('ControllerNotConverged at time step %s' % time_step)
if not ts_variables["continue_on_divergence"]:
raise ControllerNotConverged
def pf_not_converged(time_step, ts_variables):
logger.error('CalculationNotConverged at time step %s' % time_step)
if not ts_variables["continue_on_divergence"]:
raise ts_variables['errors'][0]
def control_time_step(controller_order, time_step):
for levelorder in controller_order:
for ctrl, net in levelorder:
ctrl.time_step(net, time_step)
def finalize_step(controller_order, time_step):
for levelorder in controller_order:
for ctrl, net in levelorder:
ctrl.finalize_step(net, time_step)
def output_writer_routine(net, time_step, pf_converged, ctrl_converged, recycle_options):
output_writer = net["output_writer"].iat[0, 0]
# update time step for output writer
output_writer.time_step = time_step
# save
output_writer.save_results(net, time_step, pf_converged=pf_converged, ctrl_converged=ctrl_converged,
recycle_options=recycle_options)
def _call_output_writer(net, time_step, pf_converged, ctrl_converged, ts_variables):
output_writer_routine(net, time_step, pf_converged, ctrl_converged, ts_variables['recycle_options'])
def run_time_step(net, time_step, ts_variables, run_control_fct=run_control, output_writer_fct=_call_output_writer,
**kwargs):
"""
Time Series step function
Is called to run the PANDAPOWER AC power flows with the timeseries module
INPUT:
**net** - The pandapower format network
**time_step** (int) - time_step to be calculated
**ts_variables** (dict) - contains settings for controller and time series simulation. See init_time_series()
"""
ctrl_converged = True
pf_converged = True
# run time step function for each controller
control_time_step(ts_variables['controller_order'], time_step)
try:
# calls controller init, control steps and run function (runpp usually is called in here)
run_control_fct(net, ctrl_variables=ts_variables, **kwargs)
except ControllerNotConverged:
ctrl_converged = False
# If controller did not converge do some stuff
controller_not_converged(time_step, ts_variables)
except ts_variables['errors']:
# If power flow did not converge simulation aborts or continues if continue_on_divergence is True
pf_converged = False
pf_not_converged(time_step, ts_variables)
output_writer_fct(net, time_step, pf_converged, ctrl_converged, ts_variables)
finalize_step(ts_variables['controller_order'], time_step)
def _check_controller_recyclability(net):
# if a parameter is set to True here, it will be recalculated during the time series simulation
recycle = dict(trafo=False, gen=False, bus_pq=False)
if "controller" not in net:
# everything can be recycled since no controller is in net. But the time series simulation makes no sense
# then anyway...
return recycle
for idx in net.controller.index:
# todo: write to controller data frame recycle column instead of using self.recycle of controller instance
ctrl_recycle = net.controller.at[idx, "recycle"]
if not isinstance(ctrl_recycle, dict):
# if one controller has a wrong recycle configuration it is deactived
recycle = False
break
# else check which recycle parameter are set to True
for rp in ["trafo", "bus_pq", "gen"]:
recycle[rp] = recycle[rp] or ctrl_recycle[rp]
return recycle
def _check_output_writer_recyclability(net, recycle, run):
if "output_writer" not in net:
raise ValueError("OutputWriter not defined")
ow = net.output_writer.at[0, "object"]
# results which are read with a faster batch function after the time series simulation
recycle["batch_read"] = list()
recycle["only_v_results"] = False
new_log_variables = list()
if hasattr(run, "__name__") and run.__name__ == "rundcpp":
recycle["only_v_results"] = False
recycle["batch_read"] = False
return recycle
for output in ow.log_variables:
table, variable = output[0], output[1]
if table not in ["res_bus", "res_line", "res_trafo", "res_trafo3w"] or recycle["trafo"] or len(output) > 2:
# no fast read of outputs possible if other elements are required as these or tap changer is active
recycle["only_v_results"] = False
recycle["batch_read"] = False
return recycle
else:
# fast read is possible
if variable in ["vm_pu", "va_degree"]:
new_log_variables.append(('ppc_bus', 'vm'))
new_log_variables.append(('ppc_bus', 'va'))
recycle["only_v_results"] = True
recycle["batch_read"].append((table, variable))
ow.log_variables = new_log_variables
ow.log_variable('ppc_bus', 'vm')
ow.log_variable('ppc_bus', 'va')
return recycle
def get_recycle_settings(net, **kwargs):
"""
checks if "run" is specified in kwargs and calls this function in time series loop.
if "recycle" is in kwargs we use the TimeSeriesRunpp class (not implemented yet)
INPUT:
**net** - The pandapower format network
RETURN:
**recycle** - a dict with recycle options to be used by runpp
"""
recycle = kwargs.get("recycle", None)
if recycle is not False:
# check if every controller can be recycled and what can be recycled
recycle = _check_controller_recyclability(net)
# if still recycle is not None, also check for fast output_writer features
if recycle is not False:
recycle = _check_output_writer_recyclability(net, recycle, kwargs.get("run", kwargs.get("run_control_fct")))
return recycle
def init_time_steps(net, time_steps, **kwargs):
# initializes time steps if as a range
if not isinstance(time_steps, Iterable):
if isinstance(time_steps, tuple):
time_steps = range(time_steps[0], time_steps[1])
elif time_steps is None and ("start_step" in kwargs and "stop_step" in kwargs):
logger.warning("start_step and stop_step are depricated. "
"Please use a tuple like time_steps = (start_step, stop_step) instead or a list")
time_steps = range(kwargs["start_step"], kwargs["stop_step"] + 1)
else:
logger.warning("No time steps to calculate are specified. "
"I'll check the datasource of the first controller for avaiable time steps")
ds = net.controller.object.at[0].data_source
if ds is None:
raise UserWarning("No time steps are specified and the first controller doesn't have a data source"
"the time steps could be retrieved from")
else:
max_timestep = ds.get_time_steps_len()
time_steps = range(max_timestep)
return time_steps
def init_time_series(net, time_steps, continue_on_divergence=False, verbose=True,
**kwargs):
"""
inits the time series calculation
creates the dict ts_variables, which includes necessary variables for the time series / control function
INPUT:
**net** - The pandapower format network
**time_steps** (list or tuple, None) - time_steps to calculate as list or tuple (start, stop)
if None, all time steps from provided data source are simulated
OPTIONAL:
**continue_on_divergence** (bool, False) - If True time series calculation continues in case of errors.
**verbose** (bool, True) - prints progress bar or logger debug messages
"""
time_steps = init_time_steps(net, time_steps, **kwargs)
init_default_outputwriter(net, time_steps, **kwargs)
# get run function
run = kwargs.pop("run", pp.runpp)
recycle_options = None
if hasattr(run, "__name__") and (run.__name__ == "runpp" or run.__name__ == "rundcpp"):
# use faster runpp options if possible
recycle_options = get_recycle_settings(net, run=run, **kwargs)
init_output_writer(net, time_steps)
# as base take everything considered when preparing run_control
ts_variables = prepare_run_ctrl(net, None, run=run, **kwargs)
# recycle options, which define what can be recycled
ts_variables["recycle_options"] = recycle_options
# time steps to be calculated (list or range)
ts_variables["time_steps"] = time_steps
# If True, a diverged run is ignored and the next step is calculated
ts_variables["continue_on_divergence"] = continue_on_divergence
# print settings
ts_variables["verbose"] = verbose
if logger.level != 10 and verbose:
# simple progress bar
ts_variables['progress_bar'] = tqdm.tqdm(total=len(time_steps))
return ts_variables
def cleanup(net, ts_variables):
if isinstance(ts_variables["recycle_options"], dict):
# Todo: delete internal variables and dumped results which are not needed
net._ppc = None # remove _ppc because if recycle == True and a new timeseries calculation is started with a different setup (in_service of lines or trafos, open switches etc.) it can lead to a disaster
def print_progress(i, time_step, time_steps, verbose, **kwargs):
# simple status print in each time step.
if logger.level != 10 and verbose:
kwargs['ts_variables']["progress_bar"].update(1)
# print debug info
if logger.level == pplog.DEBUG and verbose:
logger.debug("run time step %i" % time_step)
# call a custom progress function
if "progress_function" in kwargs:
func = kwargs["progress_function"]
func(i, time_step, time_steps, **kwargs)
def run_loop(net, ts_variables, run_control_fct=run_control, output_writer_fct=_call_output_writer, **kwargs):
"""
runs the time series loop which calls pp.runpp (or another run function) in each iteration
Parameters
----------
net - pandapower net
ts_variables - settings for time series
"""
for i, time_step in enumerate(ts_variables["time_steps"]):
print_progress(i, time_step, ts_variables["time_steps"], ts_variables["verbose"], ts_variables=ts_variables,
**kwargs)
run_time_step(net, time_step, ts_variables, run_control_fct, output_writer_fct, **kwargs)
def run_timeseries(net, time_steps=None, continue_on_divergence=False, verbose=True, check_controllers=True, **kwargs):
"""
Time Series main function
Runs multiple PANDAPOWER AC power flows based on time series which are stored in a **DataSource** inside
**Controllers**. Optionally other functions than the pp power flow can be called by setting the run function in kwargs
INPUT:
**net** - The pandapower format network
OPTIONAL:
**time_steps** (list or tuple, None) - time_steps to calculate as list or tuple (start, stop)
if None, all time steps from provided data source are simulated
**continue_on_divergence** (bool, False) - If True time series calculation continues in case of errors.
**verbose** (bool, True) - prints progress bar or if logger.level == Debug it prints debug messages
**kwargs** - Keyword arguments for run_control and runpp. If "run" is in kwargs the default call to runpp()
is replaced by the function kwargs["run"]
"""
ts_variables = init_time_series(net, time_steps, continue_on_divergence, verbose, **kwargs)
# cleanup ppc before first time step
cleanup(net, ts_variables)
if check_controllers:
control_diagnostic(net) # produces significant overhead if you run many timeseries of short duration
run_loop(net, ts_variables, **kwargs)
# cleanup functions after the last time step was calculated
cleanup(net, ts_variables)
# both cleanups, at the start AND at the end, are important!