├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------