1414from contextlib import closing
1515from typing import Union , List
1616
17- from pydantic import Field
17+ from pydantic import Field , BaseModel
1818import pandas as pd
1919
2020from ebcpy import TimeSeriesData
@@ -42,6 +42,27 @@ class DymolaSimulationSetup(SimulationSetup):
4242 "Radau" , "Dopri45" , "Dopri853" , "Sdirk34hw" ]
4343
4444
45+ class 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+
4566class DymolaAPI (SimulationAPI ):
4667 """
4768 API to a Dymola instance.
@@ -63,6 +84,15 @@ class DymolaAPI(SimulationAPI):
6384 :keyword Boolean equidistant_output:
6485 If True (Default), Dymola stores variables in an
6586 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
6696 :keyword int n_restart:
6797 Number of iterations after which Dymola should restart.
6898 This is done to free memory. Default value -1. For values
@@ -144,6 +174,7 @@ class DymolaAPI(SimulationAPI):
144174 "modify_structural_parameters" ,
145175 "dymola_path" ,
146176 "equidistant_output" ,
177+ "variables_to_save" ,
147178 "n_restart" ,
148179 "debug" ,
149180 "mos_script_pre" ,
@@ -170,6 +201,9 @@ def __init__(
170201 self .show_window = kwargs .pop ("show_window" , False )
171202 self .modify_structural_parameters = kwargs .pop ("modify_structural_parameters" , True )
172203 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+
173207 self .mos_script_pre = kwargs .pop ("mos_script_pre" , None )
174208 self .mos_script_post = kwargs .pop ("mos_script_post" , None )
175209 self .dymola_version = kwargs .pop ("dymola_version" , None )
@@ -277,7 +311,8 @@ def __init__(
277311 if not self .license_is_available ():
278312 warnings .warn ("You have no licence to use Dymola. "
279313 "Hence you can only simulate models with 8 or less equations." )
280-
314+ # Update experiment setup output
315+ self .update_experiment_setup_output (self .experiment_setup_output )
281316 self .fully_initialized = True
282317 # Trigger on init.
283318 if model_name is not None :
@@ -325,6 +360,15 @@ def simulate(self,
325360 If inputs are given, you have to specify the file_name of the table
326361 in the instance of CombiTimeTable. In order for the inputs to
327362 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`.
328372 :keyword List[str] structural_parameters:
329373 A list containing all parameter names which are structural in Modelica.
330374 This means a modifier has to be created in order to change
@@ -374,6 +418,12 @@ def _single_simulation(self, kwargs):
374418 table_name = kwargs .pop ("table_name" , None )
375419 file_name = kwargs .pop ("file_name" , None )
376420 savepath = kwargs .pop ("savepath" , None )
421+
422+ def empty_postprocessing (mat_result , ** _kwargs ):
423+ return mat_result
424+
425+ postprocess_mat_result = kwargs .pop ("postprocess_mat_result" , empty_postprocessing )
426+ kwargs_postprocessing = kwargs .pop ("kwargs_postprocessing" , {})
377427 if kwargs :
378428 self .logger .error (
379429 "You passed the following kwargs which "
@@ -388,9 +438,14 @@ def _single_simulation(self, kwargs):
388438 # method used in the DymolaInterface should work.
389439 self ._setup_dymola_interface (dict (use_mp = True ))
390440
441+ # Re-set the dymola experiment output if API is newly started
442+ self .dymola .experimentSetupOutput (** self .experiment_setup_output .model_dump ())
443+
391444 # Handle eventlog
392445 if show_eventlog :
393- self .dymola .experimentSetupOutput (events = True )
446+ if not self .experiment_setup_output .events :
447+ raise ValueError ("You can't log events and have an "
448+ "equidistant output, set equidistant output=False" )
394449 self .dymola .ExecuteCommand ("Advanced.Debug.LogEvents = true" )
395450 self .dymola .ExecuteCommand ("Advanced.Debug.LogEventsInitialization = true" )
396451
@@ -582,23 +637,26 @@ def _single_simulation(self, kwargs):
582637 self .dymola .cd ()
583638 # Get the value and convert it to a 100 % fitting str-path
584639 dymola_working_directory = str (Path (self .dymola .getLastErrorLog ().replace ("\n " , "" )))
640+ mat_working_directory = os .path .join (dymola_working_directory , _save_name_dsres )
585641 if savepath is None or str (savepath ) == dymola_working_directory :
586- return os .path .join (dymola_working_directory , _save_name_dsres )
587- os .makedirs (savepath , exist_ok = True )
588- for filename in [_save_name_dsres ]:
642+ mat_result_file = mat_working_directory
643+ else :
644+ mat_save_path = os .path .join (savepath , _save_name_dsres )
645+ os .makedirs (savepath , exist_ok = True )
589646 # Copying dslogs and dsfinals can lead to errors,
590647 # as the names are not unique
591648 # for filename in [_save_name_dsres, "dslog.txt", "dsfinal.txt"]:
592649 # Delete existing files
593650 try :
594- os .remove (os . path . join ( savepath , filename ) )
651+ os .remove (mat_save_path )
595652 except OSError :
596653 pass
597654 # Move files
598- shutil .copy (os .path .join (dymola_working_directory , filename ),
599- os .path .join (savepath , filename ))
600- os .remove (os .path .join (dymola_working_directory , filename ))
601- return os .path .join (savepath , _save_name_dsres )
655+ shutil .copy (mat_working_directory , mat_save_path )
656+ os .remove (mat_working_directory )
657+ mat_result_file = mat_save_path
658+ result_file = postprocess_mat_result (mat_result_file , ** kwargs_postprocessing )
659+ return result_file
602660
603661 data = res [1 ] # Get data
604662 if return_option == "last_point" :
@@ -832,17 +890,36 @@ def _setup_dymola_interface(self, kwargs: dict):
832890 if not res :
833891 raise ImportError (dymola .getLastErrorLog ())
834892 self .logger .info ("Loaded modules" )
835- if self .equidistant_output :
836- # Change the Simulation Output, to ensure all
837- # simulation results have the same array shape.
838- # Events can also cause errors in the shape.
839- dymola .experimentSetupOutput (equidistant = True ,
840- events = False )
893+
894+ dymola .experimentSetupOutput (** self .experiment_setup_output .dict ())
841895 if use_mp :
842896 DymolaAPI .dymola = dymola
843897 return None
844898 return dymola
845899
900+ def update_experiment_setup_output (self , experiment_setup_output : Union [ExperimentSetupOutput , dict ]):
901+ """
902+ Function to update the ExperimentSetupOutput in Dymola for selection
903+ of which variables are going to be saved. The options
904+ `events` and `equidistant` are overridden if equidistant output is required.
905+
906+ :param (ExperimentSetupOutput, dict) experiment_setup_output:
907+ An instance of ExperimentSetupOutput or a dict with valid keys for it.
908+ """
909+ if isinstance (experiment_setup_output , dict ):
910+ self .experiment_setup_output = ExperimentSetupOutput (** experiment_setup_output )
911+ else :
912+ self .experiment_setup_output = experiment_setup_output
913+ if self .equidistant_output :
914+ # Change the Simulation Output, to ensure all
915+ # simulation results have the same array shape.
916+ # Events can also cause errors in the shape.
917+ self .experiment_setup_output .equidistant = True
918+ self .experiment_setup_output .events = False
919+ if self .dymola is None :
920+ return
921+ self .dymola .experimentSetupOutput (** self .experiment_setup_output .model_dump ())
922+
846923 def license_is_available (self , option : str = "Standard" ):
847924 """Check if license is available"""
848925 if self .dymola is None :
@@ -1215,7 +1292,7 @@ def _check_dymola_instances(self):
12151292 try :
12161293 if "Dymola" in proc .name ():
12171294 counter += 1
1218- except psutil .AccessDenied :
1295+ except ( psutil .AccessDenied , psutil . NoSuchProcess ) :
12191296 continue
12201297 if counter >= self ._critical_number_instances :
12211298 warnings .warn ("There are currently %s Dymola-Instances "
0 commit comments