Skip to content

Commit 9967a55

Browse files
committed
Merge branch 'ModelicaSystemDoE' into ModelicaSystemDoE_use_OMCPath
2 parents 2f19215 + 08cf292 commit 9967a55

File tree

3 files changed

+461
-2
lines changed

3 files changed

+461
-2
lines changed

OMPython/ModelicaSystem.py

Lines changed: 375 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@
3434

3535
import ast
3636
from dataclasses import dataclass
37+
import itertools
3738
import logging
3839
import numbers
3940
import numpy as np
4041
import os
42+
import pathlib
43+
import queue
4144
import textwrap
42-
from typing import Optional, Any
45+
import threading
46+
from typing import Any, Optional
4347
import warnings
4448
import xml.etree.ElementTree as ET
4549

@@ -1728,3 +1732,373 @@ def getLinearOutputs(self) -> list[str]:
17281732
def getLinearStates(self) -> list[str]:
17291733
"""Get names of state variables of the linearized model."""
17301734
return self._linearized_states
1735+
1736+
1737+
class ModelicaSystemDoE:
1738+
"""
1739+
Class to run DoEs based on a (Open)Modelica model using ModelicaSystem
1740+
1741+
Example
1742+
-------
1743+
```
1744+
import OMPython
1745+
import pathlib
1746+
1747+
1748+
def run_doe():
1749+
mypath = pathlib.Path('.')
1750+
1751+
model = mypath / "M.mo"
1752+
model.write_text(
1753+
" model M\n"
1754+
" parameter Integer p=1;\n"
1755+
" parameter Integer q=1;\n"
1756+
" parameter Real a = -1;\n"
1757+
" parameter Real b = -1;\n"
1758+
" Real x[p];\n"
1759+
" Real y[q];\n"
1760+
" equation\n"
1761+
" der(x) = a * fill(1.0, p);\n"
1762+
" der(y) = b * fill(1.0, q);\n"
1763+
" end M;\n"
1764+
)
1765+
1766+
param = {
1767+
# structural
1768+
'p': [1, 2],
1769+
'q': [3, 4],
1770+
# simple
1771+
'a': [5, 6],
1772+
'b': [7, 8],
1773+
}
1774+
1775+
resdir = mypath / 'DoE'
1776+
resdir.mkdir(exist_ok=True)
1777+
1778+
doe_mod = OMPython.ModelicaSystemDoE(
1779+
fileName=model.as_posix(),
1780+
modelName="M",
1781+
parameters=param,
1782+
resultpath=resdir,
1783+
simargs={"override": {'stopTime': 1.0}},
1784+
)
1785+
doe_mod.prepare()
1786+
doe_dict = doe_mod.get_doe()
1787+
doe_mod.simulate()
1788+
doe_sol = doe_mod.get_solutions()
1789+
1790+
# ... work with doe_df and doe_sol ...
1791+
1792+
1793+
if __name__ == "__main__":
1794+
run_doe()
1795+
```
1796+
1797+
"""
1798+
1799+
DICT_RESULT_FILENAME: str = 'result filename'
1800+
DICT_RESULT_AVAILABLE: str = 'result available'
1801+
1802+
def __init__(
1803+
self,
1804+
fileName: Optional[str | os.PathLike | pathlib.Path] = None,
1805+
modelName: Optional[str] = None,
1806+
lmodel: Optional[list[str | tuple[str, str]]] = None,
1807+
commandLineOptions: Optional[str] = None,
1808+
variableFilter: Optional[str] = None,
1809+
customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None,
1810+
omhome: Optional[str] = None,
1811+
1812+
simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None,
1813+
timeout: Optional[int] = None,
1814+
1815+
resultpath: Optional[pathlib.Path] = None,
1816+
parameters: Optional[dict[str, list[str] | list[int] | list[float]]] = None,
1817+
) -> None:
1818+
"""
1819+
Initialisation of ModelicaSystemDoE. The parameters are based on: ModelicaSystem.__init__() and
1820+
ModelicaSystem.simulate(). Additionally, the path to store the result files is needed (= resultpath) as well as
1821+
a list of parameters to vary for the Doe (= parameters). All possible combinations are considered.
1822+
"""
1823+
self._lmodel = lmodel
1824+
self._modelName = modelName
1825+
self._fileName = fileName
1826+
1827+
self._CommandLineOptions = commandLineOptions
1828+
self._variableFilter = variableFilter
1829+
self._customBuildDirectory = customBuildDirectory
1830+
self._omhome = omhome
1831+
1832+
# reference for the model; not used for any simulations but to evaluate parameters, etc.
1833+
self._mod = ModelicaSystem(
1834+
fileName=self._fileName,
1835+
modelName=self._modelName,
1836+
lmodel=self._lmodel,
1837+
commandLineOptions=self._CommandLineOptions,
1838+
variableFilter=self._variableFilter,
1839+
customBuildDirectory=self._customBuildDirectory,
1840+
omhome=self._omhome,
1841+
)
1842+
1843+
self._simargs = simargs
1844+
self._timeout = timeout
1845+
1846+
if isinstance(resultpath, pathlib.Path):
1847+
self._resultpath = resultpath
1848+
else:
1849+
self._resultpath = pathlib.Path('.')
1850+
1851+
if isinstance(parameters, dict):
1852+
self._parameters = parameters
1853+
else:
1854+
self._parameters = {}
1855+
1856+
self._sim_dict: Optional[dict[str, dict[str, Any]]] = None
1857+
self._sim_task_query: queue.Queue = queue.Queue()
1858+
1859+
def prepare(self) -> int:
1860+
"""
1861+
Prepare the DoE by evaluating the parameters. Each structural parameter requires a new instance of
1862+
ModelicaSystem while the non-structural parameters can just be set on the executable.
1863+
1864+
The return value is the number of simulation defined.
1865+
"""
1866+
1867+
param_structure = {}
1868+
param_simple = {}
1869+
for param_name in self._parameters.keys():
1870+
changeable = self._mod.isParameterChangeable(name=param_name)
1871+
logger.info(f"Parameter {repr(param_name)} is changeable? {changeable}")
1872+
1873+
if changeable:
1874+
param_simple[param_name] = self._parameters[param_name]
1875+
else:
1876+
param_structure[param_name] = self._parameters[param_name]
1877+
1878+
param_structure_combinations = list(itertools.product(*param_structure.values()))
1879+
param_simple_combinations = list(itertools.product(*param_simple.values()))
1880+
1881+
self._sim_dict = {}
1882+
for idx_pc_structure, pc_structure in enumerate(param_structure_combinations):
1883+
mod_structure = ModelicaSystem(
1884+
fileName=self._fileName,
1885+
modelName=self._modelName,
1886+
lmodel=self._lmodel,
1887+
commandLineOptions=self._CommandLineOptions,
1888+
variableFilter=self._variableFilter,
1889+
customBuildDirectory=self._customBuildDirectory,
1890+
omhome=self._omhome,
1891+
build=False,
1892+
)
1893+
1894+
sim_param_structure = {}
1895+
for idx_structure, pk_structure in enumerate(param_structure.keys()):
1896+
sim_param_structure[pk_structure] = pc_structure[idx_structure]
1897+
1898+
pk_value = pc_structure[idx_structure]
1899+
if isinstance(pk_value, str):
1900+
expression = f"setParameterValue({self._modelName}, {pk_structure}, \"{pk_value}\")"
1901+
elif isinstance(pk_value, bool):
1902+
pk_value_bool_str = "true" if pk_value else "false"
1903+
expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value_bool_str});"
1904+
else:
1905+
expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value})"
1906+
res = mod_structure.sendExpression(expression)
1907+
if not res:
1908+
raise ModelicaSystemError(f"Cannot set structural parameter {self._modelName}.{pk_structure} "
1909+
f"to {pk_value} using {repr(expression)}")
1910+
1911+
mod_structure.buildModel(variableFilter=self._variableFilter)
1912+
1913+
for idx_pc_simple, pc_simple in enumerate(param_simple_combinations):
1914+
sim_param_simple = {}
1915+
for idx_simple, pk_simple in enumerate(param_simple.keys()):
1916+
sim_param_simple[pk_simple] = pc_simple[idx_simple]
1917+
1918+
resfilename = f"DOE_{idx_pc_structure:09d}_{idx_pc_simple:09d}.mat"
1919+
logger.info(f"use result file {repr(resfilename)} "
1920+
f"for structural parameters: {sim_param_structure} "
1921+
f"and simple parameters: {sim_param_simple}")
1922+
resultfile = self._resultpath / resfilename
1923+
1924+
df_data = (
1925+
{
1926+
'ID structure': idx_pc_structure,
1927+
}
1928+
| sim_param_structure
1929+
| {
1930+
'ID non-structure': idx_pc_simple,
1931+
}
1932+
| sim_param_simple
1933+
| {
1934+
self.DICT_RESULT_AVAILABLE: False,
1935+
}
1936+
)
1937+
1938+
self._sim_dict[resfilename] = df_data
1939+
1940+
mscmd = mod_structure.simulate_cmd(
1941+
result_file=resultfile.absolute().resolve(),
1942+
timeout=self._timeout,
1943+
)
1944+
if self._simargs is not None:
1945+
mscmd.args_set(args=self._simargs)
1946+
mscmd.args_set(args={"override": sim_param_simple})
1947+
1948+
self._sim_task_query.put(mscmd)
1949+
1950+
logger.info(f"Prepared {self._sim_task_query.qsize()} simulation definitions for the defined DoE.")
1951+
1952+
return self._sim_task_query.qsize()
1953+
1954+
def get_doe(self) -> Optional[dict[str, dict[str, Any]]]:
1955+
"""
1956+
Get the defined DoE as a dict, where each key is the result filename and the value is a dict of simulation
1957+
settings including structural and non-structural parameters.
1958+
1959+
The following code snippet can be used to convert the data to a pandas dataframe:
1960+
1961+
```
1962+
import pandas as pd
1963+
1964+
doe_dict = doe_mod.get_doe()
1965+
doe_df = pd.DataFrame.from_dict(data=doe_dict, orient='index')
1966+
```
1967+
1968+
"""
1969+
return self._sim_dict
1970+
1971+
def simulate(
1972+
self,
1973+
num_workers: int = 3,
1974+
) -> bool:
1975+
"""
1976+
Simulate the DoE using the defined number of workers.
1977+
1978+
Returns True if all simulations were done successfully, else False.
1979+
"""
1980+
1981+
sim_query_total = self._sim_task_query.qsize()
1982+
if not isinstance(self._sim_dict, dict) or len(self._sim_dict) == 0:
1983+
raise ModelicaSystemError("Missing Doe Summary!")
1984+
sim_dict_total = len(self._sim_dict)
1985+
1986+
def worker(worker_id, task_queue):
1987+
while True:
1988+
try:
1989+
# Get the next task from the queue
1990+
mscmd = task_queue.get(block=False)
1991+
except queue.Empty:
1992+
logger.info(f"[Worker {worker_id}] No more simulations to run.")
1993+
break
1994+
1995+
if mscmd is None:
1996+
raise ModelicaSystemError("Missing simulation definition!")
1997+
1998+
resultfile = mscmd.arg_get(key='r')
1999+
resultpath = pathlib.Path(resultfile)
2000+
2001+
logger.info(f"[Worker {worker_id}] Performing task: {resultpath.name}")
2002+
2003+
try:
2004+
mscmd.run()
2005+
except ModelicaSystemError as ex:
2006+
logger.warning(f"Simulation error for {resultpath.name}: {ex}")
2007+
2008+
# Mark the task as done
2009+
task_queue.task_done()
2010+
2011+
sim_query_done = sim_query_total - self._sim_task_query.qsize()
2012+
logger.info(f"[Worker {worker_id}] Task completed: {resultpath.name} "
2013+
f"({sim_query_total - sim_query_done}/{sim_query_total} = "
2014+
f"{(sim_query_total - sim_query_done) / sim_query_total * 100:.2f}% of tasks left)")
2015+
2016+
logger.info(f"Start simulations for DoE with {sim_query_total} simulations "
2017+
f"using {num_workers} workers ...")
2018+
2019+
# Create and start worker threads
2020+
threads = []
2021+
for i in range(num_workers):
2022+
thread = threading.Thread(target=worker, args=(i, self._sim_task_query))
2023+
thread.start()
2024+
threads.append(thread)
2025+
2026+
# Wait for all threads to complete
2027+
for thread in threads:
2028+
thread.join()
2029+
2030+
sim_dict_done = 0
2031+
for resultfilename in self._sim_dict:
2032+
resultfile = self._resultpath / resultfilename
2033+
2034+
# include check for an empty (=> 0B) result file which indicates a crash of the model executable
2035+
# see: https://github.com/OpenModelica/OMPython/issues/261
2036+
# https://github.com/OpenModelica/OpenModelica/issues/13829
2037+
if resultfile.is_file() and resultfile.stat().st_size > 0:
2038+
self._sim_dict[resultfilename][self.DICT_RESULT_AVAILABLE] = True
2039+
sim_dict_done += 1
2040+
2041+
logger.info(f"All workers finished ({sim_dict_done} of {sim_dict_total} simulations with a result file).")
2042+
2043+
return sim_dict_total == sim_dict_done
2044+
2045+
def get_solutions(
2046+
self,
2047+
var_list: Optional[list] = None,
2048+
) -> Optional[tuple[str] | dict[str, dict[str, np.ndarray]]]:
2049+
"""
2050+
Get all solutions of the DoE run. The following return values are possible:
2051+
2052+
* A list of variables if val_list == None
2053+
2054+
* The Solutions as dict[str, pd.DataFrame] if a value list (== val_list) is defined.
2055+
2056+
The following code snippet can be used to convert the solution data for each run to a pandas dataframe:
2057+
2058+
```
2059+
import pandas as pd
2060+
2061+
doe_sol = doe_mod.get_solutions()
2062+
for key in doe_sol:
2063+
data = doe_sol[key]['data']
2064+
if data:
2065+
doe_sol[key]['df'] = pd.DataFrame.from_dict(data=data)
2066+
else:
2067+
doe_sol[key]['df'] = None
2068+
```
2069+
2070+
"""
2071+
if not isinstance(self._sim_dict, dict):
2072+
return None
2073+
2074+
if len(self._sim_dict) == 0:
2075+
raise ModelicaSystemError("No result files available - all simulations did fail?")
2076+
2077+
sol_dict: dict[str, dict[str, Any]] = {}
2078+
for resultfilename in self._sim_dict:
2079+
resultfile = self._resultpath / resultfilename
2080+
2081+
sol_dict[resultfilename] = {}
2082+
2083+
if not self._sim_dict[resultfilename][self.DICT_RESULT_AVAILABLE]:
2084+
sol_dict[resultfilename]['msg'] = 'No result file available!'
2085+
sol_dict[resultfilename]['data'] = {}
2086+
continue
2087+
2088+
if var_list is None:
2089+
var_list_row = list(self._mod.getSolutions(resultfile=resultfile.as_posix()))
2090+
else:
2091+
var_list_row = var_list
2092+
2093+
try:
2094+
sol = self._mod.getSolutions(varList=var_list_row, resultfile=resultfile.as_posix())
2095+
sol_data = {var: sol[idx] for idx, var in enumerate(var_list_row)}
2096+
sol_dict[resultfilename]['msg'] = 'Simulation available'
2097+
sol_dict[resultfilename]['data'] = sol_data
2098+
except ModelicaSystemError as ex:
2099+
msg = f"Error reading solution for {resultfilename}: {ex}"
2100+
logger.warning(msg)
2101+
sol_dict[resultfilename]['msg'] = msg
2102+
sol_dict[resultfilename]['data'] = {}
2103+
2104+
return sol_dict

0 commit comments

Comments
 (0)