rtc-tools 2.7.2__tar.gz → 2.7.3__tar.gz

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.
Files changed (59) hide show
  1. {rtc_tools-2.7.2/src/rtc_tools.egg-info → rtc_tools-2.7.3}/PKG-INFO +1 -1
  2. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/README.md +6 -0
  3. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/pyproject.toml +1 -0
  4. {rtc_tools-2.7.2 → rtc_tools-2.7.3/src/rtc_tools.egg-info}/PKG-INFO +1 -1
  5. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/_internal/casadi_helpers.py +39 -3
  6. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/_version.py +3 -3
  7. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/collocated_integrated_optimization_problem.py +185 -131
  8. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/modelica_mixin.py +12 -5
  9. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/optimization_problem.py +3 -0
  10. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/single_pass_goal_programming_mixin.py +20 -1
  11. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/COPYING.LESSER +0 -0
  12. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/MANIFEST.in +0 -0
  13. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/setup.cfg +0 -0
  14. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/setup.py +0 -0
  15. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtc_tools.egg-info/SOURCES.txt +0 -0
  16. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtc_tools.egg-info/dependency_links.txt +0 -0
  17. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtc_tools.egg-info/entry_points.txt +0 -0
  18. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtc_tools.egg-info/requires.txt +0 -0
  19. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtc_tools.egg-info/top_level.txt +0 -0
  20. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/__init__.py +0 -0
  21. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/_internal/__init__.py +0 -0
  22. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/_internal/alias_tools.py +0 -0
  23. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/_internal/caching.py +0 -0
  24. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/_internal/debug_check_helpers.py +0 -0
  25. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/__init__.py +0 -0
  26. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/csv.py +0 -0
  27. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/interpolation/__init__.py +0 -0
  28. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/interpolation/bspline.py +0 -0
  29. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/interpolation/bspline1d.py +0 -0
  30. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/interpolation/bspline2d.py +0 -0
  31. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/netcdf.py +0 -0
  32. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/pi.py +0 -0
  33. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/rtc.py +0 -0
  34. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/data/storage.py +0 -0
  35. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/__init__.py +0 -0
  36. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/control_tree_mixin.py +0 -0
  37. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/csv_lookup_table_mixin.py +0 -0
  38. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/csv_mixin.py +0 -0
  39. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/goal_programming_mixin.py +0 -0
  40. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/goal_programming_mixin_base.py +0 -0
  41. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/homotopy_mixin.py +0 -0
  42. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/initial_state_estimation_mixin.py +0 -0
  43. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/io_mixin.py +0 -0
  44. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/linearization_mixin.py +0 -0
  45. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/linearized_order_goal_programming_mixin.py +0 -0
  46. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/min_abs_goal_programming_mixin.py +0 -0
  47. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/netcdf_mixin.py +0 -0
  48. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/pi_mixin.py +0 -0
  49. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/planning_mixin.py +0 -0
  50. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/optimization/timeseries.py +0 -0
  51. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/rtctoolsapp.py +0 -0
  52. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/simulation/__init__.py +0 -0
  53. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/simulation/csv_mixin.py +0 -0
  54. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/simulation/io_mixin.py +0 -0
  55. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/simulation/pi_mixin.py +0 -0
  56. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/simulation/simulation_problem.py +0 -0
  57. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/src/rtctools/util.py +0 -0
  58. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/tests/test_case.py +0 -0
  59. {rtc_tools-2.7.2 → rtc_tools-2.7.3}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rtc-tools
3
- Version: 2.7.2
3
+ Version: 2.7.3
4
4
  Summary: Toolbox for control and optimization of water systems.
5
5
  Home-page: https://oss.deltares.nl/web/rtc-tools/home
6
6
  Download-URL: http://github.com/deltares/rtc-tools/
@@ -50,6 +50,12 @@ pip install rtc-tools
50
50
 
