rtc-tools 2.7.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.
- rtc_tools-2.7.3.dist-info/METADATA +53 -0
- rtc_tools-2.7.3.dist-info/RECORD +50 -0
- rtc_tools-2.7.3.dist-info/WHEEL +5 -0
- rtc_tools-2.7.3.dist-info/entry_points.txt +3 -0
- rtc_tools-2.7.3.dist-info/licenses/COPYING.LESSER +165 -0
- rtc_tools-2.7.3.dist-info/top_level.txt +1 -0
- rtctools/__init__.py +5 -0
- rtctools/_internal/__init__.py +0 -0
- rtctools/_internal/alias_tools.py +188 -0
- rtctools/_internal/caching.py +25 -0
- rtctools/_internal/casadi_helpers.py +99 -0
- rtctools/_internal/debug_check_helpers.py +41 -0
- rtctools/_version.py +21 -0
- rtctools/data/__init__.py +4 -0
- rtctools/data/csv.py +150 -0
- rtctools/data/interpolation/__init__.py +3 -0
- rtctools/data/interpolation/bspline.py +31 -0
- rtctools/data/interpolation/bspline1d.py +169 -0
- rtctools/data/interpolation/bspline2d.py +54 -0
- rtctools/data/netcdf.py +467 -0
- rtctools/data/pi.py +1236 -0
- rtctools/data/rtc.py +228 -0
- rtctools/data/storage.py +343 -0
- rtctools/optimization/__init__.py +0 -0
- rtctools/optimization/collocated_integrated_optimization_problem.py +3208 -0
- rtctools/optimization/control_tree_mixin.py +221 -0
- rtctools/optimization/csv_lookup_table_mixin.py +462 -0
- rtctools/optimization/csv_mixin.py +300 -0
- rtctools/optimization/goal_programming_mixin.py +769 -0
- rtctools/optimization/goal_programming_mixin_base.py +1094 -0
- rtctools/optimization/homotopy_mixin.py +165 -0
- rtctools/optimization/initial_state_estimation_mixin.py +89 -0
- rtctools/optimization/io_mixin.py +320 -0
- rtctools/optimization/linearization_mixin.py +33 -0
- rtctools/optimization/linearized_order_goal_programming_mixin.py +235 -0
- rtctools/optimization/min_abs_goal_programming_mixin.py +385 -0
- rtctools/optimization/modelica_mixin.py +482 -0
- rtctools/optimization/netcdf_mixin.py +177 -0
- rtctools/optimization/optimization_problem.py +1302 -0
- rtctools/optimization/pi_mixin.py +292 -0
- rtctools/optimization/planning_mixin.py +19 -0
- rtctools/optimization/single_pass_goal_programming_mixin.py +676 -0
- rtctools/optimization/timeseries.py +56 -0
- rtctools/rtctoolsapp.py +131 -0
- rtctools/simulation/__init__.py +0 -0
- rtctools/simulation/csv_mixin.py +171 -0
- rtctools/simulation/io_mixin.py +195 -0
- rtctools/simulation/pi_mixin.py +255 -0
- rtctools/simulation/simulation_problem.py +1293 -0
- rtctools/util.py +241 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
import rtctools.data.csv as csv
|
|
8
|
+
from rtctools._internal.alias_tools import AliasDict
|
|
9
|
+
from rtctools._internal.caching import cached
|
|
10
|
+
from rtctools.optimization.io_mixin import IOMixin
|
|
11
|
+
from rtctools.optimization.timeseries import Timeseries
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("rtctools")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CSVMixin(IOMixin):
|
|
17
|
+
"""
|
|
18
|
+
Adds reading and writing of CSV timeseries and parameters to your optimization problem.
|
|
19
|
+
|
|
20
|
+
During preprocessing, files named ``timeseries_import.csv``, ``initial_state.csv``,
|
|
21
|
+
and ``parameters.csv`` are read from the ``input`` subfolder.
|
|
22
|
+
|
|
23
|
+
During postprocessing, a file named ``timeseries_export.csv`` is written to the ``output``
|
|
24
|
+
subfolder.
|
|
25
|
+
|
|
26
|
+
In ensemble mode, a file named ``ensemble.csv`` is read from the ``input`` folder. This file
|
|
27
|
+
contains two columns. The first column gives the name of the ensemble member, and the second
|
|
28
|
+
column its probability. Furthermore, the other XML files appear one level deeper inside the
|
|
29
|
+
filesystem hierarchy, inside subfolders with the names of the ensemble members.
|
|
30
|
+
|
|
31
|
+
:cvar csv_initial_state_basename:
|
|
32
|
+
Initial state file basename. Default is ``initial_state``.
|
|
33
|
+
:cvar csv_parameters_basename:
|
|
34
|
+
Parameters file basename. Default is ``parameters``.
|
|
35
|
+
:cvar csv_ensemble_basename:
|
|
36
|
+
Ensemble file basename. Default is ``ensemble``.
|
|
37
|
+
:cvar csv_delimiter:
|
|
38
|
+
Column delimiter used in CSV files. Default is ``,``.
|
|
39
|
+
:cvar csv_equidistant:
|
|
40
|
+
Whether or not the timeseries data is equidistant. Default is ``True``.
|
|
41
|
+
:cvar csv_ensemble_mode:
|
|
42
|
+
Whether or not to use ensembles. Default is ``False``.
|
|
43
|
+
:cvar csv_validate_timeseries:
|
|
44
|
+
Check consistency of timeseries. Default is ``True``.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
#: Initial state file basename
|
|
48
|
+
csv_initial_state_basename = "initial_state"
|
|
49
|
+
|
|
50
|
+
#: Parameters file basename
|
|
51
|
+
csv_parameters_basename = "parameters"
|
|
52
|
+
|
|
53
|
+
#: Ensemble file basename
|
|
54
|
+
csv_ensemble_basename = "ensemble"
|
|
55
|
+
|
|
56
|
+
#: Column delimiter used in CSV files
|
|
57
|
+
csv_delimiter = ","
|
|
58
|
+
|
|
59
|
+
#: Whether or not the timeseries data is equidistant
|
|
60
|
+
csv_equidistant = True
|
|
61
|
+
|
|
62
|
+
#: Whether or not to use ensembles
|
|
63
|
+
csv_ensemble_mode = False
|
|
64
|
+
|
|
65
|
+
#: Check consistency of timeseries
|
|
66
|
+
csv_validate_timeseries = True
|
|
67
|
+
|
|
68
|
+
def __init__(self, **kwargs):
|
|
69
|
+
# Call parent class first for default behaviour.
|
|
70
|
+
super().__init__(**kwargs)
|
|
71
|
+
|
|
72
|
+
def read(self):
|
|
73
|
+
# Call parent class first for default behaviour.
|
|
74
|
+
super().read()
|
|
75
|
+
|
|
76
|
+
# Helper function to check if initial state array actually defines
|
|
77
|
+
# only the initial state
|
|
78
|
+
def check_initial_state_array(initial_state):
|
|
79
|
+
"""
|
|
80
|
+
Check length of initial state array, throw exception when larger than 1.
|
|
81
|
+
"""
|
|
82
|
+
if initial_state.shape:
|
|
83
|
+
raise Exception(
|
|
84
|
+
"CSVMixin: Initial state file {} contains more than one row of data. "
|
|
85
|
+
"Please remove the data row(s) that do not describe the initial state.".format(
|
|
86
|
+
os.path.join(self._input_folder, self.csv_initial_state_basename + ".csv")
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Read CSV files
|
|
91
|
+
self.__initial_state = []
|
|
92
|
+
if self.csv_ensemble_mode:
|
|
93
|
+
self.__ensemble = np.genfromtxt(
|
|
94
|
+
os.path.join(self._input_folder, self.csv_ensemble_basename + ".csv"),
|
|
95
|
+
delimiter=",",
|
|
96
|
+
deletechars="",
|
|
97
|
+
dtype=None,
|
|
98
|
+
names=True,
|
|
99
|
+
encoding=None,
|
|
100
|
+
)
|
|
101
|
+
if len(self.__ensemble.shape) == 0:
|
|
102
|
+
# If there is only one ensemble member, the array is 0-dimensional.
|
|
103
|
+
self.__ensemble = np.expand_dims(self.__ensemble, 0)
|
|
104
|
+
|
|
105
|
+
logger.debug("CSVMixin: Read ensemble description")
|
|
106
|
+
|
|
107
|
+
for ensemble_member_index, ensemble_member_name in enumerate(self.__ensemble["name"]):
|
|
108
|
+
_timeseries = csv.load(
|
|
109
|
+
os.path.join(
|
|
110
|
+
self._input_folder,
|
|
111
|
+
ensemble_member_name,
|
|
112
|
+
self.timeseries_import_basename + ".csv",
|
|
113
|
+
),
|
|
114
|
+
delimiter=self.csv_delimiter,
|
|
115
|
+
with_time=True,
|
|
116
|
+
)
|
|
117
|
+
self.__timeseries_times = _timeseries[_timeseries.dtype.names[0]]
|
|
118
|
+
|
|
119
|
+
self.io.reference_datetime = self.__timeseries_times[0]
|
|
120
|
+
|
|
121
|
+
for key in _timeseries.dtype.names[1:]:
|
|
122
|
+
self.io.set_timeseries(
|
|
123
|
+
key,
|
|
124
|
+
self.__timeseries_times,
|
|
125
|
+
np.asarray(_timeseries[key], dtype=np.float64),
|
|
126
|
+
ensemble_member_index,
|
|
127
|
+
)
|
|
128
|
+
logger.debug("CSVMixin: Read timeseries")
|
|
129
|
+
|
|
130
|
+
for ensemble_member_index, ensemble_member_name in enumerate(self.__ensemble["name"]):
|
|
131
|
+
try:
|
|
132
|
+
_parameters = csv.load(
|
|
133
|
+
os.path.join(
|
|
134
|
+
self._input_folder,
|
|
135
|
+
ensemble_member_name,
|
|
136
|
+
self.csv_parameters_basename + ".csv",
|
|
137
|
+
),
|
|
138
|
+
delimiter=self.csv_delimiter,
|
|
139
|
+
)
|
|
140
|
+
for key in _parameters.dtype.names:
|
|
141
|
+
self.io.set_parameter(key, float(_parameters[key]), ensemble_member_index)
|
|
142
|
+
except IOError:
|
|
143
|
+
pass
|
|
144
|
+
logger.debug("CSVMixin: Read parameters.")
|
|
145
|
+
|
|
146
|
+
for ensemble_member_name in self.__ensemble["name"]:
|
|
147
|
+
try:
|
|
148
|
+
_initial_state = csv.load(
|
|
149
|
+
os.path.join(
|
|
150
|
+
self._input_folder,
|
|
151
|
+
ensemble_member_name,
|
|
152
|
+
self.csv_initial_state_basename + ".csv",
|
|
153
|
+
),
|
|
154
|
+
delimiter=self.csv_delimiter,
|
|
155
|
+
)
|
|
156
|
+
check_initial_state_array(_initial_state)
|
|
157
|
+
_initial_state = {
|
|
158
|
+
key: float(_initial_state[key]) for key in _initial_state.dtype.names
|
|
159
|
+
}
|
|
160
|
+
except IOError:
|
|
161
|
+
_initial_state = {}
|
|
162
|
+
self.__initial_state.append(AliasDict(self.alias_relation, _initial_state))
|
|
163
|
+
logger.debug("CSVMixin: Read initial state.")
|
|
164
|
+
else:
|
|
165
|
+
_timeseries = csv.load(
|
|
166
|
+
os.path.join(self._input_folder, self.timeseries_import_basename + ".csv"),
|
|
167
|
+
delimiter=self.csv_delimiter,
|
|
168
|
+
with_time=True,
|
|
169
|
+
)
|
|
170
|
+
self.__timeseries_times = _timeseries[_timeseries.dtype.names[0]]
|
|
171
|
+
|
|
172
|
+
self.io.reference_datetime = self.__timeseries_times[0]
|
|
173
|
+
|
|
174
|
+
for key in _timeseries.dtype.names[1:]:
|
|
175
|
+
self.io.set_timeseries(
|
|
176
|
+
key, self.__timeseries_times, np.asarray(_timeseries[key], dtype=np.float64)
|
|
177
|
+
)
|
|
178
|
+
logger.debug("CSVMixin: Read timeseries.")
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
_parameters = csv.load(
|
|
182
|
+
os.path.join(self._input_folder, self.csv_parameters_basename + ".csv"),
|
|
183
|
+
delimiter=self.csv_delimiter,
|
|
184
|
+
)
|
|
185
|
+
logger.debug("CSVMixin: Read parameters.")
|
|
186
|
+
for key in _parameters.dtype.names:
|
|
187
|
+
self.io.set_parameter(key, float(_parameters[key]))
|
|
188
|
+
except IOError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
_initial_state = csv.load(
|
|
193
|
+
os.path.join(self._input_folder, self.csv_initial_state_basename + ".csv"),
|
|
194
|
+
delimiter=self.csv_delimiter,
|
|
195
|
+
)
|
|
196
|
+
logger.debug("CSVMixin: Read initial state.")
|
|
197
|
+
check_initial_state_array(_initial_state)
|
|
198
|
+
_initial_state = {
|
|
199
|
+
key: float(_initial_state[key]) for key in _initial_state.dtype.names
|
|
200
|
+
}
|
|
201
|
+
except IOError:
|
|
202
|
+
_initial_state = {}
|
|
203
|
+
self.__initial_state.append(AliasDict(self.alias_relation, _initial_state))
|
|
204
|
+
|
|
205
|
+
# Timestamp check
|
|
206
|
+
if self.csv_validate_timeseries:
|
|
207
|
+
times = self.__timeseries_times
|
|
208
|
+
for i in range(len(times) - 1):
|
|
209
|
+
if times[i] >= times[i + 1]:
|
|
210
|
+
raise Exception("CSVMixin: Time stamps must be strictly increasing.")
|
|
211
|
+
|
|
212
|
+
if self.csv_equidistant:
|
|
213
|
+
# Check if the timeseries are truly equidistant
|
|
214
|
+
if self.csv_validate_timeseries:
|
|
215
|
+
times = self.__timeseries_times
|
|
216
|
+
dt = times[1] - times[0]
|
|
217
|
+
for i in range(len(times) - 1):
|
|
218
|
+
if times[i + 1] - times[i] != dt:
|
|
219
|
+
raise Exception(
|
|
220
|
+
"CSVMixin: Expecting equidistant timeseries, the time step towards "
|
|
221
|
+
"{} is not the same as the time step(s) before. "
|
|
222
|
+
"Set csv_equidistant = False if this is intended.".format(times[i + 1])
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def ensemble_member_probability(self, ensemble_member):
|
|
226
|
+
if self.csv_ensemble_mode:
|
|
227
|
+
return self.__ensemble["probability"][ensemble_member]
|
|
228
|
+
else:
|
|
229
|
+
return 1.0
|
|
230
|
+
|
|
231
|
+
@cached
|
|
232
|
+
def history(self, ensemble_member):
|
|
233
|
+
# Call parent class first for default values.
|
|
234
|
+
history = super().history(ensemble_member)
|
|
235
|
+
|
|
236
|
+
initial_time = np.array([self.initial_time])
|
|
237
|
+
|
|
238
|
+
# Load parameters from parameter config
|
|
239
|
+
for variable in self.dae_variables["free_variables"]:
|
|
240
|
+
variable = variable.name()
|
|
241
|
+
try:
|
|
242
|
+
history[variable] = Timeseries(
|
|
243
|
+
initial_time, self.__initial_state[ensemble_member][variable]
|
|
244
|
+
)
|
|
245
|
+
except (KeyError, ValueError):
|
|
246
|
+
pass
|
|
247
|
+
else:
|
|
248
|
+
if logger.getEffectiveLevel() == logging.DEBUG:
|
|
249
|
+
logger.debug("CSVMixin: Read initial state {}".format(variable))
|
|
250
|
+
return history
|
|
251
|
+
|
|
252
|
+
def write(self):
|
|
253
|
+
# Call parent class first for default behaviour.
|
|
254
|
+
super().write()
|
|
255
|
+
|
|
256
|
+
# Write output
|
|
257
|
+
times = self.times()
|
|
258
|
+
|
|
259
|
+
def write_output(ensemble_member, folder):
|
|
260
|
+
results = self.extract_results(ensemble_member)
|
|
261
|
+
names = ["time"] + sorted({sym.name() for sym in self.output_variables})
|
|
262
|
+
formats = ["O"] + (len(names) - 1) * ["f8"]
|
|
263
|
+
dtype = {"names": names, "formats": formats}
|
|
264
|
+
data = np.zeros(len(times), dtype=dtype)
|
|
265
|
+
data["time"] = [self.io.reference_datetime + timedelta(seconds=s) for s in times]
|
|
266
|
+
for output_variable in self.output_variables:
|
|
267
|
+
output_variable = output_variable.name()
|
|
268
|
+
try:
|
|
269
|
+
values = results[output_variable]
|
|
270
|
+
if len(values) != len(times):
|
|
271
|
+
values = self.interpolate(
|
|
272
|
+
times,
|
|
273
|
+
self.times(output_variable),
|
|
274
|
+
values,
|
|
275
|
+
self.interpolation_method(output_variable),
|
|
276
|
+
)
|
|
277
|
+
except KeyError:
|
|
278
|
+
try:
|
|
279
|
+
ts = self.get_timeseries(output_variable, ensemble_member)
|
|
280
|
+
if len(ts.times) != len(times):
|
|
281
|
+
values = self.interpolate(times, ts.times, ts.values)
|
|
282
|
+
else:
|
|
283
|
+
values = ts.values
|
|
284
|
+
except KeyError:
|
|
285
|
+
logger.error(
|
|
286
|
+
"Output requested for non-existent variable {}".format(output_variable)
|
|
287
|
+
)
|
|
288
|
+
continue
|
|
289
|
+
data[output_variable] = values
|
|
290
|
+
|
|
291
|
+
fname = os.path.join(folder, self.timeseries_export_basename + ".csv")
|
|
292
|
+
csv.save(fname, data, delimiter=self.csv_delimiter, with_time=True)
|
|
293
|
+
|
|
294
|
+
if self.csv_ensemble_mode:
|
|
295
|
+
for ensemble_member, ensemble_member_name in enumerate(self.__ensemble["name"]):
|
|
296
|
+
write_output(
|
|
297
|
+
ensemble_member, os.path.join(self._output_folder, ensemble_member_name)
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
write_output(0, self._output_folder)
|