Coverage for ebcpy/simulationapi/dymola_api.py: 64%

604 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-03-24 10:18 +0000

1"""Module containing the DymolaAPI used for simulation 

2of Modelica-Models.""" 

3 

4import sys 

5import os 

6import shutil 

7import uuid 

8import warnings 

9import atexit 

10import json 

11import time 

12import socket 

13from pathlib import Path 

14from contextlib import closing 

15from typing import Union, List 

16 

17from pydantic import Field, BaseModel 

18import pandas as pd 

19 

20from ebcpy import TimeSeriesData 

21from ebcpy.modelica import manipulate_ds 

22from ebcpy.simulationapi import SimulationSetup, SimulationAPI, \ 

23 SimulationSetupClass, Variable 

24from ebcpy.utils.conversion import convert_tsd_to_modelica_txt 

25 

26 

27class DymolaSimulationSetup(SimulationSetup): 

28 """ 

29 Adds ``tolerance`` to the list of possible 

30 setup fields. 

31 """ 

32 tolerance: float = Field( 

33 title="tolerance", 

34 default=0.0001, 

35 description="Tolerance of integration" 

36 ) 

37 

38 _default_solver = "Dassl" 

39 _allowed_solvers = ["Dassl", "Euler", "Cerk23", "Cerk34", "Cerk45", 

40 "Esdirk23a", "Esdirk34a", "Esdirk45a", "Cvode", 

41 "Rkfix2", "Rkfix3", "Rkfix4", "Lsodar", 

42 "Radau", "Dopri45", "Dopri853", "Sdirk34hw"] 

43 

44 

45class ExperimentSetupOutput(BaseModel): 

46 """ 

47 Experiment setup output data model with 

48 defaults equal to those in Dymola 

49 """ 

50 states: bool = True 

51 derivatives: bool = True 

52 inputs: bool = True 

53 outputs: bool = True 

54 auxiliaries: bool = True 

55 equidistant: bool = False 

56 events: bool = True 

57 

58 class Config: 

59 """ 

60 Pydantic internal model settings 

61 """ 

62 # pylint: disable=too-few-public-methods 

63 extra = "forbid" 

64 

65 

66class DymolaAPI(SimulationAPI): 

67 """ 

68 API to a Dymola instance. 

69 

70 :param str,Path working_directory: 

71 Dirpath for the current working directory of dymola 

72 :param str model_name: 

73 Name of the model to be simulated. 

74 If None, it has to be provided prior to or when calling simulate(). 

75 :param list packages: 

76 List with path's to the packages needed to simulate the model 

77 :keyword Boolean show_window: 

78 True to show the Dymola window. Default is False 

79 :keyword Boolean modify_structural_parameters: 

80 True to automatically set the structural parameters of the 

81 simulation model via Modelica modifiers. Default is True. 

82 See also the keyword ``structural_parameters`` 

83 of the ``simulate`` function. 

84 :keyword Boolean equidistant_output: 

85 If True (Default), Dymola stores variables in an 

86 equisdistant output and does not store variables at events. 

87 :keyword dict[str,bool] variables_to_save: 

88 A dictionary to select which variables are going 

89 to be stored if the simulation creates .mat files. 

90 Options (with the default being all True): 

91 - states=True 

92 - derivatives=True 

93 - inputs=True 

94 - outputs=True 

95 - auxiliaries=False 

96 :keyword int n_restart: 

97 Number of iterations after which Dymola should restart. 

98 This is done to free memory. Default value -1. For values 

99 below 1 Dymola does not restart. 

100 :keyword bool extract_variables: 

101 If True (the default), all variables of the model will be extracted 

102 on init of this class. 

103 This required translating the model. 

104 :keyword bool debug: 

105 If True (not the default), the dymola instance is not closed 

106 on exit of the python script. This allows further debugging in 

107 dymola itself if API-functions cause a python error. 

108 :keyword str mos_script_pre: 

109 Path to a valid mos-script for Modelica/Dymola. 

110 If given, the script is executed **prior** to laoding any 

111 package specified in this API. 

112 May be relevant for handling version conflicts. 

113 :keyword str mos_script_post: 

114 Path to a valid mos-script for Modelica/Dymola. 

115 If given, the script is executed before closing Dymola. 

116 :keyword str dymola_version: 

117 Version of Dymola to use. 

118 If not given, newest version will be used. 

119 If given, the Version needs to be equal to the folder name 

120 of your installation. 

121 

122 **Example:** If you have two versions installed at 

123 

124 - ``C://Program Files//Dymola 2021`` and 

125 - ``C://Program Files//Dymola 2020x`` 

126 

127 and you want to use Dymola 2020x, specify 

128 ``dymola_version='Dymola 2020x'``. 

129 

130 This parameter is overwritten if ``dymola_path`` is specified. 

131 :keyword str dymola_path: 

132 Path to the dymola installation on the device. Necessary 

133 e.g. on linux, if we can't find the path automatically. 

134 Example: ``dymola_path="C://Program Files//Dymola 2020x"`` 

135 :keyword str dymola_interface_path: 

136 Direct path to the .egg-file of the dymola interface. 

137 Only relevant when the dymola_path 

138 differs from the interface path. 

139 :keyword str dymola_exe_path: 

140 Direct path to the dymola executable. 

141 Only relevant if the dymola installation do not follow 

142 the official guideline. 

143 :keyword float time_delay_between_starts: 

144 If starting multiple Dymola instances on multiple 

145 cores, a time delay between each start avoids weird 

146 behaviour, such as requiring to set the C-Compiler again 

147 as Dymola overrides the default .dymx setup file. 

148 If you start e.g. 20 instances and specify `time_delay_between_starts=5`, 

149 each 5 seconds one instance will start, taking in total 

150 100 seconds. Default is no delay. 

151 

152 Example: 

153 

154 >>> import os 

155 >>> from ebcpy import DymolaAPI 

156 >>> # Specify the model name 

157 >>> model_name = "Modelica.Thermal.FluidHeatFlow.Examples.PumpAndValve" 

158 >>> dym_api = DymolaAPI(working_directory=os.getcwd(), 

159 >>> model_name=model_name, 

160 >>> packages=[], 

161 >>> show_window=True) 

162 >>> dym_api.sim_setup = {"start_time": 100, 

163 >>> "stop_time": 200} 

164 >>> dym_api.simulate() 

165 >>> dym_api.close() 

166 

167 """ 

168 _sim_setup_class: SimulationSetupClass = DymolaSimulationSetup 

169 _items_to_drop = ["pool", "dymola", "_dummy_dymola_instance"] 

170 dymola = None 

171 # Default simulation setup 

172 _supported_kwargs = [ 

173 "show_window", 

174 "modify_structural_parameters", 

175 "dymola_path", 

176 "equidistant_output", 

177 "variables_to_save", 

178 "n_restart", 

179 "debug", 

180 "mos_script_pre", 

181 "mos_script_post", 

182 "dymola_version", 

183 "dymola_interface_path", 

184 "dymola_exe_path", 

185 "time_delay_between_starts" 

186 ] 

187 

188 def __init__( 

189 self, 

190 working_directory: Union[Path, str], 

191 model_name: str = None, 

192 packages: List[Union[Path, str]] = None, 

193 **kwargs 

194 ): 

195 """Instantiate class objects.""" 

