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.
Files changed (50) hide show
  1. rtc_tools-2.7.3.dist-info/METADATA +53 -0
  2. rtc_tools-2.7.3.dist-info/RECORD +50 -0
  3. rtc_tools-2.7.3.dist-info/WHEEL +5 -0
  4. rtc_tools-2.7.3.dist-info/entry_points.txt +3 -0
  5. rtc_tools-2.7.3.dist-info/licenses/COPYING.LESSER +165 -0
  6. rtc_tools-2.7.3.dist-info/top_level.txt +1 -0
  7. rtctools/__init__.py +5 -0
  8. rtctools/_internal/__init__.py +0 -0
  9. rtctools/_internal/alias_tools.py +188 -0
  10. rtctools/_internal/caching.py +25 -0
  11. rtctools/_internal/casadi_helpers.py +99 -0
  12. rtctools/_internal/debug_check_helpers.py +41 -0
  13. rtctools/_version.py +21 -0
  14. rtctools/data/__init__.py +4 -0
  15. rtctools/data/csv.py +150 -0
  16. rtctools/data/interpolation/__init__.py +3 -0
  17. rtctools/data/interpolation/bspline.py +31 -0
  18. rtctools/data/interpolation/bspline1d.py +169 -0
  19. rtctools/data/interpolation/bspline2d.py +54 -0
  20. rtctools/data/netcdf.py +467 -0
  21. rtctools/data/pi.py +1236 -0
  22. rtctools/data/rtc.py +228 -0
  23. rtctools/data/storage.py +343 -0
  24. rtctools/optimization/__init__.py +0 -0
  25. rtctools/optimization/collocated_integrated_optimization_problem.py +3208 -0
  26. rtctools/optimization/control_tree_mixin.py +221 -0
  27. rtctools/optimization/csv_lookup_table_mixin.py +462 -0
  28. rtctools/optimization/csv_mixin.py +300 -0
  29. rtctools/optimization/goal_programming_mixin.py +769 -0
  30. rtctools/optimization/goal_programming_mixin_base.py +1094 -0
  31. rtctools/optimization/homotopy_mixin.py +165 -0
  32. rtctools/optimization/initial_state_estimation_mixin.py +89 -0
  33. rtctools/optimization/io_mixin.py +320 -0
  34. rtctools/optimization/linearization_mixin.py +33 -0
  35. rtctools/optimization/linearized_order_goal_programming_mixin.py +235 -0
  36. rtctools/optimization/min_abs_goal_programming_mixin.py +385 -0
  37. rtctools/optimization/modelica_mixin.py +482 -0
  38. rtctools/optimization/netcdf_mixin.py +177 -0
  39. rtctools/optimization/optimization_problem.py +1302 -0
  40. rtctools/optimization/pi_mixin.py +292 -0
  41. rtctools/optimization/planning_mixin.py +19 -0
  42. rtctools/optimization/single_pass_goal_programming_mixin.py +676 -0
  43. rtctools/optimization/timeseries.py +56 -0
  44. rtctools/rtctoolsapp.py +131 -0
  45. rtctools/simulation/__init__.py +0 -0
  46. rtctools/simulation/csv_mixin.py +171 -0
  47. rtctools/simulation/io_mixin.py +195 -0
  48. rtctools/simulation/pi_mixin.py +255 -0
  49. rtctools/simulation/simulation_problem.py +1293 -0
  50. rtctools/util.py +241 -0
