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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools
3
- Version: 2.6.0b2
3
+ Version: 2.7.0a1
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
  Author: Deltares
@@ -29,8 +29,9 @@ License-File: COPYING.LESSER
29
29
  Requires-Dist: casadi ==3.6.*,>=3.6.3
30
30
  Requires-Dist: numpy <1.26,>=1.16.0
31
31
  Requires-Dist: scipy <1.11,>=1.0.0
32
- Requires-Dist: pymoca ==0.9.1
32
+ Requires-Dist: pymoca ==0.9.*,>=0.9.1
33
33
  Requires-Dist: rtc-tools-channel-flow >=1.1.0
34
+ Requires-Dist: defusedxml >=0.7.0
34
35
  Provides-Extra: all
35
36
  Requires-Dist: netCDF4 ; extra == 'all'
36
37
  Provides-Extra: netcdf
@@ -1,6 +1,6 @@
1
1
  rtctools/__init__.py,sha256=91hvS2-ryd2Pvw0COpsUzTwJwSnTZ035REiej-1hNI4,107
2
- rtctools/_version.py,sha256=dUPcNcf2xQ9Ynkv1d2c0aTxHuz_HBjht0yZ3i8NDzGc,499
3
- rtctools/rtctoolsapp.py,sha256=A6M0v6vymAZWdBPLzgRVyAUfahD0knC8B4IptJ7_sBM,4097
2
+ rtctools/_version.py,sha256=My52FWqz9UB3_PwxQjfVCPUkHjej_S48gNc1rsNpaKc,499
3
+ rtctools/rtctoolsapp.py,sha256=UnkuiJhv0crEEVs8H6PYvMuc2y_q6V_xLuyKEgXj9GM,4200
4
4
  rtctools/util.py,sha256=PaeKfDUA174ODZbY5fZjCTf-F-TdhW7yEuP189Ro190,9075
5
5
  rtctools/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  rtctools/_internal/alias_tools.py,sha256=XuQSAhhFuVtwn0yrAObZWIKPsSF4j2axXRtEmitIFPs,5310
@@ -10,8 +10,8 @@ rtctools/_internal/debug_check_helpers.py,sha256=UgQTEPw4PyR7MbYLewSSWQqTwQj7xr5
10
10
  rtctools/data/__init__.py,sha256=EllgSmCdrlvQZSd1VilvjPaeYJGhY9ErPiQtedmuFoA,157
11
11
  rtctools/data/csv.py,sha256=iYOEED3AaNxt4ews_aAkHfl9Tq9a-9vjxvYwjVR_lQE,5231
12
12
  rtctools/data/netcdf.py,sha256=xpk4Ggl7gItNG6lO7p3OJPR-elK8_CiCtxUI7cX0gwk,19109
13
- rtctools/data/pi.py,sha256=hBgK_dSLv_V9pWFGNzOEnG76ffLsjW3SBSWfgVJpXSA,45380
14
- rtctools/data/rtc.py,sha256=FBuUQ6aL4D3y6puKudyuLAeP_vmpDiliqzJg8f1kd7g,9043
13
+ rtctools/data/pi.py,sha256=Ni1hBDdhQdcWYO-NUPhKA1WJdzbSXjZc5w5xauOBcJM,45437
14
+ rtctools/data/rtc.py,sha256=1yGJZGq2Z36MYLiLuZaHnxupL4mgw-Wuu54PAG05kcM,9077
15
15
  rtctools/data/storage.py,sha256=67J4BRTl0AMEzlKNZ8Xdpy_4cGtwx8Lo_tL2n0G4S9w,13206
16
16
  rtctools/data/interpolation/__init__.py,sha256=GBubCIT5mFoSTV-lOk7cpwvZekNMEe5bvqSQJ9HE34M,73
17
17
  rtctools/data/interpolation/bspline.py,sha256=qevB842XWCH3fWlWMBqKMy1mw37ust-0YtSnb9PKCEc,948