196 self.dymola = None # Avoid key-error in get-state. Instance attribute needs to be there. 

197 # Update kwargs with regard to what kwargs are supported. 

198 self.extract_variables = kwargs.pop("extract_variables", True) 

199 self.fully_initialized = False 

200 self.debug = kwargs.pop("debug", False) 

201 self.show_window = kwargs.pop("show_window", False) 

202 self.modify_structural_parameters = kwargs.pop("modify_structural_parameters", True) 

203 self.equidistant_output = kwargs.pop("equidistant_output", True) 

204 _variables_to_save = kwargs.pop("variables_to_save", {}) 

205 self.experiment_setup_output = ExperimentSetupOutput(**_variables_to_save) 

206 

207 self.mos_script_pre = kwargs.pop("mos_script_pre", None) 

208 self.mos_script_post = kwargs.pop("mos_script_post", None) 

209 self.dymola_version = kwargs.pop("dymola_version", None) 

210 self.dymola_interface_path = kwargs.pop("dymola_interface_path", None) 

211 self.dymola_exe_path = kwargs.pop("dymola_exe_path", None) 

212 _time_delay_between_starts = kwargs.pop("time_delay_between_starts", 0) 

213 for mos_script in [self.mos_script_pre, self.mos_script_post]: 

214 if mos_script is not None: 

215 if not os.path.isfile(mos_script): 

216 raise FileNotFoundError( 

217 f"Given mos_script '{mos_script}' does " 

218 f"not exist." 

219 ) 

220 if not str(mos_script).endswith(".mos"): 

221 raise TypeError( 

222 f"Given mos_script '{mos_script}' " 

223 f"is not a valid .mos file." 

224 ) 

225 

226 # Convert to modelica path 

227 if self.mos_script_pre is not None: 

228 self.mos_script_pre = self._make_modelica_normpath(self.mos_script_pre) 

229 if self.mos_script_post is not None: 

230 self.mos_script_post = self._make_modelica_normpath(self.mos_script_post) 

231 

232 super().__init__(working_directory=working_directory, 

233 model_name=model_name, 

234 n_cpu=kwargs.pop("n_cpu", 1), 

235 save_logs=kwargs.pop("save_logs", True)) 

236 

237 # First import the dymola-interface 

238 dymola_path = kwargs.pop("dymola_path", None) 

239 if dymola_path is not None: 

240 if not os.path.exists(dymola_path): 

241 raise FileNotFoundError(f"Given path '{dymola_path}' can not be found on " 

242 "your machine.") 

243 else: 

244 # Get the dymola-install-path: 

245 _dym_installations = self.get_dymola_install_paths() 

246 if _dym_installations: 

247 if self.dymola_version: 

248 dymola_path = _get_dymola_path_of_version( 

249 dymola_installations=_dym_installations, 

250 dymola_version=self.dymola_version 

251 ) 

252 else: 

253 dymola_path = _dym_installations[0] # 0 is the newest 

254 self.logger.info("Using dymola installation at %s", dymola_path) 

255 else: 

256 if self.dymola_exe_path is None or self.dymola_interface_path is None: 

257 raise FileNotFoundError( 

258 "Could not find dymola on your machine. " 

259 "Thus, not able to find the `dymola_exe_path` and `dymola_interface_path`. " 

260 "Either specify both or pass an existing `dymola_path`." 

261 ) 

262 self.dymola_path = dymola_path 

263 if self.dymola_exe_path is None: 

264 self.dymola_exe_path = self.get_dymola_exe_path(dymola_path) 

265 self.logger.info("Using dymola.exe: %s", self.dymola_exe_path) 

266 if self.dymola_interface_path is None: 

267 self.dymola_interface_path = self.get_dymola_interface_path(dymola_path) 

268 self.logger.info("Using dymola interface: %s", self.dymola_interface_path) 

269 

270 self.packages = [] 

271 if packages is not None: 

272 for package in packages: 

273 if isinstance(package, Path): 

274 self.packages.append(str(package)) 

275 elif isinstance(package, str): 

276 self.packages.append(package) 

277 else: 

278 raise TypeError(f"Given package is of type {type(package)}" 

279 f" but should be any valid path.") 

280 

281 # Import n_restart 

282 self.sim_counter = 0 

283 self.n_restart = kwargs.pop("n_restart", -1) 

284 if not isinstance(self.n_restart, int): 

285 raise TypeError(f"n_restart has to be type int but " 

286 f"is of type {type(self.n_restart)}") 

287 

288 self._dummy_dymola_instance = None # Ensure self._close_dummy gets the attribute. 

289 if self.n_restart > 0: 

290 self.logger.info("Open blank placeholder Dymola instance to ensure" 

291 " a licence during Dymola restarts") 

292 # Use standard port allocation, should always work 

293 self._dummy_dymola_instance = self._open_dymola_interface(port=-1) 

294 atexit.register(self._close_dummy) 

295 

296 # List storing structural parameters for later modifying the simulation-name. 

297 # Parameter for raising a warning if to many dymola-instances are running 

298 self._critical_number_instances = 10 + self.n_cpu 

299 # Register the function now in case of an error. 

300 if not self.debug: 

301 atexit.register(self.close) 

302 if self.use_mp: 

303 ports = _get_n_available_ports(n_ports=self.n_cpu) 

304 self.pool.map( 

305 self._setup_dymola_interface, 

306 [dict(use_mp=True, port=port, time_delay=i * _time_delay_between_starts) 

307 for i, port in enumerate(ports)] 

308 ) 

309 # For translation etc. always setup a default dymola instance 

310 self.dymola = self._setup_dymola_interface(dict(use_mp=False)) 

311 if not self.license_is_available(): 

312 warnings.warn("You have no licence to use Dymola. " 

313 "Hence you can only simulate models with 8 or less equations.") 

314 # Update experiment setup output 

315 self.update_experiment_setup_output(self.experiment_setup_output) 

316 self.fully_initialized = True 

317 # Trigger on init. 

318 if model_name is not None: 

319 self._update_model() 

320 # Set result_names to output variables. 

321 self.result_names = list(self.outputs.keys()) 

322 

323 # Check if some kwargs are still present. If so, inform the user about 

324 # false usage of kwargs: 

325 if kwargs: 

326 self.logger.error( 

327 "You passed the following kwargs which " 

328 "are not part of the supported kwargs and " 

329 "have thus no effect: %s.", " ,".join(list(kwargs.keys()))) 

330 

331 def _update_model(self): 

332 # Translate the model and extract all variables, 

333 # if the user wants to: 

334 if self.extract_variables and self.fully_initialized: 

335 self.extract_model_variables() 

336 

337 def simulate(self, 

338 parameters: Union[dict, List[dict]] = None, 

339 return_option: str = "time_series", 

340 **kwargs): 

