Coverage for ebcpy/simulationapi/dymola_utils.py: 92%

49 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2026-05-27 10:55 +0000

1from pathlib import Path 

2from typing import Union, List 

3 

4from .dymola_api import DymolaAPI 

5from ..data_types import load_time_series_data 

6 

7 

8def _default_result_file_names(result_file_name, parameters): 

9 """Generate unique result file names from parameter values.""" 

10 result_file_names = [] 

11 for idx, param_dict in enumerate(parameters): 

12 name = result_file_name 

13 for key, val in param_dict.items(): 

14 short_key = key.split(".")[-1] 

15 name += f"_{short_key}{str(val).replace('.', '_')}" 

16 result_file_names.append(name) 

17 return result_file_names 

18 

19 

20def simple_dymola_sim_study( 

21 model_names: List[str], 

22 simulation_setup: dict, 

23 working_directory: Union[str, Path], 

24 save_path: Union[str, Path], 

25 model_result_file_names: List[str], 

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

27 n_cpu: int = 4, 

28 use_parameter_study: bool = False, 

29 result_file_name_func=None, 

30 kwargs_postprocessing: dict = None, 

31 postprocess_mat_result=None, 

32 mos_script_pre: Union[str, Path] = None, 

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

34 **kwargs 

35): 

36 """ 

37 Run a Dymola simulation study with multiple models and/or parameter variations. 

38 

39 This function supports two simulation modes: 

40 

41 **Parameter study** (``use_parameter_study=True``): 

42 Each model is simulated separately with all parameter sets. 

43 Useful when you want the full cross-product of models × parameters. 

44 Each model gets its own DymolaAPI instance, which translates the model 

45 once and then runs all parameter variations. 

46 

47 **Model comparison** (``use_parameter_study=False``): 

48 All models are simulated in a single ``simulate()`` call using the 

49 ``model_names`` keyword. Each model can receive the same parameters 

50 (pass a single dict) or individual parameters (pass a list of dicts 

51 matching the length of ``model_names``). Each model is translated 

52 individually. 

53 

54 Both modes use model name modifiers to change structural parameters. 

55 This is the recommended approach — write the modifier directly in the 

56 model name string. 

57 (e.g. 'BESMod.Examples.GasBoilerBuildingOnly( 

58 redeclare BESMod.Systems.Control.DHWSuperheating control(dTDHW=10))') 

59 

60 :param list[str] model_names: 

61 List of Dymola model names, optionally with modifiers. 

62 E.g. ``["MyModel(nLayer=1)", "MyModel(nLayer=2)"]`` 

63 :param dict simulation_setup: 

64 Simulation settings with keys ``start_time``, ``stop_time``, 

65 and ``output_interval``. 

66 :param str,Path working_directory: 

67 Dymola working directory. 

68 :param str,Path save_path: 

69 Directory for saving simulation results. 

70 :param list[str] model_result_file_names: 

71 Base names for the result files, one per model. 

72 :param dict,list[dict] parameters: 

73 Parameter values for the simulation. For parameter studies, pass a list 

74 of dicts. For model comparison, pass a single dict (applied to all models) 

75 or a list of dicts (one per model). 

76 :param int n_cpu: 

77 Number of parallel Dymola processes. Default is 4. 

78 :param bool use_parameter_study: 

79 If True, runs each model with all parameter sets (cross-product). 

80 If False, runs all models in a single call. 

81 :param callable result_file_name_func: 

82 Function to generate unique result file names for parameter studies. 

83 Signature: ``func(result_file_name, parameters) -> list[str]`` 

84 Default generates names by appending parameter key-value pairs. 

85 :param dict kwargs_postprocessing: 

86 Keyword arguments passed to the post-processing function. 

87 Required if ``postprocess_mat_result`` is provided. 

88 :param callable postprocess_mat_result: 

89 Custom post-processing function. If None (default), .mat files 

90 are kept unchanged. Signature: ``func(mat_result_file, **kwargs_postprocessing)`` 

91 :param str,Path mos_script_pre: 

92 Path to a .mos script executed before loading packages. 

93 Typically, the startup script of your Modelica library. 

94 :param list packages: 

95 Additional Modelica packages not loaded by ``mos_script_pre``. 

96 :param kwargs: 

97 Additional keyword arguments forwarded to ``DymolaAPI`` constructor 

98 (e.g. ``show_window``, ``debug``, ``n_restart``, ``dymola_version``) 

99 and to ``DymolaAPI.simulate()`` (e.g. ``fail_on_error``). 

100 :return: Result file paths. For parameter studies, a dict mapping model names 

101 to lists of paths. For model comparison, a list of paths. 

102 :rtype: dict or list 

103 """ 

