Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2dc57cf
update save parquet for compressions
HvanderStok Apr 27, 2026
c944153
Add deprecation warnings for the structual parameters keywords
HvanderStok Apr 27, 2026
f9be639
add first raw workflow for new dymola example
HvanderStok Apr 27, 2026
4804327
update documentation for example e3_0
HvanderStok Apr 27, 2026
ec4a98f
make example more reusable
HvanderStok Apr 28, 2026
9d12afa
clean up new example
HvanderStok Apr 28, 2026
2d11eff
fix typo
HvanderStok Apr 28, 2026
e174ef7
chore: increase version and update changelog
HvanderStok Apr 28, 2026
6d52caa
fix typo
HvanderStok Apr 28, 2026
71cf27c
fix typo
HvanderStok Apr 28, 2026
1e12a7c
update documentation
HvanderStok Apr 28, 2026
13cbe2b
update utils.get_names
HvanderStok Apr 28, 2026
3271581
add simple_dymola_sim_study as a dymola_utils function
HvanderStok Apr 28, 2026
405c4cf
update example
HvanderStok Apr 28, 2026
85fb460
add tests for new function
HvanderStok Apr 28, 2026
8c86a5b
chore: increase version and update changelog
HvanderStok Apr 28, 2026
9cbb2de
update for CI
HvanderStok Apr 28, 2026
169f64c
add comment to save_path and working_directory
HvanderStok Apr 30, 2026
74c35fc
Update ebcpy/simulationapi/dymola_utils.py
HvanderStok Apr 30, 2026
696b541
Update ebcpy/simulationapi/dymola_api.py
HvanderStok Apr 30, 2026
ae3e02c
Make model_result_file_names always required in simple_dymola_sim_study
Copilot Apr 30, 2026
8326496
Revert "Make model_result_file_names always required in simple_dymola…
HvanderStok Apr 30, 2026
6e1b787
Make model_reult_file_names required in simple_dymola_sim_study
HvanderStok Apr 30, 2026
649decd
use copy for parquet save
HvanderStok Apr 30, 2026
5d475e1
fix test of mp
HvanderStok Apr 30, 2026
a1bfd86
fix example readme
HvanderStok Apr 30, 2026
6580a2d
Update ebcpy/simulationapi/dymola_utils.py
HvanderStok Apr 30, 2026
93a2c2e
fix docstring
HvanderStok Apr 30, 2026
f4860b1
fix tests
HvanderStok Apr 30, 2026
0fb0845
move example code in a main function
HvanderStok May 26, 2026
6aa0ccd
add example in docstring
HvanderStok May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,10 @@
- cap pandas version to <3
- v0.7.1
- fix bug in clean_and_space_equally #171
- v0.8.0
- Add `simple_dymola_sim_study` utility for common Dymola simulation workflows
- Add `exclude` parameter to `get_names` for filtering out unwanted variable matches
- Handle SparseDtype columns automatically in `tsd.save()` for parquet format
- Deprecate `structural_parameters` keyword in `DymolaAPI.simulate()` #176
- Deprecate `modify_structural_parameters` keyword in `DymolaAPI.__init__()`
- Add basic Dymola simulation workflow example #175
3 changes: 2 additions & 1 deletion ebcpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from .data_types import TimeSeriesData, TimeSeries, load_time_series_data
from .simulationapi.dymola_api import DymolaAPI
from .simulationapi.fmu import FMU_API
from .simulationapi.dymola_utils import simple_dymola_sim_study
from .optimization import Optimizer