341 """ 

342 Simulate the given parameters. 

343 

344 Additional settings: 

345 

346 :keyword List[str] model_names: 

347 List of Dymola model-names to simulate. Should be either the size 

348 of parameters or parameters needs to be sized 1. 

349 Keep in mind that different models may use different parameters! 

350 :keyword Boolean show_eventlog: 

351 Default False. True to show evenlog of simulation (advanced) 

352 :keyword Boolean squeeze: 

353 Default True. If only one set of initialValues is provided, 

354 a DataFrame is returned directly instead of a list. 

355 :keyword str table_name: 

356 If inputs are given, you have to specify the name of the table 

357 in the instance of CombiTimeTable. In order for the inputs to 

358 work the value should be equal to the value of 'tableName' in Modelica. 

359 :keyword str file_name: 

360 If inputs are given, you have to specify the file_name of the table 

361 in the instance of CombiTimeTable. In order for the inputs to 

362 work the value should be equal to the value of 'fileName' in Modelica. 

363 :keyword callable postprocess_mat_result: 

364 When choosing return_option savepath and no equidistant output, the mat files may take up 

365 a lot of disk space while you are only interested in some variables or parts 

366 of the simulation results. This features enables you to pass any function which 

367 gets the mat-path as an input and returns some result you are interested in. 

368 The function signature is `foo(mat_result_file, **kwargs_postprocessing) -> Any`. 

369 Be sure to define the function in a global scope to allow multiprocessing. 

370 :keyword dict kwargs_postprocessing: 

371 Keyword arguments used in the function `postprocess_mat_result`. 

372 :keyword List[str] structural_parameters: 

373 A list containing all parameter names which are structural in Modelica. 

374 This means a modifier has to be created in order to change 

375 the value of this parameter. Internally, the given list 

376 is added to the known states of the model. Hence, you only have to 

377 specify this keyword argument if your structural parameter 

378 does not appear in the dsin.txt file created during translation. 

379 

380 Example: 

381 Changing a record in a model: 

382 

383 >>> sim_api.simulate( 

384 >>> parameters={"parameterPipe": "AixLib.DataBase.Pipes.PE_X.DIN_16893_SDR11_d160()"}, 

385 >>> structural_parameters=["parameterPipe"]) 

386 

387 """ 

388 # Handle special case for structural_parameters 

389 if "structural_parameters" in kwargs: 

390 _struc_params = kwargs["structural_parameters"] 

391 # Check if input is 2-dimensional for multiprocessing. 

392 # If not, make it 2-dimensional to avoid list flattening in 

393 # the super method. 

394 if not isinstance(_struc_params[0], list): 

395 kwargs["structural_parameters"] = [_struc_params] 

396 if "model_names" in kwargs: 

397 model_names = kwargs["model_names"] 

398 if not isinstance(model_names, list): 

399 raise TypeError("model_names needs to be a list.") 

400 if isinstance(parameters, dict): 

401 # Make an array of parameters to enable correct use of super function. 

402 parameters = [parameters] * len(model_names) 

403 if parameters is None: 

404 parameters = [{}] * len(model_names) 

405 return super().simulate(parameters=parameters, return_option=return_option, **kwargs) 

406 

407 def _single_simulation(self, kwargs): 

408 # Unpack kwargs 

409 show_eventlog = kwargs.pop("show_eventlog", False) 

410 squeeze = kwargs.pop("squeeze", True) 

411 result_file_name = kwargs.pop("result_file_name", 'resultFile') 

412 if not isinstance(result_file_name, str): 

413 raise TypeError(f"result_file_name has to be type str but is of type {type(result_file_name)}") 

414 parameters = kwargs.pop("parameters") 

415 return_option = kwargs.pop("return_option") 

416 model_names = kwargs.pop("model_names", None) 

417 inputs = kwargs.pop("inputs", None) 

418 fail_on_error = kwargs.pop("fail_on_error", True) 

419 structural_parameters = kwargs.pop("structural_parameters", []) 

420 table_name = kwargs.pop("table_name", None) 

421 file_name = kwargs.pop("file_name", None) 

422 savepath = kwargs.pop("savepath", None) 

423 

424 def empty_postprocessing(mat_result, **_kwargs): 

425 return mat_result 

426 

427 postprocess_mat_result = kwargs.pop("postprocess_mat_result", empty_postprocessing) 

428 kwargs_postprocessing = kwargs.pop("kwargs_postprocessing", {}) 

429 if kwargs: 

430 self.logger.error( 

431 "You passed the following kwargs which " 

432 "are not part of the supported kwargs and " 

433 "have thus no effect: %s.", " ,".join(list(kwargs.keys()))) 

434 

435 # Handle multiprocessing 

436 if self.use_mp: 

437 idx_worker = self.worker_idx 

438 if self.dymola is None: 

439 # This should not affect #119, as this rarely happens. Thus, the 

440 # method used in the DymolaInterface should work. 

441 self._setup_dymola_interface(dict(use_mp=True)) 

442 

443 # Re-set the dymola experiment output if API is newly started 

444 self.dymola.experimentSetupOutput(**self.experiment_setup_output.model_dump()) 

445 

446 # Handle eventlog 

447 if show_eventlog: 

448 if not self.experiment_setup_output.events: 

449 raise ValueError("You can't log events and have an " 

450 "equidistant output, set equidistant output=False") 

451 self.dymola.ExecuteCommand("Advanced.Debug.LogEvents = true") 

452 self.dymola.ExecuteCommand("Advanced.Debug.LogEventsInitialization = true") 

453 

454 # Restart Dymola after n_restart iterations 

455 self._check_restart() 

456 

457 # Handle custom model_names 

458 if model_names is not None: 

459 # Custom model_name setting 

460 _res_names = self.result_names.copy() 

461 self._model_name = model_names 

462 self._update_model_variables() 

463 if _res_names != self.result_names: 

464 self.logger.info( 

465 "Result names changed due to setting the new model. " 

466 "If you do not expect custom result names, ignore this warning." 

467 "If you do expect them, please raise an issue to add the " 

468 "option when using the model_names keyword.") 

469 self.logger.info( 

470 "Difference: %s", 

471 " ,".join(list(set(_res_names).difference(self.result_names))) 

472 ) 

473 

474 if self.model_name is None: 

475 raise ValueError( 

476 "You neither passed a model_name when " 

477 "starting DymolaAPI, nor when calling simulate. " 

478 "Can't simulate no model." 

479 ) 

480 

481 # Handle parameters: 

482 if parameters is None: 

483 parameters = {} 

484 unsupported_parameters = False 

485 else: 

486 unsupported_parameters = self.check_unsupported_variables( 

487 variables=list(parameters.keys()), 

488 type_of_var="parameters" 

489 ) 

490 

491 # Handle structural parameters 

492 

493 if (unsupported_parameters and 

494 (self.modify_structural_parameters or 

495 structural_parameters)): 

496 # Alter the model_name for the next simulation 

497 model_name, parameters_new = self._alter_model_name( 

498 parameters=parameters, 

499 model_name=self.model_name, 

500 structural_params=list(self.states.keys()) + structural_parameters 

501 ) 

502 # Trigger translation only if something changed 

503 if model_name != self.model_name: 

504 _res_names = self.result_names.copy() 

505 self.model_name = model_name 

506 self.result_names = _res_names # Restore previous result names 

507 self.logger.warning( 

508 "Warning: Currently, the model is re-translating " 

509 "for each simulation. You should add to your Modelica " 

510 "parameters \"annotation(Evaluate=false)\".\n " 

511 "Check for these parameters: %s", 

512 ', '.join(set(parameters.keys()).difference(parameters_new.keys())) 

513 ) 

514 parameters = parameters_new 

515 # Check again 

516 unsupported_parameters = self.check_unsupported_variables( 

517 variables=list(parameters.keys()), 

518 type_of_var="parameters" 

519 ) 

