rtc-tools 2.5.2rc4__py3-none-any.whl → 2.6.0__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.
Potentially problematic release.
This version of rtc-tools might be problematic. Click here for more details.
- {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/METADATA +7 -7
- rtc_tools-2.6.0.dist-info/RECORD +50 -0
- {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/WHEEL +1 -1
- rtctools/__init__.py +2 -1
- rtctools/_internal/alias_tools.py +12 -10
- rtctools/_internal/caching.py +5 -3
- rtctools/_internal/casadi_helpers.py +11 -32
- rtctools/_internal/debug_check_helpers.py +1 -1
- rtctools/_version.py +3 -3
- rtctools/data/__init__.py +2 -2
- rtctools/data/csv.py +54 -33
- rtctools/data/interpolation/bspline.py +3 -3
- rtctools/data/interpolation/bspline1d.py +42 -29
- rtctools/data/interpolation/bspline2d.py +10 -4
- rtctools/data/netcdf.py +137 -93
- rtctools/data/pi.py +304 -210
- rtctools/data/rtc.py +64 -53
- rtctools/data/storage.py +91 -51
- rtctools/optimization/collocated_integrated_optimization_problem.py +1244 -696
- rtctools/optimization/control_tree_mixin.py +68 -66
- rtctools/optimization/csv_lookup_table_mixin.py +107 -74
- rtctools/optimization/csv_mixin.py +83 -52
- rtctools/optimization/goal_programming_mixin.py +237 -146
- rtctools/optimization/goal_programming_mixin_base.py +204 -111
- rtctools/optimization/homotopy_mixin.py +36 -27
- rtctools/optimization/initial_state_estimation_mixin.py +8 -8
- rtctools/optimization/io_mixin.py +48 -43
- rtctools/optimization/linearization_mixin.py +3 -1
- rtctools/optimization/linearized_order_goal_programming_mixin.py +57 -28
- rtctools/optimization/min_abs_goal_programming_mixin.py +72 -29
- rtctools/optimization/modelica_mixin.py +135 -81
- rtctools/optimization/netcdf_mixin.py +32 -18
- rtctools/optimization/optimization_problem.py +181 -127
- rtctools/optimization/pi_mixin.py +68 -36
- rtctools/optimization/planning_mixin.py +19 -0
- rtctools/optimization/single_pass_goal_programming_mixin.py +159 -112
- rtctools/optimization/timeseries.py +4 -6
- rtctools/rtctoolsapp.py +18 -18
- rtctools/simulation/csv_mixin.py +37 -30
- rtctools/simulation/io_mixin.py +9 -5
- rtctools/simulation/pi_mixin.py +62 -32
- rtctools/simulation/simulation_problem.py +471 -180
- rtctools/util.py +84 -56
- rtc_tools-2.5.2rc4.dist-info/RECORD +0 -49
- {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/COPYING.LESSER +0 -0
- {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/entry_points.txt +0 -0
- {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/top_level.txt +0 -0
|
@@ -4,13 +4,18 @@ from collections import OrderedDict
|
|
|
4
4
|
from typing import Dict, Union
|
|
5
5
|
|
|
6
6
|
import casadi as ca
|
|
7
|
-
|
|
8
7
|
import numpy as np
|
|
9
8
|
|
|
10
9
|
from rtctools._internal.alias_tools import AliasDict
|
|
11
10
|
|
|
12
|
-
from .goal_programming_mixin_base import
|
|
13
|
-
|
|
11
|
+
from .goal_programming_mixin_base import ( # noqa: F401
|
|
12
|
+
Goal,
|
|
13
|
+
StateGoal,
|
|
14
|
+
_EmptyEnsembleList,
|
|
15
|
+
_EmptyEnsembleOrderedDict,
|
|
16
|
+
_GoalConstraint,
|
|
17
|
+
_GoalProgrammingMixinBase,
|
|
18
|
+
)
|
|
14
19
|
from .timeseries import Timeseries
|
|
15
20
|
|
|
16
21
|
logger = logging.getLogger("rtctools")
|
|
@@ -62,8 +67,12 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
62
67
|
|
|
63
68
|
def bounds(self):
|
|
64
69
|
bounds = super().bounds()
|
|
65
|
-
for epsilon in (
|
|
66
|
-
|
|
70
|
+
for epsilon in (
|
|
71
|
+
self.__subproblem_epsilons
|
|
72
|
+
+ self.__subproblem_path_epsilons
|
|
73
|
+
+ self.__problem_epsilons
|
|
74
|
+
+ self.__problem_path_epsilons
|
|
75
|
+
):
|
|
67
76
|
bounds[epsilon.name()] = (0.0, 1.0)
|
|
68
77
|
return bounds
|
|
69
78
|
|
|
@@ -82,7 +91,7 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
82
91
|
|
|
83
92
|
# Append min/max timeseries to the constant inputs. Note that min/max
|
|
84
93
|
# timeseries are shared between all ensemble members.
|
|
85
|
-
for
|
|
94
|
+
for variable, value in self.__subproblem_path_timeseries + self.__problem_path_timeseries:
|
|
86
95
|
if isinstance(value, np.ndarray):
|
|
87
96
|
value = Timeseries(self.times(), np.broadcast_to(value, (n_times, len(value))))
|
|
88
97
|
elif not isinstance(value, Timeseries):
|
|
@@ -104,7 +113,7 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
104
113
|
|
|
105
114
|
# Append min/max values to the parameters. Note that min/max values
|
|
106
115
|
# are shared between all ensemble members.
|
|
107
|
-
for
|
|
116
|
+
for variable, value in self.__subproblem_parameters + self.__problem_parameters:
|
|
108
117
|
parameters[variable] = value
|
|
109
118
|
return parameters
|
|
110
119
|
|
|
@@ -116,13 +125,15 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
116
125
|
seed = AliasDict(self.alias_relation)
|
|
117
126
|
for key, result in self.__results[ensemble_member].items():
|
|
118
127
|
times = self.times(key)
|
|
119
|
-
if (
|
|
120
|
-
|
|
128
|
+
if (result.ndim == 1 and len(result) == len(times)) or (
|
|
129
|
+
result.ndim == 2 and result.shape[0] == len(times)
|
|
130
|
+
):
|
|
121
131
|
# Only include seed timeseries which are consistent
|
|
122
132
|
# with the specified time stamps.
|
|
123
133
|
seed[key] = Timeseries(times, result)
|
|
124
|
-
elif (
|
|
125
|
-
|
|
134
|
+
elif (result.ndim == 1 and len(result) == 1) or (
|
|
135
|
+
result.ndim == 2 and result.shape[0] == 1
|
|
136
|
+
):
|
|
126
137
|
seed[key] = result
|
|
127
138
|
|
|
128
139
|
# Seed epsilons of current priority
|
|
@@ -144,14 +155,18 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
144
155
|
return seed
|
|
145
156
|
|
|
146
157
|
def objective(self, ensemble_member):
|
|
147
|
-
n_objectives = self._gp_n_objectives(
|
|
148
|
-
|
|
158
|
+
n_objectives = self._gp_n_objectives(
|
|
159
|
+
self.__subproblem_objectives, self.__subproblem_path_objectives, ensemble_member
|
|
160
|
+
)
|
|
149
161
|
return self._gp_objective(self.__subproblem_objectives, n_objectives, ensemble_member)
|
|
150
162
|
|
|
151
163
|
def path_objective(self, ensemble_member):
|
|
152
|
-
n_objectives = self._gp_n_objectives(
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
)
|
|
155
170
|
|
|
156
171
|
def constraints(self, ensemble_member):
|
|
157
172
|
constraints = super().constraints(ensemble_member)
|
|
@@ -159,7 +174,8 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
159
174
|
additional_constraints = itertools.chain(
|
|
160
175
|
self.__constraint_store[ensemble_member].values(),
|
|
161
176
|
self.__problem_constraints[ensemble_member],
|
|
162
|
-
self.__subproblem_soft_constraints[ensemble_member]
|
|
177
|
+
self.__subproblem_soft_constraints[ensemble_member],
|
|
178
|
+
)
|
|
163
179
|
|
|
164
180
|
for constraint in additional_constraints:
|
|
165
181
|
constraints.append((constraint.function(self), constraint.min, constraint.max))
|
|
@@ -172,7 +188,8 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
172
188
|
additional_path_constraints = itertools.chain(
|
|
173
189
|
self.__path_constraint_store[ensemble_member].values(),
|
|
174
190
|
self.__problem_path_constraints[ensemble_member],
|
|
175
|
-
self.__subproblem_path_soft_constraints[ensemble_member]
|
|
191
|
+
self.__subproblem_path_soft_constraints[ensemble_member],
|
|
192
|
+
)
|
|
176
193
|
|
|
177
194
|
for constraint in additional_path_constraints:
|
|
178
195
|
path_constraints.append((constraint.function(self), constraint.min, constraint.max))
|
|
@@ -183,26 +200,25 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
183
200
|
# Call parent
|
|
184
201
|
options = super().solver_options()
|
|
185
202
|
|
|
186
|
-
solver = options[
|
|
187
|
-
assert solver in [
|
|
203
|
+
solver = options["solver"]
|
|
204
|
+
assert solver in ["bonmin", "ipopt"]
|
|
188
205
|
|
|
189
206
|
# Make sure constant states, such as min/max timeseries for violation variables,
|
|
190
207
|
# are turned into parameters for the final optimization problem.
|
|
191
208
|
ipopt_options = options[solver]
|
|
192
|
-
ipopt_options[
|
|
209
|
+
ipopt_options["fixed_variable_treatment"] = "make_parameter"
|
|
193
210
|
|
|
194
211
|
# Define temporary variable to avoid infinite loop between
|
|
195
212
|
# solver_options and goal_programming_options.
|
|
196
213
|
self._loop_breaker_solver_options = True
|
|
197
214
|
|
|
198
|
-
if not hasattr(self,
|
|
199
|
-
if not self.goal_programming_options()[
|
|
200
|
-
ipopt_options[
|
|
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"
|
|
201
218
|
if not self._gp_first_run:
|
|
202
|
-
ipopt_options[
|
|
203
|
-
'mu'][-1]
|
|
219
|
+
ipopt_options["mu_init"] = self.solver_stats["iterations"]["mu"][-1]
|
|
204
220
|
|
|
205
|
-
delattr(self,
|
|
221
|
+
delattr(self, "_loop_breaker_solver_options")
|
|
206
222
|
|
|
207
223
|
return options
|
|
208
224
|
|
|
@@ -243,9 +259,9 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
243
259
|
1. Minimization goals do not get ``constraint_relaxation`` applied when
|
|
244
260
|
``fix_minimized_values`` is True.
|
|
245
261
|
|
|
246
|
-
2. Because of the constraints it generates, when ``keep_soft_constraints`` is True, the
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
249
265
|
|
|
250
266
|
A goal is considered to be violated if the violation, scaled between 0 and 1, is greater
|
|
251
267
|
than the specified tolerance. Violated goals are fixed. Use of this option is normally not
|
|
@@ -259,67 +275,74 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
259
275
|
If ``fix_minimized_values`` is set to ``True``, goal functions will be set to equal their
|
|
260
276
|
optimized values in optimization problems generated during subsequent priorities. Otherwise,
|
|
261
277
|
only an upper bound will be set. Use of this option is normally not required.
|
|
262
|
-
Note that a non-zero goal relaxation overrules this option; a non-zero relaxation will
|
|
263
|
-
result in only an upper bound being set.
|
|
264
|
-
Also note that the use of this option may add non-convex constraints to the optimization
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
value
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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).
|
|
293
314
|
|
|
294
315
|
:returns: A dictionary of goal programming options.
|
|
295
316
|
"""
|
|
296
317
|
|
|
297
318
|
options = {}
|
|
298
319
|
|
|
299
|
-
options[
|
|
300
|
-
options[
|
|
301
|
-
options[
|
|
302
|
-
options[
|
|
303
|
-
options[
|
|
304
|
-
options[
|
|
305
|
-
options[
|
|
306
|
-
options[
|
|
307
|
-
options[
|
|
308
|
-
options[
|
|
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
|
|
309
330
|
|
|
310
331
|
# Define temporary variable to avoid infinite loop between
|
|
311
332
|
# solver_options and goal_programming_options.
|
|
312
333
|
self._loop_breaker_goal_programming_options = True
|
|
313
334
|
|
|
314
|
-
if not hasattr(self,
|
|
315
|
-
if self.solver_options()[
|
|
316
|
-
options[
|
|
335
|
+
if not hasattr(self, "_loop_breaker_solver_options"):
|
|
336
|
+
if self.solver_options()["solver"] in {"ipopt", "bonmin"}:
|
|
337
|
+
options["fix_minimized_values"] = True
|
|
317
338
|
|
|
318
|
-
delattr(self,
|
|
339
|
+
delattr(self, "_loop_breaker_goal_programming_options")
|
|
319
340
|
|
|
320
341
|
return options
|
|
321
342
|
|
|
322
|
-
def __goal_hard_constraint(
|
|
343
|
+
def __goal_hard_constraint(
|
|
344
|
+
self, goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
|
|
345
|
+
):
|
|
323
346
|
if not is_path_goal:
|
|
324
347
|
epsilon = epsilon[:1]
|
|
325
348
|
|
|
@@ -328,21 +351,29 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
328
351
|
if goal.has_target_bounds:
|
|
329
352
|
# We use a violation variable formulation, with the violation
|
|
330
353
|
# variables epsilon bounded between 0 and 1.
|
|
331
|
-
m, M = np.full_like(epsilon, -np.inf, dtype=np.float64), np.full_like(
|
|
354
|
+
m, M = np.full_like(epsilon, -np.inf, dtype=np.float64), np.full_like(
|
|
355
|
+
epsilon, np.inf, dtype=np.float64
|
|
356
|
+
)
|
|
332
357
|
|
|
333
358
|
# A function range does not have to be specified for critical
|
|
334
359
|
# goals. Avoid multiplying with NaN in that case.
|
|
335
360
|
if goal.has_target_min:
|
|
336
|
-
m = (
|
|
337
|
-
|
|
361
|
+
m = (
|
|
362
|
+
epsilon * ((goal.function_range[0] - goal_m) if not goal.critical else 0.0)
|
|
363
|
+
+ goal_m
|
|
364
|
+
- goal.relaxation
|
|
365
|
+
) / goal.function_nominal
|
|
338
366
|
if goal.has_target_max:
|
|
339
|
-
M = (
|
|
340
|
-
|
|
367
|
+
M = (
|
|
368
|
+
epsilon * ((goal.function_range[1] - goal_M) if not goal.critical else 0.0)
|
|
369
|
+
+ goal_M
|
|
370
|
+
+ goal.relaxation
|
|
371
|
+
) / goal.function_nominal
|
|
341
372
|
|
|
342
373
|
if goal.has_target_min and goal.has_target_max:
|
|
343
374
|
# Avoid comparing with NaN
|
|
344
375
|
inds = ~(np.isnan(m) | np.isnan(M))
|
|
345
|
-
inds[inds] &= np.abs(m[inds] - M[inds]) < options[
|
|
376
|
+
inds[inds] &= np.abs(m[inds] - M[inds]) < options["equality_threshold"]
|
|
346
377
|
if np.any(inds):
|
|
347
378
|
avg = 0.5 * (m + M)
|
|
348
379
|
m[inds] = M[inds] = avg[inds]
|
|
@@ -350,31 +381,35 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
350
381
|
m[~np.isfinite(goal_m)] = -np.inf
|
|
351
382
|
M[~np.isfinite(goal_M)] = np.inf
|
|
352
383
|
|
|
353
|
-
inds = epsilon > options[
|
|
384
|
+
inds = epsilon > options["violation_tolerance"]
|
|
354
385
|
if np.any(inds):
|
|
355
386
|
if is_path_goal:
|
|
356
|
-
expr = self.map_path_expression(
|
|
387
|
+
expr = self.map_path_expression(
|
|
388
|
+
goal.function(self, ensemble_member), ensemble_member
|
|
389
|
+
)
|
|
357
390
|
else:
|
|
358
391
|
expr = goal.function(self, ensemble_member)
|
|
359
392
|
|
|
360
|
-
function = ca.Function(
|
|
393
|
+
function = ca.Function("f", [self.solver_input], [expr])
|
|
361
394
|
value = np.array(function(self.solver_output))
|
|
362
395
|
|
|
363
396
|
m[inds] = (value - goal.relaxation) / goal.function_nominal
|
|
364
397
|
M[inds] = (value + goal.relaxation) / goal.function_nominal
|
|
365
398
|
|
|
366
|
-
m -= options[
|
|
367
|
-
M += options[
|
|
399
|
+
m -= options["constraint_relaxation"]
|
|
400
|
+
M += options["constraint_relaxation"]
|
|
368
401
|
else:
|
|
369
402
|
# Epsilon encodes the position within the function range.
|
|
370
|
-
if options[
|
|
403
|
+
if options["fix_minimized_values"] and goal.relaxation == 0.0:
|
|
371
404
|
m = epsilon / goal.function_nominal
|
|
372
405
|
M = epsilon / goal.function_nominal
|
|
373
406
|
self.check_collocation_linearity = False
|
|
374
407
|
self.linear_collocation = False
|
|
375
408
|
else:
|
|
376
409
|
m = -np.inf * np.ones(epsilon.shape)
|
|
377
|
-
M = (epsilon + goal.relaxation) / goal.function_nominal + options[
|
|
410
|
+
M = (epsilon + goal.relaxation) / goal.function_nominal + options[
|
|
411
|
+
"constraint_relaxation"
|
|
412
|
+
]
|
|
378
413
|
|
|
379
414
|
if is_path_goal:
|
|
380
415
|
m = Timeseries(self.times(), m)
|
|
@@ -386,13 +421,17 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
386
421
|
constraint = _GoalConstraint(
|
|
387
422
|
goal,
|
|
388
423
|
lambda problem, ensemble_member=ensemble_member, goal=goal: (
|
|
389
|
-
goal.function(problem, ensemble_member) / goal.function_nominal
|
|
390
|
-
|
|
424
|
+
goal.function(problem, ensemble_member) / goal.function_nominal
|
|
425
|
+
),
|
|
426
|
+
m,
|
|
427
|
+
M,
|
|
428
|
+
True,
|
|
429
|
+
)
|
|
391
430
|
|
|
392
431
|
# Epsilon is fixed. Override previous {min,max} constraints for this
|
|
393
432
|
# state.
|
|
394
433
|
if existing_constraint:
|
|
395
|
-
constraint.update_bounds(existing_constraint, enforce=
|
|
434
|
+
constraint.update_bounds(existing_constraint, enforce="other")
|
|
396
435
|
|
|
397
436
|
return constraint
|
|
398
437
|
|
|
@@ -419,9 +458,9 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
419
458
|
|
|
420
459
|
for j, goal in enumerate(goals):
|
|
421
460
|
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
461
|
+
not goal.has_target_bounds
|
|
462
|
+
or goal.violation_timeseries_id is not None
|
|
463
|
+
or goal.function_value_timeseries_id is not None
|
|
425
464
|
):
|
|
426
465
|
goal_functions[j] = goal.function(self, ensemble_member)
|
|
427
466
|
|
|
@@ -432,11 +471,10 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
432
471
|
else:
|
|
433
472
|
expr = ca.transpose(ca.vertcat(*goal_functions.values()))
|
|
434
473
|
|
|
435
|
-
f = ca.Function(
|
|
474
|
+
f = ca.Function("f", [self.solver_input], [expr])
|
|
436
475
|
raw_function_values = np.array(f(self.solver_output))
|
|
437
476
|
goal_function_values[ensemble_member] = {
|
|
438
|
-
k: raw_function_values[:, j].ravel()
|
|
439
|
-
for j, k in enumerate(goal_functions.keys())
|
|
477
|
+
k: raw_function_values[:, j].ravel() for j, k in enumerate(goal_functions.keys())
|
|
440
478
|
}
|
|
441
479
|
|
|
442
480
|
# Re-add constraints, this time with epsilon values fixed
|
|
@@ -450,15 +488,14 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
450
488
|
self.set_timeseries(
|
|
451
489
|
goal.function_value_timeseries_id,
|
|
452
490
|
Timeseries(times, function_value),
|
|
453
|
-
ensemble_member
|
|
491
|
+
ensemble_member,
|
|
454
492
|
)
|
|
455
493
|
|
|
456
494
|
if goal.critical:
|
|
457
495
|
continue
|
|
458
496
|
|
|
459
497
|
if goal.has_target_bounds:
|
|
460
|
-
epsilon = self.__results[ensemble_member][
|
|
461
|
-
eps_format.format(sym_index, j)]
|
|
498
|
+
epsilon = self.__results[ensemble_member][eps_format.format(sym_index, j)]
|
|
462
499
|
|
|
463
500
|
# Store results
|
|
464
501
|
if goal.violation_timeseries_id is not None:
|
|
@@ -466,34 +503,48 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
466
503
|
epsilon_active = np.copy(epsilon)
|
|
467
504
|
m = goal.target_min
|
|
468
505
|
if isinstance(m, Timeseries):
|
|
469
|
-
m = self.interpolate(
|
|
506
|
+
m = self.interpolate(
|
|
507
|
+
times, goal.target_min.times, goal.target_min.values
|
|
508
|
+
)
|
|
470
509
|
M = goal.target_max
|
|
471
510
|
if isinstance(M, Timeseries):
|
|
472
|
-
M = self.interpolate(
|
|
511
|
+
M = self.interpolate(
|
|
512
|
+
times, goal.target_max.times, goal.target_max.values
|
|
513
|
+
)
|
|
473
514
|
w = np.ones_like(function_value)
|
|
474
515
|
if goal.has_target_min:
|
|
475
516
|
# Avoid comparing with NaN while making sure that
|
|
476
517
|
# w[i] is True when m[i] is not finite.
|
|
477
518
|
m = np.array(m)
|
|
478
519
|
m[~np.isfinite(m)] = -np.inf
|
|
479
|
-
w = np.logical_and(
|
|
480
|
-
|
|
520
|
+
w = np.logical_and(
|
|
521
|
+
w,
|
|
522
|
+
(
|
|
523
|
+
function_value / goal.function_nominal
|
|
524
|
+
> m / goal.function_nominal + options["interior_distance"]
|
|
525
|
+
),
|
|
526
|
+
)
|
|
481
527
|
if goal.has_target_max:
|
|
482
528
|
# Avoid comparing with NaN while making sure that
|
|
483
529
|
# w[i] is True when M[i] is not finite.
|
|
484
530
|
M = np.array(M)
|
|
485
531
|
M[~np.isfinite(M)] = np.inf
|
|
486
|
-
w = np.logical_and(
|
|
487
|
-
|
|
532
|
+
w = np.logical_and(
|
|
533
|
+
w,
|
|
534
|
+
(
|
|
535
|
+
function_value / goal.function_nominal
|
|
536
|
+
< M / goal.function_nominal + options["interior_distance"]
|
|
537
|
+
),
|
|
538
|
+
)
|
|
488
539
|
epsilon_active[w] = np.nan
|
|
489
540
|
self.set_timeseries(
|
|
490
541
|
goal.violation_timeseries_id,
|
|
491
542
|
Timeseries(times, epsilon_active),
|
|
492
|
-
ensemble_member
|
|
543
|
+
ensemble_member,
|
|
493
544
|
)
|
|
494
545
|
|
|
495
546
|
# Add a relaxation to appease the barrier method.
|
|
496
|
-
epsilon += options[
|
|
547
|
+
epsilon += options["violation_relaxation"]
|
|
497
548
|
else:
|
|
498
549
|
epsilon = function_value
|
|
499
550
|
|
|
@@ -501,7 +552,8 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
501
552
|
existing_constraint = constraint_store[ensemble_member].get(fk, None)
|
|
502
553
|
|
|
503
554
|
constraint_store[ensemble_member][fk] = self.__goal_hard_constraint(
|
|
504
|
-
goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
|
|
555
|
+
goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
|
|
556
|
+
)
|
|
505
557
|
|
|
506
558
|
def __add_subproblem_objective_constraint(self):
|
|
507
559
|
# We want to keep the additional variables/parameters we set around
|
|
@@ -512,44 +564,54 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
512
564
|
|
|
513
565
|
for ensemble_member in range(self.ensemble_size):
|
|
514
566
|
self.__problem_constraints[ensemble_member].extend(
|
|
515
|
-
self.__subproblem_soft_constraints[ensemble_member]
|
|
567
|
+
self.__subproblem_soft_constraints[ensemble_member]
|
|
568
|
+
)
|
|
516
569
|
self.__problem_path_constraints[ensemble_member].extend(
|
|
517
|
-
self.__subproblem_path_soft_constraints[ensemble_member]
|
|
570
|
+
self.__subproblem_path_soft_constraints[ensemble_member]
|
|
571
|
+
)
|
|
518
572
|
|
|
519
|
-
# Extract information about the objective value, this is used for the Pareto optimality
|
|
520
|
-
# We only retain information about the objective functions defined through the
|
|
521
|
-
# define objective functions may relay on local variables.
|
|
573
|
+
# Extract information about the objective value, this is used for the Pareto optimality
|
|
574
|
+
# constraint. We only retain information about the objective functions defined through the
|
|
575
|
+
# goal framework as user define objective functions may relay on local variables.
|
|
522
576
|
subproblem_objectives = self.__subproblem_objectives.copy()
|
|
523
577
|
subproblem_path_objectives = self.__subproblem_path_objectives.copy()
|
|
524
578
|
|
|
525
|
-
def _constraint_func(
|
|
526
|
-
|
|
527
|
-
|
|
579
|
+
def _constraint_func(
|
|
580
|
+
problem,
|
|
581
|
+
subproblem_objectives=subproblem_objectives,
|
|
582
|
+
subproblem_path_objectives=subproblem_path_objectives,
|
|
583
|
+
):
|
|
528
584
|
val = 0.0
|
|
529
585
|
for ensemble_member in range(problem.ensemble_size):
|
|
530
586
|
# NOTE: Users might be overriding objective() and/or path_objective(). Use the
|
|
531
587
|
# private methods that work only on the goals.
|
|
532
588
|
n_objectives = problem._gp_n_objectives(
|
|
533
|
-
subproblem_objectives, subproblem_path_objectives, ensemble_member
|
|
589
|
+
subproblem_objectives, subproblem_path_objectives, ensemble_member
|
|
590
|
+
)
|
|
534
591
|
expr = problem._gp_objective(subproblem_objectives, n_objectives, ensemble_member)
|
|
535
|
-
expr += ca.sum1(
|
|
536
|
-
problem.
|
|
537
|
-
|
|
592
|
+
expr += ca.sum1(
|
|
593
|
+
problem.map_path_expression(
|
|
594
|
+
problem._gp_path_objective(
|
|
595
|
+
subproblem_path_objectives, n_objectives, ensemble_member
|
|
596
|
+
),
|
|
597
|
+
ensemble_member,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
538
600
|
val += problem.ensemble_member_probability(ensemble_member) * expr
|
|
539
601
|
|
|
540
602
|
return val
|
|
541
603
|
|
|
542
|
-
f = ca.Function(
|
|
604
|
+
f = ca.Function("tmp", [self.solver_input], [_constraint_func(self)])
|
|
543
605
|
obj_val = float(f(self.solver_output))
|
|
544
606
|
|
|
545
607
|
options = self.goal_programming_options()
|
|
546
608
|
|
|
547
|
-
if options[
|
|
609
|
+
if options["fix_minimized_values"]:
|
|
548
610
|
constraint = _GoalConstraint(None, _constraint_func, obj_val, obj_val, True)
|
|
549
611
|
self.check_collocation_linearity = False
|
|
550
612
|
self.linear_collocation = False
|
|
551
613
|
else:
|
|
552
|
-
obj_val += options[
|
|
614
|
+
obj_val += options["constraint_relaxation"]
|
|
553
615
|
constraint = _GoalConstraint(None, _constraint_func, -np.inf, obj_val, True)
|
|
554
616
|
|
|
555
617
|
# The goal works over all ensemble members, so we add it to the last
|
|
@@ -570,20 +632,36 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
570
632
|
options = self.goal_programming_options()
|
|
571
633
|
|
|
572
634
|
# Validate (in)compatible options
|
|
573
|
-
if options[
|
|
574
|
-
raise Exception(
|
|
635
|
+
if options["keep_soft_constraints"] and options["violation_relaxation"]:
|
|
636
|
+
raise Exception(
|
|
637
|
+
"The option 'violation_relaxation' cannot be used "
|
|
638
|
+
"when 'keep_soft_constraints' is set."
|
|
639
|
+
)
|
|
575
640
|
|
|
576
641
|
# Validate goal definitions
|
|
577
642
|
self._gp_validate_goals(goals, is_path_goal=False)
|
|
578
643
|
self._gp_validate_goals(path_goals, is_path_goal=True)
|
|
579
644
|
|
|
580
|
-
priorities = {
|
|
645
|
+
priorities = {
|
|
646
|
+
int(goal.priority) for goal in itertools.chain(goals, path_goals) if not goal.is_empty
|
|
647
|
+
}
|
|
581
648
|
|
|
582
649
|
for priority in sorted(priorities):
|
|
583
|
-
subproblems.append(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
650
|
+
subproblems.append(
|
|
651
|
+
(
|
|
652
|
+
priority,
|
|
653
|
+
[
|
|
654
|
+
goal
|
|
655
|
+
for goal in goals
|
|
656
|
+
if int(goal.priority) == priority and not goal.is_empty
|
|
657
|
+
],
|
|
658
|
+
[
|
|
659
|
+
goal
|
|
660
|
+
for goal in path_goals
|
|
661
|
+
if int(goal.priority) == priority and not goal.is_empty
|
|
662
|
+
],
|
|
663
|
+
)
|
|
664
|
+
)
|
|
587
665
|
|
|
588
666
|
# Solve the subproblems one by one
|
|
589
667
|
logger.info("Starting goal programming")
|
|
@@ -591,7 +669,9 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
591
669
|
success = False
|
|
592
670
|
|
|
593
671
|
self.__constraint_store = [OrderedDict() for ensemble_member in range(self.ensemble_size)]
|
|
594
|
-
self.__path_constraint_store = [
|
|
672
|
+
self.__path_constraint_store = [
|
|
673
|
+
OrderedDict() for ensemble_member in range(self.ensemble_size)
|
|
674
|
+
]
|
|
595
675
|
|
|
596
676
|
# Lists for when `keep_soft_constraints` is True
|
|
597
677
|
self.__problem_constraints = [[] for ensemble_member in range(self.ensemble_size)]
|
|
@@ -611,15 +691,21 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
611
691
|
# Call the pre priority hook
|
|
612
692
|
self.priority_started(priority)
|
|
613
693
|
|
|
614
|
-
(
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
self.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
694
|
+
(
|
|
695
|
+
self.__subproblem_epsilons,
|
|
696
|
+
self.__subproblem_objectives,
|
|
697
|
+
self.__subproblem_soft_constraints,
|
|
698
|
+
hard_constraints,
|
|
699
|
+
self.__subproblem_parameters,
|
|
700
|
+
) = self._gp_goal_constraints(goals, i, options, is_path_goal=False)
|
|
701
|
+
|
|
702
|
+
(
|
|
703
|
+
self.__subproblem_path_epsilons,
|
|
704
|
+
self.__subproblem_path_objectives,
|
|
705
|
+
self.__subproblem_path_soft_constraints,
|
|
706
|
+
path_hard_constraints,
|
|
707
|
+
self.__subproblem_path_timeseries,
|
|
708
|
+
) = self._gp_goal_constraints(path_goals, i, options, is_path_goal=True)
|
|
623
709
|
|
|
624
710
|
# Put hard constraints in the constraint stores
|
|
625
711
|
self._gp_update_constraint_store(self.__constraint_store, hard_constraints)
|
|
@@ -627,7 +713,10 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
627
713
|
|
|
628
714
|
# Solve subproblem
|
|
629
715
|
success = super().optimize(
|
|
630
|
-
preprocessing=False,
|
|
716
|
+
preprocessing=False,
|
|
717
|
+
postprocessing=False,
|
|
718
|
+
log_solver_failure_as_error=log_solver_failure_as_error,
|
|
719
|
+
)
|
|
631
720
|
if not success:
|
|
632
721
|
break
|
|
633
722
|
|
|
@@ -636,15 +725,17 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
636
725
|
# Store results. Do this here, to make sure we have results even
|
|
637
726
|
# if a subsequent priority fails.
|
|
638
727
|
self.__results_are_current = False
|
|
639
|
-
self.__results = [
|
|
640
|
-
|
|
728
|
+
self.__results = [
|
|
729
|
+
self.extract_results(ensemble_member)
|
|
730
|
+
for ensemble_member in range(self.ensemble_size)
|
|
731
|
+
]
|
|
641
732
|
self.__results_are_current = True
|
|
642
733
|
|
|
643
734
|
# Call the post priority hook, so that intermediate results can be
|
|
644
735
|
# logged/inspected.
|
|
645
736
|
self.priority_completed(priority)
|
|
646
737
|
|
|
647
|
-
if options[
|
|
738
|
+
if options["keep_soft_constraints"]:
|
|
648
739
|
self.__add_subproblem_objective_constraint()
|
|
649
740
|
else:
|
|
650
741
|
self.__soft_to_hard_constraints(goals, i, is_path_goal=False)
|