rtc-tools 2.6.0b2__py3-none-any.whl → 2.7.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.

@@ -86,6 +86,11 @@ class ControlTreeMixin(OptimizationProblem):
86
86
  logger.debug("ControlTreeMixin: Branching times:")
87
87
  logger.debug(self.__branching_times)
88
88
 
89
+ # Avoid calling constant_inputs() many times
90
+ constant_inputs = [
91
+ self.constant_inputs(ensemble_member=i) for i in range(self.ensemble_size)
92
+ ]
93
+
89
94
  # Branches start at branching times, so that the tree looks like the following:
90
95
  #
91
96
  # *-----
@@ -122,18 +127,16 @@ class ControlTreeMixin(OptimizationProblem):
122
127
  for forecast_variable in options["forecast_variables"]:
123
128
  # We assume the time stamps of the forecasts in all ensemble
124
129
  # members to be identical
125
- timeseries = self.constant_inputs(ensemble_member=0)[forecast_variable]
130
+ timeseries = constant_inputs[0][forecast_variable]
126
131
  els = np.logical_and(
127
132
  timeseries.times >= branching_time_0, timeseries.times < branching_time_1
128
133
  )
129
134
 
130
135
  # Compute distance between ensemble members
131
136
  for i, member_i in enumerate(branches[current_branch]):
132
- timeseries_i = self.constant_inputs(ensemble_member=member_i)[forecast_variable]
137
+ timeseries_i = constant_inputs[member_i][forecast_variable]
133
138
  for j, member_j in enumerate(branches[current_branch]):
134
- timeseries_j = self.constant_inputs(ensemble_member=member_j)[
135
- forecast_variable
136
- ]
139
+ timeseries_j = constant_inputs[member_j][forecast_variable]
137
140
  distances[i, j] += np.linalg.norm(
138
141
  timeseries_i.values[els] - timeseries_j.values[els]
139
142
  )
@@ -2,7 +2,6 @@ import configparser
2
2
  import glob
3
3
  import logging
4
4
  import os
5
- import pickle
6
5
  from typing import Iterable, List, Tuple, Union
7
6
 
8
7
  import casadi as ca
@@ -56,7 +55,7 @@ class LookupTable(LookupTableBase):
56
55
  "This lookup table was not instantiated with tck metadata. \
57
56
  Domain/Range information is unavailable."
58
57
  )
59
- if type(t) == tuple and len(t) == 2:
58
+ if isinstance(t, tuple) and len(t) == 2:
60
59
  raise NotImplementedError(
61
60
  "Domain/Range information is not yet implemented for 2D LookupTables"
62
61
  )
@@ -299,8 +298,9 @@ class CSVLookupTableMixin(OptimizationProblem):
299
298
  def check_lookup_table(lookup_table):
300
299
  if lookup_table in self.__lookup_tables:
301
300
  raise Exception(
302
- "Cannot add lookup table {},"
303
- "since there is already one with this name.".format(lookup_table)
301
+ "Cannot add lookup table {},since there is already one with this name.".format(
302
+ lookup_table
303
+ )
304
304
  )
305
305
 
306
306
  # Read CSV files
@@ -323,7 +323,7 @@ class CSVLookupTableMixin(OptimizationProblem):
323
323
 
324
324
  # If tck file is newer than the csv file, first try to load the cached values from
325
325
  # the tck file
326
- tck_filename = filename.replace(".csv", ".tck")
326
+ tck_filename = filename.replace(".csv", ".npz")
327
327
  valid_cache = False
328
328
  if os.path.exists(tck_filename):
329
329
  if no_curvefit_options:
@@ -338,11 +338,13 @@ class CSVLookupTableMixin(OptimizationProblem):
338
338
  output
339
339
  )
340
340
  )
341
- with open(tck_filename, "rb") as f:
342
- try:
343
- tck, function = pickle.load(f)
344
- except Exception:
345
- valid_cache = False
341
+ try:
342
+ with np.load(filename.replace(".csv", ".npz")) as data:
343
+ tck = (data["arr_0"], data["arr_1"], int(data["arr_2"]))
344
+ function = ca.Function.load(filename.replace(".csv", ".ca"))
345
+ except Exception:
346
+ valid_cache = False
347
+
346
348
  if not valid_cache:
347
349
  logger.info("CSVLookupTableMixin: Recalculating tck values for {}".format(output))
348
350
 
@@ -357,6 +359,7 @@ class CSVLookupTableMixin(OptimizationProblem):
357
359
  k=k,
358
360
  monotonicity=mono,
359
361
  curvature=curv,
362
+ ipopt_options={"nlp_scaling_method": "none"},
360
363
  )
361
364
  else:
362
365
  raise Exception(
@@ -446,11 +449,8 @@ class CSVLookupTableMixin(OptimizationProblem):
446
449
  )
447
450
 
448
451
  if not valid_cache:
449
- pickle.dump(
450
- (tck, function),
451
- open(filename.replace(".csv", ".tck"), "wb"),
452
- protocol=pickle.HIGHEST_PROTOCOL,
453
- )
452
+ np.savez(filename.replace(".csv", ".npz"), *tck)
453
+ function.save(filename.replace(".csv", ".ca"))
454
454
 
455
455
  def lookup_tables(self, ensemble_member):
456
456
  # Call parent class first for default values.
@@ -98,6 +98,9 @@ class CSVMixin(IOMixin):
98
98
  names=True,
99
99
  encoding=None,
100
100
  )
101
+ if len(self.__ensemble.shape) == 0:
102
+ # If there is only one ensemble member, the array is 0-dimensional.
103
+ self.__ensemble = np.expand_dims(self.__ensemble, 0)
101
104
 
102
105
  logger.debug("CSVMixin: Read ensemble description")
103
106
 
@@ -351,8 +351,9 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
351
351
  if goal.has_target_bounds:
352
352
  # We use a violation variable formulation, with the violation
353
353
  # variables epsilon bounded between 0 and 1.