520 

521 initial_names = list(parameters.keys()) 

522 initial_values = list(parameters.values()) 

523 # Convert to float for Boolean and integer types: 

524 try: 

525 initial_values = [float(v) for v in initial_values] 

526 except (ValueError, TypeError) as err: 

527 raise TypeError("Dymola only accepts float values. " 

528 "Could bot automatically convert the given " 

529 "parameter values to float.") from err 

530 

531 # Handle inputs 

532 if inputs is not None: 

533 # Unpack additional kwargs 

534 if table_name is None or file_name is None: 

535 raise KeyError("For inputs to be used by DymolaAPI.simulate, you " 

536 "have to specify the 'table_name' and the 'file_name' " 

537 "as keyword arguments of the function. These must match" 

538 "the values 'tableName' and 'fileName' in the CombiTimeTable" 

539 " model in your modelica code.") from err 

540 # Generate the input in the correct format 

541 offset = self.sim_setup.start_time - inputs.index[0] 

542 filepath = convert_tsd_to_modelica_txt( 

543 tsd=inputs, 

544 table_name=table_name, 

545 save_path_file=file_name, 

546 offset=offset 

547 ) 

548 self.logger.info("Successfully created Dymola input file at %s", filepath) 

549 

550 if return_option == "savepath": 

551 if unsupported_parameters: 

552 raise KeyError("Dymola does not accept invalid parameter " 

553 "names for option return_type='savepath'. " 

554 "To use this option, delete unsupported " 

555 "parameters from your setup.") 

556 res = self.dymola.simulateExtendedModel( 

557 self.model_name, 

558 startTime=self.sim_setup.start_time, 

559 stopTime=self.sim_setup.stop_time, 

560 numberOfIntervals=0, 

561 outputInterval=self.sim_setup.output_interval, 

562 method=self.sim_setup.solver, 

563 tolerance=self.sim_setup.tolerance, 

564 fixedstepsize=self.sim_setup.fixedstepsize, 

565 resultFile=result_file_name, 

566 initialNames=initial_names, 

567 initialValues=initial_values) 

568 else: 

569 if not parameters and not self.parameters: 

570 raise ValueError( 

571 "Sadly, simulating a model in Dymola " 

572 "with no parameters returns no result. " 

573 "Call this function using return_option='savepath' to get the results." 

574 ) 

575 if not parameters: 

576 random_name = list(self.parameters.keys())[0] 

577 initial_values = [self.parameters[random_name].value] 

578 initial_names = [random_name] 

579 

580 # Handle 1 and 2 D initial names: 

581 # Convert a 1D list to 2D list 

582 if initial_values and isinstance(initial_values[0], (float, int)): 

583 initial_values = [initial_values] 

584 

585 # Handle the time of the simulation: 

586 res_names = self.result_names.copy() 

587 if "Time" not in res_names: 

588 res_names.append("Time") 

589 

590 # Internally convert output Interval to number of intervals 

591 # (Required by function simulateMultiResultsModel 

592 number_of_intervals = (self.sim_setup.stop_time - self.sim_setup.start_time) / \ 

593 self.sim_setup.output_interval 

594 if int(number_of_intervals) != number_of_intervals: 

595 raise ValueError( 

596 "Given output_interval and time interval did not yield " 

597 "an integer numberOfIntervals. To use this functions " 

598 "without savepaths, you have to provide either a " 

599 "numberOfIntervals or a value for output_interval " 

600 "which can be converted to numberOfIntervals.") 

601 

602 res = self.dymola.simulateMultiResultsModel( 

603 self.model_name, 

604 startTime=self.sim_setup.start_time, 

605 stopTime=self.sim_setup.stop_time, 

606 numberOfIntervals=int(number_of_intervals), 

607 method=self.sim_setup.solver, 

608 tolerance=self.sim_setup.tolerance, 

609 fixedstepsize=self.sim_setup.fixedstepsize, 

610 resultFile=None, 

611 initialNames=initial_names, 

612 initialValues=initial_values, 

613 resultNames=res_names) 

614 

615 if not res[0]: 

616 self.logger.error("Simulation failed!") 

617 self.logger.error("The last error log from Dymola:") 

618 log = self.dymola.getLastErrorLog() 

619 # Only print first part as output is sometimes to verbose. 

620 self.logger.error(log[:10000]) 

621 dslog_path = self.working_directory.joinpath('dslog.txt') 

622 try: 

623 with open(dslog_path, "r") as dslog_file: 

624 dslog_content = dslog_file.read() 

625 self.logger.error(dslog_content) 

626 except Exception: 

627 dslog_content = "Not retreivable. Open it yourself." 

628 msg = f"Simulation failed: Reason according " \ 

629 f"to dslog, located at '{dslog_path}': {dslog_content}" 

630 if fail_on_error: 

631 raise Exception(msg) 

632 # Don't raise and return None 

633 self.logger.error(msg) 

634 return None 

635 

636 if return_option == "savepath": 

637 _save_name_dsres = f"{result_file_name}.mat" 

638 # Get the working_directory of the current dymola instance 

639 self.dymola.cd() 

640 # Get the value and convert it to a 100 % fitting str-path 

641 dymola_working_directory = str(Path(self.dymola.getLastErrorLog().replace("\n", ""))) 

642 mat_working_directory = os.path.join(dymola_working_directory, _save_name_dsres) 

643 if savepath is None or str(savepath) == dymola_working_directory: 

644 mat_result_file = mat_working_directory 

645 else: 

646 mat_save_path = os.path.join(savepath, _save_name_dsres) 

647 os.makedirs(savepath, exist_ok=True) 

648 # Copying dslogs and dsfinals can lead to errors, 

649 # as the names are not unique 

650 # for filename in [_save_name_dsres, "dslog.txt", "dsfinal.txt"]: 

651 # Delete existing files 

652 try: 

653 os.remove(mat_save_path) 

654 except OSError: 

655 pass 

656 # Move files 

657 shutil.copy(mat_working_directory, mat_save_path) 

658 os.remove(mat_working_directory) 

659 mat_result_file = mat_save_path 

660 result_file = postprocess_mat_result(mat_result_file, **kwargs_postprocessing) 

661 return result_file 

662 

663 data = res[1] # Get data 

664 if return_option == "last_point": 

665 results = [] 

666 for ini_val_set in data: 

667 results.append({result_name: ini_val_set[idx][-1] for idx, result_name 

668 in enumerate(res_names)}) 

669 if len(results) == 1 and squeeze: 

670 return results[0] 

671 return results 

672 # Else return as dataframe. 

673 dfs = [] 

674 for ini_val_set in data: 

675 df = pd.DataFrame({result_name: ini_val_set[idx] for idx, result_name 

676 in enumerate(res_names)}) 

677 # Set time index 

678 df = df.set_index("Time") 

679 # Convert it to float 

680 df.index = df.index.astype("float64") 

681 dfs.append(df) 

682 # Most of the cases, only one set is provided. In that case, avoid 

683 if len(dfs) == 1 and squeeze: 

684 return TimeSeriesData(dfs[0], default_tag="sim") 

685 return [TimeSeriesData(df, default_tag="sim") for df in dfs] 

686 

687 def translate(self): 

688 """ 

689 Translates the current model using dymola.translateModel() 

690 and checks if erros occur. 

691 """ 

