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,1094 @@
1
+ import functools
2
+ import logging
3
+ import sys
4
+ from abc import ABCMeta, abstractmethod
5
+ from collections import OrderedDict
6
+ from typing import Callable, Dict, List, Union
7
+
8
+ import casadi as ca
9
+ import numpy as np
10
+
11
+ from .optimization_problem import OptimizationProblem
12
+ from .timeseries import Timeseries
13
+
14
+ logger = logging.getLogger("rtctools")
15
+
16
+
17
+ class _EmptyEnsembleList(list):
18
+ """
19
+ An indexable object containing infinitely many empty lists.
20
+ Only to be used as a placeholder.
21
+ """
22
+
23
+ def __getitem__(self, key):
24
+ return []
25
+
26
+
27
+ class _EmptyEnsembleOrderedDict(OrderedDict):
28
+ """
29
+ An indexable object containing infinitely many empty OrderedDicts.
30
+ Only to be used as a placeholder.
31
+ """
32
+
33
+ def __getitem__(self, key):
34
+ return OrderedDict()
35
+
36
+
37
+ class Goal(metaclass=ABCMeta):
38
+ r"""
39
+ Base class for lexicographic goal programming goals.
40
+
41
+ **Types of goals**
42
+
43
+ There are 2 types of goals: minimization goals and target goals.
44
+
45
+ *Minimization goals*
46
+
47
+ Minimization goals are of the form:
48
+
49
+ .. math::
50
+ \text{minimize } f(x).
51
+
52
+ *Target goals*
53
+
54
+ Target goals weakly enforce a constraint of the form
55
+
56
+ .. math::
57
+ m_{target} \leq g(x) \leq M_{target},
58
+
59
+ by turning it into a minimization problem of the form
60
+
61
+ .. math::
62
+ \text{minimize } & \epsilon^r, \\
63
+ \text{subject to } &g_{low}(\epsilon) \leq g(x) \leq g_{up}(\epsilon), \\
64
+ \text{and } &0 \leq \epsilon \leq 1,
65
+
66
+ where
67
+
68
+ .. math::
69
+ g_{low}(\epsilon) &:= (1-\epsilon) m_{target} + \epsilon m, \\
70
+ g_{up}(\epsilon) &:= (1-\epsilon) M_{target} + \epsilon M.
71
+
72
+ Here, :math:`m` and :math:`M` are hard constraints for :math:`g(x)`,
73
+ :math:`m_{target}` and :math:`M_{target}` are target bounds for :math:`g(x)`,
74
+ :math:`\epsilon` is an auxiliary variable
75
+ that indicates how strongly the target bounds are violated,
76
+ and :math:`\epsilon^r` is a function that indicates the variation of :math:`\epsilon`,
77
+ where the order :math:`r` is by default :math:`2`.
78
+ We have
79
+
80
+ .. math::
81
+ m < m_{target} \leq M_{target} < M.
82
+
83
+ Note that when :math:`\epsilon=0`,
84
+ the constraint on :math:`g(x)` becomes
85
+
86
+ .. math::
87
+ m_{target} \leq g(x) \leq M_{target}
88
+
89
+ and if :math:`\epsilon=1`, it becomes
90
+
91
+ .. math::
92
+ m \leq g(x) \leq M.
93
+
94
+
95
+ **Scaling goals**
96
+
97
+ Goals can be scaled by a nominal value :math:`c_{nom}`
98
+ to improve the performance of the solvers.
99
+ In case of a minimization goal, the scaled problem is given by
100
+
101
+ .. math::
102
+ \text{minimize } \hat{f}(x),
103
+
104
+ where :math:`\hat{f}(x) := f(x) / c_{nom}`.
105
+ In case of a target goal, the scaled problem is given by
106
+
107
+ .. math::
108
+ \text{minimize } & \epsilon^r, \\
109
+ \text{subject to } &\hat{g}_{low}(\epsilon) \leq \hat{g}(x) \leq \hat{g}_{up}(\epsilon), \\
110
+ \text{and } &0 \leq \epsilon \leq 1,
111
+
112
+ where :math:`\hat{g}(x) := g(x) / c_{nom}`,
113
+ :math:`\hat{g}_{low}(\epsilon) := {g}_{low}(\epsilon) / c_{nom}`,
114
+ and :math:`\hat{g}_{up}(\epsilon) := {g}_{up}(\epsilon) / c_{nom}`.
115
+
116
+
117
+ **Implementing goals**
118
+
119
+ A goal class is created by inheriting from the :py:class:Goal class and
120
+ overriding the :func:`function` method.
121
+ This method defines the goal function :math:`f(x)` in case of a minimization goal,
122
+ and the goal function :math:`g(x)` in case of a target goal.
123
+ A goal becomes a target goal
124
+ if either the class attribute ``target_min`` or ``target_max`` is set.
125
+
126
+ To further define a goal, the following class attributes can also be set.
127
+
128
+ :cvar function_range: Range of goal function :math:`[m ,M]`.
129
+ Only applies to target goals.
130
+ Required for a target goal.
131
+ :cvar function_nominal: Nominal value of a function :math:`c_{nom}`.
132
+ Used for scaling. Default is ``1``.
133
+ :cvar target_min: Desired lower bound for goal function :math:`m_{target}`.
134
+ Default is ``numpy.nan``.
135
+ :cvar target_max: Desired upper bound for goal function :math:`M_{target}`.
136
+ Default is ``numpy.nan``.
137
+ :cvar priority: Priority of a goal. Default is ``1``.
138
+ :cvar weight: Optional weighting applied to the goal. Default is ``1.0``.
139
+ :cvar order: Penalization order of goal violation :math:`r`. Default is ``2``.
140
+ :cvar critical: If ``True``, the algorithm will abort if this goal cannot be fully met.
141
+ Default is ``False``.
142
+ :cvar relaxation: Amount of slack added to the hard constraints related to the goal.
143
+ Must be a nonnegative value.
144
+ The unit is equal to that of the goal function.
145
+ Default is ``0.0``.
146
+
147
+ When ``target_min`` is set, but not ``target_max``,
148
+ the target goal becomes a lower bound target goal
149
+ and the constraint on :math:`g(x)` becomes
150
+
151
+ .. math::
152
+ g_{low}(\epsilon) \leq g(x).
153
+
154
+ Similary, if ``target_max`` is set, but not ``target_min``,
155
+ the target goal becomes a upper bound target goal
156
+ and the constraint on :math:`g(x)` becomes
157
+
158
+ .. math::
159
+ g(x) \leq g_{up}(\epsilon).
160
+
161
+ Relaxation is used to loosen the constraints that are set
162
+ after the optimization of the goal's priority.
163
+
164
+ Notes:
165
+ * If one is unsure about the function range,
166
+ it is recommended to overestimate this interval.
167
+ However, this will negatively influence how accurately the target bounds are met.
168
+ * The function range should be strictly larger than the target range.
169
+ In particular, :math:`m < m_{target}` and :math:`M_{target} < M`.
170
+ * In a path goal, the target can be a Timeseries.
171
+ * In case of multiple goals with the same priority,
172
+ it is crucial that an accurate function nominal value is provided.
173
+ This ensures that all goals are given similar importance.
174
+
175
+ A goal can be written in vector form. In a vector goal:
176
+ * The goal size determines how many goals there are.
177
+ * The goal function has shape ``(goal size, 1)``.
178
+ * The function is either minimized or has, possibly various, targets.
179
+ * Function nominal can either be an array with as many entries as the goal size or have a
180
+ single value.
181
+ * Function ranges can either be an array with as many entries as the goal size or have a
182
+ single value.
183
+ * In a goal, the target can either be an array with as many entries as the goal size or
184
+ have a single value.
185
+ * In a path goal, the target can also be a Timeseries whose values are either a
186
+ 1-dimensional vector or have as many columns as the goal size.
187
+
188
+ **Examples**
189
+
190
+ Example definition of the point goal :math:`x(t) \geq 1.1` for :math:`t=1.0` at priority 1::
191
+
192
+ class MyGoal(Goal):
193
+ def function(self, optimization_problem, ensemble_member):
194
+ # State 'x' at time t = 1.0
195
+ t = 1.0
196
+ return optimization_problem.state_at('x', t, ensemble_member)
197
+
198
+ function_range = (1.0, 2.0)
199
+ target_min = 1.1
200
+ priority = 1
201
+
202
+ Example definition of the path goal :math:`x(t) \geq 1.1` for all :math:`t` at priority 2::
203
+
204
+ class MyPathGoal(Goal):
205
+ def function(self, optimization_problem, ensemble_member):
206
+ # State 'x' at any point in time
207
+ return optimization_problem.state('x')
208
+
209
+ function_range = (1.0, 2.0)
210
+ target_min = 1.1
211
+ priority = 2
212
+
213
+ **Note path goals**
214
+
215
+ Note that for path goals, the ensemble member index is not passed to the call
216
+ to :func:`OptimizationProblem.state`. This call returns a time-independent symbol
217
+ that is also independent of the active ensemble member. Path goals are
218
+ applied to all times and all ensemble members simultaneously.
219
+
220
+ """
221
+
222
+ @abstractmethod
223
+ def function(self, optimization_problem: OptimizationProblem, ensemble_member: int) -> ca.MX:
224
+ """
225
+ This method returns a CasADi :class:`MX` object describing the goal function.
226
+
227
+ :returns: A CasADi :class:`MX` object.
228
+ """
229
+ pass
230
+
231
+ #: Range of goal function
232
+ function_range = (np.nan, np.nan)
233
+
234
+ #: Nominal value of function (used for scaling)
235
+ function_nominal = 1.0
236
+
237
+ #: Desired lower bound for goal function
238
+ target_min = np.nan
239
+
240
+ #: Desired upper bound for goal function
241
+ target_max = np.nan
242
+
243
+ #: Lower priority goals take precedence over higher priority goals.
244
+ priority = 1
245
+
246
+ #: Goals with the same priority are weighted off against each other in a
247
+ #: single objective function.
248
+ weight = 1.0
249
+
250
+ #: The goal violation value is taken to the order'th power in the objective
251
+ #: function.
252
+ order = 2
253
+
254
+ #: The size of the goal if it's a vector goal.
255
+ size = 1
256
+
257
+ #: Critical goals must always be fully satisfied.
258
+ critical = False
259
+
260
+ #: Absolute relaxation applied to the optimized values of this goal
261
+ relaxation = 0.0
262
+
263
+ #: Timeseries ID for function value data (optional)
264
+ function_value_timeseries_id = None
265
+
266
+ #: Timeseries ID for goal violation data (optional)
267
+ violation_timeseries_id = None
268
+
269
+ @property
270
+ def has_target_min(self) -> bool:
271
+ """
272
+ ``True`` if the user goal has min bounds.
273
+ """
274
+ if isinstance(self.target_min, Timeseries):
275
+ return True
276
+ else:
277
+ return np.any(np.isfinite(self.target_min))
278
+
279
+ @property
280
+ def has_target_max(self) -> bool:
281
+ """
282
+ ``True`` if the user goal has max bounds.
283
+ """
284
+ if isinstance(self.target_max, Timeseries):
285
+ return True
286
+ else:
287
+ return np.any(np.isfinite(self.target_max))
288
+
289
+ @property
290
+ def has_target_bounds(self) -> bool:
291
+ """
292
+ ``True`` if the user goal has min/max bounds.
293
+ """
294
+ return self.has_target_min or self.has_target_max
295
+
296
+ @property
297
+ def is_empty(self) -> bool:
298
+ target_min_set = isinstance(self.target_min, Timeseries) or np.any(
299
+ np.isfinite(self.target_min)
300
+ )
301
+ target_max_set = isinstance(self.target_max, Timeseries) or np.any(
302
+ np.isfinite(self.target_max)
303
+ )
304
+
305
+ if not target_min_set and not target_max_set:
306
+ # A minimization goal
307
+ return False
308
+
309
+ target_min = self.target_min
310
+ if isinstance(target_min, Timeseries):
311
+ target_min = target_min.values
312
+
313
+ target_max = self.target_max
314
+ if isinstance(target_max, Timeseries):
315
+ target_max = target_max.values
316
+
317
+ min_empty = not np.any(np.isfinite(target_min))
318
+ max_empty = not np.any(np.isfinite(target_max))
319
+
320
+ return min_empty and max_empty
321
+
322
+ def get_function_key(
323
+ self, optimization_problem: OptimizationProblem, ensemble_member: int
324
+ ) -> str:
325
+ """
326
+ Returns a key string uniquely identifying the goal function. This
327
+ is used to eliminate linearly dependent constraints from the optimization problem.
328
+ """
329
+ if hasattr(self, "function_key"):
330
+ return self.function_key
331
+
332
+ # This must be deterministic. See RTCTOOLS-485.
333
+ if not hasattr(Goal, "_function_key_counter"):
334
+ Goal._function_key_counter = 0
335
+ self.function_key = "{}_{}".format(self.__class__.__name__, Goal._function_key_counter)
336
+ Goal._function_key_counter += 1
337
+
338
+ return self.function_key
339
+
340
+ def __repr__(self) -> str:
341
+ return "{}(priority={}, target_min={}, target_max={}, function_range={})".format(
342
+ self.__class__, self.priority, self.target_min, self.target_max, self.function_range
343
+ )
344
+
345
+
346
+ class StateGoal(Goal):
347
+ r"""
348
+ Base class for lexicographic goal programming path goals that act on a single model state.
349
+
350
+ A state goal is defined by setting at least the ``state`` class variable.
351
+
352
+ :cvar state: State on which the goal acts. *Required*.
353
+ :cvar target_min: Desired lower bound for goal function. Default is ``numpy.nan``.
354
+ :cvar target_max: Desired upper bound for goal function. Default is ``numpy.nan``.
355
+ :cvar priority: Integer priority of goal. Default is ``1``.
356
+ :cvar weight: Optional weighting applied to the goal. Default is ``1.0``.
357
+ :cvar order: Penalization order of goal violation. Default is ``2``.
358
+ :cvar critical: If ``True``, the algorithm will abort if this goal cannot be fully met.
359
+ Default is ``False``.
360
+
361
+ Example definition of the goal :math:`x(t) \geq 1.1` for all :math:`t` at priority 2::
362
+
363
+ class MyStateGoal(StateGoal):
364
+ state = 'x'
365
+ target_min = 1.1
366
+ priority = 2
367
+
368
+ Contrary to ordinary ``Goal`` objects, ``PathGoal`` objects need to be initialized with an
369
+ ``OptimizationProblem`` instance to allow extraction of state metadata, such as bounds and
370
+ nominal values. Consequently, state goals must be instantiated as follows::
371
+
372
+ my_state_goal = MyStateGoal(optimization_problem)
373
+
374
+ Note that ``StateGoal`` is a helper class. State goals can also be defined using ``Goal`` as
375
+ direct base class, by implementing the ``function`` method and providing the
376
+ ``function_range`` and ``function_nominal`` class variables manually.
377
+
378
+ """
379
+
380
+ #: The state on which the goal acts.
381
+ state = None
382
+
383
+ def __init__(self, optimization_problem):
384
+ """
385
+ Initialize the state goal object.
386
+
387
+ :param optimization_problem: ``OptimizationProblem`` instance.
388
+ """
389
+
390
+ # Check whether a state has been specified
391
+ if self.state is None:
392
+ raise Exception("Please specify a state.")
393
+
394
+ # Extract state range from model
395
+ if self.has_target_bounds:
396
+ try:
397
+ self.function_range = optimization_problem.bounds()[self.state]
398
+ except KeyError:
399
+ raise Exception(
400
+ "State {} has no bounds or does not exist in the model.".format(self.state)
401
+ )
402
+
403
+ if self.function_range[0] is None:
404
+ raise Exception("Please provide a lower bound for state {}.".format(self.state))
405
+ if self.function_range[1] is None:
406
+ raise Exception("Please provide an upper bound for state {}.".format(self.state))
407
+
408
+ # Extract state nominal from model
409
+ self.function_nominal = optimization_problem.variable_nominal(self.state)
410
+
411
+ # Set function key
412
+ canonical, sign = optimization_problem.alias_relation.canonical_signed(self.state)
413
+ self.function_key = canonical if sign > 0.0 else "-" + canonical
414
+
415
+ def function(self, optimization_problem, ensemble_member):
416
+ return optimization_problem.state(self.state)
417
+
418
+ def __repr__(self):
419
+ return "{}(priority={}, state={}, target_min={}, target_max={}, function_range={})".format(
420
+ self.__class__,
421
+ self.priority,
422
+ self.state,
423
+ self.target_min,
424
+ self.target_max,
425
+ self.function_range,
426
+ )
427
+
428
+
429
+ class _GoalConstraint:
430
+ def __init__(
431
+ self,
432
+ goal: Goal,
433
+ function: Callable[[OptimizationProblem], ca.MX],
434
+ m: Union[float, np.ndarray, Timeseries],
435
+ M: Union[float, np.ndarray, Timeseries],
436
+ optimized: bool,
437
+ ):
438
+ assert isinstance(m, (float, np.ndarray, Timeseries))
439
+ assert isinstance(M, (float, np.ndarray, Timeseries))
440
+ assert type(m) is type(M)
441
+
442
+ # NumPy arrays only allowed for vector goals
443
+ if isinstance(m, np.ndarray):
444
+ assert len(m) == goal.size
445
+ assert len(M) == goal.size
446
+
447
+ self.goal = goal
448
+ self.function = function
449
+ self.min = m
450
+ self.max = M
451
+ self.optimized = optimized
452
+
453
+ def update_bounds(self, other, enforce="self"):
454
+ # NOTE: a.update_bounds(b) is _not_ the same as b.update_bounds(a).
455
+ # See how the 'enforce' parameter is used.
456
+
457
+ min_, max_ = self.min, self.max
458
+ other_min, other_max = other.min, other.max
459
+
460
+ if isinstance(min_, Timeseries):
461
+ assert isinstance(max_, Timeseries)
462
+ assert isinstance(other_min, Timeseries)
463
+ assert isinstance(other_max, Timeseries)
464
+
465
+ min_ = min_.values
466
+ max_ = max_.values
467
+ other_min = other_min.values
468
+ other_max = other_max.values
469
+
470
+ min_ = np.maximum(min_, other_min)
471
+ max_ = np.minimum(max_, other_max)
472
+
473
+ # Ensure new constraint bounds do not loosen or shift
474
+ # previous bounds due to numerical errors.
475
+ if enforce == "self":
476
+ min_ = np.minimum(max_, other_min)
477
+ max_ = np.maximum(min_, other_max)
478
+ else:
479
+ min_ = np.minimum(min_, other_max)
480
+ max_ = np.maximum(max_, other_min)
481
+
482
+ # Ensure consistency of bounds. Bounds may become inconsistent due to
483
+ # small numerical computation errors.
484
+ min_ = np.minimum(min_, max_)
485
+
486
+ if isinstance(self.min, Timeseries):
487
+ self.min = Timeseries(self.min.times, min_)
488
+ self.max = Timeseries(self.max.times, max_)
489
+ else:
490
+ self.min = min_
491
+ self.max = max_
492
+
493
+
494
+ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
495
+ def _gp_n_objectives(self, subproblem_objectives, subproblem_path_objectives, ensemble_member):
496
+ return (
497
+ ca.vertcat(*[o(self, ensemble_member) for o in subproblem_objectives]).size1()
498
+ + ca.vertcat(*[o(self, ensemble_member) for o in subproblem_path_objectives]).size1()
499
+ )
500
+
501
+ def _gp_objective(self, subproblem_objectives, n_objectives, ensemble_member):
502
+ if len(subproblem_objectives) > 0:
503
+ acc_objective = ca.sum1(
504
+ ca.vertcat(*[o(self, ensemble_member) for o in subproblem_objectives])
505
+ )
506
+
507
+ if self.goal_programming_options()["scale_by_problem_size"]:
508
+ acc_objective = acc_objective / n_objectives
509
+
510
+ return acc_objective
511
+ else:
512
+ return ca.MX(0)
513
+
514
+ def _gp_path_objective(self, subproblem_path_objectives, n_objectives, ensemble_member):
515
+ if len(subproblem_path_objectives) > 0:
516
+ acc_objective = ca.sum1(
517
+ ca.vertcat(*[o(self, ensemble_member) for o in subproblem_path_objectives])
518
+ )
519
+
520
+ if self.goal_programming_options()["scale_by_problem_size"]:
521
+ # Objective is already divided by number of active time steps
522
+ # at this point when `scale_by_problem_size` is set.
523
+ acc_objective = acc_objective / n_objectives
524
+
525
+ return acc_objective
526
+ else:
527
+ return ca.MX(0)
528
+
529
+ @abstractmethod
530
+ def goal_programming_options(self) -> Dict[str, Union[float, bool]]:
531
+ raise NotImplementedError()
532
+
533
+ def goals(self) -> List[Goal]:
534
+ """
535
+ User problem returns list of :class:`Goal` objects.
536
+
537
+ :returns: A list of goals.
538
+ """
539
+ return []
540
+
541
+ def path_goals(self) -> List[Goal]:
542
+ """
543
+ User problem returns list of path :class:`Goal` objects.
544
+
545
+ :returns: A list of path goals.
546
+ """
547
+ return []
548
+
549
+ def _gp_min_max_arrays(self, g, target_shape=None):
550
+ """
551
+ Broadcasts the goal target minimum and target maximum to arrays of a desired target shape.
552
+
553
+ Depending on whether g is a vector goal or not, the output shape differs:
554
+
555
+ - A 2-D array of size (goal.size, target_shape or 1) if the goal size
556
+ is larger than one, i.e. a vector goal
557
+ - A 1-D array of size (target_shape or 1, ) otherwise
558
+ """
559
+
560
+ times = self.times()
561
+
562
+ m, M = None, None
563
+ if isinstance(g.target_min, Timeseries):
564
+ m = self.interpolate(times, g.target_min.times, g.target_min.values, -np.inf, -np.inf)
565
+ if m.ndim > 1:
566
+ m = m.transpose()
567
+ elif isinstance(g.target_min, np.ndarray) and target_shape:
568
+ m = np.broadcast_to(g.target_min, (target_shape, g.size)).transpose()
569
+ elif target_shape:
570
+ m = np.full(target_shape, g.target_min)
571
+ else:
572
+ m = np.array([g.target_min]).transpose()
573
+ if isinstance(g.target_max, Timeseries):
574
+ M = self.interpolate(times, g.target_max.times, g.target_max.values, np.inf, np.inf)
575
+ if M.ndim > 1:
576
+ M = M.transpose()
577
+ elif isinstance(g.target_max, np.ndarray) and target_shape:
578
+ M = np.broadcast_to(g.target_max, (target_shape, g.size)).transpose()
579
+ elif target_shape:
580
+ M = np.full(target_shape, g.target_max)
581
+ else:
582
+ M = np.array([g.target_max]).transpose()
583
+
584
+ if g.size > 1 and m.ndim == 1:
585
+ m = np.broadcast_to(m, (g.size, len(m)))
586
+ if g.size > 1 and M.ndim == 1:
587
+ M = np.broadcast_to(M, (g.size, len(M)))
588
+
589
+ if g.size > 1:
590
+ assert m.shape == (g.size, 1 if target_shape is None else target_shape)
591
+ else:
592
+ assert m.shape == (1 if target_shape is None else target_shape,)
593
+ assert m.shape == M.shape
594
+
595
+ return m, M
596
+
597
+ def _gp_validate_goals(self, goals, is_path_goal):
598
+ goals = sorted(goals, key=lambda x: x.priority)
599
+
600
+ options = self.goal_programming_options()
601
+
602
+ # Validate goal definitions
603
+ for goal in goals:
604
+ m, M = goal.function_range
605
+
606
+ # The function range should not be a symbolic expression
607
+ if isinstance(m, ca.MX):
608
+ assert m.is_constant()
609
+ if m.size1() == 1:
610
+ m = float(m)
611
+ else:
612
+ m = np.array(m.to_DM())
613
+
614
+ if isinstance(M, ca.MX):
615
+ assert M.is_constant()
616
+ if M.size1() == 1:
617
+ M = float(M)
618
+ else:
619
+ M = np.array(M.to_DM())
620
+
621
+ assert isinstance(m, (float, int, np.ndarray))
622
+ assert isinstance(M, (float, int, np.ndarray))
623
+
624
+ if np.any(goal.function_nominal <= 0):
625
+ raise Exception("Nonpositive nominal value specified for goal {}".format(goal))
626
+
627
+ if goal.critical and not goal.has_target_bounds:
628
+ raise Exception("Minimization goals cannot be critical")
629
+
630
+ if goal.critical:
631
+ # Allow a function range for backwards compatibility reasons.
632
+ # Maybe raise a warning that its not actually used?
633
+ pass
634
+ elif goal.has_target_bounds:
635
+ if not np.all(np.isfinite(m)) or not np.all(np.isfinite(M)):
636
+ raise Exception("No function range specified for goal {}".format(goal))
637
+
638
+ if np.any(m >= M):
639
+ raise Exception("Invalid function range for goal {}".format(goal))
640
+
641
+ if goal.weight <= 0:
642
+ raise Exception("Goal weight should be positive for goal {}".format(goal))
643
+ else:
644
+ if goal.function_range != (np.nan, np.nan):
645
+ raise Exception(
646
+ "Specifying function range not allowed for goal {}".format(goal)
647
+ )
648
+
649
+ if not is_path_goal:
650
+ if isinstance(goal.target_min, Timeseries):
651
+ raise Exception("Target min cannot be a Timeseries for goal {}".format(goal))
652
+ if isinstance(goal.target_max, Timeseries):
653
+ raise Exception("Target max cannot be a Timeseries for goal {}".format(goal))
654
+
655
+ try:
656
+ int(goal.priority)
657
+ except ValueError:
658
+ raise Exception("Priority of not int or castable to int for goal {}".format(goal))
659
+
660
+ if options["keep_soft_constraints"]:
661
+ if goal.relaxation != 0.0:
662
+ raise Exception(
663
+ "Relaxation not allowed with `keep_soft_constraints` for goal {}".format(
664
+ goal
665
+ )
666
+ )
667
+ if goal.violation_timeseries_id is not None:
668
+ raise Exception(
669
+ "Violation timeseries id not allowed with "
670
+ "`keep_soft_constraints` for goal {}".format(goal)
671
+ )
672
+ else:
673
+ if goal.size > 1:
674
+ raise Exception(
675
+ "Option `keep_soft_constraints` needs to be set for vector goal {}".format(
676
+ goal
677
+ )
678
+ )
679
+
680
+ if goal.critical and goal.size > 1:
681
+ raise Exception("Vector goal cannot be critical for goal {}".format(goal))
682
+
683
+ if is_path_goal:
684
+ target_shape = len(self.times())
685
+ else:
686
+ target_shape = None
687
+
688
+ # Check consistency and monotonicity of goals. Scalar target min/max
689
+ # of normal goals are also converted to arrays to unify checks with
690
+ # path goals.
691
+ if options["check_monotonicity"]:
692
+ for e in range(self.ensemble_size):
693
+ # Store the previous goal of a certain function key we
694
+ # encountered, such that we can compare to it.
695
+ fk_goal_map = {}
696
+
697
+ for goal in goals:
698
+ fk = goal.get_function_key(self, e)
699
+ prev = fk_goal_map.get(fk)
700
+ fk_goal_map[fk] = goal
701
+
702
+ if prev is not None:
703
+ goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape)
704
+ other_m, other_M = self._gp_min_max_arrays(prev, target_shape)
705
+
706
+ indices = np.where(
707
+ np.logical_not(np.logical_or(np.isnan(goal_m), np.isnan(other_m)))
708
+ )
709
+ if goal.has_target_min:
710
+ if np.any(goal_m[indices] < other_m[indices]):
711
+ raise Exception(
712
+ "Target minimum of goal {} must be greater or equal than "
713
+ "target minimum of goal {}.".format(goal, prev)
714
+ )
715
+
716
+ indices = np.where(
717
+ np.logical_not(np.logical_or(np.isnan(goal_M), np.isnan(other_M)))
718
+ )
719
+ if goal.has_target_max:
720
+ if np.any(goal_M[indices] > other_M[indices]):
721
+ raise Exception(
722
+ "Target maximum of goal {} must be less or equal than "
723
+ "target maximum of goal {}".format(goal, prev)
724
+ )
725
+
726
+ for goal in goals:
727
+ goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape)
728
+ goal_lb = np.broadcast_to(goal.function_range[0], goal_m.shape[::-1]).transpose()
729
+ goal_ub = np.broadcast_to(goal.function_range[1], goal_M.shape[::-1]).transpose()
730
+
731
+ if goal.has_target_min and goal.has_target_max:
732
+ indices = np.where(
733
+ np.logical_not(np.logical_or(np.isnan(goal_m), np.isnan(goal_M)))
734
+ )
735
+
736
+ if np.any(goal_m[indices] > goal_M[indices]):
737
+ raise Exception(
738
+ "Target minimum exceeds target maximum for goal {}".format(goal)
739
+ )
740
+
741
+ if goal.has_target_min and not goal.critical:
742
+ indices = np.where(np.isfinite(goal_m))
743
+ if np.any(goal_m[indices] <= goal_lb[indices]):
744
+ raise Exception(
745
+ "Target minimum should be greater than the lower bound "
746
+ "of the function range for goal {}".format(goal)
747
+ )
748
+ if np.any(goal_m[indices] > goal_ub[indices]):
749
+ raise Exception(
750
+ "Target minimum should not be greater than the upper bound "
751
+ "of the function range for goal {}".format(goal)
752
+ )
753
+ if goal.has_target_max and not goal.critical:
754
+ indices = np.where(np.isfinite(goal_M))
755
+ if np.any(goal_M[indices] >= goal_ub[indices]):
756
+ raise Exception(
757
+ "Target maximum should be smaller than the upper bound "
758
+ "of the function range for goal {}".format(goal)
759
+ )
760
+ if np.any(goal_M[indices] < goal_lb[indices]):
761
+ raise Exception(
762
+ "Target maximum should not be smaller than the lower bound "
763
+ "of the function range for goal {}".format(goal)
764
+ )
765
+
766
+ if goal.relaxation < 0.0:
767
+ raise Exception("Relaxation of goal {} should be a nonnegative value".format(goal))
768
+
769
+ def _gp_goal_constraints(self, goals, sym_index, options, is_path_goal):
770
+ """
771
+ There are three ways in which a goal turns into objectives/constraints:
772
+
773
+ 1. A goal with target bounds results in a part for the objective (the
774
+ violation variable), and 1 or 2 constraints (target min, max, or both).
775
+ 2. A goal without target bounds (i.e. minimization goal) results in just a
776
+ part for the objective.
777
+ 3. A critical goal results in just a (pair of) constraint(s). These are hard
778
+ constraints, which need to be put in the constraint store to guarantee
779
+ linear independence.
780
+ """
781
+
782
+ epsilons = []
783
+ objectives = []
784
+ soft_constraints = [[] for ensemble_member in range(self.ensemble_size)]
785
+ hard_constraints = [[] for ensemble_member in range(self.ensemble_size)]
786
+ extra_constants = []
787
+
788
+ eps_format = "eps_{}_{}"
789
+ min_format = "min_{}_{}"
790
+ max_format = "max_{}_{}"
791
+
792
+ if is_path_goal:
793
+ eps_format = "path_" + eps_format
794
+ min_format = "path_" + min_format
795
+ max_format = "path_" + max_format
796
+
797
+ for j, goal in enumerate(goals):
798
+ if goal.critical:
799
+ assert goal.size == 1, "Critical goals cannot be vector goals"
800
+ epsilon = np.zeros(len(self.times()) if is_path_goal else 1)
801
+ elif goal.has_target_bounds:
802
+ epsilon = ca.MX.sym(eps_format.format(sym_index, j), goal.size)
803
+ epsilons.append(epsilon)
804
+
805
+ # Make symbols for the target bounds (if set)
806
+ if goal.has_target_min:
807
+ min_variable = min_format.format(sym_index, j)
808
+
809
+ # NOTE: When using a vector goal, we want to be sure that its constraints
810
+ # and objective end up _exactly_ equal to its non-vector equivalent. We
811
+ # therefore have to get rid of any superfluous/trivial constraints that
812
+ # would otherwise be generated by the vector goal.
813
+ target_min_slice_inds = np.full(goal.size, True)
814
+
815
+ if isinstance(goal.target_min, Timeseries):
816
+ target_min = Timeseries(goal.target_min.times, goal.target_min.values)
817
+ inds = np.logical_or(
818
+ np.isnan(target_min.values), np.isneginf(target_min.values)
819
+ )
820
+ target_min.values[inds] = -sys.float_info.max
821
+ n_times = len(goal.target_min.times)
822
+ target_min_slice_inds = ~np.all(
823
+ np.broadcast_to(inds.transpose(), (goal.size, n_times)), axis=1
824
+ )
825
+ elif isinstance(goal.target_min, np.ndarray):
826
+ target_min = goal.target_min.copy()
827
+ inds = np.logical_or(np.isnan(target_min), np.isneginf(target_min))
828
+ target_min[inds] = -sys.float_info.max
829
+ target_min_slice_inds = ~inds
830
+ else:
831
+ target_min = goal.target_min
832
+
833
+ extra_constants.append((min_variable, target_min))
834
+ else:
835
+ min_variable = None
836
+
837
+ if goal.has_target_max:
838
+ max_variable = max_format.format(sym_index, j)
839
+
840
+ target_max_slice_inds = np.full(goal.size, True)
841
+
842
+ if isinstance(goal.target_max, Timeseries):
843
+ target_max = Timeseries(goal.target_max.times, goal.target_max.values)
844
+ inds = np.logical_or(
845
+ np.isnan(target_max.values), np.isposinf(target_max.values)
846
+ )
847
+ target_max.values[inds] = sys.float_info.max
848
+ n_times = len(goal.target_max.times)
849
+ target_max_slice_inds = ~np.all(
850
+ np.broadcast_to(inds.transpose(), (goal.size, n_times)), axis=1
851
+ )
852
+ elif isinstance(goal.target_max, np.ndarray):
853
+ target_max = goal.target_max.copy()
854
+ inds = np.logical_or(np.isnan(target_max), np.isposinf(target_max))
855
+ target_max[inds] = sys.float_info.max
856
+ target_max_slice_inds = ~inds
857
+ else:
858
+ target_max = goal.target_max
859
+
860
+ extra_constants.append((max_variable, target_max))
861
+ else:
862
+ max_variable = None
863
+
864
+ # Make objective for soft constraints and minimization goals
865
+ if not goal.critical:
866
+ if hasattr(goal, "_objective_func"):
867
+ _objective_func = goal._objective_func
868
+ elif goal.has_target_bounds:
869
+ if is_path_goal and options["scale_by_problem_size"]:
870
+ goal_m, goal_M = self._gp_min_max_arrays(
871
+ goal, target_shape=len(self.times())
872
+ )
873
+ goal_active = np.isfinite(goal_m) | np.isfinite(goal_M)
874
+ n_active = np.sum(goal_active.astype(int), axis=-1)
875
+ # Avoid possible division by zero if goal is inactive
876
+ n_active = np.maximum(n_active, 1)
877
+ else:
878
+ n_active = 1
879
+
880
+ def _objective_func(
881
+ problem,
882
+ ensemble_member,
883
+ goal=goal,
884
+ epsilon=epsilon,
885
+ is_path_goal=is_path_goal,
886
+ n_active=n_active,
887
+ ):
888
+ if is_path_goal:
889
+ epsilon = problem.variable(epsilon.name())
890
+ else:
891
+ epsilon = problem.extra_variable(epsilon.name(), ensemble_member)
892
+
893
+ return goal.weight * ca.constpow(epsilon, goal.order) / n_active
894
+
895
+ else:
896
+ if is_path_goal and options["scale_by_problem_size"]:
897
+ n_active = len(self.times())
898
+ else:
899
+ n_active = 1
900
+
901
+ def _objective_func(
902
+ problem,
903
+ ensemble_member,
904
+ goal=goal,
905
+ is_path_goal=is_path_goal,
906
+ n_active=n_active,
907
+ ):
908
+ f = goal.function(problem, ensemble_member) / goal.function_nominal
909
+ return goal.weight * ca.constpow(f, goal.order) / n_active
910
+
911
+ objectives.append(_objective_func)
912
+
913
+ # Make constraints for goals with target bounds
914
+ if goal.has_target_bounds:
915
+ if goal.critical:
916
+ for ensemble_member in range(self.ensemble_size):
917
+ constraint = self._gp_goal_hard_constraint(
918
+ goal, epsilon, None, ensemble_member, options, is_path_goal
919
+ )
920
+ hard_constraints[ensemble_member].append(constraint)
921
+ else:
922
+ for ensemble_member in range(self.ensemble_size):
923
+ # We use a violation variable formulation, with the violation
924
+ # variables epsilon bounded between 0 and 1.
925
+ def _soft_constraint_func(
926
+ problem,
927
+ target,
928
+ bound,
929
+ inds,
930
+ goal=goal,
931
+ epsilon=epsilon,
932
+ ensemble_member=ensemble_member,
933
+ is_path_constraint=is_path_goal,
934
+ ):
935
+ if is_path_constraint:
936
+ target = problem.variable(target)
937
+ eps = problem.variable(epsilon.name())
938
+ else:
939
+ target = problem.parameters(ensemble_member)[target]
940
+ eps = problem.extra_variable(epsilon.name(), ensemble_member)
941
+
942
+ inds = inds.nonzero()[0].astype(int).tolist()
943
+
944
+ f = goal.function(problem, ensemble_member)
945
+ nominal = goal.function_nominal
946
+
947
+ return ca.if_else(
948
+ ca.fabs(target) < sys.float_info.max,
949
+ (f - eps * (bound - target) - target) / nominal,
950
+ 0.0,
951
+ )[inds]
952
+
953
+ if goal.has_target_min and np.any(target_min_slice_inds):
954
+ _f = functools.partial(
955
+ _soft_constraint_func,
956
+ target=min_variable,
957
+ bound=goal.function_range[0],
958
+ inds=target_min_slice_inds,
959
+ )
960
+ constraint = _GoalConstraint(goal, _f, 0.0, np.inf, False)
961
+ soft_constraints[ensemble_member].append(constraint)
962
+ if goal.has_target_max and np.any(target_max_slice_inds):
963
+ _f = functools.partial(
964
+ _soft_constraint_func,
965
+ target=max_variable,
966
+ bound=goal.function_range[1],
967
+ inds=target_max_slice_inds,
968
+ )
969
+ constraint = _GoalConstraint(goal, _f, -np.inf, 0.0, False)
970
+ soft_constraints[ensemble_member].append(constraint)
971
+
972
+ return epsilons, objectives, soft_constraints, hard_constraints, extra_constants
973
+
974
+ def _gp_goal_hard_constraint(
975
+ self, goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
976
+ ):
977
+ if not is_path_goal:
978
+ epsilon = epsilon[:1]
979
+
980
+ goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape=epsilon.shape[0])
981
+
982
+ if goal.has_target_bounds:
983
+ # We use a violation variable formulation, with the violation
984
+ # variables epsilon bounded between 0 and 1.
985
+ m, M = (
986
+ np.full_like(epsilon, -np.inf, dtype=np.float64),
987
+ np.full_like(epsilon, np.inf, dtype=np.float64),
988
+ )
989
+
990
+ # A function range does not have to be specified for critical
991
+ # goals. Avoid multiplying with NaN in that case.
992
+ if goal.has_target_min:
993
+ m = (
994
+ epsilon * ((goal.function_range[0] - goal_m) if not goal.critical else 0.0)
995
+ + goal_m
996
+ - goal.relaxation
997
+ ) / goal.function_nominal
998
+ if goal.has_target_max:
999
+ M = (
1000
+ epsilon * ((goal.function_range[1] - goal_M) if not goal.critical else 0.0)
1001
+ + goal_M
1002
+ + goal.relaxation
1003
+ ) / goal.function_nominal
1004
+
1005
+ if goal.has_target_min and goal.has_target_max:
1006
+ # Avoid comparing with NaN
1007
+ inds = ~(np.isnan(m) | np.isnan(M))
1008
+ inds[inds] &= np.abs(m[inds] - M[inds]) < options["equality_threshold"]
1009
+ if np.any(inds):
1010
+ avg = 0.5 * (m + M)
1011
+ m[inds] = M[inds] = avg[inds]
1012
+
1013
+ m[~np.isfinite(goal_m)] = -np.inf
1014
+ M[~np.isfinite(goal_M)] = np.inf
1015
+
1016
+ inds = epsilon > options["violation_tolerance"]
1017
+ if np.any(inds):
1018
+ if is_path_goal:
1019
+ expr = self.map_path_expression(
1020
+ goal.function(self, ensemble_member), ensemble_member
1021
+ )
1022
+ else:
1023
+ expr = goal.function(self, ensemble_member)
1024
+
1025
+ function = ca.Function("f", [self.solver_input], [expr])
1026
+ value = np.array(function(self.solver_output))
1027
+
1028
+ m[inds] = (value - goal.relaxation) / goal.function_nominal
1029
+ M[inds] = (value + goal.relaxation) / goal.function_nominal
1030
+
1031
+ m -= options["constraint_relaxation"]
1032
+ M += options["constraint_relaxation"]
1033
+ else:
1034
+ # Epsilon encodes the position within the function range.
1035
+ if options["fix_minimized_values"] and goal.relaxation == 0.0:
1036
+ m = epsilon / goal.function_nominal
1037
+ M = epsilon / goal.function_nominal
1038
+ self.check_collocation_linearity = False
1039
+ self.linear_collocation = False
1040
+ else:
1041
+ m = -np.inf * np.ones(epsilon.shape)
1042
+ M = (epsilon + goal.relaxation) / goal.function_nominal + options[
1043
+ "constraint_relaxation"
1044
+ ]
1045
+
1046
+ if is_path_goal:
1047
+ m = Timeseries(self.times(), m)
1048
+ M = Timeseries(self.times(), M)
1049
+ else:
1050
+ m = m[0]
1051
+ M = M[0]
1052
+
1053
+ constraint = _GoalConstraint(
1054
+ goal,
1055
+ lambda problem, ensemble_member=ensemble_member, goal=goal: (
1056
+ goal.function(problem, ensemble_member) / goal.function_nominal
1057
+ ),
1058
+ m,
1059
+ M,
1060
+ True,
1061
+ )
1062
+
1063
+ # Epsilon is fixed. Override previous {min,max} constraints for this
1064
+ # state.
1065
+ if existing_constraint:
1066
+ constraint.update_bounds(existing_constraint, enforce="other")
1067
+
1068
+ return constraint
1069
+
1070
+ def _gp_update_constraint_store(self, constraint_store, constraints):
1071
+ for ensemble_member in range(self.ensemble_size):
1072
+ for other in constraints[ensemble_member]:
1073
+ fk = other.goal.get_function_key(self, ensemble_member)
1074
+ try:
1075
+ constraint_store[ensemble_member][fk].update_bounds(other)
1076
+ except KeyError:
1077
+ constraint_store[ensemble_member][fk] = other
1078
+
1079
+ def priority_started(self, priority: int) -> None:
1080
+ """
1081
+ Called when optimization for goals of certain priority is started.
1082
+
1083
+ :param priority: The priority level that was started.
1084
+ """
1085
+ self.skip_priority = False
1086
+ pass
1087
+
1088
+ def priority_completed(self, priority: int) -> None:
1089
+ """
1090
+ Called after optimization for goals of certain priority is completed.
1091
+
1092
+ :param priority: The priority level that was completed.
1093
+ """
1094
+ pass