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,165 @@
1
+ import logging
2
+ from typing import Dict, Union
3
+
4
+ from .optimization_problem import OptimizationProblem
5
+ from .timeseries import Timeseries
6
+
7
+ logger = logging.getLogger("rtctools")
8
+
9
+
10
+ class HomotopyMixin(OptimizationProblem):
11
+ """
12
+ Adds homotopy to your optimization problem. A homotopy is a continuous transformation between
13
+ two optimization problems, parametrized by a single parameter :math:`\\theta \\in [0, 1]`.
14
+
15
+ Homotopy may be used to solve non-convex optimization problems, by starting with a convex
16
+ approximation at :math:`\\theta = 0.0` and ending with the non-convex problem at
17
+ :math:`\\theta = 1.0`.
18
+
19
+ .. note::
20
+
21
+ It is advised to look for convex reformulations of your problem, before resorting to a use
22
+ of the (potentially expensive) homotopy process.
23
+
24
+ """
25
+
26
+ def seed(self, ensemble_member):
27
+ seed = super().seed(ensemble_member)
28
+ options = self.homotopy_options()
29
+
30
+ # Overwrite the seed only when the results of the latest run are
31
+ # stored within this class. That is, when the GoalProgrammingMixin
32
+ # class is not used or at the first run of the goal programming loop.
33
+ if self.__theta > options["theta_start"] and getattr(self, "_gp_first_run", True):
34
+ for key, result in self.__results[ensemble_member].items():
35
+ times = self.times(key)
36
+ if (result.ndim == 1 and len(result) == len(times)) or (
37
+ result.ndim == 2 and result.shape[0] == len(times)
38
+ ):
39
+ # Only include seed timeseries which are consistent
40
+ # with the specified time stamps.
41
+ seed[key] = Timeseries(times, result)
42
+ elif (result.ndim == 1 and len(result) == 1) or (
43
+ result.ndim == 2 and result.shape[0] == 1
44
+ ):
45
+ seed[key] = result
46
+ return seed
47
+
48
+ def parameters(self, ensemble_member):
49
+ parameters = super().parameters(ensemble_member)
50
+
51
+ options = self.homotopy_options()
52
+ try:
53
+ # Only set the theta if we are in the optimization loop. We want
54
+ # to avoid accidental usage of the parameter value in e.g. pre().
55
+ # Note that we use a try-except here instead of hasattr, to avoid
56
+ # explicit name mangling.
57
+ parameters[options["homotopy_parameter"]] = self.__theta
58
+ except AttributeError:
59
+ pass
60
+
61
+ return parameters
62
+
63
+ def homotopy_options(self) -> Dict[str, Union[str, float]]:
64
+ """
65
+ Returns a dictionary of options controlling the homotopy process.
66
+
67
+ +------------------------+------------+---------------+
68
+ | Option | Type | Default value |
69
+ +========================+============+===============+
70
+ | ``theta_start`` | ``float`` | ``0.0`` |
71
+ +------------------------+------------+---------------+
72
+ | ``delta_theta_0`` | ``float`` | ``1.0`` |
73
+ +------------------------+------------+---------------+
74
+ | ``delta_theta_min`` | ``float`` | ``0.01`` |
75
+ +------------------------+------------+---------------+
76
+ | ``homotopy_parameter`` | ``string`` | ``theta`` |
77
+ +------------------------+------------+---------------+
78
+
79
+ The homotopy process is controlled by the homotopy parameter in the model, specified by the
80
+ option ``homotopy_parameter``. The homotopy parameter is initialized to ``theta_start``,
81
+ and increases to a value of ``1.0`` with a dynamically changing step size. This step size
82
+ is initialized with the value of the option ``delta_theta_0``. If this step size is too
83
+ large, i.e., if the problem with the increased homotopy parameter fails to converge, the
84
+ step size is halved. The process of halving terminates when the step size falls below the
85
+ minimum value specified by the option ``delta_theta_min``.
86
+
87
+ :returns: A dictionary of homotopy options.
88
+ """
89
+
90
+ return {
91
+ "theta_start": 0.0,
92
+ "delta_theta_0": 1.0,
93
+ "delta_theta_min": 0.01,
94
+ "homotopy_parameter": "theta",
95
+ }
96
+
97
+ def dynamic_parameters(self):
98
+ dynamic_parameters = super().dynamic_parameters()
99
+
100
+ if self.__theta > 0:
101
+ # For theta = 0, we don't mark the homotopy parameter as being dynamic,
102
+ # so that the correct sparsity structure is obtained for the linear model.
103
+ options = self.homotopy_options()
104
+ dynamic_parameters.append(self.variable(options["homotopy_parameter"]))
105
+
106
+ return dynamic_parameters
107
+
108
+ def optimize(self, preprocessing=True, postprocessing=True, log_solver_failure_as_error=True):
109
+ # Pre-processing
110
+ if preprocessing:
111
+ self.pre()
112
+
113
+ options = self.homotopy_options()
114
+ delta_theta = options["delta_theta_0"]
115
+
116
+ # Homotopy loop
117
+ self.__theta = options["theta_start"]
118
+
119
+ while self.__theta <= 1.0:
120
+ logger.info("Solving with homotopy parameter theta = {}.".format(self.__theta))
121
+
122
+ success = super().optimize(
123
+ preprocessing=False, postprocessing=False, log_solver_failure_as_error=False
124
+ )
125
+ if success:
126
+ self.__results = [
127
+ self.extract_results(ensemble_member)
128
+ for ensemble_member in range(self.ensemble_size)
129
+ ]
130
+
131
+ if self.__theta == 0.0:
132
+ self.check_collocation_linearity = False
133
+ self.linear_collocation = False
134
+
135
+ # Recompute the sparsity structure for the nonlinear model family.
136
+ self.clear_transcription_cache()
137
+
138
+ else:
139
+ if self.__theta == options["theta_start"]:
140
+ break
141
+
142
+ self.__theta -= delta_theta
143
+ delta_theta /= 2
144
+
145
+ if delta_theta < options["delta_theta_min"]:
146
+ failure_message = (
147
+ "Solver failed with homotopy parameter theta = {}. Theta cannot "
148
+ "be decreased further, as that would violate the minimum delta "
149
+ "theta of {}.".format(self.__theta, options["delta_theta_min"])
150
+ )
151
+ if log_solver_failure_as_error:
152
+ logger.error(failure_message)
153
+ else:
154
+ # In this case we expect some higher level process to deal
155
+ # with the solver failure, so we only log it as info here.
156
+ logger.info(failure_message)
157
+ break
158
+
159
+ self.__theta += delta_theta
160
+
161
+ # Post-processing
162
+ if postprocessing:
163
+ self.post()
164
+
165
+ return success
@@ -0,0 +1,89 @@
1
+ from typing import List, Tuple, Union
2
+
3
+ from .goal_programming_mixin import Goal, GoalProgrammingMixin
4
+
5
+
6
+ class _MeasurementGoal(Goal):
7
+ def __init__(self, state, measurement_id, max_deviation=1.0):
8
+ self.__state = state
9
+ self.__measurement_id = measurement_id
10
+
11
+ self.function_nominal = max_deviation
12
+
13
+ def function(self, optimization_problem, ensemble_member):
14
+ op = optimization_problem
15
+ return op.state_at(self.__state, op.initial_time, ensemble_member) - op.timeseries_at(
16
+ self.__measurement_id, op.initial_time, ensemble_member
17
+ )
18
+
19
+ order = 2
20
+ priority = -2
21
+
22
+
23
+ class _SmoothingGoal(Goal):
24
+ def __init__(self, state1, state2, max_deviation=1.0):
25
+ self.__state1 = state1
26
+ self.__state2 = state2
27
+
28
+ self.function_nominal = max_deviation
29
+
30
+ def function(self, optimization_problem, ensemble_member):
31
+ op = optimization_problem
32
+ return op.state_at(self.__state1, op.initial_time, ensemble_member) - op.state_at(
33
+ self.__state2, op.initial_time, ensemble_member
34
+ )
35
+
36
+ order = 2
37
+ priority = -1
38
+
39
+
40
+ class InitialStateEstimationMixin(GoalProgrammingMixin):
41
+ """
42
+ Adds initial state estimation to your optimization problem *using goal programming*.
43
+
44
+ Before any other goals are evaluated, first, the deviation between initial
45
+ state measurements and their respective model states is minimized in the
46
+ least squares sense (1DVAR, priority -2). Secondly, the distance between
47
+ pairs of states is minimized, again in the least squares sense, so that
48
+ "smooth" initial guesses are provided for states without measurements
49
+ (priority -1).
50
+
51
+ .. note::
52
+
53
+ There are types of problems where, in addition to minimizing
54
+ differences between states and measurements, it is advisable to
55
+ perform a steady-state initialization using additional initial-time
56
+ model equations. For hydraulic models, for instance, it is often
57
+ helpful to require that the time-derivative of the flow variables
58
+ vanishes at the initial time.
59
+
60
+ """
61
+
62
+ def initial_state_measurements(self) -> List[Union[Tuple[str, str], Tuple[str, str, float]]]:
63
+ """
64
+ List of pairs ``(state, measurement_id)`` or triples ``(state, measurement_id,
65
+ max_deviation)``, relating states to measurement time series IDs.
66
+
67
+ The default maximum deviation is ``1.0``.
68
+ """
69
+ return []
70
+
71
+ def initial_state_smoothing_pairs(self) -> List[Union[Tuple[str, str], Tuple[str, str, float]]]:
72
+ """
73
+ List of pairs ``(state1, state2)`` or triples ``(state1, state2, max_deviation)``, relating
74
+ states the distance of which is to be minimized.
75
+
76
+ The default maximum deviation is ``1.0``.
77
+ """
78
+ return []
79
+
80
+ def goals(self):
81
+ g = super().goals()
82
+
83
+ for measurement in self.initial_state_measurements():
84
+ g.append(_MeasurementGoal(*measurement))
85
+
86
+ for smoothing_pair in self.initial_state_smoothing_pairs():
87
+ g.append(_SmoothingGoal(*smoothing_pair))
88
+
89
+ return g
@@ -0,0 +1,320 @@
1
+ import bisect
2
+ import logging
3
+ import warnings
4
+ from abc import ABCMeta, abstractmethod
5
+
6
+ import casadi as ca
7
+ import numpy as np
8
+
9
+ from rtctools._internal.caching import cached
10
+ from rtctools.optimization.optimization_problem import OptimizationProblem
11
+ from rtctools.optimization.timeseries import Timeseries
12
+
13
+ logger = logging.getLogger("rtctools")
14
+
15
+
16
+ class IOMixin(OptimizationProblem, metaclass=ABCMeta):
17
+ """
18
+ Base class for all IO methods of optimization problems.
19
+ """
20
+
21
+ def __init__(self, **kwargs):
22
+ # Call parent class first for default behaviour.
23
+ super().__init__(**kwargs)
24
+
25
+ # Additional output variables
26
+ self.__output_timeseries = set()
27
+ self.__equidistant = False
28
+
29
+ def pre(self) -> None:
30
+ # Call parent class first for default behaviour.
31
+ super().pre()
32
+
33
+ # Call read method to read all input
34
+ self.read()
35
+
36
+ # Check equidistancy on read input data
37
+ if self.io.datetimes:
38
+ self.__equidistant = len(set(np.diff(self.io.datetimes))) == 1
39
+
40
+ @abstractmethod
41
+ def read(self) -> None:
42
+ """
43
+ Reads input data from files
44
+ """
45
+ pass
46
+
47
+ def post(self) -> None:
48
+ # Call parent class first for default behaviour.
49
+ super().post()
50
+
51
+ # Call write method to write all output
52
+ self.write()
53
+
54
+ @abstractmethod
55
+ def write(self) -> None:
56
+ """
57
+ Writes output data to files
58
+ """
59
+ pass
60
+
61
+ def times(self, variable=None) -> np.ndarray:
62
+ """
63
+ Returns the times in seconds from the reference datetime onwards.
64
+
65
+ :param variable:
66
+ """
67
+ times_sec = self.io.times_sec
68
+ t_idx = bisect.bisect_left(times_sec, 0.0)
69
+ return times_sec[t_idx:]
70
+
71
+ @property
72
+ def equidistant(self):
73
+ return self.__equidistant
74
+
75
+ @property
76
+ def ensemble_size(self):
77
+ return self.io.ensemble_size
78
+
79
+ def get_timeseries(self, variable: str, ensemble_member: int = 0) -> Timeseries:
80
+ return Timeseries(*self.io.get_timeseries_sec(variable, ensemble_member))
81
+
82
+ def set_timeseries(
83
+ self,
84
+ variable: str,
85
+ timeseries: Timeseries,
86
+ ensemble_member: int = 0,
87
+ output: bool = True,
88
+ check_consistency: bool = True,
89
+ ):
90
+ def stretch_values(values, t_pos):
91
+ # Construct a values range with preceding and possibly following nans
92
+ new_values = np.full(self.io.times_sec.shape, np.nan)
93
+ new_values[t_pos : t_pos + len(values)] = values
94
+ return new_values
95
+
96
+ if output:
97
+ self.__output_timeseries.add(variable)
98
+
99
+ if isinstance(timeseries, Timeseries):
100
+ if len(timeseries.values) != len(timeseries.times):
101
+ raise ValueError(
102
+ "IOMixin: Trying to set timeseries {} with times and values that are of "
103
+ "different length (lengths of {} and {}, respectively).".format(
104
+ variable, len(timeseries.times), len(timeseries.values)
105
+ )
106
+ )
107
+
108
+ timeseries_times_sec = self.io.times_sec
109
+
110
+ if not np.array_equal(timeseries_times_sec, timeseries.times):
111
+ if check_consistency:
112
+ if not set(timeseries_times_sec).issuperset(timeseries.times):
113
+ raise ValueError(
114
+ "IOMixin: Trying to set timeseries {} with different times "
115
+ "(in seconds) than the imported timeseries. Please make sure the "
116
+ "timeseries covers all timesteps of the longest "
117
+ "imported timeseries.".format(variable)
118
+ )
119
+
120
+ # Determine position of first times of added timeseries within the
121
+ # import times. For this we assume that both time ranges are ordered,
122
+ # and that the times of the added series is a subset of the import
123
+ # times.
124
+ t_pos = bisect.bisect_left(timeseries_times_sec, timeseries.times[0])
125
+
126
+ # Construct a new values range with length of self.io.get_times()
127
+ values = stretch_values(timeseries.values, t_pos)
128
+ else:
129
+ values = timeseries.values
130
+
131
+ else:
132
+ timeseries_times_sec = self.io.times_sec
133
+
134
+ if check_consistency:
135
+ if len(self.times()) != len(timeseries):
136
+ raise ValueError(
137
+ "IOMixin: Trying to set values for {} with a different "
138
+ "length ({}) than the forecast length ({}).".format(
139
+ variable, len(timeseries), len(self.times())
140
+ )
141
+ )
142
+ elif not set(timeseries_times_sec).issuperset(self.times()):
143
+ raise ValueError(
144
+ "IOMixin: Trying to set timeseries {} with different times "
145
+ "(in seconds) than the imported timeseries. Please make sure the "
146
+ "timeseries covers all timesteps of the longest "
147
+ "imported timeseries.".format(variable)
148
+ )
149
+
150
+ # If times is not supplied with the timeseries, we add the
151
+ # forecast times range to a new Timeseries object. Hereby
152
+ # we assume that the supplied values stretch from T0 to end.
153
+ t_pos = bisect.bisect_left(timeseries_times_sec, self.initial_time)
154
+
155
+ # Construct a new values range with length of self.io.get_times()
156
+ values = stretch_values(timeseries, t_pos)
157
+
158
+ self.io.set_timeseries(variable, self.io.datetimes, values, ensemble_member)
159
+
160
+ def min_timeseries_id(self, variable: str) -> str:
161
+ """
162
+ Returns the name of the lower bound timeseries for the specified variable.
163
+
164
+ :param variable: Variable name.
165
+ """
166
+ return "_".join((variable, "Min"))
167
+
168
+ def max_timeseries_id(self, variable: str) -> str:
169
+ """
170
+ Returns the name of the upper bound timeseries for the specified variable.
171
+
172
+ :param variable: Variable name.
173
+ """
174
+ return "_".join((variable, "Max"))
175
+
176
+ @cached
177
+ def bounds(self):
178
+ # Call parent class first for default values.
179
+ bounds = super().bounds()
180
+
181
+ io_times = self.io.times_sec
182
+ t_pos = bisect.bisect_left(io_times, self.initial_time)
183
+
184
+ # Load bounds from timeseries
185
+ for variable in self.dae_variables["free_variables"]:
186
+ variable_name = variable.name()
187
+
188
+ m, M = None, None
189
+
190
+ timeseries_id = self.min_timeseries_id(variable_name)
191
+ try:
192
+ _, values = self.io.get_timeseries_sec(timeseries_id, 0)
193
+ m = values[t_pos:]
194
+ except KeyError:
195
+ pass
196
+ else:
197
+ if logger.getEffectiveLevel() == logging.DEBUG:
198
+ logger.debug("Read lower bound for variable {}".format(variable_name))
199
+
200
+ timeseries_id = self.max_timeseries_id(variable_name)
201
+ try:
202
+ _, values = self.io.get_timeseries_sec(timeseries_id, 0)
203
+ M = values[t_pos:]
204
+ except KeyError:
205
+ pass
206
+ else:
207
+ if logger.getEffectiveLevel() == logging.DEBUG:
208
+ logger.debug("Read upper bound for variable {}".format(variable_name))
209
+
210
+ # Replace NaN with +/- inf, and create Timeseries objects
211
+ if m is not None:
212
+ m[np.isnan(m)] = np.finfo(m.dtype).min
213
+ m = Timeseries(io_times[t_pos:], m)
214
+ if M is not None:
215
+ M[np.isnan(M)] = np.finfo(M.dtype).max
216
+ M = Timeseries(io_times[t_pos:], M)
217
+
218
+ # Store
219
+ if m is not None or M is not None:
220
+ bounds[variable_name] = (m, M)
221
+ return bounds
222
+
223
+ @cached
224
+ def history(self, ensemble_member):
225
+ # Load history
226
+ history = super().history(ensemble_member)
227
+
228
+ end_index = bisect.bisect_left(self.io.times_sec, self.initial_time) + 1
229
+
230
+ variable_list = (
231
+ self.dae_variables["states"]
232
+ + self.dae_variables["algebraics"]
233
+ + self.dae_variables["control_inputs"]
234
+ + self.dae_variables["constant_inputs"]
235
+ )
236
+
237
+ for variable in variable_list:
238
+ variable = variable.name()
239
+ try:
240
+ times, values = self.io.get_timeseries_sec(variable, ensemble_member)
241
+ history[variable] = Timeseries(times[:end_index], values[:end_index])
242
+ except KeyError:
243
+ pass
244
+ else:
245
+ if logger.getEffectiveLevel() == logging.DEBUG:
246
+ logger.debug("IOMixin: Read history for state {}".format(variable))
247
+ return history
248
+
249
+ @cached
250
+ def seed(self, ensemble_member):
251
+ # Call parent class first for default values.
252
+ seed = super().seed(ensemble_member)
253
+
254
+ # Load seeds
255
+ for variable in self.dae_variables["free_variables"]:
256
+ variable = variable.name()
257
+ try:
258
+ s = Timeseries(*self.io.get_timeseries_sec(variable, ensemble_member))
259
+ except KeyError:
260
+ pass
261
+ else:
262
+ if logger.getEffectiveLevel() == logging.DEBUG:
263
+ logger.debug("IOMixin: Seeded free variable {}".format(variable))
264
+ # A seeding of NaN means no seeding
265
+ s.values[np.isnan(s.values)] = 0.0
266
+ seed[variable] = s
267
+ return seed
268
+
269
+ @cached
270
+ def parameters(self, ensemble_member):
271
+ # Call parent class first for default values.
272
+ parameters = super().parameters(ensemble_member)
273
+
274
+ for parameter, value in self.io.parameters(ensemble_member).items():
275
+ parameters[parameter] = value
276
+
277
+ # Done
278
+ return parameters
279
+
280
+ @cached
281
+ def constant_inputs(self, ensemble_member):
282
+ # Call parent class first for default values.
283
+ constant_inputs = super().constant_inputs(ensemble_member)
284
+
285
+ # Load inputs from timeseries
286
+ for variable in self.dae_variables["constant_inputs"]:
287
+ variable = variable.name()
288
+ try:
289
+ timeseries = Timeseries(*self.io.get_timeseries_sec(variable, ensemble_member))
290
+ except KeyError:
291
+ pass
292
+ else:
293
+ inds = timeseries.times >= self.initial_time
294
+ if np.any(np.isnan(timeseries.values[inds])):
295
+ raise Exception("IOMixin: Constant input {} contains NaN".format(variable))
296
+ constant_inputs[variable] = timeseries
297
+ if logger.getEffectiveLevel() == logging.DEBUG:
298
+ logger.debug("IOMixin: Read constant input {}".format(variable))
299
+ return constant_inputs
300
+
301
+ def timeseries_at(self, variable, t, ensemble_member=0):
302
+ return self.interpolate(t, *self.io.get_timeseries_sec(variable, ensemble_member))
303
+
304
+ @property
305
+ def output_variables(self):
306
+ variables = super().output_variables.copy()
307
+ variables.extend([ca.MX.sym(variable) for variable in self.__output_timeseries])
308
+ return variables
309
+
310
+ def get_forecast_index(self):
311
+ """
312
+ Deprecated, use `io.reference_datetime` and `io.datetimes`, or override behavior using
313
+ :py:meth:`OptimizationProblem.times` and/or :py:attr:`OptimizationProblem.initial_time`.
314
+ """
315
+ warnings.warn(
316
+ "get_forecast_index() is deprecated and will be removed in the future",
317
+ FutureWarning,
318
+ stacklevel=1,
319
+ )
320
+ return bisect.bisect_left(self.io.datetimes, self.io.reference_datetime)
@@ -0,0 +1,33 @@
1
+ from typing import Dict
2
+
3
+ from .optimization_problem import OptimizationProblem
4
+
5
+
6
+ class LinearizationMixin(OptimizationProblem):
7
+ """
8
+ Adds linearized equation parameter bookkeeping to your optimization aproblem.
9
+
10
+ If your model contains linearized equations, this mixin will set the
11
+ parameters of these equations based on the t0 value of an associated
12
+ timeseries.
13
+
14
+ The mapping between linearization parameters and time series is provided
15
+ in the ``linearization_parameters`` method.
16
+ """
17
+
18
+ def parameters(self, ensemble_member):
19
+ parameters = super().parameters(ensemble_member)
20
+
21
+ for parameter, timeseries_id in self.linearization_parameters().items():
22
+ parameters[parameter] = self.timeseries_at(
23
+ timeseries_id, self.initial_time, ensemble_member
24
+ )
25
+
26
+ return parameters
27
+
28
+ def linearization_parameters(self) -> Dict[str, str]:
29
+ """
30
+ :returns: A dictionary of parameter names mapping to time series identifiers.
31
+ """
32
+
33
+ return {}