692 res = self.dymola.translateModel(self.model_name) 

693 if not res: 

694 self.logger.error("Translation failed!") 

695 self.logger.error("The last error log from Dymola:") 

696 self.logger.error(self.dymola.getLastErrorLog()) 

697 raise Exception("Translation failed - Aborting") 

698 

699 def set_compiler(self, name, path, dll=False, dde=False, opc=False): 

700 """ 

701 Set up the compiler and compiler options on Windows. 

702 Optional: Specify if you want to enable dll, dde or opc. 

703 

704 :param str name: 

705 Name of the compiler, avaiable options: 

706 - 'vs': Visual Studio 

707 - 'gcc': GCC 

708 :param str,os.path.normpath path: 

709 Path to the compiler files. 

710 Example for name='vs': path='C:/Program Files (x86)/Microsoft Visual Studio 10.0/Vc' 

711 Example for name='gcc': path='C:/MinGW/bin/gcc' 

712 :param Boolean dll: 

713 Set option for dll support. Check Dymolas Manual on what this exactly does. 

714 :param Boolean dde: 

715 Set option for dde support. Check Dymolas Manual on what this exactly does. 

716 :param Boolean opc: 

717 Set option for opc support. Check Dymolas Manual on what this exactly does. 

718 :return: True, on success. 

719 """ 

720 # Lookup dict for internal name of CCompiler-Variable 

721 _name_int = {"vs": "MSVC", 

722 "gcc": "GCC"} 

723 

724 if "win" not in sys.platform: 

725 raise OSError(f"set_compiler function only implemented " 

726 f"for windows systems, you are using {sys.platform}") 

727 # Manually check correct input as Dymola's error are not a help 

728 name = name.lower() 

729 if name not in ["vs", "gcc"]: 

730 raise ValueError(f"Given compiler name {name} not supported.") 

731 if not os.path.exists(path): 

732 raise FileNotFoundError(f"Given compiler path {path} does not exist on your machine.") 

733 # Convert path for correct input 

734 path = self._make_modelica_normpath(path) 

735 if self.use_mp: 

736 raise ValueError("Given function is not yet supported for multiprocessing") 

737 

738 res = self.dymola.SetDymolaCompiler(name.lower(), 

739 [f"CCompiler={_name_int[name]}", 

740 f"{_name_int[name]}DIR={path}", 

741 f"DLL={int(dll)}", 

742 f"DDE={int(dde)}", 

743 f"OPC={int(opc)}"]) 

744 

745 return res 

746 

747 def import_initial(self, filepath): 

748 """ 

749 Load given dsfinal.txt into dymola 

750 

751 :param str,os.path.normpath filepath: 

752 Path to the dsfinal.txt to be loaded 

753 """ 

754 if not os.path.isfile(filepath): 

755 raise FileNotFoundError(f"Given filepath {filepath} does not exist") 

756 if not os.path.splitext(filepath)[1] == ".txt": 

757 raise TypeError('File is not of type .txt') 

758 if self.use_mp: 

759 raise ValueError("Given function is not yet supported for multiprocessing") 

760 res = self.dymola.importInitial(dsName=filepath) 

761 if res: 

762 self.logger.info("Successfully loaded dsfinal.txt") 

763 else: 

764 raise Exception("Could not load dsfinal into Dymola.") 

765 

766 @SimulationAPI.working_directory.setter 

767 def working_directory(self, working_directory: Union[Path, str]): 

768 """Set the working directory to the given path""" 

769 if isinstance(working_directory, str): 

770 working_directory = Path(working_directory) 

771 self._working_directory = working_directory 

772 if self.dymola is None: # Not yet started 

773 return 

774 # Also set the working_directory in the dymola api 

775 self.set_dymola_cd(dymola=self.dymola, 

776 cd=working_directory) 

777 if self.use_mp: 

778 self.logger.warning("Won't set the working_directory for all workers, " 

779 "not yet implemented.") 

780 

781 @SimulationAPI.cd.setter 

782 def cd(self, cd): 

783 warnings.warn("cd was renamed to working_directory in all classes. Use working_directory instead.", category=DeprecationWarning) 

784 self.working_directory = cd 

785 

786 def set_dymola_cd(self, dymola, cd): 

787 """ 

788 Set the cd of the Dymola Instance. 

789 Before calling the Function, create the path and 

790 convert to a modelica-normpath. 

791 """ 

792 os.makedirs(cd, exist_ok=True) 

793 cd_modelica = self._make_modelica_normpath(path=cd) 

794 res = dymola.cd(cd_modelica) 

795 if not res: 

796 raise OSError(f"Could not change working directory to {cd}") 

797 

798 def close(self): 

799 """Closes dymola.""" 

800 # Close MP of super class 

801 super().close() 

802 # Always close main instance 

803 self._single_close(dymola=self.dymola) 

804 

805 def _close_multiprocessing(self, _): 

806 self._single_close() 

807 DymolaAPI.dymola = None 

808 

809 def _single_close(self, **kwargs): 

810 """Closes a single dymola instance""" 

811 if self.dymola is None: 

812 return # Already closed prior 

813 # Execute the mos-script if given: 

814 if self.mos_script_post is not None: 

815 self.logger.info("Executing given mos_script_post " 

816 "prior to closing.") 

817 self.dymola.RunScript(self.mos_script_post) 

818 self.logger.info("Output of mos_script_post: %s", self.dymola.getLastErrorLog()) 

819 self.logger.info('Closing Dymola') 

820 self.dymola.close() 

821 self.logger.info('Successfully closed Dymola') 

822 self.dymola = None 

823 

824 def _close_dummy(self): 

825 """ 

826 Closes dummy instance at the end of the execution 

827 """ 

828 if self._dummy_dymola_instance is not None: 

829 self.logger.info('Closing dummy Dymola instance') 

830 self._dummy_dymola_instance.close() 

831 self.logger.info('Successfully closed dummy Dymola instance') 

832 

833 def extract_model_variables(self): 

834 """ 

835 Extract all variables of the model by 

836 translating it and then processing the dsin 

837 using the manipulate_ds module. 

838 """ 

839 # Translate model 

840 self.logger.info("Translating model '%s' to extract model variables ", 

841 self.model_name) 

842 self.translate() 

843 # Get path to dsin: 

844 dsin_path = os.path.join(self.cd, "dsin.txt") 

845 df = manipulate_ds.convert_ds_file_to_dataframe(dsin_path) 

846 # Convert and return all parameters of dsin to initial values and names 

847 for idx, row in df.iterrows(): 

848 _max = float(row["4"]) 

849 _min = float(row["3"]) 

850 if _min >= _max: 

851 _var_ebcpy = Variable(value=float(row["2"])) 

852 else: 

853 _var_ebcpy = Variable( 

854 min=_min, 

855 max=_max, 

856 value=float(row["2"]) 

857 ) 

858 if row["5"] == "1": 

859 self.parameters[idx] = _var_ebcpy 

860 elif row["5"] == "5": 

861 self.inputs[idx] = _var_ebcpy 

862 elif row["5"] == "4": 

863 self.outputs[idx] = _var_ebcpy 

864 else: 

865 self.states[idx] = _var_ebcpy 

866 

867 def _setup_dymola_interface(self, kwargs: dict): 

868 """Load all packages and change the current working directory""" 