@@ -20,7 +20,7 @@ rtctools/data/interpolation/bspline2d.py,sha256=ScmX0fPDxbUVtj3pbUE0L7UJocqroD_6
20
20
  rtctools/optimization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  rtctools/optimization/collocated_integrated_optimization_problem.py,sha256=Bv1a1O4QTAdIpe9SOA9_oEDDph-oj-9q5yfN4SFMdW8,131343
22
22
  rtctools/optimization/control_tree_mixin.py,sha256=CC6TWp3kFQgMokx6213pRLx9iY17Fd0VcwG4Wpwa0Uo,8974
23
- rtctools/optimization/csv_lookup_table_mixin.py,sha256=xiyOKpPT3kQ6yuxSJRorGYW_QwnlBzW6vvrtvQNhHsE,17264
23
+ rtctools/optimization/csv_lookup_table_mixin.py,sha256=h4WKuPR1BJbYuJfQ9lx4rXalB6yYK-zajINabRL7BrA,17298
24
24
  rtctools/optimization/csv_mixin.py,sha256=sRp5paHWfCw2bz-23Nw-HdFLS3CZTpVwaBdFo98DbvE,12252
25
25
  rtctools/optimization/goal_programming_mixin.py,sha256=GK25DrbAY_rMsra080pSCDZwzLQNN2Ppd-2d0_FEllg,32999
26
26
  rtctools/optimization/goal_programming_mixin_base.py,sha256=oh9CsEiyYTmthcfvRbX-9Z9bIo6SHv_DCiVt9kx0sjI,43781
@@ -30,21 +30,21 @@ rtctools/optimization/io_mixin.py,sha256=AsZQ7YOUcUbWoczmjTXaSje5MUEsPNbQyZBJ6qz
30
30
  rtctools/optimization/linearization_mixin.py,sha256=mG5S7uwvwDydw-eBPyQKnLyKoy08EBjQh25vu97afhY,1049
31
31
  rtctools/optimization/linearized_order_goal_programming_mixin.py,sha256=LQ2qpYt0YGLpEoerif4FJ5wwzq16q--27bsRjcqIU5A,9087
32
32
  rtctools/optimization/min_abs_goal_programming_mixin.py,sha256=WMOv9EF8cfDJgTunzXfI_cUmBSQK26u1HJB_9EAarfM,14031
33
- rtctools/optimization/modelica_mixin.py,sha256=ysVMayNA4sSFoHkSdhjWOxT6UzOVbN0ZeM4v-RpvZXE,17161
33
+ rtctools/optimization/modelica_mixin.py,sha256=6rotTEge4l2oDib4tnG2ccXqG1ryEzNxbRkk_StFAHg,17769
34
34
  rtctools/optimization/netcdf_mixin.py,sha256=-zkXh3sMYE50c3kHsrmUVGWMSFm-0cXQpGrCm0yn-Tc,7563
35
35
  rtctools/optimization/optimization_problem.py,sha256=qzpc81NaZMeoXKuayFmBF15iXYuNAk5yxmaER_Gcz_A,44131
36
- rtctools/optimization/pi_mixin.py,sha256=63qda6i7hjtDuP3hL6RO29vCCP11aUpR9B4KoqlLFVI,11314
36
+ rtctools/optimization/pi_mixin.py,sha256=n_ZyifuNPxcRoJXKR6Ks7DgK_b7-EToYJ23_khOwpzo,11786
37
37
  rtctools/optimization/planning_mixin.py,sha256=O_Y74X8xZmaNZR4iYOe7BR06s9hnmcapbuHYHQTBPPQ,724
38
38
  rtctools/optimization/single_pass_goal_programming_mixin.py,sha256=Zb9szg3PGT2o6gkGsXluSfEaAswkw3TKfPQDzUrj_Y4,25784
39
39
  rtctools/optimization/timeseries.py,sha256=nCrsGCJThBMh9lvngEpbBDa834_QvklVvkxJqwX4a1M,1734
40
40
  rtctools/simulation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  rtctools/simulation/csv_mixin.py,sha256=rGDUFPsqGHmF0_dWdXeWzWzMpkPmwCNweTBVrwSh31g,6704