__version__ = '0.7.1'
__version__ = '0.8.0'
7 changes: 6 additions & 1 deletion ebcpy/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,12 @@ def save(self, filepath: str = None, **kwargs) -> None:
self._obj.to_csv(filepath, sep=kwargs.get("sep", ","))
elif ".parquet" in filepath.name:
parquet_split = filepath.name.split(".parquet")
self._obj.to_parquet(
# Parquet doesn't support SparseDtype — densify before writing
df_to_save = self._obj.copy()
for col in df_to_save.columns:
if isinstance(df_to_save[col].dtype, pd.SparseDtype):
df_to_save[col] = df_to_save[col].sparse.to_dense()
df_to_save.to_parquet(
Comment thread
HvanderStok marked this conversation as resolved.
filepath, engine=kwargs.get('engine', 'pyarrow'),
compression=parquet_split[-1][1:] if parquet_split[-1] else None,
index=True
Expand Down
26 changes: 25 additions & 1 deletion ebcpy/simulationapi/dymola_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class DymolaAPI(SimulationAPI):
:keyword Boolean show_window:
True to show the Dymola window. Default is False
:keyword Boolean modify_structural_parameters:
.. deprecated::
Will be removed in the next major release.
Use model name modifiers directly instead.
True to automatically set the structural parameters of the
simulation model via Modelica modifiers. Default is True.
See also the keyword ``structural_parameters``
Expand Down Expand Up @@ -198,7 +201,15 @@ def __init__(
self.fully_initialized = False
self.debug = kwargs.pop("debug", False)
self.show_window = kwargs.pop("show_window", False)
self.modify_structural_parameters = kwargs.pop("modify_structural_parameters", True)
_modify_structural_parameters = kwargs.pop("modify_structural_parameters", True)
if _modify_structural_parameters is not True:
warnings.warn(
"'modify_structural_parameters' is deprecated and will be removed "
"in the next major release. Use model name modifiers directly instead.",
FutureWarning,
stacklevel=2,
)
self.modify_structural_parameters = _modify_structural_parameters
self.equidistant_output = kwargs.pop("equidistant_output", True)
_variables_to_save = kwargs.pop("variables_to_save", {})
self.experiment_setup_output = ExperimentSetupOutput(**_variables_to_save)
Expand Down Expand Up @@ -369,6 +380,10 @@ def simulate(self,
:keyword dict kwargs_postprocessing:
Keyword arguments used in the function `postprocess_mat_result`.
:keyword List[str] structural_parameters:
.. deprecated::
Will be removed in the next major release.
Write modifiers directly in the model_name instead, e.g.
``model_name='MyModel(myParam=newValue)'``
A list containing all parameter names which are structural in Modelica.
This means a modifier has to be created in order to change
the value of this parameter. Internally, the given list
Expand All @@ -386,6 +401,15 @@ def simulate(self,
"""
# Handle special case for structural_parameters
if "structural_parameters" in kwargs:
warnings.warn(
"The 'structural_parameters' keyword argument is deprecated and will "
"be removed in the next major release. Instead, write modifiers directly "
"in the model_name, e.g.:\n"
" model_name='MyModel(myParam=newValue)'\n"
"or pass modified model names via the 'model_names' keyword argument of simulate().",
FutureWarning,
stacklevel=2,
)
_struc_params = kwargs["structural_parameters"]
# Check if input is 2-dimensional for multiprocessing.
# If not, make it 2-dimensional to avoid list flattening in
Expand Down
207 changes: 207 additions & 0 deletions ebcpy/simulationapi/dymola_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
from pathlib import Path
from typing import Union, List

from .dymola_api import DymolaAPI
from ..data_types import load_time_series_data


def _default_result_file_names(result_file_name, parameters):
"""Generate unique result file names from parameter values."""
result_file_names = []
for idx, param_dict in enumerate(parameters):
name = result_file_name
for key, val in param_dict.items():
short_key = key.split(".")[-1]
name += f"_{short_key}{str(val).replace('.', '_')}"
result_file_names.append(name)
return result_file_names


def simple_dymola_sim_study(
model_names: List[str],
simulation_setup: dict,
working_directory: Union[str, Path],
save_path: Union[str, Path],
model_result_file_names: List[str],
parameters: Union[dict, List[dict]] = None,
n_cpu: int = 4,
use_parameter_study: bool = False,
result_file_name_func=None,
kwargs_postprocessing: dict = None,
postprocess_mat_result=None,
mos_script_pre: Union[str, Path] = None,
packages: List[Union[str, Path]] = None,
**kwargs
):
"""
Run a Dymola simulation study with multiple models and/or parameter variations.

This function supports two simulation modes:

**Parameter study** (``use_parameter_study=True``):
Each model is simulated separately with all parameter sets.
Useful when you want the full cross-product of models × parameters.
Each model gets its own DymolaAPI instance, which translates the model
once and then runs all parameter variations.

**Model comparison** (``use_parameter_study=False``):
All models are simulated in a single ``simulate()`` call using the
``model_names`` keyword. Each model can receive the same parameters
(pass a single dict) or individual parameters (pass a list of dicts
matching the length of ``model_names``). Each model is translated
individually.

Both modes use model name modifiers to change structural parameters.
This is the recommended approach — write the modifier directly in the
model name string.
Comment thread
HoeppJ marked this conversation as resolved.
(e.g. 'BESMod.Examples.GasBoilerBuildingOnly(
redeclare BESMod.Systems.Control.DHWSuperheating control(dTDHW=10))')

:param list[str] model_names:
List of Dymola model names, optionally with modifiers.
E.g. ``["MyModel(nLayer=1)", "MyModel(nLayer=2)"]``
:param dict simulation_setup:
Simulation settings with keys ``start_time``, ``stop_time``,
and ``output_interval``.
:param str,Path working_directory:
Dymola working directory.
:param str,Path save_path:
Directory for saving simulation results.
:param list[str] model_result_file_names:
Base names for the result files, one per model.
:param dict,list[dict] parameters:
Parameter values for the simulation. For parameter studies, pass a list
of dicts. For model comparison, pass a single dict (applied to all models)
or a list of dicts (one per model).
:param int n_cpu:
Number of parallel Dymola processes. Default is 4.
:param bool use_parameter_study:
If True, runs each model with all parameter sets (cross-product).
If False, runs all models in a single call.
:param callable result_file_name_func:
Function to generate unique result file names for parameter studies.
Signature: ``func(result_file_name, parameters) -> list[str]``
Default generates names by appending parameter key-value pairs.
:param dict kwargs_postprocessing:
Keyword arguments passed to the post-processing function.
Required if ``postprocess_mat_result`` is provided.
:param callable postprocess_mat_result:
Custom post-processing function. If None (default), .mat files
are kept unchanged. Signature: ``func(mat_result_file, **kwargs_postprocessing)``
:param str,Path mos_script_pre:
Path to a .mos script executed before loading packages.
Typically, the startup script of your Modelica library.
:param list packages:
Additional Modelica packages not loaded by ``mos_script_pre``.
:param kwargs:
Additional keyword arguments forwarded to ``DymolaAPI`` constructor
(e.g. ``show_window``, ``debug``, ``n_restart``, ``dymola_version``)
and to ``DymolaAPI.simulate()`` (e.g. ``fail_on_error``).
:return: Result file paths. For parameter studies, a dict mapping model names
to lists of paths. For model comparison, a list of paths.
:rtype: dict or list
"""
# ## Default paths
if working_directory is None:
working_directory = Path(__file__).parent.joinpath("results", "working_directory")
if save_path is None:
save_path = Path(__file__).parent.joinpath("results", "SimResults")
if packages is None:
packages = []

if len(model_result_file_names) != len(model_names):
raise ValueError(
f"model_result_file_names has length {len(model_result_file_names)} "
f"but model_names has length {len(model_names)}. They must match."
)
if use_parameter_study and not isinstance(parameters, list):
raise TypeError(
"For parameter studies, parameters must be a list of dicts."
)

# ## Post-processing setup
# Dymola produces .mat files by default. These are large and use a float
# index (seconds). The post-processing function converts them to a more
# usable format (e.g. datetime-indexed parquet) containing only the
# variables you need.
if postprocess_mat_result is not None and kwargs_postprocessing is None:
raise ValueError(
"kwargs_postprocessing is required when postprocess_mat_result is provided. "
"Pass a dict with the keyword arguments for your post-processing function."
)
# Build the simulate kwargs for postprocessing
postprocessing_kwargs = {}
if postprocess_mat_result is not None:
postprocessing_kwargs["postprocess_mat_result"] = postprocess_mat_result
postprocessing_kwargs["kwargs_postprocessing"] = kwargs_postprocessing

# ## Separate kwargs for DymolaAPI constructor and simulate()
# Known simulate() kwargs are forwarded there, everything else goes to DymolaAPI.
simulate_kwarg_keys = {"inputs", "table_name", "file_name", "fail_on_error", "show_eventlog", "squeeze"}
simulate_kwargs = {k: kwargs.pop(k) for k in simulate_kwarg_keys if k in kwargs}

# ## Run simulations
if use_parameter_study:
# ### Parameter study mode
# Iterate over each model variant. For each model, a separate DymolaAPI
# instance is created, the model is translated once, and all parameter
# sets are simulated. This is efficient because translation (the slow part)
# happens only once per model.
if result_file_name_func is None:
result_file_name_func = _default_result_file_names
all_result_paths = {}
for model_name, result_file_name in zip(model_names, model_result_file_names):
# Create unique result file names by encoding the varied parameter values.
# Adapt this naming scheme to your parameter study.
result_file_names = result_file_name_func(result_file_name, parameters)
Comment thread
HvanderStok marked this conversation as resolved.

dym_api = DymolaAPI(
mos_script_pre=mos_script_pre,
model_name=model_name,
working_directory=working_directory,
n_cpu=n_cpu,
packages=packages,
**kwargs
)
dym_api.set_sim_setup(sim_setup=simulation_setup)

result_paths = dym_api.simulate(
parameters=parameters,
return_option="savepath",
savepath=save_path,
result_file_name=result_file_names,
**postprocessing_kwargs,
**simulate_kwargs
)
all_result_paths[model_name] = result_paths
dym_api.close()

return all_result_paths

else:
# ### Model comparison mode
# All models are simulated in a single DymolaAPI call using the
# model_names keyword. Dymola translates each model on the fly.
# This is convenient for comparing different model configurations
# with the same or individual parameter sets.
dym_api = DymolaAPI(
mos_script_pre=mos_script_pre,
working_directory=working_directory,
n_cpu=n_cpu,
packages=packages,
**kwargs
)
dym_api.set_sim_setup(sim_setup=simulation_setup)

result_paths = dym_api.simulate(
model_names=model_names,
parameters=parameters,
return_option="savepath",
savepath=save_path,
result_file_name=model_result_file_names,
**postprocessing_kwargs,
**simulate_kwargs
)
dym_api.close()

return result_paths
67 changes: 50 additions & 17 deletions ebcpy/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,26 +48,44 @@ def setup_logger(name: str,
return logger


def get_names(all_names: list, patterns: Union[str, List[str]]) -> List[str]:
def get_names(
all_names: list,
patterns: Union[str, List[str]],
exclude: Union[str, List[str]] = None
) -> List[str]:
"""
Filter a list of candidate names by literal values or glob-style patterns.
Filter a list of candidate names by literal values or glob-style patterns,
optionally excluding names that match exclusion patterns.

This function returns all names from `all_names` that match the provided
`patterns`. Patterns may be a single string or a list of strings, and may
contain the wildcard `*` to match any sequence of characters. Literal names
without `*` must match exactly. The matching is performed in two steps:
1. Each pattern is translated to a regular expression if it contains `*`,
otherwise used as a literal match.
2. Any pattern that matches no names in `all_names` raises a warning.
This function returns all names from ``all_names`` that match the provided
``patterns`` and do not match any of the ``exclude`` patterns.
Patterns may be a single string or a list of strings, and may
contain the wildcard ``*`` to match any sequence of characters. Literal names
without ``*`` must match exactly.

The returned list preserves the order of `all_names`.
The returned list preserves the order of ``all_names``.

:param all_names: List of available names to filter.
:param patterns: A pattern or list of patterns (with optional `*` wildcards)
to match against `all_names`.
:return: A list of names from `all_names` that match any of the given patterns,
in original order.
:raises KeyError: If any pattern does not match at least one name.
:param list all_names:
List of available names to filter.
:param str,list[str] patterns:
A pattern or list of patterns (with optional ``*`` wildcards)
to match against ``all_names``.
:param str,list[str] exclude:
A pattern or list of patterns to exclude from the results.
Names matching any exclusion pattern are removed after the
inclusion step. Default is None (no exclusion).
:return: A list of names from ``all_names`` that match any of the given
patterns and none of the exclusion patterns, in original order.
:rtype: list[str]
:raises warning: If any inclusion pattern does not match at least one name.

Example:

>>> names = ["wall.layer[1].T", "wall.layer[2].T", "wall.layer[1].Q_flow"]
>>> get_names(names, "wall.layer[*].T")
['wall.layer[1].T', 'wall.layer[2].T']
>>> get_names(names, "wall.layer[*].*", exclude="*Q_flow")
['wall.layer[1].T', 'wall.layer[2].T']
"""
if isinstance(patterns, str):
patterns = [patterns]
Expand All @@ -93,6 +111,21 @@ def get_names(all_names: list, patterns: Union[str, List[str]]) -> List[str]:
"The following variable names/patterns are not in the given .mat file: "
+ ", ".join(unmatched)
)
Comment thread
HvanderStok marked this conversation as resolved.
# preserve original order

# Apply exclusion patterns
if exclude is not None:
if isinstance(exclude, str):
exclude = [exclude]
excluded = set()
for pat in exclude:
if '*' in pat:
regex = '^' + re.escape(pat).replace(r'\*', '.*') + '$'
excluded.update(k for k in matched if re.match(regex, k))
else:
if pat in matched:
excluded.add(pat)
matched -= excluded

# Preserve original order
names = [var for var in all_names if var in matched]
return names
Loading
Loading