869 use_mp = kwargs["use_mp"] 

870 port = kwargs.get("port", -1) 

871 time_delay = kwargs.get("time_delay", 0) 

872 time.sleep(time_delay) 

873 dymola = self._open_dymola_interface(port=port) 

874 self._check_dymola_instances() 

875 if use_mp: 

876 cd = os.path.join(self.cd, f"worker_{self.worker_idx}") 

877 else: 

878 cd = self.cd 

879 # Execute the mos-script if given: 

880 if self.mos_script_pre is not None: 

881 self.logger.info("Executing given mos_script_pre " 

882 "prior to loading packages.") 

883 dymola.RunScript(self.mos_script_pre) 

884 self.logger.info("Output of mos_script_pre: %s", dymola.getLastErrorLog()) 

885 

886 # Set the cd in the dymola api 

887 self.set_dymola_cd(dymola=dymola, cd=cd) 

888 

889 for package in self.packages: 

890 self.logger.info("Loading Model %s", os.path.dirname(package).split("\\")[-1]) 

891 res = dymola.openModel(package, changeDirectory=False) 

892 if not res: 

893 raise ImportError(dymola.getLastErrorLog()) 

894 self.logger.info("Loaded modules") 

895 

896 dymola.experimentSetupOutput(**self.experiment_setup_output.dict()) 

897 if use_mp: 

898 DymolaAPI.dymola = dymola 

899 return None 

900 return dymola 

901 

902 def update_experiment_setup_output(self, experiment_setup_output: Union[ExperimentSetupOutput, dict]): 

903 """ 

904 Function to update the ExperimentSetupOutput in Dymola for selection 

905 of which variables are going to be saved. The options 

906 `events` and `equidistant` are overridden if equidistant output is required. 

907 

908 :param (ExperimentSetupOutput, dict) experiment_setup_output: 

909 An instance of ExperimentSetupOutput or a dict with valid keys for it. 

910 """ 

911 if isinstance(experiment_setup_output, dict): 

912 self.experiment_setup_output = ExperimentSetupOutput(**experiment_setup_output) 

913 else: 

914 self.experiment_setup_output = experiment_setup_output 

915 if self.equidistant_output: 

916 # Change the Simulation Output, to ensure all 

917 # simulation results have the same array shape. 

918 # Events can also cause errors in the shape. 

919 self.experiment_setup_output.equidistant = True 

920 self.experiment_setup_output.events = False 

921 if self.dymola is None: 

922 return 

923 self.dymola.experimentSetupOutput(**self.experiment_setup_output.model_dump()) 

924 

925 def license_is_available(self, option: str = "Standard"): 

926 """Check if license is available""" 

927 if self.dymola is None: 

928 warnings.warn("You want to check the license before starting dymola, this is not supported.") 

929 return False 

930 return self.dymola.RequestOption(option) 

931 

932 def _open_dymola_interface(self, port): 

933 """Open an instance of dymola and return the API-Object""" 

934 if self.dymola_interface_path not in sys.path: 

935 sys.path.insert(0, self.dymola_interface_path) 

936 try: 

937 from dymola.dymola_interface import DymolaInterface 

938 from dymola.dymola_exception import DymolaConnectionException 

939 return DymolaInterface(showwindow=self.show_window, 

940 dymolapath=self.dymola_exe_path, 

941 port=port) 

942 except ImportError as error: 

943 raise ImportError("Given dymola-interface could not be " 

944 "loaded:\n %s" % self.dymola_interface_path) from error 

945 except DymolaConnectionException as error: 

946 raise ConnectionError(error) from error 

947 

948 def to_dict(self): 

949 """ 

950 Store the most relevant information of this class 

951 into a dictionary. This may be used for future configuration. 

952 

953 :return: dict config: 

954 Dictionary with keys to re-init this class. 

955 """ 

956 # Convert Path to str to enable json-dumping 

957 config = {"cd": str(self.cd), 

958 "packages": [str(pack) for pack in self.packages], 

959 "model_name": self.model_name, 

960 "type": "DymolaAPI", 

961 } 

962 # Update kwargs 

963 config.update({kwarg: self.__dict__.get(kwarg, None) 

964 for kwarg in self._supported_kwargs}) 

965 

966 return config 

967 

968 def get_packages(self): 

969 """ 

970 Get the currently loaded packages of Dymola 

971 """ 

972 packages = self.dymola.ExecuteCommand( 

973 'ModelManagement.Structure.AST.Misc.ClassesInPackage("")' 

974 ) 

975 if packages is None: 

976 self.logger.error("Could not load packages from Dymola, using self.packages") 

977 packages = [] 

978 for pack in self.packages: 

979 pack = Path(pack) 

980 if pack.name == "package.mo": 

981 packages.append(pack.parent.name) 

982 valid_packages = [] 

983 for pack in packages: 

984 current_package = f"modelica://{pack}/package.order" 

985 pack_path = self.dymola.ExecuteCommand( 

986 f'Modelica.Utilities.Files.loadResource("{current_package}")' 

987 ) 

988 if not isinstance(pack_path, str): 

989 self.logger.error("Could not load model resource for package %s", pack) 

990 if os.path.isfile(pack_path): 

991 valid_packages.append(Path(pack_path).parent) 

992 return valid_packages 

993 

994 def save_for_reproduction( 

995 self, 

996 title: str, 

997 path: Path = None, 

998 files: list = None, 

999 save_total_model: bool = True, 

1000 export_fmu: bool = True, 

1001 **kwargs 

1002 ): 

1003 """ 

1004 Additionally to the basic reproduction, add info 

1005 for Dymola packages. 

1006 

1007 Content which is saved: 

1008 - DymolaAPI configuration 

1009 - Information on Dymola: Version, flags 

1010 - All loaded packages 

1011 - Total model, if save_total_model = True 

1012 - FMU, if export_fmu = True 

1013 

1014 :param bool save_total_model: 

1015 True to save the total model 

1016 :param bool export_fmu: 

1017 True to export the FMU of the current model. 

1018 """ 

1019 # Local import to require git-package only when called 

1020 from ebcpy.utils.reproduction import ReproductionFile, CopyFile, get_git_information 

1021 

1022 if files is None: 

1023 files = [] 

1024 # DymolaAPI Info: 

1025 files.append(ReproductionFile( 

1026 filename="Dymola/DymolaAPI_config.json", 

1027 content=json.dumps(self.to_dict(), indent=2) 

1028 )) 

1029 # Dymola info: 

1030 self.dymola.ExecuteCommand("list();") 

1031 _flags = self.dymola.getLastErrorLog() 

1032 dymola_info = [ 

1033 self.dymola.ExecuteCommand("DymolaVersion()"), 

1034 str(self.dymola.ExecuteCommand("DymolaVersionNumber()")), 

1035 "\n\n" 

1036 ] 

1037 files.append(ReproductionFile( 

1038 filename="Dymola/DymolaInfo.txt", 

1039 content="\n".join(dymola_info) + _flags 

1040 )) 

1041 

1042 # Packages 

1043 packages = self.get_packages() 

1044 package_infos = [] 

1045 for pack_path in packages: 

1046 

1047 for pack_dir_parent in [pack_path] + list(pack_path.parents): 

1048 repo_info = get_git_information( 

1049 path=pack_dir_parent, 

1050 zip_folder_path="Dymola" 

1051 ) 

