From 5b670ae60ead3aa6b1f343cfde5aed2783bdd4ce Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 8 Feb 2026 21:05:21 +0100 Subject: [PATCH 1/5] (D006) small fixes in ModelicaSystem [ModelicaSystemABC] reorder code in __init__() [ModelicaSystem*] linter fixes --- OMPython/ModelicaSystem.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 0eea5f15..07b5b5a2 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -383,22 +383,21 @@ def __init__( self._linearized_outputs: list[str] = [] # linearization output list self._linearized_states: list[str] = [] # linearization states list - self._session = session - - # get OpenModelica version - version_str = self._session.get_version() - self._version = self._parse_om_version(version=version_str) - self._simulated = False # True if the model has already been simulated self._result_file: Optional[OMPathABC] = None # for storing result file - self._work_dir: OMPathABC = self.setWorkDirectory(work_directory) - self._model_name: Optional[str] = None self._libraries: Optional[list[str | tuple[str, str]]] = None self._file_name: Optional[OMPathABC] = None self._variable_filter: Optional[str] = None + self._session = session + # get OpenModelica version + version_str = self._session.get_version() + self._version = self._parse_om_version(version=version_str) + + self._work_dir: OMPathABC = self.setWorkDirectory(work_directory) + def get_session(self) -> OMSessionABC: """ Return the OMC session used for this class. @@ -468,6 +467,8 @@ def _xmlparse(self, xml_file: OMPathABC): xml_content = xml_file.read_text() tree = ET.ElementTree(ET.fromstring(xml_content)) root = tree.getroot() + if root is None: + raise ModelicaSystemError(f"Cannot read XML file: {xml_file}") for attr in root.iter('DefaultExperiment'): for key in ("startTime", "stopTime", "stepSize", "tolerance", "solver", "outputFormat"): @@ -1935,7 +1936,7 @@ def getSolutions( self, varList: Optional[str | list[str]] = None, resultfile: Optional[str | os.PathLike] = None, - ) -> tuple[str] | np.ndarray: + ) -> tuple[str, ...] | np.ndarray: """Extract simulation results from a result data file. Args: @@ -1984,7 +1985,8 @@ def getSolutions( result_vars = self.sendExpression(expr=f'readSimulationResultVars("{result_file.as_posix()}")') self.sendExpression(expr="closeSimulationResultFile()") if varList is None: - return result_vars + var_list = [str(var) for var in result_vars] + return tuple(var_list) if isinstance(varList, str): var_list_checked = [varList] @@ -2064,6 +2066,8 @@ def convertFmu2Mo( raise ModelicaSystemError(f"Missing FMU file: {fmu_path.as_posix()}") filename = self._requestApi(apiName='importFMU', entity=fmu_path.as_posix()) + if not isinstance(filename, str): + raise ModelicaSystemError(f"Invalid return value for the FMU filename: {filename}") filepath = self.getWorkDirectory() / filename # report proper error message @@ -2106,7 +2110,9 @@ def optimize(self) -> dict[str, Any]: """ properties = ','.join(f"{key}={val}" for key, val in self._optimization_options.items()) self.set_command_line_options("-g=Optimica") - return self._requestApi(apiName='optimize', entity=self._model_name, properties=properties) + retval = self._requestApi(apiName='optimize', entity=self._model_name, properties=properties) + retval = cast(dict, retval) + return retval class ModelicaSystem(ModelicaSystemOMC): From b477a94cadcc1637b0171b8ca2ab4293864cf5a8 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 9 Feb 2026 21:23:45 +0100 Subject: [PATCH 2/5] (D008) add v4.0.0 compatibility layer [OMTypedParser] compatibility layer [__init__/OMCSession] prepare compatibility layer [ModelicaSystem] define as compatibility layer [ModelicaSystemCmd] define as compatibility layer --- OMPython/ModelicaSystem.py | 179 +++++++++++++++++++++++++++++ OMPython/OMCSession.py | 8 ++ OMPython/OMTypedParser.py | 3 + OMPython/__init__.py | 13 +++ tests/test_ModelicaSystemRunner.py | 2 +- 5 files changed, 204 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 07b5b5a2..03fd060b 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -2120,6 +2120,138 @@ class ModelicaSystem(ModelicaSystemOMC): Compatibility class. """ + def __init__( + self, + fileName: Optional[str | os.PathLike | pathlib.Path] = None, + modelName: Optional[str] = None, + lmodel: Optional[list[str | tuple[str, str]]] = None, + commandLineOptions: Optional[list[str]] = None, + variableFilter: Optional[str] = None, + customBuildDirectory: Optional[str | os.PathLike] = None, + omhome: Optional[str] = None, + omc_process: Optional[OMCSessionLocal] = None, + build: bool = True, + ) -> None: + super().__init__( + command_line_options=commandLineOptions, + work_directory=customBuildDirectory, + omhome=omhome, + session=omc_process, + ) + self.model( + model_name=modelName, + model_file=fileName, + libraries=lmodel, + variable_filter=variableFilter, + build=build, + ) + self._getconn = self._session + + def setCommandLineOptions(self, commandLineOptions: str): + super().set_command_line_options(command_line_option=commandLineOptions) + + def setContinuous( # type: ignore[override] + self, + cvals: str | list[str] | dict[str, Any], + ) -> bool: + if isinstance(cvals, dict): + return super().setContinuous(**cvals) + raise ModelicaSystemError("Only dict input supported for setContinuous()") + + def setParameters( # type: ignore[override] + self, + pvals: str | list[str] | dict[str, Any], + ) -> bool: + if isinstance(pvals, dict): + return super().setParameters(**pvals) + raise ModelicaSystemError("Only dict input supported for setParameters()") + + def setOptimizationOptions( # type: ignore[override] + self, + optimizationOptions: str | list[str] | dict[str, Any], + ) -> bool: + if isinstance(optimizationOptions, dict): + return super().setOptimizationOptions(**optimizationOptions) + raise ModelicaSystemError("Only dict input supported for setOptimizationOptions()") + + def setInputs( # type: ignore[override] + self, + name: str | list[str] | dict[str, Any], + ) -> bool: + if isinstance(name, dict): + return super().setInputs(**name) + raise ModelicaSystemError("Only dict input supported for setInputs()") + + def setSimulationOptions( # type: ignore[override] + self, + simOptions: str | list[str] | dict[str, Any], + ) -> bool: + if isinstance(simOptions, dict): + return super().setSimulationOptions(**simOptions) + raise ModelicaSystemError("Only dict input supported for setSimulationOptions()") + + def setLinearizationOptions( # type: ignore[override] + self, + linearizationOptions: str | list[str] | dict[str, Any], + ) -> bool: + if isinstance(linearizationOptions, dict): + return super().setLinearizationOptions(**linearizationOptions) + raise ModelicaSystemError("Only dict input supported for setLinearizationOptions()") + + def getContinuous( + self, + names: Optional[str | list[str]] = None, + ): + retval = super().getContinuous(names=names) + if self._simulated: + return retval + + if isinstance(retval, dict): + retval2: dict = {} + for key, val in retval.items(): + if np.isnan(val): + retval2[key] = None + else: + retval2[key] = str(val) + return retval2 + if isinstance(retval, list): + retval3: list[str | None] = [] + for val in retval: + if np.isnan(val): + retval3.append(None) + else: + retval3.append(str(val)) + return retval3 + + raise ModelExecutionException("Invalid data!") + + def getOutputs( + self, + names: Optional[str | list[str]] = None, + ): + retval = super().getOutputs(names=names) + if self._simulated: + return retval + + if isinstance(retval, dict): + retval2: dict = {} + for key, val in retval.items(): + if np.isnan(val): + retval2[key] = None + else: + retval2[key] = str(val) + return retval2 + if isinstance(retval, list): + retval3: list[str | None] = [] + for val in retval: + if np.isnan(val): + retval3.append(None) + else: + retval3.append(str(val)) + return retval3 + + raise ModelExecutionException("Invalid data!") + class ModelicaDoEABC(metaclass=abc.ABCMeta): """ @@ -2691,3 +2823,50 @@ def _prepare_structure_parameters( "pre-compiled binary of model.") return {} + + +class ModelicaSystemCmd(ModelExecutionCmd): + # TODO: docstring + + def __init__( + self, + runpath: pathlib.Path, + modelname: str, + timeout: float = 10.0, + ) -> None: + super().__init__( + runpath=runpath, + timeout=timeout, + cmd_prefix=[], + model_name=modelname, + ) + + def get_exe(self) -> pathlib.Path: + """Get the path to the compiled model executable.""" + # TODO: move to the top + import platform + + path_run = pathlib.Path(self._runpath) + if platform.system() == "Windows": + path_exe = path_run / f"{self._model_name}.exe" + else: + path_exe = path_run / self._model_name + + if not path_exe.exists(): + raise ModelicaSystemError(f"Application file path not found: {path_exe}") + + return path_exe + + def get_cmd(self) -> list: + """Get a list with the path to the executable and all command line args. + + This can later be used as an argument for subprocess.run(). + """ + + cmdl = [self.get_exe().as_posix()] + self.get_cmd_args() + + return cmdl + + def run(self): + cmd_definition = self.definition() + return cmd_definition.run() diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 83b5bb32..731005f1 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -2154,3 +2154,11 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC def sendExpression(self, expr: str, parsed: bool = True) -> Any: raise OMCSessionException(f"{self.__class__.__name__} does not uses an OMC server!") + + +DummyPopen = DockerPopen +OMCProcessLocal = OMCSessionLocal +OMCProcessPort = OMCSessionPort +OMCProcessDocker = OMCSessionDocker +OMCProcessDockerContainer = OMCSessionDockerContainer +OMCProcessWSL = OMCSessionWSL diff --git a/OMPython/OMTypedParser.py b/OMPython/OMTypedParser.py index 06912221..9fe810e0 100644 --- a/OMPython/OMTypedParser.py +++ b/OMPython/OMTypedParser.py @@ -161,3 +161,6 @@ def om_parser_typed(string) -> Any: if len(res) == 0: return None return res[0] + + +parseString = om_parser_typed diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 4dc2f974..c12f8524 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -23,6 +23,8 @@ ModelicaDoERunner, doe_get_solutions, + + ModelicaSystemCmd, ) from OMPython.OMCSession import ( OMPathABC, @@ -47,6 +49,11 @@ OMCSessionWSL, OMCSessionZMQ, + + OMCProcessLocal, + OMCProcessPort, + OMCProcessDocker, + OMCProcessDockerContainer, ) # global names imported if import 'from OMPython import *' is used @@ -58,6 +65,7 @@ 'ModelicaSystem', 'ModelicaSystemOMC', + 'ModelicaSystemCmd', 'ModelExecutionCmd', 'ModelicaSystemDoE', 'ModelicaDoEOMC', @@ -87,4 +95,9 @@ 'OMCSessionWSL', 'OMCSessionZMQ', + + 'OMCProcessLocal', + 'OMCProcessPort', + 'OMCProcessDocker', + 'OMCProcessDockerContainer', ] diff --git a/tests/test_ModelicaSystemRunner.py b/tests/test_ModelicaSystemRunner.py index 35541c99..ec9d734d 100644 --- a/tests/test_ModelicaSystemRunner.py +++ b/tests/test_ModelicaSystemRunner.py @@ -39,7 +39,7 @@ def param(): def test_runner(model_firstorder, param): # create a model using ModelicaSystem - mod = OMPython.ModelicaSystem() + mod = OMPython.ModelicaSystemOMC() mod.model( model_file=model_firstorder, model_name="M", From e568703c8c518257b6d3efde05cfd748521e8dcd Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 8 Feb 2026 20:48:16 +0100 Subject: [PATCH 3/5] (D007) define unittest / workflow for v4.0.0 add workflow to run unittests in ./tests tests from v4.0.0 fix test_linearization from v4.0.0 flake8 error: test_linearization.py:71:5: E741 ambiguous variable name 'l' this was fixed in: 'update usage of flake8 (#357)' (SHA1: 70cb446f537345c33f024aa44bc107548970ebc4) fix test_ModelicaSystem - needed adaptions: * convert OMCPath to pathlib.Path * use correct exceptions define test workflows for v400 --- .github/workflows/Test_v400.yml | 73 +++++ .github/workflows/Test_v400_py310.yml | 70 +++++ .github/workflows/Test_v4xx.yml | 73 +++++ .pre-commit-config.yaml | 2 +- tests_v400/__init__.py | 0 tests_v400/test_ArrayDimension.py | 19 ++ tests_v400/test_FMIExport.py | 24 ++ tests_v400/test_ModelicaSystem.py | 411 ++++++++++++++++++++++++++ tests_v400/test_ModelicaSystemCmd.py | 51 ++++ tests_v400/test_OMParser.py | 43 +++ tests_v400/test_OMSessionCmd.py | 17 ++ tests_v400/test_ZMQ.py | 70 +++++ tests_v400/test_docker.py | 32 ++ tests_v400/test_linearization.py | 102 +++++++ tests_v400/test_optimization.py | 67 +++++ tests_v400/test_typedParser.py | 53 ++++ 16 files changed, 1106 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/Test_v400.yml create mode 100644 .github/workflows/Test_v400_py310.yml create mode 100644 .github/workflows/Test_v4xx.yml create mode 100644 tests_v400/__init__.py create mode 100644 tests_v400/test_ArrayDimension.py create mode 100644 tests_v400/test_FMIExport.py create mode 100644 tests_v400/test_ModelicaSystem.py create mode 100644 tests_v400/test_ModelicaSystemCmd.py create mode 100644 tests_v400/test_OMParser.py create mode 100644 tests_v400/test_OMSessionCmd.py create mode 100644 tests_v400/test_ZMQ.py create mode 100644 tests_v400/test_docker.py create mode 100644 tests_v400/test_linearization.py create mode 100644 tests_v400/test_optimization.py create mode 100644 tests_v400/test_typedParser.py diff --git a/.github/workflows/Test_v400.yml b/.github/workflows/Test_v400.yml new file mode 100644 index 00000000..2407e060 --- /dev/null +++ b/.github/workflows/Test_v400.yml @@ -0,0 +1,73 @@ +name: Test-v4.0.0 + +on: + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + matrix: + # test for: + # * oldest supported version + # * latest available Python version + python-version: ['3.10', '3.14'] + # * Linux using ubuntu-latest + # * Windows using windows-latest + os: ['ubuntu-latest', 'windows-latest'] + # * OM stable - latest stable version + # * OM nightly - latest nightly build + omc-version: ['stable', 'nightly'] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip build setuptools wheel twine + pip install . pytest pytest-md pytest-emoji pre-commit + + - name: Set timezone + uses: szenius/set-timezone@v2.0 + with: + timezoneLinux: 'Europe/Berlin' + + - name: Run pre-commit linters + run: 'pre-commit run --all-files' + + - name: "Set up OpenModelica Compiler" + uses: OpenModelica/setup-openmodelica@v1.0.6 + with: + version: ${{ matrix.omc-version }} + packages: | + omc + libraries: | + 'Modelica 4.0.0' + - run: "omc --version" + + - name: Pull OpenModelica docker image + if: runner.os != 'Windows' + run: docker pull openmodelica/openmodelica:v1.25.0-minimal + + - name: Build wheel and sdist packages + run: python -m build --wheel --sdist --outdir dist + + - name: Check twine + run: python -m twine check dist/* + + - name: Run pytest + uses: pavelzw/pytest-action@v2 + with: + verbose: true + emoji: true + job-summary: true + custom-arguments: '-v ./tests_v400' + click-to-expand: true + report-title: 'Test Report' diff --git a/.github/workflows/Test_v400_py310.yml b/.github/workflows/Test_v400_py310.yml new file mode 100644 index 00000000..dbe635be --- /dev/null +++ b/.github/workflows/Test_v400_py310.yml @@ -0,0 +1,70 @@ +name: Test-v4.0.0-py310 + +on: + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + matrix: + # test for: + # * oldest supported version + python-version: ['3.10'] + # * Linux using ubuntu-latest + os: ['ubuntu-latest'] + # * OM stable - latest stable version + omc-version: ['stable'] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip build setuptools wheel twine + pip install . pytest pytest-md pytest-emoji pre-commit + + - name: Set timezone + uses: szenius/set-timezone@v2.0 + with: + timezoneLinux: 'Europe/Berlin' + + - name: Run pre-commit linters + run: 'pre-commit run --all-files' + + - name: "Set up OpenModelica Compiler" + uses: OpenModelica/setup-openmodelica@v1.0.6 + with: + version: ${{ matrix.omc-version }} + packages: | + omc + libraries: | + 'Modelica 4.0.0' + - run: "omc --version" + + - name: Pull OpenModelica docker image + if: runner.os != 'Windows' + run: docker pull openmodelica/openmodelica:v1.25.0-minimal + + - name: Build wheel and sdist packages + run: python -m build --wheel --sdist --outdir dist + + - name: Check twine + run: python -m twine check dist/* + + - name: Run pytest + uses: pavelzw/pytest-action@v2 + with: + verbose: true + emoji: true + job-summary: true + custom-arguments: '-v ./tests_v400' + click-to-expand: true + report-title: 'Test Report' diff --git a/.github/workflows/Test_v4xx.yml b/.github/workflows/Test_v4xx.yml new file mode 100644 index 00000000..cc662ff9 --- /dev/null +++ b/.github/workflows/Test_v4xx.yml @@ -0,0 +1,73 @@ +name: Test-v4.x.x + +on: + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + matrix: + # test for: + # * oldest supported version + # * latest available Python version + python-version: ['3.10', '3.14'] + # * Linux using ubuntu-latest + # * Windows using windows-latest + os: ['ubuntu-latest', 'windows-latest'] + # * OM stable - latest stable version + # * OM nightly - latest nightly build + omc-version: ['stable', 'nightly'] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip build setuptools wheel twine + pip install . pytest pytest-md pytest-emoji pre-commit + + - name: Set timezone + uses: szenius/set-timezone@v2.0 + with: + timezoneLinux: 'Europe/Berlin' + + - name: Run pre-commit linters + run: 'pre-commit run --all-files' + + - name: "Set up OpenModelica Compiler" + uses: OpenModelica/setup-openmodelica@v1.0.6 + with: + version: ${{ matrix.omc-version }} + packages: | + omc + libraries: | + 'Modelica 4.0.0' + - run: "omc --version" + + - name: Pull OpenModelica docker image + if: runner.os != 'Windows' + run: docker pull openmodelica/openmodelica:v1.25.0-minimal + + - name: Build wheel and sdist packages + run: python -m build --wheel --sdist --outdir dist + + - name: Check twine + run: python -m twine check dist/* + + - name: Run pytest + uses: pavelzw/pytest-action@v2 + with: + verbose: true + emoji: true + job-summary: true + custom-arguments: '-v ./tests' + click-to-expand: true + report-title: 'Test Report' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 484570b6..dd477775 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: hooks: - id: mypy args: [] - exclude: tests/ + exclude: 'test|test_v400' additional_dependencies: - pyparsing - types-psutil diff --git a/tests_v400/__init__.py b/tests_v400/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_v400/test_ArrayDimension.py b/tests_v400/test_ArrayDimension.py new file mode 100644 index 00000000..13b3c11b --- /dev/null +++ b/tests_v400/test_ArrayDimension.py @@ -0,0 +1,19 @@ +import OMPython + + +def test_ArrayDimension(tmp_path): + omc = OMPython.OMCSessionZMQ() + + omc.sendExpression(f'cd("{tmp_path.as_posix()}")') + + omc.sendExpression('loadString("model A Integer x[5+1,1+6]; end A;")') + omc.sendExpression("getErrorString()") + + result = omc.sendExpression("getComponents(A)") + assert result[0][-1] == (6, 7), "array dimension does not match" + + omc.sendExpression('loadString("model A Integer y = 5; Integer x[y+1,1+9]; end A;")') + omc.sendExpression("getErrorString()") + + result = omc.sendExpression("getComponents(A)") + assert result[-1][-1] == ('y+1', 10), "array dimension does not match" diff --git a/tests_v400/test_FMIExport.py b/tests_v400/test_FMIExport.py new file mode 100644 index 00000000..f47b87ae --- /dev/null +++ b/tests_v400/test_FMIExport.py @@ -0,0 +1,24 @@ +import OMPython +import shutil +import os + + +def test_CauerLowPassAnalog(): + mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", + lmodel=["Modelica"]) + tmp = mod.getWorkDirectory() + try: + fmu = mod.convertMo2Fmu(fileNamePrefix="CauerLowPassAnalog") + assert os.path.exists(fmu) + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +def test_DrumBoiler(): + mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel=["Modelica"]) + tmp = mod.getWorkDirectory() + try: + fmu = mod.convertMo2Fmu(fileNamePrefix="DrumBoiler") + assert os.path.exists(fmu) + finally: + shutil.rmtree(tmp, ignore_errors=True) diff --git a/tests_v400/test_ModelicaSystem.py b/tests_v400/test_ModelicaSystem.py new file mode 100644 index 00000000..c55e95fc --- /dev/null +++ b/tests_v400/test_ModelicaSystem.py @@ -0,0 +1,411 @@ +import OMPython +import os +import pathlib +import pytest +import tempfile +import numpy as np + + +@pytest.fixture +def model_firstorder(tmp_path): + mod = tmp_path / "M.mo" + mod.write_text("""model M + Real x(start = 1, fixed = true); + parameter Real a = -1; +equation + der(x) = x*a; +end M; +""") + return mod + + +def test_ModelicaSystem_loop(model_firstorder): + def worker(): + filePath = model_firstorder.as_posix() + m = OMPython.ModelicaSystem(filePath, "M") + m.simulate() + m.convertMo2Fmu(fmuType="me") + for _ in range(10): + worker() + + +def test_setParameters(): + omc = OMPython.OMCSessionZMQ() + model_path = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels/" + mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") + + # method 1 + mod.setParameters(pvals={"e": 1.234}) + mod.setParameters(pvals={"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 + mod.setParameters(pvals={"e": 21.3, "g": 0.12}) + 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 = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels/" + mod = OMPython.ModelicaSystem(fileName=model_path + "BouncingBall.mo", modelName="BouncingBall") + + # method 1 + mod.setSimulationOptions(simOptions={"stopTime": 1.234}) + mod.setSimulationOptions(simOptions={"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(simOptions={"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(fileName=model_relative, modelName="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() + m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir) + assert pathlib.Path(m.getWorkDirectory().resolve()) == tmpdir.resolve() + result_file = tmpdir / "a.mat" + assert not result_file.exists() + m.simulate(resultfile="a.mat") + assert result_file.is_file() + + +def test_getSolutions(model_firstorder): + filePath = model_firstorder.as_posix() + mod = OMPython.ModelicaSystem(filePath, "M") + x0 = 1 + a = -1 + tau = -1 / a + stopTime = 5*tau + mod.setSimulationOptions(simOptions={"stopTime": stopTime, "stepSize": 0.1, "tolerance": 1e-8}) + 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(fileName=model_file.as_posix(), modelName="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(simOptions={"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(KeyError): + mod.getContinuous("a") # a is a parameter + with pytest.raises(KeyError): + 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(KeyError): + mod.getContinuous("a") # a is a parameter + + with pytest.raises(OMPython.ModelicaSystemError): + mod.setSimulationOptions(simOptions={"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(fileName=model_file.as_posix(), modelName="M_input") + + mod.setSimulationOptions(simOptions={"stopTime": 1.0}) + + # 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(name={"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 + mod.setInputs(name={"u1": [(0.0, 0.0), (0.5, 2), (1.0, 0)]}) + 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(name={"u1": [(-0.5, 0.0), (1.0, 1)]}) + mod.simulate() + # unmatched stopTime + with pytest.raises(OMPython.ModelicaSystemError): + mod.setInputs(name={"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. + mod.setInputs(name={"u1": [(0.0, 0), (1.0, 1)], + "u2": [(0.0, 0), (0.25, 0.5), (0.5, 1.0), (1.0, 0)]}) + 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) diff --git a/tests_v400/test_ModelicaSystemCmd.py b/tests_v400/test_ModelicaSystemCmd.py new file mode 100644 index 00000000..3544a1bd --- /dev/null +++ b/tests_v400/test_ModelicaSystemCmd.py @@ -0,0 +1,51 @@ +import OMPython +import pytest + + +@pytest.fixture +def model_firstorder(tmp_path): + mod = tmp_path / "M.mo" + mod.write_text("""model M + Real x(start = 1, fixed = true); + parameter Real a = -1; +equation + der(x) = x*a; +end M; +""") + return mod + + +@pytest.fixture +def mscmd_firstorder(model_firstorder): + mod = OMPython.ModelicaSystem(fileName=model_firstorder.as_posix(), modelName="M") + mscmd = OMPython.ModelicaSystemCmd(runpath=mod.getWorkDirectory(), modelname=mod._model_name) + return mscmd + + +def test_simflags(mscmd_firstorder): + mscmd = mscmd_firstorder + + mscmd.args_set({ + "noEventEmit": None, + "override": {'b': 2} + }) + with pytest.deprecated_call(): + mscmd.args_set(args=mscmd.parse_simflags(simflags="-noEventEmit -noRestart -override=a=1,x=3")) + + assert mscmd.get_cmd() == [ + mscmd.get_exe().as_posix(), + '-noEventEmit', + '-noRestart', + '-override=a=1,b=2,x=3', + ] + + mscmd.args_set({ + "override": {'b': None}, + }) + + assert mscmd.get_cmd() == [ + mscmd.get_exe().as_posix(), + '-noEventEmit', + '-noRestart', + '-override=a=1,x=3', + ] diff --git a/tests_v400/test_OMParser.py b/tests_v400/test_OMParser.py new file mode 100644 index 00000000..875604e5 --- /dev/null +++ b/tests_v400/test_OMParser.py @@ -0,0 +1,43 @@ +from OMPython import OMParser + +typeCheck = OMParser.typeCheck + + +def test_newline_behaviour(): + pass + + +def test_boolean(): + assert typeCheck('TRUE') is True + assert typeCheck('True') is True + assert typeCheck('true') is True + assert typeCheck('FALSE') is False + assert typeCheck('False') is False + assert typeCheck('false') is False + + +def test_int(): + assert typeCheck('2') == 2 + assert type(typeCheck('1')) == int + assert type(typeCheck('123123123123123123232323')) == int + assert type(typeCheck('9223372036854775808')) == int + + +def test_float(): + assert type(typeCheck('1.2e3')) == float + + +# def test_dict(): +# assert type(typeCheck('{"a": "b"}')) == dict + + +def test_ident(): + assert typeCheck('blabla2') == "blabla2" + + +def test_str(): + pass + + +def test_UnStringable(): + pass diff --git a/tests_v400/test_OMSessionCmd.py b/tests_v400/test_OMSessionCmd.py new file mode 100644 index 00000000..1588fac8 --- /dev/null +++ b/tests_v400/test_OMSessionCmd.py @@ -0,0 +1,17 @@ +import OMPython + + +def test_isPackage(): + omczmq = OMPython.OMCSessionZMQ() + omccmd = OMPython.OMCSessionCmd(session=omczmq) + assert not omccmd.isPackage('Modelica') + + +def test_isPackage2(): + mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", + lmodel=["Modelica"]) + omccmd = OMPython.OMCSessionCmd(session=mod._getconn) + assert omccmd.isPackage('Modelica') + + +# TODO: add more checks ... diff --git a/tests_v400/test_ZMQ.py b/tests_v400/test_ZMQ.py new file mode 100644 index 00000000..30bf78e7 --- /dev/null +++ b/tests_v400/test_ZMQ.py @@ -0,0 +1,70 @@ +import OMPython +import pathlib +import os +import pytest + + +@pytest.fixture +def model_time_str(): + return """model M + Real r = time; +end M; +""" + + +@pytest.fixture +def om(tmp_path): + origDir = pathlib.Path.cwd() + os.chdir(tmp_path) + om = OMPython.OMCSessionZMQ() + os.chdir(origDir) + return om + + +def testHelloWorld(om): + assert om.sendExpression('"HelloWorld!"') == "HelloWorld!" + + +def test_Translate(om, model_time_str): + assert om.sendExpression(model_time_str) == ("M",) + assert om.sendExpression('translateModel(M)') is True + + +def test_Simulate(om, model_time_str): + assert om.sendExpression(f'loadString("{model_time_str}")') is True + om.sendExpression('res:=simulate(M, stopTime=2.0)') + assert om.sendExpression('res.resultFile') + + +def test_execute(om): + with pytest.deprecated_call(): + assert om.execute('"HelloWorld!"') == '"HelloWorld!"\n' + assert om.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + assert om.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' + + +def test_omcprocessport_execute(om): + port = om.omc_process.get_port() + omcp = OMPython.OMCProcessPort(omc_port=port) + + # run 1 + om1 = OMPython.OMCSessionZMQ(omc_process=omcp) + assert om1.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + + # run 2 + om2 = OMPython.OMCSessionZMQ(omc_process=omcp) + assert om2.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + + del om1 + del om2 + + +def test_omcprocessport_simulate(om, model_time_str): + port = om.omc_process.get_port() + omcp = OMPython.OMCProcessPort(omc_port=port) + + om = OMPython.OMCSessionZMQ(omc_process=omcp) + assert om.sendExpression(f'loadString("{model_time_str}")') is True + om.sendExpression('res:=simulate(M, stopTime=2.0)') + assert om.sendExpression('res.resultFile') != "" + del om diff --git a/tests_v400/test_docker.py b/tests_v400/test_docker.py new file mode 100644 index 00000000..8d68f11f --- /dev/null +++ b/tests_v400/test_docker.py @@ -0,0 +1,32 @@ +import sys +import pytest +import OMPython + +skip_on_windows = pytest.mark.skipif( + sys.platform.startswith("win"), + reason="OpenModelica Docker image is Linux-only; skipping on Windows.", +) + + +@skip_on_windows +def test_docker(): + omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + om = OMPython.OMCSessionZMQ(omc_process=omcp) + assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" + + omcpInner = OMPython.OMCProcessDockerContainer(dockerContainer=omcp.get_docker_container_id()) + omInner = OMPython.OMCSessionZMQ(omc_process=omcpInner) + assert omInner.sendExpression("getVersion()") == "OpenModelica 1.25.0" + + omcp2 = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal", port=11111) + om2 = OMPython.OMCSessionZMQ(omc_process=omcp2) + assert om2.sendExpression("getVersion()") == "OpenModelica 1.25.0" + + del omcp2 + del om2 + + del omcpInner + del omInner + + del omcp + del om diff --git a/tests_v400/test_linearization.py b/tests_v400/test_linearization.py new file mode 100644 index 00000000..bccbc40b --- /dev/null +++ b/tests_v400/test_linearization.py @@ -0,0 +1,102 @@ +import OMPython +import pytest +import numpy as np + + +@pytest.fixture +def model_linearTest(tmp_path): + mod = tmp_path / "M.mo" + mod.write_text(""" +model linearTest + Real x1(start=1); + Real x2(start=-2); + Real x3(start=3); + Real x4(start=-5); + parameter Real a=3,b=2,c=5,d=7,e=1,f=4; +equation + a*x1 = b*x2 -der(x1); + der(x2) + c*x3 + d*x1 = x4; + f*x4 - e*x3 - der(x3) = x1; + der(x4) = x1 + x2 + der(x3) + x4; +end linearTest; +""") + return mod + + +def test_example(model_linearTest): + mod = OMPython.ModelicaSystem(model_linearTest, "linearTest") + [A, B, C, D] = mod.linearize() + expected_matrixA = [[-3, 2, 0, 0], [-7, 0, -5, 1], [-1, 0, -1, 4], [0, 1, -1, 5]] + assert A == expected_matrixA, f"Matrix does not match the expected value. Got: {A}, Expected: {expected_matrixA}" + assert B == [], f"Matrix does not match the expected value. Got: {B}, Expected: {[]}" + assert C == [], f"Matrix does not match the expected value. Got: {C}, Expected: {[]}" + assert D == [], f"Matrix does not match the expected value. Got: {D}, Expected: {[]}" + assert mod.getLinearInputs() == [] + assert mod.getLinearOutputs() == [] + assert mod.getLinearStates() == ["x1", "x2", "x3", "x4"] + + +def test_getters(tmp_path): + model_file = tmp_path / "pendulum.mo" + model_file.write_text(""" +model Pendulum +Real phi(start=Modelica.Constants.pi, fixed=true); +Real omega(start=0, fixed=true); +input Real u1; +input Real u2; +output Real y1; +output Real y2; +parameter Real l = 1.2; +parameter Real g = 9.81; +equation +der(phi) = omega + u2; +der(omega) = -g/l * sin(phi); +y1 = y2 + 0.5*omega; +y2 = phi + u1; +end Pendulum; +""") + mod = OMPython.ModelicaSystem(fileName=model_file.as_posix(), modelName="Pendulum", lmodel=["Modelica"]) + + d = mod.getLinearizationOptions() + assert isinstance(d, dict) + assert "startTime" in d + assert "stopTime" in d + assert mod.getLinearizationOptions(["stopTime", "startTime"]) == [d["stopTime"], d["startTime"]] + mod.setLinearizationOptions(linearizationOptions={"stopTime": 0.02}) + assert mod.getLinearizationOptions("stopTime") == ["0.02"] + + mod.setInputs(name={"u1": 10, "u2": 0}) + [A, B, C, D] = mod.linearize() + param_g = float(mod.getParameters("g")[0]) + param_l = float(mod.getParameters("l")[0]) + assert mod.getLinearInputs() == ["u1", "u2"] + assert mod.getLinearStates() == ["omega", "phi"] + assert mod.getLinearOutputs() == ["y1", "y2"] + assert np.isclose(A, [[0, param_g/param_l], [1, 0]]).all() + assert np.isclose(B, [[0, 0], [0, 1]]).all() + assert np.isclose(C, [[0.5, 1], [0, 1]]).all() + assert np.isclose(D, [[1, 0], [1, 0]]).all() + + # test LinearizationResult + result = mod.linearize() + assert result[0] == A + assert result[1] == B + assert result[2] == C + assert result[3] == D + with pytest.raises(KeyError): + result[4] + + A2, B2, C2, D2 = result + assert A2 == A + assert B2 == B + assert C2 == C + assert D2 == D + + assert result.n == 2 + assert result.m == 2 + assert result.p == 2 + assert np.isclose(result.x0, [0, np.pi]).all() + assert np.isclose(result.u0, [10, 0]).all() + assert result.stateVars == ["omega", "phi"] + assert result.inputVars == ["u1", "u2"] + assert result.outputVars == ["y1", "y2"] diff --git a/tests_v400/test_optimization.py b/tests_v400/test_optimization.py new file mode 100644 index 00000000..b4164397 --- /dev/null +++ b/tests_v400/test_optimization.py @@ -0,0 +1,67 @@ +import OMPython +import numpy as np + + +def test_optimization_example(tmp_path): + model_file = tmp_path / "BangBang2021.mo" + model_file.write_text(""" +model BangBang2021 "Model to verify that optimization gives bang-bang optimal control" +parameter Real m = 1; +parameter Real p = 1 "needed for final constraints"; + +Real a; +Real v(start = 0, fixed = true); +Real pos(start = 0, fixed = true); +Real pow(min = -30, max = 30) = f * v annotation(isConstraint = true); + +input Real f(min = -10, max = 10); + +Real costPos(nominal = 1) = -pos "minimize -pos(tf)" annotation(isMayer=true); + +Real conSpeed(min = 0, max = 0) = p * v " 0<= p*v(tf) <=0" annotation(isFinalConstraint = true); + +equation + +der(pos) = v; +der(v) = a; +f = m * a; + +annotation(experiment(StartTime = 0, StopTime = 1, Tolerance = 1e-07, Interval = 0.01), +__OpenModelica_simulationFlags(s="optimization", optimizerNP="1"), +__OpenModelica_commandLineOptions="+g=Optimica"); + +end BangBang2021; +""") + + mod = OMPython.ModelicaSystem(fileName=model_file.as_posix(), modelName="BangBang2021") + + mod.setOptimizationOptions(optimizationOptions={"numberOfIntervals": 16, + "stopTime": 1, + "stepSize": 0.001, + "tolerance": 1e-8}) + + # test the getter + assert mod.getOptimizationOptions()["stopTime"] == "1" + assert mod.getOptimizationOptions("stopTime") == ["1"] + assert mod.getOptimizationOptions(["tolerance", "stopTime"]) == ["1e-08", "1"] + + r = mod.optimize() + # it is necessary to specify resultfile, otherwise it wouldn't find it. + time, f, v = mod.getSolutions(["time", "f", "v"], resultfile=r["resultFile"]) + assert np.isclose(f[0], 10) + assert np.isclose(f[-1], -10) + + def f_fcn(time, v): + if time < 0.3: + return 10 + if time <= 0.5: + return 30 / v + if time < 0.7: + return -30 / v + return -10 + f_expected = [f_fcn(t, v) for t, v in zip(time, v)] + + # The sharp edge at time=0.5 probably won't match, let's leave that out. + matches = np.isclose(f, f_expected, 1e-3) + assert matches[:498].all() + assert matches[502:].all() diff --git a/tests_v400/test_typedParser.py b/tests_v400/test_typedParser.py new file mode 100644 index 00000000..60daedec --- /dev/null +++ b/tests_v400/test_typedParser.py @@ -0,0 +1,53 @@ +from OMPython import OMTypedParser + +typeCheck = OMTypedParser.parseString + + +def test_newline_behaviour(): + pass + + +def test_boolean(): + assert typeCheck('true') is True + assert typeCheck('false') is False + + +def test_int(): + assert typeCheck('2') == 2 + assert type(typeCheck('1')) == int + assert type(typeCheck('123123123123123123232323')) == int + assert type(typeCheck('9223372036854775808')) == int + + +def test_float(): + assert type(typeCheck('1.2e3')) == float + + +def test_ident(): + assert typeCheck('blabla2') == "blabla2" + + +def test_empty(): + assert typeCheck('') is None + + +def test_str(): + pass + + +def test_UnStringable(): + pass + + +def test_everything(): + # this test used to be in OMTypedParser.py's main() + testdata = """ + (1.0,{{1,true,3},{"4\\" +",5.9,6,NONE ( )},record ABC + startTime = ErrorLevel.warning, + 'stop*Time' = SOME(1.0) +end ABC;}) + """ + expected = (1.0, ((1, True, 3), ('4"\n', 5.9, 6, None), {"'stop*Time'": 1.0, 'startTime': 'ErrorLevel.warning'})) + results = typeCheck(testdata) + assert results == expected From 44e071329102cbb2447d242af40e979119fc4531 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 8 Feb 2026 20:52:18 +0100 Subject: [PATCH 4/5] (E001) update tests (v4.x.x) [test_*] reorder imports [tests_ModelicaDoE*] fix pylint hint * use .items() [tests_*] use OMSessionABC.get_version() [test_ModelicaSystemCmd] use get_model_name() instead of access to private variable _model_name [test_ModelicaSystemOMC] read file using utf-8 encoding / linter fix [test_ModelicaSystemRunner] update test case * ModelicaSystemRunner & OMCPath * ModelicaSystemRunner & OMPathRunnerLocal * ModelicaSystemRunner & OMPathRunnerBash * ModelicaSystemRunner & OMPathRunnerBash using docker * ModelicaSystemRunner & OMPathRunnerBash using WSL (not tested!) [test_OMCPath] update test case * OMCPath & OMCSessionZMQ * OMCPath & OMCSessionLocal * OMCPath & OMCSessionDocker * OMCPath & OMCSessionWSL (not tested!) * OMPathLocal & OMCSessionRunner * OMPathBash & OMCSessionRunner * OMPathBash & OMCSessionRunner in docker * OMPathBash & OMCSessionRunner in WSL (not tested!) add workflow to run unittests in ./tests [test_OMParser] use only the public interface => om_parser_basic() [test_OMTypedParser] rename file / use om_parser_typed() update tests - do NOT run test_FMIRegression.py reason: * it is only a test for OMC / not OMPython specific * furthermore, it is run automatically via cron job (= FMITest) [test_ModelExecutionCmd] rename from test_ModelicaSystemCmd --- OMPython/__init__.py | 2 + tests/test_FMIExport.py | 2 +- ...SystemCmd.py => test_ModelExecutionCmd.py} | 2 +- tests/test_ModelicaDoEOMC.py | 6 +- tests/test_ModelicaDoERunner.py | 6 +- tests/test_ModelicaSystemOMC.py | 2 +- tests/test_ModelicaSystemRunner.py | 176 +++++++++++++++++- tests/test_OMCPath.py | 96 +++++++--- tests/test_OMParser.py | 53 ++++-- tests/test_OMTypedParser.py | 65 +++++++ tests/test_ZMQ.py | 1 + tests/test_docker.py | 2 + tests/test_typedParser.py | 53 ------ 13 files changed, 365 insertions(+), 101 deletions(-) rename tests/{test_ModelicaSystemCmd.py => test_ModelExecutionCmd.py} (97%) create mode 100644 tests/test_OMTypedParser.py delete mode 100644 tests/test_typedParser.py diff --git a/OMPython/__init__.py b/OMPython/__init__.py index c12f8524..22c88137 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -30,6 +30,7 @@ OMPathABC, OMCPath, + OMSessionABC, OMSessionRunner, OMCSessionABC, @@ -77,6 +78,7 @@ 'OMPathABC', 'OMCPath', + 'OMSessionABC', 'OMSessionRunner', 'OMCSessionABC', diff --git a/tests/test_FMIExport.py b/tests/test_FMIExport.py index c7ab038a..65ac2766 100644 --- a/tests/test_FMIExport.py +++ b/tests/test_FMIExport.py @@ -1,6 +1,6 @@ -import shutil import os import pathlib +import shutil import OMPython diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelExecutionCmd.py similarity index 97% rename from tests/test_ModelicaSystemCmd.py rename to tests/test_ModelExecutionCmd.py index 3d35376b..db5aadeb 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelExecutionCmd.py @@ -29,7 +29,7 @@ def mscmd_firstorder(model_firstorder): cmd_local=mod.get_session().model_execution_local, cmd_windows=mod.get_session().model_execution_windows, cmd_prefix=mod.get_session().model_execution_prefix(cwd=mod.getWorkDirectory()), - model_name=mod._model_name, + model_name=mod.get_model_name(), ) return mscmd diff --git a/tests/test_ModelicaDoEOMC.py b/tests/test_ModelicaDoEOMC.py index 143932fc..9d6afc63 100644 --- a/tests/test_ModelicaDoEOMC.py +++ b/tests/test_ModelicaDoEOMC.py @@ -159,6 +159,6 @@ def _run_ModelicaDoEOMC(doe_mod): f"y[{row['p']}]": float(row['b']), } - for var in var_dict: - assert var in sol['data'] - assert np.isclose(sol['data'][var][-1], var_dict[var]) + for key, val in var_dict.items(): + assert key in sol['data'] + assert np.isclose(sol['data'][key][-1], val) diff --git a/tests/test_ModelicaDoERunner.py b/tests/test_ModelicaDoERunner.py index 2d41315f..e29e7e05 100644 --- a/tests/test_ModelicaDoERunner.py +++ b/tests/test_ModelicaDoERunner.py @@ -153,6 +153,6 @@ def _check_runner_result(mod, doe_mod): 'b': float(row['b']), } - for var in var_dict: - assert var in sol['data'] - assert np.isclose(sol['data'][var][-1], var_dict[var]) + for key, val in var_dict.items(): + assert key in sol['data'] + assert np.isclose(sol['data'][key][-1], val) diff --git a/tests/test_ModelicaSystemOMC.py b/tests/test_ModelicaSystemOMC.py index 8dd17ef0..c63b92e1 100644 --- a/tests/test_ModelicaSystemOMC.py +++ b/tests/test_ModelicaSystemOMC.py @@ -495,7 +495,7 @@ def test_simulate_inputs(tmp_path): } mod.setInputs(**inputs) csv_file = mod._createCSVData() - assert pathlib.Path(csv_file).read_text() == """time,u1,u2,end + assert pathlib.Path(csv_file).read_text(encoding='utf-8') == """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 diff --git a/tests/test_ModelicaSystemRunner.py b/tests/test_ModelicaSystemRunner.py index ec9d734d..a207368c 100644 --- a/tests/test_ModelicaSystemRunner.py +++ b/tests/test_ModelicaSystemRunner.py @@ -1,9 +1,22 @@ +import sys + import numpy as np import pytest import OMPython +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 """ @@ -37,7 +50,7 @@ def param(): } -def test_runner(model_firstorder, param): +def test_ModelicaSystemRunner_OMC(model_firstorder, param): # create a model using ModelicaSystem mod = OMPython.ModelicaSystemOMC() mod.model( @@ -71,6 +84,167 @@ def test_runner(model_firstorder, param): _check_result(mod=mod, resultfile=resultfile_modr, param=param) +def test_ModelicaSystemRunner_local(model_firstorder, param): + # create a model using ModelicaSystem + mod = OMPython.ModelicaSystemOMC() + mod.model( + model_file=model_firstorder, + model_name="M", + ) + + resultfile_mod = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_mod.mat" + _run_simulation(mod=mod, resultfile=resultfile_mod, param=param) + + # run the model using only the runner class + omcs = OMPython.OMSessionRunner( + version=mod.get_session().get_version(), + ompath_runner=OMPython.OMPathRunnerLocal, + ) + modr = OMPython.ModelicaSystemRunner( + session=omcs, + work_directory=mod.getWorkDirectory(), + ) + modr.setup( + model_name="M", + ) + + resultfile_modr = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_modr.mat" + _run_simulation(mod=modr, resultfile=resultfile_modr, param=param) + + # cannot check the content as runner does not have the capability to open a result file + assert resultfile_mod.size() == resultfile_modr.size() + + # check results + _check_result(mod=mod, resultfile=resultfile_mod, param=param) + _check_result(mod=mod, resultfile=resultfile_modr, param=param) + + +@skip_on_windows +def test_ModelicaSystemRunner_bash(model_firstorder, param): + # create a model using ModelicaSystem + mod = OMPython.ModelicaSystemOMC() + mod.model( + model_file=model_firstorder, + model_name="M", + ) + + resultfile_mod = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_mod.mat" + _run_simulation(mod=mod, resultfile=resultfile_mod, param=param) + + # run the model using only the runner class + omcsr = OMPython.OMSessionRunner( + version=mod.get_session().get_version(), + ompath_runner=OMPython.OMPathRunnerBash, + ) + modr = OMPython.ModelicaSystemRunner( + session=omcsr, + work_directory=mod.getWorkDirectory(), + ) + modr.setup( + model_name="M", + ) + + resultfile_modr = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_modr.mat" + _run_simulation(mod=modr, resultfile=resultfile_modr, param=param) + + # cannot check the content as runner does not have the capability to open a result file + assert resultfile_mod.size() == resultfile_modr.size() + + # check results + _check_result(mod=mod, resultfile=resultfile_mod, param=param) + _check_result(mod=mod, resultfile=resultfile_modr, param=param) + + +@skip_on_windows +@skip_python_older_312 +def test_ModelicaSystemRunner_bash_docker(model_firstorder, param): + omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + omversion = omcs.sendExpression("getVersion()") + assert isinstance(omversion, str) and omversion.startswith("OpenModelica") + + # create a model using ModelicaSystem + mod = OMPython.ModelicaSystemOMC( + session=omcs, + ) + mod.model( + model_file=model_firstorder, + model_name="M", + ) + + resultfile_mod = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_mod.mat" + _run_simulation(mod=mod, resultfile=resultfile_mod, param=param) + + # run the model using only the runner class + omcsr = OMPython.OMSessionRunner( + version=mod.get_session().get_version(), + cmd_prefix=omcs.model_execution_prefix(cwd=mod.getWorkDirectory()), + ompath_runner=OMPython.OMPathRunnerBash, + model_execution_local=False, + ) + modr = OMPython.ModelicaSystemRunner( + session=omcsr, + work_directory=mod.getWorkDirectory(), + ) + modr.setup( + model_name="M", + ) + + resultfile_modr = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_modr.mat" + _run_simulation(mod=modr, resultfile=resultfile_modr, param=param) + + # cannot check the content as runner does not have the capability to open a result file + assert resultfile_mod.size() == resultfile_modr.size() + + # check results + _check_result(mod=mod, resultfile=resultfile_mod, param=param) + _check_result(mod=mod, resultfile=resultfile_modr, param=param) + + +@pytest.mark.skip(reason="Not able to run WSL on github") +@skip_python_older_312 +def test_ModelicaSystemDoE_WSL(tmp_path, model_doe, param_doe): + omcs = OMPython.OMCSessionWSL() + omversion = omcs.sendExpression("getVersion()") + assert isinstance(omversion, str) and omversion.startswith("OpenModelica") + + # create a model using ModelicaSystem + mod = OMPython.ModelicaSystemOMC( + session=omcs, + ) + mod.model( + model_file=model_firstorder, + model_name="M", + ) + + resultfile_mod = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_mod.mat" + _run_simulation(mod=mod, resultfile=resultfile_mod, param=param) + + # run the model using only the runner class + omcsr = OMPython.OMSessionRunner( + version=mod.get_session().get_version(), + cmd_prefix=omcs.model_execution_prefix(cwd=mod.getWorkDirectory()), + ompath_runner=OMPython.OMPathRunnerBash, + model_execution_local=False, + ) + modr = OMPython.ModelicaSystemRunner( + session=omcsr, + work_directory=mod.getWorkDirectory(), + ) + modr.setup( + model_name="M", + ) + + resultfile_modr = mod.getWorkDirectory() / f"{mod.get_model_name()}_res_modr.mat" + _run_simulation(mod=modr, resultfile=resultfile_modr, param=param) + + # cannot check the content as runner does not have the capability to open a result file + assert resultfile_mod.size() == resultfile_modr.size() + + # check results + _check_result(mod=mod, resultfile=resultfile_mod, param=param) + _check_result(mod=mod, resultfile=resultfile_modr, param=param) + + def _run_simulation(mod, resultfile, param): simOptions = {"stopTime": param['stopTime'], "stepSize": 0.1, "tolerance": 1e-8} mod.setSimulationOptions(**simOptions) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index df01b86a..e15c75ff 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -15,42 +15,98 @@ ) -def test_OMCPath_OMCProcessLocal(): - omcs = OMPython.OMCSessionLocal() +# TODO: based on compatibility layer +def test_OMCPath_OMCSessionZMQ(): + om = OMPython.OMCSessionZMQ() - _run_OMCPath_checks(omcs) + _run_OMPath_checks(om) + _run_OMPath_write_file(om) - del omcs + +def test_OMCPath_OMCSessionLocal(): + oms = OMPython.OMCSessionLocal() + + _run_OMPath_checks(oms) + _run_OMPath_write_file(oms) @skip_on_windows @skip_python_older_312 -def test_OMCPath_OMCProcessDocker(): +def test_OMCPath_OMCSessionDocker(): omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") omversion = omcs.sendExpression("getVersion()") assert isinstance(omversion, str) and omversion.startswith("OpenModelica") - _run_OMCPath_checks(omcs) - - del omcs + _run_OMPath_checks(omcs) + _run_OMPath_write_file(omcs) @pytest.mark.skip(reason="Not able to run WSL on github") @skip_python_older_312 -def test_OMCPath_OMCProcessWSL(): - omcs = OMPython.OMCSessionWSL( +def test_OMCPath_OMCSessionWSL(): + oms = OMPython.OMCSessionWSL( wsl_omc='omc', wsl_user='omc', timeout=30.0, ) - _run_OMCPath_checks(omcs) + _run_OMPath_checks(oms) + _run_OMPath_write_file(oms) + + +@skip_python_older_312 +def test_OMPathLocal_OMSessionRunner(): + oms = OMPython.OMSessionRunner() + + _run_OMPath_checks(oms) + _run_OMPath_write_file(oms) + + +@skip_on_windows +@skip_python_older_312 +def test_OMPathBash_OMSessionRunner(): + oms = OMPython.OMSessionRunner( + ompath_runner=OMPython.OMPathRunnerBash, + ) + + _run_OMPath_checks(oms) + _run_OMPath_write_file(oms) + + +@skip_on_windows +@skip_python_older_312 +def test_OMPathBash_OMSessionRunner_Docker(): + oms_docker = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + omversion = oms_docker.sendExpression("getVersion()") + assert isinstance(omversion, str) and omversion.startswith("OpenModelica") + + oms = OMPython.OMSessionRunner( + cmd_prefix=oms_docker.get_cmd_prefix(), + ompath_runner=OMPython.OMPathRunnerBash, + ) + + _run_OMPath_checks(oms) + _run_OMPath_write_file(oms) - del omcs +@pytest.mark.skip(reason="Not able to run WSL on github") +@skip_python_older_312 +def test_OMPathBash_OMSessionRunner_WSL(): + oms_docker = OMPython.OMCSessionWSL() + omversion = oms_docker.sendExpression("getVersion()") + assert isinstance(omversion, str) and omversion.startswith("OpenModelica") + + oms = OMPython.OMSessionRunner( + cmd_prefix=oms_docker.get_cmd_prefix(), + ompath_runner=OMPython.OMPathRunnerBash, + ) + + _run_OMPath_checks(oms) + _run_OMPath_write_file(oms) -def _run_OMCPath_checks(omcs: OMPython.OMCSessionABC): - p1 = omcs.omcpath_tempdir() + +def _run_OMPath_checks(om: OMPython.OMSessionABC): + p1 = om.omcpath_tempdir() p2 = p1 / 'test' p2.mkdir() assert p2.is_dir() @@ -59,8 +115,8 @@ def _run_OMCPath_checks(omcs: OMPython.OMCSessionABC): assert p3.write_text('test') assert p3.is_file() assert p3.size() > 0 - p3 = p3.resolve().absolute() - assert str(p3) == str((p2 / 'test.txt').resolve().absolute()) + p3 = p3.resolve() + assert str(p3) == str((p2 / 'test.txt').resolve()) assert p3.read_text() == "test" assert p3.is_file() assert p3.parent.is_dir() @@ -68,15 +124,11 @@ def _run_OMCPath_checks(omcs: OMPython.OMCSessionABC): assert p3.is_file() is False -def test_OMCPath_write_file(tmpdir): - omcs = OMPython.OMCSessionLocal() - +def _run_OMPath_write_file(om: OMPython.OMSessionABC): data = "abc # \\t # \" # \\n # xyz" - p1 = omcs.omcpath_tempdir() + p1 = om.omcpath_tempdir() p2 = p1 / 'test.txt' p2.write_text(data=data) assert data == p2.read_text() - - del omcs diff --git a/tests/test_OMParser.py b/tests/test_OMParser.py index 875604e5..9dca784d 100644 --- a/tests/test_OMParser.py +++ b/tests/test_OMParser.py @@ -1,6 +1,6 @@ -from OMPython import OMParser +import OMPython -typeCheck = OMParser.typeCheck +parser = OMPython.OMParser.om_parser_basic def test_newline_behaviour(): @@ -8,31 +8,38 @@ def test_newline_behaviour(): def test_boolean(): - assert typeCheck('TRUE') is True - assert typeCheck('True') is True - assert typeCheck('true') is True - assert typeCheck('FALSE') is False - assert typeCheck('False') is False - assert typeCheck('false') is False + assert parser('TRUE') is True + assert parser('True') is True + assert parser('true') is True + assert parser('FALSE') is False + assert parser('False') is False + assert parser('false') is False def test_int(): - assert typeCheck('2') == 2 - assert type(typeCheck('1')) == int - assert type(typeCheck('123123123123123123232323')) == int - assert type(typeCheck('9223372036854775808')) == int + assert parser('2') == 2 + assert type(parser('1')) == int + assert type(parser('123123123123123123232323')) == int + assert type(parser('9223372036854775808')) == int def test_float(): - assert type(typeCheck('1.2e3')) == float + assert type(parser('1.2e3')) == float -# def test_dict(): -# assert type(typeCheck('{"a": "b"}')) == dict +def test_dict(): + # TODO: why does it fail? + # assert type(parser('{"a": "b"}')) == dict + pass def test_ident(): - assert typeCheck('blabla2') == "blabla2" + assert parser('blabla2') == "blabla2" + + +def test_empty(): + # TODO: this differs from OMTypedParser + assert parser('') == {} def test_str(): @@ -41,3 +48,17 @@ def test_str(): def test_UnStringable(): pass + + +# def test_everything(): +# # this test used to be in OMTypedParser.py's main() +# testdata = """ +# (1.0,{{1,true,3},{"4\\" +# ",5.9,6,NONE ( )},record ABC +# startTime = ErrorLevel.warning, +# 'stop*Time' = SOME(1.0) +# end ABC;}) +# """ +# expected = (1.0, ((1, True, 3), ('4"\n', 5.9, 6, None), {"'stop*Time'": 1.0, 'startTime': 'ErrorLevel.warning'})) +# results = parser(testdata) +# assert results == expected diff --git a/tests/test_OMTypedParser.py b/tests/test_OMTypedParser.py new file mode 100644 index 00000000..94a14210 --- /dev/null +++ b/tests/test_OMTypedParser.py @@ -0,0 +1,65 @@ +import OMPython + +parser = OMPython.OMTypedParser.om_parser_typed + + +def test_newline_behaviour(): + pass + + +def test_boolean(): + # TODO: why does these fail? + # assert parser('TRUE') is True + # assert parser('True') is True + assert parser('true') is True + # TODO: why does these fail? + # assert parser('FALSE') is False + # assert parser('False') is False + assert parser('false') is False + + +def test_int(): + assert parser('2') == 2 + assert type(parser('1')) == int + assert type(parser('123123123123123123232323')) == int + assert type(parser('9223372036854775808')) == int + + +def test_float(): + assert type(parser('1.2e3')) == float + + +def test_dict(): + # TODO: why does it fail? + # assert type(parser('{"a": "b"}')) == dict + pass + + +def test_ident(): + assert parser('blabla2') == "blabla2" + + +def test_empty(): + assert parser('') is None + + +def test_str(): + pass + + +def test_UnStringable(): + pass + + +def test_everything(): + # this test used to be in OMTypedParser.py's main() + testdata = """ + (1.0,{{1,true,3},{"4\\" +",5.9,6,NONE ( )},record ABC + startTime = ErrorLevel.warning, + 'stop*Time' = SOME(1.0) +end ABC;}) + """ + expected = (1.0, ((1, True, 3), ('4"\n', 5.9, 6, None), {"'stop*Time'": 1.0, 'startTime': 'ErrorLevel.warning'})) + results = parser(testdata) + assert results == expected diff --git a/tests/test_ZMQ.py b/tests/test_ZMQ.py index 1302a79d..89a8387b 100644 --- a/tests/test_ZMQ.py +++ b/tests/test_ZMQ.py @@ -1,5 +1,6 @@ import pathlib import os + import pytest import OMPython diff --git a/tests/test_docker.py b/tests/test_docker.py index a1acfbe1..50d2763a 100644 --- a/tests/test_docker.py +++ b/tests/test_docker.py @@ -1,5 +1,7 @@ import sys + import pytest + import OMPython skip_on_windows = pytest.mark.skipif( diff --git a/tests/test_typedParser.py b/tests/test_typedParser.py deleted file mode 100644 index 8e74a556..00000000 --- a/tests/test_typedParser.py +++ /dev/null @@ -1,53 +0,0 @@ -from OMPython import OMTypedParser - -typeCheck = OMTypedParser.om_parser_typed - - -def test_newline_behaviour(): - pass - - -def test_boolean(): - assert typeCheck('true') is True - assert typeCheck('false') is False - - -def test_int(): - assert typeCheck('2') == 2 - assert type(typeCheck('1')) == int - assert type(typeCheck('123123123123123123232323')) == int - assert type(typeCheck('9223372036854775808')) == int - - -def test_float(): - assert type(typeCheck('1.2e3')) == float - - -def test_ident(): - assert typeCheck('blabla2') == "blabla2" - - -def test_empty(): - assert typeCheck('') is None - - -def test_str(): - pass - - -def test_UnStringable(): - pass - - -def test_everything(): - # this test used to be in OMTypedParser.py's main() - testdata = """ - (1.0,{{1,true,3},{"4\\" -",5.9,6,NONE ( )},record ABC - startTime = ErrorLevel.warning, - 'stop*Time' = SOME(1.0) -end ABC;}) - """ - expected = (1.0, ((1, True, 3), ('4"\n', 5.9, 6, None), {"'stop*Time'": 1.0, 'startTime': 'ErrorLevel.warning'})) - results = typeCheck(testdata) - assert results == expected From b8cc0e046573a36b43bf05dae1c38f8a2ae5e21a Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 13 Feb 2026 21:16:47 +0100 Subject: [PATCH 5/5] (E002) prepare restructure [ModelicaSystemCmd] add missing docstring [OMCSession] spelling fixes [OMCSessionCmd] add warning about depreciated class [OMCSessionABC] remove duplicated code; see OMSessionABC [OMSessionRunnerABC] define class [OMCSessionZMQ] call super()__init__() [OMCPath] fix forward dependency on OMCSessionLocal [OMSessionException] rename from OMCSessionException [__init__] fix imports --- OMPython/ModelicaSystem.py | 8 +- OMPython/OMCSession.py | 212 ++++++++++++++++++++----------------- OMPython/__init__.py | 6 +- 3 files changed, 120 insertions(+), 106 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 03fd060b..ca752f09 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -25,7 +25,7 @@ ModelExecutionData, ModelExecutionException, - OMCSessionException, + OMSessionException, OMCSessionLocal, OMPathABC, @@ -1685,7 +1685,7 @@ def sendExpression(self, expr: str, parsed: bool = True) -> Any: """ try: retval = self._session.sendExpression(expr=expr, parsed=parsed) - except OMCSessionException as ex: + except OMSessionException as ex: raise ModelicaSystemError(f"Error executing {repr(expr)}: {ex}") from ex logger.debug(f"Result of executing {repr(expr)}: {textwrap.shorten(repr(retval), width=100)}") @@ -2826,7 +2826,9 @@ def _prepare_structure_parameters( class ModelicaSystemCmd(ModelExecutionCmd): - # TODO: docstring + """ + Compatibility class; in the new version it is renamed as MOdelExecutionCmd. + """ def __init__( self, diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 731005f1..167cb267 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -59,20 +59,33 @@ def wait(self, timeout): pass -class OMCSessionException(Exception): +class OMSessionException(Exception): """ Exception which is raised by any OMC* class. """ +class OMCSessionException(OMSessionException): + """ + Just a compatibility layer ... + """ + + class OMCSessionCmd: """ Implementation of Open Modelica Compiler API functions. Depreciated! """ def __init__(self, session: OMSessionABC, readonly: bool = False): + warnings.warn( + message="The class OMCSessionCMD is depreciated and will be removed in future versions; " + "please use OMCSession*.sendExpression(...) instead!", + category=DeprecationWarning, + stacklevel=2, + ) + if not isinstance(session, OMSessionABC): - raise OMCSessionException("Invalid OMC process definition!") + raise OMSessionException("Invalid OMC process definition!") self._session = session self._readonly = readonly self._omc_cache: dict[tuple[str, bool], Any] = {} @@ -84,7 +97,7 @@ def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: bool = Tr elif isinstance(opt, list): expression = f"{question}({','.join([str(x) for x in opt])})" else: - raise OMCSessionException(f"Invalid definition of options for {repr(question)}: {repr(opt)}") + raise OMSessionException(f"Invalid definition of options for {repr(question)}: {repr(opt)}") p = (expression, parsed) @@ -95,8 +108,8 @@ def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: bool = Tr try: res = self._session.sendExpression(expression, parsed=parsed) - except OMCSessionException as ex: - raise OMCSessionException(f"OMC _ask() failed: {expression} (parsed={parsed})") from ex + except OMSessionException as ex: + raise OMSessionException(f"OMC _ask() failed: {expression} (parsed={parsed})") from ex # save response self._omc_cache[p] = res @@ -411,7 +424,7 @@ def is_file(self) -> bool: """ retval = self.get_session().sendExpression(expr=f'regularFileExists("{self.as_posix()}")') if not isinstance(retval, bool): - raise OMCSessionException(f"Invalid return value for is_file(): {retval} - expect bool") + raise OMSessionException(f"Invalid return value for is_file(): {retval} - expect bool") return retval def is_dir(self) -> bool: @@ -420,14 +433,14 @@ def is_dir(self) -> bool: """ retval = self.get_session().sendExpression(expr=f'directoryExists("{self.as_posix()}")') if not isinstance(retval, bool): - raise OMCSessionException(f"Invalid return value for is_dir(): {retval} - expect bool") + raise OMSessionException(f"Invalid return value for is_dir(): {retval} - expect bool") return retval def is_absolute(self) -> bool: """ Check if the path is an absolute path. Special handling to differentiate Windows and Posix definitions. """ - if isinstance(self._session, OMCSessionLocal) and platform.system() == 'Windows': + if self._session.model_execution_windows and self._session.model_execution_local: return pathlib.PureWindowsPath(self.as_posix()).is_absolute() return pathlib.PurePosixPath(self.as_posix()).is_absolute() @@ -437,7 +450,7 @@ def read_text(self) -> str: """ retval = self.get_session().sendExpression(expr=f'readFile("{self.as_posix()}")') if not isinstance(retval, str): - raise OMCSessionException(f"Invalid return value for read_text(): {retval} - expect str") + raise OMSessionException(f"Invalid return value for read_text(): {retval} - expect str") return retval def write_text(self, data: str) -> int: @@ -464,7 +477,7 @@ def mkdir(self, parents: bool = True, exist_ok: bool = False) -> None: raise FileExistsError(f"Directory {self.as_posix()} already exists!") if not self._session.sendExpression(expr=f'mkdir("{self.as_posix()}")'): - raise OMCSessionException(f"Error on directory creation for {self.as_posix()}!") + raise OMSessionException(f"Error on directory creation for {self.as_posix()}!") def cwd(self) -> OMPathABC: """ @@ -486,7 +499,7 @@ def resolve(self, strict: bool = False) -> OMPathABC: Resolve the path to an absolute path. This is done based on available OMC functions. """ if strict and not (self.is_file() or self.is_dir()): - raise OMCSessionException(f"Path {self.as_posix()} does not exist!") + raise OMSessionException(f"Path {self.as_posix()} does not exist!") if self.is_file(): pathstr_resolved = self._omc_resolve(self.parent.as_posix()) @@ -495,10 +508,10 @@ def resolve(self, strict: bool = False) -> OMPathABC: pathstr_resolved = self._omc_resolve(self.as_posix()) omcpath_resolved = self._session.omcpath(pathstr_resolved) else: - raise OMCSessionException(f"Path {self.as_posix()} is neither a file nor a directory!") + raise OMSessionException(f"Path {self.as_posix()} is neither a file nor a directory!") if not omcpath_resolved.is_file() and not omcpath_resolved.is_dir(): - raise OMCSessionException(f"OMCPath resolve failed for {self.as_posix()} - path does not exist!") + raise OMSessionException(f"OMCPath resolve failed for {self.as_posix()} - path does not exist!") return omcpath_resolved @@ -514,12 +527,12 @@ def _omc_resolve(self, pathstr: str) -> str: try: retval = self.get_session().sendExpression(expr=expr, parsed=False) if not isinstance(retval, str): - raise OMCSessionException(f"Invalid return value for _omc_resolve(): {retval} - expect str") + raise OMSessionException(f"Invalid return value for _omc_resolve(): {retval} - expect str") result_parts = retval.split('\n') pathstr_resolved = result_parts[1] pathstr_resolved = pathstr_resolved[1:-1] # remove quotes - except OMCSessionException as ex: - raise OMCSessionException(f"OMCPath resolve failed for {pathstr}!") from ex + except OMSessionException as ex: + raise OMSessionException(f"OMCPath resolve failed for {pathstr}!") from ex return pathstr_resolved @@ -528,13 +541,13 @@ def size(self) -> int: Get the size of the file in bytes - this is an extra function and the best we can do using OMC. """ if not self.is_file(): - raise OMCSessionException(f"Path {self.as_posix()} is not a file!") + raise OMSessionException(f"Path {self.as_posix()} is not a file!") res = self._session.sendExpression(expr=f'stat("{self.as_posix()}")') if res[0]: return int(res[1]) - raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!") + raise OMSessionException(f"Error reading file size for path {self.as_posix()}!") class OMPathRunnerABC(OMPathABC, metaclass=abc.ABCMeta): """ @@ -618,10 +631,10 @@ def resolve(self, strict: bool = False) -> OMPathABC: def size(self) -> int: """ - Get the size of the file in bytes - implementation baseon on pathlib.Path. + Get the size of the file in bytes - implementation based on pathlib.Path. """ if not self.is_file(): - raise OMCSessionException(f"Path {self.as_posix()} is not a file!") + raise OMSessionException(f"Path {self.as_posix()} is not a file!") path = self._path() return path.stat().st_size @@ -729,7 +742,7 @@ def mkdir(self, parents: bool = True, exist_ok: bool = False) -> None: try: subprocess.run(cmdl, check=True) except subprocess.CalledProcessError as exc: - raise OMCSessionException(f"Error on directory creation for {self.as_posix()}!") from exc + raise OMSessionException(f"Error on directory creation for {self.as_posix()}!") from exc def cwd(self) -> OMPathABC: """ @@ -776,10 +789,10 @@ def resolve(self, strict: bool = False) -> OMPathABC: def size(self) -> int: """ - Get the size of the file in bytes - implementation baseon on pathlib.Path. + Get the size of the file in bytes - implementation based on pathlib.Path. """ if not self.is_file(): - raise OMCSessionException(f"Path {self.as_posix()} is not a file!") + raise OMSessionException(f"Path {self.as_posix()} is not a file!") cmdl = self.get_session().get_cmd_prefix() cmdl += ['bash', '-c', f'stat -c %s "{self.as_posix()}"'] @@ -790,7 +803,7 @@ def size(self) -> int: try: return int(stdout) except ValueError as exc: - raise OSError(f"Invalid return value for filesize ({self.as_posix()}): {stdout}") from exc + raise OSError(f"Invalid return value for file size ({self.as_posix()}): {stdout}") from exc else: raise OSError(f"Cannot get size for file {self.as_posix()}") @@ -1087,7 +1100,7 @@ def __init__( try: self._omc_loghandle = open(file=self._omc_logfile, mode="w+", encoding="utf-8") except OSError as ex: - raise OMCSessionException(f"Cannot open log file {self._omc_logfile}.") from ex + raise OMSessionException(f"Cannot open log file {self._omc_logfile}.") from ex # variables to store compiled re expressions use in self.sendExpression() self._re_log_entries: Optional[re.Pattern[str]] = None @@ -1102,7 +1115,7 @@ def __post_init__(self) -> None: """ port = self.get_port() if not isinstance(port, str): - raise OMCSessionException(f"Invalid content for port: {port}") + raise OMSessionException(f"Invalid content for port: {port}") # Create the ZeroMQ socket and connect to OMC server context = zmq.Context.instance() @@ -1117,7 +1130,7 @@ def __del__(self): if isinstance(self._omc_zmq, zmq.Socket): try: self.sendExpression(expr="quit()") - except OMCSessionException as exc: + except OMSessionException as exc: logger.warning(f"Exception on sending 'quit()' to OMC: {exc}! Continue nevertheless ...") finally: self._omc_zmq = None @@ -1156,7 +1169,7 @@ def _timeout_loop( if timeout is None: timeout = self._timeout if timeout <= 0: - raise OMCSessionException(f"Invalid timeout: {timeout}") + raise OMSessionException(f"Invalid timeout: {timeout}") timer = 0.0 yield True @@ -1205,7 +1218,7 @@ def omcpath(self, *path) -> OMPathABC: if isinstance(self, OMCSessionLocal): # noinspection PyArgumentList return OMCPath(*path) - raise OMCSessionException("OMCPath is supported for Python < 3.12 only if OMCSessionLocal is used!") + raise OMSessionException("OMCPath is supported for Python < 3.12 only if OMCSessionLocal is used!") return OMCPath(*path, session=self) def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC: @@ -1224,26 +1237,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC return self._tempdir(tempdir_base=tempdir_base) - @staticmethod - def _tempdir(tempdir_base: OMPathABC) -> OMPathABC: - names = [str(uuid.uuid4()) for _ in range(100)] - - tempdir: Optional[OMPathABC] = None - for name in names: - # create a unique temporary directory name - tempdir = tempdir_base / name - - if tempdir.exists(): - continue - - tempdir.mkdir(parents=True, exist_ok=False) - break - - if tempdir is None or not tempdir.is_dir(): - raise OMCSessionException("Cannot create a temporary directory!") - - return tempdir - def execute(self, command: str): warnings.warn( message="This function is depreciated and will be removed in future versions; " @@ -1258,12 +1251,12 @@ def sendExpression(self, expr: str, parsed: bool = True) -> Any: """ Send an expression to the OMC server and return the result. - The complete error handling of the OMC result is done within this method using '"getMessagesStringInternal()'. + The complete error handling of the OMC result is done within this method using 'getMessagesStringInternal()'. Caller should only check for OMCSessionException. """ if self._omc_zmq is None: - raise OMCSessionException("No OMC running. Please create a new instance of OMCSession!") + raise OMSessionException("No OMC running. Please create a new instance of OMCSession!") logger.debug("sendExpression(expr='%r', parsed=%r)", str(expr), parsed) @@ -1278,11 +1271,11 @@ def sendExpression(self, expr: str, parsed: bool = True) -> Any: # in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked try: log_content = self.get_log() - except OMCSessionException: + except OMSessionException: log_content = 'log not available' logger.error(f"OMC did not start. Log-file says:\n{log_content}") - raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}).") + raise OMSessionException(f"No connection with OMC (timeout={self._timeout}).") if expr == "quit()": self._omc_zmq.close() @@ -1292,7 +1285,7 @@ def sendExpression(self, expr: str, parsed: bool = True) -> Any: result = self._omc_zmq.recv_string() if result.startswith('Error occurred building AST'): - raise OMCSessionException(f"OMC error: {result}") + raise OMSessionException(f"OMC error: {result}") if expr == "getErrorString()": # no error handling if 'getErrorString()' is called @@ -1376,8 +1369,8 @@ def sendExpression(self, expr: str, parsed: bool = True) -> Any: msg_long_list.append(msg_long) if has_error: msg_long_str = '\n'.join(f"{idx:02d}: {msg}" for idx, msg in enumerate(msg_long_list)) - raise OMCSessionException(f"OMC error occurred for 'sendExpression(expr={expr}, parsed={parsed}):\n" - f"{msg_long_str}") + raise OMSessionException(f"OMC error occurred for 'sendExpression(expr={expr}, parsed={parsed}):\n" + f"{msg_long_str}") if not parsed: return result @@ -1389,14 +1382,14 @@ def sendExpression(self, expr: str, parsed: bool = True) -> Any: try: return om_parser_basic(result) except (TypeError, UnboundLocalError) as ex2: - raise OMCSessionException("Cannot parse OMC result") from ex2 + raise OMSessionException("Cannot parse OMC result") from ex2 def get_port(self) -> Optional[str]: """ Get the port to connect to the OMC session. """ if not isinstance(self._omc_port, str): - raise OMCSessionException(f"Invalid port to connect to OMC process: {self._omc_port}") + raise OMSessionException(f"Invalid port to connect to OMC process: {self._omc_port}") return self._omc_port def get_log(self) -> str: @@ -1404,7 +1397,7 @@ def get_log(self) -> str: Get the log file content of the OMC session. """ if self._omc_loghandle is None: - raise OMCSessionException("Log file not available!") + raise OMSessionException("Log file not available!") self._omc_loghandle.seek(0) log = self._omc_loghandle.read() @@ -1475,7 +1468,7 @@ def _omc_home_get(omhome: Optional[str | os.PathLike] = None) -> pathlib.Path: if path_to_omc is not None: return pathlib.Path(path_to_omc).parents[1] - raise OMCSessionException("Cannot find OpenModelica executable, please install from openmodelica.org") + raise OMSessionException("Cannot find OpenModelica executable, please install from openmodelica.org") def _omc_process_get(self) -> subprocess.Popen: my_env = os.environ.copy() @@ -1509,8 +1502,8 @@ def _omc_port_get(self) -> str: break else: logger.error(f"OMC server did not start. Log-file says:\n{self.get_log()}") - raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}, " - f"logfile={repr(self._omc_logfile)}).") + raise OMSessionException(f"OMC Server did not start (timeout={self._timeout}, " + f"logfile={repr(self._omc_logfile)}).") logger.info(f"Local OMC Server is up and running at ZMQ port {port} " f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") @@ -1540,9 +1533,11 @@ def __init__( if omc_process is None: omc_process = OMCSessionLocal(omhome=omhome, timeout=timeout) elif not isinstance(omc_process, OMCSessionABC): - raise OMCSessionException("Invalid definition of the OMC process!") + raise OMSessionException("Invalid definition of the OMC process!") self.omc_process = omc_process + super().__init__(timeout=timeout) + def __del__(self): if hasattr(self, 'omc_process'): del self.omc_process @@ -1624,7 +1619,7 @@ def __init__( # connect to the running omc instance using ZMQ self._omc_port = self._omc_port_get(docker_cid=self._docker_container_id) if port is not None and not self._omc_port.endswith(f":{port}"): - raise OMCSessionException(f"Port mismatch: {self._omc_port} is not using the defined port {port}!") + raise OMSessionException(f"Port mismatch: {self._omc_port} is not using the defined port {port}!") self._cmd_prefix = self.model_execution_prefix() @@ -1642,13 +1637,13 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: try: docker_process = DockerPopen(int(columns[1])) except psutil.NoSuchProcess as ex: - raise OMCSessionException(f"Could not find PID {docker_top} - " - "is this a docker instance spawned without --pid=host?") from ex + raise OMSessionException(f"Could not find PID {docker_top} - " + "is this a docker instance spawned without --pid=host?") from ex if docker_process is not None: break else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") - raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") + raise OMSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") return docker_process @@ -1679,7 +1674,7 @@ def _omc_port_get( port = None if not isinstance(docker_cid, str): - raise OMCSessionException(f"Invalid docker container ID: {docker_cid}") + raise OMSessionException(f"Invalid docker container ID: {docker_cid}") # See if the omc server is running loop = self._timeout_loop(timestep=0.1) @@ -1698,8 +1693,8 @@ def _omc_port_get( break else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") - raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}, " - f"logfile={repr(self._omc_logfile)}).") + raise OMSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}, " + f"logfile={repr(self._omc_logfile)}).") logger.info(f"Docker based OMC Server is up and running at port {port}") @@ -1713,7 +1708,7 @@ def get_server_address(self) -> Optional[str]: output = subprocess.check_output(["docker", "inspect", self._docker_container_id]).decode().strip() address = json.loads(output)[0]["NetworkSettings"]["IPAddress"] if not isinstance(address, str): - raise OMCSessionException(f"Invalid docker server address: {address}!") + raise OMSessionException(f"Invalid docker server address: {address}!") return address return None @@ -1723,7 +1718,7 @@ def get_docker_container_id(self) -> str: Get the Docker container ID of the Docker container with the OMC server. """ if not isinstance(self._docker_container_id, str): - raise OMCSessionException(f"Invalid docker container ID: {self._docker_container_id}!") + raise OMSessionException(f"Invalid docker container ID: {self._docker_container_id}!") return self._docker_container_id @@ -1800,8 +1795,8 @@ def _docker_omc_cmd( if sys.platform == "win32": extra_flags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] if not self._omc_port: - raise OMCSessionException("Docker on Windows requires knowing which port to connect to - " - "please set the interactivePort argument") + raise OMSessionException("Docker on Windows requires knowing which port to connect to - " + "please set the interactivePort argument") port: Optional[int] = None if isinstance(omc_port, str): @@ -1811,8 +1806,8 @@ def _docker_omc_cmd( if sys.platform == "win32": if not isinstance(port, int): - raise OMCSessionException("OMC on Windows needs the interactive port - " - f"missing or invalid value: {repr(omc_port)}!") + raise OMSessionException("OMC on Windows needs the interactive port - " + f"missing or invalid value: {repr(omc_port)}!") docker_network_str = ["-p", f"127.0.0.1:{port}:{port}"] elif self._docker_network == "host" or self._docker_network is None: docker_network_str = ["--network=host"] @@ -1820,8 +1815,8 @@ def _docker_omc_cmd( docker_network_str = [] extra_flags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] else: - raise OMCSessionException(f'dockerNetwork was set to {self._docker_network}, ' - 'but only \"host\" or \"separate\" is allowed') + raise OMSessionException(f'dockerNetwork was set to {self._docker_network}, ' + 'but only \"host\" or \"separate\" is allowed') if isinstance(port, int): extra_flags = extra_flags + [f"--interactivePort={port}"] @@ -1848,7 +1843,7 @@ def _docker_omc_start( ) -> Tuple[subprocess.Popen, DockerPopen, str]: if not isinstance(docker_image, str): - raise OMCSessionException("A docker image name must be provided!") + raise OMSessionException("A docker image name must be provided!") my_env = os.environ.copy() @@ -1869,7 +1864,7 @@ def _docker_omc_start( env=my_env) if not isinstance(docker_cid_file, pathlib.Path): - raise OMCSessionException(f"Invalid content for docker container ID file path: {docker_cid_file}") + raise OMSessionException(f"Invalid content for docker container ID file path: {docker_cid_file}") # the provided value for docker_cid is not used docker_cid = None @@ -1885,14 +1880,14 @@ def _docker_omc_start( time.sleep(self._timeout / 40.0) if docker_cid is None: - raise OMCSessionException(f"Docker did not start (timeout={self._timeout} might be too short " - "especially if you did not docker pull the image before this command). " - f"Log-file says:\n{self.get_log()}") + raise OMSessionException(f"Docker did not start (timeout={self._timeout} might be too short " + "especially if you did not docker pull the image before this command). " + f"Log-file says:\n{self.get_log()}") docker_process = self._docker_process_get(docker_cid=docker_cid) if docker_process is None: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") - raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}.") + raise OMSessionException(f"Docker top did not contain omc process {self._random_string}.") return omc_process, docker_process, docker_cid @@ -1942,10 +1937,10 @@ def _docker_omc_cmd( if sys.platform == "win32": extra_flags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] if not isinstance(omc_port, int): - raise OMCSessionException("Docker on Windows requires knowing which port to connect to - " - "Please set the interactivePort argument. Furthermore, the container needs " - "to have already manually exposed this port when it was started " - "(-p 127.0.0.1:n:n) or you get an error later.") + raise OMSessionException("Docker on Windows requires knowing which port to connect to - " + "Please set the interactivePort argument. Furthermore, the container needs " + "to have already manually exposed this port when it was started " + "(-p 127.0.0.1:n:n) or you get an error later.") if isinstance(omc_port, int): extra_flags = extra_flags + [f"--interactivePort={omc_port}"] @@ -1969,7 +1964,7 @@ def _docker_omc_start( ) -> Tuple[subprocess.Popen, DockerPopen, str]: if not isinstance(docker_cid, str): - raise OMCSessionException("A docker container ID must be provided!") + raise OMSessionException("A docker container ID must be provided!") my_env = os.environ.copy() @@ -1991,8 +1986,8 @@ def _docker_omc_start( docker_process = self._docker_process_get(docker_cid=docker_cid) if docker_process is None: - raise OMCSessionException(f"Docker top did not contain omc process {self._random_string} " - f"/ {docker_cid}. Log-file says:\n{self.get_log()}") + raise OMSessionException(f"Docker top did not contain omc process {self._random_string} " + f"/ {docker_cid}. Log-file says:\n{self.get_log()}") return omc_process, docker_process, docker_cid @@ -2076,8 +2071,8 @@ def _omc_port_get(self) -> str: break else: logger.error(f"WSL based OMC server did not start. Log-file says:\n{self.get_log()}") - raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}, " - f"logfile={repr(self._omc_logfile)}).") + raise OMSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}, " + f"logfile={repr(self._omc_logfile)}).") logger.info(f"WSL based OMC Server is up and running at ZMQ port {port} " f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") @@ -2085,16 +2080,16 @@ def _omc_port_get(self) -> str: return port -class OMSessionRunner(OMSessionABC): +class OMSessionRunnerABC(OMSessionABC, metaclass=abc.ABCMeta): """ Implementation based on OMSessionABC without any use of an OMC server. """ def __init__( self, + ompath_runner: Type[OMPathRunnerABC], timeout: Optional[float] = None, version: str = "1.27.0", - ompath_runner: Type[OMPathRunnerABC] = OMPathRunnerLocal, cmd_prefix: Optional[list[str]] = None, model_execution_local: bool = True, ) -> None: @@ -2102,15 +2097,34 @@ def __init__( self._version = version if not issubclass(ompath_runner, OMPathRunnerABC): - raise OMCSessionException(f"Invalid OMPathRunner class: {type(ompath_runner)}!") + raise OMSessionException(f"Invalid OMPathRunner class: {type(ompath_runner)}!") self._ompath_runner = ompath_runner self.model_execution_local = model_execution_local if cmd_prefix is not None: self._cmd_prefix = cmd_prefix - # TODO: some checking?! - # if ompath_runner == Type[OMPathRunnerBash]: + +class OMSessionRunner(OMSessionRunnerABC): + """ + Implementation based on OMSessionABC without any use of an OMC server. + """ + + def __init__( + self, + ompath_runner: Type[OMPathRunnerABC] = OMPathRunnerLocal, + timeout: float = 10.0, + version: str = "1.27.0", + cmd_prefix: Optional[list[str]] = None, + model_execution_local: bool = True, + ) -> None: + super().__init__( + ompath_runner=ompath_runner, + timeout=timeout, + version=version, + cmd_prefix=cmd_prefix, + model_execution_local=model_execution_local, + ) def __post_init__(self) -> None: """ @@ -2153,7 +2167,7 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC return self._tempdir(tempdir_base=tempdir_base) def sendExpression(self, expr: str, parsed: bool = True) -> Any: - raise OMCSessionException(f"{self.__class__.__name__} does not uses an OMC server!") + raise OMSessionException(f"{self.__class__.__name__} does not uses an OMC server!") DummyPopen = DockerPopen diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 22c88137..96f5fb7c 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -33,11 +33,10 @@ OMSessionABC, OMSessionRunner, - OMCSessionABC, - ModelExecutionData, ModelExecutionException, + OMCSessionABC, OMCSessionCmd, OMCSessionDocker, OMCSessionDockerContainer, @@ -81,10 +80,9 @@ 'OMSessionABC', 'OMSessionRunner', - 'OMCSessionABC', - 'doe_get_solutions', + 'OMCSessionABC', 'OMCSessionCmd', 'OMCSessionDocker', 'OMCSessionDockerContainer',