Skip to content

Commit d89f46b

Browse files
authored
Merge pull request #131 from RWTH-EBC/130-add-option-to-dymolaapi-to-start-with-no-model_name [PYPI-RELEASE]
130 add option to dymolaapi to start with no model name
2 parents 66cb6fe + 74635d1 commit d89f46b

File tree

7 files changed

+88
-40
lines changed

7 files changed

+88
-40
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,7 @@
100100
- Preprocessing functions now copy the input
101101
DataFrame by default with the new inplace argument (default False). #125
102102
- Add inplace option to TimeSeriesData function (default True)
103+
- v0.4.1
104+
- fix reproduction with modifiers #82
105+
- enable model_name=None upon startup of DymolaAPI #130
106+
- Only create logger handler if not already done by root-logger

ebcpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
from .optimization import Optimizer
99

1010

11-
__version__ = '0.4.0'
11+
__version__ = '0.4.1'

ebcpy/simulationapi/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def __init__(self, working_directory: Union[Path, str], model_name: str, **kwarg
196196
self.parameters: Dict[str, Variable] = {} # Parameter of model
197197
self.states: Dict[str, Variable] = {} # States of model
198198
self.result_names = []
199+
self._model_name = None
199200
self.model_name = model_name
200201

201202
# MP-Functions
@@ -448,13 +449,13 @@ def model_name(self) -> str:
448449
return self._model_name
449450

450451
@model_name.setter
451-
def model_name(self, model_name):
452+
def model_name(self, model_name: str):
452453
"""
453454
Set new model_name and trigger further functions
454455
to load parameters etc.
455456
"""
456457
# Only update if the model_name actually changes
457-
if hasattr(self, "_model_name") and self._model_name == model_name:
458+
if self._model_name == model_name:
458459
return
459460
self._model_name = model_name
460461
# Only update model if it's the first setup. On multiprocessing,

ebcpy/simulationapi/dymola_api.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import os
66
import shutil
7-
import pathlib
7+
import uuid
88
import warnings
99
import atexit
1010
import json
@@ -49,7 +49,8 @@ class DymolaAPI(SimulationAPI):
4949
:param str,Path working_directory:
5050
Dirpath for the current working directory of dymola
5151
:param str model_name:
52-
Name of the model to be simulated
52+
Name of the model to be simulated.
53+
If None, it has to be provided prior to or when calling simulate().
5354
:param list packages:
5455
List with path's to the packages needed to simulate the model
5556
:keyword Boolean show_window:
@@ -156,7 +157,7 @@ class DymolaAPI(SimulationAPI):
156157
def __init__(
157158
self,
158159
working_directory: Union[Path, str],
159-
model_name: str,
160+
model_name: str = None,
160161
packages: List[Union[Path, str]] = None,
161162
**kwargs
162163
):
@@ -223,8 +224,9 @@ def __init__(
223224
"Thus, not able to find the `dymola_exe_path` and `dymola_interface_path`. "
224225
"Either specify both or pass an existing `dymola_path`."
225226
)
227+
self.dymola_path = dymola_path
226228
if self.dymola_exe_path is None:
227-
self.dymola_exe_path = self.get_dymola_path(dymola_path)
229+
self.dymola_exe_path = self.get_dymola_exe_path(dymola_path)
228230
self.logger.info("Using dymola.exe: %s", self.dymola_exe_path)
229231
if self.dymola_interface_path is None:
230232
self.dymola_interface_path = self.get_dymola_interface_path(dymola_path)
@@ -271,10 +273,14 @@ def __init__(
271273
)
272274
# For translation etc. always setup a default dymola instance
273275
self.dymola = self._setup_dymola_interface(dict(use_mp=False))
276+
if not self.license_is_available():
277+
warnings.warn("You have no licence to use Dymola. "
278+
"Hence you can only simulate models with 8 or less equations.")
274279

275280
self.fully_initialized = True
276281
# Trigger on init.
277-
self._update_model()
282+
if model_name is not None:
283+
self._update_model()
278284
# Set result_names to output variables.
279285
self.result_names = list(self.outputs.keys())
280286

@@ -407,6 +413,13 @@ def _single_simulation(self, kwargs):
407413
" ,".join(list(set(_res_names).difference(self.result_names)))
408414
)
409415

416+
if self.model_name is None:
417+
raise ValueError(
418+
"You neither passed a model_name when "
419+
"starting DymolaAPI, nor when calling simulate. "
420+
"Can't simulate no model."
421+
)
422+
410423
# Handle parameters:
411424
if parameters is None:
412425
parameters = {}
@@ -824,14 +837,18 @@ def _setup_dymola_interface(self, kwargs: dict):
824837
# Events can also cause errors in the shape.
825838
dymola.experimentSetupOutput(equidistant=True,
826839
events=False)
827-
if not dymola.RequestOption("Standard"):
828-
warnings.warn("You have no licence to use Dymola. "
829-
"Hence you can only simulate models with 8 or less equations.")
830840
if use_mp:
831841
DymolaAPI.dymola = dymola
832842
return None
833843
return dymola
834844