1052 if not repo_info: 

1053 continue 

1054 

1055 files.extend(repo_info.pop("difference_files")) 

1056 pack_path = str(pack_path) + "; " + "; ".join([f"{key}: {value}" for key, value in repo_info.items()]) 

1057 break 

1058 package_infos.append(str(pack_path)) 

1059 files.append(ReproductionFile( 

1060 filename="Dymola/Modelica_packages.txt", 

1061 content="\n".join(package_infos) 

1062 )) 

1063 # Total model 

1064 if save_total_model: 

1065 _total_model_name = f"Dymola/{self.model_name.replace('.', '_')}_total.mo" 

1066 _total_model = Path(self.cd).joinpath(_total_model_name) 

1067 os.makedirs(_total_model.parent, exist_ok=True) # Create to ensure model can be saved. 

1068 if "(" in self.model_name: 

1069 # Create temporary model: 

1070 temp_model_file = Path(self.cd).joinpath(f"temp_total_model_{uuid.uuid4()}.mo") 

1071 temp_mode_name = f"{self.model_name.split('(')[0].split('.')[-1]}WithModifier" 

1072 with open(temp_model_file, "w") as file: 

1073 file.write(f"model {temp_mode_name}\n extends {self.model_name};\nend {temp_mode_name};") 

1074 res = self.dymola.openModel(str(temp_model_file), changeDirectory=False) 

1075 if not res: 

1076 self.logger.error( 

1077 "Could not create separate model for model with modifiers: %s", 

1078 self.model_name 

1079 ) 

1080 model_name_to_save = self.model_name 

1081 else: 

1082 model_name_to_save = temp_mode_name 

1083 os.remove(temp_model_file) 

1084 else: 

1085 model_name_to_save = self.model_name 

1086 res = self.dymola.saveTotalModel( 

1087 fileName=str(_total_model), 

1088 modelName=model_name_to_save 

1089 ) 

1090 if res: 

1091 files.append(ReproductionFile( 

1092 filename=_total_model_name, 

1093 content=_total_model.read_text() 

1094 )) 

1095 os.remove(_total_model) 

1096 else: 

1097 self.logger.error("Could not save total model: %s", 

1098 self.dymola.getLastErrorLog()) 

1099 # FMU 

1100 if export_fmu: 

1101 _fmu_path = self._save_to_fmu(fail_on_error=False) 

1102 if _fmu_path is not None: 

1103 files.append(CopyFile( 

1104 sourcepath=_fmu_path, 

1105 filename="Dymola/" + _fmu_path.name, 

1106 remove=True 

1107 )) 

1108 

1109 return super().save_for_reproduction( 

1110 title=title, 

1111 path=path, 

1112 files=files, 

1113 **kwargs 

1114 ) 

1115 

1116 def _save_to_fmu(self, fail_on_error): 

1117 """Save model as an FMU""" 

1118 res = self.dymola.translateModelFMU( 

1119 modelToOpen=self.model_name, 

1120 storeResult=False, 

1121 modelName='', 

1122 fmiVersion='2', 

1123 fmiType='all', 

1124 includeSource=False, 

1125 includeImage=0 

1126 ) 

1127 if not res: 

1128 msg = "Could not export fmu: %s" % self.dymola.getLastErrorLog() 

1129 self.logger.error(msg) 

1130 if fail_on_error: 

1131 raise Exception(msg) 

1132 else: 

1133 path = Path(self.cd).joinpath(res + ".fmu") 

1134 return path 

1135 

1136 @staticmethod 

1137 def _make_modelica_normpath(path): 

1138 """ 

1139 Convert given path to a path readable in dymola. 

1140 If the base path does not exist, create it. 

1141 

1142 :param str,os.path.normpath path: 

1143 Either a file or a folder path. The base to this 

1144 path is created in non existent. 

1145 :return: str 

1146 Path readable in dymola 

1147 """ 

1148 if isinstance(path, Path): 

1149 path = str(path) 

1150 

1151 path = path.replace("\\", "/") 

1152 # Search for e.g. "D:testzone" and replace it with D:/testzone 

1153 loc = path.find(":") 

1154 if path[loc + 1] != "/" and loc != -1: 

1155 path = path.replace(":", ":/") 

1156 return path 

1157 

1158 @staticmethod 

1159 def get_dymola_interface_path(dymola_install_dir): 

1160 """ 

1161 Function to get the path of the newest dymola interface 

1162 installment on the used machine 

1163 

1164 :param str dymola_install_dir: 

1165 The dymola installation folder. Example: 

1166 "C://Program Files//Dymola 2020" 

1167 :return: str 

1168 Path to the dymola.egg-file or .whl file (for 2024 refresh 1 or newer versions) 

1169 """ 

1170 path_to_interface = os.path.join(dymola_install_dir, "Modelica", "Library", "python_interface") 

1171 path_to_egg_file = os.path.join(path_to_interface, "dymola.egg") 

1172 if os.path.isfile(path_to_egg_file): 

1173 return path_to_egg_file 

1174 # Try to find .whl file: 

1175 for file in os.listdir(path_to_interface): 

1176 if file.endswith(".whl"): 

1177 return os.path.join(path_to_interface, file) 

1178 # If still here, no .egg or .whl was found 

1179 raise FileNotFoundError(f"The given dymola installation directory " 

1180 f"'{dymola_install_dir}' has no " 

1181 f"dymola-interface .egg or .whl-file.") 

1182 

1183 @staticmethod 

1184 def get_dymola_exe_path(dymola_install_dir, dymola_name=None): 

1185 """ 

1186 Function to get the path of the dymola exe-file 

1187 on the current used machine. 

1188 

1189 :param str dymola_install_dir: 

1190 The dymola installation folder. Example: 

1191 "C://Program Files//Dymola 2020" 

1192 :param str dymola_name: 

1193 Name of the executable. On Windows it is always Dymola.exe, on 

1194 linux just dymola. 

1195 :return: str 

1196 Path to the dymola-exe-file. 

1197 """ 

1198 if dymola_name is None: 

1199 if "linux" in sys.platform: 

1200 dymola_name = "dymola" 

1201 elif "win" in sys.platform: 

1202 dymola_name = "Dymola.exe" 

1203 else: 

1204 raise OSError(f"Your operating system {sys.platform} has no default dymola-name." 

1205 f"Please provide one.") 

1206 

1207 bin_64 = os.path.join(dymola_install_dir, "bin64", dymola_name) 

1208 bin_32 = os.path.join(dymola_install_dir, "bin", dymola_name) 

1209 if os.path.isfile(bin_64): # First check for 64bit installation 

1210 dym_file = bin_64 

1211 elif os.path.isfile(bin_32): # Else use the 32bit version 

1212 dym_file = bin_32 

1213 else: 

1214 raise FileNotFoundError( 

1215 f"The given dymola installation has not executable at '{bin_32}'. " 

1216 f"If your dymola_path exists, please raise an issue." 

1217 ) 

1218 

1219 return dym_file 

1220 

1221 @staticmethod 

1222 def get_dymola_install_paths(basedir=None): 

