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
|
@@ -5,12 +5,16 @@ from enum import Enum
|
|
|
5
5
|
from typing import Dict, Union
|
|
6
6
|
|
|
7
7
|
import casadi as ca
|
|
8
|
-
|
|
9
8
|
import numpy as np
|
|
10
9
|
|
|
11
10
|
from .goal_programming_mixin import GoalProgrammingMixin
|
|
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
|
+
_GoalProgrammingMixinBase,
|
|
17
|
+
)
|
|
14
18
|
from .timeseries import Timeseries
|
|
15
19
|
|
|
16
20
|
logger = logging.getLogger("rtctools")
|
|
@@ -92,7 +96,10 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
92
96
|
self.__path_objectives_per_priority = []
|
|
93
97
|
|
|
94
98
|
if isinstance(self, GoalProgrammingMixin):
|
|
95
|
-
raise Exception(
|
|
99
|
+
raise Exception(
|
|
100
|
+
"Cannot be an instance of both GoalProgrammingMixin "
|
|
101
|
+
"and SinglePassGoalProgrammingMixin"
|
|
102
|
+
)
|
|
96
103
|
|
|
97
104
|
@property
|
|
98
105
|
def extra_variables(self):
|
|
@@ -104,7 +111,7 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
104
111
|
|
|
105
112
|
def bounds(self):
|
|
106
113
|
bounds = super().bounds()
|
|
107
|
-
for epsilon in
|
|
114
|
+
for epsilon in self.__problem_epsilons + self.__problem_path_epsilons:
|
|
108
115
|
bounds[epsilon.name()] = (0.0, 1.0)
|
|
109
116
|
return bounds
|
|
110
117
|
|
|
@@ -115,7 +122,7 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
115
122
|
|
|
116
123
|
# Append min/max timeseries to the constant inputs. Note that min/max
|
|
117
124
|
# timeseries are shared between all ensemble members.
|
|
118
|
-
for
|
|
125
|
+
for variable, value in self.__problem_path_timeseries:
|
|
119
126
|
if isinstance(value, np.ndarray):
|
|
120
127
|
value = Timeseries(self.times(), np.broadcast_to(value, (n_times, len(value))))
|
|
121
128
|
elif not isinstance(value, Timeseries):
|
|
@@ -129,7 +136,7 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
129
136
|
|
|
130
137
|
# Append min/max values to the parameters. Note that min/max values
|
|
131
138
|
# are shared between all ensemble members.
|
|
132
|
-
for
|
|
139
|
+
for variable, value in self.__problem_parameters:
|
|
133
140
|
parameters[variable] = value
|
|
134
141
|
|
|
135
142
|
return parameters
|
|
@@ -143,7 +150,8 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
143
150
|
|
|
144
151
|
additional_constraints = itertools.chain(
|
|
145
152
|
self.__constraint_store[ensemble_member].values(),
|
|
146
|
-
self.__problem_constraints[ensemble_member]
|
|
153
|
+
self.__problem_constraints[ensemble_member],
|
|
154
|
+
)
|
|
147
155
|
|
|
148
156
|
for constraint in additional_constraints:
|
|
149
157
|
constraints.append((constraint.function(self), constraint.min, constraint.max))
|
|
@@ -155,7 +163,8 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
155
163
|
|
|
156
164
|
additional_path_constraints = itertools.chain(
|
|
157
165
|
self.__path_constraint_store[ensemble_member].values(),
|
|
158
|
-
self.__problem_path_constraints[ensemble_member]
|
|
166
|
+
self.__problem_path_constraints[ensemble_member],
|
|
167
|
+
)
|
|
159
168
|
|
|
160
169
|
for constraint in additional_path_constraints:
|
|
161
170
|
path_constraints.append((constraint.function(self), constraint.min, constraint.max))
|
|
@@ -163,32 +172,30 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
163
172
|
return path_constraints
|
|
164
173
|
|
|
165
174
|
def solver_options(self):
|
|
166
|
-
|
|
167
175
|
# TODO: Split off into private
|
|
168
176
|
|
|
169
177
|
# Call parent
|
|
170
178
|
options = super().solver_options()
|
|
171
179
|
|
|
172
|
-
solver = options[
|
|
173
|
-
assert solver in [
|
|
180
|
+
solver = options["solver"]
|
|
181
|
+
assert solver in ["bonmin", "ipopt"]
|
|
174
182
|
|
|
175
183
|
# Make sure constant states, such as min/max timeseries for violation variables,
|
|
176
184
|
# are turned into parameters for the final optimization problem.
|
|
177
185
|
ipopt_options = options[solver]
|
|
178
|
-
ipopt_options[
|
|
186
|
+
ipopt_options["fixed_variable_treatment"] = "make_parameter"
|
|
179
187
|
|
|
180
188
|
# Define temporary variable to avoid infinite loop between
|
|
181
189
|
# solver_options and goal_programming_options.
|
|
182
190
|
self._loop_breaker_solver_options = True
|
|
183
191
|
|
|
184
|
-
if not hasattr(self,
|
|
185
|
-
if not self.goal_programming_options()[
|
|
186
|
-
ipopt_options[
|
|
192
|
+
if not hasattr(self, "_loop_breaker_goal_programming_options"):
|
|
193
|
+
if not self.goal_programming_options()["mu_reinit"]:
|
|
194
|
+
ipopt_options["mu_strategy"] = "monotone"
|
|
187
195
|
if not self._gp_first_run:
|
|
188
|
-
ipopt_options[
|
|
189
|
-
'mu'][-1]
|
|
196
|
+
ipopt_options["mu_init"] = self.solver_stats["iterations"]["mu"][-1]
|
|
190
197
|
|
|
191
|
-
delattr(self,
|
|
198
|
+
delattr(self, "_loop_breaker_solver_options")
|
|
192
199
|
|
|
193
200
|
return options
|
|
194
201
|
|
|
@@ -224,51 +231,53 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
224
231
|
If ``fix_minimized_values`` is set to ``True``, goal functions will be set to equal their
|
|
225
232
|
optimized values in optimization problems generated during subsequent priorities. Otherwise,
|
|
226
233
|
only an upper bound will be set. Use of this option is normally not required.
|
|
227
|
-
Note that the use of this option may add non-convex constraints to the optimization
|
|
228
|
-
The default value for this parameter is ``True`` for the default solvers
|
|
229
|
-
other solver is used, the default value is ``False``.
|
|
234
|
+
Note that the use of this option may add non-convex constraints to the optimization
|
|
235
|
+
problem. The default value for this parameter is ``True`` for the default solvers
|
|
236
|
+
IPOPT/BONMIN. If any other solver is used, the default value is ``False``.
|
|
230
237
|
|
|
231
|
-
If ``check_monotonicity`` is set to ``True``, then it will be checked whether goals with
|
|
232
|
-
function key form a monotonically decreasing sequence with regards to the target
|
|
238
|
+
If ``check_monotonicity`` is set to ``True``, then it will be checked whether goals with
|
|
239
|
+
the same function key form a monotonically decreasing sequence with regards to the target
|
|
240
|
+
interval.
|
|
233
241
|
|
|
234
|
-
The option ``equality_threshold`` controls when a two-sided inequality constraint is folded
|
|
235
|
-
an equality constraint.
|
|
242
|
+
The option ``equality_threshold`` controls when a two-sided inequality constraint is folded
|
|
243
|
+
into an equality constraint.
|
|
236
244
|
|
|
237
|
-
If ``scale_by_problem_size`` is set to ``True``, the objective (i.e. the sum of the
|
|
238
|
-
will be divided by the number of goals, and the path objective will
|
|
239
|
-
of path goals and the number of active time steps (per goal).
|
|
240
|
-
the range [0, 1], at the cost of solving
|
|
245
|
+
If ``scale_by_problem_size`` is set to ``True``, the objective (i.e. the sum of the
|
|
246
|
+
violation variables) will be divided by the number of goals, and the path objective will
|
|
247
|
+
be divided by the number of path goals and the number of active time steps (per goal).
|
|
248
|
+
This will make sure the objectives are always in the range [0, 1], at the cost of solving
|
|
249
|
+
each goal/time step less accurately.
|
|
241
250
|
|
|
242
251
|
:returns: A dictionary of goal programming options.
|
|
243
252
|
"""
|
|
244
253
|
|
|
245
254
|
options = {}
|
|
246
255
|
|
|
247
|
-
options[
|
|
248
|
-
options[
|
|
249
|
-
options[
|
|
250
|
-
options[
|
|
251
|
-
options[
|
|
252
|
-
options[
|
|
256
|
+
options["mu_reinit"] = True
|
|
257
|
+
options["constraint_relaxation"] = 0.0 # Disable by default
|
|
258
|
+
options["fix_minimized_values"] = False
|
|
259
|
+
options["check_monotonicity"] = True
|
|
260
|
+
options["equality_threshold"] = 1e-8
|
|
261
|
+
options["scale_by_problem_size"] = False
|
|
253
262
|
|
|
254
263
|
# Forced options to be able to re-use GoalProgrammingMixin's
|
|
255
264
|
# GoalProgrammingMixin._gp_* functions. These are not relevant for
|
|
256
265
|
# SinglePassGoalProgrammingMixin, or should be set to a certain value
|
|
257
266
|
# for it to make sense.
|
|
258
|
-
options[
|
|
259
|
-
options[
|
|
260
|
-
options[
|
|
261
|
-
options[
|
|
267
|
+
options["violation_relaxation"] = 0.0 # Disable by default
|
|
268
|
+
options["violation_tolerance"] = np.inf # Disable by default
|
|
269
|
+
options["interior_distance"] = 1e-6
|
|
270
|
+
options["keep_soft_constraints"] = True
|
|
262
271
|
|
|
263
272
|
# Define temporary variable to avoid infinite loop between
|
|
264
273
|
# solver_options and goal_programming_options.
|
|
265
274
|
self._loop_breaker_goal_programming_options = True
|
|
266
275
|
|
|
267
|
-
if not hasattr(self,
|
|
268
|
-
if self.solver_options()[
|
|
269
|
-
options[
|
|
276
|
+
if not hasattr(self, "_loop_breaker_solver_options"):
|
|
277
|
+
if self.solver_options()["solver"] in {"ipopt", "bonmin"}:
|
|
278
|
+
options["fix_minimized_values"] = True
|
|
270
279
|
|
|
271
|
-
delattr(self,
|
|
280
|
+
delattr(self, "_loop_breaker_goal_programming_options")
|
|
272
281
|
|
|
273
282
|
return options
|
|
274
283
|
|
|
@@ -288,13 +297,26 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
288
297
|
self._gp_validate_goals(goals, is_path_goal=False)
|
|
289
298
|
self._gp_validate_goals(path_goals, is_path_goal=True)
|
|
290
299
|
|
|
291
|
-
priorities = sorted(
|
|
300
|
+
priorities = sorted(
|
|
301
|
+
{int(goal.priority) for goal in itertools.chain(goals, path_goals) if not goal.is_empty}
|
|
302
|
+
)
|
|
292
303
|
|
|
293
304
|
for priority in priorities:
|
|
294
|
-
subproblems.append(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
subproblems.append(
|
|
306
|
+
(
|
|
307
|
+
priority,
|
|
308
|
+
[
|
|
309
|
+
goal
|
|
310
|
+
for goal in goals
|
|
311
|
+
if int(goal.priority) == priority and not goal.is_empty
|
|
312
|
+
],
|
|
313
|
+
[
|
|
314
|
+
goal
|
|
315
|
+
for goal in path_goals
|
|
316
|
+
if int(goal.priority) == priority and not goal.is_empty
|
|
317
|
+
],
|
|
318
|
+
)
|
|
319
|
+
)
|
|
298
320
|
|
|
299
321
|
# Solve the subproblems one by one
|
|
300
322
|
logger.info("Starting goal programming")
|
|
@@ -302,7 +324,9 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
302
324
|
success = False
|
|
303
325
|
|
|
304
326
|
self.__constraint_store = [OrderedDict() for ensemble_member in range(self.ensemble_size)]
|
|
305
|
-
self.__path_constraint_store = [
|
|
327
|
+
self.__path_constraint_store = [
|
|
328
|
+
OrderedDict() for ensemble_member in range(self.ensemble_size)
|
|
329
|
+
]
|
|
306
330
|
|
|
307
331
|
self.__problem_constraints = [[] for ensemble_member in range(self.ensemble_size)]
|
|
308
332
|
self.__problem_path_constraints = [[] for ensemble_member in range(self.ensemble_size)]
|
|
@@ -325,15 +349,21 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
325
349
|
self.__objectives = []
|
|
326
350
|
|
|
327
351
|
for i, (_, goals, path_goals) in enumerate(subproblems):
|
|
328
|
-
(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
352
|
+
(
|
|
353
|
+
subproblem_epsilons,
|
|
354
|
+
subproblem_objectives,
|
|
355
|
+
subproblem_soft_constraints,
|
|
356
|
+
hard_constraints,
|
|
357
|
+
subproblem_parameters,
|
|
358
|
+
) = self._gp_goal_constraints(goals, i, options, is_path_goal=False)
|
|
359
|
+
|
|
360
|
+
(
|
|
361
|
+
subproblem_path_epsilons,
|
|
362
|
+
subproblem_path_objectives,
|
|
363
|
+
subproblem_path_soft_constraints,
|
|
364
|
+
path_hard_constraints,
|
|
365
|
+
subproblem_path_timeseries,
|
|
366
|
+
) = self._gp_goal_constraints(path_goals, i, options, is_path_goal=True)
|
|
337
367
|
|
|
338
368
|
# Put hard constraints in the constraint stores
|
|
339
369
|
self._gp_update_constraint_store(self.__constraint_store, hard_constraints)
|
|
@@ -349,9 +379,11 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
349
379
|
|
|
350
380
|
for ensemble_member in range(self.ensemble_size):
|
|
351
381
|
self.__problem_constraints[ensemble_member].extend(
|
|
352
|
-
subproblem_soft_constraints[ensemble_member]
|
|
382
|
+
subproblem_soft_constraints[ensemble_member]
|
|
383
|
+
)
|
|
353
384
|
self.__problem_path_constraints[ensemble_member].extend(
|
|
354
|
-
subproblem_path_soft_constraints[ensemble_member]
|
|
385
|
+
subproblem_path_soft_constraints[ensemble_member]
|
|
386
|
+
)
|
|
355
387
|
|
|
356
388
|
self.__objectives_per_priority.append(subproblem_objectives)
|
|
357
389
|
self.__path_objectives_per_priority.append(subproblem_path_objectives)
|
|
@@ -364,7 +396,10 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
364
396
|
|
|
365
397
|
# Solve subproblem
|
|
366
398
|
success = super().optimize(
|
|
367
|
-
preprocessing=False,
|
|
399
|
+
preprocessing=False,
|
|
400
|
+
postprocessing=False,
|
|
401
|
+
log_solver_failure_as_error=log_solver_failure_as_error,
|
|
402
|
+
)
|
|
368
403
|
if not success:
|
|
369
404
|
break
|
|
370
405
|
|
|
@@ -376,15 +411,18 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
376
411
|
# two relevant options here for later use.
|
|
377
412
|
options = self.goal_programming_options()
|
|
378
413
|
self.__objective_constraint_options = {
|
|
379
|
-
k: v
|
|
380
|
-
|
|
414
|
+
k: v
|
|
415
|
+
for k, v in options.items()
|
|
416
|
+
if k in {"fix_minimized_values", "constraint_relaxation"}
|
|
381
417
|
}
|
|
382
418
|
|
|
383
419
|
# Store results. Do this here, to make sure we have results even
|
|
384
420
|
# if a subsequent priority fails.
|
|
385
421
|
self.__results_are_current = False
|
|
386
|
-
self.__results = [
|
|
387
|
-
|
|
422
|
+
self.__results = [
|
|
423
|
+
self.extract_results(ensemble_member)
|
|
424
|
+
for ensemble_member in range(self.ensemble_size)
|
|
425
|
+
]
|
|
388
426
|
self.__results_are_current = True
|
|
389
427
|
|
|
390
428
|
# Call the post priority hook, so that intermediate results can be
|
|
@@ -403,16 +441,21 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
403
441
|
return success
|
|
404
442
|
|
|
405
443
|
def transcribe(self):
|
|
406
|
-
|
|
407
444
|
def _objective_func(subproblem_objectives, subproblem_path_objectives):
|
|
408
445
|
val = 0.0
|
|
409
446
|
for ensemble_member in range(self.ensemble_size):
|
|
410
447
|
n_objectives = self._gp_n_objectives(
|
|
411
|
-
subproblem_objectives, subproblem_path_objectives, ensemble_member
|
|
448
|
+
subproblem_objectives, subproblem_path_objectives, ensemble_member
|
|
449
|
+
)
|
|
412
450
|
expr = self._gp_objective(subproblem_objectives, n_objectives, ensemble_member)
|
|
413
|
-
expr += ca.sum1(
|
|
414
|
-
self.
|
|
415
|
-
|
|
451
|
+
expr += ca.sum1(
|
|
452
|
+
self.map_path_expression(
|
|
453
|
+
self._gp_path_objective(
|
|
454
|
+
subproblem_path_objectives, n_objectives, ensemble_member
|
|
455
|
+
),
|
|
456
|
+
ensemble_member,
|
|
457
|
+
)
|
|
458
|
+
)
|
|
416
459
|
val += self.ensemble_member_probability(ensemble_member) * expr
|
|
417
460
|
|
|
418
461
|
return val
|
|
@@ -426,8 +469,11 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
426
469
|
|
|
427
470
|
# Objectives
|
|
428
471
|
for subproblem_objectives, subproblem_path_objectives in zip(
|
|
429
|
-
|
|
430
|
-
|
|
472
|
+
self.__objectives_per_priority, self.__path_objectives_per_priority
|
|
473
|
+
):
|
|
474
|
+
self.__objectives.append(
|
|
475
|
+
_objective_func(subproblem_objectives, subproblem_path_objectives)
|
|
476
|
+
)
|
|
431
477
|
|
|
432
478
|
if self.single_pass_method == SinglePassMethod.UPDATE_OBJECTIVE_CONSTRAINT_BOUNDS:
|
|
433
479
|
# The objectives are also directly added as constraints
|
|
@@ -439,18 +485,20 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
439
485
|
options = self.__objective_constraint_options
|
|
440
486
|
|
|
441
487
|
previous_objective = self.__objectives[self.__current_priority - 1]
|
|
442
|
-
f = ca.Function(
|
|
488
|
+
f = ca.Function("tmp", [self.solver_input], [previous_objective])
|
|
443
489
|
obj_val = float(f(self.solver_output))
|
|
444
490
|
|
|
445
|
-
if options[
|
|
491
|
+
if options["fix_minimized_values"]:
|
|
446
492
|
lb, ub = obj_val, obj_val
|
|
447
493
|
self.linear_collocation = False # Disable solver option jac_c_constant for IPOPT
|
|
448
494
|
else:
|
|
449
|
-
obj_val += options[
|
|
495
|
+
obj_val += options["constraint_relaxation"]
|
|
450
496
|
lb, ub = -np.inf, obj_val
|
|
451
497
|
|
|
452
498
|
if self.single_pass_method == SinglePassMethod.APPEND_CONSTRAINTS_OBJECTIVE:
|
|
453
|
-
self.__additional_constraints.append(
|
|
499
|
+
self.__additional_constraints.append(
|
|
500
|
+
(self.__objectives[self.__current_priority - 1], lb, ub)
|
|
501
|
+
)
|
|
454
502
|
elif self.single_pass_method == SinglePassMethod.UPDATE_OBJECTIVE_CONSTRAINT_BOUNDS:
|
|
455
503
|
ind = self.__current_priority - 1
|
|
456
504
|
constraint = self.__additional_constraints[ind]
|
|
@@ -463,13 +511,13 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
|
|
|
463
511
|
if self.__additional_constraints:
|
|
464
512
|
g_extra, lbg_extra, ubg_extra = zip(*self.__additional_constraints)
|
|
465
513
|
|
|
466
|
-
g = ca.vertcat(nlp[
|
|
514
|
+
g = ca.vertcat(nlp["g"], *g_extra)
|
|
467
515
|
lbg = [*lbg.copy(), *lbg_extra]
|
|
468
516
|
ubg = [*ubg.copy(), *ubg_extra]
|
|
469
517
|
|
|
470
|
-
nlp[
|
|
518
|
+
nlp["g"] = g
|
|
471
519
|
|
|
472
|
-
nlp[
|
|
520
|
+
nlp["f"] = self.__objectives[self.__current_priority]
|
|
473
521
|
|
|
474
522
|
if not self._gp_first_run:
|
|
475
523
|
x0 = self.solver_output.copy()
|
|
@@ -508,24 +556,25 @@ class CachingQPSol:
|
|
|
508
556
|
self._tlcache = {}
|
|
509
557
|
|
|
510
558
|
def __call__(self, name, solver_name, nlp, options):
|
|
511
|
-
|
|
512
559
|
class Solver:
|
|
513
|
-
def __init__(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
560
|
+
def __init__(
|
|
561
|
+
self, nlp=nlp, solver_name=solver_name, options=options, cache=self._tlcache
|
|
562
|
+
):
|
|
563
|
+
x = nlp["x"]
|
|
564
|
+
f = nlp["f"]
|
|
565
|
+
g = nlp["g"]
|
|
517
566
|
|
|
518
567
|
if isinstance(x, ca.MX):
|
|
519
568
|
# Can only convert SX to DM
|
|
520
|
-
x = ca.SX.sym(
|
|
521
|
-
x_mx = nlp[
|
|
569
|
+
x = ca.SX.sym("X", *x.shape)
|
|
570
|
+
x_mx = nlp["x"]
|
|
522
571
|
expand = True
|
|
523
572
|
else:
|
|
524
573
|
x_mx = None
|
|
525
574
|
expand = False
|
|
526
575
|
|
|
527
576
|
if expand:
|
|
528
|
-
expand_f = ca.Function(
|
|
577
|
+
expand_f = ca.Function("f", [x_mx], [f]).expand()
|
|
529
578
|
f = expand_f(x)
|
|
530
579
|
|
|
531
580
|
# Gradient of the objective: gf == Hx + g
|
|
@@ -535,37 +584,40 @@ class CachingQPSol:
|
|
|
535
584
|
c = ca.substitute(gf, x, ca.DM.zeros(x.sparsity()))
|
|
536
585
|
|
|
537
586
|
# Identify the quadratic term in the objective
|
|
538
|
-
H = 0.5*ca.jacobian(gf, x, {"symmetric": True})
|
|
587
|
+
H = 0.5 * ca.jacobian(gf, x, {"symmetric": True})
|
|
539
588
|
|
|
540
589
|
if cache:
|
|
541
|
-
if not x.size1() == cache[
|
|
590
|
+
if not x.size1() == cache["A"].size2():
|
|
542
591
|
raise Exception(
|
|
543
|
-
"Number of variables {} does not match
|
|
544
|
-
|
|
592
|
+
"Number of variables {} does not match "
|
|
593
|
+
"cached constraint matrix dimensions {}".format(
|
|
594
|
+
x.size1(), cache["A"].shape
|
|
595
|
+
)
|
|
596
|
+
)
|
|
545
597
|
|
|
546
|
-
n_g_cache = cache[
|
|
598
|
+
n_g_cache = cache["A"].size1()
|
|
547
599
|
n_g = g.size1()
|
|
548
600
|
|
|
549
601
|
if n_g_cache == n_g:
|
|
550
|
-
b = cache[
|
|
551
|
-
A = cache[
|
|
602
|
+
b = cache["b"]
|
|
603
|
+
A = cache["A"]
|
|
552
604
|
else:
|
|
553
605
|
g_new = g[n_g_cache:]
|
|
554
606
|
|
|
555
607
|
if expand:
|
|
556
|
-
expand_g_new = ca.Function(
|
|
608
|
+
expand_g_new = ca.Function("f", [x_mx], [g_new]).expand()
|
|
557
609
|
g_new = expand_g_new(x)
|
|
558
610
|
|
|
559
611
|
# Identify the constant term in the constraints
|
|
560
|
-
b = ca.vertcat(
|
|
561
|
-
|
|
612
|
+
b = ca.vertcat(
|
|
613
|
+
cache["b"], ca.substitute(g_new, x, ca.DM.zeros(x.sparsity()))
|
|
614
|
+
)
|
|
562
615
|
|
|
563
616
|
# Identify the linear term in the constraints
|
|
564
|
-
A = ca.vertcat(cache[
|
|
565
|
-
ca.jacobian(g_new, x))
|
|
617
|
+
A = ca.vertcat(cache["A"], ca.jacobian(g_new, x))
|
|
566
618
|
else:
|
|
567
619
|
if expand:
|
|
568
|
-
expand_g = ca.Function(
|
|
620
|
+
expand_g = ca.Function("f", [x_mx], [g]).expand()
|
|
569
621
|
g = expand_g(x)
|
|
570
622
|
|
|
571
623
|
# Identify the constant term in the constraints
|
|
@@ -574,16 +626,12 @@ class CachingQPSol:
|
|
|
574
626
|
# Identify the linear term in the constraints
|
|
575
627
|
A = ca.jacobian(g, x)
|
|
576
628
|
|
|
577
|
-
cache[
|
|
578
|
-
cache[
|
|
629
|
+
cache["A"] = A
|
|
630
|
+
cache["b"] = b
|
|
579
631
|
|
|
580
|
-
self._solver = ca.conic(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
'h': H.sparsity(),
|
|
584
|
-
'a': A.sparsity()
|
|
585
|
-
},
|
|
586
|
-
options)
|
|
632
|
+
self._solver = ca.conic(
|
|
633
|
+
"mysolver", solver_name, {"h": H.sparsity(), "a": A.sparsity()}, options
|
|
634
|
+
)
|
|
587
635
|
self._solver_in = {}
|
|
588
636
|
self._solver_in["h"] = ca.DM(H)
|
|
589
637
|
self._solver_in["g"] = ca.DM(c)
|
|
@@ -591,7 +639,6 @@ class CachingQPSol:
|
|
|
591
639
|
self._b = ca.DM(b)
|
|
592
640
|
|
|
593
641
|
def __call__(self, x0, lbx, ubx, lbg, ubg):
|
|
594
|
-
|
|
595
642
|
self._solver_in["x0"] = x0
|
|
596
643
|
self._solver_in["lbx"] = lbx
|
|
597
644
|
self._solver_in["ubx"] = ubx
|
|
@@ -600,7 +647,7 @@ class CachingQPSol:
|
|
|
600
647
|
|
|
601
648
|
solver_out = self._solver(**self._solver_in)
|
|
602
649
|
|
|
603
|
-
solver_out[
|
|
650
|
+
solver_out["f"] = solver_out["cost"]
|
|
604
651
|
|
|
605
652
|
return solver_out
|
|
606
653
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import Union
|
|
2
2
|
|
|
3
3
|
import casadi as ca
|
|
4
|
-
|
|
5
4
|
import numpy as np
|
|
6
5
|
|
|
7
6
|
|
|
@@ -28,7 +27,7 @@ class Timeseries:
|
|
|
28
27
|
elif isinstance(values, (np.ndarray, list)) and len(values) == 1:
|
|
29
28
|
values = values[0]
|
|
30
29
|
|
|
31
|
-
if hasattr(values,
|
|
30
|
+
if hasattr(values, "__iter__"):
|
|
32
31
|
self.__values = np.array(values, dtype=np.float64, copy=True)
|
|
33
32
|
else:
|
|
34
33
|
self.__values = np.full_like(times, values, dtype=np.float64)
|
|
@@ -47,12 +46,11 @@ class Timeseries:
|
|
|
47
46
|
"""
|
|
48
47
|
return self.__values
|
|
49
48
|
|
|
50
|
-
def __neg__(self) ->
|
|
49
|
+
def __neg__(self) -> "Timeseries":
|
|
51
50
|
return self.__class__(self.times, -self.values)
|
|
52
51
|
|
|
53
52
|
def __repr__(self) -> str:
|
|
54
|
-
return
|
|
53
|
+
return "Timeseries({}, {})".format(self.__times, self.__values)
|
|
55
54
|
|
|
56
55
|
def __eq__(self, other: "Timeseries") -> bool:
|
|
57
|
-
return
|
|
58
|
-
np.array_equal(self.values, other.values))
|
|
56
|
+
return np.array_equal(self.times, other.times) and np.array_equal(self.values, other.values)
|
rtctools/rtctoolsapp.py
CHANGED
|
@@ -6,13 +6,12 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import rtctools
|
|
8
8
|
|
|
9
|
-
logging.basicConfig(format=
|
|
9
|
+
logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s")
|
|
10
10
|
logger = logging.getLogger("rtctools")
|
|
11
11
|
logger.setLevel(logging.INFO)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def copy_libraries(*args):
|
|
15
|
-
|
|
16
15
|
if not args:
|
|
17
16
|
args = sys.argv[1:]
|
|
18
17
|
|
|
@@ -38,25 +37,30 @@ def copy_libraries(*args):
|
|
|
38
37
|
else:
|
|
39
38
|
if not os.path.exists(d):
|
|
40
39
|
shutil.copy2(s, d)
|
|
41
|
-
elif Path(s).name.lower() ==
|
|
40
|
+
elif Path(s).name.lower() == "package.mo":
|
|
42
41
|
# Pick the largest one, assuming that all plugin packages
|
|
43
42
|
# to not provide a meaningful package.mo
|
|
44
43
|
if os.stat(s).st_size > os.stat(d).st_size:
|
|
45
|
-
logger.warning(
|
|
44
|
+
logger.warning(
|
|
45
|
+
"Overwriting '{}' with '{}' as the latter is larger.".format(d, s)
|
|
46
|
+
)
|
|
46
47
|
os.remove(d)
|
|
47
48
|
shutil.copy2(s, d)
|
|
48
49
|
else:
|
|
49
|
-
logger.warning(
|
|
50
|
+
logger.warning(
|
|
51
|
+
"Not copying '{}' to '{}' as the latter is larger.".format(s, d)
|
|
52
|
+
)
|
|
50
53
|
else:
|
|
51
54
|
raise OSError("Could not combine two folders")
|
|
52
55
|
|
|
53
56
|
dst = Path(path)
|
|
54
57
|
|
|
55
58
|
library_folders = []
|
|
56
|
-
for ep in pkg_resources.iter_entry_points(group=
|
|
59
|
+
for ep in pkg_resources.iter_entry_points(group="rtctools.libraries.modelica"):
|
|
57
60
|
if ep.name == "library_folder":
|
|
58
61
|
library_folders.append(
|
|
59
|
-
Path(pkg_resources.resource_filename(ep.module_name, ep.attrs[0]))
|
|
62
|
+
Path(pkg_resources.resource_filename(ep.module_name, ep.attrs[0]))
|
|
63
|
+
)
|
|
60
64
|
|
|
61
65
|
tlds = {}
|
|
62
66
|
for lf in library_folders:
|
|
@@ -78,7 +82,6 @@ def copy_libraries(*args):
|
|
|
78
82
|
|
|
79
83
|
|
|
80
84
|
def download_examples(*args):
|
|
81
|
-
|
|
82
85
|
if not args:
|
|
83
86
|
args = sys.argv[1:]
|
|
84
87
|
|
|
@@ -96,25 +99,22 @@ def download_examples(*args):
|
|
|
96
99
|
from urllib.error import HTTPError
|
|
97
100
|
from zipfile import ZipFile
|
|
98
101
|
|
|
99
|
-
# GitLab is blocking requests unless we specify a user agent
|
|
100
|
-
user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'
|
|
101
|
-
|
|
102
102
|
version = rtctools.__version__
|
|
103
|
-
rtc_full_name =
|
|
103
|
+
rtc_full_name = "rtc-tools-{}".format(version)
|
|
104
104
|
try:
|
|
105
|
-
url =
|
|
106
|
-
|
|
105
|
+
url = "https://gitlab.com/deltares/rtc-tools/-/archive/{}/{}.zip".format(
|
|
106
|
+
version, rtc_full_name
|
|
107
|
+
)
|
|
107
108
|
|
|
108
109
|
opener = urllib.request.build_opener()
|
|
109
|
-
opener.addheaders = [('User-agent', user_agent)]
|
|
110
110
|
urllib.request.install_opener(opener)
|
|
111
111
|
local_filename, _ = urllib.request.urlretrieve(url)
|
|
112
112
|
except HTTPError:
|
|
113
113
|
sys.exit("Could not found examples for RTC-Tools version {}.".format(version))
|
|
114
114
|
|
|
115
|
-
with ZipFile(local_filename,
|
|
116
|
-
target = path /
|
|
117
|
-
prefix =
|
|
115
|
+
with ZipFile(local_filename, "r") as z:
|
|
116
|
+
target = path / "rtc-tools-examples"
|
|
117
|
+
prefix = "{}/examples/".format(rtc_full_name)
|
|
118
118
|
members = [x for x in z.namelist() if x.startswith(prefix)]
|
|
119
119
|
z.extractall(members=members)
|
|
120
120
|
shutil.move(prefix, target)
|