845+
def license_is_available(self, option: str = "Standard"):
846+
"""Check if license is available"""
847+
if self.dymola is None:
848+
warnings.warn("You want to check the license before starting dymola, this is not supported.")
849+
return False
850+
return self.dymola.RequestOption(option)
851+
835852
def _open_dymola_interface(self, port):
836853
"""Open an instance of dymola and return the API-Object"""
837854
if self.dymola_interface_path not in sys.path:
@@ -968,9 +985,27 @@ def save_for_reproduction(
968985
_total_model_name = f"Dymola/{self.model_name.replace('.', '_')}_total.mo"
969986
_total_model = Path(self.cd).joinpath(_total_model_name)
970987
os.makedirs(_total_model.parent, exist_ok=True) # Create to ensure model can be saved.
988+
if "(" in self.model_name:
989+
# Create temporary model:
990+
temp_model_file = Path(self.cd).joinpath(f"temp_total_model_{uuid.uuid4()}.mo")
991+
temp_mode_name = f"{self.model_name.split('(')[0].split('.')[-1]}WithModifier"
992+
with open(temp_model_file, "w") as file:
993+
file.write(f"model {temp_mode_name}\n extends {self.model_name};\nend {temp_mode_name};")
994+
res = self.dymola.openModel(str(temp_model_file), changeDirectory=False)
995+
if not res:
996+
self.logger.error(
997+
"Could not create separate model for model with modifiers: %s",
998+
self.model_name
999+
)
1000+
model_name_to_save = self.model_name
1001+
else:
1002+
model_name_to_save = temp_mode_name
1003+
os.remove(temp_model_file)
1004+
else:
1005+
model_name_to_save = self.model_name
9711006
res = self.dymola.saveTotalModel(
9721007
fileName=str(_total_model),
973-
modelName=self.model_name
1008+
modelName=model_name_to_save
9741009
)
9751010
if res:
9761011
files.append(ReproductionFile(
@@ -1061,7 +1096,7 @@ def get_dymola_interface_path(dymola_install_dir):
10611096
return egg_file
10621097

10631098
@staticmethod
1064-
def get_dymola_path(dymola_install_dir, dymola_name=None):
1099+
def get_dymola_exe_path(dymola_install_dir, dymola_name=None):
10651100
"""
10661101
Function to get the path of the dymola exe-file
10671102
on the current used machine.

ebcpy/utils/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ def setup_logger(name: str,
3131
# Check if logger was already instantiated. If so, return already.
3232
if logger.handlers:
3333
return logger
34-
# Add handlers
34+
# Add handlers if not set already by logging.basicConfig and if path is specified
3535
formatter = logging.Formatter(fmt='%(asctime)s %(levelname)s %(name)s: %(message)s',
3636
datefmt='%d.%m.%Y-%H:%M:%S')
37-
console = logging.StreamHandler()
38-
console.setFormatter(fmt=formatter)
39-
logger.addHandler(hdlr=console)
37+
if not logging.getLogger().hasHandlers():
38+
console = logging.StreamHandler()
39+
console.setFormatter(fmt=formatter)
40+
logger.addHandler(hdlr=console)
4041
if working_directory is not None:
4142
os.makedirs(working_directory, exist_ok=True)
4243
file_handler = logging.FileHandler(filename=working_directory.joinpath(f"{name}.log"))

examples/e5_modifier_example.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,19 @@
1010

1111

1212
def main(
13-
aixlib_mo,
14-
ibpsa_mo,
15-
besmod_mo,
13+
besmod_startup_mos,
1614
ext_model_name,
1715
working_directory=None,
18-
n_cpu=1,
19-
with_plot=True
16+
n_cpu=1
2017
):
2118
"""
2219
Arguments of this example:
23-
:param str aixlib_mo:
24-
Path to the package.mo of the AixLib.
25-
This example was tested for AixLib version 1.3.2.
26-
:param str ibpsa_mo:
27-
Path to the package.mo of the IBSPA.
28-
This example was tested for IBSPA version 3.0.0.
29-
:param str besmod_mo:
30-
Path to the package.mo of the BESMod.
20+
21+
:param str besmod_startup_mos:
22+
Path to the startup.mos of the BESMod.
3123
This example was tested for BESMod version 0.4.0.
24+
This example was tested for IBSPA version 3.0.0.
25+
This example was tested for AixLib version 1.3.2.
3226
:param list ext_model_name:
3327
Executable model name with redeclared subsystems and modifiers.
3428
:param str working_directory:
@@ -50,7 +44,7 @@ def main(
5044
model_name=ext_model_name,
5145
working_directory=working_directory,
5246
n_cpu=n_cpu,
53-
packages=[aixlib_mo, ibpsa_mo, besmod_mo],
47+
mos_script_pre=besmod_startup_mos,
5448
show_window=False,
5549
# Only necessary if you need a specific dymola version
5650
dymola_path=r"C:\Program Files\Dymola 2023x",
@@ -77,33 +71,33 @@ def main(
7771
# You can also simulate a list of different `model_names` (or modified versions of the same model)
7872
# by passing a list to the `simulate` function in `DymolaAPI`:
7973
model_names_to_simulate = [
74+
"BESMod.Examples.DesignOptimization.BES",
8075
"BESMod.Examples.GasBoilerBuildingOnly(redeclare BESMod.Systems.Control.DHWSuperheating control(dTDHW=10))",
8176
"BESMod.Examples.GasBoilerBuildingOnly(redeclare BESMod.Systems.Control.DHWSuperheating control(dTDHW=5))",
82-
"BESMod.Examples.DesignOptimization.BES",
8377
]
8478
results = dym_api.simulate(
8579
return_option="time_series",
8680
model_names=model_names_to_simulate
8781
)
8882
print(results)
89-
83+
dym_api.save_for_reproduction(
84+
title="FMUTest",
85+
log_message="This is just an example."
86+
)
9087
# ######################### Closing ##########################
9188
# Close Dymola. If you forget to do so,
9289
# we call this function at the exit of your script.
9390
dym_api.close()
9491

9592

9693
if __name__ == '__main__':
97-
# TODO-User: Change the AixLib and BESMod path!
98-
94+
# TODO-User: Change the BESMod path!
9995
# call function main
10096
# - External libraries AixLib, IBSPA and BESMod will be loaded
10197
# - Model ext_model_name will be called. Subsystem for controller will be exchanged from NoControl to DHWSuperheating.
10298
# Additional to the new subsystem, the parameter dTDHW will be set from 5 K to 10 K.
10399
# Furthermore, inside the main function, a method for simulating multiple models at one call is shown.
104100
main(
105-
aixlib_mo=r"D:\900_repository\000_general\AixLib-1.3.2\AixLib\package.mo",
106-
ibpsa_mo=r"D:\900_repository\000_general\modelica-ibpsa-master\IBPSA\package.mo",
107-
besmod_mo=r"D:\900_repository\000_general\BESMod\BESMod\package.mo",
101+
besmod_startup_mos=r"D:\04_git\BESMod\startup.mos",
108102
ext_model_name='BESMod.Examples.GasBoilerBuildingOnly(redeclare BESMod.Systems.Control.DHWSuperheating control(dTDHW=10))'
109103
)

tests/test_simulationapi.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def setUp(self) -> None:
169169
if self.__class__ == PartialTestDymolaAPI:
170170
self.skipTest("Just a partial class")
171171
ebcpy_test_package_dir = self.data_dir.joinpath("TestModelVariables.mo")
172-
packages = [ebcpy_test_package_dir]
172+
self.packages = [ebcpy_test_package_dir]
173173
model_name = "TestModelVariables"
174174
self.parameters = {"test_real": 10.0,
175175
"test_int": 5,
@@ -182,7 +182,11 @@ def setUp(self) -> None:
182182
}
183183
# Mos script
184184
mos_script = self.data_dir.joinpath("mos_script_test.mos")
185+
self._start_dymola_api(
186+
model_name=model_name, mos_script=mos_script
187+
)
185188

189+
def _start_dymola_api(self, model_name: str, mos_script):
186190
# Just for tests in the gitlab-ci:
187191
if "linux" in sys.platform:
188192
dymola_exe_path = "/usr/local/bin/dymola"
@@ -192,7 +196,7 @@ def setUp(self) -> None:
192196
self.sim_api = dymola_api.DymolaAPI(
193197
working_directory=self.example_sim_dir,
194198
model_name=model_name,
195-
packages=packages,
199+
packages=self.packages,
196200
dymola_exe_path=dymola_exe_path,
197201
n_cpu=self.n_cpu,
198202
mos_script_pre=mos_script,
@@ -202,6 +206,15 @@ def setUp(self) -> None:
202206
self.skipTest(f"Could not load the dymola interface "
203207
f"on this machine. Error message: {error}")
204208

209+
def test_no_model_none(self):
210+
self.sim_api.close()
211+
self._start_dymola_api(
212+
mos_script=None, model_name=None,
213+
)
214+
with self.assertRaises(ValueError):
215+
self.sim_api.simulate()
216+
self.sim_api.simulate(model_names=["TestModelVariables"], parameters=self.parameters)
217+
205218
def test_close(self):
206219
"""Test close functionality of dymola api"""
207220
self.sim_api.close()

0 commit comments

Comments
 (0)