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.
- {rtc_tools-2.6.0b2.dist-info → rtc_tools-2.7.0.dist-info}/METADATA +26 -15
- rtc_tools-2.7.0.dist-info/RECORD +50 -0
- {rtc_tools-2.6.0b2.dist-info → rtc_tools-2.7.0.dist-info}/WHEEL +1 -1
- {rtc_tools-2.6.0b2.dist-info → rtc_tools-2.7.0.dist-info}/entry_points.txt +0 -1
- rtctools/_internal/casadi_helpers.py +5 -5
- rtctools/_version.py +4 -4
- rtctools/data/csv.py +18 -7
- rtctools/data/interpolation/bspline1d.py +5 -1
- rtctools/data/netcdf.py +16 -15
- rtctools/data/pi.py +72 -41
- rtctools/data/rtc.py +6 -5
- rtctools/optimization/collocated_integrated_optimization_problem.py +14 -17
- rtctools/optimization/control_tree_mixin.py +8 -5
- rtctools/optimization/csv_lookup_table_mixin.py +15 -15
- rtctools/optimization/csv_mixin.py +3 -0
- rtctools/optimization/goal_programming_mixin.py +11 -2
- rtctools/optimization/goal_programming_mixin_base.py +5 -3
- rtctools/optimization/modelica_mixin.py +28 -8
- rtctools/optimization/optimization_problem.py +18 -0
- rtctools/optimization/pi_mixin.py +13 -0
- rtctools/rtctoolsapp.py +17 -14
- rtctools/simulation/io_mixin.py +1 -1
- rtctools/simulation/pi_mixin.py +13 -0
- rtctools/simulation/simulation_problem.py +130 -22
- rtctools/util.py +1 -0
- rtc_tools-2.6.0b2.dist-info/RECORD +0 -50
- {rtc_tools-2.6.0b2.dist-info → rtc_tools-2.7.0.dist-info/licenses}/COPYING.LESSER +0 -0
- {rtc_tools-2.6.0b2.dist-info → rtc_tools-2.7.0.dist-info}/top_level.txt +0 -0
|
@@ -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 =
|
|
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 =
|
|
137
|
+
timeseries_i = constant_inputs[member_i][forecast_variable]
|
|
133
138
|
for j, member_j in enumerate(branches[current_branch]):
|
|
134
|
-
timeseries_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
|
|
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
|
-
|
|
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", ".
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
tck
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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 =
|
|
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)
|
|
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 =
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
185
|
+
for ep in importlib_metadata.entry_points(group="rtctools.libraries.modelica"):
|
|
166
186
|
if ep.name == "library_folder":
|
|
167
|
-
library_folders.append(
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
124
|
+
shutil.rmtree(zip_folder_name)
|
|
122
125
|
|
|
123
126
|
sys.exit("Succesfully downloaded the RTC-Tools examples to '{}'".format(target.resolve()))
|
|
124
127
|
|
rtctools/simulation/io_mixin.py
CHANGED
|
@@ -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.
|
|
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
|
rtctools/simulation/pi_mixin.py
CHANGED
|
@@ -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)
|