├── .github ├── scripts │ ├── fetch_models.py │ └── run_tests.py └── workflows │ ├── 1_build.yml │ ├── 2_sphinx.yml │ └── 3_deploy.yml ├── .gitignore ├── .pylintrc ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── _static │ └── imgs │ │ ├── banner_white.svg │ │ ├── code_interface.png │ │ ├── code_interface_advanced.png │ │ ├── codegen_settings.png │ │ ├── gen_code.png │ │ ├── model_settings.png │ │ ├── signal_highlight.png │ │ ├── signal_highlight2.png │ │ ├── solver_settings.png │ │ └── toolbar_1.png ├── conf.py ├── index.rst ├── make.bat └── src │ ├── autodoc.rst │ ├── howto.rst │ ├── license.rst │ └── quickstart.rst ├── pyproject.toml ├── pysimlink ├── __init__.py ├── c_files │ ├── include │ │ ├── containers.hpp │ │ ├── dev_tools.hpp │ │ ├── model_interface.hpp │ │ ├── model_interface.tpp │ │ ├── model_utils.hpp │ │ ├── model_utils.tpp │ │ └── safe_utils.hpp │ └── src │ │ ├── bindings.cpp │ │ ├── dev_tools.cpp │ │ ├── hash.cpp │ │ ├── model_interface.cpp │ │ ├── model_utils.cpp │ │ └── safe_utils.cpp ├── lib │ ├── __init__.py │ ├── cmake_gen.py │ ├── compilers │ │ ├── __init__.py │ │ ├── compiler.py │ │ ├── model_ref_compiler.py │ │ └── one_shot_compiler.py │ ├── dependency_graph.py │ ├── exceptions.py │ ├── model.py │ ├── model_paths.py │ ├── model_types.py │ ├── spinner.py │ └── struct_parser.py └── utils │ ├── __init__.py │ ├── annotation_utils.py │ └── model_utils.py ├── refs ├── banner.png ├── banner.pptx ├── banner.svg └── logo.svg ├── requirements.txt └── setup.py /.github/scripts/fetch_models.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from urllib import request 4 | import zipfile 5 | import pickle 6 | 7 | 8 | def extract_zip(file, dest): 9 | with zipfile.ZipFile(file, "r") as f: 10 | f.extractall(dest) 11 | file_list = f.namelist() 12 | return file_list 13 | 14 | 15 | def get_model_name(file_path): 16 | with zipfile.ZipFile(os.path.join(file_path), "r") as zip_file: 17 | file_list = zip_file.namelist() 18 | for fle in file_list: 19 | if "_data.c" in fle: 20 | base = os.path.basename(fle) 21 | model_name = base.split("_data.c")[0] 22 | return model_name 23 | else: 24 | raise Exception(f"Could not find name of root model for {file_path}") 25 | 26 | 27 | def main(args): 28 | url = "https://github.com/lharri73/PySimlink-ci-models/raw/master/generated/" 29 | with request.urlopen(url + "manifest.txt") as f: 30 | files = f.readlines() 31 | with open(os.path.join(args.dir, "manifest.txt"), "wb") as f: 32 | f.writelines(files) 33 | files = list(map(lambda a: a.decode("utf-8").strip(), files)) 34 | 35 | os.makedirs(os.path.join(args.dir, "zips")) 36 | for file in files: 37 | print(f"Fetching {file}") 38 | with request.urlopen(url + file) as f: 39 | zip_file = f.read() 40 | with open(os.path.join(args.dir, "zips", file), "wb") as f: 41 | f.write(zip_file) 42 | 43 | os.makedirs(os.path.join(args.dir, "extract")) 44 | models = [] 45 | for file in files: 46 | print(f"Extracting {file}") 47 | file_list = extract_zip( 48 | os.path.join(args.dir, "zips", file), os.path.join(args.dir, "extract", file + "_e") 49 | ) 50 | 51 | data_file = None 52 | zip_file = None 53 | for fle in file_list: 54 | if fle.split(".")[-1] == "pkl": 55 | data_file = fle 56 | elif fle.split(".")[-1] == "zip": 57 | zip_file = fle 58 | if zip_file is None or data_file is None: 59 | raise Exception("invalid zip file format. Should contain data file and model zip") 60 | model_name = get_model_name(os.path.join(args.dir, "extract", file + "_e", zip_file)) 61 | 62 | models.append( 63 | [ 64 | model_name, 65 | os.path.join(args.dir, "extract", file + "_e", data_file), 66 | os.path.join(args.dir, "extract", file + "_e", zip_file), 67 | ] 68 | ) 69 | 70 | with open(os.path.join(args.dir, "manifest.pkl"), "wb") as f: 71 | pickle.dump(models, f) 72 | 73 | 74 | if __name__ == "__main__": 75 | parser = argparse.ArgumentParser() 76 | parser.add_argument("dir") 77 | args = parser.parse_args() 78 | main(args) 79 | -------------------------------------------------------------------------------- /.github/scripts/run_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import pickle 4 | import os 5 | import time 6 | import numpy as np 7 | import shutil 8 | 9 | from pysimlink import Model, GenerationError, BuildError 10 | 11 | 12 | class ModelTester(unittest.TestCase): 13 | model_path = None 14 | model_name = None 15 | data_file = None 16 | data = None 17 | 18 | def test_01_compile(self): 19 | try: 20 | tic = time.time() 21 | model = Model(self.model_name, self.model_path) 22 | toc = time.time() 23 | cur_data = {"nominal": toc - tic} 24 | with open("data.pkl", "wb") as f: 25 | pickle.dump(cur_data, f) 26 | 27 | except (GenerationError, BuildError) as e: 28 | with open(e.dump, "r") as f: 29 | lines = f.read() 30 | print(lines) 31 | print("--------------------") 32 | print("Offending CMakeLists.txt: ") 33 | with open(e.cmake, "r") as f: 34 | lines = f.read() 35 | print(lines) 36 | raise e 37 | 38 | def test_02_force_compile(self): 39 | with open("data.pkl", "rb") as f: 40 | cur_data = pickle.load(f) 41 | time.sleep(1) 42 | tic = time.perf_counter() 43 | model = Model(self.model_name, self.model_path, force_rebuild=True) 44 | self.assertGreater(time.time() - tic, cur_data["nominal"] // 2) 45 | 46 | def test_03_no_compile(self): 47 | with open("data.pkl", "rb") as f: 48 | cur_data = pickle.load(f) 49 | tic = time.time() 50 | model = Model(self.model_name, self.model_path) 51 | self.assertLess(time.time() - tic, cur_data["nominal"] // 2) 52 | 53 | def test_04_len(self): 54 | model = Model(self.model_name, self.model_path) 55 | model.reset() 56 | self.assertEqual(len(model), self.data["model_length"]) 57 | 58 | @unittest.expectedFailure 59 | def test_05_step_no_reset(self): 60 | model = Model(self.model_name, self.model_path) 61 | model.step() 62 | 63 | def test_06_step(self): 64 | model = Model(self.model_name, self.model_path) 65 | model.reset() 66 | model.step() 67 | 68 | def test_07_get_tfinal(self): 69 | model = Model(self.model_name, self.model_path) 70 | model.reset() 71 | self.assertEqual(model.tFinal, self.data["tFinal"]) 72 | 73 | @unittest.expectedFailure 74 | def test_08_get_tfinal_no_reset(self): 75 | model = Model(self.model_name, self.model_path) 76 | model.tFinal 77 | 78 | def test_09_set_tfinal(self): 79 | model = Model(self.model_name, self.model_path) 80 | model.reset() 81 | old_tfinal = model.tFinal 82 | new_tfinal = old_tfinal 83 | while new_tfinal != old_tfinal: 84 | new_tfinal = np.random.randint(5, 100) 85 | model.set_tFinal(new_tfinal) 86 | self.assertEqual(model.tFinal, new_tfinal) 87 | 88 | @unittest.expectedFailure 89 | def test_10_set_neg_tfinal(self): 90 | model = Model(self.model_name, self.model_path) 91 | model.reset() 92 | model.set_tFinal(-1) 93 | 94 | def test_11_get_step_size(self): 95 | model = Model(self.model_name, self.model_path) 96 | model.reset() 97 | self.assertEqual(model.step_size, self.data["step_size"]) 98 | 99 | @unittest.expectedFailure 100 | def test_12_get_step_size_no_reset(self): 101 | model = Model(self.model_name, self.model_path) 102 | model.step_size 103 | 104 | def test_99_cleanup(self): 105 | model = Model(self.model_name, self.model_path) 106 | if model is not None: 107 | # print("removing directories", model._model_paths.tmp_dir, model._model_paths.root_dir) 108 | shutil.rmtree(model._model_paths.tmp_dir, ignore_errors=True) 109 | shutil.rmtree(model._model_paths.root_dir, ignore_errors=True) 110 | 111 | 112 | def make_test_cls(model_name, data_file, zip_file): 113 | with open(data_file, "rb") as f: 114 | data = pickle.load(f) 115 | tester = type( 116 | "ModelTester" + model_name, 117 | (ModelTester,), 118 | {"model_path": zip_file, "model_name": model_name, "data_file": data_file, "data": data}, 119 | ) 120 | return tester 121 | 122 | 123 | def main(pth): 124 | with open(os.path.join(pth, "manifest.pkl"), "rb") as f: 125 | data = pickle.load(f) 126 | 127 | test_suite = unittest.TestSuite() 128 | for model_name, data_file, zip_file in data: 129 | tstCls = make_test_cls(model_name, data_file, zip_file) 130 | 131 | cur_cls = unittest.makeSuite(tstCls) 132 | test_suite.addTest(cur_cls) 133 | 134 | runner = unittest.TextTestRunner(failfast=True) 135 | ret = runner.run(test_suite) 136 | if ret.wasSuccessful(): 137 | exit(0) 138 | exit(1) 139 | 140 | 141 | if __name__ == "__main__": 142 | pth = sys.argv[-1] 143 | os.environ["PYSIMLINK_DEBUG"] = "TRUE" 144 | main(pth) 145 | -------------------------------------------------------------------------------- /.github/workflows/1_build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'pysimlink/**' 7 | branches: 8 | - "master" 9 | pull_request: 10 | paths: 11 | - 'pysimlink/**' 12 | 13 | jobs: 14 | wheels: 15 | name: Build Wheel 16 | runs-on: ubuntu-20.04 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.9' 24 | 25 | - name: Install deps 26 | run: pip install build twine 27 | 28 | - name: Build Wheels 29 | run: python -m build 30 | 31 | - name: Get wheel name 32 | run: | 33 | echo "::set-output name=whl_file::$(ls -1 dist/*.whl)" 34 | id: get_whl 35 | 36 | - name: Upload Wheel 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: PySimlink-wheel 40 | path: ${{ steps.get_whl.outputs.whl_file }} 41 | 42 | test_models: 43 | name: Test Models on ${{ matrix.os }} 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | matrix: 47 | os: 48 | - ubuntu-20.04 49 | # - windows-latest 50 | - macos-latest 51 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | - uses: actions/setup-python@v4 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | 59 | - name: Install PySimlink 60 | run: | 61 | python -m pip install --upgrade pip 62 | pip install -e .[dev] 63 | 64 | - name: Setup Test Env 65 | run: | 66 | mkdir ci 67 | 68 | - name: Download Models 69 | run: | 70 | python .github/scripts/fetch_models.py ci 71 | 72 | - name: Test Models 73 | run: python .github/scripts/run_tests.py ci 74 | -------------------------------------------------------------------------------- /.github/workflows/2_sphinx.yml: -------------------------------------------------------------------------------- 1 | name: sphinx 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | build-and-deploy: 10 | name: Build and gh-pages 11 | runs-on: ubuntu-20.04 12 | steps: 13 | # https://github.com/marketplace/actions/checkout 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | lfs: true 18 | # https://github.com/marketplace/actions/setup-python 19 | # ^-- This gives info on matrix testing. 20 | - name: Install Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.8 24 | # https://docs.github.com/en/actions/guides/building-and-testing-python#installing-dependencies 25 | # ^-- This gives info on installing dependencies with pip 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -e .[dev] 30 | - name: Debugging information 31 | run: | 32 | echo "github.ref:" ${{github.ref}} 33 | echo "github.event_name:" ${{github.event_name}} 34 | echo "github.head_ref:" ${{github.head_ref}} 35 | echo "github.base_ref:" ${{github.base_ref}} 36 | set -x 37 | git rev-parse --abbrev-ref HEAD 38 | git branch 39 | git branch -a 40 | git remote -v 41 | python -V 42 | pip list --not-required 43 | pip list 44 | # Build 45 | - uses: ammaraskar/sphinx-problem-matcher@master 46 | - name: Build Sphinx docs 47 | run: | 48 | cd docs 49 | make html 50 | # This fixes broken copy button icons, as explained in 51 | # https://github.com/coderefinery/sphinx-lesson/issues/50 52 | # https://github.com/executablebooks/sphinx-copybutton/issues/110 53 | # This can be removed once these PRs are accepted (but the 54 | # fixes also need to propagate to other themes): 55 | # https://github.com/sphinx-doc/sphinx/pull/8524 56 | # https://github.com/readthedocs/sphinx_rtd_theme/pull/1025 57 | sed -i 's/url_root="#"/url_root=""/' _build/html/index.html || true 58 | # The following supports building all branches and combining on 59 | # gh-pages 60 | 61 | # Clone and set up the old gh-pages branch 62 | - name: Clone old gh-pages 63 | if: ${{ github.event_name == 'push' }} 64 | run: | 65 | set -x 66 | git fetch 67 | ( git branch gh-pages remotes/origin/gh-pages && git clone . --branch=gh-pages _gh-pages/ ) || mkdir _gh-pages 68 | rm -rf _gh-pages/.git/ 69 | mkdir -p _gh-pages/branch/ 70 | # If a push and default branch, copy build to _gh-pages/ as the "main" 71 | # deployment. 72 | - name: Copy new build (default branch) 73 | if: contains(github.event_name, 'push') 74 | run: | 75 | set -x 76 | # Delete everything under _gh-pages/ that is from the 77 | # primary branch deployment. Eicludes the other branches 78 | # _gh-pages/branch-* paths, and not including 79 | # _gh-pages itself. 80 | find _gh-pages/ -mindepth 1 ! -path '_gh-pages/branch*' -delete 81 | rsync -a docs/_build/html/ _gh-pages/ 82 | # If a push and not on default branch, then copy the build to 83 | # _gh-pages/branch/$brname (transforming '/' into '--') 84 | - name: Copy new build (branch) 85 | if: contains(github.event_name, 'push') 86 | run: | 87 | set -x 88 | #brname=$(git rev-parse --abbrev-ref HEAD) 89 | brname="${{github.ref}}" 90 | brname="${brname##refs/heads/}" 91 | brdir=${brname//\//--} # replace '/' with '--' 92 | rm -rf _gh-pages/branch/${brdir} 93 | rsync -a docs/_build/html/ _gh-pages/branch/${brdir} 94 | # Go through each branch in _gh-pages/branch/, if it's not a 95 | # ref, then delete it. 96 | - name: Delete old feature branches 97 | if: ${{ github.event_name == 'push' }} 98 | run: | 99 | set -x 100 | for brdir in `ls _gh-pages/branch/` ; do 101 | brname=${brdir//--/\/} # replace '--' with '/' 102 | if ! git show-ref remotes/origin/$brname ; then 103 | echo "Removing $brdir" 104 | rm -r _gh-pages/branch/$brdir/ 105 | fi 106 | done 107 | # Deploy 108 | # https://github.com/peaceiris/actions-gh-pages 109 | - name: Deploy 110 | uses: peaceiris/actions-gh-pages@v3 111 | if: ${{ github.event_name == 'push' }} 112 | with: 113 | publish_branch: gh-pages 114 | github_token: ${{ secrets.GITHUB_TOKEN }} 115 | publish_dir: _gh-pages/ 116 | force_orphan: true 117 | -------------------------------------------------------------------------------- /.github/workflows/3_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Wheels 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | branch_check: 10 | runs-on: ubuntu-20.04 11 | outputs: 12 | tag: ${{ steps.ref.outputs.result }} 13 | master: ${{ steps.master.outputs.result }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - id: ref 17 | name: ref branch check 18 | run: echo "::set-output name=result::$(git log -1 --format='%H')" 19 | - uses: actions/checkout@v3 20 | with: 21 | ref: master 22 | 23 | - id: master 24 | name: master branch check 25 | run: echo "::set-output name=result::$(git log -1 --format='%H')" 26 | 27 | 28 | deploy_wheels: 29 | needs: branch_check 30 | name: Deploy wheels 31 | runs-on: ubuntu-20.04 32 | if: needs.branch_check.outputs.tag == needs.branch_check.outputs.master 33 | 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-python@v4 37 | 38 | - name: Install deps 39 | run: pip install build twine 40 | 41 | - name: Build Wheels 42 | run: python -m build 43 | 44 | - name: Publish a Python distribution to PyPI 45 | uses: pypa/gh-action-pypi-publish@release/v1 46 | with: 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | 49 | - name: Get Changelog 50 | id: vars 51 | run: | 52 | lineNo=$(grep -n "## v" CHANGELOG.md | awk '{split($0,a,":"); print a[1]}' | tail -n 1) 53 | lineNo=$((lineNo+1)) 54 | contents=$(tail -n +$lineNo CHANGELOG.md) 55 | echo "::set-output name=change::$contents" 56 | 57 | - name: Github Release 58 | uses: ncipollo/release-action@v1 59 | with: 60 | artifacts: "dist/*" 61 | draft: false 62 | prerelease: false 63 | body: ${{ steps.vars.outputs.change }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | models/ 2 | .vscode 3 | __pycache__ 4 | .DS_Store 5 | *.egg-info 6 | *.so 7 | *.dll 8 | build/ 9 | *.o 10 | dist/ 11 | *.log 12 | .idea 13 | cmake-build-debug 14 | 15 | tests 16 | 17 | docs/_build 18 | dev_files/ 19 | *.txt 20 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold to be exceeded before program exits with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regex patterns to the ignore-list. The 48 | # regex matches against paths and can be in Posix or Windows format. 49 | ignore-paths= 50 | 51 | # Files or directories matching the regex patterns are skipped. The regex 52 | # matches against base names, not paths. The default value ignores Emacs file 53 | # locks 54 | ignore-patterns=^\.# 55 | 56 | # List of module names for which member attributes should not be checked 57 | # (useful for modules/projects where namespaces are manipulated during runtime 58 | # and thus existing member attributes cannot be deduced by static analysis). It 59 | # supports qualified module names, as well as Unix pattern matching. 60 | ignored-modules= 61 | 62 | # Python code to execute, usually for sys.path manipulation such as 63 | # pygtk.require(). 64 | #init-hook= 65 | 66 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 67 | # number of processors available to use, and will cap the count on Windows to 68 | # avoid hangs. 69 | jobs=1 70 | 71 | # Control the amount of potential inferred values when inferring a single 72 | # object. This can help the performance when dealing with large functions or 73 | # complex, nested conditions. 74 | limit-inference-results=100 75 | 76 | # List of plugins (as comma separated values of python module names) to load, 77 | # usually to register additional checkers. 78 | load-plugins= 79 | 80 | # Pickle collected data for later comparisons. 81 | persistent=yes 82 | 83 | # Minimum Python version to use for version dependent checks. Will default to 84 | # the version used to run pylint. 85 | py-version=3.9 86 | 87 | # Discover python modules and packages in the file system subtree. 88 | recursive=no 89 | 90 | # When enabled, pylint would attempt to guess common misconfiguration and emit 91 | # user-friendly hints instead of false-positive error messages. 92 | suggestion-mode=yes 93 | 94 | # Allow loading of arbitrary C extensions. Extensions are imported into the 95 | # active Python interpreter and may run arbitrary code. 96 | unsafe-load-any-extension=no 97 | 98 | # In verbose mode, extra non-checker-related info will be displayed. 99 | #verbose= 100 | 101 | 102 | [REPORTS] 103 | 104 | # Python expression which should return a score less than or equal to 10. You 105 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 106 | # 'convention', and 'info' which contain the number of messages in each 107 | # category, as well as 'statement' which is the total number of statements 108 | # analyzed. This score is used by the global evaluation report (RP0004). 109 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 110 | 111 | # Template used to display messages. This is a python new-style format string 112 | # used to format the message information. See doc for all details. 113 | msg-template= 114 | 115 | # Set the output format. Available formats are text, parseable, colorized, json 116 | # and msvs (visual studio). You can also give a reporter class, e.g. 117 | # mypackage.mymodule.MyReporterClass. 118 | #output-format= 119 | 120 | # Tells whether to display a full report or only the messages. 121 | reports=no 122 | 123 | # Activate the evaluation score. 124 | score=yes 125 | 126 | 127 | [MESSAGES CONTROL] 128 | 129 | # Only show warnings with the listed confidence levels. Leave empty to show 130 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 131 | # UNDEFINED. 132 | confidence=HIGH, 133 | CONTROL_FLOW, 134 | INFERENCE, 135 | INFERENCE_FAILURE, 136 | UNDEFINED 137 | 138 | # Disable the message, report, category or checker with the given id(s). You 139 | # can either give multiple identifiers separated by comma (,) or put this 140 | # option multiple times (only on the command line, not in the configuration 141 | # file where it should appear only once). You can also use "--disable=all" to 142 | # disable everything first and then re-enable specific checks. For example, if 143 | # you want to run only the similarities checker, you can use "--disable=all 144 | # --enable=similarities". If you want to run only the classes checker, but have 145 | # no Warning level messages displayed, use "--disable=all --enable=classes 146 | # --disable=W". 147 | disable=raw-checker-failed, 148 | bad-inline-option, 149 | locally-disabled, 150 | file-ignored, 151 | suppressed-message, 152 | useless-suppression, 153 | deprecated-pragma, 154 | use-symbolic-message-instead, 155 | C0301, 156 | C0114, 157 | C0103, 158 | W0611, 159 | R0902, 160 | R0913, 161 | C0415, 162 | E0401 163 | 164 | # Enable the message, report, category or checker with the given id(s). You can 165 | # either give multiple identifier separated by comma (,) or put this option 166 | # multiple time (only on the command line, not in the configuration file where 167 | # it should appear only once). See also the "--disable" option for examples. 168 | enable=c-extension-no-member 169 | 170 | 171 | [LOGGING] 172 | 173 | # The type of string formatting that logging methods do. `old` means using % 174 | # formatting, `new` is for `{}` formatting. 175 | logging-format-style=old 176 | 177 | # Logging modules to check that the string format arguments are in logging 178 | # function parameter format. 179 | logging-modules=logging 180 | 181 | 182 | [SPELLING] 183 | 184 | # Limits count of emitted suggestions for spelling mistakes. 185 | max-spelling-suggestions=4 186 | 187 | # Spelling dictionary name. Available dictionaries: none. To make it work, 188 | # install the 'python-enchant' package. 189 | spelling-dict= 190 | 191 | # List of comma separated words that should be considered directives if they 192 | # appear at the beginning of a comment and should not be checked. 193 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 194 | 195 | # List of comma separated words that should not be checked. 196 | spelling-ignore-words= 197 | 198 | # A path to a file that contains the private dictionary; one word per line. 199 | spelling-private-dict-file= 200 | 201 | # Tells whether to store unknown words to the private dictionary (see the 202 | # --spelling-private-dict-file option) instead of raising a message. 203 | spelling-store-unknown-words=no 204 | 205 | 206 | [MISCELLANEOUS] 207 | 208 | # List of note tags to take in consideration, separated by a comma. 209 | notes=FIXME, 210 | XXX, 211 | TODO 212 | 213 | # Regular expression of note tags to take in consideration. 214 | notes-rgx= 215 | 216 | 217 | [TYPECHECK] 218 | 219 | # List of decorators that produce context managers, such as 220 | # contextlib.contextmanager. Add to this list to register other decorators that 221 | # produce valid context managers. 222 | contextmanager-decorators=contextlib.contextmanager 223 | 224 | # List of members which are set dynamically and missed by pylint inference 225 | # system, and so shouldn't trigger E1101 when accessed. Python regular 226 | # expressions are accepted. 227 | generated-members= 228 | 229 | # Tells whether to warn about missing members when the owner of the attribute 230 | # is inferred to be None. 231 | ignore-none=yes 232 | 233 | # This flag controls whether pylint should warn about no-member and similar 234 | # checks whenever an opaque object is returned when inferring. The inference 235 | # can return multiple potential results while evaluating a Python object, but 236 | # some branches might not be evaluated, which results in partial inference. In 237 | # that case, it might be useful to still emit no-member and other checks for 238 | # the rest of the inferred objects. 239 | ignore-on-opaque-inference=yes 240 | 241 | # List of symbolic message names to ignore for Mixin members. 242 | ignored-checks-for-mixins=no-member, 243 | not-async-context-manager, 244 | not-context-manager, 245 | attribute-defined-outside-init 246 | 247 | # List of class names for which member attributes should not be checked (useful 248 | # for classes with dynamically set attributes). This supports the use of 249 | # qualified names. 250 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 251 | 252 | # Show a hint with possible names when a member name was not found. The aspect 253 | # of finding the hint is based on edit distance. 254 | missing-member-hint=yes 255 | 256 | # The minimum edit distance a name should have in order to be considered a 257 | # similar match for a missing member name. 258 | missing-member-hint-distance=1 259 | 260 | # The total number of similar names that should be taken in consideration when 261 | # showing a hint for a missing member. 262 | missing-member-max-choices=1 263 | 264 | # Regex pattern to define which classes are considered mixins. 265 | mixin-class-rgx=.*[Mm]ixin 266 | 267 | # List of decorators that change the signature of a decorated function. 268 | signature-mutators= 269 | 270 | 271 | [CLASSES] 272 | 273 | # Warn about protected attribute access inside special methods 274 | check-protected-access-in-special-methods=no 275 | 276 | # List of method names used to declare (i.e. assign) instance attributes. 277 | defining-attr-methods=__init__, 278 | __new__, 279 | setUp, 280 | __post_init__ 281 | 282 | # List of member names, which should be excluded from the protected access 283 | # warning. 284 | exclude-protected=_asdict, 285 | _fields, 286 | _replace, 287 | _source, 288 | _make 289 | 290 | # List of valid names for the first argument in a class method. 291 | valid-classmethod-first-arg=cls 292 | 293 | # List of valid names for the first argument in a metaclass class method. 294 | valid-metaclass-classmethod-first-arg=cls 295 | 296 | 297 | [VARIABLES] 298 | 299 | # List of additional names supposed to be defined in builtins. Remember that 300 | # you should avoid defining new builtins when possible. 301 | additional-builtins= 302 | 303 | # Tells whether unused global variables should be treated as a violation. 304 | allow-global-unused-variables=yes 305 | 306 | # List of names allowed to shadow builtins 307 | allowed-redefined-builtins= 308 | 309 | # List of strings which can identify a callback function by name. A callback 310 | # name must start or end with one of those strings. 311 | callbacks=cb_, 312 | _cb 313 | 314 | # A regular expression matching the name of dummy variables (i.e. expected to 315 | # not be used). 316 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 317 | 318 | # Argument names that match this expression will be ignored. Default to name 319 | # with leading underscore. 320 | ignored-argument-names=_.*|^ignored_|^unused_ 321 | 322 | # Tells whether we should check for unused import in __init__ files. 323 | init-import=no 324 | 325 | # List of qualified module names which can have objects that can redefine 326 | # builtins. 327 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 328 | 329 | 330 | [FORMAT] 331 | 332 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 333 | expected-line-ending-format= 334 | 335 | # Regexp for a line that is allowed to be longer than the limit. 336 | ignore-long-lines=^\s*(# )??$ 337 | 338 | # Number of spaces of indent required inside a hanging or continued line. 339 | indent-after-paren=4 340 | 341 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 342 | # tab). 343 | indent-string=' ' 344 | 345 | # Maximum number of characters on a single line. 346 | max-line-length=100 347 | 348 | # Maximum number of lines in a module. 349 | max-module-lines=1000 350 | 351 | # Allow the body of a class to be on the same line as the declaration if body 352 | # contains single statement. 353 | single-line-class-stmt=no 354 | 355 | # Allow the body of an if to be on the same line as the test if there is no 356 | # else. 357 | single-line-if-stmt=yes 358 | 359 | 360 | [IMPORTS] 361 | 362 | # List of modules that can be imported at any level, not just the top level 363 | # one. 364 | allow-any-import-level= 365 | 366 | # Allow wildcard imports from modules that define __all__. 367 | allow-wildcard-with-all=no 368 | 369 | # Deprecated modules which should not be used, separated by a comma. 370 | deprecated-modules= 371 | 372 | # Output a graph (.gv or any supported image format) of external dependencies 373 | # to the given file (report RP0402 must not be disabled). 374 | ext-import-graph= 375 | 376 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 377 | # external) dependencies to the given file (report RP0402 must not be 378 | # disabled). 379 | import-graph= 380 | 381 | # Output a graph (.gv or any supported image format) of internal dependencies 382 | # to the given file (report RP0402 must not be disabled). 383 | int-import-graph= 384 | 385 | # Force import order to recognize a module as part of the standard 386 | # compatibility libraries. 387 | known-standard-library= 388 | 389 | # Force import order to recognize a module as part of a third party library. 390 | known-third-party=enchant 391 | 392 | # Couples of modules and preferred modules, separated by a comma. 393 | preferred-modules= 394 | 395 | 396 | [EXCEPTIONS] 397 | 398 | # Exceptions that will emit a warning when caught. 399 | overgeneral-exceptions=BaseException, 400 | Exception 401 | 402 | 403 | [REFACTORING] 404 | 405 | # Maximum number of nested blocks for function / method body 406 | max-nested-blocks=5 407 | 408 | # Complete name of functions that never returns. When checking for 409 | # inconsistent-return-statements if a never returning function is called then 410 | # it will be considered as an explicit return statement and no message will be 411 | # printed. 412 | never-returning-functions=sys.exit,argparse.parse_error 413 | 414 | 415 | [SIMILARITIES] 416 | 417 | # Comments are removed from the similarity computation 418 | ignore-comments=yes 419 | 420 | # Docstrings are removed from the similarity computation 421 | ignore-docstrings=yes 422 | 423 | # Imports are removed from the similarity computation 424 | ignore-imports=yes 425 | 426 | # Signatures are removed from the similarity computation 427 | ignore-signatures=yes 428 | 429 | # Minimum lines number of a similarity. 430 | min-similarity-lines=4 431 | 432 | 433 | [DESIGN] 434 | 435 | # List of regular expressions of class ancestor names to ignore when counting 436 | # public methods (see R0903) 437 | exclude-too-few-public-methods= 438 | 439 | # List of qualified class names to ignore when counting class parents (see 440 | # R0901) 441 | ignored-parents= 442 | 443 | # Maximum number of arguments for function / method. 444 | max-args=5 445 | 446 | # Maximum number of attributes for a class (see R0902). 447 | max-attributes=7 448 | 449 | # Maximum number of boolean expressions in an if statement (see R0916). 450 | max-bool-expr=5 451 | 452 | # Maximum number of branch for function / method body. 453 | max-branches=12 454 | 455 | # Maximum number of locals for function / method body. 456 | max-locals=15 457 | 458 | # Maximum number of parents for a class (see R0901). 459 | max-parents=7 460 | 461 | # Maximum number of public methods for a class (see R0904). 462 | max-public-methods=20 463 | 464 | # Maximum number of return / yield for function / method body. 465 | max-returns=6 466 | 467 | # Maximum number of statements in function / method body. 468 | max-statements=50 469 | 470 | # Minimum number of public methods for a class (see R0903). 471 | min-public-methods=2 472 | 473 | 474 | [STRING] 475 | 476 | # This flag controls whether inconsistent-quotes generates a warning when the 477 | # character used as a quote delimiter is used inconsistently within a module. 478 | check-quote-consistency=yes 479 | 480 | # This flag controls whether the implicit-str-concat should generate a warning 481 | # on implicit string concatenation in sequences defined over several lines. 482 | check-str-concat-over-line-jumps=no 483 | 484 | 485 | [BASIC] 486 | 487 | # Naming style matching correct argument names. 488 | argument-naming-style=snake_case 489 | 490 | # Regular expression matching correct argument names. Overrides argument- 491 | # naming-style. If left empty, argument names will be checked with the set 492 | # naming style. 493 | #argument-rgx= 494 | 495 | # Naming style matching correct attribute names. 496 | attr-naming-style=snake_case 497 | 498 | # Regular expression matching correct attribute names. Overrides attr-naming- 499 | # style. If left empty, attribute names will be checked with the set naming 500 | # style. 501 | #attr-rgx= 502 | 503 | # Bad variable names which should always be refused, separated by a comma. 504 | bad-names=foo, 505 | bar, 506 | baz, 507 | toto, 508 | tutu, 509 | tata 510 | 511 | # Bad variable names regexes, separated by a comma. If names match any regex, 512 | # they will always be refused 513 | bad-names-rgxs= 514 | 515 | # Naming style matching correct class attribute names. 516 | class-attribute-naming-style=any 517 | 518 | # Regular expression matching correct class attribute names. Overrides class- 519 | # attribute-naming-style. If left empty, class attribute names will be checked 520 | # with the set naming style. 521 | #class-attribute-rgx= 522 | 523 | # Naming style matching correct class constant names. 524 | class-const-naming-style=UPPER_CASE 525 | 526 | # Regular expression matching correct class constant names. Overrides class- 527 | # const-naming-style. If left empty, class constant names will be checked with 528 | # the set naming style. 529 | #class-const-rgx= 530 | 531 | # Naming style matching correct class names. 532 | class-naming-style=PascalCase 533 | 534 | # Regular expression matching correct class names. Overrides class-naming- 535 | # style. If left empty, class names will be checked with the set naming style. 536 | #class-rgx= 537 | 538 | # Naming style matching correct constant names. 539 | const-naming-style=UPPER_CASE 540 | 541 | # Regular expression matching correct constant names. Overrides const-naming- 542 | # style. If left empty, constant names will be checked with the set naming 543 | # style. 544 | #const-rgx= 545 | 546 | # Minimum line length for functions/classes that require docstrings, shorter 547 | # ones are exempt. 548 | docstring-min-length=-1 549 | 550 | # Naming style matching correct function names. 551 | function-naming-style=snake_case 552 | 553 | # Regular expression matching correct function names. Overrides function- 554 | # naming-style. If left empty, function names will be checked with the set 555 | # naming style. 556 | #function-rgx= 557 | 558 | # Good variable names which should always be accepted, separated by a comma. 559 | good-names=f, 560 | i, 561 | j, 562 | k, 563 | ex, 564 | Run, 565 | _ 566 | 567 | # Good variable names regexes, separated by a comma. If names match any regex, 568 | # they will always be accepted 569 | good-names-rgxs= 570 | 571 | # Include a hint for the correct naming format with invalid-name. 572 | include-naming-hint=no 573 | 574 | # Naming style matching correct inline iteration names. 575 | inlinevar-naming-style=any 576 | 577 | # Regular expression matching correct inline iteration names. Overrides 578 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 579 | # with the set naming style. 580 | #inlinevar-rgx= 581 | 582 | # Naming style matching correct method names. 583 | method-naming-style=snake_case 584 | 585 | # Regular expression matching correct method names. Overrides method-naming- 586 | # style. If left empty, method names will be checked with the set naming style. 587 | #method-rgx= 588 | 589 | # Naming style matching correct module names. 590 | module-naming-style=snake_case 591 | 592 | # Regular expression matching correct module names. Overrides module-naming- 593 | # style. If left empty, module names will be checked with the set naming style. 594 | #module-rgx= 595 | 596 | # Colon-delimited sets of names that determine each other's naming style when 597 | # the name regexes allow several styles. 598 | name-group= 599 | 600 | # Regular expression which should only match function or class names that do 601 | # not require a docstring. 602 | no-docstring-rgx=^_ 603 | 604 | # List of decorators that produce properties, such as abc.abstractproperty. Add 605 | # to this list to register other decorators that produce valid properties. 606 | # These decorators are taken in consideration only for invalid-name. 607 | property-classes=abc.abstractproperty 608 | 609 | # Regular expression matching correct type variable names. If left empty, type 610 | # variable names will be checked with the set naming style. 611 | #typevar-rgx= 612 | 613 | # Naming style matching correct variable names. 614 | variable-naming-style=snake_case 615 | 616 | # Regular expression matching correct variable names. Overrides variable- 617 | # naming-style. If left empty, variable names will be checked with the set 618 | # naming style. 619 | #variable-rgx= 620 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.0 4 | Initial Release 5 | 6 | ## v1.0.1 7 | Bug Fixes 8 | 9 | - change ssize_t to long int 10 | - update readme 11 | 12 | ## v1.0.2 13 | Update manifest 14 | 15 | - Change wheel build method 16 | 17 | ## v1.1.0 18 | Multithreading 19 | 20 | - Add support for mutlithreading the `Model` class 21 | - Fix __len__ property 22 | 23 | ## v1.1.1 24 | Importlib Changes 25 | 26 | - Change c module generation and use importlib to 27 | dynamically import multiple simulink models 28 | - Update test cases 29 | 30 | ## v1.1.2 31 | Compatibility Updates 32 | 33 | - Some models don't implement tFinal 34 | - Support for not enabling mat file logging 35 | - Added spinner during compile time 36 | 37 | ## v1.2.0 38 | Bus signal capability 39 | 40 | - Fixed scalar valued block/model parameters 41 | - Added struct capability 42 | - Fixed array->scalar when reading signals 43 | - Added math header for other solvers 44 | - Pinned fasteners requirement 45 | - Fixed cleanup of zipped files 46 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pysimlink *.py *.cpp *.hpp *.tpp 2 | recursive-include tests *.py 3 | include *.* 4 | prune dist/* 5 | 6 | prune */__pycache__ 7 | global-exclude *.pyc *.pyo *.pyd *.swp *.bak *~ *.o 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ![PyPI](https://img.shields.io/pypi/v/pysimlink) 6 | 7 | **PySimlink** is a python package that automatically compiles Simulink codegen files 8 | into a simple interface that you can interact with in Python! 9 | 10 | With this package, you can: 11 | - Interact with the internals of your Simulink model 12 | - Run the model in "accelerator mode" 13 | - Send and receive data in the form of numpy arrays 14 | - Run multiple instances of the same model 15 | 16 | All without requiring a MATLAB runtime on the target machine! No C/C++ programming required! 17 | 18 | To get started, you either need a copy of the generated model you want to simulate or, to generate 19 | the code yourself, you need the Simulink Coder. There are some limitations, namely that your model *must* use a fixed step solver 20 | (a requirement of the grt target). 21 | 22 | ## Demo 23 | 24 | ### Read Signal Values 25 | 26 | ```python 27 | from pysimlink import Model 28 | 29 | model = Model("my_awesome_model", "model.zip") 30 | model.reset() 31 | 32 | for i in range(len(model)): 33 | model.step() 34 | signal_val = model.get_signal(block_path="Constant1", sig_name="Signal1") 35 | print(signal_val) 36 | 37 | ``` 38 | 39 | ### Change Block Parameters 40 | 41 | ```python 42 | from pysimlink import Model 43 | import numpy as np 44 | 45 | model = Model("my_awesome_model", "model.zip") 46 | model.reset() 47 | 48 | new_param = np.eye(3) 49 | model.set_block_param(block="Constant1", param="Value", value=new_param) 50 | ``` 51 | 52 | ### Change a Model's Final Time Step 53 | 54 | ```python 55 | from pysimlink import Model 56 | 57 | model = Model("my_awesome_model", "model.zip") 58 | model.reset() 59 | model.set_tFinal(500) 60 | 61 | print(model.tFinal) 62 | ``` 63 | 64 | And more... 65 | 66 | Check out the [docs](https://lharri73.github.io/PySimlink/) to get started! 67 | 68 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/imgs/banner_white.svg: -------------------------------------------------------------------------------- 1 | 2 | PySimlink 193 | -------------------------------------------------------------------------------- /docs/_static/imgs/code_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/code_interface.png -------------------------------------------------------------------------------- /docs/_static/imgs/code_interface_advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/code_interface_advanced.png -------------------------------------------------------------------------------- /docs/_static/imgs/codegen_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/codegen_settings.png -------------------------------------------------------------------------------- /docs/_static/imgs/gen_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/gen_code.png -------------------------------------------------------------------------------- /docs/_static/imgs/model_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/model_settings.png -------------------------------------------------------------------------------- /docs/_static/imgs/signal_highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/signal_highlight.png -------------------------------------------------------------------------------- /docs/_static/imgs/signal_highlight2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/signal_highlight2.png -------------------------------------------------------------------------------- /docs/_static/imgs/solver_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/solver_settings.png -------------------------------------------------------------------------------- /docs/_static/imgs/toolbar_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/docs/_static/imgs/toolbar_1.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | import sys, os 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | sys.path.insert(0, os.path.abspath("../pysimlink")) 21 | project = "PySimlink" 22 | copyright = "2023, Landon Harris" 23 | author = "Landon Harris" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "1.2.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.viewcode", 37 | "sphinx.ext.githubpages", 38 | "sphinx.ext.napoleon", 39 | "sphinx_toolbox.collapse", 40 | "sphinx_search.extension", 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ["_templates"] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 50 | 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "sphinx_rtd_theme" 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | html_static_path = ["_static"] 63 | 64 | html_context = { 65 | "display_github": True, # Integrate GitHub 66 | "github_user": "lharri73", # Username 67 | "github_repo": "PySimlink", # Repo name 68 | "github_version": "master", # Version 69 | "conf_py_path": "/docs/", # Path in the checkout to the docs root 70 | } 71 | 72 | html_logo = "_static/imgs/banner_white.svg" 73 | html_theme_options = { 74 | "logo_only": True, 75 | "display_version": False, 76 | "analytics_id": "G-K9ME9NLFYB", 77 | "canonical_url": "https://lharri73.github.io/PySimlink/", 78 | } 79 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Python + Simulink = PySimlink! 2 | ============================== 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | 7 | src/quickstart 8 | src/howto 9 | src/autodoc 10 | src/license 11 | 12 | **PySimlink** is a python package that compiles Simulink codegen files (with additional 13 | mixins) into a simple interface that you can interact with in Python! The impetus to 14 | create this project stems from the frustration with how difficult it is to model dynamical 15 | systems in pure Python. While there are tools that make this task possible, why reinvent the 16 | wheel when Simulink is so well suited for the task. 17 | 18 | **With this package, you can**: 19 | 20 | * Read and set block parameters 21 | * Read and set model parameters 22 | * Read signal values 23 | * Change the simulation stop time of your model 24 | * Run a model in "accelerator mode" 25 | * Run multiple instances of the same model 26 | 27 | All without the MATLAB runtime! 28 | 29 | **What PySimlink can't do:** 30 | 31 | * Add or remove blocks to a model (the structure of the model is final once code is generated) 32 | * Interact with models using variable step solvers 33 | * Some signals and blocks are reduced/optimized during code generation. These are not accessible. 34 | 35 | * `virtual blocks `_ 36 | are not compiled and cannot be accessed by PySimlink. 37 | * Change the value of signals during simulation 38 | 39 | * Changing the value of a signal could cause a singularity in derivatives/integrals depending on the solver. 40 | This also would only affect the major time step and would be overwritten during the signal update of the next minor timestep. 41 | 42 | * See the :ref:`How-To guide ` for a workaround. 43 | 44 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/src/autodoc.rst: -------------------------------------------------------------------------------- 1 | .. _autodoc: 2 | 3 | API 4 | === 5 | 6 | Model 7 | ----- 8 | 9 | .. autoclass:: pysimlink.Model 10 | :members: 11 | :undoc-members: 12 | :special-members: __init__, __len__ 13 | 14 | 15 | Model Structures 16 | ---------------- 17 | 18 | .. autoclass:: pysimlink.types.ModelInfo 19 | :members: 20 | 21 | .. autoclass:: pysimlink.types.ModelParam 22 | :members: 23 | 24 | .. autoclass:: pysimlink.types.BlockParam 25 | :members: 26 | 27 | .. autoclass:: pysimlink.types.Signal 28 | :members: 29 | 30 | .. autoclass:: pysimlink.types.DataType 31 | :members: 32 | 33 | Utility Functions 34 | ----------------- 35 | 36 | .. autofunction:: pysimlink.print_all_params 37 | 38 | 39 | Errors 40 | ------ 41 | 42 | .. autoclass:: pysimlink.BuildError 43 | 44 | .. autoclass:: pysimlink.GenerationError 45 | -------------------------------------------------------------------------------- /docs/src/howto.rst: -------------------------------------------------------------------------------- 1 | How-To and Basic Usage Guide 2 | ============================ 3 | 4 | Install PySimlink 5 | ----------------- 6 | PySimlink installs with pip, but requires compiler to build models. This compiler needs to be discoverable by the 7 | cmake binary that's installed with pip. 8 | 9 | While mingw is a valid compiler on Windows, it's much simpler to use the Visual Studio tools. 10 | 11 | .. _compiler setup: 12 | 13 | Setup 14 | ^^^^^ 15 | 16 | A couple of os-specific steps are required if you've never done any c/c++ development before. Don't worry though, this 17 | doesn't mean you have to dig into the c++ code. We just need to be able to compile it. 18 | 19 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 20 | | Windows | Download `Visual Studio `_ (Only the "Desktop Development with C++" workload) [#f1]_ | 21 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 22 | | OSX | Install the `Command Line Tools for Xcode `_. | 23 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 24 | | Linux | All you need is gcc, g++, ld, and make. How you get that is up to your distribution. | 25 | +---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 26 | 27 | If you use ninja or another build system, you'll need to modify the 28 | :code:`generator` argument of the :code:`Model` constructor to match. You can 29 | see the list of available generators by running :code:`cmake --help` (these are 30 | what cmake knows how to generate, not what is installed on your system). 31 | 32 | .. [#f1] You can also use WSL instead and install gcc & g++. 33 | 34 | Install PySimlink with pip 35 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 36 | .. code-block:: bash 37 | 38 | pip install pysimlink 39 | 40 | 41 | Generate Code From Your Simulink Model 42 | -------------------------------------- 43 | .. note:: To be able to generate code, you must have a license to 44 | the `Simulink Coder `_. 45 | 46 | PySimlink requires a set of model parameters be set before the code generation process. 47 | This comes with a few limitations, the most important is that the model must run using a fixed-step solver. 48 | 49 | .. _gen model params: 50 | 51 | 1. Update Model Parameters 52 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 | .. tip:: If you are using model references, try using `configuration references `_ 54 | so you don't have to change these settings in **each** model's configuration set. 55 | 56 | PySimlink requires a few model settings to generate code in a format that it can interact with. 57 | 58 | You can do this either using the script provided in the :ref:`Quickstart page ` 59 | *or* manually by changing the parameters manually in the following steps. 60 | 61 | First, open the model settings under the modelling tab. 62 | 63 | .. figure:: /_static/imgs/model_settings.png 64 | :height: 85 65 | :align: center 66 | 67 | Model Settings located in the modelling Menu Bar. 68 | 69 | Solver Settings 70 | """"""""""""""" 71 | On the :guilabel:`Solver` page, make sure you're using a :guilabel:`Fixed-step` solver. Also uncheck the 72 | :guilabel:`Treat each discrete rate as a separate task` box. Finally, uncheck the :guilabel:`Allow tasks to execute concurrently on the target` box. 73 | 74 | .. figure:: /_static/imgs/solver_settings.png 75 | :align: center 76 | :height: 500 77 | 78 | Solver settings 79 | 80 | Code Generation Settings 81 | """""""""""""""""""""""" 82 | 83 | On the :guilabel:`Code Generation` page, use :file:`grt.tlc` as the System target file. Also check the :guilabel:`Generate code only` and 84 | the :guilabel:`Package code and artifacts` boxes. This will generate a zip file with all of the code generated that you 85 | can use with PySimlink. Since we're not building anything, uncheck the :guilabel:`Generate makefile` box. 86 | 87 | .. figure:: /_static/imgs/codegen_settings.png 88 | :align: center 89 | :height: 500 90 | 91 | Code Generation Settings 92 | 93 | Code Interface Settings 94 | """"""""""""""""""""""" 95 | On the :guilabel:`Code Generation -> Interface` page, check the :guilabel:`signals`, :guilabel:`parameters`, :guilabel:`states`, 96 | and :guilabel:`root-level I/O` (even if you won't be interacting with some of them, PySimlink requires these functions to be defined). 97 | 98 | Next, scroll down and click the :guilabel:`...` at the bottom of this page to reveal more Advanced parameters. 99 | 100 | .. figure:: /_static/imgs/code_interface.png 101 | :align: center 102 | :height: 500 103 | 104 | Basic Code Generation Interface Settings 105 | 106 | Under the :guilabel:`Advanced parameters` section, *uncheck* the :guilabel:`Classic call interface` box and check the 107 | :guilabel:`Single output/update function` box. 108 | 109 | .. figure:: /_static/imgs/code_interface_advanced.png 110 | :align: center 111 | :height: 500 112 | 113 | Advanced Code Generation Interface Settings 114 | 115 | Done with model settings! If it's a model reference, you'll need to propagate these changes to each model. 116 | 117 | 2. Generate Code! 118 | ^^^^^^^^^^^^^^^^^ 119 | Now you can execute the code generation step. Open the Simulink Coder app and click the :guilabel:`Generate Code` button! 120 | 121 | .. figure:: /_static/imgs/toolbar_1.png 122 | :height: 85 123 | :align: center 124 | 125 | Simulink Coder in the Simulink Apps Menu Bar. 126 | 127 | .. figure:: /_static/imgs/gen_code.png 128 | :height: 85 129 | :align: center 130 | 131 | :guilabel:`Generate Code` button within Simulink Coder. 132 | 133 | This will produce the :file:`my_awesome_model.zip` file that you can use with PySimlink. Unless you modified the 134 | :guilabel:`Zip file name` parameter in the Code generation settings, this will also be the name of your root model. 135 | 136 | Find the Name of Your Root Model Without Simulink 137 | ------------------------------------------------- 138 | Let's say the file you've been given is called :file:`my_awesome_model.zip`. It's a good guess that the root model is 139 | called "my_awesome_mode", but here's how you can double check without having to whip out Simulink. 140 | 141 | Inside the archive, you'll find 2 folders (if you don't, then this was not generated by Simulink and PySimlink will 142 | throw an error). One folder will contain :file:`extern`, :file:`rtw`, and :file:`simulink`. The other will contain two 143 | folders: :file:`slprj` and :file:`[your_model_name]_grt_rtw`. 144 | 145 | In short, it looks like this: 146 | 147 | .. code-block:: 148 | 149 | my_awesome_model.zip/ 150 | ├─ [a]/ 151 | │ ├─ extern/ 152 | │ ├─ rtw/ 153 | │ ├─ simulink/ 154 | ├─ [b]/ 155 | │ ├─ slprj/ 156 | │ ├─ [your_model_name]_grt_rtw/ 157 | 158 | :file:`your_model_name` is the name of the root model. 159 | 160 | 161 | Compile and Run a Model 162 | ----------------------- 163 | You've been given a zip file with a generated model (or you've generated it yourself. Way to go!). Now you want to 164 | run it in Python. It's as simple as importing PySimlink and calling :code:`print_all_params` 165 | 166 | .. code-block:: python 167 | 168 | from pysimlink import Model, print_all_params 169 | 170 | model = Model("my_awesome_model", "./my_awesome_model.zip") 171 | model.reset() 172 | print_all_params(model) 173 | 174 | 175 | Once you've figured out what signals you need to read, you can call :code:`model.step()` to iterate over the model! 176 | 177 | .. _change signals: 178 | 179 | Change the Value of Signals 180 | --------------------------- 181 | This feature is not directly supported by PySimlink. The reason why is kind of complex. In short, it would work for 182 | the fixed-step discrete solver. But for a solver with minor timesteps, changing the value of a signal could cause a 183 | singularity in an integrator, or might not even affect anything at all (if the signal is changed on the major time step 184 | from PySimlink, then updated by the originating block at the next minor timestep (which PySimlink does not control), 185 | then the change from PySimlink would have no affect). 186 | 187 | How do we get around this? You'll have to tweak the model and generate it again... While this is not ideal, this keeps 188 | us from violating solver during simulation. 189 | 190 | Take this model, for example. And say you want to change the value of the highlighted signal during simulation. 191 | 192 | .. figure:: /_static/imgs/signal_highlight.png 193 | :height: 290 194 | :align: center 195 | 196 | A garbage model with a signal whose value we want to change during simulation. 197 | 198 | To be able to change the value, we need to terminate the signal and replace it with a constant. The resulting change 199 | looks like this. 200 | 201 | .. figure:: /_static/imgs/signal_highlight2.png 202 | :height: 290 203 | :align: center 204 | 205 | The same model adjusted so we can change the value of this signal during simulation. 206 | 207 | Now, during simulation, we change the "Value" parameter of the block labeled "Constant". While this may not simulate 208 | properly in Simulink, you can change this value at every timestep to get your desired behavior. 209 | 210 | .. tip:: If you're making changes to your model like this just for code generation or for PySimlink and don't want to 211 | change its normal behavior, check out `Variant Subsystems `_. 212 | You can have one subsystem for code generation and one for Simulink simulation. 213 | 214 | Read The Value of Bus Signals 215 | ----------------------------- 216 | 217 | .. note:: New in pysimlink 1.2.0 218 | 219 | Bus signals are a bit more complicated than other signals. 220 | They are a collection of other signals, and can even be nested in other busses. PySimlink 221 | allows you to read bus signal types and their contents. To see the available signals, 222 | you can use the :code:`dir` function. 223 | 224 | .. code-block:: python 225 | 226 | model = Model("my_awesome_model", "./my_awesome_model.zip") 227 | model.reset() 228 | struct_signal = model.get_signal("my_awesome_model/sig1") 229 | print(dir(struct_signal)) 230 | 231 | 232 | .. note:: There are a few python-internal properties prepended and appended by two 233 | underscores. These are not part of your bus and can be ignored. 234 | 235 | You can access the signals in the bus as follows: 236 | 237 | .. code-block:: python 238 | 239 | print(struct_signal.a) 240 | print(struct_signal.b) 241 | 242 | 243 | -------------------------------------------------------------------------------- /docs/src/license.rst: -------------------------------------------------------------------------------- 1 | License & Notices 2 | ================= 3 | .. _license: 4 | 5 | PySimlink 6 | --------- 7 | PySimlink is free software: you can redistribute it and/or modify it under the terms of the 8 | GNU General Public License as published by the Free Software Foundation, either version 3 9 | of the License, or (at your option) any later version. 10 | 11 | PySimlink is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | See the GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along with Foobar. 16 | If not, see . 17 | 18 | **What PySimlink is not:** 19 | 20 | * A replacement to the `Simulink Coder `_. 21 | 22 | * This does not generate code for a Simulink model. You still need the Simulink Coder. 23 | 24 | * A replacement for the `MATLAB Engine For Python `_ 25 | 26 | * This does not interact with MATLAB in any way. It only interacts with your generated model. 27 | 28 | * A repository containing **any** Mathworks library header files. 29 | 30 | * Distributing library header files from any Mathworks program or its content is a violation 31 | of the MathWorks license agreement, with the exception of code generated by the Simulink Coder. 32 | 33 | * See the `MathWorks license agreement `_ 34 | (sec. 3.1) for more details. 35 | 36 | MathWorks 37 | --------- 38 | MATLAB, Simulink, Stateflow, Embedded Coder, Simulink Coder, and Real-Time Workshop 39 | are registered trademarks, of `The MathWorks, Inc `_. 40 | -------------------------------------------------------------------------------- /docs/src/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | If you already have the generated model code, skip to step 2. If you get the joy 4 | of generating the model source, well start from the top. 5 | 6 | Step 1: Generate the Model's Code. 7 | ---------------------------------- 8 | This is likely the hardest part of the whole process. But we've tried to make it 9 | simple for you. You just need to check a few settings and build the model. If 10 | you're feeling bold, you can copy-paste a script to do it for you! 11 | 12 | Here's said script: 13 | 14 | .. _param set script: 15 | 16 | .. code-block:: matlab 17 | 18 | model = 'MyAwesomeModel'; % <-- The name of your model...change this! 19 | configs = getActiveConfigSet(model); 20 | mustProp=false; 21 | if isa(configs, 'Simulink.ConfigSetRef') 22 | configs = eval(configs.SourceName); 23 | mustProp=true; 24 | end 25 | 26 | set_param(configs, 'ConcurrentTasks', 'off'); 27 | set_param(configs, 'EnableMultiTasking', 'off'); 28 | set_param(configs, 'SystemTargetFile', 'grt.tlc'); 29 | set_param(configs, 'GenerateMakefile', 'off'); 30 | set_param(configs, 'GenCodeOnly', 'on'); 31 | set_param(configs, 'PackageGeneratedCodeAndArtifacts', 'on'); 32 | set_param(configs, 'RTWCAPIParams', 'on'); 33 | set_param(configs, 'RTWCAPISignals', 'on'); 34 | set_param(configs, 'RTWCAPIRootIO', 'on'); 35 | set_param(configs, 'RTWCAPIStates', 'on'); 36 | set_param(configs, 'GRTInterface', 'off'); 37 | set_param(configs, 'CombineOutputUpdateFcns', 'on'); 38 | 39 | if mustProp 40 | Simulink.BlockDiagram.propagateConfigSet(model); 41 | end 42 | slbuild(model); 43 | 44 | What's going on here? 45 | 46 | It's just changing a few settings in your model's config. These are the minimum 47 | settings required for PySimlink, and should not affect your model's ability to 48 | build (knock on wood)... 49 | 50 | Want to do this manually? Check out the :ref:`How-To guide ` for details. 51 | 52 | Step 2: Let PySimlink Handle The Rest 53 | ------------------------------------- 54 | At this point, you should have a copy of the model in the form of generated code. 55 | Let's say it's called :file:`my_awesome_model.zip` and the name of your root model is "my_awesome_model". 56 | 57 | .. tip:: The zip file does not needed to be named after the model. To slightly improve build time, you can also extract 58 | the zip file and provide the path to the directory that contains the zip folder's contents instead. 59 | 60 | First, install PySimlink 61 | 62 | .. code-block:: bash 63 | 64 | pip install pysimlink 65 | 66 | Now, you can import and inspect the model. 67 | 68 | .. code-block:: python 69 | 70 | from pysimlink import Model, print_all_params 71 | 72 | my_awesome_model = Model("my_awesome_model", "./my_awesome_model.zip") 73 | my_awesome_mode.reset() 74 | print_all_params(my_awesome_model) 75 | 76 | 77 | Which will show you all of the parameters that you can view and change. 78 | 79 | .. code-block:: 80 | 81 | Parameters for model at 'my_awesome_model' 82 | model parameters: 83 | block parameters: 84 | Block: 'my_awesome_model/Constant' | Parameter: 'Value' | data_type: 'float64 (double) dims: [3, 3, 2] order: rtwCAPI_Orientation.col_major_nd' 85 | Block: 'my_awesome_model/Constant2' | Parameter: 'Value' | data_type: 'float64 (double) dims: [3, 4, 2] order: rtwCAPI_Orientation.col_major_nd' 86 | Block: 'my_awesome_model/Constant4' | Parameter: 'Value' | data_type: 'float64 (double) dims: [3, 4, 3] order: rtwCAPI_Orientation.col_major_nd' 87 | signals: 88 | Block: 'my_awesome_model/Clock' | Signal Name: '' | data_type: 'float64 (double) dims: [1, 1] order: rtwCAPI_Orientation.scalar' 89 | Block: 'my_awesome_model/Clock1' | Signal Name: '' | data_type: 'float64 (double) dims: [1, 1] order: rtwCAPI_Orientation.scalar' 90 | ... 91 | 92 | Now you can run the model and start manipulating parameters. 93 | 94 | .. code-block:: python 95 | 96 | from pysimlink import Model 97 | import numpy as np 98 | 99 | my_awesome_model = Model("my_awesome_model", "./my_awesome_model.zip") 100 | my_awesome_model.reset() 101 | 102 | for i in range(len(my_awesome_model)): 103 | Constant = my_awesome_model.get_block_param("my_awesome_model/Constant", param="Value") 104 | print(Constant) # np.ndarray 105 | new_val = np.full((3,3,2), i) 106 | my_awesome_model.set_block_param("my_awesome_model/Constant", param="Value", value=new_val) 107 | 108 | Have a Generation error? Couldn't find a compiler or :code:`nmake -? not found`? See the :ref:`compiler setup ` page. 109 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=59.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 100 7 | target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] 8 | include = '\.pyi?$' 9 | exclude = ''' 10 | /( 11 | \.toml 12 | |\.sh 13 | |\.git 14 | |\.ini 15 | |\.c 16 | |\.cpp 17 | |\.hpp 18 | |\.h 19 | |\.txt 20 | )/ 21 | ''' 22 | -------------------------------------------------------------------------------- /pysimlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .lib.model import Model 2 | from .lib.exceptions import BuildError, GenerationError 3 | from .utils.model_utils import print_all_params 4 | from .lib import model_types as types 5 | from .utils import annotation_utils as anno 6 | 7 | __all__ = ["Model", "BuildError", "GenerationError", "print_all_params", "types", "anno"] 8 | -------------------------------------------------------------------------------- /pysimlink/c_files/include/containers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C"{ 4 | #include "rtw_capi.h" 5 | #include "rtw_modelmap.h" 6 | #include "<>" 7 | } 8 | 9 | #include 10 | #include 11 | #include "pybind11/pybind11.h" 12 | 13 | #ifndef ssize_t 14 | #define ssize_t long int 15 | #endif 16 | 17 | namespace PYSIMLINK{ 18 | class map_key_2s{ 19 | public: 20 | std::string a; 21 | std::string b; 22 | const rtwCAPI_ModelMappingInfo *c; 23 | bool operator==(const map_key_2s &other) const{ 24 | if(c != other.c) return false; 25 | 26 | return ((a == other.a) && (b == other.b)); 27 | } 28 | }; 29 | class map_key_1s{ 30 | public: 31 | std::string a; 32 | const rtwCAPI_ModelMappingInfo *c; 33 | bool operator==(const map_key_1s &other) const{ 34 | if(c != other.c) return false; 35 | return (a == other.a); 36 | } 37 | }; 38 | 39 | struct pair_hash{ 40 | std::size_t operator()(const map_key_2s &p) const; 41 | std::size_t operator()(const map_key_1s &p) const; 42 | }; 43 | struct Compare{ 44 | bool operator()(const map_key_2s &lhs, const map_key_2s &rhs) const; 45 | bool operator()(const map_key_1s &lhs, const map_key_1s &rhs) const; 46 | }; 47 | 48 | 49 | struct DataType{ 50 | std::string cDataType; 51 | std::string pythonType; 52 | std::string mwDataType; 53 | std::vector dims; 54 | rtwCAPI_Orientation orientation; 55 | }; 56 | 57 | struct ModelParam{ 58 | std::string model_param; 59 | struct DataType data_type; 60 | }; 61 | 62 | struct BlockParam{ 63 | std::string block_name; 64 | std::string block_param; 65 | struct DataType data_type; 66 | }; 67 | 68 | struct Signal{ 69 | std::string block_name; 70 | std::string signal_name; 71 | struct DataType data_type; 72 | }; 73 | 74 | struct ModelInfo{ 75 | std::string model_name; 76 | std::vector model_params; 77 | std::vector block_params; 78 | std::vector signals; 79 | }; 80 | 81 | struct BufferLike{ 82 | void *ptr; 83 | ssize_t itemsize; 84 | char format[256]; 85 | ssize_t ndim; 86 | ssize_t shape[32]; 87 | ssize_t strides[32]; 88 | bool readonly; 89 | }; 90 | 91 | struct signal_info { 92 | bool is_array; 93 | char struct_name[128]; 94 | unsigned int type_size; 95 | union _garb { 96 | void *addr; 97 | BufferLike arr; 98 | } data; 99 | }; 100 | 101 | union all_dtypes { 102 | void *addr; 103 | <> 104 | }; 105 | }; 106 | -------------------------------------------------------------------------------- /pysimlink/c_files/include/dev_tools.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by landon on 8/8/22. 3 | // 4 | #pragma once 5 | #include "model_utils.hpp" 6 | 7 | #if false 8 | namespace PYSIMLINK{ 9 | void print_model_params(const rtwCAPI_ModelMappingInfo *mmi); 10 | void print_block_params(const rtwCAPI_ModelMappingInfo *mmi); 11 | void print_signals(const rtwCAPI_ModelMappingInfo *mmi); 12 | void print_params_recursive(const rtwCAPI_ModelMappingInfo *child_mmi); 13 | }; 14 | 15 | } 16 | #endif -------------------------------------------------------------------------------- /pysimlink/c_files/include/model_interface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define NAN_CHECK(num) if(isnan(num)) num=0; 4 | 5 | extern "C"{ 6 | #include "rtw_capi.h" 7 | #include "rtw_modelmap_logging.h" 8 | <> 9 | #include "rtw_modelmap.h" 10 | #include "<>" 11 | #include "<>" 12 | } 13 | 14 | #include 15 | #include "model_utils.hpp" 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "pybind11/pybind11.h" 22 | #include "pybind11/numpy.h" 23 | #include "pybind11/stl.h" 24 | 25 | /* create generic macros that work with any model */ 26 | # define EXPAND_CONCAT(name1,name2) name1 ## name2 27 | # define CONCAT(name1,name2) EXPAND_CONCAT(name1,name2) 28 | # define MODEL_INITIALIZE CONCAT(MODEL,_initialize) 29 | # define MODEL_STEP CONCAT(MODEL,_step) 30 | # define MODEL_TERMINATE CONCAT(MODEL,_terminate) 31 | # define RT_MDL CONCAT(MODEL,_M) 32 | 33 | namespace py = pybind11; 34 | 35 | namespace PYSIMLINK{ 36 | class Model{ 37 | public: 38 | Model(std::string name); 39 | ~Model(); 40 | 41 | void step(int num_steps); 42 | void reset(); 43 | 44 | std::vector get_params() const; 45 | py::array get_sig(const std::string& model, const std::string& path, const std::string &sig_name); 46 | PYSIMLINK::all_dtypes get_sig_union(const std::string &model, const std::string &path, const std::string &sig_name); 47 | py::array get_block_param(const std::string& model, const std::string& block_path, const std::string& param); 48 | py::array get_model_param(const std::string& model, const std::string& param); 49 | 50 | template 51 | void set_block_param(const std::string& model, const std::string &block_path, const std::string ¶m, py::array_t value); 52 | 53 | template 54 | void set_model_param(const std::string& model, const std::string ¶m, py::array_t value); 55 | 56 | struct PYSIMLINK::DataType block_param_info(const std::string &model, const std::string& block_path, const std::string& param); 57 | struct PYSIMLINK::DataType model_param_info(const std::string &model, const std::string& param); 58 | struct PYSIMLINK::DataType signal_info(const std::string &model, const std::string &block_path, const std::string &signal); 59 | 60 | double step_size(); 61 | double tFinal(); 62 | void set_tFinal(float); 63 | std::vector get_models() const; 64 | 65 | protected: 66 | bool initialized; 67 | void discover_mmis(const rtwCAPI_ModelMappingInfo *mmi); 68 | static void terminate(); 69 | std::string mdl_name; 70 | 71 | rtwCAPI_ModelMappingInfo *root_mmi; 72 | boolean_T OverrunFlags[1]; /* ISR overrun flags */ 73 | boolean_T eventFlags[1]; /* necessary for overlapping preemption */ 74 | std::map mmi_map; 75 | 76 | std::unordered_map model_param; 77 | std::unordered_map sig_map; 78 | std::unordered_map block_map; 79 | }; 80 | 81 | #include "model_interface.tpp" 82 | 83 | }; 84 | -------------------------------------------------------------------------------- /pysimlink/c_files/include/model_interface.tpp: -------------------------------------------------------------------------------- 1 | 2 | template 3 | void Model::set_block_param(const std::string &model, const std::string &block_path, const std::string ¶m, 4 | py::array_t value) { 5 | if(!initialized){ 6 | throw std::runtime_error("Model must be initialized before calling get_block_param. Call `reset()` first!"); 7 | } 8 | if(block_path.empty()) 9 | throw std::runtime_error("No path provided to get_block_param!"); 10 | if(model.empty()) 11 | throw std::runtime_error("No model name provided to get_block_param!"); 12 | if(param.empty()) 13 | throw std::runtime_error("No parameter provided to get_block_param!"); 14 | auto mmi_idx = mmi_map.find(model); 15 | if(mmi_idx == mmi_map.end()){ 16 | char buf[512]; 17 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 18 | throw std::runtime_error(buf); 19 | } 20 | PYSIMLINK::set_block_param(mmi_idx->second, block_path.c_str(), param.c_str(), value); 21 | } 22 | 23 | template 24 | void Model::set_model_param(const std::string &model, const std::string ¶m, py::array_t value) { 25 | if(!initialized){ 26 | throw std::runtime_error("Model must be initialized before calling get_block_param. Call `reset()` first!"); 27 | } 28 | if(model.empty()) 29 | throw std::runtime_error("No model name provided to get_block_param!"); 30 | if(param.empty()) 31 | throw std::runtime_error("No parameter provided to get_block_param!"); 32 | auto mmi_idx = mmi_map.find(model); 33 | if(mmi_idx == mmi_map.end()){ 34 | char buf[256]; 35 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 36 | throw std::runtime_error(buf); 37 | } 38 | PYSIMLINK::set_model_param(mmi_idx->second, param.c_str(), value); 39 | } 40 | -------------------------------------------------------------------------------- /pysimlink/c_files/include/model_utils.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by landon on 10/24/21. 3 | // 4 | #pragma once 5 | extern "C"{ 6 | #include "<>" 7 | #include "rtw_capi.h" 8 | #include "rtw_modelmap_logging.h" 9 | <> 10 | #include "rtw_modelmap.h" 11 | } 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | //#include 18 | #include 19 | #include "pybind11/pybind11.h" 20 | #include "pybind11/numpy.h" 21 | #include "pybind11/stl.h" 22 | 23 | #include "containers.hpp" 24 | #include "safe_utils.hpp" 25 | 26 | namespace py = pybind11; 27 | 28 | namespace PYSIMLINK{ 29 | 30 | const std::map c_python_dtypes = { 31 | {"char", "int8"}, 32 | {"unsigned char", "uint8"}, 33 | {"short", "int16"}, 34 | {"unsigned short", "uint16"}, 35 | {"int", "int32"}, 36 | {"unsigned int", "uint32"}, 37 | {"float", "float32"}, 38 | {"double", "float64"} 39 | }; 40 | 41 | std::string translate_c_type_name(const std::string& c_name, bool should_throw=false); 42 | 43 | py::buffer_info get_model_param(const rtwCAPI_ModelMappingInfo *mmi, const char *param, std::unordered_map &model_params); 44 | void set_model_param(const rtwCAPI_ModelMappingInfo *mmi, 45 | const char* param, 46 | py::array value); 47 | 48 | py::buffer_info get_block_param(const rtwCAPI_ModelMappingInfo *mmi, const char *block, const char *param, std::unordered_map &block_map); 49 | void set_block_param(const rtwCAPI_ModelMappingInfo *mmi, 50 | const char *block, 51 | const char *param, 52 | py::array value); 53 | 54 | struct std::unique_ptr get_signal_val(const rtwCAPI_ModelMappingInfo *mmi, std::unordered_map &sig_map, const char* block=nullptr, const char* signNam=nullptr); 55 | struct PYSIMLINK::DataType describe_signal(const rtwCAPI_ModelMappingInfo *mmi, const char* block, const char* sigName, std::unordered_map &sig_map); 56 | 57 | PYSIMLINK::BufferLike 58 | format_pybuffer(const rtwCAPI_ModelMappingInfo *mmi, rtwCAPI_DataTypeMap dt, rtwCAPI_DimensionMap sigDim, void *addr); 59 | void format_pybuffer(const rtwCAPI_ModelMappingInfo *mmi, rtwCAPI_DataTypeMap dt, rtwCAPI_DimensionMap sigDim, void *addr, PYSIMLINK::BufferLike *ret); 60 | 61 | void fill_from_buffer(const rtwCAPI_ModelMappingInfo *mmi, rtwCAPI_DataTypeMap dt, rtwCAPI_DimensionMap blockDim, void* addr, py::array value); 62 | py::buffer_info from_buffer_struct(const PYSIMLINK::BufferLike &buffer); 63 | 64 | struct PYSIMLINK::DataType describe_block_param(const rtwCAPI_ModelMappingInfo *mmi, const char *block_path, const char *param); 65 | struct PYSIMLINK::DataType describe_model_param(const rtwCAPI_ModelMappingInfo *mmi, const char *param); 66 | 67 | std::vector debug_model_params(const rtwCAPI_ModelMappingInfo *mmi); 68 | std::vector debug_block_param(const rtwCAPI_ModelMappingInfo *mmi); 69 | std::vector debug_signals(const rtwCAPI_ModelMappingInfo *mmi); 70 | PYSIMLINK::ModelInfo debug_model_info(const rtwCAPI_ModelMappingInfo *mmi); 71 | 72 | 73 | #include "model_utils.tpp" 74 | }; 75 | -------------------------------------------------------------------------------- /pysimlink/c_files/include/model_utils.tpp: -------------------------------------------------------------------------------- 1 | template 2 | void validate_scalar(const rtwCAPI_ModelMappingInfo *mmi, T param, const char* funcName, const char* identifier){ 3 | rtwCAPI_DimensionMap sigDim = mmi->staticMap->Maps.dimensionMap[param.dimIndex]; 4 | rtwCAPI_DataTypeMap dt = mmi->staticMap->Maps.dataTypeMap[param.dataTypeIndex]; 5 | 6 | if(sigDim.orientation != rtwCAPI_Orientation::rtwCAPI_SCALAR){ 7 | std::stringstream err(""); 8 | err << funcName << ": signal (" << identifier << ") has too many dimensions(" << (int)sigDim.numDims; 9 | err << ") or is not a scalar(" << sigDim.orientation << ")"; 10 | throw std::runtime_error(err.str().c_str()); 11 | } 12 | if(dt.isPointer){ 13 | std::stringstream err(""); 14 | err << funcName << ": Cannot read value from pointer (isPointer=True) for parameter (" << identifier << ")"; 15 | throw std::runtime_error(err.str().c_str()); 16 | } 17 | } 18 | template 19 | struct DataType populate_dtype(const rtwCAPI_ModelMappingInfo *mmi, T capi_struct){ 20 | struct DataType ret; 21 | ret.cDataType = rtwCAPI_GetDataTypeMap(mmi)[capi_struct.dataTypeIndex].cDataName; 22 | ret.pythonType = PYSIMLINK::translate_c_type_name(ret.cDataType); 23 | ret.mwDataType = rtwCAPI_GetDataTypeMap(mmi)[capi_struct.dataTypeIndex].mwDataName; 24 | ret.orientation = rtwCAPI_GetDimensionMap(mmi)[capi_struct.dimIndex].orientation; 25 | for(size_t j = 0; j < rtwCAPI_GetDimensionMap(mmi)[capi_struct.dimIndex].numDims; j++){ 26 | ret.dims.push_back(rtwCAPI_GetDimensionArray(mmi)[rtwCAPI_GetDimensionMap(mmi)[capi_struct.dimIndex].dimArrayIndex + j]); 27 | } 28 | return ret; 29 | } -------------------------------------------------------------------------------- /pysimlink/c_files/include/safe_utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace PYSIMLINK{ 5 | std::string safe_string(const char* chars); 6 | }; -------------------------------------------------------------------------------- /pysimlink/c_files/src/bindings.cpp: -------------------------------------------------------------------------------- 1 | #include "model_interface.hpp" 2 | #include "containers.hpp" 3 | #include 4 | 5 | extern "C"{ 6 | #include "<>" 7 | #include "<>" 8 | } 9 | 10 | 11 | #define NEW_TEMPLATE_FUNC(A,B) \ 12 | .def(A, B) \ 13 | .def(A, B) \ 14 | .def(A, B) \ 15 | .def(A, B) \ 16 | .def(A, B) \ 17 | .def(A, B) \ 18 | .def(A, B) \ 19 | .def(A, B) 20 | 21 | 22 | namespace py = pybind11; 23 | 24 | PYBIND11_MODULE(<>, m) { 25 | py::class_(m, "<>_Model", py::module_local()) 26 | .def(py::init()) 27 | .def("reset", &PYSIMLINK::Model::reset) 28 | .def("step_size", &PYSIMLINK::Model::step_size) 29 | .def("tFinal", &PYSIMLINK::Model::tFinal) 30 | .def("set_tFinal", &PYSIMLINK::Model::set_tFinal) 31 | .def("step", &PYSIMLINK::Model::step) 32 | .def("get_models", &PYSIMLINK::Model::get_models) 33 | .def("get_signal_arr", &PYSIMLINK::Model::get_sig) 34 | .def("get_signal_union", &PYSIMLINK::Model::get_sig_union) 35 | .def("desc_signal", &PYSIMLINK::Model::signal_info) 36 | .def("get_block_param", &PYSIMLINK::Model::get_block_param) 37 | NEW_TEMPLATE_FUNC("set_block_param", &PYSIMLINK::Model::set_block_param) 38 | .def("get_model_param", &PYSIMLINK::Model::get_model_param) 39 | NEW_TEMPLATE_FUNC("set_model_param", &PYSIMLINK::Model::set_model_param) 40 | .def("get_params", &PYSIMLINK::Model::get_params) 41 | .def("block_param_info", &PYSIMLINK::Model::block_param_info) 42 | .def("model_param_info", &PYSIMLINK::Model::model_param_info); 43 | 44 | py::enum_(m, "<>_rtwCAPI_Orientation", py::module_local()) 45 | .value("vector", rtwCAPI_VECTOR) 46 | .value("scalar", rtwCAPI_SCALAR) 47 | .value("col_major_nd", rtwCAPI_MATRIX_COL_MAJOR_ND) 48 | .value("col_major", rtwCAPI_MATRIX_COL_MAJOR) 49 | .value("row_major_nd", rtwCAPI_MATRIX_ROW_MAJOR_ND) 50 | .value("row_major", rtwCAPI_MATRIX_ROW_MAJOR); 51 | 52 | py::class_(m, "<>_BlockParam", py::module_local()) 53 | .def_readonly("block_name", &PYSIMLINK::BlockParam::block_name) 54 | .def_readonly("block_param", &PYSIMLINK::BlockParam::block_param) 55 | .def_readonly("data_type", &PYSIMLINK::BlockParam::data_type); 56 | 57 | py::class_(m, "<>_Signal", py::module_local()) 58 | .def_readonly("block_name", &PYSIMLINK::Signal::block_name) 59 | .def_readonly("signal_name", &PYSIMLINK::Signal::signal_name) 60 | .def_readonly("data_type", &PYSIMLINK::Signal::data_type); 61 | 62 | py::class_(m, "<>_ModelParam", py::module_local()) 63 | .def_readonly("model_param", &PYSIMLINK::ModelParam::model_param) 64 | .def_readonly("data_type", &PYSIMLINK::ModelParam::data_type); 65 | 66 | py::class_(m, "<>_ModelInfo", py::module_local()) 67 | .def_readonly("model_name", &PYSIMLINK::ModelInfo::model_name) 68 | .def_readonly("model_params", &PYSIMLINK::ModelInfo::model_params) 69 | .def_readonly("block_params", &PYSIMLINK::ModelInfo::block_params) 70 | .def_readonly("signals", &PYSIMLINK::ModelInfo::signals); 71 | 72 | py::class_(m, "<>_DataType", py::module_local()) 73 | .def_readonly("cDataType", &PYSIMLINK::DataType::cDataType) 74 | .def_readonly("pythonType", &PYSIMLINK::DataType::pythonType) 75 | .def_readonly("mwType", &PYSIMLINK::DataType::mwDataType) 76 | .def_readonly("dims", &PYSIMLINK::DataType::dims) 77 | .def_readonly("orientation", &PYSIMLINK::DataType::orientation); 78 | 79 | <> 80 | } 81 | -------------------------------------------------------------------------------- /pysimlink/c_files/src/dev_tools.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by landon on 8/8/22. 3 | // 4 | #include "dev_tools.hpp" 5 | 6 | #if false 7 | void PYSIMLINK::print_model_params(const rtwCAPI_ModelMappingInfo *mmi){ 8 | uint_T numParams = get_num_model_params(mmi); 9 | const rtwCAPI_ModelParameters* capiModelParams = rtwCAPI_GetModelParameters(mmi); 10 | printf("model params for %s\n", mmi->InstanceMap.path); 11 | for (size_t i=0; i < numParams; i++) { 12 | printf("\tname: %s\n", capiModelParams[i].varName); 13 | } 14 | } 15 | 16 | void PYSIMLINK::print_block_params(const rtwCAPI_ModelMappingInfo *mmi){ 17 | const rtwCAPI_BlockParameters *capiBlockParams = rtwCAPI_GetBlockParameters(mmi); 18 | uint_T nParams = get_num_block_params(mmi); 19 | printf("Block params for %s\n", mmi->InstanceMap.path); 20 | for (size_t i=0; i < nParams; i++) { 21 | printf("\tBlock path: %-128s\tparam name: %s\n", capiBlockParams[i].blockPath, capiBlockParams[i].paramName); 22 | } 23 | } 24 | 25 | void PYSIMLINK::print_signals(const rtwCAPI_ModelMappingInfo *mmi){ 26 | uint_T numSigs = get_num_signals(mmi); 27 | const rtwCAPI_Signals *capiSignals = rtwCAPI_GetSignals(mmi); 28 | printf("Signals for %s\n", mmi->InstanceMap.path); 29 | for(size_t i = 0; i < numSigs; i++){ 30 | printf("\tBlock path: %-128s\tsignal_name: %s\n", capiSignals[i].blockPath, capiSignals[i].signalName); 31 | } 32 | } 33 | 34 | 35 | void PYSIMLINK::print_params_recursive(const rtwCAPI_ModelMappingInfo *child_mmi){ 36 | print_model_params(child_mmi); 37 | print_block_params(child_mmi); 38 | print_signals(child_mmi); 39 | for(size_t i = 0; i < child_mmi->InstanceMap.childMMIArrayLen; i++){ 40 | print_params_recursive(child_mmi->InstanceMap.childMMIArray[i]); 41 | } 42 | } 43 | 44 | #endif -------------------------------------------------------------------------------- /pysimlink/c_files/src/hash.cpp: -------------------------------------------------------------------------------- 1 | #include "model_utils.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace PYSIMLINK{ 8 | 9 | std::size_t pair_hash::operator()(const map_key_2s &p) const{ 10 | size_t a = std::hash{}(p.a); 11 | size_t b = std::hash{}(p.b); 12 | size_t c = std::hash{}((void*)p.c); 13 | 14 | size_t combined = a+b+c; 15 | return combined; 16 | } 17 | 18 | std::size_t pair_hash::operator()(const map_key_1s &p) const{ 19 | size_t a = std::hash{}(p.a); 20 | size_t b = std::hash{}((void*)p.c); 21 | 22 | return a+b; 23 | } 24 | 25 | bool Compare::operator()(const map_key_2s &lhs, const map_key_2s &rhs) const{ 26 | return lhs == rhs; 27 | } 28 | bool Compare::operator()(const map_key_1s &lhs, const map_key_1s &rhs) const{ 29 | return lhs==rhs; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /pysimlink/c_files/src/model_interface.cpp: -------------------------------------------------------------------------------- 1 | #include "model_interface.hpp" 2 | #include 3 | 4 | #ifdef _WIN32 5 | #define NULL_FILE "NUL" 6 | #else 7 | #define NULL_FILE "/dev/null" 8 | #endif 9 | 10 | using namespace PYSIMLINK; 11 | 12 | namespace py = pybind11; 13 | 14 | Model::~Model(){ 15 | if(initialized){ 16 | terminate(); 17 | } 18 | } 19 | 20 | Model::Model(std::string name){ 21 | initialized = false; 22 | memset(OverrunFlags, 0, sizeof(boolean_T)); 23 | memset(eventFlags, 0, sizeof(boolean_T)); 24 | root_mmi = nullptr; 25 | mdl_name = name; 26 | } 27 | 28 | void Model::terminate(){ 29 | #ifdef rtmGetRTWLogInfo 30 | rt_StopDataLogging(NULL_FILE,rtmGetRTWLogInfo(RT_MDL)); 31 | #endif 32 | MODEL_TERMINATE(); 33 | } 34 | 35 | void Model::reset(){ 36 | // clear all model mapping information bc it may change 37 | // between runs (verify this) 38 | model_param.clear(); 39 | sig_map.clear(); 40 | block_map.clear(); 41 | 42 | // clear loggind data 43 | if(initialized){ 44 | terminate(); 45 | } 46 | MODEL_INITIALIZE(); 47 | 48 | // get the MMI 49 | root_mmi = &(rtmGetDataMapInfo(RT_MDL).mmi); 50 | 51 | mmi_map.insert(std::make_pair(mdl_name, root_mmi)); 52 | discover_mmis(root_mmi); 53 | initialized = true; 54 | } 55 | 56 | double Model::step_size() { 57 | if(!initialized){ 58 | throw std::runtime_error("Model must be initialized before calling step_size. Call `reset()` first!"); 59 | } 60 | return RT_MDL->Timing.stepSize0; 61 | } 62 | 63 | std::vector Model::get_params() const{ 64 | if(!initialized){ 65 | throw std::runtime_error("Model must be initialized before calling print_params. Call `reset()` first!"); 66 | } 67 | std::vector ret; 68 | ret.reserve(mmi_map.size()); 69 | 70 | for(auto it : mmi_map){ 71 | struct ModelInfo to_add; 72 | to_add.model_name = it.first; 73 | to_add.block_params = debug_block_param(it.second); 74 | to_add.model_params = debug_model_params(it.second); 75 | to_add.signals = debug_signals(it.second); 76 | ret.push_back(to_add); 77 | } 78 | return ret; 79 | } 80 | 81 | void Model::discover_mmis(const rtwCAPI_ModelMappingInfo *mmi){ 82 | // go through all child mmis and insert them into the map. 83 | for(size_t i = 0; i < mmi->InstanceMap.childMMIArrayLen; i++){ 84 | mmi_map.insert(std::make_pair(std::string(mmi->InstanceMap.childMMIArray[i]->InstanceMap.path), mmi->InstanceMap.childMMIArray[i])); 85 | discover_mmis(mmi->InstanceMap.childMMIArray[i]); 86 | } 87 | } 88 | 89 | void Model::step(int num_steps){ 90 | assert(((void)"num_steps must be a positive number", num_steps>0)); 91 | if(!initialized){ 92 | throw std::runtime_error("Model must be initialized before calling step. Call `reset()` first!"); 93 | } 94 | 95 | for(int cur_step=0; cur_step < num_steps; cur_step++){ 96 | if (OverrunFlags[0]++) 97 | rtmSetErrorStatus(RT_MDL, "Overrun"); 98 | 99 | if (rtmGetErrorStatus(RT_MDL) != NULL) { 100 | char buf[256]; 101 | sprintf(buf, "Model is in errored state: %s", rtmGetErrorStatus(RT_MDL)); 102 | throw std::runtime_error(buf); 103 | } 104 | 105 | MODEL_STEP(); 106 | 107 | OverrunFlags[0]--; 108 | } 109 | } 110 | 111 | double Model::tFinal() { 112 | if(!initialized){ 113 | throw std::runtime_error("Model must be initialized before calling tFinal. Call `reset()` first!"); 114 | } 115 | #ifndef rtmGetFinalTime 116 | throw std::runtime_error("Getting/Setting tFinal is not supported for this model."); 117 | #else 118 | return rtmGetFinalTime(RT_MDL); 119 | #endif 120 | } 121 | 122 | void Model::set_tFinal(float tFinal){ 123 | if(!initialized){ 124 | throw std::runtime_error("Model must be initialized before calling set_tFinal. Call `reset()` first!"); 125 | } 126 | 127 | #ifndef rtmSetTFinal 128 | throw std::runtime_error("Getting/Setting tFinal is not supported for this model."); 129 | #else 130 | rtmSetTFinal(RT_MDL, tFinal); 131 | #endif 132 | } 133 | 134 | std::vector Model::get_models() const{ 135 | if(!initialized){ 136 | throw std::runtime_error("Model must be initialized before calling get_models. Call `reset()` first!"); 137 | } 138 | std::vector ret; 139 | ret.reserve(mmi_map.size()); 140 | 141 | for(auto i : mmi_map){ 142 | ret.push_back(i.first); 143 | } 144 | 145 | return ret; 146 | } 147 | 148 | PYSIMLINK::DataType Model::signal_info(const std::string &model, const std::string &block_path, 149 | const std::string &signal) { 150 | if(!initialized){ 151 | throw std::runtime_error("Model must be initialized before calling get_sig. Call `reset()` first!"); 152 | } 153 | 154 | if(block_path.empty()) 155 | throw std::runtime_error("No path provided to get_sig!"); 156 | if(model.empty()) 157 | throw std::runtime_error("No model name provided to get_sig!"); 158 | 159 | auto mmi_idx = mmi_map.find(model); 160 | if(mmi_idx == mmi_map.end()){ 161 | char buf[256]; 162 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 163 | throw std::runtime_error(buf); 164 | } 165 | 166 | const char* sig_name = signal.empty() ? nullptr : signal.c_str(); 167 | return PYSIMLINK::describe_signal(mmi_idx->second, block_path.c_str(), sig_name, sig_map); 168 | } 169 | 170 | py::array Model::get_sig(const std::string& model, const std::string& block_path, const std::string& sig_name_raw){ 171 | if(!initialized){ 172 | throw std::runtime_error("Model must be initialized before calling get_sig. Call `reset()` first!"); 173 | } 174 | 175 | if(block_path.empty()) 176 | throw std::runtime_error("No path provided to get_sig!"); 177 | if(model.empty()) 178 | throw std::runtime_error("No model name provided to get_sig!"); 179 | 180 | auto mmi_idx = mmi_map.find(model); 181 | if(mmi_idx == mmi_map.end()){ 182 | char buf[256]; 183 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 184 | throw std::runtime_error(buf); 185 | } 186 | 187 | 188 | const char* sig_name = sig_name_raw.empty() ? nullptr : sig_name_raw.c_str(); 189 | auto ret = PYSIMLINK::get_signal_val(mmi_idx->second, sig_map, block_path.c_str(), sig_name); 190 | // py::handle tmp(ret); 191 | return py::array(PYSIMLINK::from_buffer_struct(ret->data.arr)); 192 | } 193 | 194 | py::array Model::get_block_param(const std::string& model, const std::string& block_path, const std::string& param){ 195 | if(!initialized){ 196 | throw std::runtime_error("Model must be initialized before calling get_block_param. Call `reset()` first!"); 197 | } 198 | 199 | if(block_path.empty()) 200 | throw std::runtime_error("No path provided to get_block_param!"); 201 | if(model.empty()) 202 | throw std::runtime_error("No model name provided to get_block_param!"); 203 | if(param.empty()) 204 | throw std::runtime_error("No parameter provided to get_block_param!"); 205 | 206 | auto mmi_idx = mmi_map.find(model); 207 | if(mmi_idx == mmi_map.end()){ 208 | char buf[256]; 209 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 210 | throw std::runtime_error(buf); 211 | } 212 | py::buffer_info ret = PYSIMLINK::get_block_param(mmi_idx->second, block_path.c_str(), param.c_str(), block_map); 213 | return py::array(ret); 214 | } 215 | 216 | struct PYSIMLINK::DataType Model::block_param_info(const std::string &model, const std::string& block_path, const std::string& param){ 217 | if(!initialized){ 218 | throw std::runtime_error("Model must be initialized before calling get_block_param. Call `reset()` first!"); 219 | } 220 | if(block_path.empty()) 221 | throw std::runtime_error("No path provided to get_block_param!"); 222 | if(model.empty()) 223 | throw std::runtime_error("No model name provided to get_block_param!"); 224 | if(param.empty()) 225 | throw std::runtime_error("No parameter provided to get_block_param!"); 226 | auto mmi_idx = mmi_map.find(model); 227 | if(mmi_idx == mmi_map.end()){ 228 | char buf[256]; 229 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 230 | throw std::runtime_error(buf); 231 | } 232 | return PYSIMLINK::describe_block_param(mmi_idx->second, block_path.c_str(), param.c_str()); 233 | } 234 | 235 | py::array Model::get_model_param(const std::string &model, const std::string ¶m) { 236 | if(!initialized){ 237 | throw std::runtime_error("Model must be initialized before calling get_block_param. Call `reset()` first!"); 238 | } 239 | 240 | if(model.empty()) 241 | throw std::runtime_error("No model name provided to get_model_param!"); 242 | if(param.empty()) 243 | throw std::runtime_error("No parameter provided to get_model_param!"); 244 | 245 | auto mmi_idx = mmi_map.find(model); 246 | if(mmi_idx == mmi_map.end()){ 247 | char buf[256]; 248 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 249 | throw std::runtime_error(buf); 250 | } 251 | py::buffer_info ret = PYSIMLINK::get_model_param(mmi_idx->second, param.c_str(), model_param); 252 | return py::array(ret); 253 | } 254 | 255 | struct PYSIMLINK::DataType Model::model_param_info(const std::string &model, const std::string ¶m) { 256 | if(!initialized){ 257 | throw std::runtime_error("Model must be initialized before calling get_block_param. Call `reset()` first!"); 258 | } 259 | 260 | if(model.empty()) 261 | throw std::runtime_error("No model name provided to get_model_param!"); 262 | if(param.empty()) 263 | throw std::runtime_error("No parameter provided to get_model_param!"); 264 | auto mmi_idx = mmi_map.find(model); 265 | if(mmi_idx == mmi_map.end()){ 266 | char buf[256]; 267 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 268 | throw std::runtime_error(buf); 269 | } 270 | return PYSIMLINK::describe_model_param(mmi_idx->second, param.c_str()); 271 | } 272 | 273 | all_dtypes PYSIMLINK::Model::get_sig_union(const std::string &model, const std::string &block_path, 274 | const std::string &sig_name_raw) { 275 | all_dtypes ret; 276 | if(!initialized){ 277 | throw std::runtime_error("Model must be initialized before calling get_sig. Call `reset()` first!"); 278 | } 279 | 280 | if(block_path.empty()) 281 | throw std::runtime_error("No path provided to get_sig!"); 282 | if(model.empty()) 283 | throw std::runtime_error("No model name provided to get_sig!"); 284 | 285 | auto mmi_idx = mmi_map.find(model); 286 | if(mmi_idx == mmi_map.end()){ 287 | char buf[256]; 288 | sprintf(buf, "Cannot find model with name: %s", model.c_str()); 289 | throw std::runtime_error(buf); 290 | } 291 | 292 | 293 | const char* sig_name = sig_name_raw.empty() ? nullptr : sig_name_raw.c_str(); 294 | auto sig_info = PYSIMLINK::get_signal_val(mmi_idx->second, sig_map, block_path.c_str(), sig_name); 295 | (void)memcpy(&ret, sig_info->data.addr, sig_info->type_size); 296 | return ret; 297 | } 298 | -------------------------------------------------------------------------------- /pysimlink/c_files/src/safe_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "safe_utils.hpp" 2 | 3 | std::string PYSIMLINK::safe_string(const char* chars){ 4 | if(chars == nullptr){ 5 | return {"Null"}; 6 | }else{ 7 | return {chars}; 8 | } 9 | } -------------------------------------------------------------------------------- /pysimlink/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/pysimlink/lib/__init__.py -------------------------------------------------------------------------------- /pysimlink/lib/cmake_gen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import re 4 | 5 | import pybind11 6 | 7 | 8 | from pysimlink.utils.model_utils import sanitize_model_name 9 | 10 | 11 | class CmakeTemplate: 12 | """ 13 | Generates the CMakeLists.txt file that can be used to compile the model. 14 | 15 | The generated cmakelists.txt includes custom mixins and pybind bindings that 16 | allow a model to be imported in python. 17 | """ 18 | 19 | def __init__(self, model_name): 20 | self.model_name = model_name 21 | self.libs = [] 22 | self.replacers = [(re.compile(r"(? bool: 64 | """ 65 | check if the model extension exists. 66 | 67 | Returns: 68 | bool: True if the model needs to be compiled, False otherwise 69 | """ 70 | if os.name == "nt": 71 | lib = glob.glob( 72 | os.path.join( 73 | self.model_paths.tmp_dir, 74 | "build", 75 | "out", 76 | "library", 77 | "Debug", 78 | self.model_paths.module_name + ".*", 79 | ) 80 | ) 81 | else: 82 | lib = glob.glob( 83 | os.path.join( 84 | self.model_paths.tmp_dir, 85 | "build", 86 | "out", 87 | "library", 88 | self.model_paths.module_name + ".*", 89 | ) 90 | ) 91 | return len(lib) == 0 92 | 93 | def _get_simulink_deps(self): 94 | """ 95 | Generates a list of all simulink dependencies and their paths 96 | """ 97 | files = glob.glob(self.model_paths.simulink_native + "/**/*.h", recursive=True) 98 | 99 | self.simulink_deps = {os.path.basename(f).split(".")[0] for f in files} 100 | self.simulink_deps_path = files 101 | 102 | simulink_deps = glob.glob(self.model_paths.simulink_native + "/**/*.c", recursive=True) 103 | rt_main = None 104 | for file in simulink_deps: 105 | if os.path.basename(file) in ["rt_main.c", "classic_main.c"]: 106 | rt_main = file 107 | break 108 | if rt_main is not None: 109 | simulink_deps.remove(rt_main) 110 | 111 | for file in self.simulink_deps: 112 | if file == "rtw_matlogging": 113 | self.matlogging = True 114 | break 115 | 116 | self.simulink_deps_src = simulink_deps 117 | 118 | def _gen_custom_srcs(self): 119 | """ 120 | Moves all custom mixin source files to the temporary directory and makes appropriate replacements 121 | in the source files 122 | """ 123 | shutil.rmtree( 124 | os.path.join(self.model_paths.tmp_dir, "c_files"), 125 | ignore_errors=True, 126 | ) 127 | shutil.copytree( 128 | os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "c_files")), 129 | os.path.join(self.model_paths.tmp_dir, "c_files"), 130 | ) 131 | self.custom_includes = os.path.join(self.model_paths.tmp_dir, "c_files", "include") 132 | self.custom_sources = os.path.join(self.model_paths.tmp_dir, "c_files", "src") 133 | 134 | replacements = { 135 | "<>": self.model_paths.root_model_name + ".h", 136 | "<>": self.model_paths.root_model_name + "_private.h", 137 | "<>": self.model_paths.module_name, 138 | "<>": sanitize_model_name(self.model_paths.root_model_name), 139 | "<>": self.gather_types(), 140 | "<>": self.get_type_names(), 141 | "<>": '#include "rtw_matlogging.h"' if self.matlogging else "", 142 | } 143 | self._replace_macros(os.path.join(self.custom_includes, "model_utils.hpp"), replacements) 144 | self._replace_macros( 145 | os.path.join(self.custom_includes, "model_interface.hpp"), replacements 146 | ) 147 | self._replace_macros(os.path.join(self.custom_sources, "bindings.cpp"), replacements) 148 | self._replace_macros(os.path.join(self.custom_includes, "containers.hpp"), replacements) 149 | 150 | defines = os.path.join(self.model_paths.root_model_path, "defines.txt") 151 | if os.path.exists(defines): 152 | with open(defines, "r", encoding="utf-8") as f: 153 | self.defines = [line.strip() for line in f.readlines()] 154 | else: 155 | self.defines = infer_defines(self.model_paths) 156 | 157 | def _build(self): 158 | """ 159 | Cals cmake to configure and build the extension. Writes errors to the current working directory 160 | in a log file. 161 | """ 162 | build_dir = os.path.join(self.model_paths.tmp_dir, "build") 163 | 164 | with Popen( 165 | [ 166 | os.path.join(cmake.CMAKE_BIN_DIR, "cmake"), 167 | "-S", 168 | self.model_paths.tmp_dir, 169 | "-DCMAKE_BUILD_TYPE=Release", 170 | '-G', 171 | self.generator, 172 | "-B", 173 | build_dir, 174 | ], 175 | stdout=PIPE, 176 | stderr=PIPE, 177 | ) as p: 178 | (output1, err1) = p.communicate() 179 | build = p.wait() 180 | 181 | if build != 0: 182 | now = datetime.now() 183 | err_file = os.path.join( 184 | os.getcwd(), 185 | now.strftime("%Y-%m-%d_%H-%M-%S_PySimlink_Generation_Error.log"), 186 | ) 187 | with open(err_file, "w", encoding="utf-8") as f: 188 | f.write(output1.decode() if output1 else "") 189 | f.write(err1.decode() if err1 else "") 190 | raise GenerationError( 191 | err_file, os.path.join(self.model_paths.tmp_dir, "CMakeLists.txt") 192 | ) 193 | 194 | with Popen( 195 | [os.path.join(cmake.CMAKE_BIN_DIR, "cmake"), "--build", build_dir], 196 | stdout=PIPE, 197 | stderr=PIPE, 198 | ) as p: 199 | (output2, err2) = p.communicate() 200 | make = p.wait() 201 | 202 | if make != 0: 203 | now = datetime.now() 204 | err_file = os.path.join( 205 | os.getcwd(), 206 | now.strftime("%Y-%m-%d_%H-%M-%S_PySimlink_Build_Error.log"), 207 | ) 208 | with open(err_file, "w", encoding="utf-8") as f: 209 | f.write(output2.decode() if output2 else "") 210 | f.write(err2.decode() if err2 else "") 211 | 212 | raise BuildError(err_file, os.path.join(self.model_paths.tmp_dir, "CMakeLists.txt")) 213 | 214 | @staticmethod 215 | def _replace_macros(path: str, replacements: "dict[str, str]"): 216 | """ 217 | Replaces strings in a file 218 | 219 | Args: 220 | path: path to file to replace strings in 221 | replacements: dictionary of replacements 222 | 223 | """ 224 | with open(path, "r", encoding="utf-8") as f: 225 | lines = f.readlines() 226 | 227 | for i, line in enumerate(lines): 228 | for key, val in replacements.items(): 229 | lines[i] = lines[i].replace(str(key), str(val)) 230 | 231 | with open(path, "w", encoding="utf-8") as f: 232 | f.writelines(lines) 233 | 234 | def _read_types_single_file(self, lines): 235 | define_re = re.compile(r"^#define DEFINED_TYPEDEF") 236 | endif = re.compile(r"^#endif") 237 | pairs = [] 238 | cur = None 239 | for i, line in enumerate(lines): 240 | test1 = re.search(define_re, line) 241 | test2 = re.search(endif, line) 242 | if test1 is not None: 243 | if cur is not None: 244 | warnings.warn("types file malformed") 245 | else: 246 | cur = i 247 | continue 248 | if test2 is not None: 249 | if cur is not None: 250 | pairs.append((cur, i)) 251 | cur = None 252 | 253 | for pair in pairs: 254 | new_struct = parse_struct(lines[pair[0] + 2 : pair[1] - 1]) 255 | for struct in self.types: 256 | ## Prevent duplicate types 257 | if struct.name == new_struct.name: 258 | break 259 | else: 260 | self.types.append(new_struct) 261 | 262 | def _gen_types(self): 263 | ret = [] 264 | for type in self.types: 265 | ret += [f' py::class_<{type.name}>(m, "{type.name}", py::module_local())'] 266 | for field in type.fields: 267 | ret += [f' .def_readonly("{field.name}", &{type.name}::{field.name})'] 268 | ret[-1] += ";" 269 | ret.append("") 270 | 271 | ret += [ 272 | f' py::class_(m, "{sanitize_model_name(self.model_paths.root_model_name)}_all_dtypes", py::module_local())' 273 | ] 274 | for type in self.types: 275 | ret += [ 276 | f' .def_readonly("{type.name}", &PYSIMLINK::all_dtypes::{type.name}_obj)' 277 | ] 278 | ret[-1] += ";" 279 | ret.append("") 280 | 281 | return "\n".join(ret) 282 | 283 | def get_type_names(self): 284 | ret = [] 285 | for struct in self.types: 286 | ret.append(f"{struct.name} {struct.name}_obj;") 287 | 288 | return "\n ".join(ret) 289 | -------------------------------------------------------------------------------- /pysimlink/lib/compilers/model_ref_compiler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import glob 4 | import warnings 5 | 6 | from pysimlink.lib.dependency_graph import DepGraph 7 | from pysimlink.lib.cmake_gen import CmakeTemplate 8 | from pysimlink.lib.compilers.compiler import Compiler 9 | from pysimlink.utils import annotation_utils as anno 10 | from pysimlink.utils.model_utils import sanitize_model_name 11 | 12 | 13 | class ModelRefCompiler(Compiler): 14 | """ 15 | Compiler for a model that do use model references 16 | """ 17 | 18 | def __init__(self, model_paths: "anno.ModelPaths", generator: str): 19 | super().__init__(model_paths, generator) 20 | self.models = None 21 | 22 | def compile(self): 23 | self.clean() 24 | self._get_simulink_deps() 25 | self._build_deps_tree() 26 | self._gen_custom_srcs() 27 | self._gen_cmake() 28 | self.gather_types() 29 | self._build() 30 | 31 | def _build_deps_tree(self): 32 | """ 33 | Get the dependencies for this model and all child models recursively, 34 | starting at the root model. 35 | 36 | """ 37 | self.models = DepGraph() 38 | self.update_recurse(self.model_paths.root_model_name, self.models, is_root=True) 39 | 40 | def update_recurse(self, model_name: str, models: "anno.DepGraph", is_root=False): 41 | """ 42 | Recursively gathers dependencies for a _model and adds them to the 43 | dependency graph. 44 | Argument: 45 | model_name: Name of the _model. This is used to locate the _model in 46 | the slprj/{compile_type} folder 47 | models: An instance of the dependency graph 48 | is_root: Since the root _model is in a different place, we handle the 49 | path differently. This is set to true only when processing the 50 | root _model 51 | Returns: 52 | None: always 53 | """ 54 | if model_name in models or model_name in ["math"]: 55 | return 56 | 57 | model_path = ( 58 | self.model_paths.root_model_path 59 | if is_root 60 | else os.path.join(self.model_paths.slprj_dir, model_name) 61 | ) 62 | deps = self._get_deps(model_path, model_name) 63 | models.add_dependency(model_name, deps) 64 | for dep in deps: 65 | self.update_recurse(dep, models) 66 | 67 | def _get_deps(self, path, model_name): 68 | """Get all dependencies of a model 69 | 70 | Args: 71 | path: Path to the _model (including it's name). Must contain 72 | {model_name}.h 73 | model_name: Name of the _model. path must contain {model_name}.h 74 | 75 | Returns: 76 | deps: list of dependencies for this _model. 77 | """ 78 | deps = set() 79 | with open(os.path.join(path, model_name + ".h"), encoding="utf-8") as f: 80 | regex = re.compile('^#include "(.*?)"') 81 | end = re.compile("^typedef") 82 | for line in f.readlines(): 83 | inc_test = re.match(regex, line) 84 | if inc_test: 85 | dep = inc_test.groups()[0] 86 | ## Could probably replace this with .split('.')[0] but can _model names have a '.'? 87 | suffix_idx = dep.find(".h") 88 | deps.add(dep[:suffix_idx]) 89 | continue 90 | if re.match(end, line): 91 | break 92 | this_deps = deps.difference(self.simulink_deps) 93 | to_remove = [dep for dep in this_deps if dep.startswith(model_name)] 94 | return this_deps.difference(to_remove) 95 | 96 | def _get_simulink_deps(self): 97 | super()._get_simulink_deps() 98 | _shared_utils = glob.glob( 99 | os.path.join(self.model_paths.slprj_dir, "_sharedutils") + "/**/*.c", 100 | recursive=True, 101 | ) 102 | self.simulink_deps_src += _shared_utils 103 | 104 | _shared_utils = glob.glob( 105 | os.path.join(self.model_paths.slprj_dir, "_sharedutils") + "/**/*.h", 106 | recursive=True, 107 | ) 108 | self.simulink_deps = self.simulink_deps.union( 109 | [os.path.basename(f).split(".")[0] for f in _shared_utils] 110 | ) 111 | 112 | def _gen_cmake(self): 113 | includes = [self.custom_includes] 114 | for dir_name in os.walk(self.model_paths.root_dir, followlinks=False): 115 | for file in dir_name[2]: 116 | if ".h" in file: 117 | includes.append(dir_name[0]) 118 | break 119 | 120 | maker = CmakeTemplate( 121 | self.model_paths.root_model_name.replace(" ", "_").replace("-", "_").lower() 122 | ) 123 | cmake_text = maker.header() 124 | cmake_text += maker.set_includes(includes) 125 | 126 | for lib in self.models.dep_map: 127 | if lib == self.model_paths.root_model_name: 128 | files = glob.glob(self.model_paths.root_model_path + "/*.c") 129 | else: 130 | files = glob.glob(os.path.join(self.model_paths.slprj_dir, lib) + "/*.c") 131 | cmake_text += maker.add_library(lib, files) 132 | 133 | cmake_text += maker.add_library("shared_utils", self.simulink_deps_src) 134 | ## the custom code depends on the root _model. 135 | self.models.add_dependency(self.model_paths.root_model_name, ["shared_utils"]) 136 | 137 | cmake_text += maker.add_custom_libs(self.custom_sources) 138 | cmake_text += maker.add_private_link(self.model_paths.root_model_name) 139 | cmake_text += maker.set_lib_props() 140 | cmake_text += maker.add_link_libs(self.models.dep_map) 141 | cmake_text += maker.add_compile_defs(self.defines) 142 | cmake_text += maker.footer() 143 | 144 | with open( 145 | os.path.join(self.model_paths.tmp_dir, "CMakeLists.txt"), "w", encoding="utf-8" 146 | ) as f: 147 | f.write(cmake_text) 148 | 149 | @property 150 | def _module_name(self): 151 | return super()._module_name 152 | 153 | def gather_types(self): 154 | types_files = [] 155 | for lib in self.models.dep_map: 156 | if lib == self.model_paths.root_model_path: 157 | files = glob.glob(self.model_paths.root_model_path + "/*_types.h") 158 | else: 159 | files = glob.glob(os.path.join(self.model_paths.slprj_dir, lib) + "/*_types.h") 160 | 161 | types_files += files 162 | 163 | for file in types_files: 164 | with open(file, "r") as f: 165 | lines = f.readlines() 166 | 167 | self._read_types_single_file(lines) 168 | 169 | return self._gen_types() 170 | -------------------------------------------------------------------------------- /pysimlink/lib/compilers/one_shot_compiler.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | from pysimlink.lib import cmake_gen 5 | from pysimlink.lib.compilers.compiler import Compiler 6 | from pysimlink.utils import annotation_utils as anno 7 | 8 | 9 | class NoRefCompiler(Compiler): 10 | """ 11 | Compiler for a model that does not use model references 12 | """ 13 | 14 | def __init__(self, model_paths: "anno.ModelPaths", generator: str): 15 | super().__init__(model_paths, generator) 16 | self.model_srcs = [] 17 | self.model_incs = [] 18 | 19 | def compile(self): 20 | self.clean() 21 | self._get_simulink_deps() 22 | self._gen_custom_srcs() 23 | self._gen_model_deps() 24 | self._gen_cmake() 25 | self._build() 26 | 27 | def _gen_model_deps(self): 28 | self.model_srcs = glob.glob(self.model_paths.root_model_path + "/**/*.c", recursive=True) 29 | self.model_incs = [] 30 | for dir_name in os.walk(self.model_paths.root_model_path, followlinks=False): 31 | for file in dir_name[2]: 32 | if ".h" in file: 33 | self.model_incs.append(dir_name[0]) 34 | break 35 | 36 | def _gen_cmake(self): 37 | includes = [self.custom_includes] + self.model_incs 38 | for dir_name in os.walk(self.model_paths.simulink_native, followlinks=False): 39 | for file in dir_name[2]: 40 | if ".h" in file: 41 | includes.append(dir_name[0]) 42 | break 43 | maker = cmake_gen.CmakeTemplate( 44 | self.model_paths.root_model_name.replace(" ", "_").replace("-", "_").lower() 45 | ) 46 | cmake_text = maker.header() 47 | cmake_text += maker.set_includes(includes) 48 | 49 | cmake_text += maker.add_library(self.model_paths.root_model_name, self.model_srcs) 50 | cmake_text += maker.add_library("shared_utils", self.simulink_deps_src) 51 | cmake_text += maker.add_custom_libs(self.custom_sources) 52 | cmake_text += maker.set_lib_props() 53 | 54 | dep_map = {self.model_paths.root_model_name: ["shared_utils"]} 55 | cmake_text += maker.add_link_libs(dep_map) 56 | cmake_text += maker.add_private_link(self.model_paths.root_model_name) 57 | cmake_text += maker.set_lib_props() 58 | cmake_text += maker.add_compile_defs(self.defines) 59 | 60 | cmake_text += maker.footer() 61 | 62 | with open( 63 | os.path.join(self.model_paths.tmp_dir, "CMakeLists.txt"), "w", encoding="utf-8" 64 | ) as f: 65 | f.write(cmake_text) 66 | 67 | def gather_types(self): 68 | types_files = glob.glob(self.model_paths.root_model_path + "/*_types.h") 69 | 70 | for file in types_files: 71 | with open(file, "r") as f: 72 | lines = f.readlines() 73 | 74 | self._read_types_single_file(lines) 75 | 76 | return self._gen_types() 77 | -------------------------------------------------------------------------------- /pysimlink/lib/dependency_graph.py: -------------------------------------------------------------------------------- 1 | class DepGraph: 2 | """ 3 | Based on a simple map, this class holds the name of each _model and 4 | contains a set with all of the dependent models (only needed when _model 5 | references are present) 6 | """ 7 | 8 | def __init__(self): 9 | self.dep_map = {} 10 | 11 | def add_dependency(self, lib: str, dependents: "list[str]"): 12 | """Add a dependency to the graph 13 | 14 | Args: 15 | lib: name of the _model to add 16 | dependents: list of all dependents 17 | """ 18 | if lib not in self.dep_map: 19 | self.dep_map.update({lib: set(dependents)}) 20 | else: 21 | self.dep_map.update({lib: self.dep_map[lib].union(dependents)}) 22 | 23 | def __contains__(self, obj): 24 | return obj in self.dep_map 25 | -------------------------------------------------------------------------------- /pysimlink/lib/exceptions.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class GenerationError(Exception): 4 | """ 5 | Exception raised when generating build files fails. 6 | 7 | Attributes: 8 | dump (str): stdout and stderr output of the **generation** process 9 | cmake (str): contents of the :file:`CMakeLists.txt` file 10 | """ 11 | 12 | def __init__(self, *args): 13 | self.dump = args[0] 14 | self.cmake = args[1] 15 | 16 | def __str__(self): 17 | if os.environ.get("PYSIMLINK_DEBUG", "FALSE") == "TRUE": 18 | with open(self.cmake, 'r') as f: 19 | contents = f.read() 20 | with open(self.dump, 'r') as f: 21 | dump = f.read() 22 | ret = f"GENERATION ERROR\n\n{dump}\nCMAKE_LISTS\n{contents}" 23 | return ret 24 | else: 25 | return ( 26 | "Generating the CMakeLists for this model failed. This could be a c/c++/cmake setup issue, bad paths, or a bug! " 27 | f"Output from CMake generation is in {self.dump}" 28 | ) 29 | 30 | 31 | class BuildError(Exception): 32 | """ 33 | Exception raised when generating build files succeeded but compiling the model fails. 34 | 35 | Attributes: 36 | dump (str): stdout and stderr output of the **build** process 37 | cmake (str): contents of the :file:`CMakeLists.txt` file 38 | """ 39 | 40 | def __init__(self, *args): 41 | self.dump = args[0] 42 | self.cmake = args[1] 43 | 44 | def __str__(self): 45 | if os.environ.get("PYSIMLINK_DEBUG", "FALSE") == "TRUE": 46 | with open(self.cmake, 'r') as f: 47 | contents = f.read() 48 | with open(self.dump, 'r') as f: 49 | dump = f.read() 50 | ret = f"BUILD ERROR\n\n{dump}\n\n---\nCMAKE_LISTS\n{contents}" 51 | return ret 52 | else: 53 | return ( 54 | "Building the model failed. This could be a c/c++/cmake setup issue, bad paths, or a bug! " 55 | f"Output from the build process is in {self.dump}" 56 | ) 57 | -------------------------------------------------------------------------------- /pysimlink/lib/model.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | 5 | import numpy as np 6 | 7 | if os.name != "nt": 8 | from fcntl import lockf, LOCK_EX, LOCK_UN 9 | else: 10 | import msvcrt 11 | # import tempfile 12 | 13 | from pysimlink.lib.model_paths import ModelPaths 14 | from pysimlink.utils import annotation_utils as anno 15 | from pysimlink.utils.model_utils import mt_rebuild_check, sanitize_model_name, cast_type 16 | from pysimlink.lib.model_types import DataType, ModelInfo 17 | from pysimlink.lib.spinner import open_spinner 18 | import pickle 19 | import time 20 | import importlib 21 | 22 | 23 | class Model: 24 | """ 25 | Instance of the simulink mode. This class compiles and imports 26 | the model once built. You can have multiple instances of the same 27 | model in one python runtime. 28 | """ 29 | 30 | _model_paths: "anno.ModelPaths" 31 | _compiler: "anno.Compiler" 32 | 33 | def __init__( # pylint: disable=R0913 34 | self, 35 | model_name: str, 36 | path_to_model: str, 37 | compile_type: str = "grt", 38 | suffix: str = "rtw", 39 | tmp_dir: "anno.Optional[str]" = None, 40 | force_rebuild: bool = False, 41 | skip_compile: bool = False, 42 | generator: str = None, 43 | ): 44 | """ 45 | Args: 46 | model_name (str): name of the root simulink model 47 | path_to_model (str): path to the directory containing code generated from the Simulink Coder *or* the packaged zip file 48 | compile_type (str): Makefile template used to generate code from Simulink Coder. Only GRT is supported. 49 | suffix (str): Simulink Coder folders are almost always suffixed with rtw (real time workshop). 50 | tmp_dir (Optional[str]): Path to the directory that will be used to build the model. Defaults to :file:`__pycache__/{model_name}` 51 | force_rebuild (bool): force pysimlink to recompile the model from the source located at :code:`path_to_model`. Removes all build artifacts. 52 | skip_compile (bool): skip compilation of the model. This is useful if you have already compiled the model and just want to import it. 53 | generator (str): Type of generator to use for cmake. defaults to :code:`NMake Makefiles` on windows and :code:`Unix Makefiles` on mac/linux. 54 | 55 | 56 | Attributes: 57 | orientations: enumeration describing matrix orientations (row major, column major, etc.). This enumeration is 58 | likely the same among all models, but could change across MATLAB versions. 59 | """ 60 | 61 | self._model_paths = ModelPaths(path_to_model, model_name, compile_type, suffix, tmp_dir, skip_compile) 62 | 63 | if generator is None: 64 | if os.name == "nt": 65 | generator = "NMake Makefiles" 66 | else: 67 | generator = "Unix Makefiles" 68 | 69 | self._compiler = self._model_paths.compiler_factory(generator) 70 | 71 | self._lock() 72 | # Check need to compile 73 | if ( 74 | (mt_rebuild_check(self._model_paths, force_rebuild) 75 | or self._compiler.needs_to_compile()) and not skip_compile 76 | ): 77 | # Need to compile 78 | with open_spinner("Compiling"): 79 | self._compiler.compile() 80 | with open(os.path.join(self._model_paths.tmp_dir, "compile_info.pkl"), "wb") as f: 81 | obj = {"pid": os.getpid(), "parent": os.getppid(), "time": time.time()} 82 | pickle.dump(obj, f) 83 | self._unlock() 84 | 85 | self.path_dirs = [] 86 | for dir, _, _ in os.walk( 87 | os.path.join(self._model_paths.tmp_dir, "build", "out", "library") 88 | ): 89 | sys.path.append(dir) 90 | self.path_dirs.append(dir) 91 | 92 | self.module = importlib.import_module(self._model_paths.module_name) 93 | model_class = getattr( 94 | self.module, sanitize_model_name(self._model_paths.root_model_name) + "_Model" 95 | ) 96 | 97 | self._model = model_class(self._model_paths.root_model_name) 98 | 99 | self.orientations = getattr( 100 | self.module, 101 | sanitize_model_name(self._model_paths.root_model_name) + "_rtwCAPI_Orientation", 102 | ) 103 | 104 | def __del__(self): 105 | if sys.path is not None and hasattr(self, "path_dirs"): 106 | for dir in self.path_dirs: 107 | sys.path.remove(dir) 108 | # if hasattr(self, "module"): 109 | # del sys.modules[self.module] 110 | # sys.modules.remove(self.module) 111 | 112 | def __len__(self): 113 | """ 114 | Get the total number of steps this model can run 115 | """ 116 | return int(self.tFinal / self.step_size) 117 | 118 | def _lock(self): 119 | f = open(os.path.join(self._model_paths.tmp_dir, self._model_paths.root_model_name + ".lock"), "w") 120 | if os.name == "nt": 121 | rv = msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1) 122 | else: 123 | rv = lockf(f, LOCK_EX) 124 | f.write(str(os.getpid())) 125 | f.close() 126 | 127 | def _unlock(self): 128 | f = open(os.path.join(self._model_paths.tmp_dir, self._model_paths.root_model_name + ".lock"), "w") 129 | if os.name == "nt": 130 | rv = msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1) 131 | else: 132 | rv = lockf(f, LOCK_UN) 133 | f.close() 134 | 135 | def get_params(self) -> "list[anno.ModelInfo]": 136 | """ 137 | Return an instance of all parameters, blocks, and signals in the _model 138 | 139 | See :func:`pysimlink.print_all_params` for iterating and printing the contents of this object 140 | 141 | Returns: 142 | list[:class:`pysimlink.types.ModelInfo`]: List of model info, one for each model (if reference models present). One ModelInfo if no reference models 143 | """ 144 | return list(map(ModelInfo, self._model.get_params())) 145 | 146 | def reset(self): 147 | """ 148 | Reset the simulink model. This clears all signal values and reinstantiates the model. 149 | """ 150 | self._model.reset() 151 | 152 | def step(self, iterations: int = 1): 153 | """ 154 | Step the simulink model 155 | 156 | Args: 157 | iterations: Number of timesteps to step internally. 158 | :code:`model.step(10)` is equivalent to calling :code:`for _ range(10): model.step(1)` functionally, but compiled. 159 | 160 | Raises: 161 | RuntimeError: If the model encounters an error (these will be raised from simulink). Most commonly, this 162 | will be `simulation complete`. 163 | 164 | """ 165 | self._model.step(iterations) 166 | 167 | @property 168 | def tFinal(self) -> float: 169 | """ 170 | Get the final timestep of the model. 171 | 172 | Returns: 173 | float: Final timestep of the model (seconds from zero). 174 | """ 175 | return self._model.tFinal() 176 | 177 | @property 178 | def step_size(self) -> float: 179 | """ 180 | Get the step size of the model 181 | 182 | Returns: 183 | float: step size of the fixed step solver. 184 | """ 185 | return self._model.step_size() 186 | 187 | def set_tFinal(self, tFinal: float): 188 | """ 189 | Change the final timestep of the model 190 | 191 | Args: 192 | tFinal: New final timestep of the model (seconds from zero). 193 | 194 | Raises: 195 | ValueError: if tFinal is <= 0 196 | """ 197 | if tFinal <= 0: 198 | raise ValueError("new tFinal must be > 0") 199 | self._model.set_tFinal(tFinal) 200 | 201 | def get_signal(self, block_path, model_name=None, sig_name="") -> "np.ndarray": 202 | """ 203 | Get the value of a signal 204 | 205 | Args: 206 | block_path: Path to the originating block 207 | model_name: Name of the model provided by :func:`pysimlink.print_all_params`. None if there are no model 208 | references (using :code:`None` will retrieve from the root model). 209 | sig_name: Name of the signal 210 | 211 | Returns: 212 | Value of the signal at the current timestep 213 | """ 214 | model_name = self._model_paths.root_model_name if model_name is None else model_name 215 | 216 | sig_type = self._model.desc_signal(model_name, block_path, sig_name) 217 | if sig_type.cDataType == "struct": 218 | data = self._model.get_signal_union(model_name, block_path, sig_name) 219 | return getattr(data, sig_type.mwType) 220 | else: 221 | data: "np.ndarray" = self._model.get_signal_arr(model_name, block_path, sig_name) 222 | if data.size == 1: 223 | return data.item() 224 | else: 225 | return data 226 | 227 | def get_block_param(self, block_path, param, model_name=None) -> "np.ndarray": 228 | """ 229 | Get the value of a block parameter 230 | 231 | Args: 232 | block_path: Path the block within the model 233 | param: Name of the parameter to retrieve 234 | model_name: Name of the model provided by :func:`pysimlink.print_all_params`. None if there are no model references. 235 | 236 | Returns: 237 | np.ndarray with the value of the parameter 238 | """ 239 | model_name = self._model_paths.root_model_name if model_name is None else model_name 240 | return self._model.get_block_param(model_name, block_path, param) 241 | 242 | def get_model_param(self, param, model_name=None) -> "np.ndarray": 243 | """ 244 | Get the value of a model parameter 245 | 246 | Args: 247 | param: Name of the parameter to retrieve 248 | model_name: Name of the model provided by :func:`pysimlink.print_all_params`. None if there are no model references. 249 | 250 | Returns: 251 | np.ndarray with the value of the parameter 252 | """ 253 | model_name = self._model_paths.root_model_name if model_name is None else model_name 254 | return self._model.get_model_param(model_name, param) 255 | 256 | def get_models(self) -> "list[str]": 257 | """ 258 | Gets a list of all reference models (and the root model) in this model. 259 | 260 | Returns: 261 | list of paths, one for each model 262 | """ 263 | return self._model.get_models() 264 | 265 | def set_block_param( 266 | self, 267 | block: str, 268 | param: str, 269 | value: "anno.ndarray", 270 | model_name: "anno.Union[str,None]" = None, 271 | ): 272 | """ 273 | Set the parameter of a block within the model. 274 | 275 | Args: 276 | block: Path to the block within the model 277 | param: Name of the parameter to change 278 | value: new value of the parameter 279 | model_name: Name of the model provided by :func:`pysimlink.print_all_params`. None if there are no model references. 280 | Raises: 281 | RuntimeError: If the value array is not the correct shape or orientation as the parameter to change 282 | """ 283 | model_name = self._model_paths.root_model_name if model_name is None else model_name 284 | info = self._model.block_param_info(model_name, block, param) 285 | dtype = DataType(info) 286 | 287 | value = cast_type(value, dtype, self.orientations) 288 | 289 | self._model.set_block_param(model_name, block, param, value) 290 | 291 | def set_model_param( 292 | self, param: str, value: "anno.ndarray", model_name: "anno.Union[str,None]" = None 293 | ): 294 | """ 295 | Set a model parameter. 296 | 297 | Args: 298 | param: Name of the parameter to change 299 | value: new value of the parameter 300 | model_name: Name of the model provided by :func:`pysimlink.print_all_params`. None if there are no model references. 301 | Raises: 302 | RuntimeError: If the value array is not the correct shape or orientation as the parameter to change 303 | """ 304 | model_name = self._model_paths.root_model_name if model_name is None else model_name 305 | info = self._model.model_param_info(model_name, param) 306 | dtype = DataType(info) 307 | value = cast_type(value, dtype, self.orientations) 308 | 309 | self._model.set_model_param(model_name, param, value) 310 | -------------------------------------------------------------------------------- /pysimlink/lib/model_paths.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import re 4 | import sys 5 | from typing import Union 6 | import zipfile 7 | import shutil 8 | 9 | from pysimlink.utils import annotation_utils as anno 10 | from pysimlink.utils.model_utils import get_other_in_dir, sanitize_model_name 11 | 12 | 13 | class ModelPaths: 14 | """ 15 | Holds information about the paths to the model being built. 16 | """ 17 | 18 | root_dir: str ## Directory containing all model components. 19 | simulink_native: str ## path to the directory containing stuff 20 | # generated by simulink for every model 21 | root_model_path: str ## Path to the root model 22 | root_model_name: str ## Name of the root model 23 | compile_type: str ## grt, ert, etc... 24 | suffix: str ## Suffix added to the model name. Usually 'rtw' 25 | has_references: bool ## If this model contains references 26 | models_dir: str ## directory containing all simulink code related to the models 27 | slprj_dir: Union[str, None] ## Directory will all child models (contains compile_type) 28 | tmp_dir: str ## Directory where all compiled models will be built 29 | was_zip: bool ## Whether the source was a zip file or not 30 | 31 | def __init__( 32 | self, 33 | root_dir: str, 34 | model_name: str, 35 | compile_type: str = "grt", 36 | suffix: str = "rtw", 37 | tmp_dir: "Union[str, None]" = None, 38 | skip_compile: bool = False, 39 | ): 40 | """ 41 | Args: 42 | root_dir: Directory created during codegen. Should have two directories in it. 43 | model_name: Name of the root model. 44 | compile_type: grt, ert, etc... 45 | suffix: the suffix added to the model name directory. usually 'rtw' 46 | tmp_dir: Where to store the build files. Defaults to __pycache__ 47 | skip_compile: Don't extract the model, assume this has already been done 48 | """ 49 | self.compile_type = compile_type 50 | if self.compile_type != "grt": 51 | raise ValueError( 52 | "Unsupported compile target. grt is the only supported simulink " 53 | "code generation target.\nChange your code generation settings " 54 | "to use the grt.tlc target and try again. (compile_type should " 55 | f"be `grt` not {self.compile_type})" 56 | ) 57 | self.suffix = suffix 58 | zip_test = os.path.splitext(root_dir) 59 | if zip_test[-1] == ".zip": 60 | if skip_compile: 61 | if tmp_dir is None: 62 | ext_dir = os.path.join( 63 | os.path.dirname(sys.argv[0]), 64 | "__pycache__", 65 | "extract" 66 | ) 67 | else: 68 | ext_dir = os.path.join(tmp_dir, "extract") 69 | zip_name = os.listdir(ext_dir)[0] 70 | self.root_dir = os.path.join(ext_dir, zip_name) 71 | ext_dir = self.root_dir 72 | self.was_zip = True 73 | else: 74 | with zipfile.ZipFile(root_dir, "r") as f: 75 | if tmp_dir is None: 76 | ext_dir = os.path.join( 77 | os.path.dirname(sys.argv[0]), 78 | "__pycache__", 79 | "extract", 80 | os.path.basename(zip_test[0]), 81 | ) 82 | else: 83 | ext_dir = os.path.join(tmp_dir, "extract", os.path.basename(zip_test[0])) 84 | shutil.rmtree(ext_dir, ignore_errors=True) 85 | f.extractall(ext_dir) 86 | 87 | self.root_dir = ext_dir 88 | else: 89 | self.root_dir = root_dir 90 | self.was_zip = False 91 | 92 | walk = os.walk(self.root_dir, followlinks=False) 93 | for (cur_path, folders, _) in walk: 94 | if "simulink" in folders: 95 | self.simulink_native = cur_path 96 | break 97 | else: 98 | raise RuntimeError(f"{self.root_dir} is not a valid simulink model.") 99 | 100 | models_dir = get_other_in_dir(self.root_dir, os.path.basename(self.simulink_native)) 101 | self.models_dir = os.path.join(self.root_dir, models_dir) 102 | 103 | self.has_references = os.path.exists(os.path.join(self.models_dir, "slprj")) 104 | 105 | self.root_model_path = os.path.join( 106 | self.models_dir, model_name + "_" + compile_type + "_" + suffix 107 | ) 108 | if not os.path.exists(self.root_model_path): 109 | try: 110 | model_name = model_name.split("_" + compile_type + "_" + suffix)[0] 111 | except: # pylint: disable=W0702 112 | pass 113 | self.root_model_path = os.path.join( 114 | self.models_dir, model_name + "_" + compile_type + "_" + suffix 115 | ) 116 | if not os.path.exists(self.root_model_path): 117 | raise RuntimeError( 118 | f"Cannot find folder with name '{model_name}' in '{self.models_dir}'" 119 | ) 120 | self.root_model_name = model_name 121 | if self.has_references: 122 | self.slprj_dir = os.path.join(self.models_dir, "slprj", compile_type) 123 | 124 | if tmp_dir is None: 125 | self.tmp_dir = os.path.join( 126 | os.path.dirname(sys.argv[0]), 127 | "__pycache__", 128 | "pysimlink", 129 | self.root_model_name, 130 | ) 131 | else: 132 | self.tmp_dir = os.path.join(tmp_dir, model_name) 133 | os.makedirs(self.tmp_dir, exist_ok=True) 134 | self.verify_capi() 135 | 136 | def verify_capi(self): 137 | """ 138 | Make sure that this model was generated with the c api. This doesn't use 139 | the function in the capi, but we need the model mapping interface (mmi). 140 | """ 141 | files = glob.glob(self.root_model_path + "/*.c", recursive=False) 142 | files = list(map(os.path.basename, files)) 143 | assert ( 144 | self.root_model_name + ".c" in files 145 | ), f"Cannot find {self.root_model_name}.c in {self.root_model_path}. Is the model name correct?" 146 | 147 | assert self.root_model_name + "_capi.c" in files, ( 148 | "Model not generated with capi. Enable the following options in the Code Generation model settings: \n" 149 | "\tGenerate C API for: signals, parameters, states, root-level I/O" 150 | ) 151 | 152 | ## also check that this is not a multitasked model 153 | with open( 154 | os.path.join(self.root_model_path, self.root_model_name + ".h"), encoding="utf-8" 155 | ) as f: 156 | lines = f.readlines() 157 | 158 | regex = re.compile( 159 | f"extern void {self.root_model_name}_step\(void\);" # pylint: disable=W1401 160 | ) 161 | for line in lines: 162 | if re.search(regex, line): 163 | break 164 | else: 165 | raise RuntimeError( 166 | "Model is setup with multitasking OR single output/update function is not enabled. See the docs for proper generation format (https://lharri73.github.io/PySimlink/src/howto.html#generate-code-from-your-simulink-model)" 167 | ) 168 | 169 | def compiler_factory(self, generator) -> "anno.Compiler": 170 | """ 171 | Return the correct compiler. This could be simplified later -or- more 172 | compilers could be added if we want to use something other than cmake. 173 | """ 174 | if self.has_references: # pylint: disable=R1705 175 | from pysimlink.lib.compilers.model_ref_compiler import ( # pylint: disable=C0415 176 | ModelRefCompiler, 177 | ) 178 | 179 | return ModelRefCompiler(self, generator) 180 | else: 181 | from pysimlink.lib.compilers.one_shot_compiler import ( # pylint: disable=C0415 182 | NoRefCompiler, 183 | ) 184 | 185 | return NoRefCompiler(self, generator) 186 | 187 | @property 188 | def module_name(self): 189 | return sanitize_model_name(self.root_model_name) + "_interface_c" 190 | 191 | def clean(self): 192 | shutil.rmtree(self.tmp_dir, ignore_errors=True) 193 | -------------------------------------------------------------------------------- /pysimlink/lib/model_types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pysimlink.utils import annotation_utils as anno 3 | 4 | 5 | # These classes are provided for reference only. 6 | @dataclass 7 | class DataType: 8 | """ 9 | Contains data type information for a single parameter. 10 | 11 | Attributes: 12 | cDataType (str): The c equivalent name of this datatype 13 | pythonType (str): The python name of this datatype 14 | dims (list[int]): List of dimension sizes. (the shape of the array) 15 | orientation (int): Enumeration indicating if this is a scalar, vector, column major or row major array. 16 | Since this enum is evaluated at compile time, it is imported from the compiled model. To check this value 17 | dynamically, use :attr:`pysimlink.Model.orientations`. 18 | """ 19 | 20 | cDataType: str 21 | pythonType: str 22 | dims: "list[int]" 23 | orientation: int 24 | 25 | def __init__(self, obj: "anno.c_model_datatype"): 26 | self.cDataType = obj.cDataType 27 | self.pythonType = obj.pythonType 28 | self.mwDataType = getattr(obj, "mwType", None) or getattr(obj, "mwDataType", None) 29 | self.dims = obj.dims 30 | self.orientation = obj.orientation 31 | 32 | def __repr__(self): 33 | if self.cDataType == "struct": 34 | return f"{self.mwDataType} (struct)" 35 | else: 36 | return ( 37 | f"{self.pythonType} ({self.cDataType}) dims: {self.dims} order: {self.orientation}" 38 | ) 39 | 40 | 41 | @dataclass 42 | class ModelParam: 43 | """ 44 | Represents a single model parameter. 45 | 46 | Attributes: 47 | model_param (str): Name of the model parameter (these are usually stored withing the model workspace in simulink) 48 | data_type (:class:`DataType`): data type object describing this parameter 49 | """ 50 | 51 | model_param: str 52 | data_type: DataType 53 | 54 | def __init__(self, obj: "anno.c_model_param"): 55 | self.model_param = obj.model_param 56 | self.data_type = DataType(obj.data_type) 57 | 58 | 59 | @dataclass 60 | class BlockParam: 61 | """ 62 | Represents a single parameter in a block. 63 | 64 | If a block has *n* parameters, there will be *n* BlockParam instances. Each with the same :code:`block_name`. 65 | 66 | Attributes: 67 | block_name (str): The name (usually including the path) to this block. 68 | block_param (str): The name of the parameter within the block 69 | data_type (:class:`DataType`): data type object describing this parameter 70 | """ 71 | 72 | block_name: str 73 | block_param: str 74 | data_type: DataType 75 | 76 | def __init__(self, obj: "anno.c_model_block_param"): 77 | self.block_name = obj.block_name 78 | self.block_param = obj.block_param 79 | self.data_type = DataType(obj.data_type) 80 | 81 | 82 | @dataclass 83 | class Signal: 84 | """ 85 | Represents a single signal in the model. 86 | 87 | Attributes: 88 | block_name: name and path to the block the signal originates from 89 | signal_name: name of the signal (empty if not named) 90 | data_type (:class:`DataType`): Data type information of this signal 91 | """ 92 | 93 | block_name: str 94 | signal_name: str 95 | data_type: DataType 96 | 97 | def __init__(self, obj: "anno.c_model_signal"): 98 | self.block_name = obj.block_name 99 | self.signal_name = obj.signal_name 100 | self.data_type = DataType(obj.data_type) 101 | 102 | 103 | @dataclass 104 | class ModelInfo: 105 | """ 106 | Object returned from :func:`pysimlink.Model.get_params`. 107 | 108 | .. note:: This object only describes a single model. If a Simulink model contains model references, 109 | :func:`pysimlink.Model.get_params` will return a list of this object, each describing a reference model. 110 | 111 | Attributes: 112 | model_name (str): Name of the model this object describes 113 | model_params (list[:class:`ModelParam`]): List of all model parameters 114 | block_params (list[:class:`BlockParam`]): List of all block parameters 115 | signals (list[:class:`Signal`]): List of all signals 116 | """ 117 | 118 | model_name: str 119 | model_params: "list[ModelParam]" 120 | block_params: "list[BlockParam]" 121 | signals: "list[Signal]" 122 | 123 | def __init__(self, obj: "anno.c_model_info"): 124 | self.model_name = obj.model_name 125 | self.model_params = list(map(ModelParam, obj.model_params)) 126 | self.block_params = list(map(BlockParam, obj.block_params)) 127 | self.signals = list(map(Signal, obj.signals)) 128 | -------------------------------------------------------------------------------- /pysimlink/lib/spinner.py: -------------------------------------------------------------------------------- 1 | ## Coppied from 2 | # https://github.com/pypa/pip/blob/7efc8149162d9ae2f5a60cb346979bc8a6e81700/src/pip/_internal/cli/spinners.py 3 | 4 | # Copyright (c) 2008-present The pip developers (see AUTHORS.txt file) 5 | 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | import contextlib 26 | import itertools 27 | import sys 28 | import time 29 | import os 30 | from typing import IO, Generator, Optional 31 | import threading 32 | 33 | 34 | class SpinnerInterface: 35 | def spin(self) -> None: 36 | raise NotImplementedError() 37 | 38 | def finish(self, final_status: str) -> None: 39 | raise NotImplementedError() 40 | 41 | 42 | class InteractiveSpinner(SpinnerInterface): 43 | def __init__( 44 | self, 45 | message: str, 46 | file: Optional[IO[str]] = None, 47 | spin_chars: str = "-\\|/", 48 | # Empirically, 8 updates/second looks nice 49 | min_update_interval_seconds: float = 0.125, 50 | ): 51 | self._message = message 52 | if file is None: 53 | file = sys.stdout 54 | self._file = file 55 | self._rate_limiter = RateLimiter(min_update_interval_seconds) 56 | self._finished = False 57 | 58 | self._spin_cycle = itertools.cycle(spin_chars) 59 | 60 | self._file.write(self._message + " ... ") 61 | self._width = 0 62 | 63 | def _write(self, status: str) -> None: 64 | assert not self._finished 65 | # Erase what we wrote before by backspacing to the beginning, writing 66 | # spaces to overwrite the old text, and then backspacing again 67 | backup = "\b" * self._width 68 | self._file.write(backup + " " * self._width + backup) 69 | # Now we have a blank slate to add our status 70 | self._file.write(status) 71 | self._width = len(status) 72 | self._file.flush() 73 | self._rate_limiter.reset() 74 | 75 | def spin(self) -> None: 76 | if self._finished: 77 | return 78 | if not self._rate_limiter.ready(): 79 | return 80 | self._write(next(self._spin_cycle)) 81 | 82 | def finish(self, final_status: str) -> None: 83 | if self._finished: 84 | return 85 | self._write(final_status) 86 | self._file.write("\n") 87 | self._file.flush() 88 | self._finished = True 89 | 90 | 91 | # Used for dumb terminals, non-interactive installs (no tty), etc. 92 | # We still print updates occasionally (once every 60 seconds by default) to 93 | # act as a keep-alive for systems like Travis-CI that take lack-of-output as 94 | # an indication that a task has frozen. 95 | class NonInteractiveSpinner(SpinnerInterface): 96 | def __init__(self, message: str, min_update_interval_seconds: float = 60.0) -> None: 97 | self._message = message 98 | self._finished = False 99 | self._rate_limiter = RateLimiter(min_update_interval_seconds) 100 | self._update("started") 101 | 102 | def _update(self, status: str) -> None: 103 | assert not self._finished 104 | self._rate_limiter.reset() 105 | 106 | def spin(self) -> None: 107 | if self._finished: 108 | return 109 | if not self._rate_limiter.ready(): 110 | return 111 | self._update("still running...") 112 | 113 | def finish(self, final_status: str) -> None: 114 | if self._finished: 115 | return 116 | self._update(f"finished with status '{final_status}'") 117 | self._finished = True 118 | 119 | 120 | class RateLimiter: 121 | def __init__(self, min_update_interval_seconds: float) -> None: 122 | self._min_update_interval_seconds = min_update_interval_seconds 123 | self._last_update: float = 0 124 | 125 | def ready(self) -> bool: 126 | now = time.time() 127 | delta = now - self._last_update 128 | return delta >= self._min_update_interval_seconds 129 | 130 | def reset(self) -> None: 131 | self._last_update = time.time() 132 | 133 | 134 | def spin(spinner, event): 135 | while event.isSet(): 136 | spinner.spin() 137 | time.sleep(0.126) 138 | 139 | 140 | @contextlib.contextmanager 141 | def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]: 142 | # Interactive spinner goes directly to sys.stdout rather than being routed 143 | # through the logging system, but it acts like it has level INFO, 144 | # i.e. it's only displayed if we're at level INFO or better. 145 | # Non-interactive spinner goes through the logging system, so it is always 146 | # in sync with logging configuration. 147 | if sys.stdout.isatty(): 148 | spinner: SpinnerInterface = InteractiveSpinner(message) 149 | else: 150 | spinner = NonInteractiveSpinner(message) 151 | event = threading.Event() 152 | try: 153 | event.set() 154 | t = threading.Thread(target=spin, args=(spinner, event)) 155 | t.start() 156 | with hidden_cursor(sys.stdout): 157 | yield 158 | except KeyboardInterrupt: 159 | event.clear() 160 | t.join() 161 | spinner.finish("canceled") 162 | raise 163 | except Exception: 164 | event.clear() 165 | t.join() 166 | spinner.finish("error") 167 | raise 168 | else: 169 | event.clear() 170 | t.join() 171 | spinner.finish("done") 172 | 173 | 174 | HIDE_CURSOR = "\x1b[?25l" 175 | SHOW_CURSOR = "\x1b[?25h" 176 | 177 | 178 | @contextlib.contextmanager 179 | def hidden_cursor(file: IO[str]) -> Generator[None, None, None]: 180 | # The Windows terminal does not support the hide/show cursor ANSI codes, 181 | # even via colorama. So don't even try. 182 | if os.name == "nt": 183 | yield 184 | # We don't want to clutter the output with control characters if we're 185 | # writing to a file, or if the user is running with --quiet. 186 | # See https://github.com/pypa/pip/issues/3418 187 | elif not file.isatty(): 188 | yield 189 | else: 190 | file.write(HIDE_CURSOR) 191 | try: 192 | yield 193 | finally: 194 | file.write(SHOW_CURSOR) 195 | -------------------------------------------------------------------------------- /pysimlink/lib/struct_parser.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Field: 6 | type: str 7 | name: str 8 | 9 | 10 | class Struct: 11 | def __init__(self, name, fields): 12 | self.name = name 13 | self.fields = fields 14 | 15 | 16 | def parse_struct(lines): 17 | fields = [] 18 | for i, line in enumerate(lines): 19 | if i == 0: 20 | continue 21 | line = line.strip().split() 22 | if line[0] == "}": 23 | struct = Struct(line[1][:-1], fields) 24 | return struct 25 | else: 26 | fields.append(Field(line[0], line[1][:-1])) 27 | -------------------------------------------------------------------------------- /pysimlink/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/pysimlink/utils/__init__.py -------------------------------------------------------------------------------- /pysimlink/utils/annotation_utils.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | if typing.TYPE_CHECKING: 4 | from pysimlink.lib.model_types import ModelInfo, DataType 5 | from pysimlink.lib.dependency_graph import DepGraph 6 | from pysimlink.lib.model_paths import ModelPaths 7 | from pysimlink.lib.compilers.compiler import Compiler 8 | from pysimlink.lib.model import Model 9 | from typing import Union 10 | from numpy import ndarray 11 | from typing import Optional 12 | from pysimlink.lib.struct_parser import Struct 13 | from enum import EnumType 14 | 15 | c_model_info = typing.Any 16 | c_model_param = typing.Any 17 | c_model_datatype = typing.Any 18 | c_model_signal = typing.Any 19 | c_model_block_param = typing.Any 20 | 21 | Value = Union[float, int, ndarray] 22 | -------------------------------------------------------------------------------- /pysimlink/utils/model_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import time 4 | from pysimlink.utils import annotation_utils as anno 5 | from pysimlink.lib.model_types import DataType 6 | import numpy as np 7 | import warnings 8 | 9 | 10 | def infer_defines(model_paths: "anno.ModelPaths"): 11 | """When defines.txt is not present, add the only required defines for pysimlink 12 | 13 | Args: 14 | model_paths: instance of ModelPaths object pointing to the root _model 15 | 16 | Returns: 17 | List of _model defines. These are added the CMakeLists.txt 18 | 19 | """ 20 | ret = [f"MODEL={model_paths.root_model_name}"] 21 | return ret 22 | 23 | 24 | def print_all_params(model: "anno.Model"): 25 | """ 26 | Prints all parameters for the given model. 27 | 28 | Uses the ModelInfo object to print all model info about the root and each reference model 29 | 30 | Args: 31 | model: instance of the Model to print params of 32 | """ 33 | params = model.get_params() 34 | for model_info in params: 35 | print(f"Parameters for model at '{model_info.model_name}'") 36 | print(" model parameters:") 37 | for param in model_info.model_params: 38 | print(f" param: '{param.model_param}' | data_type: '{DataType(param.data_type)}'") 39 | print(" block parameters:") 40 | for param in model_info.block_params: 41 | print( 42 | f" Block: '{param.block_name}' | Parameter: '{param.block_param}' | data_type: '{DataType(param.data_type)}'" 43 | ) 44 | print(" signals:") 45 | for sig in model_info.signals: 46 | print( 47 | f" Block: '{sig.block_name}' | Signal Name: '{sig.signal_name}' | data_type: '{DataType(sig.data_type)}'" 48 | ) 49 | print("-" * 80) 50 | 51 | 52 | def get_other_in_dir(directory: str, known: str): 53 | """In a directory containing only two directories, get the name of the other we don't know 54 | 55 | Args: 56 | directory: path to the directory 57 | known: The file/folder known to exist in the directory 58 | 59 | Returns: 60 | the other directory/file in the directory 61 | """ 62 | 63 | model_folders = set(os.listdir(directory)) 64 | model_folders.discard(".DS_Store") 65 | assert ( 66 | len(model_folders) == 2 67 | ), f"Directory '{directory}' contains more than 2 folders (not counting .DS_Store on Mac)" 68 | assert ( 69 | known in model_folders 70 | ), f"File does not exist in {directory}. Should be one of {model_folders}" 71 | model_folders.remove(known) 72 | 73 | return model_folders.pop() 74 | 75 | 76 | def with_read_lock(func: callable) -> callable: 77 | """Use as decorator (@with_lock) around object methods that need locking. 78 | 79 | Note: The object must have a self._lock property. 80 | Locking thus works on the object level (no two locked methods of the same 81 | object can be called asynchronously). 82 | 83 | Inspired by `Rllib `_ 84 | 85 | Args: 86 | func: The function to decorate/wrap. 87 | Returns: 88 | The wrapped (object-level locked) function. 89 | """ 90 | 91 | def wrapper(self, *a, **k): 92 | try: 93 | with self._lock.read_lock(): 94 | return func(self, *a, **k) 95 | except AttributeError as e: 96 | if "has no attribute '_lock'" in e.args[0]: 97 | raise AttributeError( 98 | "Object {} must have a `self._lock` property (assigned " 99 | "to a fasteners.InterProcessReaderWriterLock object in its " 100 | "constructor)!".format(self) 101 | ) 102 | raise e 103 | 104 | return wrapper 105 | 106 | 107 | def mt_rebuild_check(model_paths: "anno.ModelPaths", force_rebuild: bool) -> bool: 108 | """ 109 | Prevent the model from being rebuilt in every multithreading instance 110 | 111 | Args: 112 | model_paths (anno.ModelPaths): instance of the model paths object. Used to get the tmp_dir 113 | force_rebuild (bool): flag set by the user that forces the model to rebuild 114 | 115 | Returns: 116 | True if the model should rebuild because of the force_rebuild flag and has not already 117 | """ 118 | if not force_rebuild: 119 | return False 120 | 121 | compile_info = os.path.join(model_paths.tmp_dir, "compile_info.pkl") 122 | if not os.path.exists(compile_info): 123 | return True 124 | 125 | with open(compile_info, "rb") as f: 126 | info = pickle.load(f) 127 | 128 | if info["parent"] == os.getppid(): 129 | tdiff = time.time() - info["time"] 130 | 131 | # assume that it takes at least 1 second to start a separate instance of a program 132 | # If it takes less than 1 second, then we assume it is run within the same python 133 | # instance 134 | return tdiff > 1.0 135 | else: 136 | return True 137 | 138 | 139 | def sanitize_model_name(model_name): 140 | return model_name.replace(" ", "").replace("-", "_").lower() 141 | 142 | 143 | def cast_type(value: "anno.Value", dtype: "anno.DataType", orientations): 144 | if not isinstance(value, np.ndarray): 145 | value_cp = np.array(value, dtype=dtype.pythonType) 146 | if value_cp != value: 147 | warnings.warn( 148 | f"Datatype of value does not match parameter: Data loss detected! ({value} -> {value_cp})", 149 | RuntimeWarning, 150 | stacklevel=3, 151 | ) 152 | value = value_cp 153 | 154 | if str(value.dtype) != dtype.pythonType: 155 | warnings.warn( 156 | f"Datatype of value does not match parameter. Expected {dtype.pythonType} got {value.dtype}", 157 | RuntimeWarning, 158 | stacklevel=3, 159 | ) 160 | 161 | if dtype.dims != value.shape: 162 | # We can perform reshaping and formatting in one operation 163 | orientation = ( 164 | "F" if dtype.orientation in [orientations.col_major_nd, orientations.col_major] else "C" 165 | ) 166 | value = np.reshape(value, tuple(dtype.dims), orientation) 167 | if str(value.dtype) != dtype.pythonType: 168 | value = value.astype(dtype.pythonType) 169 | else: 170 | ## We can perform type casting and formatting in one operation 171 | if dtype.orientation in [orientations.col_major_nd, orientations.col_major]: 172 | value = np.asfortranarray(value, dtype=dtype.pythonType) 173 | elif dtype.orientation in [orientations.row_major_nd, orientations.row_major]: 174 | value = np.ascontiguousarray(value, dtype=dtype.pythonType) 175 | elif str(value.dtype) != dtype.pythonType: 176 | value = value.astype(dtype.pythonType) 177 | 178 | return value 179 | -------------------------------------------------------------------------------- /refs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/refs/banner.png -------------------------------------------------------------------------------- /refs/banner.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lharri73/PySimlink/ab1f9ad4818ce210a22be582d7355cacc1129023/refs/banner.pptx -------------------------------------------------------------------------------- /refs/banner.svg: -------------------------------------------------------------------------------- 1 | PySimlink -------------------------------------------------------------------------------- /refs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 229 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cmake 2 | numpy 3 | pybind11 4 | 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("requirements.txt", "r") as f: 4 | reqs = f.read().splitlines() 5 | 6 | with open("README.md", "r") as f: 7 | readme = f.read() 8 | 9 | setup( 10 | name="pysimlink", 11 | version="1.2.1", 12 | author="Landon Harris", 13 | author_email="lharri73@vols.utk.edu", 14 | description="Compile, run, and interact with Simulink models natively in Python!", 15 | long_description=readme, 16 | long_description_content_type="text/markdown", 17 | packages=find_packages(), 18 | install_requires=reqs, 19 | extras_require={ 20 | "dev": [ 21 | "pylint", 22 | "black", 23 | "sphinx_rtd_theme", 24 | "sphinx", 25 | "tqdm", 26 | "sphinx-toolbox", 27 | "sphinx-hoverxref", 28 | "readthedocs-sphinx-search", 29 | ] 30 | }, 31 | include_package_data=True, 32 | python_requires=">=3.6", 33 | keywords=["Simulink"], 34 | license="GPLv3", 35 | classifiers=[ 36 | "Topic :: Scientific/Engineering", 37 | "Topic :: Software Development :: Code Generators", 38 | "Programming Language :: Python :: 3", 39 | "Development Status :: 3 - Alpha", 40 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 41 | "Operating System :: MacOS :: MacOS X", 42 | "Operating System :: POSIX", 43 | ], 44 | url="https://github.com/lharri73/PySimlink", 45 | project_urls={ 46 | "Documentation": "https://lharri73.github.io/PySimlink/", 47 | "Source": "https://github.com/lharri73/PySimlink", 48 | }, 49 | ) 50 | --------------------------------------------------------------------------------