354
- m, M = np.full_like(epsilon, -np.inf, dtype=np.float64), np.full_like(
355
- epsilon, np.inf, dtype=np.float64
354
+ m, M = (
355
+ np.full_like(epsilon, -np.inf, dtype=np.float64),
356
+ np.full_like(epsilon, np.inf, dtype=np.float64),
356
357
  )
357
358
 
358
359
  # A function range does not have to be specified for critical
@@ -667,6 +668,7 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
667
668
  logger.info("Starting goal programming")
668
669
 
669
670
  success = False
671
+ self.skip_priority = False
670
672
 
671
673
  self.__constraint_store = [OrderedDict() for ensemble_member in range(self.ensemble_size)]
672
674
  self.__path_constraint_store = [
@@ -691,6 +693,13 @@ class GoalProgrammingMixin(_GoalProgrammingMixinBase):
691
693
  # Call the pre priority hook
692
694
  self.priority_started(priority)
693
695
 
696
+ if self.skip_priority:
697
+ logger.info(
698
+ "priority {} was removed in priority_started. No optimization problem "
699
+ "is solved at this priority.".format(priority)
700
+ )
701
+ continue
702
+
694
703
  (
695
704
  self.__subproblem_epsilons,
696
705
  self.__subproblem_objectives,
@@ -437,7 +437,7 @@ class _GoalConstraint:
437
437
  ):
438
438
  assert isinstance(m, (float, np.ndarray, Timeseries))
439
439
  assert isinstance(M, (float, np.ndarray, Timeseries))
440
- assert type(m) == type(M)
440
+ assert type(m) is type(M)
441
441
 
442
442
  # NumPy arrays only allowed for vector goals
443
443
  if isinstance(m, np.ndarray):
@@ -982,8 +982,9 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
982
982
  if goal.has_target_bounds:
983
983
  # We use a violation variable formulation, with the violation
984
984
  # variables epsilon bounded between 0 and 1.
985
- m, M = np.full_like(epsilon, -np.inf, dtype=np.float64), np.full_like(
986
- epsilon, np.inf, dtype=np.float64
985
+ m, M = (
986
+ np.full_like(epsilon, -np.inf, dtype=np.float64),
987
+ np.full_like(epsilon, np.inf, dtype=np.float64),
987
988
  )
988
989
 
989
990
  # A function range does not have to be specified for critical
@@ -1081,6 +1082,7 @@ class _GoalProgrammingMixinBase(OptimizationProblem, metaclass=ABCMeta):
1081
1082
 
1082
1083
  :param priority: The priority level that was started.
1083
1084
  """
1085
+ self.skip_priority = False
1084
1086
  pass
1085
1087
 
1086
1088
  def priority_completed(self, priority: int) -> None:
@@ -1,10 +1,18 @@
1
+ import importlib.resources
1
2
  import itertools
2
3
  import logging
4
+ import sys
3
5
  from typing import Dict, Union
4
6
 
7
+ # Python 3.9's importlib.metadata does not support the "group" parameter to
8
+ # entry_points yet.
9
+ if sys.version_info < (3, 10):
10
+ import importlib_metadata
11
+ else:
12
+ from importlib import metadata as importlib_metadata
13
+
5
14
  import casadi as ca
6
15
  import numpy as np
7
- import pkg_resources
8
16
  import pymoca
9
17
  import pymoca.backends.casadi.api
10
18
 
@@ -48,9 +56,21 @@ class ModelicaMixin(OptimizationProblem):
48
56
  else:
49
57
  model_name = self.__class__.__name__
50
58
 
51
- self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
52
- kwargs["model_folder"], model_name, self.compiler_options()
53
- )
59
+ compiler_options = self.compiler_options()
60
+ logger.info(f"Loading/compiling model {model_name}.")
61
+ try:
62
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
63
+ kwargs["model_folder"], model_name, compiler_options
64
+ )
65
+ except (RuntimeError, ModuleNotFoundError) as error:
66
+ if not compiler_options.get("cache", False):
67
+ raise error
68
+ compiler_options["cache"] = False
69
+ logger.warning(f"Loading model {model_name} using a cache file failed: {error}.")
70
+ logger.info(f"Compiling model {model_name}.")
71
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
72
+ kwargs["model_folder"], model_name, compiler_options
73
+ )
54
74
 
55
75
  # Extract the CasADi MX variables used in the model
56
76
  self.__mx = {}
@@ -162,9 +182,9 @@ class ModelicaMixin(OptimizationProblem):
162
182
  # Where imported model libraries are located.
163
183
  library_folders = self.modelica_library_folders.copy()
164
184
 
165
- for ep in pkg_resources.iter_entry_points(group="rtctools.libraries.modelica"):
185
+ for ep in importlib_metadata.entry_points(group="rtctools.libraries.modelica"):
166
186
  if ep.name == "library_folder":
167
- library_folders.append(pkg_resources.resource_filename(ep.module_name, ep.attrs[0]))
187
+ library_folders.append(str(importlib.resources.files(ep.module).joinpath(ep.attr)))
168
188
 
169
189
  compiler_options["library_folders"] = library_folders
170
190
 
@@ -314,7 +334,7 @@ class ModelicaMixin(OptimizationProblem):
314
334
  try:
315
335
  (m, M) = bounds[sym_name]
316
336
  except KeyError:
317
- if self.__python_types.get(sym_name, float) == bool:
337
+ if self.__python_types.get(sym_name, float) is bool:
318
338
  (m, M) = (0, 1)
319
339
  else:
320
340
  (m, M) = (-np.inf, np.inf)
@@ -388,7 +408,7 @@ class ModelicaMixin(OptimizationProblem):
388
408
  return seed
389
409
 
390
410
  def variable_is_discrete(self, variable):
391
- return self.__python_types.get(variable, float) != float
411
+ return self.__python_types.get(variable, float) is not float
392
412
 
393
413
  @property
394
414
  @cached
@@ -314,6 +314,24 @@ class OptimizationProblem(DataStoreAccessor, metaclass=ABCMeta):
314
314
  if log_level == logging.ERROR and not log_solver_failure_as_error:
315
315
  log_level = logging.INFO
316
316
 
317
+ if self.solver_options()["solver"].lower() == "knitro":
318
+ list_feas_flags = [
319
+ "KN_RC_OPTIMAL_OR_SATISFACTORY",
320
+ "KN_RC_ITER_LIMIT_FEAS",
321
+ "KN_RC_NEAR_OPT",
322
+ "KN_RC_FEAS_XTOL",
323
+ "KN_RC_FEAS_NO_IMPROVE",
324
+ "KN_RC_FEAS_FTOL",
325
+ "KN_RC_TIME_LIMIT_FEAS",
326
+ "KN_RC_FEVAL_LIMIT_FEAS",
327
+ "KN_RC_MIP_EXH_FEAS",
328
+ "KN_RC_MIP_TERM_FEAS",
329
+ "KN_RC_MIP_SOLVE_LIMIT_FEAS",
330
+ "KN_RC_MIP_NODE_LIMIT_FEAS",
331
+ ]
332
+ if solver_stats["return_status"] in list_feas_flags:
333
+ success = True
334
+
317
335
  return success, log_level
318
336
 
319
337
  @abstractproperty
@@ -277,3 +277,16 @@ class PIMixin(IOMixin):
277
277
  :class:`pi.Timeseries` object for holding the output data.
278
278
  """
279
279
  return self.__timeseries_export
280
+
281
+ def set_unit(self, variable: str, unit: str):
282
+ """
283
+ Set the unit of a time series.
284
+
285
+ :param variable: Time series ID.
286
+ :param unit: Unit.
287
+ """
288
+ assert hasattr(self, "_PIMixin__timeseries_import"), (
289
+ "set_unit can only be called after read() in pre() has finished."
290
+ )
291
+ self.__timeseries_import.set_unit(variable, unit, 0)
292
+ self.__timeseries_export.set_unit(variable, unit, 0)
rtctools/rtctoolsapp.py CHANGED
@@ -1,9 +1,17 @@
1
+ import importlib.resources
1
2
  import logging
2
3
  import os
3
4
  import shutil
4
5
  import sys
5
6
  from pathlib import Path
6
7
 
8
+ # Python 3.9's importlib.metadata does not support the "group" parameter to
9
+ # entry_points yet.
10
+ if sys.version_info < (3, 10):
11
+ import importlib_metadata
12
+ else:
13
+ from importlib import metadata as importlib_metadata
14
+
7
15
  import rtctools
8
16
 
9
17
  logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s")
@@ -23,9 +31,6 @@ def copy_libraries(*args):
23
31
  if not os.path.exists(path):
24
32
  sys.exit("Folder '{}' does not exist".format(path))
25
33
 
26
- # pkg_resources can be quite a slow import, so we do it here
27
- import pkg_resources
28
-
29
34
  def _copytree(src, dst, symlinks=False, ignore=None):
30
35
  if not os.path.exists(dst):
31
36
  os.makedirs(dst)
@@ -56,11 +61,10 @@ def copy_libraries(*args):
56
61
  dst = Path(path)
57
62
 
58
63
  library_folders = []
59
- for ep in pkg_resources.iter_entry_points(group="rtctools.libraries.modelica"):
64
+
65
+ for ep in importlib_metadata.entry_points(group="rtctools.libraries.modelica"):
60
66
  if ep.name == "library_folder":
61
- library_folders.append(
62
- Path(pkg_resources.resource_filename(ep.module_name, ep.attrs[0]))
63
- )
67
+ library_folders.append(Path(importlib.resources.files(ep.module).joinpath(ep.attr)))
64
68
 
65
69
  tlds = {}
66
70
  for lf in library_folders:
@@ -100,25 +104,24 @@ def download_examples(*args):
100
104
  from zipfile import ZipFile
101
105
 
102
106
  version = rtctools.__version__
103
- rtc_full_name = "rtc-tools-{}".format(version)
104
107
  try:
105
- url = "https://gitlab.com/deltares/rtc-tools/-/archive/{}/{}.zip".format(
106
- version, rtc_full_name
107
- )
108
+ url = "https://github.com/deltares/rtc-tools/zipball/{}".format(version)
108
109
 
109
110
  opener = urllib.request.build_opener()
110
111
  urllib.request.install_opener(opener)
111
- local_filename, _ = urllib.request.urlretrieve(url)
112
+ # The security warning can be dismissed as the url variable is hardcoded to a remote.
113
+ local_filename, _ = urllib.request.urlretrieve(url) # nosec
112
114
  except HTTPError:
113
115
  sys.exit("Could not found examples for RTC-Tools version {}.".format(version))
114
116
 
115
117
  with ZipFile(local_filename, "r") as z:
116
118
  target = path / "rtc-tools-examples"
117
- prefix = "{}/examples/".format(rtc_full_name)
119
+ zip_folder_name = next(x for x in z.namelist() if x.startswith("Deltares-rtc-tools-"))
120
+ prefix = "{}/examples/".format(zip_folder_name.rstrip("/"))
118
121
  members = [x for x in z.namelist() if x.startswith(prefix)]
119
122
  z.extractall(members=members)
120
123
  shutil.move(prefix, target)
121
- shutil.rmtree(rtc_full_name)
124
+ shutil.rmtree(zip_folder_name)
122
125
 
123
126
  sys.exit("Succesfully downloaded the RTC-Tools examples to '{}'".format(target.resolve()))
124
127
 
@@ -94,7 +94,7 @@ class IOMixin(SimulationProblem, metaclass=ABCMeta):
94
94
  self.__cache_loop_timeseries = {}
95
95
 
96
96
  timeseries_names = set(self.io.get_timeseries_names(0))
97
- for v in self.get_variables():
97
+ for v in self.get_input_variables():
98
98
  if v in timeseries_names:
99
99
  _, values = self.io.get_timeseries_sec(v)
100
100
  self.__cache_loop_timeseries[v] = values
@@ -240,3 +240,16 @@ class PIMixin(IOMixin):
240
240
  def get_timeseries(self, variable):
241
241
  _, values = self.io.get_timeseries(variable)
242
242
  return values
243
+
244
+ def set_unit(self, variable: str, unit: str):
245
+ """
246
+ Set the unit of a time series.
247
+
248
+ :param variable: Time series ID.
249
+ :param unit: Unit.
250
+ """
251
+ assert hasattr(self, "_PIMixin__timeseries_import"), (
252
+ "set_unit can only be called after read() in pre() has finished."
253
+ )
254
+ self.__timeseries_import.set_unit(variable, unit, 0)
255
+ self.__timeseries_export.set_unit(variable, unit, 0)