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
« 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
4from .dymola_api import DymolaAPI
5from ..data_types import load_time_series_data
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
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.
39 This function supports two simulation modes:
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.
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.
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))')
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 = []
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 )
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
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}
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)
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)
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()
179 return all_result_paths
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)
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()
207 return result_paths