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,769 @@
1
+ import itertools
2
+ import logging
3
+ from collections import OrderedDict
4
+ from typing import Dict, Union
5
+
6
+ import casadi as ca
7
+ import numpy as np
8
+
9
+ from rtctools._internal.alias_tools import AliasDict
10
+
11
+ from .goal_programming_mixin_base import ( # noqa: F401
12
+ Goal,
13
+ StateGoal,
14
+ _EmptyEnsembleList,
15
+ _EmptyEnsembleOrderedDict,
16
+ _GoalConstraint,
17
+ _GoalProgrammingMixinBase,
18
+ )
19
+ from .timeseries import Timeseries
20
+
21
+ logger = logging.getLogger("rtctools")
22
+
23
+
24
+ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
25
+ """
26
+ Adds lexicographic goal programming to your optimization problem.
27
+ """
28
+
29
+ def __init__(self, **kwargs):
30
+ # Call parent class first for default behaviour.
31
+ super().__init__(**kwargs)
32
+
33
+ # Initialize instance variables, so that the overridden methods may be
34
+ # called outside of the goal programming loop, for example in pre().
35
+ self._gp_first_run = True
36
+ self.__results_are_current = False
37
+ self.__subproblem_epsilons = []
38
+ self.__subproblem_objectives = []
39
+ self.__subproblem_soft_constraints = _EmptyEnsembleList()
40
+ self.__subproblem_parameters = []
41
+ self.__constraint_store = _EmptyEnsembleOrderedDict()
42
+
43
+ self.__subproblem_path_epsilons = []
44
+ self.__subproblem_path_objectives = []
45
+ self.__subproblem_path_soft_constraints = _EmptyEnsembleList()
46
+ self.__subproblem_path_timeseries = []
47
+ self.__path_constraint_store = _EmptyEnsembleOrderedDict()
48
+
49
+ self.__original_parameter_keys = {}
50
+ self.__original_constant_input_keys = {}
51
+
52
+ # Lists that are only filled when 'keep_soft_constraints' is True
53
+ self.__problem_constraints = _EmptyEnsembleList()
54
+ self.__problem_path_constraints = _EmptyEnsembleList()
55
+ self.__problem_epsilons = []
56
+ self.__problem_path_epsilons = []
57
+ self.__problem_path_timeseries = []
58
+ self.__problem_parameters = []
59
+
60
+ @property
61
+ def extra_variables(self):
62
+ return self.__problem_epsilons + self.__subproblem_epsilons
63
+
64
+ @property
65
+ def path_variables(self):
66
+ return self.__problem_path_epsilons + self.__subproblem_path_epsilons
67
+
68
+ def bounds(self):
69
+ bounds = super().bounds()
70
+ for epsilon in (
71
+ self.__subproblem_epsilons
72
+ + self.__subproblem_path_epsilons
73
+ + self.__problem_epsilons
74
+ + self.__problem_path_epsilons
75
+ ):
76
+ bounds[epsilon.name()] = (0.0, 1.0)
77
+ return bounds
78
+
79
+ def constant_inputs(self, ensemble_member):
80
+ constant_inputs = super().constant_inputs(ensemble_member)
81
+
82
+ if ensemble_member not in self.__original_constant_input_keys:
83
+ self.__original_constant_input_keys[ensemble_member] = set(constant_inputs.keys())
84
+
85
+ # Remove min/max timeseries of previous priorities
86
+ for k in set(constant_inputs.keys()):
87
+ if k not in self.__original_constant_input_keys[ensemble_member]:
88
+ del constant_inputs[k]
89
+
90
+ n_times = len(self.times())
91
+
92
+ # Append min/max timeseries to the constant inputs. Note that min/max
93
+ # timeseries are shared between all ensemble members.
94
+ for variable, value in self.__subproblem_path_timeseries + self.__problem_path_timeseries:
95
+ if isinstance(value, np.ndarray):
96
+ value = Timeseries(self.times(), np.broadcast_to(value, (n_times, len(value))))
97
+ elif not isinstance(value, Timeseries):
98
+ value = Timeseries(self.times(), np.full(n_times, value))
99
+
100
+ constant_inputs[variable] = value
101
+ return constant_inputs
102
+
103
+ def parameters(self, ensemble_member):
104
+ parameters = super().parameters(ensemble_member)
105
+
106
+ if ensemble_member not in self.__original_parameter_keys:
107
+ self.__original_parameter_keys[ensemble_member] = set(parameters.keys())
108
+
109
+ # Remove min/max parameters of previous priorities
110
+ for k in set(parameters.keys()):
111
+ if k not in self.__original_parameter_keys[ensemble_member]:
112
+ del parameters[k]
113
+
114
+ # Append min/max values to the parameters. Note that min/max values
115
+ # are shared between all ensemble members.
116
+ for variable, value in self.__subproblem_parameters + self.__problem_parameters:
117
+ parameters[variable] = value
118
+ return parameters
119
+
120
+ def seed(self, ensemble_member):
121
+ if self._gp_first_run:
122
+ seed = super().seed(ensemble_member)
123
+ else:
124
+ # Seed with previous results
125
+ seed = AliasDict(self.alias_relation)
126
+ for key, result in self.__results[ensemble_member].items():
127
+ times = self.times(key)
128
+ if (result.ndim == 1 and len(result) == len(times)) or (
129
+ result.ndim == 2 and result.shape[0] == len(times)
130
+ ):
131
+ # Only include seed timeseries which are consistent
132
+ # with the specified time stamps.
133
+ seed[key] = Timeseries(times, result)
134
+ elif (result.ndim == 1 and len(result) == 1) or (
135
+ result.ndim == 2 and result.shape[0] == 1
136
+ ):
137
+ seed[key] = result
138
+
139
+ # Seed epsilons of current priority
140
+ for epsilon in self.__subproblem_epsilons:
141
+ eps_size = epsilon.size1()
142
+ if eps_size > 1:
143
+ seed[epsilon.name()] = np.ones(eps_size)
144
+ else:
145
+ seed[epsilon.name()] = 1.0
146
+
147
+ times = self.times()
148
+ for epsilon in self.__subproblem_path_epsilons:
149
+ eps_size = epsilon.size1()
150
+ if eps_size > 1:
151
+ seed[epsilon.name()] = Timeseries(times, np.ones((eps_size, len(times))))
152
+ else:
153
+ seed[epsilon.name()] = Timeseries(times, np.ones(len(times)))
154
+
155
+ return seed
156
+
157
+ def objective(self, ensemble_member):
158
+ n_objectives = self._gp_n_objectives(
159
+ self.__subproblem_objectives, self.__subproblem_path_objectives, ensemble_member
160
+ )
161
+ return self._gp_objective(self.__subproblem_objectives, n_objectives, ensemble_member)
162
+
163
+ def path_objective(self, ensemble_member):
164
+ n_objectives = self._gp_n_objectives(
165
+ self.__subproblem_objectives, self.__subproblem_path_objectives, ensemble_member
166
+ )
167
+ return self._gp_path_objective(
168
+ self.__subproblem_path_objectives, n_objectives, ensemble_member
169
+ )
170
+
171
+ def constraints(self, ensemble_member):
172
+ constraints = super().constraints(ensemble_member)
173
+
174
+ additional_constraints = itertools.chain(
175
+ self.__constraint_store[ensemble_member].values(),
176
+ self.__problem_constraints[ensemble_member],
177
+ self.__subproblem_soft_constraints[ensemble_member],
178
+ )
179
+
180
+ for constraint in additional_constraints:
181
+ constraints.append((constraint.function(self), constraint.min, constraint.max))
182
+
183
+ return constraints
184
+
185
+ def path_constraints(self, ensemble_member):
186
+ path_constraints = super().path_constraints(ensemble_member)
187
+
188
+ additional_path_constraints = itertools.chain(
189
+ self.__path_constraint_store[ensemble_member].values(),
190
+ self.__problem_path_constraints[ensemble_member],
191
+ self.__subproblem_path_soft_constraints[ensemble_member],
192
+ )
193
+
194
+ for constraint in additional_path_constraints:
195
+ path_constraints.append((constraint.function(self), constraint.min, constraint.max))
196
+
197
+ return path_constraints
198
+
199
+ def solver_options(self):
200
+ # Call parent
201
+ options = super().solver_options()
202
+
203
+ solver = options["solver"]
204
+ assert solver in ["bonmin", "ipopt"]
205
+
206
+ # Make sure constant states, such as min/max timeseries for violation variables,
207
+ # are turned into parameters for the final optimization problem.
208
+ ipopt_options = options[solver]
209
+ ipopt_options["fixed_variable_treatment"] = "make_parameter"
210
+
211
+ # Define temporary variable to avoid infinite loop between
212
+ # solver_options and goal_programming_options.
213
+ self._loop_breaker_solver_options = True
214
+
215
+ if not hasattr(self, "_loop_breaker_goal_programming_options"):
216
+ if not self.goal_programming_options()["mu_reinit"]:
217
+ ipopt_options["mu_strategy"] = "monotone"
218
+ if not self._gp_first_run:
219
+ ipopt_options["mu_init"] = self.solver_stats["iterations"]["mu"][-1]
220
+
221
+ delattr(self, "_loop_breaker_solver_options")
222
+
223
+ return options
224
+
225
+ def goal_programming_options(self) -> Dict[str, Union[float, bool]]:
226
+ """
227
+ Returns a dictionary of options controlling the goal programming process.
228
+
229
+ +---------------------------+-----------+---------------+
230
+ | Option | Type | Default value |
231
+ +===========================+===========+===============+
232
+ | ``violation_relaxation`` | ``float`` | ``0.0`` |
233
+ +---------------------------+-----------+---------------+
234
+ | ``constraint_relaxation`` | ``float`` | ``0.0`` |
235
+ +---------------------------+-----------+---------------+
236
+ | ``mu_reinit`` | ``bool`` | ``True`` |
237
+ +---------------------------+-----------+---------------+
238
+ | ``fix_minimized_values`` | ``bool`` | ``True/False``|
239
+ +---------------------------+-----------+---------------+
240
+ | ``check_monotonicity`` | ``bool`` | ``True`` |
241
+ +---------------------------+-----------+---------------+
242
+ | ``equality_threshold`` | ``float`` | ``1e-8`` |
243
+ +---------------------------+-----------+---------------+
244
+ | ``interior_distance`` | ``float`` | ``1e-6`` |
245
+ +---------------------------+-----------+---------------+
246
+ | ``scale_by_problem_size`` | ``bool`` | ``False`` |
247
+ +---------------------------+-----------+---------------+
248
+ | ``keep_soft_constraints`` | ``bool`` | ``False`` |
249
+ +---------------------------+-----------+---------------+
250
+
251
+ Before turning a soft constraint of the goal programming algorithm into a hard constraint,
252
+ the violation variable (also known as epsilon) of each goal is relaxed with the
253
+ ``violation_relaxation``. Use of this option is normally not required.
254
+
255
+ When turning a soft constraint of the goal programming algorithm into a hard constraint,
256
+ the constraint is relaxed with ``constraint_relaxation``. Use of this option is
257
+ normally not required. Note that:
258
+
259
+ 1. Minimization goals do not get ``constraint_relaxation`` applied when
260
+ ``fix_minimized_values`` is True.
261
+
262
+ 2. Because of the constraints it generates, when ``keep_soft_constraints`` is True, the
263
+ option ``fix_minimized_values`` needs to be set to False for the
264
+ ``constraint_relaxation`` to be applied at all.
265
+
266
+ A goal is considered to be violated if the violation, scaled between 0 and 1, is greater
267
+ than the specified tolerance. Violated goals are fixed. Use of this option is normally not
268
+ required.
269
+
270
+ When using the default solver (IPOPT), its barrier parameter ``mu`` is
271
+ normally re-initialized at every iteration of the goal programming
272
+ algorithm, unless mu_reinit is set to ``False``. Use of this option
273
+ is normally not required.
274
+
275
+ If ``fix_minimized_values`` is set to ``True``, goal functions will be set to equal their
276
+ optimized values in optimization problems generated during subsequent priorities. Otherwise,
277
+ only an upper bound will be set. Use of this option is normally not required.
278
+ Note that a non-zero goal relaxation overrules this option; a non-zero relaxation will
279
+ always result in only an upper bound being set.
280
+ Also note that the use of this option may add non-convex constraints to the optimization
281
+ problem.
282
+ The default value for this parameter is ``True`` for the default solvers IPOPT/BONMIN. If
283
+ any other solver is used, the default value is ``False``.
284
+
285
+ If ``check_monotonicity`` is set to ``True``, then it will be checked whether goals with
286
+ the same function key form a monotonically decreasing sequence with regards to the target
287
+ interval.
288
+
289
+ The option ``equality_threshold`` controls when a two-sided inequality constraint is folded
290
+ into an equality constraint.
291
+
292
+ The option ``interior_distance`` controls the distance from the scaled target bounds,
293
+ starting from which the function value is considered to lie in the interior of the target
294
+ space.
295
+
296
+ If ``scale_by_problem_size`` is set to ``True``, the objective (i.e. the sum of the
297
+ violation variables) will be divided by the number of goals, and the path objective will
298
+ be divided by the number of path goals and the number of active time steps (per goal).
299
+ This will make sure the objectives are always in the range [0, 1], at the cost of solving
300
+ each goal/time step less accurately.
301
+
302
+ The option ``keep_soft_constraints`` controls how the epsilon variables introduced in the
303
+ target goals are dealt with in subsequent priorities.
304
+ If ``keep_soft_constraints`` is set to False, each epsilon is replaced by its computed
305
+ value and those are used to derive a new set of constraints.
306
+ If ``keep_soft_constraints`` is set to True, the epsilons are kept as variables and the
307
+ constraints are not modified. To ensure the goal programming philosophy, i.e., Pareto
308
+ optimality, a single constraint is added to enforce that the objective function must
309
+ always be at most the objective value. This method allows for a larger solution space, at
310
+ the cost of having a (possibly) more complex optimization problem. Indeed, more variables
311
+ are kept around throughout the optimization and any objective function is turned into a
312
+ constraint for the subsequent priorities (while in the False option this was the case only
313
+ for the function of minimization goals).
314
+
315
+ :returns: A dictionary of goal programming options.
316
+ """
317
+
318
+ options = {}
319
+
320
+ options["mu_reinit"] = True
321
+ options["violation_relaxation"] = 0.0 # Disable by default
322
+ options["constraint_relaxation"] = 0.0 # Disable by default
323
+ options["violation_tolerance"] = np.inf # Disable by default
324
+ options["fix_minimized_values"] = False
325
+ options["check_monotonicity"] = True
326
+ options["equality_threshold"] = 1e-8
327
+ options["interior_distance"] = 1e-6
328
+ options["scale_by_problem_size"] = False
329
+ options["keep_soft_constraints"] = False
330
+
331
+ # Define temporary variable to avoid infinite loop between
332
+ # solver_options and goal_programming_options.
333
+ self._loop_breaker_goal_programming_options = True
334
+
335
+ if not hasattr(self, "_loop_breaker_solver_options"):
336
+ if self.solver_options()["solver"] in {"ipopt", "bonmin"}:
337
+ options["fix_minimized_values"] = True
338
+
339
+ delattr(self, "_loop_breaker_goal_programming_options")
340
+
341
+ return options
342
+
343
+ def __goal_hard_constraint(
344
+ self, goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
345
+ ):
346
+ if not is_path_goal:
347
+ epsilon = epsilon[:1]
348
+
349
+ goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape=epsilon.shape[0])
350
+
351
+ if goal.has_target_bounds:
352
+ # We use a violation variable formulation, with the violation
353
+ # variables epsilon bounded between 0 and 1.
354
+ m, M = (
355
+ np.full_like(epsilon, -np.inf, dtype=np.float64),
356
+ np.full_like(epsilon, np.inf, dtype=np.float64),
357
+ )
358
+
359
+ # A function range does not have to be specified for critical
360
+ # goals. Avoid multiplying with NaN in that case.
361
+ if goal.has_target_min:
362
+ m = (
363
+ epsilon * ((goal.function_range[0] - goal_m) if not goal.critical else 0.0)
364
+ + goal_m
365
+ - goal.relaxation
366
+ ) / goal.function_nominal
367
+ if goal.has_target_max:
368
+ M = (
369
+ epsilon * ((goal.function_range[1] - goal_M) if not goal.critical else 0.0)
370
+ + goal_M
371
+ + goal.relaxation
372
+ ) / goal.function_nominal
373
+
374
+ if goal.has_target_min and goal.has_target_max:
375
+ # Avoid comparing with NaN
376
+ inds = ~(np.isnan(m) | np.isnan(M))
377
+ inds[inds] &= np.abs(m[inds] - M[inds]) < options["equality_threshold"]
378
+ if np.any(inds):
379
+ avg = 0.5 * (m + M)
380
+ m[inds] = M[inds] = avg[inds]
381
+
382
+ m[~np.isfinite(goal_m)] = -np.inf
383
+ M[~np.isfinite(goal_M)] = np.inf
384
+
385
+ inds = epsilon > options["violation_tolerance"]
386
+ if np.any(inds):
387
+ if is_path_goal:
388
+ expr = self.map_path_expression(
389
+ goal.function(self, ensemble_member), ensemble_member
390
+ )
391
+ else:
392
+ expr = goal.function(self, ensemble_member)
393
+
394
+ function = ca.Function("f", [self.solver_input], [expr])
395
+ value = np.array(function(self.solver_output))
396
+
397
+ m[inds] = (value - goal.relaxation) / goal.function_nominal
398
+ M[inds] = (value + goal.relaxation) / goal.function_nominal
399
+
400
+ m -= options["constraint_relaxation"]
401
+ M += options["constraint_relaxation"]
402
+ else:
403
+ # Epsilon encodes the position within the function range.
404
+ if options["fix_minimized_values"] and goal.relaxation == 0.0:
405
+ m = epsilon / goal.function_nominal
406
+ M = epsilon / goal.function_nominal
407
+ self.check_collocation_linearity = False
408
+ self.linear_collocation = False
409
+ else:
410
+ m = -np.inf * np.ones(epsilon.shape)
411
+ M = (epsilon + goal.relaxation) / goal.function_nominal + options[
412
+ "constraint_relaxation"
413
+ ]
414
+
415
+ if is_path_goal:
416
+ m = Timeseries(self.times(), m)
417
+ M = Timeseries(self.times(), M)
418
+ else:
419
+ m = m[0]
420
+ M = M[0]
421
+
422
+ constraint = _GoalConstraint(
423
+ goal,
424
+ lambda problem, ensemble_member=ensemble_member, goal=goal: (
425
+ goal.function(problem, ensemble_member) / goal.function_nominal
426
+ ),
427
+ m,
428
+ M,
429
+ True,
430
+ )
431
+
432
+ # Epsilon is fixed. Override previous {min,max} constraints for this
433
+ # state.
434
+ if existing_constraint:
435
+ constraint.update_bounds(existing_constraint, enforce="other")
436
+
437
+ return constraint
438
+
439
+ def __soft_to_hard_constraints(self, goals, sym_index, is_path_goal):
440
+ if is_path_goal:
441
+ constraint_store = self.__path_constraint_store
442
+ else:
443
+ constraint_store = self.__constraint_store
444
+
445
+ times = self.times()
446
+ options = self.goal_programming_options()
447
+
448
+ eps_format = "eps_{}_{}"
449
+ if is_path_goal:
450
+ eps_format = "path_" + eps_format
451
+
452
+ # Handle function evaluation in a grouped manner to save time with
453
+ # the call map_path_expression(). Repeated calls will make
454
+ # repeated CasADi Function objects, which can be slow.
455
+ goal_function_values = [None] * self.ensemble_size
456
+
457
+ for ensemble_member in range(self.ensemble_size):
458
+ goal_functions = OrderedDict()
459
+
460
+ for j, goal in enumerate(goals):
461
+ if (
462
+ not goal.has_target_bounds
463
+ or goal.violation_timeseries_id is not None
464
+ or goal.function_value_timeseries_id is not None
465
+ ):
466
+ goal_functions[j] = goal.function(self, ensemble_member)
467
+
468
+ if is_path_goal:
469
+ expr = self.map_path_expression(
470
+ ca.vertcat(*goal_functions.values()), ensemble_member
471
+ )
472
+ else:
473
+ expr = ca.transpose(ca.vertcat(*goal_functions.values()))
474
+
475
+ f = ca.Function("f", [self.solver_input], [expr])
476
+ raw_function_values = np.array(f(self.solver_output))
477
+ goal_function_values[ensemble_member] = {
478
+ k: raw_function_values[:, j].ravel() for j, k in enumerate(goal_functions.keys())
479
+ }
480
+
481
+ # Re-add constraints, this time with epsilon values fixed
482
+ for ensemble_member in range(self.ensemble_size):
483
+ for j, goal in enumerate(goals):
484
+ if j in goal_function_values[ensemble_member]:
485
+ function_value = goal_function_values[ensemble_member][j]
486
+
487
+ # Store results
488
+ if goal.function_value_timeseries_id is not None:
489
+ self.set_timeseries(
490
+ goal.function_value_timeseries_id,
491
+ Timeseries(times, function_value),
492
+ ensemble_member,
493
+ )
494
+
495
+ if goal.critical:
496
+ continue
497
+
498
+ if goal.has_target_bounds:
499
+ epsilon = self.__results[ensemble_member][eps_format.format(sym_index, j)]
500
+
501
+ # Store results
502
+ if goal.violation_timeseries_id is not None:
503
+ function_value = goal_function_values[ensemble_member][j]
504
+ epsilon_active = np.copy(epsilon)
505
+ m = goal.target_min
506
+ if isinstance(m, Timeseries):
507
+ m = self.interpolate(
508
+ times, goal.target_min.times, goal.target_min.values
509
+ )
510
+ M = goal.target_max
511
+ if isinstance(M, Timeseries):
512
+ M = self.interpolate(
513
+ times, goal.target_max.times, goal.target_max.values
514
+ )
515
+ w = np.ones_like(function_value)
516
+ if goal.has_target_min:
517
+ # Avoid comparing with NaN while making sure that
518
+ # w[i] is True when m[i] is not finite.
519
+ m = np.array(m)
520
+ m[~np.isfinite(m)] = -np.inf
521
+ w = np.logical_and(
522
+ w,
523
+ (
524
+ function_value / goal.function_nominal
525
+ > m / goal.function_nominal + options["interior_distance"]
526
+ ),
527
+ )
528
+ if goal.has_target_max:
529
+ # Avoid comparing with NaN while making sure that
530
+ # w[i] is True when M[i] is not finite.
531
+ M = np.array(M)
532
+ M[~np.isfinite(M)] = np.inf
533
+ w = np.logical_and(
534
+ w,
535
+ (
536
+ function_value / goal.function_nominal
537
+ < M / goal.function_nominal + options["interior_distance"]
538
+ ),
539
+ )
540
+ epsilon_active[w] = np.nan
541
+ self.set_timeseries(
542
+ goal.violation_timeseries_id,
543
+ Timeseries(times, epsilon_active),
544
+ ensemble_member,
545
+ )
546
+
547
+ # Add a relaxation to appease the barrier method.
548
+ epsilon += options["violation_relaxation"]
549
+ else:
550
+ epsilon = function_value
551
+
552
+ fk = goal.get_function_key(self, ensemble_member)
553
+ existing_constraint = constraint_store[ensemble_member].get(fk, None)
554
+
555
+ constraint_store[ensemble_member][fk] = self.__goal_hard_constraint(
556
+ goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
557
+ )
558
+
559
+ def __add_subproblem_objective_constraint(self):
560
+ # We want to keep the additional variables/parameters we set around
561
+ self.__problem_epsilons.extend(self.__subproblem_epsilons)
562
+ self.__problem_path_epsilons.extend(self.__subproblem_path_epsilons)
563
+ self.__problem_path_timeseries.extend(self.__subproblem_path_timeseries)
564
+ self.__problem_parameters.extend(self.__subproblem_parameters)
565
+
566
+ for ensemble_member in range(self.ensemble_size):
567
+ self.__problem_constraints[ensemble_member].extend(
568
+ self.__subproblem_soft_constraints[ensemble_member]
569
+ )
570
+ self.__problem_path_constraints[ensemble_member].extend(
571
+ self.__subproblem_path_soft_constraints[ensemble_member]
572
+ )
573
+
574
+ # Extract information about the objective value, this is used for the Pareto optimality
575
+ # constraint. We only retain information about the objective functions defined through the
576
+ # goal framework as user define objective functions may relay on local variables.
577
+ subproblem_objectives = self.__subproblem_objectives.copy()
578
+ subproblem_path_objectives = self.__subproblem_path_objectives.copy()
579
+
580
+ def _constraint_func(
581
+ problem,
582
+ subproblem_objectives=subproblem_objectives,
583
+ subproblem_path_objectives=subproblem_path_objectives,
584
+ ):
585
+ val = 0.0
586
+ for ensemble_member in range(problem.ensemble_size):
587
+ # NOTE: Users might be overriding objective() and/or path_objective(). Use the
588
+ # private methods that work only on the goals.
589
+ n_objectives = problem._gp_n_objectives(
590
+ subproblem_objectives, subproblem_path_objectives, ensemble_member
591
+ )
592
+ expr = problem._gp_objective(subproblem_objectives, n_objectives, ensemble_member)
593
+ expr += ca.sum1(
594
+ problem.map_path_expression(
595
+ problem._gp_path_objective(
596
+ subproblem_path_objectives, n_objectives, ensemble_member
597
+ ),
598
+ ensemble_member,
599
+ )
600
+ )
601
+ val += problem.ensemble_member_probability(ensemble_member) * expr
602
+
603
+ return val
604
+
605
+ f = ca.Function("tmp", [self.solver_input], [_constraint_func(self)])
606
+ obj_val = float(f(self.solver_output))
607
+
608
+ options = self.goal_programming_options()
609
+
610
+ if options["fix_minimized_values"]:
611
+ constraint = _GoalConstraint(None, _constraint_func, obj_val, obj_val, True)
612
+ self.check_collocation_linearity = False
613
+ self.linear_collocation = False
614
+ else:
615
+ obj_val += options["constraint_relaxation"]
616
+ constraint = _GoalConstraint(None, _constraint_func, -np.inf, obj_val, True)
617
+
618
+ # The goal works over all ensemble members, so we add it to the last
619
+ # one, as at that point the inputs of all previous ensemble members
620
+ # will have been discretized, mapped and stored.
621
+ self.__problem_constraints[-1].append(constraint)
622
+
623
+ def optimize(self, preprocessing=True, postprocessing=True, log_solver_failure_as_error=True):
624
+ # Do pre-processing
625
+ if preprocessing:
626
+ self.pre()
627
+
628
+ # Group goals into subproblems
629
+ subproblems = []
630
+ goals = self.goals()
631
+ path_goals = self.path_goals()
632
+
633
+ options = self.goal_programming_options()
634
+
635
+ # Validate (in)compatible options
636
+ if options["keep_soft_constraints"] and options["violation_relaxation"]:
637
+ raise Exception(
638
+ "The option 'violation_relaxation' cannot be used "
639
+ "when 'keep_soft_constraints' is set."
640
+ )
641
+
642
+ # Validate goal definitions
643
+ self._gp_validate_goals(goals, is_path_goal=False)
644
+ self._gp_validate_goals(path_goals, is_path_goal=True)
645
+
646
+ priorities = {
647
+ int(goal.priority) for goal in itertools.chain(goals, path_goals) if not goal.is_empty
648
+ }
649
+
650
+ for priority in sorted(priorities):
651
+ subproblems.append(
652
+ (
653
+ priority,
654
+ [
655
+ goal
656
+ for goal in goals
657
+ if int(goal.priority) == priority and not goal.is_empty
658
+ ],
659
+ [
660
+ goal
661
+ for goal in path_goals
662
+ if int(goal.priority) == priority and not goal.is_empty
663
+ ],
664
+ )
665
+ )
666
+
667
+ # Solve the subproblems one by one
668
+ logger.info("Starting goal programming")
669
+
670
+ success = False
671
+ self.skip_priority = False
672
+
673
+ self.__constraint_store = [OrderedDict() for ensemble_member in range(self.ensemble_size)]
674
+ self.__path_constraint_store = [
675
+ OrderedDict() for ensemble_member in range(self.ensemble_size)
676
+ ]
677
+
678
+ # Lists for when `keep_soft_constraints` is True
679
+ self.__problem_constraints = [[] for ensemble_member in range(self.ensemble_size)]
680
+ self.__problem_epsilons = []
681
+ self.__problem_parameters = []
682
+ self.__problem_path_constraints = [[] for ensemble_member in range(self.ensemble_size)]
683
+ self.__problem_path_epsilons = []
684
+ self.__problem_path_timeseries = []
685
+
686
+ self._gp_first_run = True
687
+ self.__results_are_current = False
688
+ self.__original_constant_input_keys = {}
689
+ self.__original_parameter_keys = {}
690
+ for i, (priority, goals, path_goals) in enumerate(subproblems):
691
+ logger.info("Solving goals at priority {}".format(priority))
692
+
693
+ # Call the pre priority hook
694
+ self.priority_started(priority)
695
+
696
+ if self.skip_priority:
697
+ logger.info(
698
+ "priority {} was removed in priority_started. No optimization problem "
699
+ "is solved at this priority.".format(priority)
700
+ )
701
+ continue
702
+
703
+ (
704
+ self.__subproblem_epsilons,
705
+ self.__subproblem_objectives,
706
+ self.__subproblem_soft_constraints,
707
+ hard_constraints,
708
+ self.__subproblem_parameters,
709
+ ) = self._gp_goal_constraints(goals, i, options, is_path_goal=False)
710
+
711
+ (
712
+ self.__subproblem_path_epsilons,
713
+ self.__subproblem_path_objectives,
714
+ self.__subproblem_path_soft_constraints,
715
+ path_hard_constraints,
716
+ self.__subproblem_path_timeseries,
717
+ ) = self._gp_goal_constraints(path_goals, i, options, is_path_goal=True)
718
+
719
+ # Put hard constraints in the constraint stores
720
+ self._gp_update_constraint_store(self.__constraint_store, hard_constraints)
721
+ self._gp_update_constraint_store(self.__path_constraint_store, path_hard_constraints)
722
+
723
+ # Solve subproblem
724
+ success = super().optimize(
725
+ preprocessing=False,
726
+ postprocessing=False,
727
+ log_solver_failure_as_error=log_solver_failure_as_error,
728
+ )
729
+ if not success:
730
+ break
731
+
732
+ self._gp_first_run = False
733
+
734
+ # Store results. Do this here, to make sure we have results even
735
+ # if a subsequent priority fails.
736
+ self.__results_are_current = False
737
+ self.__results = [
738
+ self.extract_results(ensemble_member)
739
+ for ensemble_member in range(self.ensemble_size)
740
+ ]
741
+ self.__results_are_current = True
742
+
743
+ # Call the post priority hook, so that intermediate results can be
744
+ # logged/inspected.
745
+ self.priority_completed(priority)
746
+
747
+ if options["keep_soft_constraints"]:
748
+ self.__add_subproblem_objective_constraint()
749
+ else:
750
+ self.__soft_to_hard_constraints(goals, i, is_path_goal=False)
751
+ self.__soft_to_hard_constraints(path_goals, i, is_path_goal=True)
752
+
753
+ logger.info("Done goal programming")
754
+
755
+ # Do post-processing
756
+ if postprocessing:
757
+ self.post()
758
+
759
+ # Done
760
+ return success
761
+
762
+ def extract_results(self, ensemble_member=0):
763
+ if self.__results_are_current:
764
+ logger.debug("Returning cached results")
765
+ return self.__results[ensemble_member]
766
+
767
+ # If self.__results is not up to date, do the super().extract_results
768
+ # method
769
+ return super().extract_results(ensemble_member)