@@ -0,0 +1,56 @@
1
+ from typing import Union
2
+
3
+ import casadi as ca
4
+ import numpy as np
5
+
6
+
7
+ class Timeseries:
8
+ """
9
+ Time series object, bundling time stamps with values.
10
+ """
11
+
12
+ def __init__(self, times: np.ndarray, values: Union[float, np.ndarray, list, ca.DM]):
13
+ """
14
+ Create a new time series object.
15
+
16
+ :param times: Iterable of time stamps.
17
+ :param values: Iterable of values.
18
+ """
19
+ self.__times = times
20
+
21
+ if isinstance(values, ca.DM):
22
+ # Note that a ca.DM object has no __iter__ attribute, which we
23
+ # want it to have. We also want it to store it as a _flat_ array,
24
+ # not a 2-D column vector.
25
+ assert values.shape[0] == 1 or values.shape[1] == 1, "Only 1D ca.DM objects supported"
26
+ values = values.toarray().ravel()
27
+ elif isinstance(values, (np.ndarray, list)) and len(values) == 1:
28
+ values = values[0]
29
+
30
+ if hasattr(values, "__iter__"):
31
+ self.__values = np.array(values, dtype=np.float64, copy=True)
32
+ else:
33
+ self.__values = np.full_like(times, values, dtype=np.float64)
34
+
35
+ @property
36
+ def times(self) -> np.ndarray:
37
+ """
38
+ Array of time stamps.
39
+ """
40
+ return self.__times
41
+
42
+ @property
43
+ def values(self) -> np.ndarray:
44
+ """
45
+ Array of values.
46
+ """
47
+ return self.__values
48
+
49
+ def __neg__(self) -> "Timeseries":
50
+ return self.__class__(self.times, -self.values)
51
+
52
+ def __repr__(self) -> str:
53
+ return "Timeseries({}, {})".format(self.__times, self.__values)
54
+
55
+ def __eq__(self, other: "Timeseries") -> bool:
56
+ return np.array_equal(self.times, other.times) and np.array_equal(self.values, other.values)
@@ -0,0 +1,131 @@
1
+ import importlib.resources
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # Python 3.9's importlib.metadata does not support the "group" parameter to
9
+ # entry_points yet.
10
+ if sys.version_info < (3, 10):
11
+ import importlib_metadata
12
+ else:
13
+ from importlib import metadata as importlib_metadata
14
+
15
+ import rtctools
16
+
17
+ logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s")
18
+ logger = logging.getLogger("rtctools")
19
+ logger.setLevel(logging.INFO)
20
+
21
+
22
+ def copy_libraries(*args):
23
+ if not args:
24
+ args = sys.argv[1:]
25
+
26
+ if not args:
27
+ path = input("Folder to put the Modelica libraries: [.] ") or "."
28
+ else:
29
+ path = args[0]
30
+
31
+ if not os.path.exists(path):
32
+ sys.exit("Folder '{}' does not exist".format(path))
33
+
34
+ def _copytree(src, dst, symlinks=False, ignore=None):
35
+ if not os.path.exists(dst):
36
+ os.makedirs(dst)
37
+ for item in os.listdir(src):
38
+ s = os.path.join(src, item)
39
+ d = os.path.join(dst, item)
40
+ if os.path.isdir(s):
41
+ _copytree(s, d, symlinks, ignore)
42
+ else:
43
+ if not os.path.exists(d):
44
+ shutil.copy2(s, d)
45
+ elif Path(s).name.lower() == "package.mo":
46
+ # Pick the largest one, assuming that all plugin packages
47
+ # to not provide a meaningful package.mo
48
+ if os.stat(s).st_size > os.stat(d).st_size:
49
+ logger.warning(
50
+ "Overwriting '{}' with '{}' as the latter is larger.".format(d, s)
51
+ )
52
+ os.remove(d)
53
+ shutil.copy2(s, d)
54
+ else:
55
+ logger.warning(
56
+ "Not copying '{}' to '{}' as the latter is larger.".format(s, d)
57
+ )
58
+ else:
59
+ raise OSError("Could not combine two folders")
60
+
61
+ dst = Path(path)
62
+
63
+ library_folders = []
64
+
65
+ for ep in importlib_metadata.entry_points(group="rtctools.libraries.modelica"):
66
+ if ep.name == "library_folder":
67
+ library_folders.append(Path(importlib.resources.files(ep.module).joinpath(ep.attr)))
68
+
69
+ tlds = {}
70
+ for lf in library_folders:
71
+ for x in lf.iterdir():
72
+ if x.is_dir():
73
+ tlds.setdefault(x.name, []).append(x)
74
+
75
+ for tld, paths in tlds.items():
76
+ if Path(tld).exists():
77
+ sys.exit("Library with name '{}'' already exists".format(tld))
78
+
79
+ try:
80
+ for p in paths:
81
+ _copytree(p, dst / p.name)
82
+ except OSError:
83
+ sys.exit("Failed merging the libraries in package '{}'".format(tld))
84
+
85
+ sys.exit("Succesfully copied all library folders to '{}'".format(dst.resolve()))
86
+
87
+
88
+ def download_examples(*args):
89
+ if not args:
90
+ args = sys.argv[1:]
91
+
92
+ if not args:
93
+ path = input("Folder to download the examples to: [.] ") or "."
94
+ else:
95
+ path = args[0]
96
+
97
+ if not os.path.exists(path):
98
+ sys.exit("Folder '{}' does not exist".format(path))
99
+
100
+ path = Path(path)
101
+
102
+ import urllib.request
103
+ from urllib.error import HTTPError
104
+ from zipfile import ZipFile
105
+
106
+ version = rtctools.__version__
107
+ try:
108
+ url = "https://github.com/rtc-tools/rtc-tools/zipball/{}".format(version)
109
+
110
+ opener = urllib.request.build_opener()
111
+ urllib.request.install_opener(opener)
112
+ # The security warning can be dismissed as the url variable is hardcoded to a remote.
113
+ local_filename, _ = urllib.request.urlretrieve(url) # nosec
114
+ except HTTPError:
115
+ sys.exit("Could not found examples for RTC-Tools version {}.".format(version))
116
+
117
+ with ZipFile(local_filename, "r") as z:
118
+ target = path / "rtc-tools-examples"
119
+ zip_folder_name = next(x for x in z.namelist() if x.startswith("Deltares-rtc-tools-"))
120
+ prefix = "{}/examples/".format(zip_folder_name.rstrip("/"))
121
+ members = [x for x in z.namelist() if x.startswith(prefix)]
122
+ z.extractall(members=members)
123
+ shutil.move(prefix, target)
124
+ shutil.rmtree(zip_folder_name)
125
+
126
+ sys.exit("Succesfully downloaded the RTC-Tools examples to '{}'".format(target.resolve()))
127
+
128
+ try:
129
+ os.remove(local_filename)
130
+ except OSError:
131
+ pass
File without changes
@@ -0,0 +1,171 @@
1
+ import logging
2
+ import os
3
+
4
+ import numpy as np
5
+
6
+ import rtctools.data.csv as csv
7
+ from rtctools._internal.caching import cached
8
+ from rtctools.simulation.io_mixin import IOMixin
9
+
10
+ logger = logging.getLogger("rtctools")
11
+
12
+
13
+ class CSVMixin(IOMixin):
14
+ """
15
+ Adds reading and writing of CSV timeseries and parameters to your simulation problem.
16
+
17
+ During preprocessing, files named ``timeseries_import.csv``, ``initial_state.csv``,
18
+ and ``parameters.csv`` are read from the ``input`` subfolder.
19
+
20
+ During postprocessing, a file named ``timeseries_export.csv`` is written to the ``output``
21
+ subfolder.
22
+
23
+ :cvar csv_delimiter: Column delimiter used in CSV files. Default is ``,``.
24
+ :cvar csv_validate_timeseries: Check consistency of timeseries. Default is ``True``.
25
+ """
26
+
27
+ #: Column delimiter used in CSV files
28
+ csv_delimiter = ","
29
+
30
+ #: Check consistency of timeseries
31
+ csv_validate_timeseries = True
32
+
33
+ def __init__(self, **kwargs):
34
+ # Call parent class first for default behaviour.
35
+ super().__init__(**kwargs)
36
+
37
+ def read(self):
38
+ # Call parent class first for default behaviour.
39
+ super().read()
40
+
41
+ # Helper function to check if initial state array actually defines
42
+ # only the initial state
43
+ def check_initial_state_array(initial_state):
44
+ """
45
+ Check length of initial state array, throw exception when larger than 1.
46
+ """
47
+ if initial_state.shape:
48
+ raise Exception(
49
+ "CSVMixin: Initial state file {} contains more than one row of data. "
50
+ "Please remove the data row(s) that do not describe the initial "
51
+ "state.".format(os.path.join(self._input_folder, "initial_state.csv"))
52
+ )
53
+
54
+ # Read CSV files
55
+ _timeseries = csv.load(
56
+ os.path.join(self._input_folder, self.timeseries_import_basename + ".csv"),
57
+ delimiter=self.csv_delimiter,
58
+ with_time=True,
59
+ )
60
+ self.__timeseries_times = _timeseries[_timeseries.dtype.names[0]]
61
+
62
+ self.io.reference_datetime = self.__timeseries_times[0]
63
+
64
+ for key in _timeseries.dtype.names[1:]:
65
+ self.io.set_timeseries(
66
+ key, self.__timeseries_times, np.asarray(_timeseries[key], dtype=np.float64)
67
+ )
68
+
69
+ logger.debug("CSVMixin: Read timeseries.")
70
+
71
+ try:
72
+ _parameters = csv.load(
73
+ os.path.join(self._input_folder, "parameters.csv"), delimiter=self.csv_delimiter
74
+ )
75
+ for key in _parameters.dtype.names:
76
+ self.io.set_parameter(key, float(_parameters[key]))
77
+ logger.debug("CSVMixin: Read parameters.")
78
+ except IOError:
79
+ pass
80
+
81
+ try:
82
+ _initial_state = csv.load(
83
+ os.path.join(self._input_folder, "initial_state.csv"), delimiter=self.csv_delimiter
84
+ )
85
+ logger.debug("CSVMixin: Read initial state.")
86
+ check_initial_state_array(_initial_state)
87
+ self.__initial_state = {
88
+ key: float(_initial_state[key]) for key in _initial_state.dtype.names
89
+ }
90
+ except IOError:
91
+ self.__initial_state = {}
92
+
93
+ # Check for collisions in __initial_state and timeseries import (CSV)
94
+ for collision in set(self.__initial_state) & set(_timeseries.dtype.names[1:]):
95
+ if self.__initial_state[collision] == _timeseries[collision][0]:
96
+ continue
97
+ else:
98
+ logger.warning(
99
+ "CSVMixin: Entry {} in initial_state.csv conflicts with "
100
+ "timeseries_import.csv".format(collision)
101
+ )
102
+
103
+ # Timestamp check
104
+ if self.csv_validate_timeseries:
105
+ times = self.__timeseries_times
106
+ for i in range(len(times) - 1):
107
+ if times[i] >= times[i + 1]:
108
+ raise Exception("CSVMixin: Time stamps must be strictly increasing.")
109
+
110
+ times = self.__timeseries_times
111
+ dt = times[1] - times[0]
112
+
113
+ # Check if the timeseries are truly equidistant
114
+ if self.csv_validate_timeseries:
115
+ for i in range(len(times) - 1):
116
+ if times[i + 1] - times[i] != dt:
117
+ raise Exception(
118
+ "CSVMixin: Expecting equidistant timeseries, the time step "
119
+ "towards {} is not the same as the time step(s) before. "
120
+ "Set equidistant=False if this is intended.".format(times[i + 1])
121
+ )
122
+
123
+ def write(self):
124
+ # Call parent class first for default behaviour.
125
+ super().write()
126
+
127
+ times = self._simulation_times
128
+
129
+ # Write output
130
+ names = ["time"] + sorted(set(self._io_output_variables))
131
+ formats = ["O"] + (len(names) - 1) * ["f8"]
132
+ dtype = {"names": names, "formats": formats}
133
+ data = np.zeros(len(times), dtype=dtype)
134
+ data["time"] = self.io.sec_to_datetime(times, self.io.reference_datetime)
135
+ for variable in self._io_output_variables:
136
+ data[variable] = np.array(self._io_output[variable])
137
+
138
+ fname = os.path.join(self._output_folder, self.timeseries_export_basename + ".csv")
139
+ csv.save(fname, data, delimiter=self.csv_delimiter, with_time=True)
140
+
141
+ @cached
142
+ def initial_state(self):
143
+ """
144
+ The initial state. Includes entries from parent classes and initial_state.csv
145
+
146
+ :returns: A dictionary of variable names and initial state (t0) values.
147
+ """
148
+ # Call parent class first for default values.
149
+ initial_state = super().initial_state()
150
+
151
+ # Set of model vars that are allowed to have an initial state
152
+ valid_model_vars = set(self.get_state_variables()) | set(self.get_input_variables())
153
+
154
+ # Load initial states from __initial_state
155
+ for variable, value in self.__initial_state.items():
156
+ # Get the cannonical vars and signs
157
+ canonical_var, sign = self.alias_relation.canonical_signed(variable)
158
+
159
+ # Only store variables that are allowed to have an initial state
160
+ if canonical_var in valid_model_vars:
161
+ initial_state[canonical_var] = value * sign
162
+
163
+ if logger.getEffectiveLevel() == logging.DEBUG:
164
+ logger.debug("CSVMixin: Read initial state {} = {}".format(variable, value))
165
+ else:
166
+ logger.warning(
167
+ "CSVMixin: In initial_state.csv, {} is not an input or state variable.".format(
168
+ variable
169
+ )
170
+ )
171
+ return initial_state
@@ -0,0 +1,195 @@
1
+ import bisect
2
+ import logging
3
+ from abc import ABCMeta, abstractmethod
4
+ from math import isfinite
5
+
6
+ import numpy as np
7
+
8
+ from rtctools._internal.alias_tools import AliasDict
9
+ from rtctools._internal.caching import cached
10
+ from rtctools.simulation.simulation_problem import SimulationProblem
11
+
12
+ logger = logging.getLogger("rtctools")
13
+
14
+
15
+ class IOMixin(SimulationProblem, metaclass=ABCMeta):
16
+ """
17
+ Base class for all IO methods of optimization problems.
18
+ """
19
+
20
+ def __init__(self, **kwargs):
21
+ # Call parent class first for default behaviour.
22
+ super().__init__(**kwargs)
23
+
24
+ self._simulation_times = []
25
+
26
+ self.__first_update_call = True
27
+
28
+ def pre(self) -> None:
29
+ # Call read method to read all input
30
+ self.read()
31
+
32
+ self._simulation_times = []
33
+
34
+ @abstractmethod
35
+ def read(self) -> None:
36
+ """
37
+ Reads input data from files, storing it in the internal data store through the various set
38
+ or add methods
39
+ """
40
+ pass
41
+
42
+ def post(self) -> None:
43
+ # Call write method to write all output
44
+ self.write()
45
+
46
+ @abstractmethod
47
+ def write(self) -> None:
48
+ """
49
+ Writes output data to files, getting the data from the data store through the various get
50
+ methods
51
+ """
52
+ pass
53
+
54
+ def initialize(self, config_file=None):
55
+ # Set up experiment
56
+ timeseries_import_times = self.io.times_sec
57
+ self.__dt = timeseries_import_times[1] - timeseries_import_times[0]
58
+ self.setup_experiment(0, timeseries_import_times[-1], self.__dt)
59
+
60
+ parameter_variables = set(self.get_parameter_variables())
61
+
62
+ logger.debug("Model parameters are {}".format(parameter_variables))
63
+
64
+ for parameter, value in self.io.parameters().items():
65
+ if parameter in parameter_variables:
66
+ logger.debug("IOMixin: Setting parameter {} = {}".format(parameter, value))
67
+ self.set_var(parameter, value)
68
+
69
+ # Load input variable names
70
+ self.__input_variables = set(self.get_input_variables().keys())
71
+
72
+ # Set input values
73
+ t_idx = bisect.bisect_left(timeseries_import_times, 0.0)
74
+ self.__set_input_variables(t_idx)
75
+
76
+ logger.debug("Model inputs are {}".format(self.__input_variables))
77
+
78
+ # Set first timestep
79
+ self._simulation_times.append(self.get_current_time())
80
+
81
+ # Empty output
82
+ self._io_output_variables = self.get_output_variables()
83
+ self._io_output = AliasDict(self.alias_relation)
84
+
85
+ # Call super, which will also initialize the model itself
86
+ super().initialize(config_file)
87
+
88
+ # Extract consistent t0 values
89
+ for variable in self._io_output_variables:
90
+ self._io_output[variable] = [self.get_var(variable)]
91
+
92
+ def __set_input_variables(self, t_idx, use_cache=False):
93
+ if not use_cache:
94
+ self.__cache_loop_timeseries = {}
95
+
96
+ timeseries_names = set(self.io.get_timeseries_names(0))
97
+ for v in self.get_input_variables():
98
+ if v in timeseries_names:
99
+ _, values = self.io.get_timeseries_sec(v)
100
+ self.__cache_loop_timeseries[v] = values
101
+
102
+ for variable, values in self.__cache_loop_timeseries.items():
103
+ value = values[t_idx]
104
+ if isfinite(value):
105
+ self.set_var(variable, value)
106
+ else:
107
+ logger.debug(
108
+ "IOMixin: Found bad value {} at index [{}] "
109
+ "in timeseries aliased to input {}".format(value, t_idx, variable)
110
+ )
111
+
112
+ def update(self, dt):
113
+ # Time step
114
+ if dt < 0:
115
+ dt = self.__dt
116
+
117
+ # Current time stamp
118
+ t = self.get_current_time()
119
+ self._simulation_times.append(t + dt)
120
+
121
+ # Get current time index
122
+ t_idx = bisect.bisect_left(self.io.times_sec, t + dt)
123
+
124
+ # Set input values
125
+ self.__set_input_variables(t_idx, not self.__first_update_call)
126
+
127
+ # Call super
128
+ super().update(dt)
129
+
130
+ # Extract results
131
+ for variable, values in self._io_output.items():
132
+ values.append(self.get_var(variable))
133
+
134
+ self.__first_update_call = False
135
+
136
+ def extract_results(self):
137
+ """
138
+ Extracts the results of output
139
+
140
+ :returns: An AliasDict of output variables and results array format.
141
+ """
142
+ io_outputs_arrays = self._io_output.copy()
143
+ for k in io_outputs_arrays.keys():
144
+ io_outputs_arrays[k] = np.array(io_outputs_arrays[k])
145
+
146
+ return io_outputs_arrays
147
+
148
+ @cached
149
+ def parameters(self):
150
+ """
151
+ Return a dictionary of parameters, including parameters in the input files files.
152
+
153
+ :returns: Dictionary of parameters
154
+ """
155
+ # Call parent class first for default values.
156
+ parameters = super().parameters()
157
+
158
+ # Load parameters from input files (stored in internal data store)
159
+ for parameter_name, value in self.io.parameters().items():
160
+ parameters[parameter_name] = value
161
+
162
+ if logger.getEffectiveLevel() == logging.DEBUG:
163
+ for parameter_name in self.io.parameters().keys():
164
+ logger.debug("IOMixin: Read parameter {}".format(parameter_name))
165
+
166
+ return parameters
167
+
168
+ def times(self, variable=None):
169
+ """
170
+ Return a list of all the timesteps in seconds.
171
+
172
+ :param variable: Variable name.
173
+
174
+ :returns: List of all the timesteps in seconds.
175
+ """
176
+ idx = bisect.bisect_left(self.io.datetimes, self.io.reference_datetime)
177
+ return self.io.times_sec[idx:]
178
+
179
+ def timeseries_at(self, variable, t):
180
+ """
181
+ Return the value of a time series at the given time.
182
+
183
+ :param variable: Variable name.
184
+ :param t: Time.
185
+
186
+ :returns: The interpolated value of the time series.
187
+
188
+ :raises: KeyError
189
+ """
190
+ timeseries_times_sec, values = self.io.get_timeseries_sec(variable)
191
+ t_idx = bisect.bisect_left(timeseries_times_sec, t)
192
+ if timeseries_times_sec[t_idx] == t:
193
+ return values[t_idx]
194
+ else:
195
+ return np.interp(t, timeseries_times_sec, values)