1223 """ 

1224 Function to get all paths of dymola installations 

1225 on the used machine. Supported platforms are: 

1226 * Windows 

1227 * Linux 

1228 * Mac OS X 

1229 If multiple installation of Dymola are found, the newest version will be returned. 

1230 This assumes the names are sortable, e.g. Dymola 2020, Dymola 2019 etc. 

1231 

1232 :param str basedir: 

1233 The base-directory to search for the dymola-installation. 

1234 The default value depends on the platform one is using. 

1235 On Windows it is "C://Program Files" or "C://Program Files (x86)" (for 64 bit) 

1236 On Linux it is "/opt" (based on our ci-Docker configuration 

1237 On Mac OS X "/Application" (based on the default) 

1238 :return: str 

1239 Path to the dymola-installation 

1240 """ 

1241 

1242 if basedir is None: 

1243 if "linux" in sys.platform: 

1244 basedir = os.path.normpath("/opt") 

1245 elif "win" in sys.platform: 

1246 basedir = os.path.normpath("C:/Program Files") 

1247 elif "darwin" in sys.platform: 

1248 basedir = os.path.normpath("/Applications") 

1249 else: 

1250 raise OSError(f"Your operating system ({sys.platform})does not support " 

1251 f"a default basedir. Please provide one.") 

1252 

1253 syspaths = [basedir] 

1254 # Check if 64bit is installed (Windows only) 

1255 systempath_64 = os.path.normpath("C://Program Files (x86)") 

1256 if os.path.exists(systempath_64): 

1257 syspaths.append(systempath_64) 

1258 # Get all folders in both path's 

1259 temp_list = [] 

1260 for systempath in syspaths: 

1261 temp_list += os.listdir(systempath) 

1262 # Filter programs that are not Dymola 

1263 dym_versions = [] 

1264 for folder_name in temp_list: 

1265 # Catch both Dymola and dymola folder-names 

1266 if "dymola" in folder_name.lower(): 

1267 dym_versions.append(folder_name) 

1268 del temp_list 

1269 # Find the newest version and return the egg-file 

1270 # This sorting only works with a good Folder structure, eg. Dymola 2020, Dymola 2019 etc. 

1271 dym_versions.sort() 

1272 valid_paths = [] 

1273 for dym_version in reversed(dym_versions): 

1274 for system_path in syspaths: 

1275 full_path = os.path.join(system_path, dym_version) 

1276 if os.path.isdir(full_path): 

1277 valid_paths.append(full_path) 

1278 return valid_paths 

1279 

1280 def _check_dymola_instances(self): 

1281 """ 

1282 Check how many dymola instances are running on the machine. 

1283 Raise a warning if the number exceeds a certain amount. 

1284 """ 

1285 # The option may be useful. However the explicit requirement leads to 

1286 # Problems on linux, therefore the feature is not worth the trouble. 

1287 # pylint: disable=import-outside-toplevel 

1288 try: 

1289 import psutil 

1290 except ImportError: 

1291 return 

1292 counter = 0 

1293 for proc in psutil.process_iter(): 

1294 try: 

1295 if "Dymola" in proc.name(): 

1296 counter += 1 

1297 except (psutil.AccessDenied, psutil.NoSuchProcess): 

1298 continue 

1299 if counter >= self._critical_number_instances: 

1300 warnings.warn("There are currently %s Dymola-Instances " 

1301 "running on your machine!" % counter) 

1302 

1303 @staticmethod 

1304 def _alter_model_name(parameters, model_name, structural_params): 

1305 """ 

1306 Creates a modifier for all structural parameters, 

1307 based on the modelname and the initalNames and values. 

1308 

1309 :param dict parameters: 

1310 Parameters of the simulation 

1311 :param str model_name: 

1312 Name of the model to be modified 

1313 :param list structural_params: 

1314 List of strings with structural parameters 

1315 :return: str altered_modelName: 

1316 modified model name 

1317 """ 

1318 # the structural parameter needs to be removed from paramters dict 

1319 new_parameters = parameters.copy() 

1320 model_name = model_name.split("(")[0] # Trim old modifier 

1321 if parameters == {}: 

1322 return model_name 

1323 all_modifiers = [] 

1324 for var_name, value in parameters.items(): 

1325 # Check if the variable is in the 

1326 # given list of structural parameters 

1327 if var_name in structural_params: 

1328 all_modifiers.append(f"{var_name}={value}") 

1329 # removal of the structural parameter 

1330 new_parameters.pop(var_name) 

1331 altered_model_name = f"{model_name}({','.join(all_modifiers)})" 

1332 return altered_model_name, new_parameters 

1333 

1334 def _check_restart(self): 

1335 """Restart Dymola every n_restart iterations in order to free memory""" 

1336 

1337 if self.sim_counter == self.n_restart: 

1338 self.logger.info("Closing and restarting Dymola to free memory") 

1339 self.close() 

1340 self._dummy_dymola_instance = self._setup_dymola_interface(dict(use_mp=False)) 

1341 self.sim_counter = 1 

1342 else: 

1343 self.sim_counter += 1 

1344 

1345 

1346def _get_dymola_path_of_version(dymola_installations: list, dymola_version: str): 

1347 """ 

1348 Helper function to get the path associated to the dymola_version 

1349 from the list of all installations 

1350 """ 

1351 for dymola_path in dymola_installations: 

1352 if dymola_path.endswith(dymola_version): 

1353 return dymola_path 

1354 # If still here, version was not found 

1355 raise ValueError( 

1356 f"Given dymola_version '{dymola_version}' not found in " 

1357 f"the list of dymola installations {dymola_installations}" 

1358 ) 

1359 

1360 

1361def _get_n_available_ports(n_ports: int, start_range: int = 44000, end_range: int = 44400): 

1362 """ 

1363 Get a specified number of available network ports within a given range. 

1364 

1365 This function uses socket connections to check the availability of ports within the specified range. 

1366 If the required number of open ports is found, it returns a list of those ports. If not, it raises 

1367 a ConnectionError with a descriptive message indicating the failure to find the necessary ports. 

1368 

1369 Parameters: 

1370 - n_ports (int): The number of open ports to find. 

1371 - start_range (int, optional): 

1372 The starting port of the range to check (inclusive). 

1373 Default is 44000. 

1374 - end_range (int, optional): 

1375 The ending port of the range to check (exclusive). 

1376 Default is 44400. 

1377 

1378 Returns: 

1379 - list of int: 

1380 A list containing the available ports. 

1381 The length of the list is equal to 'n_ports'. 

1382 

1383 Raises: 

1384 - ConnectionError: 

1385 If the required number of open ports cannot 

1386 be found within the specified range. 

1387 

1388 Example: 

1389 

1390 ``` 

1391 try: 

1392 open_ports = _get_n_available_ports(3, start_range=50000, end_range=50500) 

1393 print(f"Found open ports: {open_ports}") 

1394 except ConnectionError as e: 

1395 print(f"Error: {e}") 

1396 ``` 

1397 """ 

1398 ports = [] 

1399 for port in range(start_range, end_range): 

1400 try: 

1401 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: 

1402 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 

1403 sock.bind(("127.0.0.1", port)) 

1404 ports.append(port) 

1405 except OSError: 

1406 pass 

1407 if len(ports) == n_ports: 

1408 return ports 

1409 raise ConnectionError( 

1410 f"Could not find {n_ports} open ports in range {start_range}-{end_range}." 

1411 f"Can't open {n_ports} Dymola instances" 

1412 )