42
42
  rtctools/simulation/io_mixin.py,sha256=SJasNGI--OQ9Y-Z61oeeaGCxSrNddYz4AOVfJYbmf74,6209
43
- rtctools/simulation/pi_mixin.py,sha256=uwl61LYjb8dmMz910EB2-bC0KSuhLzsrJzk0hxWYEhk,9359
44
- rtctools/simulation/simulation_problem.py,sha256=gTAimG2MLw_TTkeHLkIMxpYgAmR-voqzvje7pcFnw4U,44556
45
- rtc_tools-2.6.0b2.dist-info/COPYING.LESSER,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
46
- rtc_tools-2.6.0b2.dist-info/METADATA,sha256=J_s90hgXnSTTIZg76jopXRUeL0V3VUJfOyvorAaE_1s,1452
47
- rtc_tools-2.6.0b2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
48
- rtc_tools-2.6.0b2.dist-info/entry_points.txt,sha256=-x622IB_l1duw2M6t6syfQ4yzOiQTp0IZxKGcYRgWgk,151
49
- rtc_tools-2.6.0b2.dist-info/top_level.txt,sha256=pnBrb58PFPd1kp1dqa-JHU7R55h3alDNJIJnF3Jf9Dw,9
50
- rtc_tools-2.6.0b2.dist-info/RECORD,,
43
+ rtctools/simulation/pi_mixin.py,sha256=vcizArZsOn7tGlSFCsUmkjmRFA1A1kMNqUllLlkVc9Y,9831
44
+ rtctools/simulation/simulation_problem.py,sha256=wGRIOm4AHAy75R7cbqeA13-Qy1ED3RY3Yc5Hvtmv6PY,48541
45
+ rtc_tools-2.7.0a1.dist-info/COPYING.LESSER,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
46
+ rtc_tools-2.7.0a1.dist-info/METADATA,sha256=p33wEk1F8t8jCHyV1bUe5R4JcedANOtp-9NsTH1mWBc,1494
47
+ rtc_tools-2.7.0a1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
48
+ rtc_tools-2.7.0a1.dist-info/entry_points.txt,sha256=-x622IB_l1duw2M6t6syfQ4yzOiQTp0IZxKGcYRgWgk,151
49
+ rtc_tools-2.7.0a1.dist-info/top_level.txt,sha256=pnBrb58PFPd1kp1dqa-JHU7R55h3alDNJIJnF3Jf9Dw,9
50
+ rtc_tools-2.7.0a1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
rtctools/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-01-18T16:11:20+0000",
11
+ "date": "2024-04-25T06:42:12+0000",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "e868661a3b37bb27f61ffcf9cdf1d53d857d5108",
15
- "version": "2.6.0b2"
14
+ "full-revisionid": "e3d59506cabe8df7e05acd089ccf574e106a577f",
15
+ "version": "2.7.0a1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
rtctools/data/pi.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
5
  import os
6
6
  import xml.etree.ElementTree as ET
7
7
 
8
+ import defusedxml.ElementTree as DefusedElementTree
8
9
  import numpy as np
9
10
 
10
11
  ns = {"fews": "http://www.wldelft.nl/fews", "pi": "http://www.wldelft.nl/fews/PI"}
