rtc-tools 2.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rtc_tools-2.7.3.dist-info/METADATA +53 -0
- rtc_tools-2.7.3.dist-info/RECORD +50 -0
- rtc_tools-2.7.3.dist-info/WHEEL +5 -0
- rtc_tools-2.7.3.dist-info/entry_points.txt +3 -0
- rtc_tools-2.7.3.dist-info/licenses/COPYING.LESSER +165 -0
- rtc_tools-2.7.3.dist-info/top_level.txt +1 -0
- rtctools/__init__.py +5 -0
- rtctools/_internal/__init__.py +0 -0
- rtctools/_internal/alias_tools.py +188 -0
- rtctools/_internal/caching.py +25 -0
- rtctools/_internal/casadi_helpers.py +99 -0
- rtctools/_internal/debug_check_helpers.py +41 -0
- rtctools/_version.py +21 -0
- rtctools/data/__init__.py +4 -0
- rtctools/data/csv.py +150 -0
- rtctools/data/interpolation/__init__.py +3 -0
- rtctools/data/interpolation/bspline.py +31 -0
- rtctools/data/interpolation/bspline1d.py +169 -0
- rtctools/data/interpolation/bspline2d.py +54 -0
- rtctools/data/netcdf.py +467 -0
- rtctools/data/pi.py +1236 -0
- rtctools/data/rtc.py +228 -0
- rtctools/data/storage.py +343 -0
- rtctools/optimization/__init__.py +0 -0
- rtctools/optimization/collocated_integrated_optimization_problem.py +3208 -0
- rtctools/optimization/control_tree_mixin.py +221 -0
- rtctools/optimization/csv_lookup_table_mixin.py +462 -0
- rtctools/optimization/csv_mixin.py +300 -0
- rtctools/optimization/goal_programming_mixin.py +769 -0
- rtctools/optimization/goal_programming_mixin_base.py +1094 -0
- rtctools/optimization/homotopy_mixin.py +165 -0
- rtctools/optimization/initial_state_estimation_mixin.py +89 -0
- rtctools/optimization/io_mixin.py +320 -0
- rtctools/optimization/linearization_mixin.py +33 -0
- rtctools/optimization/linearized_order_goal_programming_mixin.py +235 -0
- rtctools/optimization/min_abs_goal_programming_mixin.py +385 -0
- rtctools/optimization/modelica_mixin.py +482 -0
- rtctools/optimization/netcdf_mixin.py +177 -0
- rtctools/optimization/optimization_problem.py +1302 -0
- rtctools/optimization/pi_mixin.py +292 -0
- rtctools/optimization/planning_mixin.py +19 -0
- rtctools/optimization/single_pass_goal_programming_mixin.py +676 -0
- rtctools/optimization/timeseries.py +56 -0
- rtctools/rtctoolsapp.py +131 -0
- rtctools/simulation/__init__.py +0 -0
- rtctools/simulation/csv_mixin.py +171 -0
- rtctools/simulation/io_mixin.py +195 -0
- rtctools/simulation/pi_mixin.py +255 -0
- rtctools/simulation/simulation_problem.py +1293 -0
- rtctools/util.py +241 -0
|
@@ -0,0 +1,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)
|