import OMPython import os import pathlib import pytest import sys import tempfile import numpy as np skip_on_windows = pytest.mark.skipif( sys.platform.startswith("win"), reason="OpenModelica Docker image is Linux-only; skipping on Windows.", ) skip_python_older_312 = pytest.mark.skipif( sys.version_info < (3, 12), reason="OMCPath(non-local) only working for Python >= 3.12.", ) @pytest.fixture def model_firstorder_content(): return ("""model M Real x(start = 1, fixed = true); parameter Real a = -1; equation der(x) = x*a; end M; """) @pytest.fixture def model_firstorder(tmp_path, model_firstorder_content): mod = tmp_path / "M.mo" mod.write_text(model_firstorder_content) return mod def test_ModelicaSystem_loop(model_firstorder): def worker(): filePath = model_firstorder.as_posix() mod = OMPython.ModelicaSystem() mod.model( file=filePath, name="M", ) mod.simulate() mod.convertMo2Fmu(fmuType="me") for _ in range(10): worker() def test_setParameters(): omc = OMPython.OMCSessionZMQ() model_path_str = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels" model_path = omc.omcpath(model_path_str) mod = OMPython.ModelicaSystem() mod.model( file=model_path / "BouncingBall.mo", name="BouncingBall", ) # method 1 (test depreciated variants) mod.setParameters("e=1.234") mod.setParameters(["g=321.0"]) assert mod.getParameters("e") == ["1.234"] assert mod.getParameters("g") == ["321.0"] assert mod.getParameters() == { "e": "1.234", "g": "321.0", } with pytest.raises(KeyError): mod.getParameters("thisParameterDoesNotExist") # method 2 (new style) pvals = {"e": 21.3, "g": 0.12} mod.setParameters(**pvals) assert mod.getParameters() == { "e": "21.3", "g": "0.12", } assert mod.getParameters(["e", "g"]) == ["21.3", "0.12"] assert mod.getParameters(["g", "e"]) == ["0.12", "21.3"] with pytest.raises(KeyError): mod.getParameters(["g", "thisParameterDoesNotExist"]) def test_setSimulationOptions(): omc = OMPython.OMCSessionZMQ() model_path_str = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels" model_path = omc.omcpath(model_path_str) mod = OMPython.ModelicaSystem() mod.model( file=model_path / "BouncingBall.mo", name="BouncingBall", ) # method 1 mod.setSimulationOptions(stopTime=1.234) mod.setSimulationOptions(tolerance=1.1e-08) assert mod.getSimulationOptions("stopTime") == ["1.234"] assert mod.getSimulationOptions("tolerance") == ["1.1e-08"] assert mod.getSimulationOptions(["tolerance", "stopTime"]) == ["1.1e-08", "1.234"] d = mod.getSimulationOptions() assert isinstance(d, dict) assert d["stopTime"] == "1.234" assert d["tolerance"] == "1.1e-08" with pytest.raises(KeyError): mod.getSimulationOptions("thisOptionDoesNotExist") # method 2 mod.setSimulationOptions(stopTime=2.1, tolerance=1.2e-08) d = mod.getSimulationOptions() assert d["stopTime"] == "2.1" assert d["tolerance"] == "1.2e-08" def test_relative_path(model_firstorder): cwd = pathlib.Path.cwd() (fd, name) = tempfile.mkstemp(prefix='tmpOMPython.tests', dir=cwd, text=True) try: with os.fdopen(fd, 'w') as f: f.write(model_firstorder.read_text()) model_file = pathlib.Path(name).relative_to(cwd) model_relative = str(model_file) assert "/" not in model_relative mod = OMPython.ModelicaSystem() mod.model( file=model_relative, name="M", ) assert float(mod.getParameters("a")[0]) == -1 finally: model_file.unlink() # clean up the temporary file def test_customBuildDirectory(tmp_path, model_firstorder): filePath = model_firstorder.as_posix() tmpdir = tmp_path / "tmpdir1" tmpdir.mkdir() mod = OMPython.ModelicaSystem(customBuildDirectory=tmpdir) mod.model( file=filePath, name="M", ) assert pathlib.Path(mod.getWorkDirectory()).resolve() == tmpdir.resolve() result_file = tmpdir / "a.mat" assert not result_file.exists() mod.simulate(resultfile="a.mat") assert result_file.is_file() @skip_on_windows @skip_python_older_312 def test_getSolutions_docker(model_firstorder): omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") omc = OMPython.OMCSessionZMQ(omc_process=omcp) mod = OMPython.ModelicaSystem( omc_process=omc.omc_process, ) mod.model( name="M", file=model_firstorder.as_posix(), ) _run_getSolutions(mod) def test_getSolutions(model_firstorder): mod = OMPython.ModelicaSystem() mod.model( file=model_firstorder.as_posix(), name="M", ) _run_getSolutions(mod) def _run_getSolutions(mod): x0 = 1 a = -1 tau = -1 / a stopTime = 5*tau simOptions = {"stopTime": stopTime, "stepSize": 0.1, "tolerance": 1e-8} mod.setSimulationOptions(**simOptions) mod.simulate() x = mod.getSolutions("x") t, x2 = mod.getSolutions(["time", "x"]) assert (x2 == x).all() sol_names = mod.getSolutions() assert isinstance(sol_names, tuple) assert "time" in sol_names assert "x" in sol_names assert "der(x)" in sol_names with pytest.raises(OMPython.ModelicaSystemError): mod.getSolutions("thisVariableDoesNotExist") assert np.isclose(t[0], 0), "time does not start at 0" assert np.isclose(t[-1], stopTime), "time does not end at stopTime" x_analytical = x0 * np.exp(a*t) assert np.isclose(x, x_analytical, rtol=1e-4).all() def test_getters(tmp_path): model_file = tmp_path / "M_getters.mo" model_file.write_text(""" model M_getters Real x(start = 1, fixed = true); output Real y "the derivative"; parameter Real a = -0.5; parameter Real b = 0.1; equation der(x) = x*a + b; y = der(x); end M_getters; """) mod = OMPython.ModelicaSystem() mod.model( file=model_file.as_posix(), name="M_getters", ) q = mod.getQuantities() assert isinstance(q, list) assert sorted(q, key=lambda d: d["name"]) == sorted([ { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'local', 'changeable': 'true', 'description': None, 'max': None, 'min': None, 'name': 'x', 'start': '1.0', 'unit': None, 'variability': 'continuous', }, { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'local', 'changeable': 'false', 'description': None, 'max': None, 'min': None, 'name': 'der(x)', 'start': None, 'unit': None, 'variability': 'continuous', }, { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'output', 'changeable': 'false', 'description': 'the derivative', 'max': None, 'min': None, 'name': 'y', 'start': '-0.4', 'unit': None, 'variability': 'continuous', }, { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'parameter', 'changeable': 'true', 'description': None, 'max': None, 'min': None, 'name': 'a', 'start': '-0.5', 'unit': None, 'variability': 'parameter', }, { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'parameter', 'changeable': 'true', 'description': None, 'max': None, 'min': None, 'name': 'b', 'start': '0.1', 'unit': None, 'variability': 'parameter', } ], key=lambda d: d["name"]) assert mod.getQuantities("y") == [ { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'output', 'changeable': 'false', 'description': 'the derivative', 'max': None, 'min': None, 'name': 'y', 'start': '-0.4', 'unit': None, 'variability': 'continuous', } ] assert mod.getQuantities(["y", "x"]) == [ { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'output', 'changeable': 'false', 'description': 'the derivative', 'max': None, 'min': None, 'name': 'y', 'start': '-0.4', 'unit': None, 'variability': 'continuous', }, { 'alias': 'noAlias', 'aliasvariable': None, 'causality': 'local', 'changeable': 'true', 'description': None, 'max': None, 'min': None, 'name': 'x', 'start': '1.0', 'unit': None, 'variability': 'continuous', }, ] with pytest.raises(KeyError): mod.getQuantities("thisQuantityDoesNotExist") assert mod.getInputs() == {} with pytest.raises(KeyError): mod.getInputs("thisInputDoesNotExist") # getOutputs before simulate() assert mod.getOutputs() == {'y': '-0.4'} assert mod.getOutputs("y") == ["-0.4"] assert mod.getOutputs(["y", "y"]) == ["-0.4", "-0.4"] with pytest.raises(KeyError): mod.getOutputs("thisOutputDoesNotExist") # getContinuous before simulate(): assert mod.getContinuous() == { 'x': '1.0', 'der(x)': None, 'y': '-0.4' } assert mod.getContinuous("y") == ['-0.4'] assert mod.getContinuous(["y", "x"]) == ['-0.4', '1.0'] with pytest.raises(KeyError): mod.getContinuous("a") # a is a parameter stopTime = 1.0 a = -0.5 b = 0.1 x0 = 1.0 x_analytical = -b/a + (x0 + b/a) * np.exp(a * stopTime) dx_analytical = (x0 + b/a) * a * np.exp(a * stopTime) mod.setSimulationOptions(stopTime=stopTime) mod.simulate() # getOutputs after simulate() d = mod.getOutputs() assert d.keys() == {"y"} assert np.isclose(d["y"], dx_analytical, 1e-4) assert mod.getOutputs("y") == [d["y"]] assert mod.getOutputs(["y", "y"]) == [d["y"], d["y"]] with pytest.raises(KeyError): mod.getOutputs("thisOutputDoesNotExist") # getContinuous after simulate() should return values at end of simulation: with pytest.raises(OMPython.ModelicaSystemError): mod.getContinuous("a") # a is a parameter with pytest.raises(OMPython.ModelicaSystemError): mod.getContinuous(["x", "a", "y"]) # a is a parameter d = mod.getContinuous() assert d.keys() == {"x", "der(x)", "y"} assert np.isclose(d["x"], x_analytical, 1e-4) assert np.isclose(d["der(x)"], dx_analytical, 1e-4) assert np.isclose(d["y"], dx_analytical, 1e-4) assert mod.getContinuous("x") == [d["x"]] assert mod.getContinuous(["y", "x"]) == [d["y"], d["x"]] with pytest.raises(OMPython.ModelicaSystemError): mod.getContinuous("a") # a is a parameter with pytest.raises(OMPython.ModelicaSystemError): mod.setSimulationOptions(thisOptionDoesNotExist=3) def test_simulate_inputs(tmp_path): model_file = tmp_path / "M_input.mo" model_file.write_text(""" model M_input Real x(start=0, fixed=true); input Real u1; input Real u2; output Real y; equation der(x) = u1 + u2; y = x; end M_input; """) mod = OMPython.ModelicaSystem() mod.model( file=model_file.as_posix(), name="M_input", ) simOptions = {"stopTime": 1.0} mod.setSimulationOptions(**simOptions) # integrate zero (no setInputs call) - it should default to None -> 0 assert mod.getInputs() == { "u1": None, "u2": None, } mod.simulate() y = mod.getSolutions("y")[0] assert np.isclose(y[-1], 0.0) # integrate a constant mod.setInputs(u1=2.5) assert mod.getInputs() == { "u1": [ (0.0, 2.5), (1.0, 2.5), ], # u2 is set due to the call to simulate() above "u2": [ (0.0, 0.0), (1.0, 0.0), ], } mod.simulate() y = mod.getSolutions("y")[0] assert np.isclose(y[-1], 2.5) # now let's integrate the sum of two ramps inputs = {"u1": [(0.0, 0.0), (0.5, 2), (1.0, 0)]} mod.setInputs(**inputs) assert mod.getInputs("u1") == [[ (0.0, 0.0), (0.5, 2.0), (1.0, 0.0), ]] mod.simulate() y = mod.getSolutions("y")[0] assert np.isclose(y[-1], 1.0) # let's try some edge cases # unmatched startTime with pytest.raises(OMPython.ModelicaSystemError): mod.setInputs(u1=[(-0.5, 0.0), (1.0, 1)]) mod.simulate() # unmatched stopTime with pytest.raises(OMPython.ModelicaSystemError): mod.setInputs(u1=[(0.0, 0.0), (0.5, 1)]) mod.simulate() # Let's use both inputs, but each one with different number of # samples. This has an effect when generating the csv file. inputs = { "u1": [(0.0, 0), (1.0, 1)], "u2": [(0.0, 0), (0.25, 0.5), (0.5, 1.0), (1.0, 0)], } mod.setInputs(**inputs) csv_file = mod._createCSVData() assert pathlib.Path(csv_file).read_text() == """time,u1,u2,end 0.0,0.0,0.0,0 0.25,0.25,0.5,0 0.5,0.5,1.0,0 1.0,1.0,0.0,0 """ mod.simulate() y = mod.getSolutions("y")[0] assert np.isclose(y[-1], 1.0)