rtc-tools 2.5.2rc3__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.2rc3.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.2rc3.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 +239 -148
- 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.2rc3.dist-info/RECORD +0 -49
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/COPYING.LESSER +0 -0
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/entry_points.txt +0 -0
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/top_level.txt +0 -0
|
@@ -6,7 +6,6 @@ from collections import OrderedDict
|
|
|
6
6
|
from typing import Callable, Dict, List, Union
|
|
7
7
|
|
|
8
8
|
import casadi as ca
|
|
9
|
-
|
|
10
9
|
import numpy as np
|
|
11
10
|
|
|
12
11
|
from .optimization_problem import OptimizationProblem
|
|
@@ -177,11 +176,14 @@ class Goal(metaclass=ABCMeta):
|
|
|
177
176
|
* The goal size determines how many goals there are.
|
|
178
177
|
* The goal function has shape ``(goal size, 1)``.
|
|
179
178
|
* The function is either minimized or has, possibly various, targets.
|
|
180
|
-
* Function nominal can either be an array with as many entries as the goal size or have a
|
|
181
|
-
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
* Function nominal can either be an array with as many entries as the goal size or have a
|
|
180
|
+
single value.
|
|
181
|
+
* Function ranges can either be an array with as many entries as the goal size or have a
|
|
182
|
+
single value.
|
|
183
|
+
* In a goal, the target can either be an array with as many entries as the goal size or
|
|
184
|
+
have a single value.
|
|
185
|
+
* In a path goal, the target can also be a Timeseries whose values are either a
|
|
186
|
+
1-dimensional vector or have as many columns as the goal size.
|
|
185
187
|
|
|
186
188
|
**Examples**
|
|
187
189
|
|
|
@@ -289,12 +291,16 @@ class Goal(metaclass=ABCMeta):
|
|
|
289
291
|
"""
|
|
290
292
|
``True`` if the user goal has min/max bounds.
|
|
291
293
|
"""
|
|
292
|
-
return
|
|
294
|
+
return self.has_target_min or self.has_target_max
|
|
293
295
|
|
|
294
296
|
@property
|
|
295
297
|
def is_empty(self) -> bool:
|
|
296
|
-
target_min_set = isinstance(self.target_min, Timeseries) or np.any(
|
|
297
|
-
|
|
298
|
+
target_min_set = isinstance(self.target_min, Timeseries) or np.any(
|
|
299
|
+
np.isfinite(self.target_min)
|
|
300
|
+
)
|
|
301
|
+
target_max_set = isinstance(self.target_max, Timeseries) or np.any(
|
|
302
|
+
np.isfinite(self.target_max)
|
|
303
|
+
)
|
|
298
304
|
|
|
299
305
|
if not target_min_set and not target_max_set:
|
|
300
306
|
# A minimization goal
|
|
@@ -313,25 +319,28 @@ class Goal(metaclass=ABCMeta):
|
|
|
313
319
|
|
|
314
320
|
return min_empty and max_empty
|
|
315
321
|
|
|
316
|
-
def get_function_key(
|
|
322
|
+
def get_function_key(
|
|
323
|
+
self, optimization_problem: OptimizationProblem, ensemble_member: int
|
|
324
|
+
) -> str:
|
|
317
325
|
"""
|
|
318
326
|
Returns a key string uniquely identifying the goal function. This
|
|
319
327
|
is used to eliminate linearly dependent constraints from the optimization problem.
|
|
320
328
|
"""
|
|
321
|
-
if hasattr(self,
|
|
329
|
+
if hasattr(self, "function_key"):
|
|
322
330
|
return self.function_key
|
|
323
331
|
|
|
324
332
|
# This must be deterministic. See RTCTOOLS-485.
|
|
325
|
-
if not hasattr(Goal,
|
|
333
|
+
if not hasattr(Goal, "_function_key_counter"):
|
|
326
334
|
Goal._function_key_counter = 0
|
|
327
|
-
self.function_key =
|
|
335
|
+
self.function_key = "{}_{}".format(self.__class__.__name__, Goal._function_key_counter)
|
|
328
336
|
Goal._function_key_counter += 1
|
|
329
337
|
|
|
330
338
|
return self.function_key
|
|
331
339
|
|
|
332
340
|
def __repr__(self) -> str:
|
|
333
|
-
return
|
|
334
|
-
self.__class__, self.priority, self.target_min, self.target_max, self.function_range
|
|
341
|
+
return "{}(priority={}, target_min={}, target_max={}, function_range={})".format(
|
|
342
|
+
self.__class__, self.priority, self.target_min, self.target_max, self.function_range
|
|
343
|
+
)
|
|
335
344
|
|
|
336
345
|
|
|
337
346
|
class StateGoal(Goal):
|
|
@@ -362,9 +371,9 @@ class StateGoal(Goal):
|
|
|
362
371
|
|
|
363
372
|
my_state_goal = MyStateGoal(optimization_problem)
|
|
364
373
|
|
|
365
|
-
Note that ``StateGoal`` is a helper class. State goals can also be defined using ``Goal`` as
|
|
366
|
-
by implementing the ``function`` method and providing the
|
|
367
|
-
class variables manually.
|
|
374
|
+
Note that ``StateGoal`` is a helper class. State goals can also be defined using ``Goal`` as
|
|
375
|
+
direct base class, by implementing the ``function`` method and providing the
|
|
376
|
+
``function_range`` and ``function_nominal`` class variables manually.
|
|
368
377
|
|
|
369
378
|
"""
|
|
370
379
|
|
|
@@ -380,45 +389,52 @@ class StateGoal(Goal):
|
|
|
380
389
|
|
|
381
390
|
# Check whether a state has been specified
|
|
382
391
|
if self.state is None:
|
|
383
|
-
raise Exception(
|
|
392
|
+
raise Exception("Please specify a state.")
|
|
384
393
|
|
|
385
394
|
# Extract state range from model
|
|
386
395
|
if self.has_target_bounds:
|
|
387
396
|
try:
|
|
388
397
|
self.function_range = optimization_problem.bounds()[self.state]
|
|
389
398
|
except KeyError:
|
|
390
|
-
raise Exception(
|
|
399
|
+
raise Exception(
|
|
400
|
+
"State {} has no bounds or does not exist in the model.".format(self.state)
|
|
401
|
+
)
|
|
391
402
|
|
|
392
403
|
if self.function_range[0] is None:
|
|
393
|
-
raise Exception(
|
|
404
|
+
raise Exception("Please provide a lower bound for state {}.".format(self.state))
|
|
394
405
|
if self.function_range[1] is None:
|
|
395
|
-
raise Exception(
|
|
406
|
+
raise Exception("Please provide an upper bound for state {}.".format(self.state))
|
|
396
407
|
|
|
397
408
|
# Extract state nominal from model
|
|
398
409
|
self.function_nominal = optimization_problem.variable_nominal(self.state)
|
|
399
410
|
|
|
400
411
|
# Set function key
|
|
401
412
|
canonical, sign = optimization_problem.alias_relation.canonical_signed(self.state)
|
|
402
|
-
self.function_key = canonical if sign > 0.0 else
|
|
413
|
+
self.function_key = canonical if sign > 0.0 else "-" + canonical
|
|
403
414
|
|
|
404
415
|
def function(self, optimization_problem, ensemble_member):
|
|
405
416
|
return optimization_problem.state(self.state)
|
|
406
417
|
|
|
407
418
|
def __repr__(self):
|
|
408
|
-
return
|
|
409
|
-
self.__class__,
|
|
419
|
+
return "{}(priority={}, state={}, target_min={}, target_max={}, function_range={})".format(
|
|
420
|
+
self.__class__,
|
|
421
|
+
self.priority,
|
|
422
|
+
self.state,
|
|
423
|
+
self.target_min,
|
|
424
|
+
self.target_max,
|
|
425
|
+
self.function_range,
|
|
426
|
+
)
|
|
410
427
|
|
|
411
428
|
|
|
412
429
|
class _GoalConstraint:
|
|
413
|
-
|
|
414
430
|
def __init__(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
431
|
+
self,
|
|
432
|
+
goal: Goal,
|
|
433
|
+
function: Callable[[OptimizationProblem], ca.MX],
|
|
434
|
+
m: Union[float, np.ndarray, Timeseries],
|
|
435
|
+
M: Union[float, np.ndarray, Timeseries],
|
|
436
|
+
optimized: bool,
|
|
437
|
+
):
|
|
422
438
|
assert isinstance(m, (float, np.ndarray, Timeseries))
|
|
423
439
|
assert isinstance(M, (float, np.ndarray, Timeseries))
|
|
424
440
|
assert type(m) == type(M)
|
|
@@ -434,7 +450,7 @@ class _GoalConstraint:
|
|
|
434
450
|
self.max = M
|
|
435
451
|
self.optimized = optimized
|
|
436
452
|
|
|
437
|
-
def update_bounds(self, other, enforce=
|
|
453
|
+
def update_bounds(self, other, enforce="self"):
|
|
438
454
|
# NOTE: a.update_bounds(b) is _not_ the same as b.update_bounds(a).
|
|
439
455
|
# See how the 'enforce' parameter is used.
|
|
440
456
|
|
|
@@ -456,7 +472,7 @@ class _GoalConstraint:
|
|
|
456
472
|
|
|
457
473
|
# Ensure new constraint bounds do not loosen or shift
|
|
458
474
|
# previous bounds due to numerical errors.
|
|
459
|
-
if enforce ==
|
|
475
|
+
if enforce == "self":
|
|
460
476
|
min_ = np.minimum(max_, other_min)
|
|
461
477
|
max_ = np.maximum(min_, other_max)
|
|
462
478
|
else:
|
|
@@ -476,16 +492,19 @@ class _GoalConstraint:
|
|
|
476
492
|
|
|
477
493
|
|
|
478
494
|
class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
479
|
-
|
|
480
495
|
def _gp_n_objectives(self, subproblem_objectives, subproblem_path_objectives, ensemble_member):
|
|
481
|
-
return
|
|
496
|
+
return (
|
|
497
|
+
ca.vertcat(*[o(self, ensemble_member) for o in subproblem_objectives]).size1()
|
|
482
498
|
+ ca.vertcat(*[o(self, ensemble_member) for o in subproblem_path_objectives]).size1()
|
|
499
|
+
)
|
|
483
500
|
|
|
484
501
|
def _gp_objective(self, subproblem_objectives, n_objectives, ensemble_member):
|
|
485
502
|
if len(subproblem_objectives) > 0:
|
|
486
|
-
acc_objective = ca.sum1(
|
|
503
|
+
acc_objective = ca.sum1(
|
|
504
|
+
ca.vertcat(*[o(self, ensemble_member) for o in subproblem_objectives])
|
|
505
|
+
)
|
|
487
506
|
|
|
488
|
-
if self.goal_programming_options()[
|
|
507
|
+
if self.goal_programming_options()["scale_by_problem_size"]:
|
|
489
508
|
acc_objective = acc_objective / n_objectives
|
|
490
509
|
|
|
491
510
|
return acc_objective
|
|
@@ -494,9 +513,11 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
494
513
|
|
|
495
514
|
def _gp_path_objective(self, subproblem_path_objectives, n_objectives, ensemble_member):
|
|
496
515
|
if len(subproblem_path_objectives) > 0:
|
|
497
|
-
acc_objective = ca.sum1(
|
|
516
|
+
acc_objective = ca.sum1(
|
|
517
|
+
ca.vertcat(*[o(self, ensemble_member) for o in subproblem_path_objectives])
|
|
518
|
+
)
|
|
498
519
|
|
|
499
|
-
if self.goal_programming_options()[
|
|
520
|
+
if self.goal_programming_options()["scale_by_problem_size"]:
|
|
500
521
|
# Objective is already divided by number of active time steps
|
|
501
522
|
# at this point when `scale_by_problem_size` is set.
|
|
502
523
|
acc_objective = acc_objective / n_objectives
|
|
@@ -540,8 +561,7 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
540
561
|
|
|
541
562
|
m, M = None, None
|
|
542
563
|
if isinstance(g.target_min, Timeseries):
|
|
543
|
-
m = self.interpolate(
|
|
544
|
-
times, g.target_min.times, g.target_min.values, -np.inf, -np.inf)
|
|
564
|
+
m = self.interpolate(times, g.target_min.times, g.target_min.values, -np.inf, -np.inf)
|
|
545
565
|
if m.ndim > 1:
|
|
546
566
|
m = m.transpose()
|
|
547
567
|
elif isinstance(g.target_min, np.ndarray) and target_shape:
|
|
@@ -551,8 +571,7 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
551
571
|
else:
|
|
552
572
|
m = np.array([g.target_min]).transpose()
|
|
553
573
|
if isinstance(g.target_max, Timeseries):
|
|
554
|
-
M = self.interpolate(
|
|
555
|
-
times, g.target_max.times, g.target_max.values, np.inf, np.inf)
|
|
574
|
+
M = self.interpolate(times, g.target_max.times, g.target_max.values, np.inf, np.inf)
|
|
556
575
|
if M.ndim > 1:
|
|
557
576
|
M = M.transpose()
|
|
558
577
|
elif isinstance(g.target_max, np.ndarray) and target_shape:
|
|
@@ -570,7 +589,7 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
570
589
|
if g.size > 1:
|
|
571
590
|
assert m.shape == (g.size, 1 if target_shape is None else target_shape)
|
|
572
591
|
else:
|
|
573
|
-
assert m.shape == (1 if target_shape is None else target_shape,
|
|
592
|
+
assert m.shape == (1 if target_shape is None else target_shape,)
|
|
574
593
|
assert m.shape == M.shape
|
|
575
594
|
|
|
576
595
|
return m, M
|
|
@@ -623,7 +642,9 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
623
642
|
raise Exception("Goal weight should be positive for goal {}".format(goal))
|
|
624
643
|
else:
|
|
625
644
|
if goal.function_range != (np.nan, np.nan):
|
|
626
|
-
raise Exception(
|
|
645
|
+
raise Exception(
|
|
646
|
+
"Specifying function range not allowed for goal {}".format(goal)
|
|
647
|
+
)
|
|
627
648
|
|
|
628
649
|
if not is_path_goal:
|
|
629
650
|
if isinstance(goal.target_min, Timeseries):
|
|
@@ -636,15 +657,25 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
636
657
|
except ValueError:
|
|
637
658
|
raise Exception("Priority of not int or castable to int for goal {}".format(goal))
|
|
638
659
|
|
|
639
|
-
if options[
|
|
660
|
+
if options["keep_soft_constraints"]:
|
|
640
661
|
if goal.relaxation != 0.0:
|
|
641
|
-
raise Exception(
|
|
662
|
+
raise Exception(
|
|
663
|
+
"Relaxation not allowed with `keep_soft_constraints` for goal {}".format(
|
|
664
|
+
goal
|
|
665
|
+
)
|
|
666
|
+
)
|
|
642
667
|
if goal.violation_timeseries_id is not None:
|
|
643
668
|
raise Exception(
|
|
644
|
-
"Violation timeseries id not allowed with
|
|
669
|
+
"Violation timeseries id not allowed with "
|
|
670
|
+
"`keep_soft_constraints` for goal {}".format(goal)
|
|
671
|
+
)
|
|
645
672
|
else:
|
|
646
673
|
if goal.size > 1:
|
|
647
|
-
raise Exception(
|
|
674
|
+
raise Exception(
|
|
675
|
+
"Option `keep_soft_constraints` needs to be set for vector goal {}".format(
|
|
676
|
+
goal
|
|
677
|
+
)
|
|
678
|
+
)
|
|
648
679
|
|
|
649
680
|
if goal.critical and goal.size > 1:
|
|
650
681
|
raise Exception("Vector goal cannot be critical for goal {}".format(goal))
|
|
@@ -657,7 +688,7 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
657
688
|
# Check consistency and monotonicity of goals. Scalar target min/max
|
|
658
689
|
# of normal goals are also converted to arrays to unify checks with
|
|
659
690
|
# path goals.
|
|
660
|
-
if options[
|
|
691
|
+
if options["check_monotonicity"]:
|
|
661
692
|
for e in range(self.ensemble_size):
|
|
662
693
|
# Store the previous goal of a certain function key we
|
|
663
694
|
# encountered, such that we can compare to it.
|
|
@@ -672,21 +703,25 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
672
703
|
goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape)
|
|
673
704
|
other_m, other_M = self._gp_min_max_arrays(prev, target_shape)
|
|
674
705
|
|
|
675
|
-
indices = np.where(
|
|
676
|
-
np.isnan(goal_m), np.isnan(other_m)))
|
|
706
|
+
indices = np.where(
|
|
707
|
+
np.logical_not(np.logical_or(np.isnan(goal_m), np.isnan(other_m)))
|
|
708
|
+
)
|
|
677
709
|
if goal.has_target_min:
|
|
678
710
|
if np.any(goal_m[indices] < other_m[indices]):
|
|
679
711
|
raise Exception(
|
|
680
|
-
|
|
681
|
-
|
|
712
|
+
"Target minimum of goal {} must be greater or equal than "
|
|
713
|
+
"target minimum of goal {}.".format(goal, prev)
|
|
714
|
+
)
|
|
682
715
|
|
|
683
|
-
indices = np.where(
|
|
684
|
-
np.isnan(goal_M), np.isnan(other_M)))
|
|
716
|
+
indices = np.where(
|
|
717
|
+
np.logical_not(np.logical_or(np.isnan(goal_M), np.isnan(other_M)))
|
|
718
|
+
)
|
|
685
719
|
if goal.has_target_max:
|
|
686
720
|
if np.any(goal_M[indices] > other_M[indices]):
|
|
687
721
|
raise Exception(
|
|
688
|
-
|
|
689
|
-
|
|
722
|
+
"Target maximum of goal {} must be less or equal than "
|
|
723
|
+
"target maximum of goal {}".format(goal, prev)
|
|
724
|
+
)
|
|
690
725
|
|
|
691
726
|
for goal in goals:
|
|
692
727
|
goal_m, goal_M = self._gp_min_max_arrays(goal, target_shape)
|
|
@@ -694,35 +729,42 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
694
729
|
goal_ub = np.broadcast_to(goal.function_range[1], goal_M.shape[::-1]).transpose()
|
|
695
730
|
|
|
696
731
|
if goal.has_target_min and goal.has_target_max:
|
|
697
|
-
indices = np.where(
|
|
698
|
-
np.isnan(goal_m), np.isnan(goal_M)))
|
|
732
|
+
indices = np.where(
|
|
733
|
+
np.logical_not(np.logical_or(np.isnan(goal_m), np.isnan(goal_M)))
|
|
734
|
+
)
|
|
699
735
|
|
|
700
736
|
if np.any(goal_m[indices] > goal_M[indices]):
|
|
701
|
-
raise Exception(
|
|
737
|
+
raise Exception(
|
|
738
|
+
"Target minimum exceeds target maximum for goal {}".format(goal)
|
|
739
|
+
)
|
|
702
740
|
|
|
703
741
|
if goal.has_target_min and not goal.critical:
|
|
704
742
|
indices = np.where(np.isfinite(goal_m))
|
|
705
743
|
if np.any(goal_m[indices] <= goal_lb[indices]):
|
|
706
744
|
raise Exception(
|
|
707
|
-
|
|
708
|
-
.format(goal)
|
|
745
|
+
"Target minimum should be greater than the lower bound "
|
|
746
|
+
"of the function range for goal {}".format(goal)
|
|
747
|
+
)
|
|
709
748
|
if np.any(goal_m[indices] > goal_ub[indices]):
|
|
710
749
|
raise Exception(
|
|
711
|
-
|
|
712
|
-
.format(goal)
|
|
750
|
+
"Target minimum should not be greater than the upper bound "
|
|
751
|
+
"of the function range for goal {}".format(goal)
|
|
752
|
+
)
|
|
713
753
|
if goal.has_target_max and not goal.critical:
|
|
714
754
|
indices = np.where(np.isfinite(goal_M))
|
|
715
755
|
if np.any(goal_M[indices] >= goal_ub[indices]):
|
|
716
756
|
raise Exception(
|
|
717
|
-
|
|
718
|
-
.format(goal)
|
|
757
|
+
"Target maximum should be smaller than the upper bound "
|
|
758
|
+
"of the function range for goal {}".format(goal)
|
|
759
|
+
)
|
|
719
760
|
if np.any(goal_M[indices] < goal_lb[indices]):
|
|
720
761
|
raise Exception(
|
|
721
|
-
|
|
722
|
-
.format(goal)
|
|
762
|
+
"Target maximum should not be smaller than the lower bound "
|
|
763
|
+
"of the function range for goal {}".format(goal)
|
|
764
|
+
)
|
|
723
765
|
|
|
724
766
|
if goal.relaxation < 0.0:
|
|
725
|
-
raise Exception(
|
|
767
|
+
raise Exception("Relaxation of goal {} should be a nonnegative value".format(goal))
|
|
726
768
|
|
|
727
769
|
def _gp_goal_constraints(self, goals, sym_index, options, is_path_goal):
|
|
728
770
|
"""
|
|
@@ -772,10 +814,14 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
772
814
|
|
|
773
815
|
if isinstance(goal.target_min, Timeseries):
|
|
774
816
|
target_min = Timeseries(goal.target_min.times, goal.target_min.values)
|
|
775
|
-
inds = np.logical_or(
|
|
817
|
+
inds = np.logical_or(
|
|
818
|
+
np.isnan(target_min.values), np.isneginf(target_min.values)
|
|
819
|
+
)
|
|
776
820
|
target_min.values[inds] = -sys.float_info.max
|
|
777
821
|
n_times = len(goal.target_min.times)
|
|
778
|
-
target_min_slice_inds = ~np.all(
|
|
822
|
+
target_min_slice_inds = ~np.all(
|
|
823
|
+
np.broadcast_to(inds.transpose(), (goal.size, n_times)), axis=1
|
|
824
|
+
)
|
|
779
825
|
elif isinstance(goal.target_min, np.ndarray):
|
|
780
826
|
target_min = goal.target_min.copy()
|
|
781
827
|
inds = np.logical_or(np.isnan(target_min), np.isneginf(target_min))
|
|
@@ -795,10 +841,14 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
795
841
|
|
|
796
842
|
if isinstance(goal.target_max, Timeseries):
|
|
797
843
|
target_max = Timeseries(goal.target_max.times, goal.target_max.values)
|
|
798
|
-
inds = np.logical_or(
|
|
844
|
+
inds = np.logical_or(
|
|
845
|
+
np.isnan(target_max.values), np.isposinf(target_max.values)
|
|
846
|
+
)
|
|
799
847
|
target_max.values[inds] = sys.float_info.max
|
|
800
848
|
n_times = len(goal.target_max.times)
|
|
801
|
-
target_max_slice_inds = ~np.all(
|
|
849
|
+
target_max_slice_inds = ~np.all(
|
|
850
|
+
np.broadcast_to(inds.transpose(), (goal.size, n_times)), axis=1
|
|
851
|
+
)
|
|
802
852
|
elif isinstance(goal.target_max, np.ndarray):
|
|
803
853
|
target_max = goal.target_max.copy()
|
|
804
854
|
inds = np.logical_or(np.isnan(target_max), np.isposinf(target_max))
|
|
@@ -813,11 +863,13 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
813
863
|
|
|
814
864
|
# Make objective for soft constraints and minimization goals
|
|
815
865
|
if not goal.critical:
|
|
816
|
-
if hasattr(goal,
|
|
866
|
+
if hasattr(goal, "_objective_func"):
|
|
817
867
|
_objective_func = goal._objective_func
|
|
818
868
|
elif goal.has_target_bounds:
|
|
819
|
-
if is_path_goal and options[
|
|
820
|
-
goal_m, goal_M = self._gp_min_max_arrays(
|
|
869
|
+
if is_path_goal and options["scale_by_problem_size"]:
|
|
870
|
+
goal_m, goal_M = self._gp_min_max_arrays(
|
|
871
|
+
goal, target_shape=len(self.times())
|
|
872
|
+
)
|
|
821
873
|
goal_active = np.isfinite(goal_m) | np.isfinite(goal_M)
|
|
822
874
|
n_active = np.sum(goal_active.astype(int), axis=-1)
|
|
823
875
|
# Avoid possible division by zero if goal is inactive
|
|
@@ -825,23 +877,34 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
825
877
|
else:
|
|
826
878
|
n_active = 1
|
|
827
879
|
|
|
828
|
-
def _objective_func(
|
|
829
|
-
|
|
830
|
-
|
|
880
|
+
def _objective_func(
|
|
881
|
+
problem,
|
|
882
|
+
ensemble_member,
|
|
883
|
+
goal=goal,
|
|
884
|
+
epsilon=epsilon,
|
|
885
|
+
is_path_goal=is_path_goal,
|
|
886
|
+
n_active=n_active,
|
|
887
|
+
):
|
|
831
888
|
if is_path_goal:
|
|
832
889
|
epsilon = problem.variable(epsilon.name())
|
|
833
890
|
else:
|
|
834
891
|
epsilon = problem.extra_variable(epsilon.name(), ensemble_member)
|
|
835
892
|
|
|
836
893
|
return goal.weight * ca.constpow(epsilon, goal.order) / n_active
|
|
894
|
+
|
|
837
895
|
else:
|
|
838
|
-
if is_path_goal and options[
|
|
896
|
+
if is_path_goal and options["scale_by_problem_size"]:
|
|
839
897
|
n_active = len(self.times())
|
|
840
898
|
else:
|
|
841
899
|
n_active = 1
|
|
842
900
|
|
|
843
|
-
def _objective_func(
|
|
844
|
-
|
|
901
|
+
def _objective_func(
|
|
902
|
+
problem,
|
|
903
|
+
ensemble_member,
|
|
904
|
+
goal=goal,
|
|
905
|
+
is_path_goal=is_path_goal,
|
|
906
|
+
n_active=n_active,
|
|
907
|
+
):
|
|
845
908
|
f = goal.function(problem, ensemble_member) / goal.function_nominal
|
|
846
909
|
return goal.weight * ca.constpow(f, goal.order) / n_active
|
|
847
910
|
|
|
@@ -852,15 +915,23 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
852
915
|
if goal.critical:
|
|
853
916
|
for ensemble_member in range(self.ensemble_size):
|
|
854
917
|
constraint = self._gp_goal_hard_constraint(
|
|
855
|
-
goal, epsilon, None, ensemble_member, options, is_path_goal
|
|
918
|
+
goal, epsilon, None, ensemble_member, options, is_path_goal
|
|
919
|
+
)
|
|
856
920
|
hard_constraints[ensemble_member].append(constraint)
|
|
857
921
|
else:
|
|
858
922
|
for ensemble_member in range(self.ensemble_size):
|
|
859
923
|
# We use a violation variable formulation, with the violation
|
|
860
924
|
# variables epsilon bounded between 0 and 1.
|
|
861
|
-
def _soft_constraint_func(
|
|
862
|
-
|
|
863
|
-
|
|
925
|
+
def _soft_constraint_func(
|
|
926
|
+
problem,
|
|
927
|
+
target,
|
|
928
|
+
bound,
|
|
929
|
+
inds,
|
|
930
|
+
goal=goal,
|
|
931
|
+
epsilon=epsilon,
|
|
932
|
+
ensemble_member=ensemble_member,
|
|
933
|
+
is_path_constraint=is_path_goal,
|
|
934
|
+
):
|
|
864
935
|
if is_path_constraint:
|
|
865
936
|
target = problem.variable(target)
|
|
866
937
|
eps = problem.variable(epsilon.name())
|
|
@@ -873,16 +944,19 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
873
944
|
f = goal.function(problem, ensemble_member)
|
|
874
945
|
nominal = goal.function_nominal
|
|
875
946
|
|
|
876
|
-
return ca.if_else(
|
|
877
|
-
|
|
878
|
-
|
|
947
|
+
return ca.if_else(
|
|
948
|
+
ca.fabs(target) < sys.float_info.max,
|
|
949
|
+
(f - eps * (bound - target) - target) / nominal,
|
|
950
|
+
0.0,
|
|
951
|
+
)[inds]
|
|
879
952
|
|
|
880
953
|
if goal.has_target_min and np.any(target_min_slice_inds):
|
|
881
954
|
_f = functools.partial(
|
|
882
955
|
_soft_constraint_func,
|
|
883
956
|
target=min_variable,
|
|
884
957
|
bound=goal.function_range[0],
|
|
885
|
-
inds=target_min_slice_inds
|
|
958
|
+
inds=target_min_slice_inds,
|
|
959
|
+
)
|
|
886
960
|
constraint = _GoalConstraint(goal, _f, 0.0, np.inf, False)
|
|
887
961
|
soft_constraints[ensemble_member].append(constraint)
|
|
888
962
|
if goal.has_target_max and np.any(target_max_slice_inds):
|
|
@@ -890,13 +964,16 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
890
964
|
_soft_constraint_func,
|
|
891
965
|
target=max_variable,
|
|
892
966
|
bound=goal.function_range[1],
|
|
893
|
-
inds=target_max_slice_inds
|
|
967
|
+
inds=target_max_slice_inds,
|
|
968
|
+
)
|
|
894
969
|
constraint = _GoalConstraint(goal, _f, -np.inf, 0.0, False)
|
|
895
970
|
soft_constraints[ensemble_member].append(constraint)
|
|
896
971
|
|
|
897
972
|
return epsilons, objectives, soft_constraints, hard_constraints, extra_constants
|
|
898
973
|
|
|
899
|
-
def _gp_goal_hard_constraint(
|
|
974
|
+
def _gp_goal_hard_constraint(
|
|
975
|
+
self, goal, epsilon, existing_constraint, ensemble_member, options, is_path_goal
|
|
976
|
+
):
|
|
900
977
|
if not is_path_goal:
|
|
901
978
|
epsilon = epsilon[:1]
|
|
902
979
|
|
|
@@ -905,21 +982,29 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
905
982
|
if goal.has_target_bounds:
|
|
906
983
|
# We use a violation variable formulation, with the violation
|
|
907
984
|
# variables epsilon bounded between 0 and 1.
|
|
908
|
-
m, M = np.full_like(epsilon, -np.inf, dtype=np.float64), np.full_like(
|
|
985
|
+
m, M = np.full_like(epsilon, -np.inf, dtype=np.float64), np.full_like(
|
|
986
|
+
epsilon, np.inf, dtype=np.float64
|
|
987
|
+
)
|
|
909
988
|
|
|
910
989
|
# A function range does not have to be specified for critical
|
|
911
990
|
# goals. Avoid multiplying with NaN in that case.
|
|
912
991
|
if goal.has_target_min:
|
|
913
|
-
m = (
|
|
914
|
-
|
|
992
|
+
m = (
|
|
993
|
+
epsilon * ((goal.function_range[0] - goal_m) if not goal.critical else 0.0)
|
|
994
|
+
+ goal_m
|
|
995
|
+
- goal.relaxation
|
|
996
|
+
) / goal.function_nominal
|
|
915
997
|
if goal.has_target_max:
|
|
916
|
-
M = (
|
|
917
|
-
|
|
998
|
+
M = (
|
|
999
|
+
epsilon * ((goal.function_range[1] - goal_M) if not goal.critical else 0.0)
|
|
1000
|
+
+ goal_M
|
|
1001
|
+
+ goal.relaxation
|
|
1002
|
+
) / goal.function_nominal
|
|
918
1003
|
|
|
919
1004
|
if goal.has_target_min and goal.has_target_max:
|
|
920
1005
|
# Avoid comparing with NaN
|
|
921
1006
|
inds = ~(np.isnan(m) | np.isnan(M))
|
|
922
|
-
inds[inds] &= np.abs(m[inds] - M[inds]) < options[
|
|
1007
|
+
inds[inds] &= np.abs(m[inds] - M[inds]) < options["equality_threshold"]
|
|
923
1008
|
if np.any(inds):
|
|
924
1009
|
avg = 0.5 * (m + M)
|
|
925
1010
|
m[inds] = M[inds] = avg[inds]
|
|
@@ -927,31 +1012,35 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
927
1012
|
m[~np.isfinite(goal_m)] = -np.inf
|
|
928
1013
|
M[~np.isfinite(goal_M)] = np.inf
|
|
929
1014
|
|
|
930
|
-
inds = epsilon > options[
|
|
1015
|
+
inds = epsilon > options["violation_tolerance"]
|
|
931
1016
|
if np.any(inds):
|
|
932
1017
|
if is_path_goal:
|
|
933
|
-
expr = self.map_path_expression(
|
|
1018
|
+
expr = self.map_path_expression(
|
|
1019
|
+
goal.function(self, ensemble_member), ensemble_member
|
|
1020
|
+
)
|
|
934
1021
|
else:
|
|
935
1022
|
expr = goal.function(self, ensemble_member)
|
|
936
1023
|
|
|
937
|
-
function = ca.Function(
|
|
1024
|
+
function = ca.Function("f", [self.solver_input], [expr])
|
|
938
1025
|
value = np.array(function(self.solver_output))
|
|
939
1026
|
|
|
940
1027
|
m[inds] = (value - goal.relaxation) / goal.function_nominal
|
|
941
1028
|
M[inds] = (value + goal.relaxation) / goal.function_nominal
|
|
942
1029
|
|
|
943
|
-
m -= options[
|
|
944
|
-
M += options[
|
|
1030
|
+
m -= options["constraint_relaxation"]
|
|
1031
|
+
M += options["constraint_relaxation"]
|
|
945
1032
|
else:
|
|
946
1033
|
# Epsilon encodes the position within the function range.
|
|
947
|
-
if options[
|
|
1034
|
+
if options["fix_minimized_values"] and goal.relaxation == 0.0:
|
|
948
1035
|
m = epsilon / goal.function_nominal
|
|
949
1036
|
M = epsilon / goal.function_nominal
|
|
950
1037
|
self.check_collocation_linearity = False
|
|
951
1038
|
self.linear_collocation = False
|
|
952
1039
|
else:
|
|
953
1040
|
m = -np.inf * np.ones(epsilon.shape)
|
|
954
|
-
M = (epsilon + goal.relaxation) / goal.function_nominal + options[
|
|
1041
|
+
M = (epsilon + goal.relaxation) / goal.function_nominal + options[
|
|
1042
|
+
"constraint_relaxation"
|
|
1043
|
+
]
|
|
955
1044
|
|
|
956
1045
|
if is_path_goal:
|
|
957
1046
|
m = Timeseries(self.times(), m)
|
|
@@ -963,13 +1052,17 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
|
|
|
963
1052
|
constraint = _GoalConstraint(
|
|
964
1053
|
goal,
|
|
965
1054
|
lambda problem, ensemble_member=ensemble_member, goal=goal: (
|
|
966
|
-
goal.function(problem, ensemble_member) / goal.function_nominal
|
|
967
|
-
|
|
1055
|
+
goal.function(problem, ensemble_member) / goal.function_nominal
|
|
1056
|
+
),
|
|
1057
|
+
m,
|
|
1058
|
+
M,
|
|
1059
|
+
True,
|
|
1060
|
+
)
|
|
968
1061
|
|
|
969
1062
|
# Epsilon is fixed. Override previous {min,max} constraints for this
|
|
970
1063
|
# state.
|
|
971
1064
|
if existing_constraint:
|
|
972
|
-
constraint.update_bounds(existing_constraint, enforce=
|
|
1065
|
+
constraint.update_bounds(existing_constraint, enforce="other")
|
|
973
1066
|
|
|
974
1067
|
return constraint
|
|
975
1068
|
|