51
51
  Documentation and examples can be found on [readthedocs](https://rtc-tools.readthedocs.io).
52
52
 
53
+ ## Contributing
54
+
55
+ We welcome contributions to RTC-Tools! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to get started, coding standards, and our development process.
56
+
57
+ The project is governed according to our [Technical Charter](CHARTER.md) and [Governance Model](GOVERNANCE.md).
58
+
53
59
 
54
60
  ## Migration from GitLab
55
61
 
@@ -10,6 +10,7 @@ exclude = '''
10
10
  [tool.ruff]
11
11
  lint.ignore = [
12
12
  "B904", # Fix someday: raising exceptions within except
13
+ "B905", # zip() without explicit strict parameter - not available in Python 3.9
13
14
  ]
14
15
  line-length = 100
15
16
  lint.select = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rtc-tools
3
- Version: 2.7.2
3
+ Version: 2.7.3
4
4
  Summary: Toolbox for control and optimization of water systems.
5
5
  Home-page: https://oss.deltares.nl/web/rtc-tools/home
6
6
  Download-URL: http://github.com/deltares/rtc-tools/
@@ -1,9 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
 
3
5
  import casadi as ca
4
6
 
5
7
  logger = logging.getLogger("rtctools")
6
8
 
9
+ # Limit the number of times we try to substitute in external functions, e.g. in
10
+ # case of infinite recursion. Generally unlikely that we will hit this limit for
11
+ # any reasonable use case.
12
+ MAX_SUBSTITUTE_DEPTH = 10
13
+
7
14
 
8
15
  def is_affine(expr, symbols):
9
16
  try:
@@ -37,12 +44,41 @@ def reduce_matvec(e, v):
37
44
  return ca.reshape(ca.mtimes(A, v), e.shape)
38
45
 
39
46
 
40
- def substitute_in_external(expr, symbols, values):
41
- if len(symbols) == 0 or all(isinstance(x, ca.DM) for x in expr):
47
+ def substitute_in_external(
48
+ expr: list[ca.MX],
49
+ symbols: list[ca.MX],
50
+ values: list[ca.MX | ca.DM | float],
51
+ /,
52
+ resolve_numerically=False,
53
+ ):
54
+ # We expect expr to be a list of (at most) length 1
55
+ assert len(expr) <= 1
56
+
57
+ if not expr or len(symbols) == 0 or all(isinstance(x, ca.DM) for x in expr):
42
58
  return expr
43
- else:
59
+ elif not resolve_numerically:
44
60
  f = ca.Function("f", symbols, expr)
45
61
  return f.call(values, True, False)
62
+ else:
63
+ # CasADi < 3.7 workaround: f.call() with MX values returns wrapped
64
+ # results like f(...){0}. Resolve symbolics with ca.substitute(), and
65
+ # convert resulting MX constants to floats first. Remove when dropping
66
+ # support for CasADi 3.6.x.
67
+ resolved_values = list(values)
68
+ for _ in range(MAX_SUBSTITUTE_DEPTH):
69
+ for i, v in enumerate(resolved_values):
70
+ if isinstance(v, ca.MX) and not v.is_constant():
71
+ resolved_values[i] = ca.substitute([v], symbols, resolved_values)[0]
72
+ elif isinstance(v, ca.MX):
73
+ resolved_values[i] = float(v)
74
+
75
+ # Substitute in expression using f.call() for external function support
76
+ f = ca.Function("f", symbols, expr).expand()
77
+ expr = f.call(resolved_values, True, False)
78
+ if expr[0].is_constant():
79
+ break
80
+
81
+ return expr
46
82
 
47
83
 
48
84
  def interpolate(ts, xs, t, equidistant, mode=0):
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-11-25T14:13:12+0100",
11
+ "date": "2026-01-20T14:58:22+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "280b3e8717728329dcbec06fd57fd2e61d5dadbf",
15
- "version": "2.7.2"
14
+ "full-revisionid": "20585c7d4d1b615468ea7f7dab44a240aeb1f05c",
15
+ "version": "2.7.3"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,11 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import itertools
2
4
  import logging
3
5
  import warnings
4
6
  from abc import ABCMeta, abstractmethod
5
- from typing import Dict, Union
6
7
 
7
8
  import casadi as ca
8
9
  import numpy as np
10
+ from numpy.typing import NDArray
9
11
 
10
12
  from rtctools._internal.alias_tools import AliasDict
11
13
  from rtctools._internal.casadi_helpers import (
@@ -17,7 +19,7 @@ from rtctools._internal.casadi_helpers import (
17
19
  )
18
20
  from rtctools._internal.debug_check_helpers import DebugLevel, debug_check
19
21
 
20
- from .optimization_problem import OptimizationProblem
22
+ from .optimization_problem import BT, OptimizationProblem
21
23
  from .timeseries import Timeseries
22
24
 
23
25
  logger = logging.getLogger("rtctools")
@@ -44,11 +46,17 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
44
46
 
45
47
  :cvar check_collocation_linearity:
46
48
  If ``True``, check whether collocation constraints are linear. Default is ``True``.
49
+ :cvar inline_delay_expressions:
50
+ If ``True``, delay expressions are inlined into constraints and objectives instead of
51
+ being added as separate equality constraints. Default is ``False``.
47
52
  """
48
53
 
49
54
  #: Check whether the collocation constraints are linear
50
55
  check_collocation_linearity = True
51
56
 
57
+ #: Inline delay expressions instead of adding them as separate equality constraints
58
+ inline_delay_expressions = False
59
+
52
60
  #: Whether or not the collocation constraints are linear (affine)
53
61
  linear_collocation = None
54
62
 
@@ -172,7 +180,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
172
180
  # algebraic parts. Theta then only applies to the ODE part.
173
181
  return 1.0
174
182
 
175
- def map_options(self) -> Dict[str, Union[str, int]]:
183
+ def map_options(self) -> dict[str, str | int]:
176
184
  """
177
185
  Returns a dictionary of CasADi ``map()`` options.
178
186
 
@@ -312,7 +320,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
312
320
  for variable in self.__integrated_states:
313
321
  if self.__variable_sizes.get(variable, 1) > 1:
314
322
  raise NotImplementedError(
315
- "Vector symbol not supported for integrated state '{}'".format(variable)
323
+ f"Vector symbol not supported for integrated state '{variable}'"
316
324
  )
317
325
  else:
318
326
  self.__integrated_states = []
@@ -321,7 +329,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
321
329
  for variable in self.controls:
322
330
  if self.__variable_sizes.get(variable, 1) > 1:
323
331
  raise NotImplementedError(
324
- "Vector symbol not supported for control state '{}'".format(variable)
332
+ f"Vector symbol not supported for control state '{variable}'"
325
333
  )
326
334
 
327
335
  # Collocation times
@@ -342,7 +350,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
342
350
  try:
343
351
  constant_input = raw_constant_inputs[variable]
344
352
  except KeyError:
345
- raise Exception("No values found for constant input {}".format(variable))
353
+ raise Exception(f"No values found for constant input {variable}")
346
354
  else:
347
355
  values = constant_input.values
348
356
  interpolation_method = self.interpolation_method(variable)
@@ -371,7 +379,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
371
379
  try:
372
380
  parameter_values[i] = parameters[variable]
373
381
  except KeyError:
374
- raise Exception("No value specified for parameter {}".format(variable))
382
+ raise Exception(f"No value specified for parameter {variable}")
375
383
 
376
384
  if len(dynamic_parameters) > 0:
377
385
  jac_1 = ca.jacobian(symbolic_parameters, ca.vertcat(*dynamic_parameters))
@@ -385,7 +393,10 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
385
393
  ):
386
394
  parameter_values = nullvertcat(*parameter_values)
387
395
  [parameter_values] = substitute_in_external(
388
- [parameter_values], self.dae_variables["parameters"], parameter_values
396
+ [parameter_values],
397
+ self.dae_variables["parameters"],
398
+ ca.vertsplit(parameter_values),
399
+ resolve_numerically=False,
389
400
  )
390
401
  else:
391
402
  parameter_values = nullvertcat(*parameter_values)
@@ -410,7 +421,10 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
410
421
  if v.ndim == 1:
411
422
  ensemble_data["extra_constant_inputs"][k] = v[:, None]
412
423
 
413
- bounds = self.bounds()
424
+ if self.ensemble_specific_bounds:
425
+ bounds = [self.bounds(ensemble_member=i) for i in range(self.ensemble_size)]
426
+ else:
427
+ bounds = self.bounds()
414
428
 
415
429
  # Initialize control discretization
416
430
  (
@@ -494,8 +508,8 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
494
508
  collocated_variables += self.dae_variables["control_inputs"]
495
509
 
496
510
  if logger.getEffectiveLevel() == logging.DEBUG:
497
- logger.debug("Integrating variables {}".format(repr(integrated_variables)))
498
- logger.debug("Collocating variables {}".format(repr(collocated_variables)))
511
+ logger.debug(f"Integrating variables {repr(integrated_variables)}")
512
+ logger.debug(f"Collocating variables {repr(collocated_variables)}")
499
513
 
500
514
  integrated_variable_names = [v.name() for v in integrated_variables]
501
515
  integrated_variable_nominals = np.array(
@@ -517,14 +531,14 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
517
531
  collocated_derivatives = self.dae_variables["derivatives"][:]
518
532
  self.__algebraic_and_control_derivatives = []
519
533
  for var in self.dae_variables["algebraics"]:
520
- sym = ca.MX.sym("der({})".format(var.name()))
534
+ sym = ca.MX.sym(f"der({var.name()})")
521
535
  self.__algebraic_and_control_derivatives.append(sym)
522
536
  if self.integrate_states:
523
537
  integrated_derivatives.append(sym)
524
538
  else:
525
539
  collocated_derivatives.append(sym)
526
540
  for var in self.dae_variables["control_inputs"]:
527
- sym = ca.MX.sym("der({})".format(var.name()))
541
+ sym = ca.MX.sym(f"der({var.name()})")
528
542
  self.__algebraic_and_control_derivatives.append(sym)
529
543
  collocated_derivatives.append(sym)
530
544
 
@@ -758,7 +772,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
758
772
  try:
759
773
  lookup_table = lookup_tables[sym_name]
760
774
  except KeyError:
761
- raise Exception("Unable to find lookup table function for {}".format(sym_name))
775
+ raise Exception(f"Unable to find lookup table function for {sym_name}")
762
776
  else:
763
777
  input_syms = [
764
778
  self.variable(input_sym.name()) for input_sym in lookup_table.inputs
@@ -823,9 +837,9 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
823
837
  if self.integrate_states:
824
838
  I = ca.MX.sym("I", len(integrated_variables)) # noqa: E741
825
839
  I0 = ca.MX.sym("I0", len(integrated_variables))
826
- C0 = [ca.MX.sym("C0[{}]".format(i)) for i in range(len(collocated_variables))]
840
+ C0 = [ca.MX.sym(f"C0[{i}]") for i in range(len(collocated_variables))]
827
841
  CI0 = [
828
- ca.MX.sym("CI0[{}]".format(i))
842
+ ca.MX.sym(f"CI0[{i}]")
829
843
  for i in range(len(self.dae_variables["constant_inputs"]))
830
844
  ]
831
845
  dt_sym = ca.MX.sym("dt")
@@ -1243,6 +1257,12 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1243
1257
  # Integrators are saved for result extraction later on
1244
1258
  self.__integrators = []
1245
1259
 
1260
+ # If inlining delay expressions, prepare for the single call to ca.substitute()
1261
+ # at the end
1262
+ if self.inline_delay_expressions:
1263
+ delayed_feedback_variable_replacement = ca.MX.zeros(X.numel())
1264
+ delayed_feedback_variable_replacement[:] = X
1265
+
1246
1266
  # Process the objectives and constraints for each ensemble member separately.
1247
1267
  # Note that we don't use map here for the moment, so as to allow each ensemble member to
1248
1268
  # define its own constraints and objectives. Path constraints are applied for all ensemble
@@ -1250,9 +1270,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1250
1270
  # ensemble member to specify its own path constraints as well, once CasADi has some kind
1251
1271
  # of loop detection.
1252
1272
  for ensemble_member in range(self.ensemble_size):
1253
- logger.info(
1254
- "Transcribing ensemble member {}/{}".format(ensemble_member + 1, self.ensemble_size)
1255
- )
1273
+ logger.info(f"Transcribing ensemble member {ensemble_member + 1}/{self.ensemble_size}")
1256
1274
 
1257
1275
  initial_state = ensemble_aggregate["initial_state"][:, ensemble_member]
1258
1276
  initial_derivatives = ensemble_aggregate["initial_derivatives"][:, ensemble_member]
@@ -1299,9 +1317,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1299
1317
 
1300
1318
  if val < lbx[idx] or val > ubx[idx]:
1301
1319
  logger.warning(
1302
- "Initial value {} for variable '{}' outside bounds.".format(
1303
- val, variable
1304
- )
1320
+ f"Initial value {val} for variable '{variable}' outside bounds."
1305
1321
  )
1306
1322
 
1307
1323
  lbx[idx] = ubx[idx] = val
@@ -1627,13 +1643,13 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1627
1643
  states_and_algebraics_and_controls,
1628
1644
  states_and_algebraics_and_controls_derivatives,
1629
1645
  *[
1630
- ca.horzcat(*constant_inputs[variable][1:])
1646
+ ca.MX(constant_inputs[variable][1:]).T
1631
1647
  for variable in dae_constant_inputs_names
1632
1648
  ],
1633
- ca.horzcat(*collocation_times[1:]),
1649
+ ca.MX(collocation_times[1:]).T,
1634
1650
  path_variables.T if path_variables.numel() > 0 else ca.MX(),
1635
1651
  *[
1636
- ca.horzcat(*extra_constant_inputs[variable][1:])
1652
+ ca.MX(extra_constant_inputs[variable][1:]).T
1637
1653
  for (variable, _) in extra_constant_inputs_name_and_size
1638
1654
  ],
1639
1655
  ),
@@ -1826,8 +1842,8 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1826
1842
  np.isnan(delayed_feedback_history[hist_start_ind:, i])
1827
1843
  ):
1828
1844
  logger.warning(
1829
- "Incomplete history for delayed expression {}. "
1830
- "Extrapolating t0 value backwards in time.".format(expression)
1845
+ f"Incomplete history for delayed expression {expression}. "
1846
+ "Extrapolating t0 value backwards in time."
1831
1847
  )
1832
1848
  out_times = out_times[len(history_times) :]
1833
1849
  out_values = out_values[len(history_times) :]
@@ -1851,10 +1867,32 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1851
1867
 
1852
1868
  nominal = nominal_delayed_feedback[i]
1853
1869
 
1854
- g.append((x_in - x_out_delayed) / nominal)
1855
- zeros = np.zeros(n_collocation_times)
1856
- lbg.extend(zeros)
1857
- ubg.extend(zeros)
1870
+ if self.inline_delay_expressions:
1871
+ # Get the indices for the delayed feedback variable in the optimization
1872
+ # vector
1873
+ indices = self.__indices[ensemble_member][in_canonical]
1874
+
1875
+ # Insert replacement values into appropriate slot
1876
+ delayed_feedback_variable_replacement[indices] = (
1877
+ x_out_delayed / in_nominal / in_sign
1878
+ )
1879
+
1880
+ # Equate delayed feedback variable to zero so that the numerical solver can
1881
+ # remove it from the problem.
1882
+ #
1883
+ # Note: The alternative approach would be to shrink self.solver_input=X,
1884
+ # self.__indices, and any other variables that directly or indirectly
1885
+ # depend on the size and order of 'X'. This would be a complex operation,
1886
+ # which would however be redundant for solvers that (like Ipopt) already
1887
+ # automatically detect and remove variables where lbx==ubx.
1888
+ lbx[indices] = 0.0
1889
+ ubx[indices] = 0.0
1890
+ else:
1891
+ # Default behavior: add delay expressions as equality constraints
1892
+ g.append((x_in - x_out_delayed) / nominal)
1893
+ zeros = np.zeros(n_collocation_times)
1894
+ lbg.extend(zeros)
1895
+ ubg.extend(zeros)
1858
1896
 
1859
1897
  # Objective
1860
1898
  f_member = self.objective(ensemble_member)
@@ -1868,7 +1906,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1868
1906
  f.append(self.ensemble_member_probability(ensemble_member) * f_member)
1869
1907
 
1870
1908
  if logger.getEffectiveLevel() == logging.DEBUG:
1871
- logger.debug("Adding objective {}".format(f_member))
1909
+ logger.debug(f"Adding objective {f_member}")
1872
1910
 
1873
1911
  # Constraints
1874
1912
  constraints = self.constraints(ensemble_member)
@@ -1898,9 +1936,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1898
1936
  elif lbg_i.shape[0] != g_i.shape[0]:
1899
1937
  raise Exception(
1900
1938
  "Shape mismatch between constraint "
1901
- "#{} ({},) and its lower bound ({},)".format(
1902
- i, g_i.shape[0], lbg_i.shape[0]
1903
- )
1939
+ f"#{i} ({g_i.shape[0]},) and its lower bound ({lbg_i.shape[0]},)"
1904
1940
  )
1905
1941
 
1906
1942
  if not isinstance(ubg_i, np.ndarray) or ubg_i.shape[0] == 1:
@@ -1908,9 +1944,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1908
1944
  elif ubg_i.shape[0] != g_i.shape[0]:
1909
1945
  raise Exception(
1910
1946
  "Shape mismatch between constraint "
1911
- "#{} ({},) and its upper bound ({},)".format(
1912
- i, g_i.shape[0], ubg_i.shape[0]
1913
- )
1947
+ f"#{i} ({g_i.shape[0]},) and its upper bound ({ubg_i.shape[0]},)"
1914
1948
  )
1915
1949
 
1916
1950
  g.extend(g_constraint)
@@ -1978,6 +2012,14 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
1978
2012
  lbg.extend(lbg_path_constraints.transpose().ravel())
1979
2013
  ubg.extend(ubg_path_constraints.transpose().ravel())
1980
2014
 
2015
+ # If inlining delay expressions, carry out the single call to ca.substitute()
2016
+ # A single call is more efficient as it constructs a single ca.Function internally,
2017
+ # rather than one for each element of the list. Also note that we do this _outside_
2018
+ # the ensemble member loop for speed reasons, as each substitute() roughly takes the
2019
+ # same amount of time regardless how many expressions we are replacing.
2020
+ if self.inline_delay_expressions:
2021
+ g = [ca.substitute(ca.vertcat(*g), X, delayed_feedback_variable_replacement)]
2022
+
1981
2023
  # NLP function
1982
2024
  logger.info("Creating NLP dictionary")
1983
2025
 
@@ -2021,7 +2063,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2021
2063
  return self.__solver_input
2022
2064
 
2023
2065
  def solver_options(self):
2024
- options = super(CollocatedIntegratedOptimizationProblem, self).solver_options()
2066
+ options = super().solver_options()
2025
2067
 
2026
2068
  solver = options["solver"]
2027
2069
  assert solver in ["bonmin", "ipopt"]
@@ -2043,7 +2085,12 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2043
2085
  def controls(self):
2044
2086
  return self.__controls
2045
2087
 
2046
- def _collint_get_lbx_ubx(self, bounds, count, indices):
2088
+ def _collint_get_lbx_ubx(
2089
+ self,
2090
+ bounds: dict[str, BT] | list[dict[str, BT]],
2091
+ count: int,
2092
+ indices: list[dict[str, slice | int]],
2093
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
2047
2094
  lbx = np.full(count, -np.inf, dtype=np.float64)
2048
2095
  ubx = np.full(count, np.inf, dtype=np.float64)
2049
2096
 
@@ -2054,6 +2101,11 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2054
2101
 
2055
2102
  # Bounds, defaulting to +/- inf, if not set
2056
2103
  for ensemble_member in range(self.ensemble_size):
2104
+ if self.ensemble_specific_bounds:
2105
+ bounds_member = bounds[ensemble_member]
2106
+ else:
2107
+ bounds_member = bounds
2108
+
2057
2109
  for variable, inds in indices[ensemble_member].items():
2058
2110
  variable_size = variable_sizes[variable]
2059
2111
 
@@ -2065,7 +2117,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2065
2117
  n_times = len(times)
2066
2118
 
2067
2119
  try:
2068
- bound = bounds[variable]
2120
+ bound = bounds_member[variable]
2069
2121
  except KeyError:
2070
2122
  pass
2071
2123
  else:
@@ -2094,7 +2146,7 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2094
2146
  )
2095
2147
  else:
2096
2148
  lower_bound = bound[0]
2097
- lbx[inds] = lower_bound / nominal
2149
+ lbx[inds] = np.maximum(lbx[inds], lower_bound / nominal)
2098
2150
 
2099
2151
  if bound[1] is not None:
2100
2152
  if isinstance(bound[1], Timeseries):
@@ -2114,13 +2166,23 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2114
2166
  )
2115
2167
  else:
2116
2168
  upper_bound = bound[1]
2117
- ubx[inds] = upper_bound / nominal
2169
+ ubx[inds] = np.minimum(ubx[inds], upper_bound / nominal)
2118
2170
 
2119
2171
  # Warn for NaNs
2120
2172
  if np.any(np.isnan(lbx[inds])):
2121
- logger.error("Lower bound on variable {} contains NaN".format(variable))
2173
+ logger.error(f"Lower bound on variable {variable} contains NaN")
2122
2174
  if np.any(np.isnan(ubx[inds])):
2123
- logger.error("Upper bound on variable {} contains NaN".format(variable))
2175
+ logger.error(f"Upper bound on variable {variable} contains NaN")
2176
+
2177
+ # Check that the lower bounds are not higher than the upper
2178
+ # bounds. To avoid spam, we just log the first offending one per
2179
+ # variable, not _all_ time steps.
2180
+ if np.any(lbx[inds] > ubx[inds]):
2181
+ error_inds = np.where(lbx[inds] > ubx[inds])[0].tolist()
2182
+ logger.error(
2183
+ f"Lower bound {lbx[inds][error_inds[0]] * nominal} is higher than "
2184
+ f"upper bound {ubx[inds][error_inds[0]] * nominal} for variable {variable}"
2185
+ )
2124
2186
 
2125
2187
  return lbx, ubx
2126
2188
 
@@ -2154,15 +2216,16 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2154
2216
  )
2155
2217
 
2156
2218
  if isinstance(seed_k, Timeseries):
2157
- x0[inds] = (
2219
+ seed_k = (
2158
2220
  self.interpolate(
2159
2221
  times, seed_k.times, seed_k.values, 0, 0, interpolation_method
2160
2222
  )
2161
2223
  .transpose()
2162
2224
  .ravel()
2163
2225
  )
2164
- else:
2165
- x0[inds] = seed_k
2226
+ if isinstance(inds, int) and isinstance(seed_k, np.ndarray):
2227
+ seed_k = seed_k.item()
2228
+ x0[inds] = seed_k
2166
2229
 
2167
2230
  x0[inds] /= nominal
2168
2231
  except KeyError:
@@ -2492,10 +2555,8 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2492
2555
  else:
2493
2556
  if not extrapolate and (t < times[0] or t > times[-1]):
2494
2557
  raise Exception(
2495
- "Cannot interpolate for {}: "
2496
- "Point {} outside of range [{}, {}]".format(
2497
- canonical, t, times[0], times[-1]
2498
- )
2558
+ f"Cannot interpolate for {canonical}: "
2559
+ f"Point {t} outside of range [{times[0]}, {times[-1]}]"
2499
2560
  )
2500
2561
 
2501
2562
  interpolation_method = self.interpolation_method(canonical)
@@ -2583,9 +2644,8 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2583
2644
  history_timeseries = history[canonical]
2584
2645
  except KeyError:
2585
2646
  raise Exception(
2586
- "No history found for variable {}, but a historical value was requested".format(
2587
- variable
2588
- )
2647
+ f"No history found for variable {variable}, "
2648
+ "but a historical value was requested"
2589
2649
  )
2590
2650
  else:
2591
2651
  history_times = history_timeseries.times[:-1]
@@ -2612,10 +2672,21 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2612
2672
 
2613
2673
  return x, t
2614
2674
 
2615
- def states_in(self, variable, t0=None, tf=None, ensemble_member=0):
2616
- x, _ = self.__states_times_in(variable, t0, tf, ensemble_member)
2675
+ def states_in(
2676
+ self,
2677
+ variable: str,
2678
+ t0: float | None = None,
2679
+ tf: float | None = None,
2680
+ ensemble_member: int = 0,
2681
+ *,
2682
+ return_times: bool = False,
2683
+ ) -> ca.MX | tuple[ca.DM, ca.MX]:
2684
+ x, t = self.__states_times_in(variable, t0, tf, ensemble_member)
2617
2685
 
2618
- return x
2686
+ if return_times:
2687
+ return x, t
2688
+ else:
2689
+ return x
2619
2690
 
2620
2691
  def integral(self, variable, t0=None, tf=None, ensemble_member=0):
2621
2692
  x, t = self.__states_times_in(variable, t0, tf, ensemble_member)
@@ -2754,11 +2825,9 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2754
2825
  if len(ensemble_members) == 1:
2755
2826
  ensemble_members = ensemble_members[0]
2756
2827
  else:
2757
- ensemble_members = "[{}]".format(
2758
- ",".join((str(x) for x in sorted(ensemble_members)))
2759
- )
2828
+ ensemble_members = "[{}]".format(",".join(str(x) for x in sorted(ensemble_members)))
2760
2829
 
2761
- var_names.append("{}__e{}__t{}".format(var_name, ensemble_members, t_i))
2830
+ var_names.append(f"{var_name}__e{ensemble_members}__t{t_i}")
2762
2831
 
2763
2832
  # Create named versions of the constraints
2764
2833
  named_x = ca.vertcat(*(ca.SX.sym(v) for v in var_names))
@@ -2805,62 +2874,62 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2805
2874
  lb, ub = lbg[i], ubg[i]
2806
2875
 
2807
2876
  if np.isfinite(lb) and np.isfinite(ub) and lb == ub:
2808
- c_str = "{} = {}".format(c_str, lb)
2877
+ c_str = f"{c_str} = {lb}"
2809
2878
  elif np.isfinite(lb) and np.isfinite(ub):
2810
- c_str = "{} <= {} <= {}".format(lb, c_str, ub)
2879
+ c_str = f"{lb} <= {c_str} <= {ub}"
2811
2880
  elif np.isfinite(lb):
2812
- c_str = "{} >= {}".format(c_str, lb)
2881
+ c_str = f"{c_str} >= {lb}"
2813
2882
  elif np.isfinite(ub):
2814
- c_str = "{} <= {}".format(c_str, ub)
2883
+ c_str = f"{c_str} <= {ub}"
2815
2884
 
2816
2885
  return c_str
2817
2886
 
2818
2887
  # Checking for right hand side of constraints
2819
- logger.info("Sanity check of lbg and ubg, checking for small values (<{})".format(tol_zero))
2888
+ logger.info(f"Sanity check of lbg and ubg, checking for small values (<{tol_zero})")
2820
2889
 
2821
2890
  lbg_abs_no_zero = np.abs(lbg.copy())
2822
2891
  lbg_abs_no_zero[lbg_abs_no_zero == 0.0] = +np.inf
2823
2892
  ind = np.argmin(lbg_abs_no_zero)
2824
2893
  if np.any(np.isfinite(lbg_abs_no_zero)):
2825
- logger.info("Smallest (absolute) lbg coefficient {}".format(lbg_abs_no_zero[ind]))
2826
- logger.info("E.g., {}".format(constr_to_str(ind)))
2894
+ logger.info(f"Smallest (absolute) lbg coefficient {lbg_abs_no_zero[ind]}")
2895
+ logger.info(f"E.g., {constr_to_str(ind)}")
2827
2896
  lbg_inds = lbg_abs_no_zero < tol_zero
2828
2897
  if np.any(lbg_inds):
2829
- logger.info("Too small of a (absolute) lbg found: {}".format(min(lbg[lbg_inds])))
2898
+ logger.info(f"Too small of a (absolute) lbg found: {min(lbg[lbg_inds])}")
2830
2899
 
2831
2900
  ubg_abs_no_zero = np.abs(ubg.copy())
2832
2901
  ubg_abs_no_zero[ubg_abs_no_zero == 0.0] = +np.inf
2833
2902
  ind = np.argmin(ubg_abs_no_zero)
2834
2903
  if np.any(np.isfinite(ubg_abs_no_zero)):
2835
- logger.info("Smallest (absolute) ubg coefficient {}".format(ubg_abs_no_zero[ind]))
2836
- logger.info("E.g., {}".format(constr_to_str(ind)))
2904
+ logger.info(f"Smallest (absolute) ubg coefficient {ubg_abs_no_zero[ind]}")
2905
+ logger.info(f"E.g., {constr_to_str(ind)}")
2837
2906
  ubg_inds = ubg_abs_no_zero < tol_zero
2838
2907
  if np.any(ubg_inds):
2839
- logger.info("Too small of a (absolute) ubg found: {}".format(min(ubg[ubg_inds])))
2908
+ logger.info(f"Too small of a (absolute) ubg found: {min(ubg[ubg_inds])}")
2840
2909
 
2841
- logger.info("Sanity check of lbg and ubg, checking for large values (>{})".format(tol_rhs))
2910
+ logger.info(f"Sanity check of lbg and ubg, checking for large values (>{tol_rhs})")
2842
2911
 
2843
2912
  lbg_abs_no_inf = np.abs(lbg.copy())
2844
2913
  lbg_abs_no_inf[~np.isfinite(lbg_abs_no_inf)] = -np.inf
2845
2914
  ind = np.argmax(lbg_abs_no_inf)
2846
2915
  if np.any(np.isfinite(lbg_abs_no_inf)):
2847
- logger.info("Largest (absolute) lbg coefficient {}".format(lbg_abs_no_inf[ind]))
2848
- logger.info("E.g., {}".format(constr_to_str(ind)))
2916
+ logger.info(f"Largest (absolute) lbg coefficient {lbg_abs_no_inf[ind]}")
2917
+ logger.info(f"E.g., {constr_to_str(ind)}")
2849
2918
 
2850
2919
  lbg_inds = lbg_abs_no_inf > tol_rhs
2851
2920
  if np.any(lbg_inds):
2852
- raise Exception("Too large of a (absolute) lbg found: {}".format(max(lbg[lbg_inds])))
2921
+ raise Exception(f"Too large of a (absolute) lbg found: {max(lbg[lbg_inds])}")
2853
2922
 
2854
2923
  ubg_abs_no_inf = np.abs(ubg.copy())
2855
2924
  ubg_abs_no_inf[~np.isfinite(ubg)] = -np.inf
2856
2925
  ind = np.argmax(ubg_abs_no_inf)
2857
2926
  if np.any(np.isfinite(ubg_abs_no_inf)):
2858
- logger.info("Largest (absolute) ubg coefficient {}".format(ubg_abs_no_inf[ind]))
2859
- logger.info("E.g., {}".format(constr_to_str(ind)))
2927
+ logger.info(f"Largest (absolute) ubg coefficient {ubg_abs_no_inf[ind]}")
2928
+ logger.info(f"E.g., {constr_to_str(ind)}")
2860
2929
 
2861
2930
  ubg_inds = ubg_abs_no_inf > tol_rhs
2862
2931
  if np.any(ubg_inds):
2863
- raise Exception("Too large of a (absolute) ubg found: {}".format(max(ubg[ubg_inds])))
2932
+ raise Exception(f"Too large of a (absolute) ubg found: {max(ubg[ubg_inds])}")
2864
2933
 
2865
2934
  eval_point = x0 if evaluate_at_x0 else 1.0
2866
2935
  eval_point_str = "x0" if evaluate_at_x0 else "1.0"
@@ -2887,25 +2956,20 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2887
2956
  # Objective
2888
2957
  A_obj, b_obj = out[0]
2889
2958
  logger.info(
2890
- "Statistics of objective: max & min of abs(jac(f, {}))) f({}), constants".format(
2891
- eval_point_str, eval_point_str
2892
- )
2959
+ f"Statistics of objective: max & min of abs(jac(f, {eval_point_str}))) "
2960
+ f"f({eval_point_str}), constants"
2893
2961
  )
2894
2962
  max_obj_A = max(np.abs(A_obj.data), default=None)
2895
2963
  min_obj_A = min(np.abs(A_obj.data[A_obj.data != 0.0]), default=None)
2896
2964
  obj_x0 = np.array(ca.Function("tmp", [in_var], [nlp["f"]])(eval_point)).ravel()[0]
2897
2965
  obj_b = b_obj.data[0] if len(b_obj.data) > 0 else 0.0
2898
2966
 
2899
- logger.info("{} & {}, {}, {}".format(max_obj_A, min_obj_A, obj_x0, obj_b))
2967
+ logger.info(f"{max_obj_A} & {min_obj_A}, {obj_x0}, {obj_b}")
2900
2968
 
2901
2969
  if abs(obj_b) > tol_up:
2902
- logger.info(
2903
- "Constant '{}' in objective exceeds upper tolerance of '{}'".format(obj_b, tol_up)
2904
- )
2970
+ logger.info(f"Constant '{obj_b}' in objective exceeds upper tolerance of '{tol_up}'")
2905
2971
  if abs(obj_b) > tol_up:
2906
- logger.info(
2907
- "Objective value at x0 '{}' exceeds upper tolerance of '{}'".format(obj_x0, tol_up)
2908
- )
2972
+ logger.info(f"Objective value at x0 '{obj_x0}' exceeds upper tolerance of '{tol_up}'")
2909
2973
 
2910
2974
  # Constraints
2911
2975
  A_constr, b_constr = out[1]
@@ -2916,12 +2980,21 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2916
2980
  min_constr_A = min(np.abs(A_constr.data[A_constr.data != 0.0]), default=None)
2917
2981
  max_constr_b = max(np.abs(b_constr.data), default=None)
2918
2982
  min_constr_b = min(np.abs(b_constr.data[b_constr.data != 0.0]), default=None)
2919
- logger.info(
2920
- "{} & {}, {} & {}".format(max_constr_A, min_constr_A, max_constr_b, min_constr_b)
2921
- )
2922
-
2923
- maxs = [x for x in [max_constr_A, max_constr_b, max_obj_A, obj_b] if x is not None]
2924
- mins = [x for x in [min_constr_A, min_constr_b, min_obj_A, obj_b] if x is not None]
2983
+ logger.info(f"{max_constr_A} & {min_constr_A}, {max_constr_b} & {min_constr_b}")
2984
+
2985
+ # Filter out exactly zero, as those entries do not show up in the
2986
+ # matrix. Shut up SonarCloud warning about this exact-to-zero
2987
+ # comparison.
2988
+ maxs = [
2989
+ x
2990
+ for x in [max_constr_A, max_constr_b, max_obj_A, obj_b]
2991
+ if x is not None and x != 0.0 # NOSONAR
2992
+ ]
2993
+ mins = [
2994
+ x
2995
+ for x in [min_constr_A, min_constr_b, min_obj_A, obj_b]
2996
+ if x is not None and x != 0.0 # NOSONAR
2997
+ ]
2925
2998
  if (maxs and max(maxs) > tol_up) or (mins and min(mins) < tol_down):
2926
2999
  logger.info("Jacobian matrix /constants coefficients values outside typical range!")
2927
3000
 
@@ -2950,25 +3023,17 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
2950
3023
  if exceedences:
2951
3024
  logger.info(
2952
3025
  "Exceedence in jacobian of constraints evaluated at x0"
2953
- " (max > {:g}, min < {:g}, or max / min > {:g}):".format(
2954
- tol_up, tol_down, tol_range
2955
- )
3026
+ f" (max > {tol_up:g}, min < {tol_down:g}, or max / min > {tol_range:g}):"
2956
3027
  )
2957
3028
 
2958
3029
  exceedences = sorted(exceedences, key=lambda x: x[1] / x[2], reverse=True)
2959
3030
 
2960
3031
  for i, (r, max_r, min_r, c) in enumerate(exceedences):
2961
- logger.info(
2962
- "row {} (max: {}, min: {}, range: {}): {}".format(
2963
- r, max_r, min_r, max_r / min_r, c
2964
- )
2965
- )
3032
+ logger.info(f"row {r} (max: {max_r}, min: {min_r}, range: {max_r / min_r}): {c}")
2966
3033
 
2967
3034
  if i >= 9:
2968
3035
  logger.info(
2969
- "Too many warnings of same type ({} others remain).".format(
2970
- len(exceedences) - 10
2971
- )
3036
+ f"Too many warnings of same type ({len(exceedences) - 10} others remain)."
2972
3037
  )
2973
3038
  break
2974
3039
 
@@ -3021,24 +3086,18 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
3021
3086
 
3022
3087
  exceedences = sorted(exceedences, key=lambda x: x[1], reverse=True)
3023
3088
 
3024
- logger.info("Max range found: {}".format(max_range_found))
3089
+ logger.info(f"Max range found: {max_range_found}")
3025
3090
  if exceedences:
3026
- logger.info("Exceedence in range per column (max / min > {:g}):".format(tol_range))
3091
+ logger.info(f"Exceedence in range per column (max / min > {tol_range:g}):")
3027
3092
 
3028
3093
  for i, (c, exc, min_, max_, c_min_str, c_max_str) in enumerate(exceedences):
3029
- logger.info(
3030
- "col {} ({}): range {}, min {}, max {}".format(
3031
- c, var_names[c], exc, min_, max_
3032
- )
3033
- )
3094
+ logger.info(f"col {c} ({var_names[c]}): range {exc}, min {min_}, max {max_}")
3034
3095
  logger.info(c_min_str)
3035
3096
  logger.info(c_max_str)
3036
3097
 
3037
3098
  if i >= 9:
3038
3099
  logger.info(
3039
- "Too many warnings of same type ({} others remain).".format(
3040
- len(exceedences) - 10
3041
- )
3100
+ f"Too many warnings of same type ({len(exceedences) - 10} others remain)."
3042
3101
  )
3043
3102
  break
3044
3103
 
@@ -3064,24 +3123,19 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
3064
3123
  if max_range > tol_range:
3065
3124
  exceedences.append((c, max_range, obj_coeff, min_r, max_r))
3066
3125
 
3067
- logger.info("Max range found: {}".format(max_range_found))
3126
+ logger.info(f"Max range found: {max_range_found}")
3068
3127
  if exceedences:
3069
- logger.info(
3070
- "Exceedence in range of objective variable (range > {:g}):".format(tol_range)
3071
- )
3128
+ logger.info(f"Exceedence in range of objective variable (range > {tol_range:g}):")
3072
3129
 
3073
3130
  for i, (c, max_range, obj_coeff, min_r, max_r) in enumerate(exceedences):
3074
3131
  logger.info(
3075
- "col {} ({}): range: {}, obj: {}, min constr: {}, max constr {}".format(
3076
- c, var_names[c], max_range, obj_coeff, min_r, max_r
3077
- )
3132
+ f"col {c} ({var_names[c]}): range: {max_range}, obj: {obj_coeff}, "
3133
+ f"min constr: {min_r}, max constr {max_r}"
3078
3134
  )
3079
3135
 
3080
3136
  if i >= 9:
3081
3137
  logger.info(
3082
- "Too many warnings of same type ({} others remain).".format(
3083
- len(exceedences) - 10
3084
- )
3138
+ f"Too many warnings of same type ({len(exceedences) - 10} others remain)."
3085
3139
  )
3086
3140
  break
3087
3141
 
@@ -3125,11 +3179,11 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
3125
3179
  if exceedences:
3126
3180
  logger.info(
3127
3181
  "Variables with at least one (absolute) state vector entry/entries "
3128
- "larger than {}".format(tol_up)
3182
+ f"larger than {tol_up}"
3129
3183
  )
3130
3184
 
3131
3185
  for k, v in exceedences:
3132
- logger.info("{}: abs max = {}".format(k, v))
3186
+ logger.info(f"{k}: abs max = {v}")
3133
3187
 
3134
3188
  if len(inds_down) > 0:
3135
3189
  exceedences = []
@@ -3144,11 +3198,11 @@ class CollocatedIntegratedOptimizationProblem(OptimizationProblem, metaclass=ABC
3144
3198
  ignore_all_zero_string = " (but not all zero)" if ignore_all_zero else ""
3145
3199
  logger.info(
3146
3200
  "Variables with all (absolute) state vector entry/entries "
3147
- "smaller than {}{}".format(tol_down, ignore_all_zero_string)
3201
+ f"smaller than {tol_down}{ignore_all_zero_string}"
3148
3202
  )
3149
3203
 
3150
3204
  for k, v in exceedences:
3151
3205
  if ignore_all_zero and v == 0.0:
3152
3206
  continue
3153
3207
 
3154
- logger.info("{}: abs max = {}".format(k, v))
3208
+ logger.info(f"{k}: abs max = {v}")
@@ -290,7 +290,10 @@ class ModelicaMixin(OptimizationProblem):
290
290
  # If start contains symbolics, try substituting parameter values
291
291
  if isinstance(start, ca.MX) and not start.is_constant():
292
292
  [start] = substitute_in_external(
293
- [start], self.__mx["parameters"], parameter_values
293
+ [start],
294
+ self.__mx["parameters"],
295
+ parameter_values,
296
+ resolve_numerically=True,
294
297
  )
295
298
  if not start.is_constant() or np.isnan(float(start)):
296
299
  raise Exception(
@@ -341,7 +344,9 @@ class ModelicaMixin(OptimizationProblem):
341
344
 
342
345
  m_ = v.min
343
346
  if isinstance(m_, ca.MX) and not m_.is_constant():
344
- [m_] = substitute_in_external([m_], self.__mx["parameters"], parameter_values)
347
+ [m_] = substitute_in_external(
348
+ [m_], self.__mx["parameters"], parameter_values, resolve_numerically=True
349
+ )
345
350
  if not m_.is_constant() or np.isnan(float(m_)):
346
351
  raise Exception(
347
352
  "Could not resolve lower bound for variable {}".format(sym_name)
@@ -350,7 +355,9 @@ class ModelicaMixin(OptimizationProblem):
350
355
 
351
356
  M_ = v.max
352
357
  if isinstance(M_, ca.MX) and not M_.is_constant():
353
- [M_] = substitute_in_external([M_], self.__mx["parameters"], parameter_values)
358
+ [M_] = substitute_in_external(
359
+ [M_], self.__mx["parameters"], parameter_values, resolve_numerically=True
360
+ )
354
361
  if not M_.is_constant() or np.isnan(float(M_)):
355
362
  raise Exception(
356
363
  "Could not resolve upper bound for variable {}".format(sym_name)
@@ -390,7 +397,7 @@ class ModelicaMixin(OptimizationProblem):
390
397
  # If start contains symbolics, try substituting parameter values
391
398
  if isinstance(start, ca.MX) and not start.is_constant():
392
399
  [start] = substitute_in_external(
393
- [start], self.__mx["parameters"], parameter_values
400
+ [start], self.__mx["parameters"], parameter_values, resolve_numerically=True
394
401
  )
395
402
  if not start.is_constant() or np.isnan(float(start)):
396
403
  logger.error(
@@ -437,7 +444,7 @@ class ModelicaMixin(OptimizationProblem):
437
444
  # If nominal contains parameter symbols, substitute them
438
445
  if isinstance(nominal, ca.MX) and not nominal.is_constant():
439
446
  [nominal] = substitute_in_external(
440
- [nominal], self.__mx["parameters"], parameter_values
447
+ [nominal], self.__mx["parameters"], parameter_values, resolve_numerically=True
441
448
  )
442
449
  if not nominal.is_constant() or np.isnan(float(nominal)):
443
450
  logger.error(
@@ -46,6 +46,9 @@ class OptimizationProblem(DataStoreAccessor, metaclass=ABCMeta):
46
46
  _debug_check_level = DebugLevel.MEDIUM
47
47
  _debug_check_options = {}
48
48
 
49
+ #: Enable ensemble-specific bounds functionality
50
+ ensemble_specific_bounds = False
51
+
49
52
  def __init__(self, **kwargs):
50
53
  # Call parent class first for default behaviour.
51
54
  super().__init__(**kwargs)
@@ -143,7 +143,26 @@ class SinglePassGoalProgrammingMixin(_GoalProgrammingMixinBase):
143
143
 
144
144
  def seed(self, ensemble_member):
145
145
  assert self._gp_first_run
146
- return super().seed(ensemble_member)
146
+
147
+ seed = super().seed(ensemble_member)
148
+
149
+ # Seed epsilons of current priority
150
+ for epsilon in self.__problem_epsilons:
151
+ eps_size = epsilon.size1()
152
+ if eps_size > 1:
153
+ seed[epsilon.name()] = np.ones(eps_size)
154
+ else:
155
+ seed[epsilon.name()] = 1.0
156
+
157
+ times = self.times()
158
+ for epsilon in self.__problem_path_epsilons:
159
+ eps_size = epsilon.size1()
160
+ if eps_size > 1:
161
+ seed[epsilon.name()] = Timeseries(times, np.ones((eps_size, len(times))))
162
+ else:
163
+ seed[epsilon.name()] = Timeseries(times, np.ones(len(times)))
164
+
165
+ return seed
147
166
 
148
167
  def constraints(self, ensemble_member):
149
168
  constraints = super().constraints(ensemble_member)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes