├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── first_bug_report.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── FMITest.yml │ └── Test.yml ├── .gitignore ├── COPYING ├── OMPython ├── ModelicaSystem.py ├── OMCSession.py ├── OMParser.py ├── OMTypedParser.py └── __init__.py ├── README.md ├── pyproject.toml ├── setup.cfg └── tests ├── __init__.py ├── test_ArrayDimension.py ├── test_FMIExport.py ├── test_FMIRegression.py ├── test_ModelicaSystem.py ├── test_ModelicaSystemCmd.py ├── test_OMParser.py ├── test_OMSessionCmd.py ├── test_ZMQ.py ├── test_docker.py ├── test_linearization.py ├── test_optimization.py └── test_typedParser.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.c text eol=native 2 | *.cpp text eol=native 3 | *.h text eol=native 4 | *.m4 text eol=native 5 | .gitattributes text eol=native 6 | .gitignore text eol=native 7 | *.py text eol=native 8 | LICENSE text eol=native 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: "Report a problem to help us improve \U0001F680" 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | 12 | 13 | 14 | ### Steps to Reproduce 15 | 16 | 17 | 18 | ### Expected Behavior 19 | 20 | 21 | 22 | ### Screenshots 23 | 24 | 25 | 26 | ### Version and OS 27 | 28 | 29 | 30 | - Python Version 31 | - OMPython Version 32 | - OpenModelica Version 33 | - OS: [e.g. Windows 10, 64 bit] 34 | 35 | ### Additional Context 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/first_bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: First Bug Report 3 | about: Detailed guideline for your first bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | A clear and concise description of what the bug is. 12 | 13 | ### Steps to reproduce 14 | Please provide us with enough information to reproduce the issue on our side, otherwise it's hard to fix it. 15 | 16 | ### Expected behavior 17 | A clear and concise description of what you expected to happen. 18 | 19 | ### Screenshots 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | ### Version and OS 23 | - Python Version 24 | - OMPython Version 25 | - OpenModelica Version 26 | - OS: [e.g. Windows 10, 64 bit] 27 | 28 | ### Additional context 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every week 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Related Issues 2 | 3 | 4 | 5 | ### Purpose 6 | 7 | 8 | 9 | ### Approach 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/FMITest.yml: -------------------------------------------------------------------------------- 1 | name: FMITest 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 9 * * *" 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | timeout-minutes: 30 12 | strategy: 13 | matrix: 14 | python-version: ['3.12'] 15 | os: ['ubuntu-latest', 'windows-latest'] 16 | omc-version: ['stable', 'nightly'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: "Set up OpenModelica Compiler" 21 | uses: OpenModelica/setup-openmodelica@v1.0 22 | with: 23 | version: ${{ matrix.omc-version }} 24 | packages: | 25 | omc 26 | libraries: | 27 | 'Modelica 4.0.0' 28 | 29 | - run: "omc --version" 30 | 31 | - uses: actions/checkout@v4 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | architecture: 'x64' 37 | 38 | - name: Install dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install . pytest pytest-md pytest-emoji 42 | 43 | - name: Set timezone 44 | uses: szenius/set-timezone@v2.0 45 | with: 46 | timezoneLinux: 'Europe/Berlin' 47 | 48 | - name: Run FMI_EXPORT TEST 49 | uses: pavelzw/pytest-action@v2 50 | with: 51 | verbose: true 52 | emoji: true 53 | job-summary: true 54 | custom-arguments: 'tests/test_FMIRegression.py -v' 55 | click-to-expand: true 56 | report-title: 'FMI_Export TEST REPORT' 57 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | timeout-minutes: 30 13 | strategy: 14 | matrix: 15 | python-version: ['3.10', '3.12'] 16 | os: ['ubuntu-latest', 'windows-latest'] 17 | omc-version: ['stable'] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: "Set up OpenModelica Compiler" 23 | uses: OpenModelica/setup-openmodelica@v1.0 24 | with: 25 | version: ${{ matrix.omc-version }} 26 | packages: | 27 | omc 28 | libraries: | 29 | 'Modelica 4.0.0' 30 | - run: "omc --version" 31 | 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | architecture: 'x64' 37 | 38 | - name: Install dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install . pytest pytest-md pytest-emoji flake8 42 | 43 | - name: Set timezone 44 | uses: szenius/set-timezone@v2.0 45 | with: 46 | timezoneLinux: 'Europe/Berlin' 47 | 48 | - name: Lint with flake8 49 | run: | 50 | flake8 . --count --statistics 51 | 52 | - name: Run pytest 53 | uses: pavelzw/pytest-action@v2 54 | with: 55 | verbose: true 56 | emoji: true 57 | job-summary: true 58 | custom-arguments: '-v ' 59 | click-to-expand: true 60 | report-title: 'Test Report' 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /OMPython.egg-info 2 | /OMPythonIDL/ 3 | /build/ 4 | /dist/ 5 | /tmp/ 6 | *.py[cod] 7 | *.bak 8 | .ipynb_checkpoints/ 9 | 10 | # IDE settings and preferences 11 | *.pyproj 12 | *.sln 13 | .idea/ 14 | .vs/ 15 | .DS_Store 16 | .vscode/ 17 | /.venv/ 18 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | --- Start of Definition of OSMC Public License --- 2 | 3 | /* 4 | * This file is part of OpenModelica. 5 | * 6 | * Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 7 | * c/o Linköpings universitet, Department of Computer and Information Science, 8 | * SE-58183 Linköping, Sweden. 9 | * 10 | * All rights reserved. 11 | * 12 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF GPL VERSION 3 LICENSE OR 13 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. 14 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 15 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, 16 | * ACCORDING TO RECIPIENTS CHOICE. 17 | * 18 | * The OpenModelica software and the Open Source Modelica 19 | * Consortium (OSMC) Public License (OSMC-PL) are obtained 20 | * from OSMC, either from the above address, 21 | * from the URLs: http://www.ida.liu.se/projects/OpenModelica or 22 | * http://www.openmodelica.org, and in the OpenModelica distribution. 23 | * GNU version 3 is obtained from: http://www.gnu.org/copyleft/gpl.html. 24 | * 25 | * This program is distributed WITHOUT ANY WARRANTY; without 26 | * even the implied warranty of MERCHANTABILITY or FITNESS 27 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 28 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 29 | * 30 | * See the full OSMC Public License conditions for more details. 31 | * 32 | */ 33 | 34 | --- End of OSMC Public License Header --- 35 | 36 | The OSMC-PL is a public license for OpenModelica with three modes/alternatives 37 | (GPL, OSMC-Internal-EPL, OSMC-External-EPL) for use and redistribution, 38 | in source and/or binary/object-code form: 39 | 40 | * GPL. Any party (member or non-member of OSMC) may use and redistribute 41 | OpenModelica under GPL version 3. 42 | 43 | * Level 1 members of OSMC may also use and redistribute OpenModelica under 44 | OSMC-Internal-EPL conditions. 45 | 46 | * Level 2 members of OSMC may also use and redistribute OpenModelica under 47 | OSMC-Internal-EPL or OSMC-External-EPL conditions. 48 | 49 | Definitions of OSMC Public license modes: 50 | 51 | * GPL = GPL version 3. 52 | 53 | * OSMC-Internal-EPL = These OSMC Public license conditions together with 54 | Internally restricted EPL, i.e., EPL version 1.0 with the Additional 55 | Condition that use and redistribution by an OSMC member is only allowed 56 | within the OSMC member's own organization (i.e., its own legal entity), 57 | or for an OSMC member paying a membership fee corresponding to the size 58 | of the organization including all its affiliates, use and redistribution 59 | is allowed within/between its affiliates. 60 | 61 | * OSMC-External-EPL = These OSMC Public license conditions together with 62 | Externally restricted EPL, i.e., EPL version 1.0 with the Additional 63 | Condition that use and redistribution by an OSMC member, or by a Licensed 64 | Third Party Distributor having a redistribution agreement with that member, 65 | to parties external to the OSMC member’s own organization (i.e., its own 66 | legal entity) is only allowed in binary/object-code form, except the case of 67 | redistribution to other OSMC members to which source is also allowed to be 68 | distributed. 69 | 70 | [This has the consequence that an external party who wishes to use 71 | OpenModelica in source form together with its own proprietary software in all 72 | cases must be a member of OSMC]. 73 | 74 | In all cases of usage and redistribution by recipients, the following 75 | conditions also apply: 76 | 77 | a) Redistributions of source code must retain the above copyright notice, 78 | all definitions, and conditions. It is sufficient if the OSMC-PL Header is 79 | present in each source file, if the full OSMC-PL is available in a prominent 80 | and easily located place in the redistribution. 81 | 82 | b) Redistributions in binary/object-code form must reproduce the above 83 | copyright notice, all definitions, and conditions. It is sufficient if the 84 | OSMC-PL Header and the location in the redistribution of the full OSMC-PL 85 | are present in the documentation and/or other materials provided with the 86 | redistribution, if the full OSMC-PL is available in a prominent and easily 87 | located place in the redistribution. 88 | 89 | c) A recipient must clearly indicate its chosen usage mode of OSMC-PL, 90 | in accompanying documentation and in a text file OSMC-USAGE-MODE.txt, 91 | provided with the distribution. 92 | 93 | d) Contributor(s) making a Contribution to OpenModelica thereby also makes a 94 | Transfer of Contribution Copyright. In return, upon the effective date of 95 | the transfer, OSMC grants the Contributor(s) a Contribution License of the 96 | Contribution. OSMC has the right to accept or refuse Contributions. 97 | 98 | Definitions: 99 | 100 | "Subsidiary license conditions" means: 101 | 102 | The additional license conditions depending on the by the recipient chosen 103 | mode of OSMC-PL, defined by GPL version 3.0 for GPL, and by EPL for 104 | OSMC-Internal-EPL and OSMC-External-EPL. 105 | 106 | "OSMC-PL" means: 107 | 108 | Open Source Modelica Consortium Public License version 1.2, i.e., the license 109 | defined here (the text between 110 | "--- Start of Definition of OSMC Public License ---" and 111 | "--- End of Definition of OSMC Public License ---", or later versions thereof. 112 | 113 | "OSMC-PL Header" means: 114 | 115 | Open Source Modelica Consortium Public License Header version 1.2, i.e., the 116 | text between "--- Start of Definition of OSMC Public License ---" and 117 | "--- End of OSMC Public License Header ---, or later versions thereof. 118 | 119 | "Contribution" means: 120 | 121 | a) in the case of the initial Contributor, the initial code and documentation 122 | distributed under OSMC-PL, and 123 | 124 | b) in the case of each subsequent Contributor: 125 | i) changes to OpenModelica, and 126 | ii) additions to OpenModelica; 127 | 128 | where such changes and/or additions to OpenModelica originate from and are 129 | distributed by that particular Contributor. A Contribution 'originates' from 130 | a Contributor if it was added to OpenModelica by such Contributor itself or 131 | anyone acting on such Contributor's behalf. 132 | 133 | For Contributors licensing OpenModelica under OSMC-Internal-EPL or 134 | OSMC-External-EPL conditions, the following conditions also hold: 135 | 136 | Contributions do not include additions to the distributed Program which: (i) 137 | are separate modules of software distributed in conjunction with OpenModelica 138 | under their own license agreement, (ii) are separate modules which are not 139 | derivative works of OpenModelica, and (iii) are separate modules of software 140 | distributed in conjunction with OpenModelica under their own license agreement 141 | where these separate modules are merged with (weaved together with) modules of 142 | OpenModelica to form new modules that are distributed as object code or source 143 | code under their own license agreement, as allowed under the Additional 144 | Condition of internal distribution according to OSMC-Internal-EPL and/or 145 | Additional Condition for external distribution according to OSMC-External-EPL. 146 | 147 | "Transfer of Contribution Copyright" means that the Contributors of a 148 | Contribution transfer the ownership and the copyright of the Contribution to 149 | Open Source Modelica Consortium, the OpenModelica Copyright owner, for 150 | inclusion in OpenModelica. The transfer takes place upon the effective date 151 | when the Contribution is made available on the OSMC web site under OSMC-PL, by 152 | such Contributors themselves or anyone acting on such Contributors' behalf. 153 | The transfer is free of charge. If the Contributors or OSMC so wish, 154 | an optional Copyright transfer agreement can be signed between OSMC and the 155 | Contributors, as specified in an Appendix of the OSMC Bylaws. 156 | 157 | "Contribution License" means a license from OSMC to the Contributors of the 158 | Contribution, effective on the date of the Transfer of Contribution Copyright, 159 | where OSMC grants the Contributors a non-exclusive, world-wide, transferable, 160 | free of charge, perpetual license, including sublicensing rights, to use, 161 | have used, modify, have modified, reproduce and or have reproduced the 162 | contributed material, for business and other purposes, including but not 163 | limited to evaluation, development, testing, integration and merging with 164 | other software and distribution. The warranty and liability disclaimers of 165 | OSMC-PL apply to this license. 166 | 167 | "Contributor" means any person or entity that distributes (part of) 168 | OpenModelica. 169 | 170 | "The Program" means the Contributions distributed in accordance with OSMC-PL. 171 | 172 | "OpenModelica" means the Contributions distributed in accordance with OSMC-PL. 173 | 174 | "Recipient" means anyone who receives OpenModelica under OSMC-PL, 175 | including all Contributors. 176 | 177 | "Licensed Third Party Distributor" means a reseller/distributor having signed 178 | a redistribution/resale agreement in accordance with OSMC-PL and OSMC Bylaws, 179 | with an OSMC Level 2 organizational member which is not an Affiliate of the 180 | reseller/distributor, for distributing a product containing part(s) of 181 | OpenModelica. The Licensed Third Party Distributor shall only be allowed 182 | further redistribution to other resellers if the Level 2 member is granting 183 | such a right to it in the redistribution/resale agreement between the 184 | Level 2 member and the Licensed Third Party Distributor. 185 | 186 | "Affiliate" shall mean any legal entity, directly or indirectly, through one 187 | or more intermediaries, controlling or controlled by or under common control 188 | with any other legal entity, as the case may be. For purposes of this 189 | definition, the term "control" (including the terms "controlling," 190 | "controlled by" and "under common control with") means the possession, 191 | direct or indirect, of the power to direct or cause the direction of the 192 | management and policies of a legal entity, whether through the ownership of 193 | voting securities, by contract or otherwise. 194 | 195 | NO WARRANTY 196 | 197 | EXCEPT AS EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY 198 | LICENSE CONDITIONS OF OSMC-PL, OPENMODELICA IS PROVIDED ON AN "AS IS" 199 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 200 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 201 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 202 | PURPOSE. Each Recipient is solely responsible for determining the 203 | appropriateness of using and distributing OPENMODELICA and assumes all risks 204 | associated with its exercise of rights under OSMC-PL , including but not 205 | limited to the risks and costs of program errors, compliance with applicable 206 | laws, damage to or loss of data, programs or equipment, and unavailability 207 | or interruption of operations. 208 | 209 | DISCLAIMER OF LIABILITY 210 | 211 | EXCEPT AS EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY 212 | LICENSE CONDITIONS OF OSMC-PL, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 213 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 214 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 215 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 216 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 217 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF OPENMODELICA OR THE 218 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 219 | POSSIBILITY OF SUCH DAMAGES. 220 | 221 | A Contributor licensing OpenModelica under OSMC-Internal-EPL or 222 | OSMC-External-EPL may choose to distribute (parts of) OpenModelica in object 223 | code form under its own license agreement, provided that: 224 | 225 | a) it complies with the terms and conditions of OSMC-PL; or for the case of 226 | redistribution of OpenModelica together with proprietary code it is a dual 227 | license where the OpenModelica parts are distributed under OSMC-PL compatible 228 | conditions and the proprietary code is distributed under proprietary license 229 | conditions; and 230 | 231 | b) its license agreement: 232 | i) effectively disclaims on behalf of all Contributors all warranties and 233 | conditions, express and implied, including warranties or conditions of title 234 | and non-infringement, and implied warranties or conditions of merchantability 235 | and fitness for a particular purpose; 236 | ii) effectively excludes on behalf of all Contributors all liability for 237 | damages, including direct, indirect, special, incidental and consequential 238 | damages, such as lost profits; 239 | iii) states that any provisions which differ from OSMC-PL are offered by that 240 | Contributor alone and not by any other party; and 241 | iv) states from where the source code for OpenModelica is available, and 242 | informs licensees how to obtain it in a reasonable manner on or through a 243 | medium customarily used for software exchange. 244 | 245 | When OPENMODELICA is made available in source code form: 246 | 247 | a) it must be made available under OSMC-PL; and 248 | 249 | b) a copy of OSMC-PL must be included with each copy of OPENMODELICA. 250 | 251 | c) a copy of the subsidiary license associated with the selected mode of 252 | OSMC-PL must be included with each copy of OPENMODELICA. 253 | 254 | Contributors may not remove or alter any copyright notices contained within 255 | OPENMODELICA. 256 | 257 | If there is a conflict between OSMC-PL and the subsidiary license conditions, 258 | OSMC-PL has priority. 259 | 260 | This Agreement is governed by the laws of Sweden. The place of jurisdiction 261 | for all disagreements related to this Agreement, is Linköping, Sweden. 262 | 263 | The EPL 1.0 license definition has been obtained from: 264 | http://www.eclipse.org/legal/epl-v10.html. It is also reproduced in Appendix B 265 | of the OSMC Bylaws, and in the OpenModelica distribution. 266 | 267 | The GPL Version 3 license definition has been obtained from 268 | http://www.gnu.org/copyleft/gpl.html. It is also reproduced in Appendix C 269 | of the OSMC Bylaws, and in the OpenModelica distribution. 270 | 271 | --- End of Definition of OSMC Public License --- 272 | -------------------------------------------------------------------------------- /OMPython/ModelicaSystem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Definition of main class to run Modelica simulations - ModelicaSystem. 4 | """ 5 | 6 | __license__ = """ 7 | This file is part of OpenModelica. 8 | 9 | Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 10 | c/o Linköpings universitet, Department of Computer and Information Science, 11 | SE-58183 Linköping, Sweden. 12 | 13 | All rights reserved. 14 | 15 | THIS PROGRAM IS PROVIDED UNDER THE TERMS OF THE BSD NEW LICENSE OR THE 16 | GPL VERSION 3 LICENSE OR THE OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. 17 | ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 18 | RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, 19 | ACCORDING TO RECIPIENTS CHOICE. 20 | 21 | The OpenModelica software and the OSMC (Open Source Modelica Consortium) 22 | Public License (OSMC-PL) are obtained from OSMC, either from the above 23 | address, from the URLs: http://www.openmodelica.org or 24 | http://www.ida.liu.se/projects/OpenModelica, and in the OpenModelica 25 | distribution. GNU version 3 is obtained from: 26 | http://www.gnu.org/copyleft/gpl.html. The New BSD License is obtained from: 27 | http://www.opensource.org/licenses/BSD-3-Clause. 28 | 29 | This program is distributed WITHOUT ANY WARRANTY; without even the implied 30 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, EXCEPT AS 31 | EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE 32 | CONDITIONS OF OSMC-PL. 33 | """ 34 | 35 | import csv 36 | from dataclasses import dataclass 37 | import importlib 38 | import logging 39 | import numbers 40 | import numpy as np 41 | import os 42 | import pathlib 43 | import platform 44 | import re 45 | import subprocess 46 | import tempfile 47 | import textwrap 48 | from typing import Optional 49 | import warnings 50 | import xml.etree.ElementTree as ET 51 | 52 | from OMPython.OMCSession import OMCSessionZMQ, OMCSessionException 53 | 54 | # define logger using the current module name as ID 55 | logger = logging.getLogger(__name__) 56 | 57 | 58 | class ModelicaSystemError(Exception): 59 | pass 60 | 61 | 62 | @dataclass 63 | class LinearizationResult: 64 | """Modelica model linearization results. 65 | 66 | Attributes: 67 | n: number of states 68 | m: number of inputs 69 | p: number of outputs 70 | A: state matrix (n x n) 71 | B: input matrix (n x m) 72 | C: output matrix (p x n) 73 | D: feedthrough matrix (p x m) 74 | x0: fixed point 75 | u0: input corresponding to the fixed point 76 | stateVars: names of state variables 77 | inputVars: names of inputs 78 | outputVars: names of outputs 79 | """ 80 | 81 | n: int 82 | m: int 83 | p: int 84 | 85 | A: list 86 | B: list 87 | C: list 88 | D: list 89 | 90 | x0: list[float] 91 | u0: list[float] 92 | 93 | stateVars: list[str] 94 | inputVars: list[str] 95 | outputVars: list[str] 96 | 97 | def __iter__(self): 98 | """Allow unpacking A, B, C, D = result.""" 99 | yield self.A 100 | yield self.B 101 | yield self.C 102 | yield self.D 103 | 104 | def __getitem__(self, index: int): 105 | """Allow accessing A, B, C, D via result[0] through result[3]. 106 | 107 | This is needed for backwards compatibility, because 108 | ModelicaSystem.linearize() used to return [A, B, C, D]. 109 | """ 110 | return {0: self.A, 1: self.B, 2: self.C, 3: self.D}[index] 111 | 112 | 113 | class ModelicaSystemCmd: 114 | """ 115 | Execute a simulation by running the compiled model. 116 | """ 117 | 118 | def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] = None) -> None: 119 | """ 120 | Initialisation 121 | 122 | Parameters 123 | ---------- 124 | runpath : pathlib.Path 125 | modelname : str 126 | timeout : Optional[int], None 127 | """ 128 | self._runpath = pathlib.Path(runpath).resolve().absolute() 129 | self._modelname = modelname 130 | self._timeout = timeout 131 | self._args: dict[str, str | None] = {} 132 | self._arg_override: dict[str, str] = {} 133 | 134 | def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: 135 | """ 136 | Set one argument for the executable model. 137 | 138 | Parameters 139 | ---------- 140 | key : str 141 | val : str, None 142 | """ 143 | if not isinstance(key, str): 144 | raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") 145 | key = key.strip() 146 | if val is None: 147 | argval = None 148 | elif isinstance(val, str): 149 | argval = val.strip() 150 | elif isinstance(val, numbers.Number): 151 | argval = str(val) 152 | elif key == 'override' and isinstance(val, dict): 153 | for okey in val: 154 | if not isinstance(okey, str) or not isinstance(val[okey], (str, numbers.Number)): 155 | raise ModelicaSystemError("Invalid argument for 'override': " 156 | f"{repr(okey)} = {repr(val[okey])}") 157 | self._arg_override[okey] = val[okey] 158 | 159 | argval = ','.join([f"{okey}={str(self._arg_override[okey])}" for okey in self._arg_override]) 160 | else: 161 | raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") 162 | 163 | if key in self._args: 164 | logger.warning(f"Overwrite model executable argument: {repr(key)} = {repr(argval)} " 165 | f"(was: {repr(self._args[key])})") 166 | self._args[key] = argval 167 | 168 | def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None: 169 | """ 170 | Define arguments for the model executable. 171 | 172 | Parameters 173 | ---------- 174 | args : dict[str, Optional[str | dict[str, str]]] 175 | """ 176 | for arg in args: 177 | self.arg_set(key=arg, val=args[arg]) 178 | 179 | def get_exe(self) -> pathlib.Path: 180 | """ 181 | Get the path to the executable / complied model. 182 | 183 | Returns 184 | ------- 185 | pathlib.Path 186 | """ 187 | if platform.system() == "Windows": 188 | path_exe = self._runpath / f"{self._modelname}.exe" 189 | else: 190 | path_exe = self._runpath / self._modelname 191 | 192 | if not path_exe.exists(): 193 | raise ModelicaSystemError(f"Application file path not found: {path_exe}") 194 | 195 | return path_exe 196 | 197 | def get_cmd(self) -> list: 198 | """ 199 | Run the requested simulation 200 | 201 | Returns 202 | ------- 203 | list 204 | """ 205 | 206 | path_exe = self.get_exe() 207 | 208 | cmdl = [path_exe.as_posix()] 209 | for key in self._args: 210 | if self._args[key] is None: 211 | cmdl.append(f"-{key}") 212 | else: 213 | cmdl.append(f"-{key}={self._args[key]}") 214 | 215 | return cmdl 216 | 217 | def run(self) -> int: 218 | """ 219 | Run the requested simulation 220 | 221 | Returns 222 | ------- 223 | int 224 | """ 225 | 226 | cmdl: list = self.get_cmd() 227 | 228 | logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix()) 229 | 230 | if platform.system() == "Windows": 231 | path_dll = "" 232 | 233 | # set the process environment from the generated .bat file in windows which should have all the dependencies 234 | path_bat = self._runpath / f"{self._modelname}.bat" 235 | if not path_bat.exists(): 236 | ModelicaSystemError("Batch file (*.bat) does not exist " + str(path_bat)) 237 | 238 | with open(path_bat, 'r') as file: 239 | for line in file: 240 | match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) 241 | if match: 242 | path_dll = match.group(1).strip(';') # Remove any trailing semicolons 243 | my_env = os.environ.copy() 244 | my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"] 245 | else: 246 | # TODO: how to handle path to resources of external libraries for any system not Windows? 247 | my_env = None 248 | 249 | try: 250 | cmdres = subprocess.run(cmdl, capture_output=True, text=True, env=my_env, cwd=self._runpath, 251 | timeout=self._timeout) 252 | stdout = cmdres.stdout.strip() 253 | stderr = cmdres.stderr.strip() 254 | returncode = cmdres.returncode 255 | 256 | logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout) 257 | 258 | if stderr: 259 | raise ModelicaSystemError(f"Error running command {repr(cmdl)}: {stderr}") 260 | except subprocess.TimeoutExpired: 261 | raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") 262 | except subprocess.CalledProcessError as ex: 263 | raise ModelicaSystemError(f"Error running command {repr(cmdl)}") from ex 264 | 265 | return returncode 266 | 267 | @staticmethod 268 | def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]: 269 | """ 270 | Parse a simflag definition; this is depreciated! 271 | 272 | The return data can be used as input for self.args_set(). 273 | 274 | Parameters 275 | ---------- 276 | simflags : str 277 | 278 | Returns 279 | ------- 280 | dict 281 | """ 282 | warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " 283 | "please use 'simargs' instead", DeprecationWarning, stacklevel=2) 284 | 285 | simargs: dict[str, Optional[str | dict[str, str]]] = {} 286 | 287 | args = [s for s in simflags.split(' ') if s] 288 | for arg in args: 289 | if arg[0] != '-': 290 | raise ModelicaSystemError(f"Invalid simulation flag: {arg}") 291 | arg = arg[1:] 292 | parts = arg.split('=') 293 | if len(parts) == 1: 294 | simargs[parts[0]] = None 295 | elif parts[0] == 'override': 296 | override = '='.join(parts[1:]) 297 | 298 | override_dict = {} 299 | for item in override.split(','): 300 | kv = item.split('=') 301 | if not (0 < len(kv) < 3): 302 | raise ModelicaSystemError(f"Invalid value for '-override': {override}") 303 | if kv[0]: 304 | try: 305 | override_dict[kv[0]] = kv[1] 306 | except (KeyError, IndexError) as ex: 307 | raise ModelicaSystemError(f"Invalid value for '-override': {override}") from ex 308 | 309 | simargs[parts[0]] = override_dict 310 | 311 | return simargs 312 | 313 | 314 | class ModelicaSystem: 315 | def __init__( 316 | self, 317 | fileName: Optional[str | os.PathLike] = None, 318 | modelName: Optional[str] = None, 319 | lmodel: Optional[list[str | tuple[str, str]]] = None, 320 | commandLineOptions: Optional[str] = None, 321 | variableFilter: Optional[str] = None, 322 | customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None, 323 | omhome: Optional[str] = None, 324 | session: Optional[OMCSessionZMQ] = None, 325 | build: Optional[bool] = True, 326 | ) -> None: 327 | """Initialize, load and build a model. 328 | 329 | The constructor loads the model file and builds it, generating exe and 330 | xml files, etc. 331 | 332 | Args: 333 | fileName: Path to the model file. Either absolute or relative to 334 | the current working directory. 335 | modelName: The name of the model class. If it is contained within 336 | a package, "PackageName.ModelName" should be used. 337 | lmodel: List of libraries to be loaded before the model itself is 338 | loaded. Two formats are supported for the list elements: 339 | lmodel=["Modelica"] for just the library name 340 | and lmodel=[("Modelica","3.2.3")] for specifying both the name 341 | and the version. 342 | commandLineOptions: String with extra command line options to be 343 | provided to omc via setCommandLineOptions(). 344 | variableFilter: A regular expression. Only variables fully 345 | matching the regexp will be stored in the result file. 346 | Leaving it unspecified is equivalent to ".*". 347 | customBuildDirectory: Path to a directory to be used for temporary 348 | files like the model executable. If left unspecified, a tmp 349 | directory will be created. 350 | omhome: OPENMODELICAHOME value to be used when creating the OMC 351 | session. 352 | session: OMC session to be used. If unspecified, a new session 353 | will be created. 354 | build: Boolean controlling whether or not the model should be 355 | built when constructor is called. If False, the constructor 356 | simply loads the model without compiling. 357 | 358 | Examples: 359 | mod = ModelicaSystem("ModelicaModel.mo", "modelName") 360 | mod = ModelicaSystem("ModelicaModel.mo", "modelName", ["Modelica"]) 361 | mod = ModelicaSystem("ModelicaModel.mo", "modelName", [("Modelica","3.2.3"), "PowerSystems"]) 362 | """ 363 | if fileName is None and modelName is None and not lmodel: # all None 364 | raise ModelicaSystemError("Cannot create ModelicaSystem object without any arguments") 365 | 366 | self.quantitiesList = [] 367 | self.paramlist = {} 368 | self.inputlist = {} 369 | self.outputlist = {} 370 | self.continuouslist = {} 371 | self.simulateOptions = {} 372 | self.overridevariables = {} 373 | self.simoptionsoverride = {} 374 | self.linearOptions = {'startTime': 0.0, 'stopTime': 1.0, 'stepSize': 0.002, 'tolerance': 1e-8} 375 | self.optimizeOptions = {'startTime': 0.0, 'stopTime': 1.0, 'numberOfIntervals': 500, 'stepSize': 0.002, 376 | 'tolerance': 1e-8} 377 | self.linearinputs = [] # linearization input list 378 | self.linearoutputs = [] # linearization output list 379 | self.linearstates = [] # linearization states list 380 | 381 | if session is not None: 382 | if not isinstance(session, OMCSessionZMQ): 383 | raise ModelicaSystemError("Invalid session data provided!") 384 | self.getconn = session 385 | else: 386 | self.getconn = OMCSessionZMQ(omhome=omhome) 387 | 388 | # set commandLineOptions if provided by users 389 | self.setCommandLineOptions(commandLineOptions=commandLineOptions) 390 | 391 | if lmodel is None: 392 | lmodel = [] 393 | 394 | if not isinstance(lmodel, list): 395 | raise ModelicaSystemError(f"Invalid input type for lmodel: {type(lmodel)} - list expected!") 396 | 397 | self.xmlFile = None 398 | self.lmodel = lmodel # may be needed if model is derived from other model 399 | self.modelName = modelName # Model class name 400 | self.fileName = pathlib.Path(fileName).resolve() if fileName is not None else None # Model file/package name 401 | self.inputFlag = False # for model with input quantity 402 | self.simulationFlag = False # if the model is simulated? 403 | self.outputFlag = False 404 | self.csvFile: Optional[pathlib.Path] = None # for storing inputs condition 405 | self.resultfile: Optional[pathlib.Path] = None # for storing result file 406 | self.variableFilter = variableFilter 407 | 408 | if self.fileName is not None and not self.fileName.is_file(): # if file does not exist 409 | raise IOError(f"{self.fileName} does not exist!") 410 | 411 | # set default command Line Options for linearization as 412 | # linearize() will use the simulation executable and runtime 413 | # flag -l to perform linearization 414 | self.setCommandLineOptions("--linearizationDumpLanguage=python") 415 | self.setCommandLineOptions("--generateSymbolicLinearization") 416 | 417 | self.tempdir = self.setTempDirectory(customBuildDirectory) 418 | 419 | if self.fileName is not None: 420 | self.loadLibrary(lmodel=self.lmodel) 421 | self.loadFile(fileName=self.fileName) 422 | 423 | # allow directly loading models from MSL without fileName 424 | elif fileName is None and modelName is not None: 425 | self.loadLibrary(lmodel=self.lmodel) 426 | 427 | if build: 428 | self.buildModel(variableFilter) 429 | 430 | def setCommandLineOptions(self, commandLineOptions: Optional[str] = None): 431 | # set commandLineOptions if provided by users 432 | if commandLineOptions is None: 433 | return 434 | exp = f'setCommandLineOptions("{commandLineOptions}")' 435 | self.sendExpression(exp) 436 | 437 | def loadFile(self, fileName: pathlib.Path): 438 | # load file 439 | self.sendExpression(f'loadFile("{fileName.as_posix()}")') 440 | 441 | # for loading file/package, loading model and building model 442 | def loadLibrary(self, lmodel: list): 443 | # load Modelica standard libraries or Modelica files if needed 444 | for element in lmodel: 445 | if element is not None: 446 | if isinstance(element, str): 447 | if element.endswith(".mo"): 448 | apiCall = "loadFile" 449 | else: 450 | apiCall = "loadModel" 451 | self.requestApi(apiCall, element) 452 | elif isinstance(element, tuple): 453 | if not element[1]: 454 | expr_load_lib = f"loadModel({element[0]})" 455 | else: 456 | expr_load_lib = f'loadModel({element[0]}, {{"{element[1]}"}})' 457 | self.sendExpression(expr_load_lib) 458 | else: 459 | raise ModelicaSystemError("loadLibrary() failed, Unknown type detected: " 460 | f"{element} is of type {type(element)}, " 461 | "The following patterns are supported:\n" 462 | '1)["Modelica"]\n' 463 | '2)[("Modelica","3.2.3"), "PowerSystems"]\n') 464 | 465 | def setTempDirectory(self, customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None) -> pathlib.Path: 466 | # create a unique temp directory for each session and build the model in that directory 467 | if customBuildDirectory is not None: 468 | if not os.path.exists(customBuildDirectory): 469 | raise IOError(customBuildDirectory, " does not exist") 470 | tempdir = pathlib.Path(customBuildDirectory) 471 | else: 472 | tempdir = pathlib.Path(tempfile.mkdtemp()) 473 | if not tempdir.is_dir(): 474 | raise IOError(tempdir, " cannot be created") 475 | 476 | logger.info("Define tempdir as %s", tempdir) 477 | exp = f'cd("{tempdir.absolute().as_posix()}")' 478 | self.sendExpression(exp) 479 | 480 | return tempdir 481 | 482 | def getWorkDirectory(self) -> pathlib.Path: 483 | return self.tempdir 484 | 485 | def buildModel(self, variableFilter: Optional[str] = None): 486 | if variableFilter is not None: 487 | self.variableFilter = variableFilter 488 | 489 | if self.variableFilter is not None: 490 | varFilter = f'variableFilter="{self.variableFilter}"' 491 | else: 492 | varFilter = 'variableFilter=".*"' 493 | 494 | buildModelResult = self.requestApi("buildModel", self.modelName, properties=varFilter) 495 | logger.debug("OM model build result: %s", buildModelResult) 496 | 497 | self.xmlFile = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] 498 | self.xmlparse() 499 | 500 | def sendExpression(self, expr: str, parsed: bool = True): 501 | try: 502 | retval = self.getconn.sendExpression(expr, parsed) 503 | except OMCSessionException as ex: 504 | raise ModelicaSystemError(f"Error executing {repr(expr)}") from ex 505 | 506 | logger.debug(f"Result of executing {repr(expr)}: {textwrap.shorten(repr(retval), width=100)}") 507 | 508 | return retval 509 | 510 | # request to OMC 511 | def requestApi(self, apiName, entity=None, properties=None): # 2 512 | if entity is not None and properties is not None: 513 | exp = f'{apiName}({entity}, {properties})' 514 | elif entity is not None and properties is None: 515 | if apiName in ("loadFile", "importFMU"): 516 | exp = f'{apiName}("{entity}")' 517 | else: 518 | exp = f'{apiName}({entity})' 519 | else: 520 | exp = f'{apiName}()' 521 | 522 | return self.sendExpression(exp) 523 | 524 | def xmlparse(self): 525 | if not self.xmlFile.is_file(): 526 | raise ModelicaSystemError(f"XML file not generated: {self.xmlFile}") 527 | 528 | tree = ET.parse(self.xmlFile) 529 | rootCQ = tree.getroot() 530 | for attr in rootCQ.iter('DefaultExperiment'): 531 | for key in ("startTime", "stopTime", "stepSize", "tolerance", 532 | "solver", "outputFormat"): 533 | self.simulateOptions[key] = attr.get(key) 534 | 535 | for sv in rootCQ.iter('ScalarVariable'): 536 | scalar = {} 537 | for key in ("name", "description", "variability", "causality", "alias"): 538 | scalar[key] = sv.get(key) 539 | scalar["changeable"] = sv.get('isValueChangeable') 540 | scalar["aliasvariable"] = sv.get('aliasVariable') 541 | ch = list(sv) 542 | for att in ch: 543 | scalar["start"] = att.get('start') 544 | scalar["min"] = att.get('min') 545 | scalar["max"] = att.get('max') 546 | scalar["unit"] = att.get('unit') 547 | 548 | if scalar["variability"] == "parameter": 549 | if scalar["name"] in self.overridevariables: 550 | self.paramlist[scalar["name"]] = self.overridevariables[scalar["name"]] 551 | else: 552 | self.paramlist[scalar["name"]] = scalar["start"] 553 | if scalar["variability"] == "continuous": 554 | self.continuouslist[scalar["name"]] = scalar["start"] 555 | if scalar["causality"] == "input": 556 | self.inputlist[scalar["name"]] = scalar["start"] 557 | if scalar["causality"] == "output": 558 | self.outputlist[scalar["name"]] = scalar["start"] 559 | 560 | self.quantitiesList.append(scalar) 561 | 562 | def getQuantities(self, names=None): # 3 563 | """ 564 | This method returns list of dictionaries. It displays details of quantities such as name, value, changeable, and description, where changeable means if value for corresponding quantity name is changeable or not. It can be called : 565 | usage: 566 | >>> getQuantities() 567 | >>> getQuantities("Name1") 568 | >>> getQuantities(["Name1","Name2"]) 569 | """ 570 | if names is None: 571 | return self.quantitiesList 572 | elif isinstance(names, str): 573 | return [x for x in self.quantitiesList if x["name"] == names] 574 | elif isinstance(names, list): 575 | return [x for y in names for x in self.quantitiesList if x["name"] == y] 576 | 577 | raise ModelicaSystemError("Unhandled input for getQuantities()") 578 | 579 | def getContinuous(self, names=None): # 4 580 | """ 581 | This method returns dict. The key is continuous names and value is corresponding continuous value. 582 | usage: 583 | >>> getContinuous() 584 | >>> getContinuous("Name1") 585 | >>> getContinuous(["Name1","Name2"]) 586 | """ 587 | if not self.simulationFlag: 588 | if names is None: 589 | return self.continuouslist 590 | elif isinstance(names, str): 591 | return [self.continuouslist.get(names, "NotExist")] 592 | elif isinstance(names, list): 593 | return [self.continuouslist.get(x, "NotExist") for x in names] 594 | else: 595 | if names is None: 596 | for i in self.continuouslist: 597 | try: 598 | value = self.getSolutions(i) 599 | self.continuouslist[i] = value[0][-1] 600 | except (OMCSessionException, ModelicaSystemError) as ex: 601 | raise ModelicaSystemError(f"{i} could not be computed") from ex 602 | return self.continuouslist 603 | 604 | elif isinstance(names, str): 605 | if names in self.continuouslist: 606 | value = self.getSolutions(names) 607 | self.continuouslist[names] = value[0][-1] 608 | return [self.continuouslist.get(names)] 609 | else: 610 | raise ModelicaSystemError(f"{names} is not continuous") 611 | 612 | elif isinstance(names, list): 613 | valuelist = [] 614 | for i in names: 615 | if i in self.continuouslist: 616 | value = self.getSolutions(i) 617 | self.continuouslist[i] = value[0][-1] 618 | valuelist.append(value[0][-1]) 619 | else: 620 | raise ModelicaSystemError(f"{i} is not continuous") 621 | return valuelist 622 | 623 | raise ModelicaSystemError("Unhandled input for getContinous()") 624 | 625 | def getParameters(self, names: Optional[str | list[str]] = None) -> dict[str, str] | list[str]: # 5 626 | """Get parameter values. 627 | 628 | Args: 629 | names: Either None (default), a string with the parameter name, 630 | or a list of parameter name strings. 631 | Returns: 632 | If `names` is None, a dict in the format 633 | {parameter_name: parameter_value} is returned. 634 | If `names` is a string, a single element list is returned. 635 | If `names` is a list, a list with one value for each parameter name 636 | in names is returned. 637 | In all cases, parameter values are returned as strings. 638 | 639 | Examples: 640 | >>> mod.getParameters() 641 | {'Name1': '1.23', 'Name2': '4.56'} 642 | >>> mod.getParameters("Name1") 643 | ['1.23'] 644 | >>> mod.getParameters(["Name1","Name2"]) 645 | ['1.23', '4.56'] 646 | """ 647 | if names is None: 648 | return self.paramlist 649 | elif isinstance(names, str): 650 | return [self.paramlist.get(names, "NotExist")] 651 | elif isinstance(names, list): 652 | return [self.paramlist.get(x, "NotExist") for x in names] 653 | 654 | raise ModelicaSystemError("Unhandled input for getParameters()") 655 | 656 | def getInputs(self, names: Optional[str | list[str]] = None) -> dict | list: # 6 657 | """Get input values. 658 | 659 | Args: 660 | names: Either None (default), a string with the input name, 661 | or a list of input name strings. 662 | Returns: 663 | If `names` is None, a dict in the format 664 | {input_name: input_value} is returned. 665 | If `names` is a string, a single element list [input_value] is 666 | returned. 667 | If `names` is a list, a list with one value for each input name 668 | in names is returned: [input1_values, input2_values, ...]. 669 | In all cases, input values are returned as a list of tuples, 670 | where the first element in the tuple is the time and the second 671 | element is the input value. 672 | 673 | Examples: 674 | >>> mod.getInputs() 675 | {'Name1': [(0.0, 0.0), (1.0, 1.0)], 'Name2': None} 676 | >>> mod.getInputs("Name1") 677 | [[(0.0, 0.0), (1.0, 1.0)]] 678 | >>> mod.getInputs(["Name1","Name2"]) 679 | [[(0.0, 0.0), (1.0, 1.0)], None] 680 | >>> mod.getInputs("ThisInputDoesNotExist") 681 | ['NotExist'] 682 | """ 683 | if names is None: 684 | return self.inputlist 685 | elif isinstance(names, str): 686 | return [self.inputlist.get(names, "NotExist")] 687 | elif isinstance(names, list): 688 | return [self.inputlist.get(x, "NotExist") for x in names] 689 | 690 | raise ModelicaSystemError("Unhandled input for getInputs()") 691 | 692 | def getOutputs(self, names: Optional[str | list[str]] = None): # 7 693 | """Get output values. 694 | 695 | If called before simulate(), the initial values are returned as 696 | strings. If called after simulate(), the final values (at stopTime) 697 | are returned as numpy.float64. 698 | 699 | Args: 700 | names: Either None (default), a string with the output name, 701 | or a list of output name strings. 702 | Returns: 703 | If `names` is None, a dict in the format 704 | {output_name: output_value} is returned. 705 | If `names` is a string, a single element list [output_value] is 706 | returned. 707 | If `names` is a list, a list with one value for each output name 708 | in names is returned: [output1_value, output2_value, ...]. 709 | 710 | Examples: 711 | Before simulate(): 712 | >>> mod.getOutputs() 713 | {'out1': '-0.4', 'out2': '1.2'} 714 | >>> mod.getOutputs("out1") 715 | ['-0.4'] 716 | >>> mod.getOutputs(["out1","out2"]) 717 | ['-0.4', '1.2'] 718 | >>> mod.getOutputs("ThisOutputDoesNotExist") 719 | ['NotExist'] 720 | 721 | After simulate(): 722 | >>> mod.getOutputs() 723 | {'out1': np.float64(-0.1234), 'out2': np.float64(2.1)} 724 | >>> mod.getOutputs("out1") 725 | [np.float64(-0.1234)] 726 | >>> mod.getOutputs(["out1","out2"]) 727 | [np.float64(-0.1234), np.float64(2.1)] 728 | """ 729 | if not self.simulationFlag: 730 | if names is None: 731 | return self.outputlist 732 | elif isinstance(names, str): 733 | return [self.outputlist.get(names, "NotExist")] 734 | else: 735 | return [self.outputlist.get(x, "NotExist") for x in names] 736 | else: 737 | if names is None: 738 | for i in self.outputlist: 739 | value = self.getSolutions(i) 740 | self.outputlist[i] = value[0][-1] 741 | return self.outputlist 742 | elif isinstance(names, str): 743 | if names in self.outputlist: 744 | value = self.getSolutions(names) 745 | self.outputlist[names] = value[0][-1] 746 | return [self.outputlist.get(names)] 747 | else: 748 | return names, " is not Output" 749 | elif isinstance(names, list): 750 | valuelist = [] 751 | for i in names: 752 | if i in self.outputlist: 753 | value = self.getSolutions(i) 754 | self.outputlist[i] = value[0][-1] 755 | valuelist.append(value[0][-1]) 756 | else: 757 | return i, "is not Output" 758 | return valuelist 759 | 760 | raise ModelicaSystemError("Unhandled input for getOutputs()") 761 | 762 | def getSimulationOptions(self, names=None): # 8 763 | """ 764 | This method returns dict. The key is simulation option names and value is corresponding simulation option value. 765 | If name is None then the function will return dict which contain all simulation option names as key and value as corresponding values. eg., getSimulationOptions() 766 | usage: 767 | >>> getSimulationOptions() 768 | >>> getSimulationOptions("Name1") 769 | >>> getSimulationOptions(["Name1","Name2"]) 770 | """ 771 | if names is None: 772 | return self.simulateOptions 773 | elif isinstance(names, str): 774 | return [self.simulateOptions.get(names, "NotExist")] 775 | elif isinstance(names, list): 776 | return [self.simulateOptions.get(x, "NotExist") for x in names] 777 | 778 | raise ModelicaSystemError("Unhandled input for getSimulationOptions()") 779 | 780 | def getLinearizationOptions(self, names=None): # 9 781 | """ 782 | This method returns dict. The key is linearize option names and value is corresponding linearize option value. 783 | If name is None then the function will return dict which contain all linearize option names as key and value as corresponding values. eg., getLinearizationOptions() 784 | usage: 785 | >>> getLinearizationOptions() 786 | >>> getLinearizationOptions("Name1") 787 | >>> getLinearizationOptions(["Name1","Name2"]) 788 | """ 789 | if names is None: 790 | return self.linearOptions 791 | elif isinstance(names, str): 792 | return [self.linearOptions.get(names, "NotExist")] 793 | elif isinstance(names, list): 794 | return [self.linearOptions.get(x, "NotExist") for x in names] 795 | 796 | raise ModelicaSystemError("Unhandled input for getLinearizationOptions()") 797 | 798 | def getOptimizationOptions(self, names=None): # 10 799 | """ 800 | usage: 801 | >>> getOptimizationOptions() 802 | >>> getOptimizationOptions("Name1") 803 | >>> getOptimizationOptions(["Name1","Name2"]) 804 | """ 805 | if names is None: 806 | return self.optimizeOptions 807 | elif isinstance(names, str): 808 | return [self.optimizeOptions.get(names, "NotExist")] 809 | elif isinstance(names, list): 810 | return [self.optimizeOptions.get(x, "NotExist") for x in names] 811 | 812 | raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") 813 | 814 | def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = None, 815 | simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, 816 | timeout: Optional[int] = None): # 11 817 | """ 818 | This method simulates model according to the simulation options. 819 | usage 820 | >>> simulate() 821 | >>> simulate(resultfile="a.mat") 822 | >>> simulate(simflags="-noEventEmit -noRestart -override=e=0.3,g=10") # set runtime simulation flags 823 | >>> simulate(simargs={"noEventEmit": None, "noRestart": None, "override": "e=0.3,g=10"}) # using simargs 824 | """ 825 | 826 | om_cmd = ModelicaSystemCmd(runpath=self.tempdir, modelname=self.modelName, timeout=timeout) 827 | 828 | if resultfile is None: 829 | # default result file generated by OM 830 | self.resultfile = self.tempdir / f"{self.modelName}_res.mat" 831 | elif os.path.exists(resultfile): 832 | self.resultfile = pathlib.Path(resultfile) 833 | else: 834 | self.resultfile = self.tempdir / resultfile 835 | # always define the resultfile to use 836 | om_cmd.arg_set(key="r", val=self.resultfile.as_posix()) 837 | 838 | # allow runtime simulation flags from user input 839 | if simflags is not None: 840 | om_cmd.args_set(args=om_cmd.parse_simflags(simflags=simflags)) 841 | 842 | if simargs: 843 | om_cmd.args_set(args=simargs) 844 | 845 | overrideFile = self.tempdir / f"{self.modelName}_override.txt" 846 | if self.overridevariables or self.simoptionsoverride: 847 | tmpdict = self.overridevariables.copy() 848 | tmpdict.update(self.simoptionsoverride) 849 | # write to override file 850 | with open(overrideFile, "w") as file: 851 | for key, value in tmpdict.items(): 852 | file.write(f"{key}={value}\n") 853 | 854 | om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix()) 855 | 856 | if self.inputFlag: # if model has input quantities 857 | for i in self.inputlist: 858 | val = self.inputlist[i] 859 | if val is None: 860 | val = [(float(self.simulateOptions["startTime"]), 0.0), 861 | (float(self.simulateOptions["stopTime"]), 0.0)] 862 | self.inputlist[i] = [(float(self.simulateOptions["startTime"]), 0.0), 863 | (float(self.simulateOptions["stopTime"]), 0.0)] 864 | if float(self.simulateOptions["startTime"]) != val[0][0]: 865 | raise ModelicaSystemError(f"startTime not matched for Input {i}!") 866 | if float(self.simulateOptions["stopTime"]) != val[-1][0]: 867 | raise ModelicaSystemError(f"stopTime not matched for Input {i}!") 868 | self.csvFile = self.createCSVData() # create csv file 869 | 870 | om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) 871 | 872 | # delete resultfile ... 873 | if self.resultfile.is_file(): 874 | self.resultfile.unlink() 875 | # ... run simulation ... 876 | returncode = om_cmd.run() 877 | # and check returncode *AND* resultfile 878 | if returncode != 0 and self.resultfile.is_file(): 879 | logger.warning(f"Return code = {returncode} but result file exists!") 880 | 881 | self.simulationFlag = True 882 | 883 | # to extract simulation results 884 | def getSolutions(self, varList=None, resultfile=None): # 12 885 | """ 886 | This method returns tuple of numpy arrays. It can be called: 887 | •with a list of quantities name in string format as argument: it returns the simulation results of the corresponding names in the same order. Here it supports Python unpacking depending upon the number of variables assigned. 888 | usage: 889 | >>> getSolutions() 890 | >>> getSolutions("Name1") 891 | >>> getSolutions(["Name1","Name2"]) 892 | >>> getSolutions(resultfile="c:/a.mat") 893 | >>> getSolutions("Name1",resultfile=""c:/a.mat"") 894 | >>> getSolutions(["Name1","Name2"],resultfile=""c:/a.mat"") 895 | """ 896 | if resultfile is None: 897 | resFile = self.resultfile.as_posix() 898 | else: 899 | resFile = resultfile 900 | 901 | # check for result file exits 902 | if not os.path.exists(resFile): 903 | raise ModelicaSystemError(f"Result file does not exist {resFile}") 904 | resultVars = self.sendExpression(f'readSimulationResultVars("{resFile}")') 905 | self.sendExpression("closeSimulationResultFile()") 906 | if varList is None: 907 | return resultVars 908 | elif isinstance(varList, str): 909 | if varList not in resultVars and varList != "time": 910 | raise ModelicaSystemError(f"Requested data {repr(varList)} does not exist") 911 | res = self.sendExpression(f'readSimulationResult("{resFile}", {{{varList}}})') 912 | npRes = np.array(res) 913 | self.sendExpression("closeSimulationResultFile()") 914 | return npRes 915 | elif isinstance(varList, list): 916 | for var in varList: 917 | if var == "time": 918 | continue 919 | if var not in resultVars: 920 | raise ModelicaSystemError(f"Requested data {repr(var)} does not exist") 921 | variables = ",".join(varList) 922 | res = self.sendExpression(f'readSimulationResult("{resFile}",{{{variables}}})') 923 | npRes = np.array(res) 924 | self.sendExpression("closeSimulationResultFile()") 925 | return npRes 926 | 927 | raise ModelicaSystemError("Unhandled input for getSolutions()") 928 | 929 | @staticmethod 930 | def _strip_space(name): 931 | if isinstance(name, str): 932 | return name.replace(" ", "") 933 | elif isinstance(name, list): 934 | return [x.replace(" ", "") for x in name] 935 | 936 | raise ModelicaSystemError("Unhandled input for strip_space()") 937 | 938 | def setMethodHelper(self, args1, args2, args3, args4=None): 939 | """ 940 | Helper function for setParameter(),setContinuous(),setSimulationOptions(),setLinearizationOption(),setOptimizationOption() 941 | args1 - string or list of string given by user 942 | args2 - dict() containing the values of different variables(eg:, parameter,continuous,simulation parameters) 943 | args3 - function name (eg; continuous, parameter, simulation, linearization,optimization) 944 | args4 - dict() which stores the new override variables list, 945 | """ 946 | def apply_single(args1): 947 | args1 = self._strip_space(args1) 948 | value = args1.split("=") 949 | if value[0] in args2: 950 | if args3 == "parameter" and self.isParameterChangeable(value[0], value[1]): 951 | args2[value[0]] = value[1] 952 | if args4 is not None: 953 | args4[value[0]] = value[1] 954 | elif args3 != "parameter": 955 | args2[value[0]] = value[1] 956 | if args4 is not None: 957 | args4[value[0]] = value[1] 958 | 959 | return True 960 | 961 | else: 962 | raise ModelicaSystemError("Unhandled case in setMethodHelper.apply_single() - " 963 | f"{repr(value[0])} is not a {repr(args3)} variable") 964 | 965 | result = [] 966 | if isinstance(args1, str): 967 | result = [apply_single(args1)] 968 | 969 | elif isinstance(args1, list): 970 | result = [] 971 | args1 = self._strip_space(args1) 972 | for var in args1: 973 | result.append(apply_single(var)) 974 | 975 | return all(result) 976 | 977 | def setContinuous(self, cvals): # 13 978 | """ 979 | This method is used to set continuous values. It can be called: 980 | with a sequence of continuous name and assigning corresponding values as arguments as show in the example below: 981 | usage 982 | >>> setContinuous("Name=value") 983 | >>> setContinuous(["Name1=value1","Name2=value2"]) 984 | """ 985 | return self.setMethodHelper(cvals, self.continuouslist, "continuous", self.overridevariables) 986 | 987 | def setParameters(self, pvals): # 14 988 | """ 989 | This method is used to set parameter values. It can be called: 990 | with a sequence of parameter name and assigning corresponding value as arguments as show in the example below: 991 | usage 992 | >>> setParameters("Name=value") 993 | >>> setParameters(["Name1=value1","Name2=value2"]) 994 | """ 995 | return self.setMethodHelper(pvals, self.paramlist, "parameter", self.overridevariables) 996 | 997 | def isParameterChangeable(self, name, value): 998 | q = self.getQuantities(name) 999 | if q[0]["changeable"] == "false": 1000 | logger.verbose(f"setParameters() failed : It is not possible to set the following signal {repr(name)}. " 1001 | "It seems to be structural, final, protected or evaluated or has a non-constant binding, " 1002 | f"use sendExpression(\"setParameterValue({self.modelName}, {name}, {value})\") " 1003 | "and rebuild the model using buildModel() API") 1004 | return False 1005 | return True 1006 | 1007 | def setSimulationOptions(self, simOptions): # 16 1008 | """ 1009 | This method is used to set simulation options. It can be called: 1010 | with a sequence of simulation options name and assigning corresponding values as arguments as show in the example below: 1011 | usage 1012 | >>> setSimulationOptions("Name=value") 1013 | >>> setSimulationOptions(["Name1=value1","Name2=value2"]) 1014 | """ 1015 | return self.setMethodHelper(simOptions, self.simulateOptions, "simulation-option", self.simoptionsoverride) 1016 | 1017 | def setLinearizationOptions(self, linearizationOptions): # 18 1018 | """ 1019 | This method is used to set linearization options. It can be called: 1020 | with a sequence of linearization options name and assigning corresponding value as arguments as show in the example below 1021 | usage 1022 | >>> setLinearizationOptions("Name=value") 1023 | >>> setLinearizationOptions(["Name1=value1","Name2=value2"]) 1024 | """ 1025 | return self.setMethodHelper(linearizationOptions, self.linearOptions, "Linearization-option", None) 1026 | 1027 | def setOptimizationOptions(self, optimizationOptions): # 17 1028 | """ 1029 | This method is used to set optimization options. It can be called: 1030 | with a sequence of optimization options name and assigning corresponding values as arguments as show in the example below: 1031 | usage 1032 | >>> setOptimizationOptions("Name=value") 1033 | >>> setOptimizationOptions(["Name1=value1","Name2=value2"]) 1034 | """ 1035 | return self.setMethodHelper(optimizationOptions, self.optimizeOptions, "optimization-option", None) 1036 | 1037 | def setInputs(self, name): # 15 1038 | """ 1039 | This method is used to set input values. It can be called: 1040 | with a sequence of input name and assigning corresponding values as arguments as show in the example below: 1041 | usage 1042 | >>> setInputs("Name=value") 1043 | >>> setInputs(["Name1=value1","Name2=value2"]) 1044 | """ 1045 | if isinstance(name, str): 1046 | name = self._strip_space(name) 1047 | value = name.split("=") 1048 | if value[0] in self.inputlist: 1049 | tmpvalue = eval(value[1]) 1050 | if isinstance(tmpvalue, int) or isinstance(tmpvalue, float): 1051 | self.inputlist[value[0]] = [(float(self.simulateOptions["startTime"]), float(value[1])), 1052 | (float(self.simulateOptions["stopTime"]), float(value[1]))] 1053 | elif isinstance(tmpvalue, list): 1054 | self.checkValidInputs(tmpvalue) 1055 | self.inputlist[value[0]] = tmpvalue 1056 | self.inputFlag = True 1057 | else: 1058 | raise ModelicaSystemError(f"{value[0]} is not an input") 1059 | elif isinstance(name, list): 1060 | name = self._strip_space(name) 1061 | for var in name: 1062 | value = var.split("=") 1063 | if value[0] in self.inputlist: 1064 | tmpvalue = eval(value[1]) 1065 | if isinstance(tmpvalue, int) or isinstance(tmpvalue, float): 1066 | self.inputlist[value[0]] = [(float(self.simulateOptions["startTime"]), float(value[1])), 1067 | (float(self.simulateOptions["stopTime"]), float(value[1]))] 1068 | elif isinstance(tmpvalue, list): 1069 | self.checkValidInputs(tmpvalue) 1070 | self.inputlist[value[0]] = tmpvalue 1071 | self.inputFlag = True 1072 | else: 1073 | raise ModelicaSystemError(f"{value[0]} is not an input!") 1074 | 1075 | def checkValidInputs(self, name): 1076 | if name != sorted(name, key=lambda x: x[0]): 1077 | raise ModelicaSystemError('Time value should be in increasing order') 1078 | for l in name: 1079 | if isinstance(l, tuple): 1080 | # if l[0] < float(self.simValuesList[0]): 1081 | if l[0] < float(self.simulateOptions["startTime"]): 1082 | raise ModelicaSystemError('Input time value is less than simulation startTime') 1083 | if len(l) != 2: 1084 | raise ModelicaSystemError(f'Value for {l} is in incorrect format!') 1085 | else: 1086 | raise ModelicaSystemError('Error!!! Value must be in tuple format') 1087 | 1088 | def createCSVData(self) -> pathlib.Path: 1089 | start_time: float = float(self.simulateOptions["startTime"]) 1090 | stop_time: float = float(self.simulateOptions["stopTime"]) 1091 | 1092 | # Replace None inputs with a default constant zero signal 1093 | inputs: dict[str, list[tuple[float, float]]] = {} 1094 | for input_name, input_signal in self.inputlist.items(): 1095 | if input_signal is None: 1096 | inputs[input_name] = [(start_time, 0.0), (stop_time, 0.0)] 1097 | else: 1098 | inputs[input_name] = input_signal 1099 | 1100 | # Collect all unique timestamps across all input signals 1101 | all_times = np.array( 1102 | sorted({t for signal in inputs.values() for t, _ in signal}), 1103 | dtype=float 1104 | ) 1105 | 1106 | # Interpolate missing values 1107 | interpolated_inputs: dict[str, np.ndarray] = {} 1108 | for signal_name, signal_values in inputs.items(): 1109 | signal = np.array(signal_values) 1110 | interpolated_inputs[signal_name] = np.interp( 1111 | all_times, 1112 | signal[:, 0], # times 1113 | signal[:, 1] # values 1114 | ) 1115 | 1116 | # Write CSV file 1117 | input_names = list(interpolated_inputs.keys()) 1118 | header = ['time'] + input_names + ['end'] 1119 | 1120 | csv_rows = [header] 1121 | for i, t in enumerate(all_times): 1122 | row = [ 1123 | t, # time 1124 | *(interpolated_inputs[name][i] for name in input_names), # input values 1125 | 0 # trailing 'end' column 1126 | ] 1127 | csv_rows.append(row) 1128 | 1129 | csvFile = self.tempdir / f'{self.modelName}.csv' 1130 | 1131 | with open(csvFile, "w", newline="") as f: 1132 | writer = csv.writer(f) 1133 | writer.writerows(csv_rows) 1134 | 1135 | return csvFile 1136 | 1137 | # to convert Modelica model to FMU 1138 | def convertMo2Fmu(self, version="2.0", fmuType="me_cs", fileNamePrefix="", includeResources=True): # 19 1139 | """ 1140 | This method is used to generate FMU from the given Modelica model. It creates "modelName.fmu" in the current working directory. It can be called: 1141 | with no arguments 1142 | with arguments of https://build.openmodelica.org/Documentation/OpenModelica.Scripting.translateModelFMU.html 1143 | usage 1144 | >>> convertMo2Fmu() 1145 | >>> convertMo2Fmu(version="2.0", fmuType="me|cs|me_cs", fileNamePrefix="", includeResources=True) 1146 | """ 1147 | 1148 | if fileNamePrefix == "": 1149 | fileNamePrefix = self.modelName 1150 | if includeResources: 1151 | includeResourcesStr = "true" 1152 | else: 1153 | includeResourcesStr = "false" 1154 | properties = f'version="{version}", fmuType="{fmuType}", fileNamePrefix="{fileNamePrefix}", includeResources={includeResourcesStr}' 1155 | fmu = self.requestApi('buildModelFMU', self.modelName, properties) 1156 | 1157 | # report proper error message 1158 | if not os.path.exists(fmu): 1159 | raise ModelicaSystemError(f"Missing FMU file: {fmu}") 1160 | 1161 | return fmu 1162 | 1163 | # to convert FMU to Modelica model 1164 | def convertFmu2Mo(self, fmuName): # 20 1165 | """ 1166 | In order to load FMU, at first it needs to be translated into Modelica model. This method is used to generate Modelica model from the given FMU. It generates "fmuName_me_FMU.mo". 1167 | Currently, it only supports Model Exchange conversion. 1168 | usage 1169 | >>> convertFmu2Mo("c:/BouncingBall.Fmu") 1170 | """ 1171 | 1172 | fileName = self.requestApi('importFMU', fmuName) 1173 | 1174 | # report proper error message 1175 | if not os.path.exists(fileName): 1176 | raise ModelicaSystemError(f"Missing file {fileName}") 1177 | 1178 | return fileName 1179 | 1180 | # to optimize model 1181 | def optimize(self): # 21 1182 | """ 1183 | This method optimizes model according to the optimized options. It can be called: 1184 | only without any arguments 1185 | usage 1186 | >>> optimize() 1187 | """ 1188 | cName = self.modelName 1189 | properties = ','.join(f"{key}={val}" for key, val in self.optimizeOptions.items()) 1190 | self.setCommandLineOptions("-g=Optimica") 1191 | optimizeResult = self.requestApi('optimize', cName, properties) 1192 | 1193 | return optimizeResult 1194 | 1195 | def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = None, 1196 | simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, 1197 | timeout: Optional[int] = None) -> LinearizationResult: 1198 | """Linearize the model according to linearOptions. 1199 | 1200 | Args: 1201 | lintime: Override linearOptions["stopTime"] value. 1202 | simflags: A string of extra command line flags for the model 1203 | binary. - depreciated in favor of simargs 1204 | simargs: A dict with command line flags and possible options; example: "simargs={'csvInput': 'a.csv'}" 1205 | timeout: Possible timeout for the execution of OM. 1206 | 1207 | Returns: 1208 | A LinearizationResult object is returned. This allows several 1209 | uses: 1210 | * `(A, B, C, D) = linearize()` to get just the matrices, 1211 | * `result = linearize(); result.A` to get everything and access the 1212 | attributes one by one, 1213 | * `result = linearize(); A = result[0]` mostly just for backwards 1214 | compatibility, because linearize() used to return `[A, B, C, D]`. 1215 | """ 1216 | 1217 | # replacement for depreciated importlib.load_module() 1218 | def load_module_from_path(module_name, file_path): 1219 | spec = importlib.util.spec_from_file_location(module_name, file_path) 1220 | module_def = importlib.util.module_from_spec(spec) 1221 | spec.loader.exec_module(module_def) 1222 | 1223 | return module_def 1224 | 1225 | if self.xmlFile is None: 1226 | raise IOError("Linearization cannot be performed as the model is not build, " 1227 | "use ModelicaSystem() to build the model first") 1228 | 1229 | om_cmd = ModelicaSystemCmd(runpath=self.tempdir, modelname=self.modelName, timeout=timeout) 1230 | 1231 | overrideLinearFile = self.tempdir / f'{self.modelName}_override_linear.txt' 1232 | 1233 | with open(overrideLinearFile, "w") as file: 1234 | for key, value in self.overridevariables.items(): 1235 | file.write(f"{key}={value}\n") 1236 | for key, value in self.linearOptions.items(): 1237 | file.write(f"{key}={value}\n") 1238 | 1239 | om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix()) 1240 | 1241 | if self.inputFlag: 1242 | nameVal = self.getInputs() 1243 | for n in nameVal: 1244 | tupleList = nameVal.get(n) 1245 | if tupleList is not None: 1246 | for l in tupleList: 1247 | if l[0] < float(self.simulateOptions["startTime"]): 1248 | raise ModelicaSystemError('Input time value is less than simulation startTime') 1249 | self.csvFile = self.createCSVData() 1250 | om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) 1251 | 1252 | om_cmd.arg_set(key="l", val=str(lintime or self.linearOptions["stopTime"])) 1253 | 1254 | # allow runtime simulation flags from user input 1255 | if simflags is not None: 1256 | om_cmd.args_set(args=om_cmd.parse_simflags(simflags=simflags)) 1257 | 1258 | if simargs: 1259 | om_cmd.args_set(args=simargs) 1260 | 1261 | returncode = om_cmd.run() 1262 | if returncode != 0: 1263 | raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") 1264 | 1265 | self.simulationFlag = True 1266 | 1267 | # code to get the matrix and linear inputs, outputs and states 1268 | linearFile = self.tempdir / "linearized_model.py" 1269 | 1270 | # support older openmodelica versions before OpenModelica v1.16.2 where linearize() generates "linear_modelname.mo" file 1271 | if not linearFile.exists(): 1272 | linearFile = pathlib.Path(f'linear_{self.modelName}.py') 1273 | 1274 | if not linearFile.exists(): 1275 | raise ModelicaSystemError(f"Linearization failed: {linearFile} not found!") 1276 | 1277 | # this function is called from the generated python code linearized_model.py at runtime, 1278 | # to improve the performance by directly reading the matrices A, B, C and D from the julia code and avoid building the linearized modelica model 1279 | try: 1280 | # do not add the linearfile directory to path, as multiple execution of linearization will always use the first added path, instead execute the file 1281 | # https://github.com/OpenModelica/OMPython/issues/196 1282 | module = load_module_from_path(module_name="linearized_model", file_path=linearFile.as_posix()) 1283 | 1284 | result = module.linearized_model() 1285 | (n, m, p, x0, u0, A, B, C, D, stateVars, inputVars, outputVars) = result 1286 | self.linearinputs = inputVars 1287 | self.linearoutputs = outputVars 1288 | self.linearstates = stateVars 1289 | return LinearizationResult(n, m, p, A, B, C, D, x0, u0, stateVars, 1290 | inputVars, outputVars) 1291 | except ModuleNotFoundError as ex: 1292 | raise ModelicaSystemError("No module named 'linearized_model'") from ex 1293 | 1294 | def getLinearInputs(self): 1295 | """ 1296 | function which returns the LinearInputs after Linearization is performed 1297 | usage 1298 | >>> getLinearInputs() 1299 | """ 1300 | return self.linearinputs 1301 | 1302 | def getLinearOutputs(self): 1303 | """ 1304 | function which returns the LinearInputs after Linearization is performed 1305 | usage 1306 | >>> getLinearOutputs() 1307 | """ 1308 | return self.linearoutputs 1309 | 1310 | def getLinearStates(self): 1311 | """ 1312 | function which returns the LinearInputs after Linearization is performed 1313 | usage 1314 | >>> getLinearStates() 1315 | """ 1316 | return self.linearstates 1317 | -------------------------------------------------------------------------------- /OMPython/OMCSession.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Definition of an OMC session. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | __license__ = """ 9 | This file is part of OpenModelica. 10 | 11 | Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 12 | c/o Linköpings universitet, Department of Computer and Information Science, 13 | SE-58183 Linköping, Sweden. 14 | 15 | All rights reserved. 16 | 17 | THIS PROGRAM IS PROVIDED UNDER THE TERMS OF THE BSD NEW LICENSE OR THE 18 | GPL VERSION 3 LICENSE OR THE OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. 19 | ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 20 | RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, 21 | ACCORDING TO RECIPIENTS CHOICE. 22 | 23 | The OpenModelica software and the OSMC (Open Source Modelica Consortium) 24 | Public License (OSMC-PL) are obtained from OSMC, either from the above 25 | address, from the URLs: http://www.openmodelica.org or 26 | http://www.ida.liu.se/projects/OpenModelica, and in the OpenModelica 27 | distribution. GNU version 3 is obtained from: 28 | http://www.gnu.org/copyleft/gpl.html. The New BSD License is obtained from: 29 | http://www.opensource.org/licenses/BSD-3-Clause. 30 | 31 | This program is distributed WITHOUT ANY WARRANTY; without even the implied 32 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, EXCEPT AS 33 | EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE 34 | CONDITIONS OF OSMC-PL. 35 | """ 36 | 37 | import getpass 38 | import json 39 | import logging 40 | import os 41 | import pathlib 42 | import psutil 43 | import pyparsing 44 | import re 45 | import shutil 46 | import signal 47 | import subprocess 48 | import sys 49 | import tempfile 50 | import time 51 | from typing import Any, Optional 52 | import uuid 53 | import warnings 54 | import zmq 55 | 56 | # TODO: replace this with the new parser 57 | from OMPython.OMTypedParser import parseString as om_parser_typed 58 | from OMPython.OMParser import om_parser_basic 59 | 60 | # define logger using the current module name as ID 61 | logger = logging.getLogger(__name__) 62 | 63 | 64 | class DummyPopen: 65 | def __init__(self, pid): 66 | self.pid = pid 67 | self.process = psutil.Process(pid) 68 | self.returncode = 0 69 | 70 | def poll(self): 71 | return None if self.process.is_running() else True 72 | 73 | def kill(self): 74 | return os.kill(self.pid, signal.SIGKILL) 75 | 76 | def wait(self, timeout): 77 | return self.process.wait(timeout=timeout) 78 | 79 | 80 | class OMCSessionException(Exception): 81 | pass 82 | 83 | 84 | class OMCSessionCmd: 85 | 86 | def __init__(self, session: OMCSessionZMQ, readonly: bool = False): 87 | if not isinstance(session, OMCSessionZMQ): 88 | raise OMCSessionException("Invalid session definition!") 89 | self._session = session 90 | self._readonly = readonly 91 | self._omc_cache: dict[tuple[str, bool], Any] = {} 92 | 93 | def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: bool = True): 94 | 95 | if opt is None: 96 | expression = question 97 | elif isinstance(opt, list): 98 | expression = f"{question}({','.join([str(x) for x in opt])})" 99 | else: 100 | raise OMCSessionException(f"Invalid definition of options for {repr(question)}: {repr(opt)}") 101 | 102 | p = (expression, parsed) 103 | 104 | if self._readonly and question != 'getErrorString': 105 | # can use cache if readonly 106 | if p in self._omc_cache: 107 | return self._omc_cache[p] 108 | 109 | try: 110 | res = self._session.sendExpression(expression, parsed=parsed) 111 | except OMCSessionException as ex: 112 | raise OMCSessionException("OMC _ask() failed: %s (parsed=%s)", expression, parsed) from ex 113 | 114 | # save response 115 | self._omc_cache[p] = res 116 | 117 | return res 118 | 119 | # TODO: Open Modelica Compiler API functions. Would be nice to generate these. 120 | def loadFile(self, filename): 121 | return self._ask(question='loadFile', opt=[f'"{filename}"']) 122 | 123 | def loadModel(self, className): 124 | return self._ask(question='loadModel', opt=[className]) 125 | 126 | def isModel(self, className): 127 | return self._ask(question='isModel', opt=[className]) 128 | 129 | def isPackage(self, className): 130 | return self._ask(question='isPackage', opt=[className]) 131 | 132 | def isPrimitive(self, className): 133 | return self._ask(question='isPrimitive', opt=[className]) 134 | 135 | def isConnector(self, className): 136 | return self._ask(question='isConnector', opt=[className]) 137 | 138 | def isRecord(self, className): 139 | return self._ask(question='isRecord', opt=[className]) 140 | 141 | def isBlock(self, className): 142 | return self._ask(question='isBlock', opt=[className]) 143 | 144 | def isType(self, className): 145 | return self._ask(question='isType', opt=[className]) 146 | 147 | def isFunction(self, className): 148 | return self._ask(question='isFunction', opt=[className]) 149 | 150 | def isClass(self, className): 151 | return self._ask(question='isClass', opt=[className]) 152 | 153 | def isParameter(self, className): 154 | return self._ask(question='isParameter', opt=[className]) 155 | 156 | def isConstant(self, className): 157 | return self._ask(question='isConstant', opt=[className]) 158 | 159 | def isProtected(self, className): 160 | return self._ask(question='isProtected', opt=[className]) 161 | 162 | def getPackages(self, className="AllLoadedClasses"): 163 | return self._ask(question='getPackages', opt=[className]) 164 | 165 | def getClassRestriction(self, className): 166 | return self._ask(question='getClassRestriction', opt=[className]) 167 | 168 | def getDerivedClassModifierNames(self, className): 169 | return self._ask(question='getDerivedClassModifierNames', opt=[className]) 170 | 171 | def getDerivedClassModifierValue(self, className, modifierName): 172 | return self._ask(question='getDerivedClassModifierValue', opt=[className, modifierName]) 173 | 174 | def typeNameStrings(self, className): 175 | return self._ask(question='typeNameStrings', opt=[className]) 176 | 177 | def getComponents(self, className): 178 | return self._ask(question='getComponents', opt=[className]) 179 | 180 | def getClassComment(self, className): 181 | try: 182 | return self._ask(question='getClassComment', opt=[className]) 183 | except pyparsing.ParseException as ex: 184 | logger.warning("Method 'getClassComment(%s)' failed; OMTypedParser error: %s", 185 | className, ex.msg) 186 | return 'No description available' 187 | except OMCSessionException: 188 | raise 189 | 190 | def getNthComponent(self, className, comp_id): 191 | """ returns with (type, name, description) """ 192 | return self._ask(question='getNthComponent', opt=[className, comp_id]) 193 | 194 | def getNthComponentAnnotation(self, className, comp_id): 195 | return self._ask(question='getNthComponentAnnotation', opt=[className, comp_id]) 196 | 197 | def getImportCount(self, className): 198 | return self._ask(question='getImportCount', opt=[className]) 199 | 200 | def getNthImport(self, className, importNumber): 201 | # [Path, id, kind] 202 | return self._ask(question='getNthImport', opt=[className, importNumber]) 203 | 204 | def getInheritanceCount(self, className): 205 | return self._ask(question='getInheritanceCount', opt=[className]) 206 | 207 | def getNthInheritedClass(self, className, inheritanceDepth): 208 | return self._ask(question='getNthInheritedClass', opt=[className, inheritanceDepth]) 209 | 210 | def getParameterNames(self, className): 211 | try: 212 | return self._ask(question='getParameterNames', opt=[className]) 213 | except KeyError as ex: 214 | logger.warning('OMPython error: %s', ex) 215 | # FIXME: OMC returns with a different structure for empty parameter set 216 | return [] 217 | except OMCSessionException: 218 | raise 219 | 220 | def getParameterValue(self, className, parameterName): 221 | try: 222 | return self._ask(question='getParameterValue', opt=[className, parameterName]) 223 | except pyparsing.ParseException as ex: 224 | logger.warning("Method 'getParameterValue(%s, %s)' failed; OMTypedParser error: %s", 225 | className, parameterName, ex.msg) 226 | return "" 227 | except OMCSessionException: 228 | raise 229 | 230 | def getComponentModifierNames(self, className, componentName): 231 | return self._ask(question='getComponentModifierNames', opt=[className, componentName]) 232 | 233 | def getComponentModifierValue(self, className, componentName): 234 | return self._ask(question='getComponentModifierValue', opt=[className, componentName]) 235 | 236 | def getExtendsModifierNames(self, className, componentName): 237 | return self._ask(question='getExtendsModifierNames', opt=[className, componentName]) 238 | 239 | def getExtendsModifierValue(self, className, extendsName, modifierName): 240 | return self._ask(question='getExtendsModifierValue', opt=[className, extendsName, modifierName]) 241 | 242 | def getNthComponentModification(self, className, comp_id): 243 | # FIXME: OMPython exception Results KeyError exception 244 | 245 | # get {$Code(....)} field 246 | # \{\$Code\((\S*\s*)*\)\} 247 | value = self._ask(question='getNthComponentModification', opt=[className, comp_id], parsed=False) 248 | value = value.replace("{$Code(", "") 249 | return value[:-3] 250 | # return self.re_Code.findall(value) 251 | 252 | # function getClassNames 253 | # input TypeName class_ = $Code(AllLoadedClasses); 254 | # input Boolean recursive = false; 255 | # input Boolean qualified = false; 256 | # input Boolean sort = false; 257 | # input Boolean builtin = false "List also builtin classes if true"; 258 | # input Boolean showProtected = false "List also protected classes if true"; 259 | # output TypeName classNames[:]; 260 | # end getClassNames; 261 | def getClassNames(self, className=None, recursive=False, qualified=False, sort=False, builtin=False, 262 | showProtected=False): 263 | opt = [className] if className else [] + [f'recursive={str(recursive).lower()}', 264 | f'qualified={str(qualified).lower()}', 265 | f'sort={str(sort).lower()}', 266 | f'builtin={str(builtin).lower()}', 267 | f'showProtected={str(showProtected).lower()}'] 268 | return self._ask(question='getClassNames', opt=opt) 269 | 270 | 271 | class OMCSessionZMQ: 272 | 273 | def __init__(self, 274 | timeout: float = 10.00, 275 | docker: Optional[str] = None, 276 | dockerContainer: Optional[int] = None, 277 | dockerExtraArgs: Optional[list] = None, 278 | dockerOpenModelicaPath: str = "omc", 279 | dockerNetwork: Optional[str] = None, 280 | port: Optional[int] = None, 281 | omhome: Optional[str] = None): 282 | if dockerExtraArgs is None: 283 | dockerExtraArgs = [] 284 | 285 | self._omhome = self._get_omhome(omhome=omhome) 286 | 287 | self._omc_process = None 288 | self._omc_command = None 289 | self._omc: Optional[Any] = None 290 | self._dockerCid: Optional[int] = None 291 | self._serverIPAddress = "127.0.0.1" 292 | self._interactivePort = None 293 | self._temp_dir = pathlib.Path(tempfile.gettempdir()) 294 | # generate a random string for this session 295 | self._random_string = uuid.uuid4().hex 296 | try: 297 | self._currentUser = getpass.getuser() 298 | if not self._currentUser: 299 | self._currentUser = "nobody" 300 | except KeyError: 301 | # We are running as a uid not existing in the password database... Pretend we are nobody 302 | self._currentUser = "nobody" 303 | 304 | self._docker = docker 305 | self._dockerContainer = dockerContainer 306 | self._dockerExtraArgs = dockerExtraArgs 307 | self._dockerOpenModelicaPath = dockerOpenModelicaPath 308 | self._dockerNetwork = dockerNetwork 309 | self._omc_log_file = self._create_omc_log_file("port") 310 | self._timeout = timeout 311 | # Locating and using the IOR 312 | if sys.platform != 'win32' or docker or dockerContainer: 313 | port_file = "openmodelica." + self._currentUser + ".port." + self._random_string 314 | else: 315 | port_file = "openmodelica.port." + self._random_string 316 | self._port_file = ((pathlib.Path("/tmp") if docker else self._temp_dir) / port_file).as_posix() 317 | self._interactivePort = port 318 | # set omc executable path and args 319 | self._omc_command = self._set_omc_command(omc_path_and_args_list=["--interactive=zmq", 320 | "--locale=C", 321 | f"-z={self._random_string}"]) 322 | # start up omc executable, which is waiting for the ZMQ connection 323 | self._omc_process = self._start_omc_process(timeout) 324 | # connect to the running omc instance using ZMQ 325 | self._omc_port = self._connect_to_omc(timeout) 326 | 327 | self._re_log_entries = None 328 | self._re_log_raw = None 329 | 330 | def __del__(self): 331 | try: 332 | self.sendExpression("quit()") 333 | except OMCSessionException: 334 | pass 335 | self._omc_log_file.close() 336 | try: 337 | self._omc_process.wait(timeout=2.0) 338 | except subprocess.TimeoutExpired: 339 | if self._omc_process: 340 | logger.warning("OMC did not exit after being sent the quit() command; " 341 | "killing the process with pid=%s", self._omc_process.pid) 342 | self._omc_process.kill() 343 | self._omc_process.wait() 344 | 345 | def _create_omc_log_file(self, suffix): # output? 346 | if sys.platform == 'win32': 347 | log_filename = f"openmodelica.{suffix}.{self._random_string}.log" 348 | else: 349 | log_filename = f"openmodelica.{self._currentUser}.{suffix}.{self._random_string}.log" 350 | # this file must be closed in the destructor 351 | omc_log_file = open(self._temp_dir / log_filename, "w+") 352 | 353 | return omc_log_file 354 | 355 | def _start_omc_process(self, timeout): # output? 356 | if sys.platform == 'win32': 357 | omhome_bin = (self._omhome / "bin").as_posix() 358 | my_env = os.environ.copy() 359 | my_env["PATH"] = omhome_bin + os.pathsep + my_env["PATH"] 360 | omc_process = subprocess.Popen(self._omc_command, stdout=self._omc_log_file, 361 | stderr=self._omc_log_file, env=my_env) 362 | else: 363 | # set the user environment variable so omc running from wsgi has the same user as OMPython 364 | my_env = os.environ.copy() 365 | my_env["USER"] = self._currentUser 366 | omc_process = subprocess.Popen(self._omc_command, stdout=self._omc_log_file, 367 | stderr=self._omc_log_file, env=my_env) 368 | if self._docker: 369 | for i in range(0, 40): 370 | try: 371 | with open(self._dockerCidFile, "r") as fin: 372 | self._dockerCid = fin.read().strip() 373 | except IOError: 374 | pass 375 | if self._dockerCid: 376 | break 377 | time.sleep(timeout / 40.0) 378 | try: 379 | os.remove(self._dockerCidFile) 380 | except FileNotFoundError: 381 | pass 382 | if self._dockerCid is None: 383 | logger.error("Docker did not start. Log-file says:\n%s" % (open(self._omc_log_file.name).read())) 384 | raise OMCSessionException("Docker did not start (timeout=%f might be too short especially if you did " 385 | "not docker pull the image before this command)." % timeout) 386 | 387 | dockerTop = None 388 | if self._docker or self._dockerContainer: 389 | if self._dockerNetwork == "separate": 390 | output = subprocess.check_output(["docker", "inspect", self._dockerCid]).decode().strip() 391 | self._serverIPAddress = json.loads(output)[0]["NetworkSettings"]["IPAddress"] 392 | for i in range(0, 40): 393 | if sys.platform == 'win32': 394 | break 395 | dockerTop = subprocess.check_output(["docker", "top", self._dockerCid]).decode().strip() 396 | omc_process = None 397 | for line in dockerTop.split("\n"): 398 | columns = line.split() 399 | if self._random_string in line: 400 | try: 401 | omc_process = DummyPopen(int(columns[1])) 402 | except psutil.NoSuchProcess: 403 | raise OMCSessionException( 404 | f"Could not find PID {dockerTop} - is this a docker instance spawned " 405 | f"without --pid=host?\nLog-file says:\n{open(self._omc_log_file.name).read()}") 406 | break 407 | if omc_process is not None: 408 | break 409 | time.sleep(timeout / 40.0) 410 | if omc_process is None: 411 | raise OMCSessionException("Docker top did not contain omc process %s:\n%s\nLog-file says:\n%s" 412 | % (self._random_string, dockerTop, open(self._omc_log_file.name).read())) 413 | return omc_process 414 | 415 | def _getuid(self): 416 | """ 417 | The uid to give to docker. 418 | On Windows, volumes are mapped with all files are chmod ugo+rwx, 419 | so uid does not matter as long as it is not the root user. 420 | """ 421 | return 1000 if sys.platform == 'win32' else os.getuid() 422 | 423 | def _set_omc_command(self, omc_path_and_args_list) -> list: 424 | """Define the command that will be called by the subprocess module. 425 | 426 | On Windows, use the list input style of the subprocess module to 427 | avoid problems resulting from spaces in the path string. 428 | Linux, however, only works with the string version. 429 | """ 430 | if (self._docker or self._dockerContainer) and sys.platform == "win32": 431 | extraFlags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] 432 | if not self._interactivePort: 433 | raise OMCSessionException("docker on Windows requires knowing which port to connect to. For " 434 | "dockerContainer=..., the container needs to have already manually exposed " 435 | "this port when it was started (-p 127.0.0.1:n:n) or you get an error later.") 436 | else: 437 | extraFlags = [] 438 | if self._docker: 439 | if sys.platform == "win32": 440 | p = int(self._interactivePort) 441 | dockerNetworkStr = ["-p", "127.0.0.1:%d:%d" % (p, p)] 442 | elif self._dockerNetwork == "host" or self._dockerNetwork is None: 443 | dockerNetworkStr = ["--network=host"] 444 | elif self._dockerNetwork == "separate": 445 | dockerNetworkStr = [] 446 | extraFlags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] 447 | else: 448 | raise OMCSessionException('dockerNetwork was set to %s, but only \"host\" or \"separate\" is allowed') 449 | self._dockerCidFile = self._omc_log_file.name + ".docker.cid" 450 | omcCommand = (["docker", "run", 451 | "--cidfile", self._dockerCidFile, 452 | "--rm", 453 | "--env", "USER=%s" % self._currentUser, 454 | "--user", str(self._getuid())] 455 | + self._dockerExtraArgs 456 | + dockerNetworkStr 457 | + [self._docker, self._dockerOpenModelicaPath]) 458 | elif self._dockerContainer: 459 | omcCommand = (["docker", "exec", 460 | "--env", "USER=%s" % self._currentUser, 461 | "--user", str(self._getuid())] 462 | + self._dockerExtraArgs 463 | + [self._dockerContainer, self._dockerOpenModelicaPath]) 464 | self._dockerCid = self._dockerContainer 465 | else: 466 | omcCommand = [str(self._get_omc_path())] 467 | if self._interactivePort: 468 | extraFlags = extraFlags + ["--interactivePort=%d" % int(self._interactivePort)] 469 | 470 | omc_command = omcCommand + omc_path_and_args_list + extraFlags 471 | 472 | return omc_command 473 | 474 | def _get_omhome(self, omhome: Optional[str] = None): 475 | # use the provided path 476 | if omhome is not None: 477 | return pathlib.Path(omhome) 478 | 479 | # check the environment variable 480 | omhome = os.environ.get('OPENMODELICAHOME') 481 | if omhome is not None: 482 | return pathlib.Path(omhome) 483 | 484 | # Get the path to the OMC executable, if not installed this will be None 485 | path_to_omc = shutil.which("omc") 486 | if path_to_omc is not None: 487 | return pathlib.Path(path_to_omc).parents[1] 488 | 489 | raise OMCSessionException("Cannot find OpenModelica executable, please install from openmodelica.org") 490 | 491 | def _get_omc_path(self) -> pathlib.Path: 492 | return self._omhome / "bin" / "omc" 493 | 494 | def _connect_to_omc(self, timeout) -> str: 495 | omc_zeromq_uri = "file:///" + self._port_file 496 | # See if the omc server is running 497 | attempts = 0 498 | port = None 499 | while True: 500 | if self._dockerCid: 501 | try: 502 | port = subprocess.check_output(args=["docker", 503 | "exec", str(self._dockerCid), 504 | "cat", str(self._port_file)], 505 | stderr=subprocess.DEVNULL).decode().strip() 506 | break 507 | except subprocess.CalledProcessError: 508 | pass 509 | else: 510 | if os.path.isfile(self._port_file): 511 | # Read the port file 512 | with open(self._port_file, 'r') as f_p: 513 | port = f_p.readline() 514 | os.remove(self._port_file) 515 | break 516 | 517 | attempts += 1 518 | if attempts == 80.0: 519 | name = self._omc_log_file.name 520 | self._omc_log_file.close() 521 | logger.error("OMC Server did not start. Please start it! Log-file says:\n%s" % open(name).read()) 522 | raise OMCSessionException(f"OMC Server did not start (timeout={timeout}). " 523 | f"Could not open file {self._port_file}") 524 | time.sleep(timeout / 80.0) 525 | 526 | port = port.replace("0.0.0.0", self._serverIPAddress) 527 | logger.info(f"OMC Server is up and running at {omc_zeromq_uri} " 528 | f"pid={self._omc_process.pid if self._omc_process else '?'} cid={self._dockerCid}") 529 | 530 | # Create the ZeroMQ socket and connect to OMC server 531 | context = zmq.Context.instance() 532 | self._omc = context.socket(zmq.REQ) 533 | self._omc.setsockopt(zmq.LINGER, 0) # Dismisses pending messages if closed 534 | self._omc.setsockopt(zmq.IMMEDIATE, True) # Queue messages only to completed connections 535 | self._omc.connect(port) 536 | 537 | return port 538 | 539 | def execute(self, command): 540 | warnings.warn("This function is depreciated and will be removed in future versions; " 541 | "please use sendExpression() instead", DeprecationWarning, stacklevel=1) 542 | 543 | return self.sendExpression(command, parsed=False) 544 | 545 | def sendExpression(self, command, parsed=True): 546 | p = self._omc_process.poll() # check if process is running 547 | if p is not None: 548 | raise OMCSessionException("Process Exited, No connection with OMC. Create a new instance of OMCSessionZMQ!") 549 | 550 | if self._omc is None: 551 | raise OMCSessionException("No OMC running. Create a new instance of OMCSessionZMQ!") 552 | 553 | logger.debug("sendExpression(%r, parsed=%r)", command, parsed) 554 | 555 | attempts = 0 556 | while True: 557 | try: 558 | self._omc.send_string(str(command), flags=zmq.NOBLOCK) 559 | break 560 | except zmq.error.Again: 561 | pass 562 | attempts += 1 563 | if attempts >= 50: 564 | self._omc_log_file.seek(0) 565 | log = self._omc_log_file.read() 566 | self._omc_log_file.close() 567 | raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}). Log-file says: \n{log}") 568 | time.sleep(self._timeout / 50.0) 569 | if command == "quit()": 570 | self._omc.close() 571 | self._omc = None 572 | return None 573 | else: 574 | result = self._omc.recv_string() 575 | 576 | if command == "getErrorString()": 577 | # no error handling if 'getErrorString()' is called 578 | pass 579 | elif command == "getMessagesStringInternal()": 580 | # no error handling if 'getMessagesStringInternal()' is called; parsing NOT possible! 581 | if parsed: 582 | logger.warning("Result of 'getMessagesStringInternal()' cannot be parsed - set parsed to False!") 583 | parsed = False 584 | else: 585 | # allways check for error 586 | self._omc.send_string('getMessagesStringInternal()', flags=zmq.NOBLOCK) 587 | error_raw = self._omc.recv_string() 588 | # run error handling only if there is something to check 589 | if error_raw != "{}\n": 590 | if not self._re_log_entries: 591 | self._re_log_entries = re.compile(pattern=r'record OpenModelica\.Scripting\.ErrorMessage' 592 | '(.*?)' 593 | r'end OpenModelica\.Scripting\.ErrorMessage;', 594 | flags=re.MULTILINE | re.DOTALL) 595 | if not self._re_log_raw: 596 | self._re_log_raw = re.compile( 597 | pattern=r"\s+message = \"(.*?)\",\n" # message 598 | r"\s+kind = .OpenModelica.Scripting.ErrorKind.(.*?),\n" # kind 599 | r"\s+level = .OpenModelica.Scripting.ErrorLevel.(.*?),\n" # level 600 | r"\s+id = (.*?)" # id 601 | "(,\n|\n)", # end marker 602 | flags=re.MULTILINE | re.DOTALL) 603 | 604 | # extract all ErrorMessage records 605 | log_entries = self._re_log_entries.findall(string=error_raw) 606 | for log_entry in reversed(log_entries): 607 | log_raw = self._re_log_raw.findall(string=log_entry) 608 | if len(log_raw) != 1 or len(log_raw[0]) != 5: 609 | logger.warning("Invalid ErrorMessage record returned by 'getMessagesStringInternal()':" 610 | f" {repr(log_entry)}!") 611 | 612 | log_message = log_raw[0][0].encode().decode('unicode_escape') 613 | log_kind = log_raw[0][1] 614 | log_level = log_raw[0][2] 615 | log_id = log_raw[0][3] 616 | 617 | msg = (f"[OMC log for 'sendExpression({command}, {parsed})']: " 618 | f"[{log_kind}:{log_level}:{log_id}] {log_message}") 619 | 620 | # response according to the used log level 621 | # see: https://build.openmodelica.org/Documentation/OpenModelica.Scripting.ErrorLevel.html 622 | if log_level == 'error': 623 | raise OMCSessionException(msg) 624 | elif log_level == 'warning': 625 | logger.warning(msg) 626 | elif log_level == 'notification': 627 | logger.info(msg) 628 | else: # internal 629 | logger.debug(msg) 630 | 631 | if parsed is True: 632 | try: 633 | return om_parser_typed(result) 634 | except pyparsing.ParseException as ex: 635 | logger.warning('OMTypedParser error: %s. Returning the basic parser result.', ex.msg) 636 | try: 637 | return om_parser_basic(result) 638 | except (TypeError, UnboundLocalError) as ex: 639 | raise OMCSessionException("Cannot parse OMC result") from ex 640 | else: 641 | return result 642 | -------------------------------------------------------------------------------- /OMPython/OMParser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | This file is part of OpenModelica. 5 | 6 | Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 7 | c/o Linköpings universitet, Department of Computer and Information Science, 8 | SE-58183 Linköping, Sweden. 9 | 10 | All rights reserved. 11 | 12 | THIS PROGRAM IS PROVIDED UNDER THE TERMS OF THE BSD NEW LICENSE OR THE 13 | GPL VERSION 3 LICENSE OR THE OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. 14 | ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 15 | RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, 16 | ACCORDING TO RECIPIENTS CHOICE. 17 | 18 | The OpenModelica software and the OSMC (Open Source Modelica Consortium) 19 | Public License (OSMC-PL) are obtained from OSMC, either from the above 20 | address, from the URLs: http://www.openmodelica.org or 21 | http://www.ida.liu.se/projects/OpenModelica, and in the OpenModelica 22 | distribution. GNU version 3 is obtained from: 23 | http://www.gnu.org/copyleft/gpl.html. The New BSD License is obtained from: 24 | http://www.opensource.org/licenses/BSD-3-Clause. 25 | 26 | This program is distributed WITHOUT ANY WARRANTY; without even the implied 27 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, EXCEPT AS 28 | EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE 29 | CONDITIONS OF OSMC-PL. 30 | 31 | Author : Anand Kalaiarasi Ganeson, ganan642@student.liu.se, 2012-03-19 32 | Version: 1.0 33 | """ 34 | 35 | import sys 36 | from typing import Dict, Any 37 | 38 | result: Dict[str, Any] = dict() 39 | 40 | inner_sets = [] 41 | next_set_list = [] 42 | next_set = [] 43 | next_set.append('') 44 | 45 | 46 | def bool_from_string(string): 47 | """Attempt conversion of string to a boolean """ 48 | if string in {'true', 'True', 'TRUE'}: 49 | return True 50 | elif string in {'false', 'False', 'FALSE'}: 51 | return False 52 | else: 53 | raise ValueError 54 | 55 | 56 | def typeCheck(string): 57 | """Attempt conversion of string to a usable value""" 58 | types = [bool_from_string, int, float, dict, str] 59 | 60 | if type(string) in {int, float}: 61 | return string 62 | 63 | string = string.strip() 64 | 65 | for t in types: 66 | try: 67 | return t(string) 68 | except ValueError: 69 | continue 70 | else: 71 | print("String contains un-handled datatype") 72 | return string 73 | 74 | 75 | def make_values(strings, name): 76 | if strings[0] == "(" and strings[-1] == ")": 77 | strings = strings[1:-1] 78 | if strings[0] == "{" and strings[-1] == "}": 79 | strings = strings[1:-1] 80 | 81 | # find the highest Set number of SET 82 | for each_name in result: 83 | if each_name.find("SET") != -1: 84 | main_set_name = each_name 85 | 86 | if strings[0] == "\"" and strings[-1] == "\"": 87 | strings = strings.replace("\\\"", "\"") 88 | result[main_set_name]['Values'] = [] 89 | result[main_set_name]['Values'].append(strings) 90 | else: 91 | anchor = 0 92 | position = 0 93 | stop = 0 94 | prop_str = strings 95 | main_set_name = "SET1" 96 | 97 | # remove braces & keep only the SET's values 98 | while position < len(prop_str): 99 | check = prop_str[position] 100 | if check == "{": 101 | anchor = position 102 | elif check == "}": 103 | stop = position 104 | delStr = prop_str[anchor:stop + 1] 105 | 106 | i = anchor 107 | while i > 0: 108 | text = prop_str[i] 109 | if text == ",": 110 | name_start = i + 1 111 | break 112 | i -= 1 113 | name_of_set = prop_str[name_start:anchor] 114 | if name_of_set.find("=") == -1: 115 | prop_str = prop_str.replace(delStr, '').strip() 116 | position = 0 117 | 118 | position += 1 119 | 120 | for each_name in result: 121 | if each_name.find("SET") != -1: 122 | main_set_name = each_name 123 | 124 | values = [] 125 | anchor = 0 126 | brace_count = 0 127 | for i, c in enumerate(prop_str): 128 | if c == "," and brace_count == 0: 129 | value = prop_str[anchor:i] 130 | value = (value.lstrip()).rstrip() 131 | if "=" in value: 132 | result[main_set_name]['Elements'][name]['Properties']['Results'] = {} 133 | else: 134 | result[main_set_name]['Elements'][name]['Properties']['Values'] = [] 135 | values.append(value) 136 | anchor = i + 1 137 | elif c == "{": 138 | brace_count += 1 139 | elif c == "}": 140 | brace_count -= 1 141 | 142 | if i == len(prop_str) - 1: 143 | values.append(((prop_str[anchor:i + 1]).lstrip()).rstrip()) 144 | 145 | for each_val in values: 146 | multiple_values = [] 147 | if "=" in each_val: 148 | pos = each_val.find("=") 149 | varName = each_val[0:pos] 150 | varName = typeCheck(varName) 151 | varValue = each_val[pos + 1:len(each_val)] 152 | if varValue != "": 153 | varValue = typeCheck(varValue) 154 | else: 155 | varName = "" 156 | varValue = each_val 157 | if varValue != "": 158 | varValue = typeCheck(varValue) 159 | 160 | if isinstance(varValue, str) and "," in varValue: 161 | varValue = (varValue.replace('{', '').strip()).replace('}', '').strip() 162 | multiple_values = varValue.split(",") 163 | 164 | for n in range(len(multiple_values)): 165 | each_v = multiple_values[n] 166 | multiple_values.pop(n) 167 | each_v = typeCheck(each_v) 168 | multiple_values.append(each_v) 169 | 170 | if len(multiple_values) != 0: 171 | result[main_set_name]['Elements'][name]['Properties']['Results'][varName] = multiple_values 172 | elif varName != "" and varValue != "": 173 | result[main_set_name]['Elements'][name]['Properties']['Results'][varName] = varValue 174 | else: 175 | if varValue != "": 176 | result[main_set_name]['Elements'][name]['Properties']['Values'].append(varValue) 177 | 178 | 179 | def delete_elements(strings): 180 | index = 0 181 | while index < len(strings): 182 | character = strings[index] 183 | # handle data within the parenthesis () 184 | if character == "(": 185 | pos = index 186 | while pos > 0: 187 | char = strings[pos] 188 | if char == "": 189 | break 190 | elif char == ",": 191 | break 192 | elif char == " ": 193 | pos = pos + 1 194 | break 195 | elif char == "{": 196 | break 197 | pos = pos - 1 198 | delStr = strings[pos: strings.rfind(")")] 199 | strings = strings.replace(delStr, '').strip() 200 | strings = ''.join(c for c in strings if c not in '{}''()') 201 | index += 1 202 | return strings 203 | 204 | 205 | def make_subset_sets(strings, name): 206 | main_set_name = "SET1" 207 | subset_name = "Subset1" 208 | set_name = "Set1" 209 | 210 | set_list = strings.split(",") 211 | items = [] 212 | 213 | # make the values list, first 214 | for each_item in set_list: 215 | each_item = ''.join(c for c in each_item if c not in '{}') 216 | each_item = typeCheck(each_item) 217 | items.append(each_item) 218 | 219 | if "SET" in name: 220 | # find the highest SET number 221 | for each_name in result: 222 | if each_name.find("SET") != -1: 223 | main_set_name = each_name 224 | 225 | # find the highest Subset number 226 | for each_name in result[main_set_name]: 227 | if each_name.find("Subset") != -1: 228 | subset_name = each_name 229 | 230 | highest_count = 1 231 | # find the highest Set number & make the next Set in Subset 232 | for each_name in result[main_set_name][subset_name]: 233 | if each_name.find("Set") != -1: 234 | the_num = each_name.replace('Set', '') 235 | the_num = int(the_num) 236 | if the_num > highest_count: 237 | highest_count = the_num 238 | the_num += 1 239 | elif highest_count > the_num: 240 | the_num = highest_count + 1 241 | else: 242 | the_num += 1 243 | set_name = 'Set' + str(the_num) 244 | 245 | result[main_set_name][subset_name] = {} 246 | result[main_set_name][subset_name][set_name] = [] 247 | result[main_set_name][subset_name][set_name] = items 248 | 249 | else: 250 | for each_name in result: 251 | if each_name.find("SET") != -1: 252 | main_set_name = each_name 253 | 254 | if "Subset1" not in result[main_set_name]['Elements'][name]['Properties']: 255 | result[main_set_name]['Elements'][name]['Properties'][subset_name] = {} 256 | 257 | for each_name in result[main_set_name]['Elements'][name]['Properties']: 258 | if each_name.find("Subset") != -1: 259 | subset_name = each_name 260 | 261 | highest_count = 1 262 | for each_name in result[main_set_name]['Elements'][name]['Properties'][subset_name]: 263 | if each_name.find("Set") != -1: 264 | the_num = each_name.replace('Set', '') 265 | the_num = int(the_num) 266 | if the_num > highest_count: 267 | highest_count = the_num 268 | the_num += 1 269 | elif highest_count > the_num: 270 | the_num = highest_count + 1 271 | else: 272 | the_num += 1 273 | set_name = 'Set' + str(the_num) 274 | 275 | result[main_set_name]['Elements'][name]['Properties'][subset_name][set_name] = [] 276 | result[main_set_name]['Elements'][name]['Properties'][subset_name][set_name] = items 277 | 278 | 279 | def make_sets(strings, name): 280 | if strings == "{}": 281 | return 282 | main_set_name = "SET1" 283 | set_name = "Set1" 284 | 285 | if strings[0] == "{" and strings[-1] == "}": 286 | strings = strings[1:-1] 287 | 288 | set_list = strings.split(",") 289 | items = [] 290 | 291 | for each_item in set_list: 292 | each_item = typeCheck(each_item) 293 | if isinstance(each_item, str): 294 | each_item = (each_item.lstrip()).rstrip() 295 | items.append(each_item) 296 | 297 | for each_name in result: 298 | if each_name.find("SET") != -1: 299 | main_set_name = each_name 300 | 301 | if "SET" in name: 302 | highest_count = 1 303 | for each_name in result[main_set_name]: 304 | if each_name.find("Set") != -1: 305 | the_num = each_name.replace('Set', '') 306 | the_num = int(the_num) 307 | if the_num > highest_count: 308 | highest_count = the_num 309 | the_num += 1 310 | elif highest_count > the_num: 311 | the_num = highest_count + 1 312 | else: 313 | the_num += 1 314 | set_name = 'Set' + str(the_num) 315 | 316 | result[main_set_name][set_name] = [] 317 | result[main_set_name][set_name] = items 318 | 319 | else: 320 | highest_count = 1 321 | for each_name in result[main_set_name]['Elements'][name]['Properties']: 322 | if each_name.find("Set") != -1: 323 | the_num = each_name.replace('Set', '') 324 | the_num = int(the_num) 325 | if the_num > highest_count: 326 | highest_count = the_num 327 | the_num += 1 328 | elif highest_count > the_num: 329 | the_num = highest_count + 1 330 | else: 331 | the_num += 1 332 | set_name = 'Set' + str(the_num) 333 | result[main_set_name]['Elements'][name]['Properties'][set_name] = [] 334 | result[main_set_name]['Elements'][name]['Properties'][set_name] = items 335 | 336 | 337 | def get_inner_sets(strings, for_this, name): 338 | start = 0 339 | end = 0 340 | main_set_name = "SET1" 341 | subset_name = "Subset1" 342 | 343 | if "{{" in strings: 344 | for each_name in result: 345 | if each_name.find("SET") != -1: 346 | main_set_name = each_name 347 | if "SET" in name: 348 | highest_count = 1 349 | for each_name in result[main_set_name]: 350 | if each_name.find("Subset") != -1: 351 | the_num = each_name.replace('Subset', '') 352 | the_num = int(the_num) 353 | if the_num > highest_count: 354 | highest_count = the_num 355 | the_num += 1 356 | elif highest_count > the_num: 357 | the_num = highest_count + 1 358 | else: 359 | the_num += 1 360 | subset_name = subset_name + str(the_num) 361 | result[main_set_name][subset_name] = {} 362 | else: 363 | highest_count = 1 364 | for each_name in result[main_set_name]['Elements'][name]['Properties']: 365 | if each_name.find("Subset") != -1: 366 | the_num = each_name.replace('Subset', '') 367 | the_num = int(the_num) 368 | if the_num > highest_count: 369 | highest_count = the_num 370 | the_num += 1 371 | elif highest_count > the_num: 372 | the_num = highest_count + 1 373 | else: 374 | the_num += 1 375 | subset_name = "Subset" + str(the_num) 376 | result[main_set_name]['Elements'][name]['Properties'][subset_name] = {} 377 | 378 | start = strings.find("{{") 379 | end = strings.find("}}") 380 | sets = strings[start + 1:end + 1] 381 | index = 0 382 | while index < len(sets): 383 | inner_set_start = sets.find("{") 384 | if inner_set_start != -1: 385 | inner_set_end = sets.find("}") 386 | inner_set = sets[inner_set_start:inner_set_end + 1] 387 | sets = sets.replace(inner_set, '') 388 | index = 0 389 | make_subset_sets(inner_set, name) 390 | index += 1 391 | elif "{" in strings: 392 | position = 0 393 | b_count = 0 394 | while position < len(strings): 395 | character = strings[position] 396 | if character == "{": 397 | b_count += 1 398 | if b_count == 1: 399 | mark_start = position 400 | elif character == "}": 401 | b_count -= 1 402 | if b_count == 0: 403 | mark_end = position + 1 404 | sets = strings[mark_start:mark_end] 405 | make_sets(sets, name) 406 | position += 1 407 | 408 | 409 | def make_elements(strings): 410 | index = 0 411 | main_set_name = "SET1" 412 | 413 | while index < len(strings): 414 | character = strings[index] 415 | if character == "(": 416 | pos = index - 1 417 | while pos > 0: 418 | char = strings[pos] 419 | if char.isalnum(): 420 | begin = pos 421 | pos = pos - 1 422 | else: 423 | break 424 | 425 | name = strings[begin:index] 426 | index = pos 427 | original_name = name 428 | name = name + str(1) 429 | 430 | for each_name in result: 431 | if each_name.find("SET") != -1: 432 | main_set_name = each_name 433 | 434 | highest_count = 1 435 | for each_name in result[main_set_name]['Elements']: 436 | if original_name in each_name: 437 | the_num = each_name.replace(original_name, '') 438 | the_num = int(the_num) 439 | if the_num > highest_count: 440 | highest_count = the_num 441 | the_num += 1 442 | elif highest_count > the_num: 443 | the_num = highest_count + 1 444 | else: 445 | the_num += 1 446 | name = original_name + str(the_num) 447 | 448 | result[main_set_name]['Elements'][name] = {} 449 | result[main_set_name]['Elements'][name]['Properties'] = {} 450 | 451 | brace_count = 0 452 | 453 | skip_brace = 0 454 | while index < len(strings): 455 | character = strings[index] 456 | if character == "(": 457 | brace_count += 1 458 | if brace_count == 1: 459 | mark_start = index 460 | elif character == ")": 461 | brace_count -= 1 462 | mark_end = index + 1 463 | if brace_count == 0: 464 | mark_end = index + 1 465 | index += 1 466 | break 467 | elif character == "=": 468 | skip_start = index + 1 469 | if strings[skip_start] == "{": 470 | skip_brace += 1 471 | indx = skip_start 472 | while indx < len(strings): 473 | char = strings[indx] 474 | if char == "}": 475 | skip_brace -= 1 476 | if skip_brace == 0: 477 | index = indx + 1 478 | break 479 | indx += 1 480 | 481 | index += 1 482 | 483 | element_str = strings[mark_start:mark_end] 484 | del_element_str = original_name + element_str 485 | strings = strings.replace(del_element_str, '').strip() 486 | 487 | index = 0 488 | start = 0 489 | end = 0 490 | position = 0 491 | while position < len(element_str): 492 | char = element_str[position] 493 | if char == "{" and element_str[position + 1] == "{": 494 | start = position - 1 495 | end = element_str.find("}}") 496 | sets = element_str[start:end + 2] 497 | position = position + len(sets) 498 | element_str = element_str.replace(sets, '') 499 | position = 0 500 | if len(sets) > 1: 501 | get_inner_sets(sets, "Subset", name) 502 | elif char == "{": 503 | start = position 504 | end = element_str.find("}") 505 | sets = element_str[start:end + 1] 506 | 507 | i = start 508 | while i > 0: 509 | text = element_str[i] 510 | if text == ",": 511 | name_start = i + 1 512 | break 513 | i -= 1 514 | name_of_set = element_str[name_start:start] 515 | if name_of_set.find("=") == -1: 516 | element_str = element_str.replace(element_str[start:end + 1], '').strip() 517 | position = 0 518 | if len(sets) > 1: 519 | get_inner_sets(sets, "Set", name) 520 | position += 1 521 | make_values(element_str, name) 522 | index += 1 523 | 524 | 525 | def check_for_next_string(next_string): 526 | anchorr = 0 527 | positionn = 0 528 | stopp = 0 529 | 530 | # remove braces & keep only the SET's values 531 | while positionn < len(next_string): 532 | check_str = next_string[positionn] 533 | if check_str == "{": 534 | anchorr = positionn 535 | elif check_str == "}": 536 | stopp = positionn 537 | delStr = next_string[anchorr:stopp + 1] 538 | next_string = next_string.replace(delStr, '') 539 | positionn = -1 540 | positionn += 1 541 | 542 | if isinstance(next_string, str): 543 | if len(next_string) == 0: 544 | next_set = "" 545 | return next_set 546 | 547 | 548 | def get_the_set(string): 549 | 550 | def skip_all_inner_sets(position): 551 | position += 1 552 | count = 1 553 | main_count = 1 554 | max_count = main_count 555 | last_set = 0 556 | last_subset = 0 557 | pos = position 558 | 559 | while pos < len(string): 560 | charac = string[pos] 561 | if charac == "{": 562 | count += 1 563 | max_count = count 564 | elif charac == "}": 565 | count -= 1 566 | if count == 0: 567 | end_of_main_set = pos 568 | break 569 | pos += 1 570 | if count != 0: 571 | print("\nParser Error: Are you missing one or more '}'s? \n") 572 | sys.exit(1) 573 | 574 | if max_count >= 2: 575 | while position < end_of_main_set: 576 | brace_count = 0 577 | char = string[position] 578 | if char == "{" and string[position + 1] != "{": 579 | start = position 580 | main_count += 1 581 | if main_count >= 2: 582 | mark_index = position 583 | 584 | b_count = 0 585 | while position < len(string): 586 | ch = string[position] 587 | if ch == "{": 588 | main_count += 1 589 | b_count += 1 590 | elif ch == "}": 591 | b_count -= 1 592 | if b_count == 0: 593 | if main_count <= 2: 594 | skip = position + 1 595 | last_set = skip 596 | inner_sets.append(string[start:skip]) 597 | elif main_count > 2: 598 | skip = position + 1 599 | last_set = skip 600 | position = skip 601 | next_set_list.append(string[mark_index:skip]) 602 | if next_set[0] == '': 603 | next_set[0] = string[mark_index:skip] 604 | else: 605 | next_set[0] = next_set[0] + string[mark_index:skip] 606 | break 607 | elif ch == "(": 608 | brace_count += 1 609 | position += 1 610 | while position < end_of_main_set: 611 | s = string[position] 612 | if s == "(": 613 | brace_count += 1 614 | elif s == ")": 615 | brace_count -= 1 616 | if brace_count == 0: 617 | break 618 | elif s == "=" and string[position + 1] == "{": 619 | indx = position + 2 620 | skip_brace = 1 621 | while indx < end_of_main_set: 622 | char = string[indx] 623 | if char == "}": 624 | skip_brace -= 1 625 | if skip_brace == 0: 626 | position = indx + 1 627 | break 628 | indx += 1 629 | position += 1 630 | position += 1 631 | elif char == "{" and string[position + 1] == "{": 632 | start = position 633 | main_count += 1 634 | if main_count >= 2: 635 | mark_index = position 636 | position += 1 637 | b_count = 1 638 | 639 | while position < len(string): 640 | ch = string[position] 641 | if ch == "{": 642 | main_count += 1 643 | b_count += 1 644 | elif ch == "}": 645 | b_count -= 1 646 | if b_count == 0: 647 | if main_count <= 3: 648 | skip = position + 1 649 | last_subset = skip 650 | inner_sets.append(string[start:skip]) 651 | elif main_count > 3: 652 | skip = position + 1 653 | last_subset = skip 654 | position = skip 655 | next_set_list.append(string[mark_index:skip]) 656 | if next_set[0] == '': 657 | next_set[0] = string[mark_index:skip] 658 | else: 659 | next_set[0] = next_set[0] + string[mark_index:skip] 660 | break 661 | elif ch == "(": 662 | brace_count += 1 663 | position += 1 664 | while position < end_of_main_set: 665 | s = string[position] 666 | if s == "(": 667 | brace_count += 1 668 | elif s == ")": 669 | brace_count -= 1 670 | if brace_count == 0: 671 | break 672 | position += 1 673 | position += 1 674 | elif char == "(": 675 | brace_count += 1 676 | position += 1 677 | while position < end_of_main_set: 678 | s = string[position] 679 | if s == "(": 680 | brace_count += 1 681 | elif s == ")": 682 | brace_count -= 1 683 | if brace_count == 0: 684 | break 685 | position += 1 686 | 687 | position += 1 688 | else: 689 | next_set[0] = "" 690 | return (len(string) - 1) 691 | 692 | max_of_sets = max(last_set, last_subset) 693 | max_of_main_set = max(max_of_sets, last_subset) 694 | 695 | if max_of_main_set != 0: 696 | return max_of_main_set 697 | else: 698 | return (len(string) - 1) 699 | 700 | # Main entry of get_the_string() 701 | index = 0 702 | count = 0 703 | next_set[0] = '' 704 | inner_sets = [] 705 | next_set_list = [] 706 | end = len(string) 707 | 708 | if "{" and "}" in string: 709 | while index < len(string): 710 | character = string[index] 711 | if character == "{": 712 | count += 1 713 | if count == 1: 714 | anchor = index 715 | index = skip_all_inner_sets(index) 716 | if index == (len(string) - 1): 717 | if string[index] == "}": 718 | count -= 1 719 | end = index 720 | elif character == "}": 721 | count -= 1 722 | if count == 0: 723 | end = index 724 | index += 1 725 | main_set = string[anchor:end + 1] 726 | current_set = main_set 727 | 728 | if next_set[0] != "": 729 | for each_next in next_set_list: 730 | current_set = current_set.replace(each_next, '').strip() 731 | 732 | pos = 0 733 | # remove unwanted commas from CS 734 | while pos < len(current_set): 735 | char = current_set[pos] 736 | if char == ",": 737 | if current_set[pos + 1] == "}": 738 | current_set = current_set[0:pos] + current_set[pos + 1:(len(current_set))] 739 | pos = 0 740 | pos += 1 741 | 742 | check_string = ''.join(e for e in current_set if e.isalnum()) 743 | 744 | if len(check_string) > 0: 745 | return current_set, next_set[0] 746 | else: 747 | current_set = "" 748 | return current_set, next_set[0] 749 | else: 750 | return current_set, next_set[0] 751 | else: 752 | print("\nThe following String has no {}s to proceed\n") 753 | print(string) 754 | 755 | # End of get_the_string() 756 | 757 | # String parsing function for SimulationResults 758 | 759 | 760 | def formatSimRes(strings): 761 | result['SimulationResults'] = {} 762 | simRes = strings[strings.find(' resultFile') + 1:strings.find('\nend SimulationResult')] 763 | simRes = simRes.translate(None, "\\") 764 | simRes = simRes.split('\n') 765 | simOps = simRes.pop(1) 766 | options = simOps[simOps.find('"startTime') + 1:simOps.find('",')] 767 | options = options + "," 768 | index = 0 769 | anchor = 0 770 | 771 | for i in simRes: 772 | var = i[i.find('') + 1:i.find(" =")] 773 | var = (var.lstrip()).rstrip() 774 | value = i[i.find("= ") + 1:i.find(",")] 775 | value = (value.lstrip()).rstrip() 776 | value = typeCheck(value) 777 | result['SimulationResults'][var] = value 778 | 779 | result['SimulationOptions'] = {} 780 | 781 | while index < len(options): 782 | update = False 783 | character = options[index] 784 | if character == "=": 785 | opVar = options[anchor:index] 786 | opVar = (opVar.lstrip()).rstrip() 787 | anchor = index + 1 788 | update = False 789 | elif character == ",": 790 | opVal = options[anchor:index] 791 | opVal = (opVal.lstrip()).rstrip() 792 | anchor = index + 1 793 | update = True 794 | index = index + 1 795 | if update: 796 | opVal = typeCheck(opVal) 797 | result['SimulationOptions'][opVar] = opVal 798 | 799 | # string parsing function for Record types 800 | 801 | 802 | def formatRecords(strings): 803 | result['RecordResults'] = {} 804 | recordName = strings[strings.find("record ") + 1:strings.find("\n")] 805 | recordName = recordName.replace("ecord ", '').strip() 806 | strings = strings.replace(("end " + recordName + ";"), '').strip() 807 | recordItems = strings[strings.find("\n") + 1: len(strings)] 808 | recordItems = recordItems.translate(None, "\\") 809 | recordItems = recordItems.split("\n") 810 | for each_item in recordItems: 811 | var = each_item[each_item.find('') + 1:each_item.find(" =")] 812 | var = (var.lstrip()).rstrip() 813 | value = each_item[each_item.find("= ") + 1:each_item.find(",")] 814 | value = (value.lstrip()).rstrip() 815 | value = typeCheck(value) 816 | if var != "": 817 | result['RecordResults'][var] = value 818 | result['RecordResults']['RecordName'] = recordName 819 | 820 | 821 | # Main entry to the OMParser module 822 | 823 | 824 | def check_for_values(string): 825 | main_set_name = "SET1" 826 | if len(string) == 0: 827 | return result 828 | 829 | # changing untyped results to typed results 830 | if string[0] == "(": 831 | string = "{" + string[1:-2] + "}" 832 | 833 | if string[0] == "\"": 834 | string = string.replace("\\\"", "\"") 835 | string = string.replace("\\?", "?") 836 | string = string.replace("\\'", "'") 837 | return string 838 | 839 | if "record SimulationResult" in string: 840 | formatSimRes(string) 841 | return result 842 | elif "record " in string: 843 | formatRecords(string) 844 | return result 845 | 846 | string = typeCheck(string) 847 | 848 | if not isinstance(string, str): 849 | return string 850 | elif string.find("{") == -1: 851 | return string 852 | 853 | current_set, next_set = get_the_set(string) 854 | 855 | for each_name in result: 856 | if each_name.find("SET") != -1: 857 | the_num = each_name.replace("SET", '') 858 | the_num = int(the_num) 859 | the_num = the_num + 1 860 | main_set_name = "SET" + str(the_num) 861 | 862 | result[main_set_name] = {} 863 | 864 | if current_set != "": 865 | if current_set[1] == "\"" and current_set[-2] == "\"": 866 | make_values(current_set, "SET") 867 | current_set = "" 868 | check_for_next_iteration = ''.join(e for e in next_set if e.isalnum()) 869 | if len(check_for_next_iteration) > 0: 870 | check_for_values(next_set) 871 | 872 | elif "(" in current_set: 873 | for each_name in result: 874 | if each_name.find("SET") != -1: 875 | main_set_name = each_name 876 | result[main_set_name]['Elements'] = {} 877 | 878 | make_elements(current_set) 879 | current_set = delete_elements(current_set) 880 | 881 | if "{{" in current_set: 882 | get_inner_sets(current_set, "Subset", main_set_name) 883 | 884 | if "{" in current_set: 885 | get_inner_sets(current_set, "Set", main_set_name) 886 | 887 | check_for_next_iteration = ''.join(e for e in next_set if e not in {""}) 888 | if len(check_for_next_iteration) > 0: 889 | check_for_values(next_set) 890 | else: 891 | check_for_next_iteration = ''.join(e for e in next_set if e.isalnum()) 892 | if len(check_for_next_iteration) > 0: 893 | check_for_values(next_set) 894 | 895 | return result 896 | 897 | 898 | # TODO: hack to be able to use one entry point which also resets the (global) variable results 899 | # this should be checked such that the content of this file can be used as class with correct handling of 900 | # variable usage 901 | def om_parser_basic(string: str): 902 | result_return = check_for_values(string=string) 903 | 904 | global result 905 | result = {} 906 | 907 | return result_return 908 | -------------------------------------------------------------------------------- /OMPython/OMTypedParser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Anand Kalaiarasi Ganeson, ganan642@student.liu.se, 2012-03-19, and Martin Sjölund" 4 | __license__ = """ 5 | This file is part of OpenModelica. 6 | 7 | Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 8 | c/o Linköpings universitet, Department of Computer and Information Science, 9 | SE-58183 Linköping, Sweden. 10 | 11 | All rights reserved. 12 | 13 | THIS PROGRAM IS PROVIDED UNDER THE TERMS OF THE BSD NEW LICENSE OR THE 14 | GPL VERSION 3 LICENSE OR THE OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. 15 | ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 16 | RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, 17 | ACCORDING TO RECIPIENTS CHOICE. 18 | 19 | The OpenModelica software and the OSMC (Open Source Modelica Consortium) 20 | Public License (OSMC-PL) are obtained from OSMC, either from the above 21 | address, from the URLs: http://www.openmodelica.org or 22 | http://www.ida.liu.se/projects/OpenModelica, and in the OpenModelica 23 | distribution. GNU version 3 is obtained from: 24 | http://www.gnu.org/copyleft/gpl.html. The New BSD License is obtained from: 25 | http://www.opensource.org/licenses/BSD-3-Clause. 26 | 27 | This program is distributed WITHOUT ANY WARRANTY; without even the implied 28 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, EXCEPT AS 29 | EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE 30 | CONDITIONS OF OSMC-PL. 31 | """ 32 | __status__ = "Prototype" 33 | __maintainer__ = "https://openmodelica.org" 34 | 35 | from pyparsing import ( 36 | Combine, 37 | Dict, 38 | Forward, 39 | Group, 40 | Keyword, 41 | Optional, 42 | QuotedString, 43 | StringEnd, 44 | Suppress, 45 | Word, 46 | alphanums, 47 | alphas, 48 | delimitedList, 49 | nums, 50 | replaceWith, 51 | infixNotation, 52 | opAssoc, 53 | ) 54 | 55 | import sys 56 | 57 | 58 | def convertNumbers(s, l, toks): 59 | n = toks[0] 60 | try: 61 | return int(n) 62 | except ValueError: 63 | return float(n) 64 | 65 | 66 | def convertString2(s, s2): 67 | tmp = s2[0].replace("\\\"", "\"") 68 | tmp = tmp.replace("\"", "\\\"") 69 | tmp = tmp.replace("\'", "\\'") 70 | tmp = tmp.replace("\f", "\\f") 71 | tmp = tmp.replace("\n", "\\n") 72 | tmp = tmp.replace("\r", "\\r") 73 | tmp = tmp.replace("\t", "\\t") 74 | return "'"+tmp+"'" 75 | 76 | 77 | def convertString(s, s2): 78 | return s2[0].replace("\\\"", '"') 79 | 80 | 81 | def convertDict(d): 82 | return dict(d[0]) 83 | 84 | 85 | def convertTuple(t): 86 | return tuple(t[0]) 87 | 88 | 89 | def evaluateExpression(s, loc, toks): 90 | # Convert the tokens (ParseResults) into a string expression 91 | flat_list = [item for sublist in toks[0] for item in sublist] 92 | expr = "".join(flat_list) 93 | try: 94 | # Evaluate the expression safely 95 | return eval(expr) 96 | except Exception: 97 | return expr 98 | 99 | 100 | # Number parsing (supports arithmetic expressions in dimensions) (e.g., {1 + 1, 1}) 101 | arrayDimension = infixNotation( 102 | Word(alphas + "_", alphanums + "_") | Word(nums), 103 | [ 104 | (Word("+-", exact=1), 1, opAssoc.RIGHT), 105 | (Word("*/", exact=1), 2, opAssoc.LEFT), 106 | (Word("+-", exact=1), 2, opAssoc.LEFT), 107 | ], 108 | ).setParseAction(evaluateExpression) 109 | 110 | omcRecord = Forward() 111 | omcValue = Forward() 112 | 113 | TRUE = Keyword("true").setParseAction(replaceWith(True)) 114 | FALSE = Keyword("false").setParseAction(replaceWith(False)) 115 | NONE = (Keyword("NONE") + Suppress("(") + Suppress(")")).setParseAction(replaceWith(None)) 116 | SOME = (Suppress(Keyword("SOME")) + Suppress("(") + omcValue + Suppress(")")) 117 | 118 | omcString = QuotedString(quoteChar='"', escChar='\\', multiline=True).setParseAction(convertString) 119 | omcNumber = Combine(Optional('-') + ('0' | Word('123456789', nums)) + 120 | Optional('.' + Word(nums)) + 121 | Optional(Word('eE', exact=1) + Word(nums + '+-', nums))) 122 | 123 | # ident = Word(alphas + "_", alphanums + "_") | Combine("'" + Word(alphanums + "!#$%&()*+,-./:;<>=?@[]^{}|~ ") + "'") 124 | ident = Word(alphas + "_", alphanums + "_") | QuotedString(quoteChar='\'', escChar='\\').setParseAction(convertString2) 125 | fqident = Forward() 126 | fqident << ((ident + "." + fqident) | ident) 127 | omcValues = delimitedList(omcValue) 128 | omcTuple = Group(Suppress('(') + Optional(omcValues) + Suppress(')')).setParseAction(convertTuple) 129 | omcArray = Group(Suppress('{') + Optional(omcValues) + Suppress('}')).setParseAction(convertTuple) 130 | omcArraySpecialTypes = Group(Suppress('{') + delimitedList(arrayDimension) + Suppress('}')).setParseAction(convertTuple) 131 | omcValue << (omcString | omcNumber | omcRecord | omcArray | omcArraySpecialTypes | omcTuple | SOME | TRUE | FALSE | NONE | Combine(fqident)) 132 | recordMember = delimitedList(Group(ident + Suppress('=') + omcValue)) 133 | omcRecord << Group(Suppress('record') + Suppress(fqident) + Dict(recordMember) + Suppress('end') + Suppress(fqident) + Suppress(';')).setParseAction(convertDict) 134 | 135 | omcGrammar = Optional(omcValue) + StringEnd() 136 | 137 | omcNumber.setParseAction(convertNumbers) 138 | 139 | 140 | def parseString(string): 141 | res = omcGrammar.parseString(string) 142 | if len(res) == 0: 143 | return 144 | return res[0] 145 | 146 | 147 | if __name__ == "__main__": 148 | testdata = """ 149 | (1.0,{{1,true,3},{"4\\" 150 | ",5.9,6,NONE ( )},record ABC 151 | startTime = ErrorLevel.warning, 152 | 'stop*Time' = SOME(1.0) 153 | end ABC;}) 154 | """ 155 | expected = (1.0, ((1, True, 3), ('4"\n', 5.9, 6, None), {"'stop*Time'": 1.0, 'startTime': 'ErrorLevel.warning'})) 156 | results = parseString(testdata) 157 | if results != expected: 158 | print("Results:", results) 159 | print("Expected:", expected) 160 | print("Failed") 161 | sys.exit(1) 162 | print("Matches expected output") 163 | print(type(results), repr(results)) 164 | -------------------------------------------------------------------------------- /OMPython/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | OMPython is a Python interface to OpenModelica. 4 | To get started, create an OMCSessionZMQ object: 5 | from OMPython import OMCSessionZMQ 6 | omc = OMCSessionZMQ() 7 | omc.sendExpression("command") 8 | """ 9 | 10 | __license__ = """ 11 | This file is part of OpenModelica. 12 | 13 | Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 14 | c/o Linköpings universitet, Department of Computer and Information Science, 15 | SE-58183 Linköping, Sweden. 16 | 17 | All rights reserved. 18 | 19 | THIS PROGRAM IS PROVIDED UNDER THE TERMS OF THE BSD NEW LICENSE OR THE 20 | GPL VERSION 3 LICENSE OR THE OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. 21 | ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 22 | RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, 23 | ACCORDING TO RECIPIENTS CHOICE. 24 | 25 | The OpenModelica software and the OSMC (Open Source Modelica Consortium) 26 | Public License (OSMC-PL) are obtained from OSMC, either from the above 27 | address, from the URLs: http://www.openmodelica.org or 28 | http://www.ida.liu.se/projects/OpenModelica, and in the OpenModelica 29 | distribution. GNU version 3 is obtained from: 30 | http://www.gnu.org/copyleft/gpl.html. The New BSD License is obtained from: 31 | http://www.opensource.org/licenses/BSD-3-Clause. 32 | 33 | This program is distributed WITHOUT ANY WARRANTY; without even the implied 34 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, EXCEPT AS 35 | EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE 36 | CONDITIONS OF OSMC-PL. 37 | """ 38 | 39 | from OMPython.ModelicaSystem import LinearizationResult, ModelicaSystem, ModelicaSystemCmd, ModelicaSystemError 40 | from OMPython.OMCSession import OMCSessionCmd, OMCSessionException, OMCSessionZMQ 41 | 42 | # global names imported if import 'from OMPython import *' is used 43 | __all__ = [ 44 | 'LinearizationResult', 45 | 'ModelicaSystem', 46 | 'ModelicaSystemCmd', 47 | 'ModelicaSystemError', 48 | 49 | 'OMCSessionCmd', 50 | 'OMCSessionException', 51 | 'OMCSessionZMQ', 52 | ] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OMPython 2 | 3 | OMPython is a Python interface that uses ZeroMQ to 4 | communicate with OpenModelica. 5 | 6 | [![FMITest](https://github.com/OpenModelica/OMPython/actions/workflows/FMITest.yml/badge.svg)](https://github.com/OpenModelica/OMPython/actions/workflows/FMITest.yml) 7 | [![Test](https://github.com/OpenModelica/OMPython/actions/workflows/Test.yml/badge.svg)](https://github.com/OpenModelica/OMPython/actions/workflows/Test.yml) 8 | 9 | ## Dependencies 10 | 11 | - Python 3.x supported 12 | - PyZMQ is required 13 | 14 | ## Installation 15 | 16 | Installation using `pip` is recommended. 17 | 18 | ### Via pip 19 | 20 | ```bash 21 | pip install OMPython 22 | ``` 23 | 24 | ### Via source 25 | 26 | Clone the repository and run: 27 | 28 | ``` 29 | cd 30 | python -m pip install -U . 31 | ``` 32 | 33 | ## Usage 34 | 35 | Running the following commands should get you started 36 | 37 | ```python 38 | import OMPython 39 | help(OMPython) 40 | ``` 41 | 42 | ```python 43 | from OMPython import OMCSessionZMQ 44 | omc = OMCSessionZMQ() 45 | omc.sendExpression("getVersion()") 46 | ``` 47 | 48 | or read the [OMPython documentation](https://openmodelica.org/doc/OpenModelicaUsersGuide/latest/ompython.html) 49 | online. 50 | 51 | ## Bug Reports 52 | 53 | - Submit bugs through the [OpenModelica GitHub issues](https://github.com/OpenModelica/OMPython/issues/new). 54 | - [Pull requests](https://github.com/OpenModelica/OMPython/pulls) are welcome. 55 | 56 | ## Contact 57 | 58 | - Adeel Asghar, 59 | - Arunkumar Palanisamy, 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "OMPython" 7 | version = "3.6.0" 8 | description = "OpenModelica-Python API Interface" 9 | readme = "README.md" 10 | authors = [ 11 | {name = "Anand Kalaiarasi Ganeson", email = "ganan642@student.liu.se"}, 12 | ] 13 | maintainers = [ 14 | {name = "Adeel Asghar", email = "adeel.asghar@liu.se"}, 15 | ] 16 | license = "BSD-3-Clause OR LicenseRef-OSMC-PL-1.2 OR GPL-3.0-only" 17 | requires-python = ">=3.10" 18 | dependencies = [ 19 | "numpy", 20 | "psutil", 21 | "pyparsing", 22 | "pyzmq", 23 | ] 24 | 25 | [tool.setuptools] 26 | packages = ["OMPython"] 27 | 28 | [project.urls] 29 | Homepage = "http://openmodelica.org/" 30 | Documentation = "https://openmodelica.org/doc/OpenModelicaUsersGuide/latest/ompython.html" 31 | "Source Code" = "https://github.com/OpenModelica/OMPython" 32 | Issues = "https://github.com/OpenModelica/OMPython/issues" 33 | "Release Notes" = "https://github.com/OpenModelica/OMPython/releases" 34 | Download = "https://pypi.org/project/OMPython/#files" 35 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E501,E741 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['tests.test_OMParser', 'tests.test_ZMQ', 'tests.test_ModelicaSystem'] 2 | -------------------------------------------------------------------------------- /tests/test_ArrayDimension.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import tempfile 3 | import shutil 4 | import os 5 | 6 | 7 | # do not change the prefix class name, the class name should have prefix "Test" 8 | # according to the documenation of pytest 9 | class Test_ArrayDimension: 10 | def test_ArrayDimension(self): 11 | omc = OMPython.OMCSessionZMQ() 12 | 13 | # create a temp dir for each session 14 | tempdir = tempfile.mkdtemp() 15 | if not os.path.exists(tempdir): 16 | return print(tempdir, " cannot be created") 17 | 18 | tempdirExp = "".join(["cd(", "\"", tempdir, "\"", ")"]).replace("\\", "/") 19 | omc.sendExpression(tempdirExp) 20 | 21 | omc.sendExpression("loadString(\"model A Integer x[5+1,1+6]; end A;\")") 22 | omc.sendExpression("getErrorString()") 23 | 24 | result = omc.sendExpression("getComponents(A)") 25 | assert result[0][-1] == (6, 7), f"array dimension does not match the expected value. Got: {result[0][-1]}, Expected: {(6, 7)}" 26 | 27 | omc.sendExpression("loadString(\"model A Integer y = 5; Integer x[y+1,1+9]; end A;\")") 28 | omc.sendExpression("getErrorString()") 29 | 30 | result = omc.sendExpression("getComponents(A)") 31 | assert result[-1][-1] == ('y+1', 10), f"array dimension does not match the expected value. Got: {result[-1][-1]}, Expected: {('y+1', 10)}" 32 | 33 | omc.__del__() 34 | shutil.rmtree(tempdir, ignore_errors=True) 35 | -------------------------------------------------------------------------------- /tests/test_FMIExport.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import unittest 3 | import shutil 4 | import os 5 | 6 | 7 | class testFMIExport(unittest.TestCase): 8 | def __init__(self, *args, **kwargs): 9 | super(testFMIExport, self).__init__(*args, **kwargs) 10 | self.tmp = "" 11 | 12 | def __del__(self): 13 | shutil.rmtree(self.tmp, ignore_errors=True) 14 | 15 | def testCauerLowPassAnalog(self): 16 | print("testing Cauer") 17 | mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", 18 | lmodel=["Modelica"]) 19 | self.tmp = mod.getWorkDirectory() 20 | 21 | fmu = mod.convertMo2Fmu(fileNamePrefix="CauerLowPassAnalog") 22 | self.assertEqual(True, os.path.exists(fmu)) 23 | 24 | def testDrumBoiler(self): 25 | print("testing DrumBoiler") 26 | mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel=["Modelica"]) 27 | self.tmp = mod.getWorkDirectory() 28 | 29 | fmu = mod.convertMo2Fmu(fileNamePrefix="DrumBoiler") 30 | self.assertEqual(True, os.path.exists(fmu)) 31 | 32 | 33 | if __name__ == '__main__': 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /tests/test_FMIRegression.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import tempfile 3 | import shutil 4 | import os 5 | 6 | 7 | # do not change the prefix class name, the class name should have prefix "Test" 8 | # according to the documenation of pytest 9 | class Test_FMIRegression: 10 | 11 | def buildModelFMU(self, modelName): 12 | omc = OMPython.OMCSessionZMQ() 13 | 14 | # create a temp dir for each session 15 | tempdir = tempfile.mkdtemp() 16 | if not os.path.exists(tempdir): 17 | return print(tempdir, " cannot be created") 18 | 19 | tempdirExp = "".join(["cd(", "\"", tempdir, "\"", ")"]).replace("\\", "/") 20 | omc.sendExpression(tempdirExp) 21 | 22 | omc.sendExpression("loadModel(Modelica)") 23 | omc.sendExpression("getErrorString()") 24 | 25 | fileNamePrefix = modelName.split(".")[-1] 26 | exp = "buildModelFMU(" + modelName + ", fileNamePrefix=\"" + fileNamePrefix + "\"" + ")" 27 | 28 | fmu = omc.sendExpression(exp) 29 | assert os.path.exists(fmu) 30 | 31 | omc.__del__() 32 | shutil.rmtree(tempdir, ignore_errors=True) 33 | 34 | def test_Modelica_Blocks_Examples_Filter(self): 35 | self.buildModelFMU("Modelica.Blocks.Examples.Filter") 36 | 37 | def test_Modelica_Blocks_Examples_RealNetwork1(self): 38 | self.buildModelFMU("Modelica.Blocks.Examples.RealNetwork1") 39 | 40 | def test_Modelica_Electrical_Analog_Examples_CauerLowPassAnalog(self): 41 | self.buildModelFMU("Modelica.Electrical.Analog.Examples.CauerLowPassAnalog") 42 | 43 | def test_Modelica_Electrical_Digital_Examples_FlipFlop(self): 44 | self.buildModelFMU("Modelica.Electrical.Digital.Examples.FlipFlop") 45 | 46 | def test_Modelica_Mechanics_Rotational_Examples_FirstGrounded(self): 47 | self.buildModelFMU("Modelica.Mechanics.Rotational.Examples.FirstGrounded") 48 | 49 | def test_Modelica_Mechanics_Rotational_Examples_CoupledClutches(self): 50 | self.buildModelFMU("Modelica.Mechanics.Rotational.Examples.CoupledClutches") 51 | 52 | def test_Modelica_Mechanics_MultiBody_Examples_Elementary_DoublePendulum(self): 53 | self.buildModelFMU("Modelica.Mechanics.MultiBody.Examples.Elementary.DoublePendulum") 54 | 55 | def test_Modelica_Mechanics_MultiBody_Examples_Elementary_FreeBody(self): 56 | self.buildModelFMU("Modelica.Mechanics.MultiBody.Examples.Elementary.FreeBody") 57 | 58 | def test_Modelica_Fluid_Examples_PumpingSystem(self): 59 | self.buildModelFMU("Modelica.Fluid.Examples.PumpingSystem") 60 | 61 | def test_Modelica_Fluid_Examples_TraceSubstances_RoomCO2WithControls(self): 62 | self.buildModelFMU("Modelica.Fluid.Examples.TraceSubstances.RoomCO2WithControls") 63 | 64 | def test_Modelica_Clocked_Examples_SimpleControlledDrive_ClockedWithDiscreteTextbookController(self): 65 | self.buildModelFMU("Modelica.Clocked.Examples.SimpleControlledDrive.ClockedWithDiscreteTextbookController") 66 | -------------------------------------------------------------------------------- /tests/test_ModelicaSystem.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import unittest 3 | import tempfile 4 | import shutil 5 | import os 6 | import pathlib 7 | import numpy as np 8 | 9 | 10 | class ModelicaSystemTester(unittest.TestCase): 11 | def __init__(self, *args, **kwargs): 12 | super(ModelicaSystemTester, self).__init__(*args, **kwargs) 13 | self.tmp = pathlib.Path(tempfile.mkdtemp(prefix='tmpOMPython.tests')) 14 | with open(self.tmp / "M.mo", "w") as fout: 15 | fout.write("""model M 16 | Real x(start = 1, fixed = true); 17 | parameter Real a = -1; 18 | equation 19 | der(x) = x*a; 20 | end M; 21 | """) 22 | 23 | def __del__(self): 24 | shutil.rmtree(self.tmp, ignore_errors=True) 25 | 26 | def testModelicaSystemLoop(self): 27 | def worker(): 28 | filePath = (self.tmp / "M.mo").as_posix() 29 | m = OMPython.ModelicaSystem(filePath, "M") 30 | m.simulate() 31 | m.convertMo2Fmu(fmuType="me") 32 | for _ in range(10): 33 | worker() 34 | 35 | def test_setParameters(self): 36 | omc = OMPython.OMCSessionZMQ() 37 | model_path = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels/" 38 | mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") 39 | 40 | # method 1 41 | mod.setParameters("e=1.234") 42 | mod.setParameters("g=321.0") 43 | assert mod.getParameters("e") == ["1.234"] 44 | assert mod.getParameters("g") == ["321.0"] 45 | assert mod.getParameters() == { 46 | "e": "1.234", 47 | "g": "321.0", 48 | } 49 | 50 | # method 2 51 | mod.setParameters(["e=21.3", "g=0.12"]) 52 | assert mod.getParameters() == { 53 | "e": "21.3", 54 | "g": "0.12", 55 | } 56 | assert mod.getParameters(["e", "g"]) == ["21.3", "0.12"] 57 | assert mod.getParameters(["g", "e"]) == ["0.12", "21.3"] 58 | 59 | def test_setSimulationOptions(self): 60 | omc = OMPython.OMCSessionZMQ() 61 | model_path = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels/" 62 | mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") 63 | 64 | # method 1 65 | mod.setSimulationOptions("stopTime=1.234") 66 | mod.setSimulationOptions("tolerance=1.1e-08") 67 | assert mod.getSimulationOptions("stopTime") == ["1.234"] 68 | assert mod.getSimulationOptions("tolerance") == ["1.1e-08"] 69 | assert mod.getSimulationOptions(["tolerance", "stopTime"]) == ["1.1e-08", "1.234"] 70 | d = mod.getSimulationOptions() 71 | assert isinstance(d, dict) 72 | assert d["stopTime"] == "1.234" 73 | assert d["tolerance"] == "1.1e-08" 74 | 75 | # method 2 76 | mod.setSimulationOptions(["stopTime=2.1", "tolerance=1.2e-08"]) 77 | d = mod.getSimulationOptions() 78 | assert d["stopTime"] == "2.1" 79 | assert d["tolerance"] == "1.2e-08" 80 | 81 | def test_relative_path(self): 82 | cwd = pathlib.Path.cwd() 83 | (fd, name) = tempfile.mkstemp(prefix='tmpOMPython.tests', dir=cwd, text=True) 84 | try: 85 | with os.fdopen(fd, 'w') as f: 86 | f.write((self.tmp / "M.mo").read_text()) 87 | 88 | model_file = pathlib.Path(name).relative_to(cwd) 89 | model_relative = str(model_file) 90 | assert "/" not in model_relative 91 | 92 | mod = OMPython.ModelicaSystem(model_relative, "M") 93 | assert float(mod.getParameters("a")[0]) == -1 94 | finally: 95 | # clean up the temporary file 96 | model_file.unlink() 97 | 98 | def test_customBuildDirectory(self): 99 | filePath = (self.tmp / "M.mo").as_posix() 100 | tmpdir = self.tmp / "tmpdir1" 101 | tmpdir.mkdir() 102 | m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir) 103 | assert m.getWorkDirectory().resolve() == tmpdir.resolve() 104 | result_file = tmpdir / "a.mat" 105 | assert not result_file.exists() 106 | m.simulate(resultfile="a.mat") 107 | assert result_file.is_file() 108 | 109 | def test_getSolutions(self): 110 | filePath = (self.tmp / "M.mo").as_posix() 111 | mod = OMPython.ModelicaSystem(filePath, "M") 112 | x0 = 1 113 | a = -1 114 | tau = -1 / a 115 | stopTime = 5*tau 116 | mod.setSimulationOptions([f"stopTime={stopTime}", "stepSize=0.1", "tolerance=1e-8"]) 117 | mod.simulate() 118 | 119 | x = mod.getSolutions("x") 120 | t, x2 = mod.getSolutions(["time", "x"]) 121 | assert (x2 == x).all() 122 | sol_names = mod.getSolutions() 123 | assert isinstance(sol_names, tuple) 124 | assert "time" in sol_names 125 | assert "x" in sol_names 126 | assert "der(x)" in sol_names 127 | with self.assertRaises(OMPython.ModelicaSystemError): 128 | mod.getSolutions("t") # variable 't' does not exist 129 | assert np.isclose(t[0], 0), "time does not start at 0" 130 | assert np.isclose(t[-1], stopTime), "time does not end at stopTime" 131 | x_analytical = x0 * np.exp(a*t) 132 | assert np.isclose(x, x_analytical, rtol=1e-4).all() 133 | 134 | def test_getters(self): 135 | model_file = self.tmp / "M_getters.mo" 136 | model_file.write_text(""" 137 | model M_getters 138 | Real x(start = 1, fixed = true); 139 | output Real y "the derivative"; 140 | parameter Real a = -0.5; 141 | parameter Real b = 0.1; 142 | equation 143 | der(x) = x*a + b; 144 | y = der(x); 145 | end M_getters; 146 | """) 147 | mod = OMPython.ModelicaSystem(model_file.as_posix(), "M_getters") 148 | 149 | q = mod.getQuantities() 150 | assert isinstance(q, list) 151 | assert sorted(q, key=lambda d: d["name"]) == sorted([ 152 | { 153 | 'alias': 'noAlias', 154 | 'aliasvariable': None, 155 | 'causality': 'local', 156 | 'changeable': 'true', 157 | 'description': None, 158 | 'max': None, 159 | 'min': None, 160 | 'name': 'x', 161 | 'start': '1.0', 162 | 'unit': None, 163 | 'variability': 'continuous', 164 | }, 165 | { 166 | 'alias': 'noAlias', 167 | 'aliasvariable': None, 168 | 'causality': 'local', 169 | 'changeable': 'false', 170 | 'description': None, 171 | 'max': None, 172 | 'min': None, 173 | 'name': 'der(x)', 174 | 'start': None, 175 | 'unit': None, 176 | 'variability': 'continuous', 177 | }, 178 | { 179 | 'alias': 'noAlias', 180 | 'aliasvariable': None, 181 | 'causality': 'output', 182 | 'changeable': 'false', 183 | 'description': 'the derivative', 184 | 'max': None, 185 | 'min': None, 186 | 'name': 'y', 187 | 'start': '-0.4', 188 | 'unit': None, 189 | 'variability': 'continuous', 190 | }, 191 | { 192 | 'alias': 'noAlias', 193 | 'aliasvariable': None, 194 | 'causality': 'parameter', 195 | 'changeable': 'true', 196 | 'description': None, 197 | 'max': None, 198 | 'min': None, 199 | 'name': 'a', 200 | 'start': '-0.5', 201 | 'unit': None, 202 | 'variability': 'parameter', 203 | }, 204 | { 205 | 'alias': 'noAlias', 206 | 'aliasvariable': None, 207 | 'causality': 'parameter', 208 | 'changeable': 'true', 209 | 'description': None, 210 | 'max': None, 211 | 'min': None, 212 | 'name': 'b', 213 | 'start': '0.1', 214 | 'unit': None, 215 | 'variability': 'parameter', 216 | } 217 | ], key=lambda d: d["name"]) 218 | 219 | assert mod.getQuantities("y") == [ 220 | { 221 | 'alias': 'noAlias', 222 | 'aliasvariable': None, 223 | 'causality': 'output', 224 | 'changeable': 'false', 225 | 'description': 'the derivative', 226 | 'max': None, 227 | 'min': None, 228 | 'name': 'y', 229 | 'start': '-0.4', 230 | 'unit': None, 231 | 'variability': 'continuous', 232 | } 233 | ] 234 | 235 | assert mod.getQuantities(["y", "x"]) == [ 236 | { 237 | 'alias': 'noAlias', 238 | 'aliasvariable': None, 239 | 'causality': 'output', 240 | 'changeable': 'false', 241 | 'description': 'the derivative', 242 | 'max': None, 243 | 'min': None, 244 | 'name': 'y', 245 | 'start': '-0.4', 246 | 'unit': None, 247 | 'variability': 'continuous', 248 | }, 249 | { 250 | 'alias': 'noAlias', 251 | 'aliasvariable': None, 252 | 'causality': 'local', 253 | 'changeable': 'true', 254 | 'description': None, 255 | 'max': None, 256 | 'min': None, 257 | 'name': 'x', 258 | 'start': '1.0', 259 | 'unit': None, 260 | 'variability': 'continuous', 261 | }, 262 | ] 263 | 264 | assert mod.getInputs() == {} 265 | # getOutputs before simulate() 266 | assert mod.getOutputs() == {'y': '-0.4'} 267 | assert mod.getOutputs("y") == ["-0.4"] 268 | assert mod.getOutputs(["y", "y"]) == ["-0.4", "-0.4"] 269 | 270 | # getContinuous before simulate(): 271 | assert mod.getContinuous() == { 272 | 'x': '1.0', 273 | 'der(x)': None, 274 | 'y': '-0.4' 275 | } 276 | assert mod.getContinuous("y") == ['-0.4'] 277 | assert mod.getContinuous(["y", "x"]) == ['-0.4', '1.0'] 278 | assert mod.getContinuous("a") == ["NotExist"] # a is a parameter 279 | 280 | stopTime = 1.0 281 | a = -0.5 282 | b = 0.1 283 | x0 = 1.0 284 | x_analytical = -b/a + (x0 + b/a) * np.exp(a * stopTime) 285 | dx_analytical = (x0 + b/a) * a * np.exp(a * stopTime) 286 | mod.setSimulationOptions(f"stopTime={stopTime}") 287 | mod.simulate() 288 | 289 | # getOutputs after simulate() 290 | d = mod.getOutputs() 291 | assert d.keys() == {"y"} 292 | assert np.isclose(d["y"], dx_analytical, 1e-4) 293 | assert mod.getOutputs("y") == [d["y"]] 294 | assert mod.getOutputs(["y", "y"]) == [d["y"], d["y"]] 295 | 296 | # getContinuous after simulate() should return values at end of simulation: 297 | with self.assertRaises(OMPython.ModelicaSystemError): 298 | mod.getContinuous("a") # a is a parameter 299 | with self.assertRaises(OMPython.ModelicaSystemError): 300 | mod.getContinuous(["x", "a", "y"]) # a is a parameter 301 | d = mod.getContinuous() 302 | assert d.keys() == {"x", "der(x)", "y"} 303 | assert np.isclose(d["x"], x_analytical, 1e-4) 304 | assert np.isclose(d["der(x)"], dx_analytical, 1e-4) 305 | assert np.isclose(d["y"], dx_analytical, 1e-4) 306 | assert mod.getContinuous("x") == [d["x"]] 307 | assert mod.getContinuous(["y", "x"]) == [d["y"], d["x"]] 308 | 309 | with self.assertRaises(OMPython.ModelicaSystemError): 310 | mod.setSimulationOptions("thisOptionDoesNotExist=3") 311 | 312 | def test_simulate_inputs(self): 313 | model_file = self.tmp / "M_input.mo" 314 | model_file.write_text(""" 315 | model M_input 316 | Real x(start=0, fixed=true); 317 | input Real u1; 318 | input Real u2; 319 | output Real y; 320 | equation 321 | der(x) = u1 + u2; 322 | y = x; 323 | end M_input; 324 | """) 325 | mod = OMPython.ModelicaSystem(model_file.as_posix(), "M_input") 326 | 327 | mod.setSimulationOptions("stopTime=1.0") 328 | 329 | # integrate zero (no setInputs call) - it should default to None -> 0 330 | assert mod.getInputs() == { 331 | "u1": None, 332 | "u2": None, 333 | } 334 | mod.simulate() 335 | y = mod.getSolutions("y")[0] 336 | assert np.isclose(y[-1], 0.0) 337 | 338 | # integrate a constant 339 | mod.setInputs("u1=2.5") 340 | assert mod.getInputs() == { 341 | "u1": [ 342 | (0.0, 2.5), 343 | (1.0, 2.5), 344 | ], 345 | "u2": None, 346 | } 347 | mod.simulate() 348 | y = mod.getSolutions("y")[0] 349 | assert np.isclose(y[-1], 2.5) 350 | 351 | # now let's integrate the sum of two ramps 352 | mod.setInputs("u1=[(0.0, 0.0), (0.5, 2), (1.0, 0)]") 353 | assert mod.getInputs("u1") == [[ 354 | (0.0, 0.0), 355 | (0.5, 2.0), 356 | (1.0, 0.0), 357 | ]] 358 | mod.simulate() 359 | y = mod.getSolutions("y")[0] 360 | assert np.isclose(y[-1], 1.0) 361 | 362 | # let's try some edge cases 363 | # unmatched startTime 364 | with self.assertRaises(OMPython.ModelicaSystemError): 365 | mod.setInputs("u1=[(-0.5, 0.0), (1.0, 1)]") 366 | mod.simulate() 367 | # unmatched stopTime 368 | with self.assertRaises(OMPython.ModelicaSystemError): 369 | mod.setInputs("u1=[(0.0, 0.0), (0.5, 1)]") 370 | mod.simulate() 371 | 372 | # Let's use both inputs, but each one with different number of of 373 | # samples. This has an effect when generating the csv file. 374 | mod.setInputs([ 375 | "u1=[(0.0, 0), (1.0, 1)]", 376 | "u2=[(0.0, 0), (0.25, 0.5), (0.5, 1.0), (1.0, 0)]", 377 | ]) 378 | mod.simulate() 379 | assert pathlib.Path(mod.csvFile).read_text() == """time,u1,u2,end 380 | 0.0,0.0,0.0,0 381 | 0.25,0.25,0.5,0 382 | 0.5,0.5,1.0,0 383 | 1.0,1.0,0.0,0 384 | """ 385 | y = mod.getSolutions("y")[0] 386 | assert np.isclose(y[-1], 1.0) 387 | 388 | 389 | if __name__ == '__main__': 390 | unittest.main() 391 | -------------------------------------------------------------------------------- /tests/test_ModelicaSystemCmd.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import pathlib 3 | import shutil 4 | import tempfile 5 | import unittest 6 | 7 | 8 | import logging 9 | logger = logging.getLogger(__name__) 10 | logging.basicConfig(level=logging.DEBUG) 11 | 12 | 13 | class ModelicaSystemCmdTester(unittest.TestCase): 14 | def __init__(self, *args, **kwargs): 15 | super(ModelicaSystemCmdTester, self).__init__(*args, **kwargs) 16 | self.tmp = pathlib.Path(tempfile.mkdtemp(prefix='tmpOMPython.tests')) 17 | self.model = self.tmp / "M.mo" 18 | with open(self.model, "w") as fout: 19 | fout.write("""model M 20 | Real x(start = 1, fixed = true); 21 | parameter Real a = -1; 22 | equation 23 | der(x) = x*a; 24 | end M; 25 | """) 26 | self.mod = OMPython.ModelicaSystem(self.model.as_posix(), "M") 27 | 28 | def __del__(self): 29 | shutil.rmtree(self.tmp, ignore_errors=True) 30 | 31 | def test_simflags(self): 32 | mscmd = OMPython.ModelicaSystemCmd(runpath=self.mod.tempdir, modelname=self.mod.modelName) 33 | mscmd.args_set(args={"noEventEmit": None, "noRestart": None, "override": {'b': 2}}) 34 | mscmd.args_set(args=mscmd.parse_simflags(simflags="-noEventEmit -noRestart -override=a=1,x=3")) 35 | 36 | logger.info(mscmd.get_cmd()) 37 | 38 | assert mscmd.get_cmd() == [mscmd.get_exe().as_posix(), '-noEventEmit', '-noRestart', '-override=b=2,a=1,x=3'] 39 | 40 | 41 | if __name__ == '__main__': 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /tests/test_OMParser.py: -------------------------------------------------------------------------------- 1 | from OMPython import OMParser 2 | import unittest 3 | 4 | typeCheck = OMParser.typeCheck 5 | 6 | 7 | class TypeCheckTester(unittest.TestCase): 8 | def testNewlineBehaviour(self): 9 | pass 10 | 11 | def testBoolean(self): 12 | self.assertEqual(typeCheck('TRUE'), True) 13 | self.assertEqual(typeCheck('True'), True) 14 | self.assertEqual(typeCheck('true'), True) 15 | self.assertEqual(typeCheck('FALSE'), False) 16 | self.assertEqual(typeCheck('False'), False) 17 | self.assertEqual(typeCheck('false'), False) 18 | 19 | def testInt(self): 20 | self.assertEqual(typeCheck('2'), 2) 21 | self.assertEqual(type(typeCheck('1')), int) 22 | self.assertEqual(type(typeCheck('123123123123123123232323')), int) 23 | self.assertEqual(type(typeCheck('9223372036854775808')), int) 24 | 25 | def testFloat(self): 26 | self.assertEqual(type(typeCheck('1.2e3')), float) 27 | 28 | # def testDict(self): 29 | # self.assertEqual(type(typeCheck('{"a": "b"}')), dict) 30 | 31 | def testIdent(self): 32 | self.assertEqual(typeCheck('blabla2'), "blabla2") 33 | pass 34 | 35 | def testStr(self): 36 | pass 37 | 38 | def testUnStringable(self): 39 | pass 40 | 41 | 42 | if __name__ == '__main__': 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/test_OMSessionCmd.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import unittest 3 | 4 | 5 | class OMCSessionCmdTester(unittest.TestCase): 6 | def __init__(self, *args, **kwargs): 7 | super(OMCSessionCmdTester, self).__init__(*args, **kwargs) 8 | 9 | def test_isPackage(self): 10 | omczmq = OMPython.OMCSessionZMQ() 11 | omccmd = OMPython.OMCSessionCmd(session=omczmq) 12 | assert not omccmd.isPackage('Modelica') 13 | 14 | def test_isPackage2(self): 15 | mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", 16 | lmodel=["Modelica"]) 17 | omccmd = OMPython.OMCSessionCmd(session=mod.getconn) 18 | assert omccmd.isPackage('Modelica') 19 | 20 | # TODO: add more checks ... 21 | 22 | 23 | if __name__ == '__main__': 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /tests/test_ZMQ.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import unittest 3 | import tempfile 4 | import shutil 5 | import os 6 | 7 | 8 | class ZMQTester(unittest.TestCase): 9 | def __init__(self, *args, **kwargs): 10 | super(ZMQTester, self).__init__(*args, **kwargs) 11 | self.simpleModel = """model M 12 | Real r = time; 13 | end M;""" 14 | self.tmp = tempfile.mkdtemp(prefix='tmpOMPython.tests') 15 | self.origDir = os.getcwd() 16 | os.chdir(self.tmp) 17 | self.om = OMPython.OMCSessionZMQ() 18 | os.chdir(self.origDir) 19 | 20 | def __del__(self): 21 | shutil.rmtree(self.tmp, ignore_errors=True) 22 | del self.om 23 | 24 | def clean(self): 25 | del self.om 26 | self.om = None 27 | 28 | def testHelloWorld(self): 29 | self.assertEqual("HelloWorld!", self.om.sendExpression('"HelloWorld!"')) 30 | self.clean() 31 | 32 | def testTranslate(self): 33 | self.assertEqual(("M",), self.om.sendExpression(self.simpleModel)) 34 | self.assertEqual(True, self.om.sendExpression('translateModel(M)')) 35 | self.clean() 36 | 37 | def testSimulate(self): 38 | self.assertEqual(True, self.om.sendExpression('loadString("%s")' % self.simpleModel)) 39 | self.om.sendExpression('res:=simulate(M, stopTime=2.0)') 40 | self.assertNotEqual("", self.om.sendExpression('res.resultFile')) 41 | self.clean() 42 | 43 | def test_execute(self): 44 | self.assertEqual('"HelloWorld!"\n', self.om.execute('"HelloWorld!"')) 45 | self.assertEqual('"HelloWorld!"\n', self.om.sendExpression('"HelloWorld!"', parsed=False)) 46 | self.assertEqual('HelloWorld!', self.om.sendExpression('"HelloWorld!"', parsed=True)) 47 | self.clean() 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /tests/test_docker.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import unittest 3 | import pytest 4 | 5 | 6 | class DockerTester(unittest.TestCase): 7 | @pytest.mark.skip(reason="This test would fail") 8 | def testDocker(self): 9 | om = OMPython.OMCSessionZMQ(docker="openmodelica/openmodelica:v1.16.1-minimal") 10 | assert om.sendExpression("getVersion()") == "OpenModelica 1.16.1" 11 | omInner = OMPython.OMCSessionZMQ(dockerContainer=om._dockerCid) 12 | assert omInner.sendExpression("getVersion()") == "OpenModelica 1.16.1" 13 | om2 = OMPython.OMCSessionZMQ(docker="openmodelica/openmodelica:v1.16.1-minimal", port=11111) 14 | assert om2.sendExpression("getVersion()") == "OpenModelica 1.16.1" 15 | del om2 16 | del omInner 17 | del om 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /tests/test_linearization.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import tempfile 3 | import shutil 4 | import unittest 5 | import pathlib 6 | import numpy as np 7 | 8 | 9 | class Test_Linearization(unittest.TestCase): 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | self.tmp = pathlib.Path(tempfile.mkdtemp(prefix='tmpOMPython.tests')) 13 | with open(self.tmp / "linearTest.mo", "w") as fout: 14 | fout.write(""" 15 | model linearTest 16 | Real x1(start=1); 17 | Real x2(start=-2); 18 | Real x3(start=3); 19 | Real x4(start=-5); 20 | parameter Real a=3,b=2,c=5,d=7,e=1,f=4; 21 | equation 22 | a*x1 = b*x2 -der(x1); 23 | der(x2) + c*x3 + d*x1 = x4; 24 | f*x4 - e*x3 - der(x3) = x1; 25 | der(x4) = x1 + x2 + der(x3) + x4; 26 | end linearTest; 27 | """) 28 | 29 | def __del__(self): 30 | shutil.rmtree(self.tmp, ignore_errors=True) 31 | 32 | def test_example(self): 33 | filePath = (self.tmp / "linearTest.mo").as_posix() 34 | mod = OMPython.ModelicaSystem(filePath, "linearTest") 35 | [A, B, C, D] = mod.linearize() 36 | expected_matrixA = [[-3, 2, 0, 0], [-7, 0, -5, 1], [-1, 0, -1, 4], [0, 1, -1, 5]] 37 | assert A == expected_matrixA, f"Matrix does not match the expected value. Got: {A}, Expected: {expected_matrixA}" 38 | assert B == [], f"Matrix does not match the expected value. Got: {B}, Expected: {[]}" 39 | assert C == [], f"Matrix does not match the expected value. Got: {C}, Expected: {[]}" 40 | assert D == [], f"Matrix does not match the expected value. Got: {D}, Expected: {[]}" 41 | assert mod.getLinearInputs() == [] 42 | assert mod.getLinearOutputs() == [] 43 | assert mod.getLinearStates() == ["x1", "x2", "x3", "x4"] 44 | 45 | def test_getters(self): 46 | model_file = self.tmp / "pendulum.mo" 47 | model_file.write_text(""" 48 | model Pendulum 49 | Real phi(start=Modelica.Constants.pi, fixed=true); 50 | Real omega(start=0, fixed=true); 51 | input Real u1; 52 | input Real u2; 53 | output Real y1; 54 | output Real y2; 55 | parameter Real l = 1.2; 56 | parameter Real g = 9.81; 57 | equation 58 | der(phi) = omega + u2; 59 | der(omega) = -g/l * sin(phi); 60 | y1 = y2 + 0.5*omega; 61 | y2 = phi + u1; 62 | end Pendulum; 63 | """) 64 | mod = OMPython.ModelicaSystem(model_file.as_posix(), "Pendulum", ["Modelica"]) 65 | 66 | d = mod.getLinearizationOptions() 67 | assert isinstance(d, dict) 68 | assert "startTime" in d 69 | assert "stopTime" in d 70 | assert mod.getLinearizationOptions(["stopTime", "startTime"]) == [d["stopTime"], d["startTime"]] 71 | mod.setLinearizationOptions("stopTime=0.02") 72 | assert mod.getLinearizationOptions("stopTime") == ["0.02"] 73 | 74 | mod.setInputs(["u1=10", "u2=0"]) 75 | [A, B, C, D] = mod.linearize() 76 | g = float(mod.getParameters("g")[0]) 77 | l = float(mod.getParameters("l")[0]) 78 | assert mod.getLinearInputs() == ["u1", "u2"] 79 | assert mod.getLinearStates() == ["omega", "phi"] 80 | assert mod.getLinearOutputs() == ["y1", "y2"] 81 | assert np.isclose(A, [[0, g/l], [1, 0]]).all() 82 | assert np.isclose(B, [[0, 0], [0, 1]]).all() 83 | assert np.isclose(C, [[0.5, 1], [0, 1]]).all() 84 | assert np.isclose(D, [[1, 0], [1, 0]]).all() 85 | 86 | # test LinearizationResult 87 | result = mod.linearize() 88 | assert result[0] == A 89 | assert result[1] == B 90 | assert result[2] == C 91 | assert result[3] == D 92 | with self.assertRaises(KeyError): 93 | result[4] 94 | 95 | A2, B2, C2, D2 = result 96 | assert A2 == A 97 | assert B2 == B 98 | assert C2 == C 99 | assert D2 == D 100 | 101 | assert result.n == 2 102 | assert result.m == 2 103 | assert result.p == 2 104 | assert np.isclose(result.x0, [0, np.pi]).all() 105 | assert np.isclose(result.u0, [10, 0]).all() 106 | assert result.stateVars == ["omega", "phi"] 107 | assert result.inputVars == ["u1", "u2"] 108 | assert result.outputVars == ["y1", "y2"] 109 | -------------------------------------------------------------------------------- /tests/test_optimization.py: -------------------------------------------------------------------------------- 1 | import OMPython 2 | import tempfile 3 | import shutil 4 | import unittest 5 | import pathlib 6 | import numpy as np 7 | 8 | 9 | class Test_Linearization(unittest.TestCase): 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | self.tmp = pathlib.Path(tempfile.mkdtemp(prefix='tmpOMPython.tests')) 13 | 14 | def __del__(self): 15 | shutil.rmtree(self.tmp, ignore_errors=True) 16 | 17 | def test_example(self): 18 | model_file = self.tmp / "BangBang2021.mo" 19 | model_file.write_text(""" 20 | model BangBang2021 "Model to verify that optimization gives bang-bang optimal control" 21 | parameter Real m = 1; 22 | parameter Real p = 1 "needed for final constraints"; 23 | 24 | Real a; 25 | Real v(start = 0, fixed = true); 26 | Real pos(start = 0, fixed = true); 27 | Real pow(min = -30, max = 30) = f * v annotation(isConstraint = true); 28 | 29 | input Real f(min = -10, max = 10); 30 | 31 | Real costPos(nominal = 1) = -pos "minimize -pos(tf)" annotation(isMayer=true); 32 | 33 | Real conSpeed(min = 0, max = 0) = p * v " 0<= p*v(tf) <=0" annotation(isFinalConstraint = true); 34 | 35 | equation 36 | 37 | der(pos) = v; 38 | der(v) = a; 39 | f = m * a; 40 | 41 | annotation(experiment(StartTime = 0, StopTime = 1, Tolerance = 1e-07, Interval = 0.01), 42 | __OpenModelica_simulationFlags(s="optimization", optimizerNP="1"), 43 | __OpenModelica_commandLineOptions="+g=Optimica"); 44 | 45 | end BangBang2021; 46 | """) 47 | 48 | mod = OMPython.ModelicaSystem(model_file.as_posix(), "BangBang2021") 49 | 50 | mod.setOptimizationOptions(["numberOfIntervals=16", "stopTime=1", 51 | "stepSize=0.001", "tolerance=1e-8"]) 52 | 53 | # test the getter 54 | assert mod.getOptimizationOptions()["stopTime"] == "1" 55 | assert mod.getOptimizationOptions("stopTime") == ["1"] 56 | assert mod.getOptimizationOptions(["tolerance", "stopTime"]) == ["1e-8", "1"] 57 | 58 | r = mod.optimize() 59 | # it is necessary to specify resultfile, otherwise it wouldn't find it. 60 | time, f, v = mod.getSolutions(["time", "f", "v"], resultfile=r["resultFile"]) 61 | assert np.isclose(f[0], 10) 62 | assert np.isclose(f[-1], -10) 63 | 64 | def f_fcn(time, v): 65 | if time < 0.3: 66 | return 10 67 | if time <= 0.5: 68 | return 30 / v 69 | if time < 0.7: 70 | return -30 / v 71 | return -10 72 | f_expected = [f_fcn(t, v) for t, v in zip(time, v)] 73 | 74 | # The sharp edge at time=0.5 probably won't match, let's leave that out. 75 | matches = np.isclose(f, f_expected, 1e-3) 76 | assert matches[:498].all() 77 | assert matches[502:].all() 78 | -------------------------------------------------------------------------------- /tests/test_typedParser.py: -------------------------------------------------------------------------------- 1 | from OMPython import OMTypedParser 2 | import unittest 3 | 4 | typeCheck = OMTypedParser.parseString 5 | 6 | 7 | class TypeCheckTester(unittest.TestCase): 8 | def testNewlineBehaviour(self): 9 | pass 10 | 11 | def testBoolean(self): 12 | self.assertEqual(typeCheck('true'), True) 13 | self.assertEqual(typeCheck('false'), False) 14 | 15 | def testInt(self): 16 | self.assertEqual(typeCheck('2'), 2) 17 | self.assertEqual(type(typeCheck('1')), int) 18 | self.assertEqual(type(typeCheck('123123123123123123232323')), int) 19 | self.assertEqual(type(typeCheck('9223372036854775808')), int) 20 | 21 | def testFloat(self): 22 | self.assertEqual(type(typeCheck('1.2e3')), float) 23 | 24 | def testIdent(self): 25 | self.assertEqual(typeCheck('blabla2'), "blabla2") 26 | pass 27 | 28 | def testEmpty(self): 29 | self.assertEqual(typeCheck(''), None) 30 | pass 31 | 32 | def testStr(self): 33 | pass 34 | 35 | def testUnStringable(self): 36 | pass 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | --------------------------------------------------------------------------------