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,235 @@
|
|
|
1
|
+
import casadi as ca
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from rtctools.optimization.goal_programming_mixin_base import (
|
|
5
|
+
Goal,
|
|
6
|
+
StateGoal,
|
|
7
|
+
_GoalConstraint,
|
|
8
|
+
_GoalProgrammingMixinBase,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LinearizedOrderGoal(Goal):
|
|
13
|
+
#: Override linearization of goal order. Related global goal programming
|
|
14
|
+
#: option is ``linearize_goal_order``
|
|
15
|
+
#: (see :py:meth:`LinearizedOrderGoalProgrammingMixin.goal_programming_options`).
|
|
16
|
+
#: The default value of None defers to the global option, but the user can
|
|
17
|
+
#: explicitly override it per goal by setting this value to True or False.
|
|
18
|
+
linearize_order = None
|
|
19
|
+
|
|
20
|
+
#: Coefficients to linearize a goal's order
|
|
21
|
+
_linear_coefficients = {}
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _get_linear_coefficients(cls, order, eps=0.1, kind="balanced"):
|
|
25
|
+
assert order > 1, "Order should be strictly larger than one"
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
return cls._linear_coefficients[eps][order]
|
|
29
|
+
except KeyError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
x = ca.SX.sym("x")
|
|
33
|
+
a = ca.SX.sym("a")
|
|
34
|
+
b = ca.SX.sym("b")
|
|
35
|
+
|
|
36
|
+
# Strike a balance between "absolute error < eps" and "relative error < eps" by
|
|
37
|
+
# multiplying eps with x**(order-1)
|
|
38
|
+
if kind == "balanced":
|
|
39
|
+
f = x**order - eps * x ** (order - 1) - (a * x + b)
|
|
40
|
+
elif kind == "abs":
|
|
41
|
+
f = x**order - eps - (a * x + b)
|
|
42
|
+
else:
|
|
43
|
+
raise Exception("Unknown error approximation strategy '{}'".format(kind))
|
|
44
|
+
|
|
45
|
+
res_vals = ca.Function("res_vals", [x, ca.vertcat(a, b)], [f])
|
|
46
|
+
|
|
47
|
+
do_step = ca.rootfinder("next_state", "fast_newton", res_vals)
|
|
48
|
+
|
|
49
|
+
x = 0.0
|
|
50
|
+
a = 0.0
|
|
51
|
+
b = 0.0
|
|
52
|
+
|
|
53
|
+
xs = [0.0]
|
|
54
|
+
while x < 1.0:
|
|
55
|
+
# Initial guess larger than 1.0 to always have the next point be
|
|
56
|
+
# on the right (i.e. not left) side.
|
|
57
|
+
x = float(do_step(2.0, [a, b]))
|
|
58
|
+
a = order * x ** (order - 1)
|
|
59
|
+
b = x**order - a * x
|
|
60
|
+
xs.append(x)
|
|
61
|
+
|
|
62
|
+
# Turn underestimate into an overestimate, such that we get rid of
|
|
63
|
+
# horizontal line at origin.
|
|
64
|
+
xs[-1] = 1.0
|
|
65
|
+
xs = np.array(xs)
|
|
66
|
+
ys = xs**order
|
|
67
|
+
|
|
68
|
+
a = (ys[1:] - ys[:-1]) / (xs[1:] - xs[:-1])
|
|
69
|
+
b = ys[1:] - a * xs[1:]
|
|
70
|
+
lines = list(zip(a, b))
|
|
71
|
+
|
|
72
|
+
cls._linear_coefficients.setdefault(eps, {})[order] = lines
|
|
73
|
+
|
|
74
|
+
return lines
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class LinearizedOrderStateGoal(LinearizedOrderGoal, StateGoal):
|
|
78
|
+
"""
|
|
79
|
+
Convenience class definition for linearized order state goals. Note that
|
|
80
|
+
it is possible to just inherit from :py:class:`.LinearizedOrderGoal` to get the needed
|
|
81
|
+
functionality for control of the linearization at goal level.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LinearizedOrderGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
88
|
+
"""
|
|
89
|
+
Adds support for linearization of the goal objective functions, i.e. the
|
|
90
|
+
violation variables to a certain power. This can be used to keep a problem
|
|
91
|
+
fully linear and/or make sure that no quadratic constraints appear when using
|
|
92
|
+
the goal programming option ``keep_soft_constraints``.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def goal_programming_options(self):
|
|
96
|
+
"""
|
|
97
|
+
If ``linearize_goal_order`` is set to ``True``, the goal's order will be
|
|
98
|
+
approximated linearly for any goals where order > 1. Note that this option
|
|
99
|
+
does not work with minimization goals of higher order. Instead, it is
|
|
100
|
+
suggested to transform these minimization goals into goals with a target (and
|
|
101
|
+
function range) when using this option. Note that this option can be overriden
|
|
102
|
+
on the level of a goal by using a :py:class:`LinearizedOrderGoal` (see
|
|
103
|
+
:py:attr:`LinearizedOrderGoal.linearize_order`).
|
|
104
|
+
"""
|
|
105
|
+
options = super().goal_programming_options()
|
|
106
|
+
options["linearize_goal_order"] = True
|
|
107
|
+
return options
|
|
108
|
+
|
|
109
|
+
def _gp_validate_goals(self, goals, is_path_goal):
|
|
110
|
+
options = self.goal_programming_options()
|
|
111
|
+
|
|
112
|
+
for goal in goals:
|
|
113
|
+
goal_linearize = None
|
|
114
|
+
if isinstance(goal, LinearizedOrderGoal):
|
|
115
|
+
goal_linearize = goal.linearize_order
|
|
116
|
+
|
|
117
|
+
if goal_linearize or (options["linearize_goal_order"] and goal_linearize is not False):
|
|
118
|
+
if not goal.has_target_bounds and goal.order > 1:
|
|
119
|
+
raise Exception(
|
|
120
|
+
"Higher order minimization goals not allowed with "
|
|
121
|
+
"`linearize_goal_order` for goal {}".format(goal)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
super()._gp_validate_goals(goals, is_path_goal)
|
|
125
|
+
|
|
126
|
+
def _gp_goal_constraints(self, goals, sym_index, options, is_path_goal):
|
|
127
|
+
options = self.goal_programming_options()
|
|
128
|
+
|
|
129
|
+
def _linearize_goal(goal):
|
|
130
|
+
goal_linearize = None
|
|
131
|
+
if isinstance(goal, LinearizedOrderGoal):
|
|
132
|
+
goal_linearize = goal.linearize_order
|
|
133
|
+
|
|
134
|
+
if goal_linearize or (options["linearize_goal_order"] and goal_linearize is not False):
|
|
135
|
+
if goal.order > 1 and not goal.critical:
|
|
136
|
+
return True
|
|
137
|
+
else:
|
|
138
|
+
return False
|
|
139
|
+
else:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
lo_soft_constraints = [[] for ensemble_member in range(self.ensemble_size)]
|
|
143
|
+
lo_epsilons = []
|
|
144
|
+
|
|
145
|
+
# For the linearized goals, we use all of the normal processing,
|
|
146
|
+
# except for the objective. We can override the objective function by
|
|
147
|
+
# setting a _objective_func function on the Goal object.
|
|
148
|
+
for j, goal in enumerate(goals):
|
|
149
|
+
if not _linearize_goal(goal):
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
assert goal.has_target_bounds, "Cannot linearize minimization goals"
|
|
153
|
+
|
|
154
|
+
# Make a linear epsilon, and constraints relating the linear
|
|
155
|
+
# variable to the original objective function
|
|
156
|
+
path_prefix = "path_" if is_path_goal else ""
|
|
157
|
+
linear_variable = ca.MX.sym(
|
|
158
|
+
path_prefix + "lineps_{}_{}".format(sym_index, j), goal.size
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
lo_epsilons.append(linear_variable)
|
|
162
|
+
|
|
163
|
+
if isinstance(goal, LinearizedOrderGoal):
|
|
164
|
+
coeffs = goal._get_linear_coefficients(goal.order)
|
|
165
|
+
else:
|
|
166
|
+
coeffs = LinearizedOrderGoal._get_linear_coefficients(goal.order)
|
|
167
|
+
|
|
168
|
+
epsilon_name = path_prefix + "eps_{}_{}".format(sym_index, j)
|
|
169
|
+
|
|
170
|
+
for a, b in coeffs:
|
|
171
|
+
# We add to soft constraints, as these constraints are no longer valid when
|
|
172
|
+
# having `keep_soft_constraints` = False. This is because the `epsilon` and
|
|
173
|
+
# the `linear_variable` no longer exist in the next priority.
|
|
174
|
+
for ensemble_member in range(self.ensemble_size):
|
|
175
|
+
|
|
176
|
+
def _f(
|
|
177
|
+
problem,
|
|
178
|
+
goal=goal,
|
|
179
|
+
epsilon_name=epsilon_name,
|
|
180
|
+
linear_variable=linear_variable,
|
|
181
|
+
a=a,
|
|
182
|
+
b=b,
|
|
183
|
+
ensemble_member=ensemble_member,
|
|
184
|
+
is_path_constraint=is_path_goal,
|
|
185
|
+
):
|
|
186
|
+
if is_path_constraint:
|
|
187
|
+
eps = problem.variable(epsilon_name)
|
|
188
|
+
lin = problem.variable(linear_variable.name())
|
|
189
|
+
else:
|
|
190
|
+
eps = problem.extra_variable(epsilon_name, ensemble_member)
|
|
191
|
+
lin = problem.extra_variable(linear_variable.name(), ensemble_member)
|
|
192
|
+
|
|
193
|
+
return lin - a * eps - b
|
|
194
|
+
|
|
195
|
+
lo_soft_constraints[ensemble_member].append(
|
|
196
|
+
_GoalConstraint(goal, _f, 0.0, np.inf, False)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if is_path_goal and options["scale_by_problem_size"]:
|
|
200
|
+
goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape=len(self.times()))
|
|
201
|
+
goal_active = np.isfinite(goal_m) | np.isfinite(goal_M)
|
|
202
|
+
n_active = np.sum(goal_active.astype(int), axis=0)
|
|
203
|
+
else:
|
|
204
|
+
n_active = 1
|
|
205
|
+
|
|
206
|
+
def _objective_func(
|
|
207
|
+
problem,
|
|
208
|
+
ensemble_member,
|
|
209
|
+
goal=goal,
|
|
210
|
+
linear_variable=linear_variable,
|
|
211
|
+
is_path_goal=is_path_goal,
|
|
212
|
+
n_active=n_active,
|
|
213
|
+
):
|
|
214
|
+
if is_path_goal:
|
|
215
|
+
lin = problem.variable(linear_variable.name())
|
|
216
|
+
else:
|
|
217
|
+
lin = problem.extra_variable(linear_variable.name(), ensemble_member)
|
|
218
|
+
|
|
219
|
+
return goal.weight * lin / n_active
|
|
220
|
+
|
|
221
|
+
goal._objective_func = _objective_func
|
|
222
|
+
|
|
223
|
+
(
|
|
224
|
+
epsilons,
|
|
225
|
+
objectives,
|
|
226
|
+
soft_constraints,
|
|
227
|
+
hard_constraints,
|
|
228
|
+
extra_constants,
|
|
229
|
+
) = super()._gp_goal_constraints(goals, sym_index, options, is_path_goal)
|
|
230
|
+
|
|
231
|
+
epsilons = epsilons + lo_epsilons
|
|
232
|
+
for ensemble_member in range(self.ensemble_size):
|
|
233
|
+
soft_constraints[ensemble_member].extend(lo_soft_constraints[ensemble_member])
|
|
234
|
+
|
|
235
|
+
return epsilons, objectives, soft_constraints, hard_constraints, extra_constants
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import itertools
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import casadi as ca
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .goal_programming_mixin import GoalProgrammingMixin
|
|
9
|
+
from .goal_programming_mixin_base import (
|
|
10
|
+
Goal,
|
|
11
|
+
StateGoal,
|
|
12
|
+
_EmptyEnsembleList,
|
|
13
|
+
_EmptyEnsembleOrderedDict,
|
|
14
|
+
_GoalConstraint,
|
|
15
|
+
_GoalProgrammingMixinBase,
|
|
16
|
+
)
|
|
17
|
+
from .single_pass_goal_programming_mixin import SinglePassGoalProgrammingMixin
|
|
18
|
+
from .timeseries import Timeseries
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MinAbsGoal(Goal):
|
|
22
|
+
"""
|
|
23
|
+
Absolute minimization goal class which can be used to minimize the
|
|
24
|
+
absolute value of the goal's (linear) goal function. Contrary to its super
|
|
25
|
+
class, the default order is 1 as absolute minimization is typically
|
|
26
|
+
desired for fully linear problems.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
order = 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MinAbsStateGoal(StateGoal, MinAbsGoal):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _ConvertedMinAbsGoal(Goal):
|
|
37
|
+
order = 1
|
|
38
|
+
|
|
39
|
+
def __init__(self, abs_variable, is_path_goal, orig_goal):
|
|
40
|
+
self.abs_variable = abs_variable
|
|
41
|
+
self.is_path_goal = is_path_goal
|
|
42
|
+
self.orig_goal = orig_goal
|
|
43
|
+
|
|
44
|
+
# Copy relevant properties
|
|
45
|
+
self.size = orig_goal.size
|
|
46
|
+
self.weight = orig_goal.weight
|
|
47
|
+
self.relaxation = orig_goal.relaxation / orig_goal.function_nominal
|
|
48
|
+
self.priority = orig_goal.priority
|
|
49
|
+
|
|
50
|
+
def function(self, optimization_problem, ensemble_member):
|
|
51
|
+
if self.is_path_goal:
|
|
52
|
+
return optimization_problem.variable(self.abs_variable.name())
|
|
53
|
+
else:
|
|
54
|
+
return optimization_problem.extra_variable(self.abs_variable.name(), ensemble_member)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MinAbsGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
58
|
+
"""
|
|
59
|
+
Similar behavior to :py:class:`.GoalProgrammingMixin`, but any
|
|
60
|
+
:py:class:`MinAbsGoal` passed to :py:meth:`.min_abs_goals` or
|
|
61
|
+
:py:meth:`.min_abs_path_goals` will be automatically converted to:
|
|
62
|
+
|
|
63
|
+
1. An auxiliary minimization variable
|
|
64
|
+
2. Two additional linear constraints relating the auxiliary variable to the goal function
|
|
65
|
+
3. A new goal (of a different type) minimizing the auxiliary variable
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *args, **kwargs):
|
|
69
|
+
super().__init__(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
# List for any absolute minimization goals
|
|
72
|
+
self.__problem_constraints = _EmptyEnsembleList()
|
|
73
|
+
self.__problem_vars = []
|
|
74
|
+
self.__problem_path_constraints = _EmptyEnsembleList()
|
|
75
|
+
self.__problem_path_vars = []
|
|
76
|
+
self.__seeds = _EmptyEnsembleOrderedDict()
|
|
77
|
+
self.__path_seeds = _EmptyEnsembleOrderedDict()
|
|
78
|
+
|
|
79
|
+
self.__first_run = True
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def extra_variables(self):
|
|
83
|
+
return super().extra_variables + self.__problem_vars
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def path_variables(self):
|
|
87
|
+
return super().path_variables + self.__problem_path_vars
|
|
88
|
+
|
|
89
|
+
def bounds(self):
|
|
90
|
+
bounds = super().bounds()
|
|
91
|
+
for abs_var in self.__problem_vars + self.__problem_path_vars:
|
|
92
|
+
bounds[abs_var.name()] = (0.0, np.inf)
|
|
93
|
+
return bounds
|
|
94
|
+
|
|
95
|
+
def seed(self, ensemble_member):
|
|
96
|
+
seed = super().seed(ensemble_member)
|
|
97
|
+
|
|
98
|
+
# Seed minimization variables of current priority (not those of
|
|
99
|
+
# previous priorities, as those are handled by GoalProgrammingMixin).
|
|
100
|
+
for abs_var, val in self.__seeds[ensemble_member].items():
|
|
101
|
+
seed[abs_var] = val
|
|
102
|
+
|
|
103
|
+
times = self.times()
|
|
104
|
+
for abs_var, val in self.__path_seeds[ensemble_member].items():
|
|
105
|
+
seed[abs_var] = Timeseries(times, val)
|
|
106
|
+
|
|
107
|
+
return seed
|
|
108
|
+
|
|
109
|
+
def constraints(self, ensemble_member):
|
|
110
|
+
constraints = super().constraints(ensemble_member)
|
|
111
|
+
|
|
112
|
+
for constraint in self.__problem_constraints[ensemble_member]:
|
|
113
|
+
constraints.append((constraint.function(self), constraint.min, constraint.max))
|
|
114
|
+
|
|
115
|
+
return constraints
|
|
116
|
+
|
|
117
|
+
def path_constraints(self, ensemble_member):
|
|
118
|
+
path_constraints = super().path_constraints(ensemble_member)
|
|
119
|
+
|
|
120
|
+
for constraint in self.__problem_path_constraints[ensemble_member]:
|
|
121
|
+
path_constraints.append((constraint.function(self), constraint.min, constraint.max))
|
|
122
|
+
|
|
123
|
+
return path_constraints
|
|
124
|
+
|
|
125
|
+
def __validate_goals(self, goals, is_path_goal):
|
|
126
|
+
goals = sorted(goals, key=lambda x: x.priority)
|
|
127
|
+
|
|
128
|
+
for goal in goals:
|
|
129
|
+
if not isinstance(goal, MinAbsGoal):
|
|
130
|
+
raise Exception(
|
|
131
|
+
"Absolute goal not an instance of MinAbsGoal for goal {}".format(goal)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if goal.function_range != (np.nan, np.nan):
|
|
135
|
+
raise Exception(
|
|
136
|
+
"Absolute goal function is only allowed for minimization for goal {}".format(
|
|
137
|
+
goal
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if goal.order != 1:
|
|
142
|
+
raise Exception(
|
|
143
|
+
"Absolute goal function is only allowed for order = 1 for goal {}".format(goal)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if goal.weight <= 0:
|
|
147
|
+
raise Exception(
|
|
148
|
+
"Absolute goal function is only allowed for weight > 0 for goal {}".format(goal)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def __convert_goals(goals, sym_index, ensemble_size, is_path_goal):
|
|
153
|
+
# Replace absolute minimization goals with a new goal, and some
|
|
154
|
+
# additional hard constraints.
|
|
155
|
+
constraints = [[] for ensemble_member in range(ensemble_size)]
|
|
156
|
+
variables = []
|
|
157
|
+
|
|
158
|
+
# It is easier to modify goals in place, but we do not want to modify
|
|
159
|
+
# the original input list of goals. Make a copy to work with and
|
|
160
|
+
# return when we are done.
|
|
161
|
+
goals = goals.copy()
|
|
162
|
+
|
|
163
|
+
for j, goal in enumerate(goals):
|
|
164
|
+
assert isinstance(goal, MinAbsGoal)
|
|
165
|
+
|
|
166
|
+
abs_variable_name = "abs_{}_{}".format(sym_index, j)
|
|
167
|
+
if is_path_goal:
|
|
168
|
+
abs_variable_name = "path_" + abs_variable_name
|
|
169
|
+
|
|
170
|
+
abs_variable = ca.MX.sym(abs_variable_name, goal.size)
|
|
171
|
+
variables.append(abs_variable)
|
|
172
|
+
|
|
173
|
+
# Set constraints on how the additional variable relates to the
|
|
174
|
+
# original goal function, such that it corresponds to its absolute
|
|
175
|
+
# value when minimizing.
|
|
176
|
+
for ensemble_member in range(ensemble_size):
|
|
177
|
+
|
|
178
|
+
def _constraint_func(
|
|
179
|
+
problem,
|
|
180
|
+
sign,
|
|
181
|
+
abs_variable=abs_variable,
|
|
182
|
+
ensemble_member=ensemble_member,
|
|
183
|
+
goal=goal,
|
|
184
|
+
is_path_goal=is_path_goal,
|
|
185
|
+
):
|
|
186
|
+
if is_path_goal:
|
|
187
|
+
abs_variable = problem.variable(abs_variable.name())
|
|
188
|
+
else:
|
|
189
|
+
abs_variable = problem.extra_variable(abs_variable.name(), ensemble_member)
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
abs_variable
|
|
193
|
+
+ sign * goal.function(problem, ensemble_member) / goal.function_nominal
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
_pos = functools.partial(_constraint_func, sign=1)
|
|
197
|
+
_neg = functools.partial(_constraint_func, sign=-1)
|
|
198
|
+
|
|
199
|
+
constraints[ensemble_member].append(_GoalConstraint(None, _pos, 0.0, np.inf, False))
|
|
200
|
+
constraints[ensemble_member].append(_GoalConstraint(None, _neg, 0.0, np.inf, False))
|
|
201
|
+
|
|
202
|
+
# Overwrite the original goal, such that it is just a minimization
|
|
203
|
+
# of the additional variable.
|
|
204
|
+
goals[j] = _ConvertedMinAbsGoal(abs_variable, is_path_goal, goal)
|
|
205
|
+
|
|
206
|
+
return goals, constraints, variables
|
|
207
|
+
|
|
208
|
+
def __calculate_seed(self, goals, is_path_goal):
|
|
209
|
+
assert self.__first_run is False
|
|
210
|
+
|
|
211
|
+
seed = [{} for ensemble_member in range(self.ensemble_size)]
|
|
212
|
+
|
|
213
|
+
for goal in goals:
|
|
214
|
+
assert isinstance(goal, _ConvertedMinAbsGoal)
|
|
215
|
+
|
|
216
|
+
for ensemble_member in range(self.ensemble_size):
|
|
217
|
+
if is_path_goal:
|
|
218
|
+
expr = self.map_path_expression(
|
|
219
|
+
goal.orig_goal.function(self, ensemble_member), ensemble_member
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
expr = goal.orig_goal.function(self, ensemble_member)
|
|
223
|
+
|
|
224
|
+
function = ca.Function("f", [self.solver_input], [expr])
|
|
225
|
+
value = np.array(function(self.solver_output))
|
|
226
|
+
|
|
227
|
+
assert value.ndim == 2
|
|
228
|
+
|
|
229
|
+
if goal.size == 1:
|
|
230
|
+
if is_path_goal:
|
|
231
|
+
value = value.ravel()
|
|
232
|
+
else:
|
|
233
|
+
value = value.item()
|
|
234
|
+
|
|
235
|
+
seed[ensemble_member][goal.abs_variable.name()] = np.abs(value)
|
|
236
|
+
|
|
237
|
+
return seed
|
|
238
|
+
|
|
239
|
+
def optimize(self, preprocessing=True, **kwargs):
|
|
240
|
+
# Do pre-processing
|
|
241
|
+
if preprocessing:
|
|
242
|
+
self.pre()
|
|
243
|
+
|
|
244
|
+
goals = self.min_abs_goals()
|
|
245
|
+
path_goals = self.min_abs_path_goals()
|
|
246
|
+
|
|
247
|
+
# Validate goal definitions
|
|
248
|
+
self.__validate_goals(goals, is_path_goal=False)
|
|
249
|
+
self.__validate_goals(path_goals, is_path_goal=True)
|
|
250
|
+
|
|
251
|
+
# List for absolute minimization goals. These will be incrementally
|
|
252
|
+
# filled only just before we need them to.
|
|
253
|
+
self.__problem_constraints = [[] for ensemble_member in range(self.ensemble_size)]
|
|
254
|
+
self.__problem_vars = []
|
|
255
|
+
self.__problem_path_constraints = [[] for ensemble_member in range(self.ensemble_size)]
|
|
256
|
+
self.__problem_path_vars = []
|
|
257
|
+
|
|
258
|
+
# Similar to the above, but these keep track of all auxiliary
|
|
259
|
+
# variables and constraints of priorities we have had (and therefore
|
|
260
|
+
# activated), and those yet to come (and have not yet activated).
|
|
261
|
+
self.__subproblem_constraints = {}
|
|
262
|
+
self.__subproblem_vars = {}
|
|
263
|
+
self.__subproblem_abs_goals = {}
|
|
264
|
+
self.__subproblem_path_constraints = {}
|
|
265
|
+
self.__subproblem_path_vars = {}
|
|
266
|
+
self.__subproblem_path_abs_goals = {}
|
|
267
|
+
|
|
268
|
+
# We want to have consistent naming with GPMixin for our auxiliary
|
|
269
|
+
# variables. We therefore need to loop over all priorities, regardless
|
|
270
|
+
# of whether there are any MinAbsGoals in it or not.
|
|
271
|
+
priorities = {
|
|
272
|
+
int(goal.priority)
|
|
273
|
+
for goal in itertools.chain(goals, path_goals, self.goals(), self.path_goals())
|
|
274
|
+
if not goal.is_empty
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
subproblems = []
|
|
278
|
+
for priority in sorted(priorities):
|
|
279
|
+
subproblems.append(
|
|
280
|
+
(
|
|
281
|
+
priority,
|
|
282
|
+
[
|
|
283
|
+
goal
|
|
284
|
+
for goal in goals
|
|
285
|
+
if int(goal.priority) == priority and not goal.is_empty
|
|
286
|
+
],
|
|
287
|
+
[
|
|
288
|
+
goal
|
|
289
|
+
for goal in path_goals
|
|
290
|
+
if int(goal.priority) == priority and not goal.is_empty
|
|
291
|
+
],
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Rewrite absolute minimization goals.
|
|
296
|
+
self.__converted_goals = []
|
|
297
|
+
self.__converted_path_goals = []
|
|
298
|
+
|
|
299
|
+
for i, (priority, goals, path_goals) in enumerate(subproblems):
|
|
300
|
+
(
|
|
301
|
+
goals,
|
|
302
|
+
self.__subproblem_constraints[priority],
|
|
303
|
+
self.__subproblem_vars[priority],
|
|
304
|
+
) = self.__convert_goals(goals, i, self.ensemble_size, False)
|
|
305
|
+
|
|
306
|
+
self.__converted_goals.extend(goals)
|
|
307
|
+
self.__subproblem_abs_goals[priority] = goals
|
|
308
|
+
|
|
309
|
+
(
|
|
310
|
+
path_goals,
|
|
311
|
+
self.__subproblem_path_constraints[priority],
|
|
312
|
+
self.__subproblem_path_vars[priority],
|
|
313
|
+
) = self.__convert_goals(path_goals, i, self.ensemble_size, True)
|
|
314
|
+
|
|
315
|
+
self.__converted_path_goals.extend(path_goals)
|
|
316
|
+
self.__subproblem_path_abs_goals[priority] = path_goals
|
|
317
|
+
|
|
318
|
+
return super().optimize(**kwargs, preprocessing=False)
|
|
319
|
+
|
|
320
|
+
def priority_started(self, priority):
|
|
321
|
+
super().priority_started(priority)
|
|
322
|
+
|
|
323
|
+
# Enable constraints and auxiliary variables that we need starting
|
|
324
|
+
# from this priority when using GoalProgrammingMixin. When using
|
|
325
|
+
# SinglePassGoalProgrammingMixin, we need to add all constraints from
|
|
326
|
+
# the start.
|
|
327
|
+
if isinstance(self, GoalProgrammingMixin):
|
|
328
|
+
priorities = [priority]
|
|
329
|
+
elif isinstance(self, SinglePassGoalProgrammingMixin):
|
|
330
|
+
if self.__first_run:
|
|
331
|
+
priorities = self.__subproblem_constraints.keys()
|
|
332
|
+
else:
|
|
333
|
+
priorities = []
|
|
334
|
+
|
|
335
|
+
for p in priorities:
|
|
336
|
+
for a, b in zip(self.__problem_constraints, self.__subproblem_constraints[p]):
|
|
337
|
+
a.extend(b)
|
|
338
|
+
|
|
339
|
+
self.__problem_vars.extend(self.__subproblem_vars[p])
|
|
340
|
+
|
|
341
|
+
for a, b in zip(self.__problem_path_constraints, self.__subproblem_path_constraints[p]):
|
|
342
|
+
a.extend(b)
|
|
343
|
+
|
|
344
|
+
self.__problem_path_vars.extend(self.__subproblem_path_vars[p])
|
|
345
|
+
|
|
346
|
+
# Calculate the seed needed for goals/variables introduced in this
|
|
347
|
+
# priority. We can only calculate a seed if this is not the first
|
|
348
|
+
# priority.
|
|
349
|
+
if not self.__first_run and isinstance(self, GoalProgrammingMixin):
|
|
350
|
+
self.__seeds = self.__calculate_seed(self.__subproblem_abs_goals[priority], False)
|
|
351
|
+
self.__path_seeds = self.__calculate_seed(
|
|
352
|
+
self.__subproblem_path_abs_goals[priority], True
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
self.__first_run = False
|
|
356
|
+
|
|
357
|
+
def min_abs_goals(self) -> List[MinAbsGoal]:
|
|
358
|
+
"""
|
|
359
|
+
User problem returns list of :py:class:`MinAbsGoal` objects.
|
|
360
|
+
|
|
361
|
+
:returns: A list of goals.
|
|
362
|
+
"""
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
def goals(self):
|
|
366
|
+
goals = super().goals()
|
|
367
|
+
try:
|
|
368
|
+
return goals + self.__converted_goals
|
|
369
|
+
except AttributeError:
|
|
370
|
+
return goals
|
|
371
|
+
|
|
372
|
+
def min_abs_path_goals(self) -> List[MinAbsGoal]:
|
|
373
|
+
"""
|
|
374
|
+
User problem returns list of :py:class:`MinAbsGoal` objects.
|
|
375
|
+
|
|
376
|
+
:returns: A list of goals.
|
|
377
|
+
"""
|
|
378
|
+
return []
|
|
379
|
+
|
|
380
|
+
def path_goals(self):
|
|
381
|
+
goals = super().path_goals()
|
|
382
|
+
try:
|
|
383
|
+
return goals + self.__converted_path_goals
|
|
384
|
+
except AttributeError:
|
|
385
|
+
return goals
|