104 # ## Default paths 

105 if working_directory is None: 

106 working_directory = Path(__file__).parent.joinpath("results", "working_directory") 

107 if save_path is None: 

108 save_path = Path(__file__).parent.joinpath("results", "SimResults") 

109 if packages is None: 

110 packages = [] 

111 

112 if len(model_result_file_names) != len(model_names): 

113 raise ValueError( 

114 f"model_result_file_names has length {len(model_result_file_names)} " 

115 f"but model_names has length {len(model_names)}. They must match." 

116 ) 

117 if use_parameter_study and not isinstance(parameters, list): 

118 raise TypeError( 

119 "For parameter studies, parameters must be a list of dicts." 

120 ) 

121 

122 # ## Post-processing setup 

123 # Dymola produces .mat files by default. These are large and use a float 

124 # index (seconds). The post-processing function converts them to a more 

125 # usable format (e.g. datetime-indexed parquet) containing only the 

126 # variables you need. 

127 if postprocess_mat_result is not None and kwargs_postprocessing is None: 

128 raise ValueError( 

129 "kwargs_postprocessing is required when postprocess_mat_result is provided. " 

130 "Pass a dict with the keyword arguments for your post-processing function." 

131 ) 

132 # Build the simulate kwargs for postprocessing 

133 postprocessing_kwargs = {} 

134 if postprocess_mat_result is not None: 

135 postprocessing_kwargs["postprocess_mat_result"] = postprocess_mat_result 

136 postprocessing_kwargs["kwargs_postprocessing"] = kwargs_postprocessing 

137 

138 # ## Separate kwargs for DymolaAPI constructor and simulate() 

139 # Known simulate() kwargs are forwarded there, everything else goes to DymolaAPI. 

140 simulate_kwarg_keys = {"inputs", "table_name", "file_name", "fail_on_error", "show_eventlog", "squeeze"} 

141 simulate_kwargs = {k: kwargs.pop(k) for k in simulate_kwarg_keys if k in kwargs} 

142 

143 # ## Run simulations 

144 if use_parameter_study: 

145 # ### Parameter study mode 

146 # Iterate over each model variant. For each model, a separate DymolaAPI 

147 # instance is created, the model is translated once, and all parameter 

148 # sets are simulated. This is efficient because translation (the slow part) 

149 # happens only once per model. 

150 if result_file_name_func is None: 

151 result_file_name_func = _default_result_file_names 

152 all_result_paths = {} 

153 for model_name, result_file_name in zip(model_names, model_result_file_names): 

154 # Create unique result file names by encoding the varied parameter values. 

155 # Adapt this naming scheme to your parameter study. 

156 result_file_names = result_file_name_func(result_file_name, parameters) 

157 

158 dym_api = DymolaAPI( 

159 mos_script_pre=mos_script_pre, 

160 model_name=model_name, 

161 working_directory=working_directory, 

162 n_cpu=n_cpu, 

163 packages=packages, 

164 **kwargs 

165 ) 

166 dym_api.set_sim_setup(sim_setup=simulation_setup) 

167 

168 result_paths = dym_api.simulate( 

169 parameters=parameters, 

170 return_option="savepath", 

171 savepath=save_path, 

172 result_file_name=result_file_names, 

173 **postprocessing_kwargs, 

174 **simulate_kwargs 

175 ) 

176 all_result_paths[model_name] = result_paths 

177 dym_api.close() 

178 

179 return all_result_paths 

180 

181 else: 

182 # ### Model comparison mode 

183 # All models are simulated in a single DymolaAPI call using the 

184 # model_names keyword. Dymola translates each model on the fly. 

185 # This is convenient for comparing different model configurations 

186 # with the same or individual parameter sets. 

187 dym_api = DymolaAPI( 

188 mos_script_pre=mos_script_pre, 

189 working_directory=working_directory, 

190 n_cpu=n_cpu, 

191 packages=packages, 

192 **kwargs 

193 ) 

194 dym_api.set_sim_setup(sim_setup=simulation_setup) 

195 

196 result_paths = dym_api.simulate( 

197 model_names=model_names, 

198 parameters=parameters, 

199 return_option="savepath", 

200 savepath=save_path, 

201 result_file_name=model_result_file_names, 

202 **postprocessing_kwargs, 

203 **simulate_kwargs 

204 ) 

205 dym_api.close() 

206 

207 return result_paths