@@ -30,7 +31,7 @@ class Diag:
30
31
  """
31
32
  self.__path_xml = os.path.join(folder, basename + ".xml")
32
33
 
33
- self.__tree = ET.parse(self.__path_xml)
34
+ self.__tree = DefusedElementTree.parse(self.__path_xml)
34
35
  self.__xml_root = self.__tree.getroot()
35
36
 
36
37
  def get(self, level=ERROR_FATAL):
@@ -87,7 +88,7 @@ class DiagHandler(logging.Handler):
87
88
  self.__path_xml = os.path.join(folder, basename + ".xml")
88
89
 
89
90
  try:
90
- self.__tree = ET.parse(self.__path_xml)
91
+ self.__tree = DefusedElementTree.parse(self.__path_xml)
91
92
  self.__xml_root = self.__tree.getroot()
92
93
  except Exception:
93
94
  self.__xml_root = ET.Element("{%s}Diag" % (ns["pi"],))
@@ -135,7 +136,7 @@ class ParameterConfig:
135
136
  basename = basename + ".xml"
136
137
  self.__path_xml = os.path.join(folder, basename)
137
138
 
138
- self.__tree = ET.parse(self.__path_xml)
139
+ self.__tree = DefusedElementTree.parse(self.__path_xml)
139
140
  self.__xml_root = self.__tree.getroot()
140
141
 
141
142
  def get(self, group_id, parameter_id, location_id=None, model=None):
@@ -300,9 +301,8 @@ class ParameterConfig:
300
301
  # get table contenstart_datetime
301
302
  el_row = child.findall("pi:row", ns)
302
303
  table = {
303
- columnId[key]: np.empty(len(el_row), columnType[key]) # initialize table
304
- for key in columnId
305
- }
304
+ columnId[key]: np.empty(len(el_row), columnType[key]) for key in columnId
305
+ } # initialize table
306
306
 
307
307
  i_row = 0
308
308
  for row in el_row:
@@ -378,7 +378,7 @@ class Timeseries:
378
378
  if self.make_new_file:
379
379
  self.__reset_xml_tree()
380
380
  else:
381
- self.__tree = ET.parse(self.__path_xml)
381
+ self.__tree = DefusedElementTree.parse(self.__path_xml)
382
382
  self.__xml_root = self.__tree.getroot()
383
383
 
384
384
  self.__values = [{}]
@@ -855,7 +855,7 @@ class Timeseries:
855
855
 
856
856
  variable = self.__data_config.variable(header)
857
857
 
858
- miss_val = float(header.find("pi:missVal", ns).text)
858
+ miss_val = header.find("pi:missVal", ns).text
859
859
  values = self.__values[ensemble_member][variable]
860
860
 
861
861
  # Update the header, which may have changed
@@ -868,11 +868,8 @@ class Timeseries:
868
868
  self.__xml_root.remove(series)
869
869
  continue
870
870
 
871
- # Replace NaN with missing value
872
- nans = np.isnan(values)
873
- values[nans] = miss_val
874
-
875
871
  # Write output
872
+ nans = np.isnan(values)
876
873
  if self.__binary:
877
874
  f.write(values.astype(self.__pi_dtype).tobytes())
878
875
  else:
@@ -897,7 +894,10 @@ class Timeseries:
897
894
  event = ET.Element("pi:event")
898
895
  event.set("date", t.strftime("%Y-%m-%d"))
899
896
  event.set("time", t.strftime("%H:%M:%S"))
900
- event.set("value", str(values[i]))
897
+ if nans[i]:
898
+ event.set("value", miss_val)
899
+ else:
900
+ event.set("value", str(values[i]))
901
901
  series.append(event)
902
902
  if self.dt:
903
903
  t += self.dt
@@ -907,9 +907,6 @@ class Timeseries:
907
907
  for i in range(len(values), len(events)):
908
908
  series.remove(events[i])
909
909
 
910
- # Restore NaN
911
- values[nans] = np.nan
912
-
913
910
  if self.__binary:
914
911
  f.close()
915
912
 
rtctools/data/rtc.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import logging
2
2
  import os
3
- import xml.etree.ElementTree as ET
4
3
  from collections import namedtuple
5
4
 
5
+ import defusedxml.ElementTree as DefusedElementTree
6
+
6
7
  ts_ids = namedtuple("ids", "location_id parameter_id qualifier_id")
7
8
  p_ids = namedtuple("ids", "model_id location_id parameter_id")
8
9
 
@@ -31,7 +32,7 @@ class DataConfig:
31
32
 
32
33
  path = os.path.join(folder, "rtcDataConfig.xml")
33
34
  try:
34
- tree = ET.parse(path)
35
+ tree = DefusedElementTree.parse(path)
35
36
  root = tree.getroot()
36
37
 
37
38
  timeseriess1 = root.findall("./*/fews:timeSeries", ns)
@@ -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
@@ -323,7 +322,7 @@ class CSVLookupTableMixin(OptimizationProblem):
323
322
 
324
323
  # If tck file is newer than the csv file, first try to load the cached values from
325
324
  # the tck file
326
- tck_filename = filename.replace(".csv", ".tck")
325
+ tck_filename = filename.replace(".csv", ".npz")
327
326
  valid_cache = False
328
327
  if os.path.exists(tck_filename):
329
328
  if no_curvefit_options:
@@ -338,11 +337,13 @@ class CSVLookupTableMixin(OptimizationProblem):
338
337
  output
339
338
  )
340
339
  )
341
- with open(tck_filename, "rb") as f:
342
- try:
343
- tck, function = pickle.load(f)
344
- except Exception:
345
- valid_cache = False
340
+ try:
341
+ with np.load(filename.replace(".csv", ".npz")) as data:
342
+ tck = (data["arr_0"], data["arr_1"], int(data["arr_2"]))
343
+ function = ca.Function.load(filename.replace(".csv", ".ca"))
344
+ except Exception:
345
+ valid_cache = False
346
+
346
347
  if not valid_cache:
347
348
  logger.info("CSVLookupTableMixin: Recalculating tck values for {}".format(output))
348
349
 
@@ -446,11 +447,8 @@ class CSVLookupTableMixin(OptimizationProblem):
446
447
  )
447
448
 
448
449
  if not valid_cache:
449
- pickle.dump(
450
- (tck, function),
451
- open(filename.replace(".csv", ".tck"), "wb"),
452
- protocol=pickle.HIGHEST_PROTOCOL,
453
- )
450
+ np.savez(filename.replace(".csv", ".npz"), *tck)
451
+ function.save(filename.replace(".csv", ".ca"))
454
452
 
455
453
  def lookup_tables(self, ensemble_member):
456
454
  # Call parent class first for default values.
@@ -48,9 +48,21 @@ class ModelicaMixin(OptimizationProblem):
48
48
  else:
49
49
  model_name = self.__class__.__name__
50
50
 
51
- self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
52
- kwargs["model_folder"], model_name, self.compiler_options()
53
- )
51
+ compiler_options = self.compiler_options()
52
+ logger.info(f"Loading/compiling model {model_name}.")
53
+ try:
54
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
55
+ kwargs["model_folder"], model_name, compiler_options
56
+ )
57
+ except RuntimeError as error:
58
+ if compiler_options.get("cache", False):
59
+ raise error
60
+ compiler_options["cache"] = False
61
+ logger.warning(f"Loading model {model_name} using a cache file failed: {error}.")
62
+ logger.info(f"Compiling model {model_name}.")
63
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
64
+ kwargs["model_folder"], model_name, compiler_options
65
+ )
54
66
 
55
67
  # Extract the CasADi MX variables used in the model
56
68
  self.__mx = {}
@@ -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(
289
+ self, "_PIMixin__timeseries_import"
290
+ ), "set_unit can only be called after read() in pre() has finished."
291
+ self.__timeseries_import.set_unit(variable, unit, 0)
292
+ self.__timeseries_export.set_unit(variable, unit, 0)
rtctools/rtctoolsapp.py CHANGED
@@ -108,7 +108,8 @@ def download_examples(*args):
108
108
 
109
109
  opener = urllib.request.build_opener()
110
110
  urllib.request.install_opener(opener)
111
- local_filename, _ = urllib.request.urlretrieve(url)
111
+ # The security warning can be dismissed as the url variable is hardcoded to a remote.
112
+ local_filename, _ = urllib.request.urlretrieve(url) # nosec
112
113
  except HTTPError:
113
114
  sys.exit("Could not found examples for RTC-Tools version {}.".format(version))
114
115
 
@@ -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(
252
+ self, "_PIMixin__timeseries_import"
253
+ ), "set_unit can only be called after read() in pre() has finished."
254
+ self.__timeseries_import.set_unit(variable, unit, 0)
255
+ self.__timeseries_export.set_unit(variable, unit, 0)
@@ -82,9 +82,21 @@ class SimulationProblem(DataStoreAccessor):
82
82
  model_name = self.__class__.__name__
83
83
 
84
84
  # Load model from pymoca backend
85
- self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
86
- kwargs["model_folder"], model_name, self.compiler_options()
87
- )
85
+ compiler_options = self.compiler_options()
86
+ logger.info(f"Loading/compiling model {model_name}.")
87
+ try:
88
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
89
+ kwargs["model_folder"], model_name, compiler_options
90
+ )
91
+ except RuntimeError as error:
92
+ if compiler_options.get("cache", False):
93
+ raise error
94
+ compiler_options["cache"] = False
95
+ logger.warning(f"Loading model {model_name} using a cache file failed: {error}.")
96
+ logger.info(f"Compiling model {model_name}.")
97
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
98
+ kwargs["model_folder"], model_name, compiler_options
99
+ )
88
100
 
89
101
  # Extract the CasADi MX variables used in the model
90
102
  self.__mx = {}
@@ -319,6 +331,17 @@ class SimulationProblem(DataStoreAccessor):
319
331
  """
320
332
  Initialize state vector with default values
321
333
 
334
+ Initial values are first read from the given Modelica files.
335
+ If an initial value equals zero or is not provided by a Modelica file,
336
+ and the variable is not marked as fixed,
337
+ then the initial value is tried to be set with the initial_state method.
338
+ When using CSVMixin, this method by default looks for initial values
339
+ in an initial_state.csv file.
340
+ Furthermore, if a variable is not marked as fixed
341
+ and no initial value is given by the initial_state method,
342
+ the initial value can be overwritten using the seed method.
343
+ When a variable is marked as fixed, the initial value is only read from the Modelica file.
344
+
322
345
  :param config_file: Path to an initialization file.
323
346
  """
324
347
  if config_file:
@@ -393,29 +416,35 @@ class SimulationProblem(DataStoreAccessor):
393
416
  for var in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states):
394
417
  var_name = var.symbol.name()
395
418
  var_nominal = self.get_variable_nominal(var_name)
419
+ start_values = {}
396
420
 
397
421
  # Attempt to cast var.start to python type
422
+ start_value_is_symbolic = False
398
423
  mx_start = ca.MX(var.start)
399
424
  if mx_start.is_constant():
400
425
  # cast var.start to python type
401
- start_val = var.python_type(mx_start.to_DM())
426
+ start_value_pymoca = var.python_type(mx_start.to_DM())
427
+ if start_value_pymoca is not None and start_value_pymoca != 0:
428
+ start_values["modelica"] = start_value_pymoca
402
429
  else:
403
- # var.start is a symbolic expression with unknown value
404
- start_val = None
430
+ start_value_is_symbolic = True
431
+ start_values["modelica"] = mx_start
405
432
 
406
- if start_val == 0.0 and not var.fixed:
433
+ if not var.fixed:
407
434
  # To make initialization easier, we allow setting initial states by providing
408
435
  # timeseries with names that match a symbol in the model. We only check for this
409
436
  # matching if the start and fixed attributes were left as default
410
437
  try:
411
- start_val = self.initial_state()[var_name]
438
+ start_values["initial_state"] = self.initial_state()[var_name]
412
439
  except KeyError:
413
440
  pass
414
441
  else:
415
442
  # An initial state was found- add it to the constrained residuals
416
443
  logger.debug(
417
444
  "Initialize: Added {} = {} to initial equations "
418
- "(found matching timeseries).".format(var_name, start_val)
445
+ "(found matching timeseries).".format(
446
+ var_name, start_values["initial_state"]
447
+ )
419
448
  )
420
449
  # Set var to be fixed
421
450
  var.fixed = True
@@ -425,36 +454,75 @@ class SimulationProblem(DataStoreAccessor):
425
454
  # timeseries with names that match a symbol in the model. We only check for this
426
455
  # matching if the start and fixed attributes were left as default
427
456
  try:
428
- start_val = self.seed()[var_name]
457
+ start_values["seed"] = self.seed()[var_name]
429
458
  except KeyError:
430
459
  pass
431
460
  else:
432
461
  # An initial state was found- add it to the constrained residuals
433
462
  logger.debug(
434
463
  "Initialize: Added {} = {} as initial guess "
435
- "(found matching timeseries).".format(var_name, start_val)
464
+ "(found matching timeseries).".format(var_name, start_values["seed"])
465
+ )
466
+
467
+ # Set the start value based on the different inputs.
468
+ if "seed" in start_values:
469
+ input_source = "seed"
470
+ source_description = "seed method"
471
+ elif "modelica" in start_values:
472
+ input_source = "modelica"
473
+ source_description = "modelica file"
474
+ elif "initial_state" in start_values:
475
+ input_source = "initial_state"
476
+ source_description = "initial_state method (typically reads initial_state.csv)"
477
+ else:
478
+ start_values["modelica"] = start_value_pymoca
479
+ input_source = "modelica"
480
+ source_description = "modelica file or default value"
481
+ start_val = start_values.get(input_source, None)
482
+ is_numeric = start_val is not None and not start_value_is_symbolic
483
+ numeric_start_val = start_val if is_numeric else 0.0
484
+ if len(start_values) > 1:
485
+ logger.warning(
486
+ "Initialize: Multiple initial values for {} are provided: {}.".format(
487
+ var_name, start_values
436
488
  )
489
+ + " Value from {} will be used to continue.".format(source_description)
490
+ )
437
491
 
438
492
  # Attempt to set start_val in the state vector. Default to zero if unknown.
439
493
  try:
440
- self.set_var(var_name, start_val if start_val is not None else 0.0)
494
+ self.set_var(var_name, numeric_start_val)
441
495
  except KeyError:
442
496
  logger.warning(
443
497
  "Initialize: {} not found in state vector. "
444
- "Initial value of {} not set.".format(var_name, start_val)
498
+ "Initial value of {} not set.".format(var_name, numeric_start_val)
445
499
  )
446
500
 
447
501
  # Add a residual for the difference between the state and its starting expression
448
- start_expr = start_val if start_val is not None else var.start
502
+ start_expr = start_val
449
503
  if var.fixed:
450
504
  # Set bounds to be equal to each other, such that IPOPT can
451
505
  # turn the decision variable into a parameter.
506
+ if var.min != -np.inf or var.max != np.inf:
507
+ logger.info(
508
+ "Initialize: bounds of {} will be overwritten".format(var_name)
509
+ + " by the start value given by {}.".format(source_description)
510
+ )
452
511
  var.min = start_expr
453
512
  var.max = start_expr
454
513
  else:
455
514
  # minimize residual
456
515
  minimized_residuals.append((var.symbol - start_expr) / var_nominal)
457
516
 
517
+ # Check that the start_value is in between the variable bounds.
518
+ if start_val is not None and is_numeric:
519
+ if not (var.min <= start_val and start_val <= var.max):
520
+ logger.warning(
521
+ "Initialize: start value {} = {}".format(var_name, start_val)
522
+ + " is not in between bounds {} and {}".format(var.min, var.max)
523
+ + " and will be adjusted."
524
+ )
525
+
458
526
  # Default start var for ders is zero
459
527
  for der_var in self.__mx["derivatives"]:
460
528
  self.set_var(der_var.name(), 0.0)
@@ -610,7 +678,15 @@ class SimulationProblem(DataStoreAccessor):
610
678
  # If unsuccessful, stop.
611
679
  return_status = solver.stats()["return_status"]
612
680
  if return_status not in {"Solve_Succeeded", "Solved_To_Acceptable_Level"}:
613
- raise Exception('Initialization Failed with return status "{}"'.format(return_status))
681
+ if return_status == "Infeasible_Problem_Detected":
682
+ message = (
683
+ "Initialization Failed with return status: {}. ".format(return_status)
684
+ + "This means no initial state could be found "
685
+ + "that satisfies all equations and constraints."
686
+ )
687
+ else:
688
+ message = "Initialization Failed with return status: {}. ".format(return_status)
689
+ raise Exception(message)
614
690
 
615
691
  # Update state vector with initial conditions
616
692
  self.__state_vector[: self.__n_states] = initial_state["x"][: self.__n_states].T