├── .clang-format
├── .coveragerc
├── .editorconfig
├── .gitattributes
├── .github
├── pull_request_template.md
└── workflows
│ ├── _build_and_publish_documentation.yml
│ ├── _build_package.yml
│ ├── _code_quality.yml
│ ├── _merge_into_release.yml
│ ├── _test.yml
│ ├── _test_future.yml
│ ├── nightly_build.yml
│ ├── publish_release.yml
│ ├── pull_request_to_main.yml
│ ├── push.yml
│ ├── push_to_main.yml
│ └── push_to_release.yml
├── .gitignore
├── .gitmodules
├── .pre-commit-config.yaml
├── .sourcery.yaml
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── ADVANCED.md
├── CHANGELOG.md
├── LICENSE
├── LICENSE_THIRD_PARTY
├── MLMODEL.md
├── README.md
├── STYLEGUIDE.md
├── docs
├── Makefile
├── interface
│ └── schema.html
├── make.bat
├── schema
│ └── schema.json
└── source
│ ├── ADVANCED.md
│ ├── CHANGELOG.md
│ ├── LICENSE.md
│ ├── LICENSE_THIRD_PARTY.md
│ ├── MLMODEL.md
│ ├── README.md
│ ├── STYLEGUIDE.md
│ ├── _static
│ └── mlfmu_logo_v1.svg
│ ├── _templates
│ ├── custom-class.rst
│ └── custom-module.rst
│ ├── api.rst
│ ├── cli.mlfmu.rst
│ ├── cli.rst
│ ├── conf.py
│ └── index.rst
├── examples
├── README.md
├── wind_generator
│ ├── WindGenerator
│ │ ├── modelDescription.xml
│ │ ├── resources
│ │ │ └── example.onnx
│ │ └── sources
│ │ │ ├── fmu.cpp
│ │ │ └── model_definitions.h
│ ├── config
│ │ ├── example.onnx
│ │ └── interface.json
│ └── generated_fmu
│ │ └── WindGenerator.fmu
├── wind_to_power
│ ├── WindToPower
│ │ ├── modelDescription.xml
│ │ ├── resources
│ │ │ └── example.onnx
│ │ └── sources
│ │ │ ├── fmu.cpp
│ │ │ └── model_definitions.h
│ ├── config
│ │ ├── example.onnx
│ │ └── interface.json
│ └── generated_fmu
│ │ └── WindToPower.fmu
└── wind_to_power_pyspark
│ ├── MLFMU_RF_Regressor
│ ├── modelDescription.xml
│ ├── resources
│ │ └── rf_model.onnx
│ └── sources
│ │ ├── fmu.cpp
│ │ └── model_definitions.h
│ ├── config
│ ├── interface.json
│ └── rf_model.onnx
│ ├── generated_fmu
│ └── MLFMU_RF_Regressor.fmu
│ └── ml_model
│ ├── T1.csv
│ └── wind-turbine-power-prediction-gbtregressor-pyspark.ipynb
├── pyproject.toml
├── pytest.ini
├── qa.bat
├── ruff.toml
├── src
└── mlfmu
│ ├── __init__.py
│ ├── api.py
│ ├── cli
│ ├── __init__.py
│ ├── mlfmu.py
│ └── publish_docs.py
│ ├── fmu_build
│ ├── CMakeLists.txt
│ ├── cmake
│ │ ├── FindFMUComplianceChecker.cmake
│ │ ├── GenerateFmuGuid.cmake
│ │ ├── ZipAll.cmake
│ │ └── fmu-uuid.h.in
│ ├── conanfile.txt
│ ├── fmi
│ │ ├── fmi2FunctionTypes.h
│ │ ├── fmi2Functions.h
│ │ ├── fmi2TypesPlatform.h
│ │ ├── fmiFunctions.h
│ │ └── fmiPlatformTypes.h
│ └── templates
│ │ ├── fmu
│ │ ├── fmu_template.cpp
│ │ └── model_definitions_template.h
│ │ └── onnx_fmu
│ │ ├── onnxFmu.cpp
│ │ └── onnxFmu.hpp
│ ├── py.typed
│ ├── types
│ ├── __init__.py
│ ├── fmu_component.py
│ └── onnx_model.py
│ └── utils
│ ├── __init__.py
│ ├── builder.py
│ ├── fmi_builder.py
│ ├── interface.py
│ ├── logging.py
│ ├── path.py
│ ├── signals.py
│ └── strings.py
├── stubs
└── onnxruntime
│ └── __init__.pyi
├── tests
├── .gitignore
├── cli
│ └── test_mlfmu_cli.py
├── conftest.py
├── data
│ └── example.onnx
├── test_api.py
├── test_working_directory
│ ├── .gitignore
│ ├── test_config_file
│ ├── test_config_file.json
│ ├── test_config_file_empty
│ └── test_config_file_empty.json
├── types
│ └── test_fmu_component.py
└── utils
│ ├── test_fmu_template.py
│ ├── test_interface_validation.py
│ └── test_modelDescription_builder.py
└── uv.lock
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | Language: Cpp
3 | Standard: Cpp11
4 | BasedOnStyle: WebKit
5 | NamespaceIndentation: None
6 | IndentCaseLabels: true
7 | IndentPPDirectives: AfterHash
8 | FixNamespaceComments: true
9 | MaxEmptyLinesToKeep: 2
10 | SpaceAfterTemplateKeyword: false
11 | AllowShortCaseLabelsOnASingleLine: true
12 | AllowAllParametersOfDeclarationOnNextLine: true
13 | AllowShortIfStatementsOnASingleLine: true
14 | AllowShortBlocksOnASingleLine: true
15 | AllowShortLoopsOnASingleLine: true
16 | BreakBeforeBinaryOperators: None
17 | Cpp11BracedListStyle: true
18 | BreakBeforeBraces: Custom
19 | BraceWrapping:
20 | AfterEnum: true
21 | AfterStruct: true
22 | AfterClass: true
23 | SplitEmptyFunction: false
24 | AfterFunction: true
25 | AfterNamespace : true
26 | AfterControlStatement: false
27 | IncludeBlocks: Regroup
28 | IncludeCategories:
29 | - Regex: '^[<"]cosim[/.]'
30 | Priority: 20
31 | - Regex: '^[<"](boost|gsl|msgpack|nlohmann|Poco|zip)[/.]'
32 | Priority: 30
33 | - Regex: '^"'
34 | Priority: 10
35 | - Regex: '.*'
36 | Priority: 40
37 | AlignTrailingComments: true
38 | ...
39 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [paths]
2 | source =
3 | src/mlfmu
4 | */site-packages/mlfmu
5 |
6 | [run]
7 | source = mlfmu
8 | branch = True
9 |
10 | [report]
11 | fail_under = 10.0
12 | show_missing = True
13 | skip_covered = True
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 4
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
13 | [*.{yml,yaml}]
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.py text
7 | *.cpp text
8 | *.hpp text
9 | *.c text
10 | *.h text
11 | *.json text
12 | *.xml text
13 | *.txt text
14 | *.yml text
15 | *.yaml text
16 | *.toml text
17 | *.rst text
18 | *.ini text
19 |
20 | # Declare files that will always have CRLF line endings on checkout.
21 | *.vcproj text eol=crlf
22 | *.sln text eol=crlf
23 | *.md text eol=crlf
24 |
25 | # Declare files that will always have LF line endings on checkout.
26 | *.sh text eol=lf
27 |
28 | # Declare files that will not be normalized regardless of their content.
29 | *.jpg -text
30 | *.png -text
31 | *.gif -text
32 | *.ico -text
33 |
34 | # Denote all files that are truly binary and should not be modified.
35 | *.onnx binary
36 | *.fmu binary
37 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 |
5 |
6 |
7 |
8 | Resolves #issue_nr
9 |
10 |
11 |
12 | ...
13 |
14 |
15 |
16 | ## How Has This Been Tested?
17 |
18 |
19 |
20 |
21 | - [ ] Test A
22 | - [ ] Test B
23 |
24 | ## Screenshots (if appropriate)
25 |
26 | ## Developer Checklist (before requesting PR review)
27 |
28 | - [ ] My code follows the style guidelines of this project
29 | - [ ] My changes generate no new warnings
30 | - [ ] Any dependent changes have been merged and published in downstream modules
31 |
32 | I have:
33 |
34 | - [ ] commented my code, particularly in hard-to-understand areas
35 | - [ ] performed a self-review of my own code
36 | - [ ] not committed unnecessary formatting changes, thereby occluding the actual changes (e.g. change of tab spacing, eol, etc.)
37 | - [ ] made corresponding changes to the documentation
38 | - [ ] added change to CHANGELOG.md
39 | - [ ] added tests that prove my fix is effective or that my feature works (for core features)
40 |
41 | ## Reviewer checklist
42 |
43 | I have:
44 |
45 | - [ ] performed a self-review of my own code have performed a review of the code
46 | - [ ] tested that the software still works as expected
47 | - [ ] checked updates to documentation
48 | - [ ] checked that the CHANGELOG is updated
49 |
--------------------------------------------------------------------------------
/.github/workflows/_build_and_publish_documentation.yml:
--------------------------------------------------------------------------------
1 | name: Build and publish documentation
2 |
3 | on: workflow_call
4 |
5 | env:
6 | DEFAULT_BRANCH: 'release'
7 | #SPHINXOPTS: '-W --keep-going -T'
8 | # ^-- If these SPHINXOPTS are enabled, then be strict about the builds and fail on any warnings
9 |
10 | jobs:
11 | build-and-publish-docs:
12 | name: Build and publish documentation
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout active branch
16 | uses: actions/checkout@v4
17 | with:
18 | lfs: true
19 | - name: Install uv
20 | uses: astral-sh/setup-uv@v2
21 | with:
22 | enable-cache: true
23 | cache-dependency-glob: "uv.lock"
24 | - name: Set up Python
25 | uses: actions/setup-python@v5
26 | with:
27 | python-version-file: "pyproject.toml"
28 | - name: Install the project
29 | run: uv sync --upgrade
30 | - name: Print 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 | uv run python -V
42 | uv pip list
43 |
44 | # Build documentation
45 | - uses: sphinx-doc/github-problem-matcher@master
46 | - name: Build documentation
47 | run: |
48 | cd docs
49 | uv run make html
50 |
51 | - name: Clone and cleanup gh-pages branch
52 | run: |
53 | set -x
54 | git fetch
55 | ( git branch gh-pages remotes/origin/gh-pages && git clone . --branch=gh-pages _gh-pages/ ) || mkdir _gh-pages
56 | rm -rf _gh-pages/.git/
57 | mkdir -p _gh-pages/branch/
58 |
59 | # Delete orphaned branch-folders:
60 | # Go through each subfolder in _gh-pages/branch/
61 | # If it relates to an orphaned branch, delete it.
62 | - name: Delete orphaned branch-folders
63 | run: |
64 | set -x
65 | for brdir in `ls _gh-pages/branch/` ; do
66 | brname=${brdir//--/\/} # replace '--' with '/'
67 | if ! git show-ref remotes/origin/$brname ; then
68 | echo "Removing $brdir"
69 | rm -r _gh-pages/branch/$brdir/
70 | fi
71 | done
72 |
73 | # Copy documentation to _gh-pages/ (if push happened on release branch)
74 | - name: Copy documentation to _gh-pages/
75 | if: |
76 | contains(github.ref, env.DEFAULT_BRANCH)
77 | run: |
78 | set -x
79 | # Delete everything under _gh-pages/ that is from the
80 | # primary branch deployment. Excludes the other branches
81 | # _gh-pages/branch-* paths, and not including
82 | # _gh-pages itself.
83 | find _gh-pages/ -mindepth 1 ! -path '_gh-pages/branch*' -delete
84 | rsync -a docs/build/html/ _gh-pages/
85 |
86 | # Copy documentation to _gh-pages/branch/$brname (if push happened on any other branch)
87 | # ('/' gets replaced by '--')
88 | - name: Copy documentation to _gh-pages/branch/${{github.ref}}
89 | if: |
90 | !contains(github.ref, env.DEFAULT_BRANCH)
91 | run: |
92 | set -x
93 | #brname=$(git rev-parse --abbrev-ref HEAD)
94 | brname="${{github.ref}}"
95 | brname="${brname##refs/heads/}"
96 | brdir=${brname//\//--} # replace '/' with '--'
97 | rm -rf _gh-pages/branch/${brdir}
98 | rsync -a docs/build/html/ _gh-pages/branch/${brdir}
99 |
100 | # Add .nojekyll file
101 | - name: Add .nojekyll file
102 | run: touch _gh-pages/.nojekyll
103 |
104 | # Publish: Commit gh-pages branch and publish it to GitHub Pages
105 | - name: Publish documentation
106 | uses: peaceiris/actions-gh-pages@v4
107 | with:
108 | publish_branch: gh-pages
109 | github_token: ${{ secrets.GITHUB_TOKEN }}
110 | publish_dir: _gh-pages/
111 | force_orphan: true
112 |
--------------------------------------------------------------------------------
/.github/workflows/_build_package.yml:
--------------------------------------------------------------------------------
1 | name: Build Package
2 |
3 | on: workflow_call
4 |
5 | jobs:
6 | build:
7 | name: Build source distribution
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | with:
12 | lfs: true
13 | - run: git submodule update --init --recursive
14 | name: Checkout submodules
15 | - name: Install uv
16 | uses: astral-sh/setup-uv@v2
17 | with:
18 | enable-cache: true
19 | cache-dependency-glob: "uv.lock"
20 | - uses: actions/setup-python@v5
21 | with:
22 | python-version-file: "pyproject.toml"
23 | - name: Build source distribution and wheel
24 | run: uv build
25 | - name: Run twine check
26 | run: uvx twine check --strict dist/*
27 | - uses: actions/upload-artifact@v4
28 | with:
29 | path: |
30 | dist/*.tar.gz
31 | dist/*.whl
32 |
--------------------------------------------------------------------------------
/.github/workflows/_code_quality.yml:
--------------------------------------------------------------------------------
1 | name: Code Quality
2 |
3 | on: workflow_call
4 |
5 | jobs:
6 | ruff_format:
7 | runs-on: ubuntu-latest
8 | name: ruff format
9 | steps:
10 | - uses: actions/checkout@v4
11 | - name: Install uv
12 | uses: astral-sh/setup-uv@v2
13 | with:
14 | enable-cache: true
15 | cache-dependency-glob: "uv.lock"
16 | - name: Set up Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version-file: "pyproject.toml"
20 | - name: Install the project
21 | run: uv sync --upgrade
22 | - name: Run ruff format
23 | run: uv run ruff format --diff
24 |
25 | ruff_check:
26 | runs-on: ubuntu-latest
27 | name: ruff check
28 | steps:
29 | - uses: actions/checkout@v4
30 | - name: Install uv
31 | uses: astral-sh/setup-uv@v2
32 | with:
33 | enable-cache: true
34 | cache-dependency-glob: "uv.lock"
35 | - name: Set up Python
36 | uses: actions/setup-python@v5
37 | with:
38 | python-version-file: "pyproject.toml"
39 | - name: Install the project
40 | run: uv sync --upgrade
41 | - name: Run ruff check
42 | run: uv run ruff check --diff
43 |
44 | pyright:
45 | runs-on: ubuntu-latest
46 | name: pyright
47 | steps:
48 | - uses: actions/checkout@v4
49 | - name: Install uv
50 | uses: astral-sh/setup-uv@v2
51 | with:
52 | enable-cache: true
53 | cache-dependency-glob: "uv.lock"
54 | - name: Set up Python
55 | uses: actions/setup-python@v5
56 | with:
57 | python-version-file: "pyproject.toml"
58 | - name: Install the project
59 | run: uv sync --upgrade
60 | - name: Run pyright
61 | run: uv run pyright
62 |
63 | mypy:
64 | runs-on: ubuntu-latest
65 | name: mypy
66 | steps:
67 | - uses: actions/checkout@v4
68 | - name: Install uv
69 | uses: astral-sh/setup-uv@v2
70 | with:
71 | enable-cache: true
72 | cache-dependency-glob: "uv.lock"
73 | - name: Set up Python
74 | uses: actions/setup-python@v5
75 | with:
76 | python-version-file: "pyproject.toml"
77 | - name: Install the project
78 | run: uv sync --upgrade
79 | - name: Run mypy
80 | run: uv run mypy
81 |
--------------------------------------------------------------------------------
/.github/workflows/_merge_into_release.yml:
--------------------------------------------------------------------------------
1 | name: Merge into release
2 |
3 | on:
4 | workflow_call:
5 | secrets:
6 | RELEASE_TOKEN:
7 | required: true
8 |
9 | jobs:
10 | merge_into_release:
11 | name: Merge ${{ github.event.ref }} -> release branch
12 | runs-on: ubuntu-latest
13 | environment: release
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | # Fetch the whole history to prevent unrelated history errors
18 | fetch-depth: 0
19 | # The branch you want to checkout (usually equal to `branchtomerge`)
20 | # ref: ${{ github.event.ref }}
21 | - uses: devmasx/merge-branch@v1.4.0
22 | with:
23 | type: now
24 | target_branch: release
25 | github_token: ${{ secrets.RELEASE_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/_test.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on: workflow_call
4 |
5 | jobs:
6 | test:
7 | name: Test on ${{matrix.python.version}}-${{matrix.platform.runner}}
8 | runs-on: ${{ matrix.platform.runner }}
9 | strategy:
10 | matrix:
11 | platform:
12 | - runner: ubuntu-latest
13 | - runner: windows-latest
14 | # - runner: macos-latest
15 | python:
16 | - version: '3.10'
17 | - version: '3.11'
18 | - version: '3.12'
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Install uv
22 | uses: astral-sh/setup-uv@v2
23 | with:
24 | enable-cache: true
25 | cache-dependency-glob: "uv.lock"
26 | - name: Install Python ${{ matrix.python.version }}
27 | uses: actions/setup-python@v5
28 | with:
29 | python-version: ${{ matrix.python.version }}
30 | - name: Install the project
31 | run: uv sync --upgrade -p ${{ matrix.python.version }} --no-dev
32 | - name: Install pytest
33 | run: |
34 | uv pip install pytest
35 | uv pip install pytest-cov
36 | - name: Run pytest
37 | run: uv run pytest --cov
38 |
--------------------------------------------------------------------------------
/.github/workflows/_test_future.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests (py313)
2 | # Test also with Python 3.13 (experimental; workflow will not fail on error.)
3 |
4 | on: workflow_call
5 |
6 | jobs:
7 | test313:
8 | name: Test on ${{matrix.python.version}}-${{matrix.platform.runner}} (experimental)
9 | continue-on-error: true
10 | runs-on: ${{ matrix.platform.runner }}
11 | strategy:
12 | matrix:
13 | platform:
14 | - runner: ubuntu-latest
15 | - runner: windows-latest
16 | python:
17 | - version: '3.13.0-alpha - 3.13.0'
18 | uvpy: '3.13'
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Install uv
22 | uses: astral-sh/setup-uv@v2
23 | with:
24 | enable-cache: true
25 | cache-dependency-glob: "uv.lock"
26 | - name: Install Python ${{ matrix.python.version }}
27 | uses: actions/setup-python@v5
28 | with:
29 | python-version: ${{ matrix.python.version }}
30 | - name: Install the project
31 | run: uv sync --upgrade -p ${{ matrix.python.uvpy }} --no-dev
32 | - name: Install pytest
33 | run: |
34 | uv pip install pytest
35 | uv pip install pytest-cov
36 | - name: Run pytest
37 | run: uv run pytest --cov
38 |
--------------------------------------------------------------------------------
/.github/workflows/nightly_build.yml:
--------------------------------------------------------------------------------
1 | name: Nightly Build
2 | run-name: Nightly Build (by @${{ github.actor }})
3 |
4 | on:
5 | schedule:
6 | - cron: '30 5 * * *'
7 |
8 | jobs:
9 | code_quality:
10 | uses: ./.github/workflows/_code_quality.yml
11 | test:
12 | uses: ./.github/workflows/_test.yml
13 | test_future:
14 | uses: ./.github/workflows/_test_future.yml
15 | build_package:
16 | needs:
17 | - test
18 | uses: ./.github/workflows/_build_package.yml
19 | build_and_publish_documentation:
20 | needs:
21 | - build_package
22 | uses: ./.github/workflows/_build_and_publish_documentation.yml
23 |
--------------------------------------------------------------------------------
/.github/workflows/publish_release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Release
2 | run-name: Publish Release ${{ github.event.ref }} created by @${{ github.actor }}
3 |
4 | on:
5 | push:
6 | tags:
7 | - v*
8 |
9 | jobs:
10 | build_package:
11 | uses: ./.github/workflows/_build_package.yml
12 | publish_package:
13 | name: Publish package
14 | needs:
15 | - build_package
16 | runs-on: ubuntu-latest
17 | environment: release
18 | permissions:
19 | id-token: write
20 | steps:
21 | - uses: actions/download-artifact@v4
22 | with:
23 | name: artifact
24 | path: dist
25 | - uses: pypa/gh-action-pypi-publish@release/v1
26 | # with: # Uncomment this line to publish to testpypi
27 | # repository-url: https://test.pypi.org/legacy/ # Uncomment this line to publish to testpypi
28 | merge_into_release:
29 | uses: ./.github/workflows/_merge_into_release.yml
30 | secrets:
31 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/pull_request_to_main.yml:
--------------------------------------------------------------------------------
1 | name: Pull Request to main
2 | run-name: Pull Request to main from ${{ github.event.pull_request.head.ref }} by @${{ github.actor }}
3 |
4 | on:
5 | pull_request:
6 | types:
7 | - opened
8 | # - synchronize
9 | - reopened
10 | - ready_for_review
11 | - converted_to_draft
12 | branches:
13 | - main
14 |
15 | concurrency:
16 | group: pr-${{ github.ref }}-1
17 | cancel-in-progress: true
18 |
19 | jobs:
20 | code_quality:
21 | uses: ./.github/workflows/_code_quality.yml
22 | test:
23 | uses: ./.github/workflows/_test.yml
24 | # build_package:
25 | # needs:
26 | # - code_quality
27 | # - test
28 | # uses: ./.github/workflows/_build_package.yml
29 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: Push to custom branches
2 | run-name: Push to ${{ github.ref }} by @${{ github.actor }}
3 |
4 | on:
5 | push:
6 | branches-ignore:
7 | - main
8 | - release
9 |
10 | concurrency:
11 | group: push-${{ github.ref }}-1
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | code_quality:
16 | uses: ./.github/workflows/_code_quality.yml
17 | test:
18 | uses: ./.github/workflows/_test.yml
19 |
--------------------------------------------------------------------------------
/.github/workflows/push_to_main.yml:
--------------------------------------------------------------------------------
1 | name: Push to main
2 | run-name: Push to main by @${{ github.actor }}
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 |
9 | concurrency:
10 | group: push-${{ github.ref }}-1
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | code_quality:
15 | uses: ./.github/workflows/_code_quality.yml
16 | test:
17 | uses: ./.github/workflows/_test.yml
18 | build_package:
19 | needs:
20 | - code_quality
21 | - test
22 | uses: ./.github/workflows/_build_package.yml
23 | build_and_publish_documentation:
24 | needs:
25 | - build_package
26 | uses: ./.github/workflows/_build_and_publish_documentation.yml
27 |
--------------------------------------------------------------------------------
/.github/workflows/push_to_release.yml:
--------------------------------------------------------------------------------
1 | name: Push to release
2 | run-name: Push to release by @${{ github.actor }}
3 |
4 | on:
5 | push:
6 | branches:
7 | - release
8 |
9 | concurrency:
10 | group: push-${{ github.ref }}-1
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | build_and_publish_documentation:
15 | uses: ./.github/workflows/_build_and_publish_documentation.yml
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 | _autosummary
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # uv
91 | # It is generally recommended to include `uv.lock` in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, `uv` might install dependencies in one environment that don't work in another.
94 | # In such case, `uv.lock` should be added to `.gitignore`
95 | # uv.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
140 | # Ruff
141 | .ruff_cache
142 |
143 | # PyCharm
144 | .idea
145 |
146 | # VS Code
147 | .vscode/*
148 | !.vscode/settings.json
149 | !.vscode/tasks.json
150 | !.vscode/launch.json
151 | !.vscode/extensions.json
152 | !.vscode/*.code-snippets
153 |
154 | # Inside /examples folder: ignore temporary logs, db's and local data files
155 | examples/**/*.log
156 | examples/**/*.db
157 | examples/**/*.nc
158 |
159 | # Cmake generated files
160 | CMakeUserPresets.json
161 |
162 | # Generated FMUs
163 | *.fmu
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "src/mlfmu/fmu_build/cppfmu"]
2 | path = src/mlfmu/fmu_build/cppfmu
3 | url = https://github.com/viproma/cppfmu
4 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.6.0
4 | hooks:
5 | - id: mixed-line-ending
6 | fix: auto
7 | - id: trailing-whitespace
8 | - id: check-yaml
9 | - id: check-toml
10 | - id: check-merge-conflict
11 | - repo: https://github.com/astral-sh/ruff-pre-commit
12 | rev: v0.6.2
13 | hooks:
14 | - id: ruff
15 | - id: ruff-format
16 | # - repo: https://github.com/pre-commit/mirrors-mypy
17 | # rev: v1.9.0
18 | # hooks:
19 | # - id: mypy
20 | # exclude: tests/
21 |
--------------------------------------------------------------------------------
/.sourcery.yaml:
--------------------------------------------------------------------------------
1 | # 🪄 This is your project's Sourcery configuration file.
2 |
3 | # You can use it to get Sourcery working in the way you want, such as
4 | # ignoring specific refactorings, skipping directories in your project,
5 | # or writing custom rules.
6 |
7 | # 📚 For a complete reference to this file, see the documentation at
8 | # https://docs.sourcery.ai/Configuration/Project-Settings/
9 |
10 | # This file was auto-generated by Sourcery on 2023-02-22 at 11:42.
11 |
12 | version: '1' # The schema version of this config file
13 |
14 | ignore: # A list of paths or files which Sourcery will ignore.
15 | - .git
16 | - .venv
17 | - .tox
18 | - dist
19 | - __pycache__
20 | - src/mlfmu/fmu_build/cppfmu
21 |
22 | rule_settings:
23 | enable:
24 | - default
25 | disable: # A list of rule IDs Sourcery will never suggest.
26 | - inline-immediately-returned-variable
27 | rule_types:
28 | - refactoring
29 | - suggestion
30 | - comment
31 | python_version: '3.10' # A string specifying the lowest Python version your project supports. Sourcery will not suggest refactorings requiring a higher Python version.
32 |
33 | # rules: # A list of custom rules Sourcery will include in its analysis.
34 | # - id: no-print-statements
35 | # description: Do not use print statements in the test directory.
36 | # pattern: print(...)
37 | # language: python
38 | # replacement:
39 | # condition:
40 | # explanation:
41 | # paths:
42 | # include:
43 | # - test
44 | # exclude:
45 | # - conftest.py
46 | # tests: []
47 | # tags: []
48 |
49 | # rule_tags: {} # Additional rule tags.
50 |
51 | # metrics:
52 | # quality_threshold: 25.0
53 |
54 | # github:
55 | # labels: []
56 | # ignore_labels:
57 | # - sourcery-ignore
58 | # request_review: author
59 | # sourcery_branch: sourcery/{base_branch}
60 |
61 | # clone_detection:
62 | # min_lines: 3
63 | # min_duplicates: 2
64 | # identical_clones_only: false
65 |
66 | # proxy:
67 | # url:
68 | # ssl_certs_file:
69 | # no_ssl_verify: false
70 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 | // List of extensions which should be recommended for users of this workspace.
5 | "recommendations": [
6 | "ms-python.python",
7 | "ms-python.vscode-pylance",
8 | "visualstudioexptteam.vscodeintellicode",
9 | "njqdev.vscode-python-typehint",
10 | "charliermarsh.ruff",
11 | "sourcery.sourcery",
12 | "njpwerner.autodocstring",
13 | "editorconfig.editorconfig",
14 | "ms-python.mypy-type-checker",
15 | ],
16 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
17 | "unwantedRecommendations": []
18 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Python: Debug Tests",
9 | "type": "debugpy",
10 | "request": "launch",
11 | "program": "${file}",
12 | "purpose": [
13 | "debug-test"
14 | ],
15 | "console": "integratedTerminal",
16 | "env": {
17 | "PYTEST_ADDOPTS": "--no-cov"
18 | },
19 | "autoReload": {
20 | "enable": true
21 | },
22 | "justMyCode": true,
23 | },
24 | {
25 | "name": "Python: Current File, cwd = file dir",
26 | "type": "debugpy",
27 | "request": "launch",
28 | "cwd": "${fileDirname}", // working dir = dir where current file is
29 | "program": "${file}",
30 | "console": "integratedTerminal",
31 | "justMyCode": true,
32 | "autoReload": {
33 | "enable": true
34 | },
35 | },
36 | {
37 | "name": "Python: Current File, cwd = workspace root folder",
38 | "type": "debugpy",
39 | "request": "launch",
40 | "cwd": "${workspaceFolder}", // working dir = workspace root folder
41 | "program": "${file}",
42 | "console": "integratedTerminal",
43 | "autoReload": {
44 | "enable": true
45 | },
46 | "justMyCode": true,
47 | },
48 | {
49 | "name": "mlfmu test_cli",
50 | "type": "debugpy",
51 | "request": "launch",
52 | "cwd": "${workspaceFolder}\\tests",
53 | "program": "${workspaceFolder}\\src\\mlfmu\\cli\\mlfmu.py",
54 | "args": [
55 | "test_config_file",
56 | "--run",
57 | "-v",
58 | ],
59 | "console": "integratedTerminal",
60 | "autoReload": {
61 | "enable": true
62 | },
63 | "justMyCode": true,
64 | },
65 | ]
66 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.languageServer": "Pylance",
3 | "editor.formatOnSave": true,
4 | "notebook.formatOnSave.enabled": true,
5 | "notebook.codeActionsOnSave": {
6 | "notebook.source.fixAll": "explicit",
7 | "notebook.source.organizeImports": "explicit",
8 | },
9 | "[python]": {
10 | "editor.formatOnSave": true,
11 | "editor.codeActionsOnSave": {
12 | "source.fixAll": "always",
13 | "source.organizeImports": "explicit",
14 | },
15 | "editor.defaultFormatter": "charliermarsh.ruff",
16 | },
17 | "autoDocstring.docstringFormat": "numpy",
18 | "python.testing.unittestEnabled": false,
19 | "python.testing.pytestEnabled": true,
20 | "python.analysis.logLevel": "Warning",
21 | "python.analysis.completeFunctionParens": false,
22 | "python.analysis.diagnosticMode": "workspace",
23 | "python.analysis.indexing": true,
24 | "python.analysis.autoImportCompletions": true,
25 | "python.analysis.inlayHints.variableTypes": false,
26 | "python.analysis.inlayHints.functionReturnTypes": false,
27 | "python.analysis.inlayHints.pytestParameters": true,
28 | "python.terminal.executeInFileDir": true,
29 | "python.terminal.activateEnvironment": true,
30 | "python.terminal.activateEnvInCurrentTerminal": false,
31 | "python.analysis.packageIndexDepths": [
32 | {
33 | "name": "pandas",
34 | "depth": 4,
35 | "includeAllSymbols": true,
36 | },
37 | {
38 | "name": "matplotlib",
39 | "depth": 4,
40 | "includeAllSymbols": true,
41 | },
42 | {
43 | "name": "mpl_toolkits",
44 | "depth": 4,
45 | "includeAllSymbols": true,
46 | },
47 | ],
48 | "mypy-type-checker.importStrategy": "fromEnvironment",
49 | "mypy-type-checker.reportingScope": "workspace",
50 | "mypy-type-checker.preferDaemon": false,
51 | "ruff.configurationPreference": "filesystemFirst",
52 | }
--------------------------------------------------------------------------------
/ADVANCED.md:
--------------------------------------------------------------------------------
1 | # Advanced Usage
2 |
3 | ## Editing generated FMU source
4 |
5 | The command `mlfmu build` will both generate the C++ source code for the mlfmu and compile it automatically. However, it is possible to split this into two steps where it is possible to edit the source code to change the behavior of the resulting FMU.
6 |
7 | ```sh
8 | mlfmu codegen --interface-file Interface.json --model-file model.onnx --fmu-source-path path/to/generated/source
9 | ```
10 |
11 | This will result in a folder containing the source structured as below.
12 |
13 | ```sh
14 | [FmuName]
15 | ├── resources
16 | │ └── *.onnx
17 | ├── sources
18 | │ ├── fmu.cpp
19 | │ └── model_definitions.h
20 | └── modelDescription.xml
21 | ```
22 |
23 | Of these generated files, it is only recommended to modify `fmu.cpp`.
24 | In this file one can e.g. modify the `DoStep` function of the generated FMU class.
25 |
26 | ```cpp
27 | class FmuName : public OnnxFmu
28 | {
29 | public:
30 | FmuName(cppfmu::FMIString fmuResourceLocation)
31 | : OnnxFmu(fmuResourceLocation)
32 | { }
33 |
34 | bool DoStep(cppfmu::FMIReal currentCommunicationPoint, cppfmu::FMIReal dt, cppfmu::FMIBoolean newStep,
35 | cppfmu::FMIReal& endOfStep) override
36 | {
37 | // Implement custom behavior here
38 | // ...
39 |
40 | // Call the base class implementation
41 | return OnnxFmu::DoStep(currentCommunicationPoint, dt, newStep, endOfStep);
42 | }
43 | private:
44 | };
45 | ```
46 |
47 | After doing the modification to the source code, one can simply run the `compile` command to complete the process.
48 |
49 | ```sh
50 | mlfmu compile --fmu-source-path path/to/generated/source
51 | ```
52 |
53 | ## Using class
54 |
55 | In addition to the command line interface, one can use the same functionality of the tool through a Python class.
56 |
57 | 1. Import `MlFmuBuilder` and create an instance of it:
58 |
59 | ```python
60 | from mlfmu.api import MlFmuBuilder
61 | from pathlib import Path
62 |
63 | builder = MlFmuBuilder(
64 | ml_model_file = Path("path/to/model.onnx")
65 | interface_file = Path("path/to/interface.json")
66 | )
67 | ```
68 |
69 | 2. Call the same commands using the class:
70 |
71 | - Run `build`
72 |
73 | ```python
74 | builder.build()
75 | ```
76 |
77 | - Run `codegen` and then `compile`
78 |
79 | ```python
80 | builder.generate()
81 |
82 | # Do something ...
83 |
84 | builder.compile()
85 | ```
86 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to the [mlfmu] project will be documented in this file.
4 | The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5 |
6 | ## Unreleased
7 |
8 | ### Changed
9 |
10 | * Add README page for Examples dir, add generated FMUs for each example (given FMI tools page requirements for Examples), and update the files generated by mlfmu codegen (add for pyspark example)
11 | * Generation of modelDescription.xml file to correctly generate tags for each output with 1-indexed indexes and InitialUnknowns tags
12 | * Generation of modelDescription.xml adds the variables (inputs, parameters and outputs) in the orderer of their valueReference. This is for the indexing to work correctly
13 |
14 | ## [1.0.2]
15 |
16 | ### Changed
17 |
18 | * MLFMU logo is added and conf.py is updated.
19 | * Added checks for Windows vs Linux and fixed compilation for Linux/MacOS, updated README. Tested with Ubuntu 20.04, Ubuntu 22.04 and MacOS 14.4.
20 |
21 | ## [1.0.1]
22 |
23 | ### Changed
24 |
25 | * Update generated docs; cleaning, fix for warnings, add missing pages and info, update authors and maintainers.
26 | * Add where the source code for cppfmu can be found, add third party license.
27 | * Added missing unit tests for the template data generated when building the FMU.
28 | * Unit tests for the modelDescription.xml generation.
29 | * Unit tests for the Interface JSON validation.
30 | * Wind turbine power prediction model example is included in the mlfmu\examples\wind_to_power_pyspark directory.
31 | * README.md : Interface.json is updated to show how to define vectors as inputs.
32 | * Changed from `pip`/`tox` to `uv` as package manager
33 | * README.md : Completely rewrote section "Development Setup", introducing `uv` as package manager.
34 | * Added missing docstrings for py/cpp/h files with help of Github Copilot
35 | * Moved CMake + conan + c++ package files and folders with cpp code inside src folder to be included in package
36 | * Replace pkg_resources with importlib.metadata for getting packages version to work in python 3.12
37 | * Replace deprecated root_validator with model_validator in pydanitc class
38 | * Remove unnecessary hard coded values in utils/builder.py
39 | * Complete cli interface
40 | * Add subparsers to cli argparser to handle several different commands
41 | * Create MlFmuBuilder from args and run code according to command
42 | * Change cli test to match the new cli interface/parser
43 | * Default agent_(input/output)_indexes is [] by default instead of None
44 | * Updated doc by running publish-interface-docs
45 | * Deleted azure files from old azure devops repo
46 | * The generated modelDescription.xml files now also contain with a list of the outputs of the FMU.
47 | * OnnxFmu cpp template class updated to be able to initialize state with FMU Variables
48 | * Add variables to specify which FMU variable that should be used to initialize which state
49 | * Add function to DoStep that initializes the state at the beginning of the first time step.
50 | * model_definitions_template.h
51 | * Add definitions for the the number of states that should be initialized and array of index/value reference pairs to describe how the states should be initialized
52 | * Fmu Component json interface
53 | * Add name, description and start_value to state in the json interface
54 | * States changed from a single InternalState to a list of InternalState
55 | * Fixed typo from 'tunnable' to 'tunable'
56 | * Fixed number of onnx output check to be correct (1-3 and not always raising exception)
57 | * Fix correct fmi causality for parameters
58 | * Fix correct default variability for parameters
59 | * Fix expanding of array variables into one variable per index for inputs and parameters to work as outputs
60 | * Default build target in builder to wind_to_power example
61 | * CMakeList.txt (from old repo) to be able to take an arbitrary path where the fmu files to be compiled are located
62 | * Builder to run commands to compile generated files into an fmu
63 | * Can take and keep track of arbitrary paths
64 | * Builder checks if the files needed to compile the fmu exists before trying to compile the fmu
65 | * Replaced "TODO: Trow Error?" with raising a fitting exception
66 | * replaced black formatter with ruff formatter
67 | * Changed publishing workflow to use OpenID Connect (Trusted Publisher Management) when publishing to PyPI
68 | * Updated copyright statement
69 | * VS Code settings: Turned off automatic venv activation
70 | * Edited descriptions in the examples/wind_to_power/config/interface.json
71 |
72 | ### Added
73 |
74 | * Added BSD 3-Clause License
75 | * Added `mypy` as static type checker (in addition to `pyright`)
76 | * Add .gitattributes to handle line endings, removed eol from .editorconfig
77 | * Add .github/pull_request_template.md for enabling PR templates on Github
78 | * Add conan dependency to pyproject.toml
79 | * Add MlFmuBuilder class to generate code and compile FMU
80 | * Find default paths to files and directories if not given in cli
81 | * Run functions in utils/builder.py according to which command is being run
82 | * Clean up temporary/build files after the process is done
83 | * Added feature to be able to initialize states using previously defined parameters or inputs
84 | * This is done by setting "initializationVariable" = "{variable name}", instead of using the "name" and "start_value" attributes
85 | * Added flag in schema to allow a variable to be reused when initializing states
86 | * Allowed for multiple states to use the same variable for initialization
87 | * .clang-format to consistently format cpp code
88 | * Added code to generate parameters for initialization of state
89 | * Wind to power model example in examples
90 | * Onnx file containing ml model
91 | * Interface json file containing information needed not contained in onnx file
92 | * The generated fmu files resulting from running running builder on the new wind_to_power example
93 | * CMakeLists.txt and conanfile.txt from old repo to configure compiling/building FMU from generated files
94 |
95 | ### Removed
96 |
97 | * VS Code settings: Removed the setting which added the /src folder to PythonPath. This is no longer necessary. `uv` installs the project itself as a package in "editable" mode, which removes the need to manually add /src to the PythonPath environment variable.
98 |
99 | ### GitHub workflows
100 |
101 | * (all workflows): Adapted to use `uv` as package manager
102 | * _test_future.yml : updated Python version to 3.13.0-alpha - 3.13.0
103 | * _test_future.yml : updated name of test job to 'test313'
104 |
105 | ### Dependencies
106 |
107 | * Updated to ruff>=0.6.3 (from ruff==0.2.1)
108 | * Updated to pyright>=1.1.378 (from pyright==1.1.350)
109 | * Updated to sourcery>=1.22 (from sourcery==1.15)
110 | * Updated to pytest>=8.3 (from pytest>=7.4)
111 | * updated to pytest-cov>=5.0 (from pytest-cov>=4.1)
112 | * Updated to Sphinx>=8.0 (from Sphinx>=7.2)
113 | * Updated to sphinx-argparse-cli>=1.17 (from sphinx-argparse-cli>=1.11)
114 | * Updated to myst-parser>=4.0 (from myst-parser>=2.0)
115 | * Updated to furo>=2024.8 (from furo>=2023.9.10)
116 | * Updated to setup-python@v5 (from setup-python@v4)
117 | * Updated to actions-gh-pages@v4 (from actions-gh-pages@v3)
118 | * Updated to upload-artifact@v4 (from upload-artifact@v3)
119 | * Updated to download-artifact@v4 (from download-artifact@v3)
120 | * Updated to dictIO>=0.3.4 (from dictIO>=0.3.1)
121 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2024 [DNV](https://www.dnv.com) [open source](https://github.com/dnv-opensource)
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/MLMODEL.md:
--------------------------------------------------------------------------------
1 | # ML Model
2 |
3 | ## Onnx file
4 |
5 | There are some requirements for the onnx file to be compatible with this tool. This is so that the ML FMU tool knows how to connect the inputs and outputs of the onnx file to the FMU, to use it correctly.
6 |
7 | If the model takes a single input and has a single output it is already compatible as long as the input and output is shaped as (1, X) and can be directly connected to the variables of the FMU.
8 |
9 | ### The possible configuration of inputs and outputs of the model
10 |
11 | ```mermaid
12 | graph TD;
13 | subgraph 0["Only direct inputs"]
14 | inputs_3["inputs"]-->Model_3["Model"]
15 | Model_3["Model"]-->outputs_3["outputs"]
16 | end
17 |
18 | subgraph 3["All input types"]
19 | inputs_0["inputs"]-->Model_0["Model"]
20 | state_0["state"]-->Model_0["Model"]
21 | time_0["time"]-->Model_0["Model"]
22 | Model_0["Model"]-->outputs_0["outputs"]
23 | end
24 |
25 | subgraph 1["Inputs and States"]
26 | inputs_1["inputs"]-->Model_1["Model"]
27 | state_1["state"]-->Model_1["Model"]
28 | Model_1["Model"]-->outputs_1["outputs"]
29 | end
30 |
31 | subgraph 2["Inputs and time input"]
32 | inputs_2["inputs"]-->Model_2["Model"]
33 | time_2["time"]-->Model_2["Model"]
34 | Model_2["Model"]-->outputs_2["outputs"]
35 | end
36 |
37 | ```
38 |
39 | ## Usage in FMU
40 |
41 | ```mermaid
42 | graph TD;
43 |
44 | fmu_inputs["FMU inputs"]-->inputs
45 | fmu_parameters["FMU parameters"]-->inputs
46 | inputs --> Model
47 |
48 | previous_outputs["previous outputs"] --> state
49 | state --> Model
50 |
51 | simulator-->time
52 | time-->Model
53 |
54 | Model-->outputs
55 | outputs-->fmu_outputs["FMU outputs"]
56 |
57 | subgraph ONNX
58 | inputs
59 | state
60 | time
61 | outputs
62 | Model
63 | end
64 |
65 | ```
66 |
67 | [//]: # (## Tips and tricks - TODO)
68 |
69 | ## Examples
70 |
71 | ### Model that only uses "pure" inputs and outputs
72 |
73 | ```python
74 | class Model():
75 | num_inputs: int = 2
76 | num_outputs: int = 2
77 |
78 | ...
79 |
80 | def call(self, all_inputs):
81 | inputs, *_ = all_inputs
82 |
83 | # Do something with the inputs
84 | outputs = self.layers(inputs)
85 |
86 | return outputs
87 | ```
88 |
89 | ### Model using every input type
90 |
91 | Say we have trained an ML model to predict the derivatives of outputs from some data as shown below:
92 |
93 | ```python
94 | class DerivativePredictor():
95 | def call(self, all_inputs):
96 | prev_outputs, prev_inputs, curr_inputs, time = all_inputs
97 |
98 | # Do some ML stuff
99 | derivative = ...
100 |
101 | return derivative
102 | ```
103 |
104 | However, the FMU we want to create cannot use the same inputs and outputs as the trained ML model.
105 |
106 | We do not want to have previous inputs and outputs as inputs to the FMU. Instead we want it to remember the previous inputs and outputs itself. To do this we need to use the state inside the generated MLFMU using this tool.
107 |
108 | We also do not want the FMU to output the derivative, but instead use the derivative to integrate the outputs. This makes it possible for the outputs themselves to be the output of the FMU.
109 |
110 | To do this we need to make a wrapper around the trained ML model so that it is compatible with the tool and what we want the generated FMU to do.
111 |
112 | ``` python
113 | class ModelWrapper():
114 | num_inputs: int = 2
115 | num_outputs: int = 2
116 | derivative_predictor: DerivativePredictor = None
117 |
118 | def call(self, all_inputs):
119 | # Unpack inputs
120 | inputs, state, time = all_inputs
121 |
122 | # Unpack state into what is known to be saved in state
123 | # What is contained in state is decided by what is output from this function and the state settings in interface.json
124 | previous_outputs = state[:,:self.num_outputs]
125 | previous_inputs = state[:,self.num_outputs:]
126 |
127 | # Unpack time input
128 | current_time = all_inputs[:,:1]
129 | dt = all_inputs[:,1:2]
130 |
131 | # Predict something (e.g. a derivative) from the unpacked input data
132 | outputs_derivative = self.derivative_predictor([previous_outputs, previous_inputs, inputs, current_time])
133 |
134 | # Do other calculation to get data that needs to be output from the FMU
135 | outputs = previous_outputs + dt * output_derivative
136 |
137 | # Format outputs from the model to contain everything that needs to output from the FMU and/or saved as state
138 | # Saving the state is easier if outputs are in the same order as they are expected to be saved in state
139 | all_outputs = self.concat([outputs, inputs])
140 |
141 | return all_outputs
142 | ```
143 |
--------------------------------------------------------------------------------
/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 = source
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/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=source
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% -a -E
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/ADVANCED.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../ADVANCED.md
2 | ```
3 |
--------------------------------------------------------------------------------
/docs/source/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../CHANGELOG.md
2 | ```
--------------------------------------------------------------------------------
/docs/source/LICENSE.md:
--------------------------------------------------------------------------------
1 | # LICENSE
2 | ```{include} ../../LICENSE
3 | ```
--------------------------------------------------------------------------------
/docs/source/LICENSE_THIRD_PARTY.md:
--------------------------------------------------------------------------------
1 | # LICENSE THIRD PARTY
2 |
3 | ```{include} ../../LICENSE_THIRD_PARTY
4 | ```
5 |
--------------------------------------------------------------------------------
/docs/source/MLMODEL.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../MLMODEL.md
2 | ```
3 |
--------------------------------------------------------------------------------
/docs/source/README.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../README.md
2 | ```
--------------------------------------------------------------------------------
/docs/source/STYLEGUIDE.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../STYLEGUIDE.md
2 | ```
--------------------------------------------------------------------------------
/docs/source/_static/mlfmu_logo_v1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/source/_templates/custom-class.rst:
--------------------------------------------------------------------------------
1 | {{ name | escape | underline}}
2 |
3 | .. currentmodule:: {{ module }}
4 |
5 | .. autoclass:: {{ objname }}
6 | :members:
7 | :show-inheritance:
8 |
9 |
10 | {% block methods %}
11 | .. automethod:: __init__
12 | {% if methods %}
13 | .. rubric:: {{ _('Methods') }}
14 | .. autosummary::
15 | {% for item in methods %}
16 | ~{{ name }}.{{ item }}
17 | {%- endfor %}
18 | {% endif %}
19 | {% endblock %}
20 |
21 |
22 | {% block attributes %}
23 | {% if attributes %}
24 | .. rubric:: {{ _('Attributes') }}
25 | .. autosummary::
26 | {% for item in attributes %}
27 | ~{{ name }}.{{ item }}
28 | {%- endfor %}
29 | {% endif %}
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/docs/source/_templates/custom-module.rst:
--------------------------------------------------------------------------------
1 | {{ fullname | escape | underline}}
2 |
3 | .. automodule:: {{ fullname }}
4 | :members:
5 | {%- if classes %}
6 | :exclude-members: {% for item in classes %}{{ item }}{{','}}{%- endfor %}
7 | {% endif %}
8 |
9 |
10 | {% block attributes %}
11 | {%- if attributes %}
12 | .. rubric:: {{ _('Module Attributes') }}
13 | .. autosummary::
14 | {% for item in attributes %}
15 | {{ item }}
16 | {%- endfor %}
17 | {% endif %}
18 | {%- endblock %}
19 |
20 |
21 | {%- block functions %}
22 | {%- if functions %}
23 | .. rubric:: {{ _('Functions') }}
24 | .. autosummary::
25 | {% for item in functions %}
26 | {{ item }}
27 | {%- endfor %}
28 | {% endif %}
29 | {%- endblock %}
30 |
31 |
32 | {%- block classes %}
33 | {%- if classes %}
34 | .. rubric:: {{ _('Classes') }}
35 | .. autosummary::
36 | :toctree:
37 | :template: custom-class.rst
38 | {% for item in classes %}
39 | {{ item }}
40 | {%- endfor %}
41 | {% endif %}
42 | {%- endblock %}
43 |
44 |
45 | {%- block exceptions %}
46 | {%- if exceptions %}
47 | .. rubric:: {{ _('Exceptions') }}
48 | .. autosummary::
49 | {% for item in exceptions %}
50 | {{ item }}
51 | {%- endfor %}
52 | {% endif %}
53 | {%- endblock %}
54 |
55 |
56 | {%- block modules %}
57 | {%- if modules %}
58 | .. rubric:: Modules
59 | .. autosummary::
60 | :toctree:
61 | :template: custom-module.rst
62 | :recursive:
63 | {% for item in modules %}
64 | {{ item }}
65 | {%- endfor %}
66 | {% endif %}
67 | {%- endblock %}
68 |
--------------------------------------------------------------------------------
/docs/source/api.rst:
--------------------------------------------------------------------------------
1 | API Reference
2 | =============
3 |
4 | .. autosummary::
5 | :toctree: _autosummary
6 | :template: custom-module.rst
7 | :recursive:
8 |
9 | mlfmu.api
10 | mlfmu.types
11 | mlfmu.utils
12 |
--------------------------------------------------------------------------------
/docs/source/cli.mlfmu.rst:
--------------------------------------------------------------------------------
1 | .. sphinx_argparse_cli::
2 | :module: mlfmu.cli.mlfmu
3 | :func: _argparser
4 |
--------------------------------------------------------------------------------
/docs/source/cli.rst:
--------------------------------------------------------------------------------
1 | CLI Documentation
2 | =================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | cli.mlfmu
8 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # ruff: noqa
2 | # mypy: ignore-errors
3 |
4 | # Configuration file for the Sphinx documentation builder.
5 | #
6 | # For the full list of built-in configuration values, see the documentation:
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 | #
15 | import sys
16 | from pathlib import Path
17 |
18 | sys.path.insert(0, str(Path("../../src").absolute()))
19 |
20 |
21 | # -- Project information -----------------------------------------------------
22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
23 |
24 | project = "mlfmu"
25 | copyright = "2024, DNV AS. All rights reserved."
26 | author = "Kristoffer Skare, Jorge Luis Mendez, Stephanie Kemna, Melih Akdag"
27 |
28 | # The full version, including alpha/beta/rc tags
29 | release = "1.0.2"
30 |
31 | # -- General configuration ---------------------------------------------------
32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
33 |
34 | extensions = [
35 | "myst_parser",
36 | "sphinx.ext.autodoc",
37 | "sphinx.ext.napoleon",
38 | "sphinx_argparse_cli",
39 | "sphinx.ext.mathjax",
40 | "sphinx.ext.autosummary",
41 | "sphinx.ext.todo",
42 | "sphinxcontrib.mermaid",
43 | ]
44 |
45 | # The file extensions of source files.
46 | source_suffix = {
47 | ".rst": "restructuredtext",
48 | ".md": "markdown",
49 | }
50 |
51 | # Add any paths that contain templates here, relative to this directory.
52 | templates_path = ["_templates"]
53 |
54 | # List of patterns, relative to source directory, that match files and
55 | # directories to ignore when looking for source files.
56 | # This pattern also affects html_static_path and html_extra_path.
57 | exclude_patterns = []
58 |
59 |
60 | # -- Options for HTML output -------------------------------------------------
61 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
62 |
63 | html_title = f"mlfmu {release}"
64 | html_theme = "furo"
65 | html_static_path = ["_static"]
66 | html_logo = "_static/mlfmu_logo_v1.svg"
67 | autodoc_default_options = {
68 | "member-order": "groupwise",
69 | "undoc-members": True,
70 | "exclude-members": "__weakref__",
71 | }
72 | autodoc_preserve_defaults = True
73 |
74 | myst_heading_anchors = 3
75 |
76 | # add markdown mermaid support
77 | myst_fence_as_directive = ["mermaid"]
78 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. mlfmu documentation master file, created by
2 | sphinx-quickstart on Wed Jul 6 21:16:21 2022.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | mlfmu: Example Python Package
7 | =========================================
8 |
9 | .. toctree::
10 | :maxdepth: 3
11 | :caption: Contents:
12 |
13 | README
14 | MLMODEL
15 | ADVANCED
16 | cli
17 | api
18 | CHANGELOG
19 | STYLEGUIDE
20 | LICENSE
21 | LICENSE_THIRD_PARTY
22 |
23 | Indices and tables
24 | ==================
25 |
26 | * :ref:`genindex`
27 | * :ref:`modindex`
28 | * :ref:`search`
29 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This folder contains a couple of example files to use for testing the MLFMU tools and some FMUs that have been generated using this tool.
4 | Each folder is set up to contain at least:
5 |
6 | * `config`: the `.onnx` and `interface.json` files, which you can use to test the `mlfmu` commands with
7 | * `generated_fmu`: binary FMU files, to serve as examples, as generated when running `mlfmu build` for the config files
8 | * ``: files for the FMU as generated when running `mlfmu codegen`
9 |
10 | The FMUs in this folder have been validated using [FMU_check].
11 |
12 | For further documentation of the `mlfmu` tool, see [README.md](../README.md) or the [docs] on GitHub pages.
13 |
14 |
15 | [FMU_check]: https://fmu-check.herokuapp.com/
16 | [docs]: https://dnv-opensource.github.io/mlfmu/
17 |
--------------------------------------------------------------------------------
/examples/wind_generator/WindGenerator/modelDescription.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/examples/wind_generator/WindGenerator/resources/example.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_generator/WindGenerator/resources/example.onnx
--------------------------------------------------------------------------------
/examples/wind_generator/WindGenerator/sources/fmu.cpp:
--------------------------------------------------------------------------------
1 | #include "fmu-uuid.h"
2 | #include "model_definitions.h"
3 |
4 | #include
5 | #include
6 |
7 |
8 | /**
9 | * \class WindGenerator
10 | * \brief A class representing an WindGenerator FMU.
11 | *
12 | * This class is derived from the OnnxFmu class and provides functionality specific to the WindGenerator FMU.
13 | */
14 | class WindGenerator : public OnnxFmu {
15 | public :
16 | /**
17 | * \brief Constructs a new WindGenerator object.
18 | *
19 | * \param fmuResourceLocation The location of the resources of the FMU.
20 | */
21 | WindGenerator(cppfmu::FMIString fmuResourceLocation) : OnnxFmu(fmuResourceLocation) {}
22 |
23 | private :
24 | // Add private members and functions here
25 | };
26 |
27 | /**
28 | * \brief Instantiate a `slave` instance of the FMU.
29 | *
30 | * \param instanceName The name of the FMU instance.
31 | * \param fmuGUID The GUID of the FMU.
32 | * \param fmuResourceLocation The location of the FMU resource.
33 | * \param mimeType The MIME type of the FMU.
34 | * \param timeout The timeout value for the instantiation process.
35 | * \param visible Flag indicating whether the FMU should be visible.
36 | * \param interactive Flag indicating whether the FMU should be interactive.
37 | * \param memory The memory to be used for the FMU instance.
38 | * \param logger The logger to be used for logging messages.
39 | * \returns A unique pointer to the instantiated slave instance.
40 | *
41 | * \throws std::runtime_error if the FMU GUID does not match.
42 | */
43 | cppfmu::UniquePtr CppfmuInstantiateSlave(
44 | cppfmu::FMIString /*instanceName*/, cppfmu::FMIString fmuGUID, cppfmu::FMIString fmuResourceLocation,
45 | cppfmu::FMIString /*mimeType*/, cppfmu::FMIReal /*timeout*/, cppfmu::FMIBoolean /*visible*/,
46 | cppfmu::FMIBoolean /*interactive*/, cppfmu::Memory memory, cppfmu::Logger /*logger*/)
47 | {
48 | if (std::strcmp(fmuGUID, FMU_UUID) != 0) {
49 | throw std::runtime_error("FMU GUID mismatch");
50 | }
51 | return cppfmu::AllocateUnique(memory, fmuResourceLocation);
52 | }
53 |
--------------------------------------------------------------------------------
/examples/wind_generator/WindGenerator/sources/model_definitions.h:
--------------------------------------------------------------------------------
1 | #define NUM_FMU_VARIABLES 6
2 |
3 | #define NUM_ONNX_INPUTS 2
4 | #define NUM_ONNX_FMU_INPUTS 2
5 | #define NUM_ONNX_OUTPUTS 130
6 | #define NUM_ONNX_FMU_OUTPUTS 2
7 | #define NUM_ONNX_STATES 130
8 | #define NUM_ONNX_STATES_OUTPUTS 130
9 | #define NUM_ONNX_STATE_INIT 2
10 |
11 | #define ONNX_INPUT_VALUE_REFERENCES 0, 0, 1, 1
12 | #define ONNX_OUTPUT_VALUE_REFERENCES 0, 2, 1, 3
13 | #define ONNX_STATE_OUTPUT_INDEXES 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129
14 | #define ONNX_STATE_INIT_VALUE_REFERENCES 0, 4, 1, 5
15 |
16 | #define ONNX_USE_TIME_INPUT true
17 |
18 | #define ONNX_INPUT_NAME "inputs"
19 | #define ONNX_OUTPUT_NAME "output_1"
20 | #define ONNX_STATE_NAME "state"
21 | #define ONNX_TIME_INPUT_NAME "time"
22 | #define ONNX_FILENAME L"example.onnx"
--------------------------------------------------------------------------------
/examples/wind_generator/config/example.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_generator/config/example.onnx
--------------------------------------------------------------------------------
/examples/wind_generator/config/interface.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "WindGenerator",
3 | "description": "A Machine Learning based FMU that outputs a synthetic time series of wind.",
4 | "usesTime": true,
5 | "inputs": [
6 | {
7 | "name": "speedNoise",
8 | "description": "Noise to be added to the change in wind speed",
9 | "agentInputIndexes": [
10 | "0"
11 | ]
12 | },
13 | {
14 | "name": "directionNoise",
15 | "description": "Noise to be added to the change in wind direction",
16 | "agentInputIndexes": [
17 | "1"
18 | ]
19 | }
20 | ],
21 | "parameters": [],
22 | "outputs": [
23 | {
24 | "name": "windSpeed",
25 | "description": "The speed of the wind",
26 | "agentOutputIndexes": [
27 | "0"
28 | ]
29 | },
30 | {
31 | "name": "windDirection",
32 | "description": "The direction of the wind",
33 | "agentOutputIndexes": [
34 | "1"
35 | ]
36 | }
37 | ],
38 | "states": [
39 | {
40 | "name": "initialWindSpeed",
41 | "startValue": 10.0,
42 | "agentOutputIndexes": [
43 | "0"
44 | ]
45 | },
46 | {
47 | "name": "initialWindDirection",
48 | "startValue": 180.0,
49 | "agentOutputIndexes": [
50 | "1"
51 | ]
52 | },
53 | {
54 | "agentOutputIndexes": [
55 | "2:130"
56 | ]
57 | }
58 | ]
59 | }
--------------------------------------------------------------------------------
/examples/wind_generator/generated_fmu/WindGenerator.fmu:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_generator/generated_fmu/WindGenerator.fmu
--------------------------------------------------------------------------------
/examples/wind_to_power/WindToPower/modelDescription.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/wind_to_power/WindToPower/resources/example.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_to_power/WindToPower/resources/example.onnx
--------------------------------------------------------------------------------
/examples/wind_to_power/WindToPower/sources/fmu.cpp:
--------------------------------------------------------------------------------
1 | #include "fmu-uuid.h"
2 | #include "model_definitions.h"
3 |
4 | #include
5 | #include
6 |
7 |
8 | /**
9 | * \class WindToPower
10 | * \brief A class representing an WindToPower FMU.
11 | *
12 | * This class is derived from the OnnxFmu class and provides functionality specific to the WindToPower FMU.
13 | */
14 | class WindToPower : public OnnxFmu {
15 | public :
16 | /**
17 | * \brief Constructs a new WindToPower object.
18 | *
19 | * \param fmuResourceLocation The location of the resources of the FMU.
20 | */
21 | WindToPower(cppfmu::FMIString fmuResourceLocation) : OnnxFmu(fmuResourceLocation) {}
22 |
23 | private :
24 | // Add private members and functions here
25 | };
26 |
27 | /**
28 | * \brief Instantiate a `slave` instance of the FMU.
29 | *
30 | * \param instanceName The name of the FMU instance.
31 | * \param fmuGUID The GUID of the FMU.
32 | * \param fmuResourceLocation The location of the FMU resource.
33 | * \param mimeType The MIME type of the FMU.
34 | * \param timeout The timeout value for the instantiation process.
35 | * \param visible Flag indicating whether the FMU should be visible.
36 | * \param interactive Flag indicating whether the FMU should be interactive.
37 | * \param memory The memory to be used for the FMU instance.
38 | * \param logger The logger to be used for logging messages.
39 | * \returns A unique pointer to the instantiated slave instance.
40 | *
41 | * \throws std::runtime_error if the FMU GUID does not match.
42 | */
43 | cppfmu::UniquePtr CppfmuInstantiateSlave(
44 | cppfmu::FMIString /*instanceName*/, cppfmu::FMIString fmuGUID, cppfmu::FMIString fmuResourceLocation,
45 | cppfmu::FMIString /*mimeType*/, cppfmu::FMIReal /*timeout*/, cppfmu::FMIBoolean /*visible*/,
46 | cppfmu::FMIBoolean /*interactive*/, cppfmu::Memory memory, cppfmu::Logger /*logger*/)
47 | {
48 | if (std::strcmp(fmuGUID, FMU_UUID) != 0) {
49 | throw std::runtime_error("FMU GUID mismatch");
50 | }
51 | return cppfmu::AllocateUnique(memory, fmuResourceLocation);
52 | }
53 |
--------------------------------------------------------------------------------
/examples/wind_to_power/WindToPower/sources/model_definitions.h:
--------------------------------------------------------------------------------
1 | #define NUM_FMU_VARIABLES 3
2 |
3 | #define NUM_ONNX_INPUTS 2
4 | #define NUM_ONNX_FMU_INPUTS 2
5 | #define NUM_ONNX_OUTPUTS 1
6 | #define NUM_ONNX_FMU_OUTPUTS 1
7 | #define NUM_ONNX_STATES 0
8 | #define NUM_ONNX_STATES_OUTPUTS 0
9 | #define NUM_ONNX_STATE_INIT 0
10 |
11 | #define ONNX_INPUT_VALUE_REFERENCES 0, 0, 1, 1
12 | #define ONNX_OUTPUT_VALUE_REFERENCES 0, 2
13 | #define ONNX_STATE_OUTPUT_INDEXES
14 | #define ONNX_STATE_INIT_VALUE_REFERENCES
15 |
16 | #define ONNX_USE_TIME_INPUT false
17 |
18 | #define ONNX_INPUT_NAME "args_0"
19 | #define ONNX_OUTPUT_NAME "output_1"
20 | #define ONNX_STATE_NAME ""
21 | #define ONNX_TIME_INPUT_NAME ""
22 | #define ONNX_FILENAME L"example.onnx"
--------------------------------------------------------------------------------
/examples/wind_to_power/config/example.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_to_power/config/example.onnx
--------------------------------------------------------------------------------
/examples/wind_to_power/config/interface.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "WindToPower",
3 | "description": "A Machine Learning based FMU that outputs the estimated power output of a wind turbine given the wind speed and direction.",
4 | "inputs": [
5 | {
6 | "name": "windSpeed",
7 | "description": "The speed of the wind",
8 | "agentInputIndexes": [
9 | "0"
10 | ]
11 | },
12 | {
13 | "name": "windDirection",
14 | "description": "The direction of the wind",
15 | "agentInputIndexes": [
16 | "1"
17 | ]
18 | }
19 | ],
20 | "parameters": [],
21 | "outputs": [
22 | {
23 | "name": "power",
24 | "description": "The estimated wind turbine power output",
25 | "agentOutputIndexes": [
26 | "0"
27 | ]
28 | }
29 | ],
30 | "states": []
31 | }
--------------------------------------------------------------------------------
/examples/wind_to_power/generated_fmu/WindToPower.fmu:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_to_power/generated_fmu/WindToPower.fmu
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/MLFMU_RF_Regressor/modelDescription.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/MLFMU_RF_Regressor/resources/rf_model.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_to_power_pyspark/MLFMU_RF_Regressor/resources/rf_model.onnx
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/MLFMU_RF_Regressor/sources/fmu.cpp:
--------------------------------------------------------------------------------
1 | #include "fmu-uuid.h"
2 | #include "model_definitions.h"
3 |
4 | #include
5 | #include
6 |
7 |
8 | /**
9 | * \class MLFMU_RF_Regressor
10 | * \brief A class representing an MLFMU_RF_Regressor FMU.
11 | *
12 | * This class is derived from the OnnxFmu class and provides functionality specific to the MLFMU_RF_Regressor FMU.
13 | */
14 | class MLFMU_RF_Regressor : public OnnxFmu {
15 | public :
16 | /**
17 | * \brief Constructs a new MLFMU_RF_Regressor object.
18 | *
19 | * \param fmuResourceLocation The location of the resources of the FMU.
20 | */
21 | MLFMU_RF_Regressor(cppfmu::FMIString fmuResourceLocation) : OnnxFmu(fmuResourceLocation) {}
22 |
23 | private :
24 | // Add private members and functions here
25 | };
26 |
27 | /**
28 | * \brief Instantiate a `slave` instance of the FMU.
29 | *
30 | * \param instanceName The name of the FMU instance.
31 | * \param fmuGUID The GUID of the FMU.
32 | * \param fmuResourceLocation The location of the FMU resource.
33 | * \param mimeType The MIME type of the FMU.
34 | * \param timeout The timeout value for the instantiation process.
35 | * \param visible Flag indicating whether the FMU should be visible.
36 | * \param interactive Flag indicating whether the FMU should be interactive.
37 | * \param memory The memory to be used for the FMU instance.
38 | * \param logger The logger to be used for logging messages.
39 | * \returns A unique pointer to the instantiated slave instance.
40 | *
41 | * \throws std::runtime_error if the FMU GUID does not match.
42 | */
43 | cppfmu::UniquePtr CppfmuInstantiateSlave(
44 | cppfmu::FMIString /*instanceName*/, cppfmu::FMIString fmuGUID, cppfmu::FMIString fmuResourceLocation,
45 | cppfmu::FMIString /*mimeType*/, cppfmu::FMIReal /*timeout*/, cppfmu::FMIBoolean /*visible*/,
46 | cppfmu::FMIBoolean /*interactive*/, cppfmu::Memory memory, cppfmu::Logger /*logger*/)
47 | {
48 | if (std::strcmp(fmuGUID, FMU_UUID) != 0) {
49 | throw std::runtime_error("FMU GUID mismatch");
50 | }
51 | return cppfmu::AllocateUnique(memory, fmuResourceLocation);
52 | }
53 |
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/MLFMU_RF_Regressor/sources/model_definitions.h:
--------------------------------------------------------------------------------
1 | #define NUM_FMU_VARIABLES 5
2 |
3 | #define NUM_ONNX_INPUTS 4
4 | #define NUM_ONNX_FMU_INPUTS 4
5 | #define NUM_ONNX_OUTPUTS 1
6 | #define NUM_ONNX_FMU_OUTPUTS 1
7 | #define NUM_ONNX_STATES 0
8 | #define NUM_ONNX_STATES_OUTPUTS 0
9 | #define NUM_ONNX_STATE_INIT 0
10 |
11 | #define ONNX_INPUT_VALUE_REFERENCES 0, 0, 1, 1, 2, 2, 3, 3
12 | #define ONNX_OUTPUT_VALUE_REFERENCES 0, 4
13 | #define ONNX_STATE_OUTPUT_INDEXES
14 | #define ONNX_STATE_INIT_VALUE_REFERENCES
15 |
16 | #define ONNX_USE_TIME_INPUT false
17 |
18 | #define ONNX_INPUT_NAME "features"
19 | #define ONNX_OUTPUT_NAME "prediction"
20 | #define ONNX_STATE_NAME ""
21 | #define ONNX_TIME_INPUT_NAME ""
22 | #define ONNX_FILENAME L"rf_model.onnx"
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/config/interface.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MLFMU_RF_Regressor",
3 | "description": "A Random Forest Regressor based FMU",
4 | "usesTime": false,
5 | "inputs": [
6 | {
7 | "name": "features",
8 | "description": "Inputs with four features: month, hour, wind_speed, wind_direction",
9 | "agentInputIndexes": ["0:4"],
10 | "type": "real",
11 | "isArray": true,
12 | "length": 4
13 | }
14 | ],
15 | "parameters": [],
16 | "outputs": [
17 | {
18 | "name": "prediction",
19 | "description": "The prediction generated by ML model",
20 | "agentOutputIndexes": ["0"]
21 | }
22 | ]
23 | }
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/config/rf_model.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_to_power_pyspark/config/rf_model.onnx
--------------------------------------------------------------------------------
/examples/wind_to_power_pyspark/generated_fmu/MLFMU_RF_Regressor.fmu:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/examples/wind_to_power_pyspark/generated_fmu/MLFMU_RF_Regressor.fmu
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "hatchling",
4 | ]
5 | build-backend = "hatchling.build"
6 |
7 | [tool.hatch.build.targets.sdist]
8 | only-include = [
9 | "src/mlfmu",
10 | "tests",
11 | ".coveragerc",
12 | ".editorconfig",
13 | "pytest.ini",
14 | "ruff.toml",
15 | "uv.lock",
16 | ]
17 |
18 | [tool.hatch.build.targets.wheel]
19 | packages = [
20 | "src/mlfmu",
21 | ]
22 |
23 | [project]
24 | name = "mlfmu"
25 | version = "1.0.2"
26 | description = "Export ML models represented as ONNX files to Functional-Mockup-Units (FMU)"
27 | readme = "README.md"
28 | requires-python = ">= 3.10"
29 | license = { file = "LICENSE" }
30 | authors = [
31 | { name = "Jorge Luis Mendez", email = "jorge.luis.mendez@dnv.com" },
32 | { name = "Kristoffer Skare", email = "kristoffer.skare@dnv.com" },
33 | ]
34 | maintainers = [
35 | { name = "Kristoffer Skare", email = "kristoffer.skare@dnv.com" },
36 | { name = "Jorge Luis Mendez", email = "jorge.luis.mendez@dnv.com" },
37 | { name = "Hee Jong Park", email = "hee.jong.park@dnv.com" },
38 | { name = "Claas Rostock", email = "claas.rostock@dnv.com" },
39 | ]
40 | keywords = [
41 | ]
42 | classifiers = [
43 | "Development Status :: 3 - Alpha",
44 | "License :: OSI Approved :: BSD License",
45 | "Programming Language :: Python :: 3.10",
46 | "Programming Language :: Python :: 3.11",
47 | "Programming Language :: Python :: 3.12",
48 | "Operating System :: Microsoft :: Windows",
49 | "Operating System :: POSIX :: Linux",
50 | # "Operating System :: MacOS",
51 | "Environment :: Console",
52 | "Intended Audience :: Developers",
53 | "Intended Audience :: Science/Research",
54 | "Topic :: Scientific/Engineering",
55 | "Topic :: Software Development :: Libraries :: Python Modules",
56 | ]
57 | dependencies = [
58 | "dictIO>=0.3.4",
59 | "pydantic>=2.6",
60 | "json-schema-for-humans>=0.4.7",
61 | "onnxruntime==1.18.1",
62 | "conan>=2.1",
63 | ]
64 |
65 | [project.urls]
66 | Homepage = "https://github.com/dnv-opensource/mlfmu"
67 | Documentation = "https://dnv-opensource.github.io/mlfmu/README.html"
68 | Repository = "https://github.com/dnv-opensource/mlfmu.git"
69 | Issues = "https://github.com/dnv-opensource/mlfmu/issues"
70 | Changelog = "https://github.com/dnv-opensource/mlfmu/blob/main/CHANGELOG.md"
71 |
72 |
73 | [tool.uv]
74 | dev-dependencies = [
75 | "pytest>=8.3",
76 | "pytest-cov>=5.0",
77 | "ruff>=0.6.3",
78 | "pyright>=1.1.378",
79 | "mypy>=1.11.1",
80 | "sourcery>=1.22",
81 | "pre-commit>=3.8",
82 | "Sphinx>=8.0",
83 | "sphinx-argparse-cli>=1.17",
84 | "sphinx-autodoc-typehints>=2.2",
85 | "sphinxcontrib-mermaid>=1.0.0",
86 | "myst-parser>=4.0",
87 | "furo>=2024.8",
88 | ]
89 | native-tls = true
90 |
91 |
92 | [project.scripts]
93 | publish-interface-docs = "mlfmu.cli.publish_docs:main"
94 | mlfmu = "mlfmu.cli.mlfmu:main"
95 |
96 |
97 | [tool.mypy]
98 | plugins = [
99 | "numpy.typing.mypy_plugin",
100 | ]
101 | mypy_path = "stubs"
102 | files = [
103 | "src",
104 | "tests",
105 | ]
106 | exclude = [
107 | "^src/mlfmu/fmu_build/cppfmu/",
108 | ]
109 | check_untyped_defs = true
110 | disable_error_code = [
111 | "misc",
112 | "import-untyped",
113 | ]
114 |
115 |
116 | [tool.pyright]
117 | stubPath = "stubs"
118 | include = [
119 | "src",
120 | "tests",
121 | "examples",
122 | ]
123 | exclude = [
124 | "src/mlfmu/fmu_build/cppfmu",
125 | ]
126 |
127 | typeCheckingMode = "basic"
128 | useLibraryCodeForTypes = true
129 | reportMissingParameterType = "error"
130 | reportUnknownParameterType = "warning"
131 | reportUnknownMemberType = "warning" # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning
132 | reportMissingTypeArgument = "error"
133 | reportPropertyTypeMismatch = "error"
134 | reportFunctionMemberAccess = "warning"
135 | reportPrivateUsage = "warning"
136 | reportTypeCommentUsage = "warning"
137 | reportIncompatibleMethodOverride = "warning"
138 | reportIncompatibleVariableOverride = "error"
139 | reportInconsistentConstructor = "error"
140 | reportOverlappingOverload = "warning"
141 | reportUninitializedInstanceVariable = "warning"
142 | reportCallInDefaultInitializer = "warning"
143 | reportUnnecessaryIsInstance = "information"
144 | reportUnnecessaryCast = "warning"
145 | reportUnnecessaryComparison = "warning"
146 | reportUnnecessaryContains = "warning"
147 | reportUnusedCallResult = "warning"
148 | reportUnusedExpression = "warning"
149 | reportMatchNotExhaustive = "warning"
150 | reportShadowedImports = "warning"
151 | reportUntypedFunctionDecorator = "warning"
152 | reportUntypedClassDecorator = "warning"
153 | reportUntypedBaseClass = "error"
154 | reportUntypedNamedTuple = "warning"
155 | reportUnnecessaryTypeIgnoreComment = "information"
156 | # Activate the following rules only locally and temporary, i.e. for a QA session.
157 | # (For server side CI they are considered too strict.)
158 | # reportMissingTypeStubs = true
159 | # reportConstantRedefinition = "warning"
160 | # reportImportCycles = "warning"
161 | # reportImplicitStringConcatenation = "warning"
162 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths =
3 | tests
4 | addopts = --strict-markers --verbose
5 | xfail_strict = True
6 |
--------------------------------------------------------------------------------
/qa.bat:
--------------------------------------------------------------------------------
1 | uv run ruff format
2 | uv run ruff check
3 | uv run pyright
4 | uv run mypy
5 | uv run sourcery review .
6 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | exclude = [
2 | ".git",
3 | ".venv",
4 | "dist",
5 | "*cache",
6 | "**/__pycache__",
7 | "src/mlfmu/fmu_build/cppfmu",
8 | ]
9 | src = [
10 | "src",
11 | ]
12 | line-length = 120
13 | target-version = "py310"
14 |
15 | [lint]
16 | # Settings for Ruff linter (invoked with `uv run ruff check`).
17 | # Start by including _all_ Ruff lint rules, then ignore selected rules as needed
18 | # https://docs.astral.sh/ruff/rules/
19 | select = [
20 | "ALL",
21 | ]
22 | ignore = [
23 | # Ruff lint rules temporarily ignored, but which should be reactivated and resolved in the future.
24 | "D100", # Missing docstring in public module
25 | "D101", # Missing docstring in public class
26 | "D102", # Missing docstring in public method
27 | "D103", # Missing docstring in public function
28 | "D104", # Missing docstring in public package
29 | "D105", # Missing docstring in magic method
30 | "D107", # Missing docstring in __init__
31 | # Ruff lint rules considered as too strict and hence ignored
32 | "ANN101", # Missing type annotation for `self` argument in instance methods (NOTE: also listed as deprecated by Ruff)
33 | "ANN102", # Missing type annotation for `cls` argument in class methods (NOTE: also listed as deprecated by Ruff)
34 | "FIX002", # Line contains TODO, consider resolving the issue
35 | "TD003", # Missing issue link on the line following a TODO
36 | "S101", # Use of assert detected
37 | "RET504", # Unnecessary assignment to `result` before `return` statement
38 | "EM101", # Exception must not use a string literal, assign to variable first
39 | "EM102", # Exception must not use an f-string literal, assign to variable first
40 | "TRY003", # Avoid specifying long messages outside the exception class
41 | "PLR1711", # Useless `return` statement at end of function
42 | "G00", # Logging statement uses string formatting ('G00' covers all rules flagging string formatting in logging, e.g. G001, G002, etc.)
43 |
44 | # Ruff lint rules recommended to keep enabled,
45 | # but which are typical candidates you might have a need to ignore,
46 | # especially in the beginning or when refactoring an existing codebase,
47 | # to avoid too many Ruff errors at once.
48 | # -> Listed here for easy access and reference.
49 | # (uncomment to ignore)
50 | # "N803", # Argument name should be lowercase (NOTE: ignore to allow capital arguments (e.g X) in scientific code)
51 | # "N806", # Variable in function should be lowercase (NOTE: ignore to allow capital variables (e.g X) in scientific code)
52 | # "TCH002", # Move third-party import into a type-checking block
53 | # "TCH003", # Move standard library import into a type-checking block
54 |
55 | # Ruff lint rules known to be in conflict with Ruff formatter.
56 | # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
57 | "W191", # Tab-indentation (in conflict with Ruff formatter)
58 | "E111", # Indentation with invalid multiple (in conflict with Ruff formatter)
59 | "E114", # Indentation with invalid multiple comment (in conflict with Ruff formatter)
60 | "E117", # Over-indented (in conflict with Ruff formatter)
61 | "D206", # Indent with spaces (in conflict with Ruff formatter)
62 | "D300", # Triple single quotes (in conflict with Ruff formatter)
63 | "Q000", # Bad quotes in inline string (in conflict with Ruff formatter)
64 | "Q001", # Bad quotes in multi-line string (in conflict with Ruff formatter)
65 | "Q002", # Bad quotes in DocString (in conflict with Ruff formatter)
66 | "Q003", # Avoidable escaped quote (in conflict with Ruff formatter)
67 | "COM812", # Missing trailing comma (in conflict with Ruff formatter)
68 | "COM819", # Prohibited trailing comma (in conflict with Ruff formatter)
69 | "ISC001", # Single-line implicit string concatenation (in conflict with Ruff formatter)
70 | "ISC002", # Multi-line implicit string concatenation (in conflict with Ruff formatter)
71 | ]
72 | # File patterns to be excluded from Ruff lint
73 | # (only needed for file patterns not already listed in the common `exclude` setting
74 | # at top of this file, i.e. list here _additional_ excludes specific to Ruff lint.)
75 | exclude = [
76 | ]
77 | allowed-confusables = [
78 | "×", # used as dim multiplication sign in comments, such as `19×16×15×16×8×6×3 = 10,506,240 possible combinations of parameters`.
79 | ]
80 |
81 | [lint.pep8-naming]
82 | ignore-names = [
83 | "test_*",
84 | "setUp",
85 | "tearDown",
86 | ]
87 |
88 | [lint.pylint]
89 | max-args = 7
90 |
91 | [lint.flake8-pytest-style]
92 | raises-require-match-for = [
93 | "BaseException",
94 | "Exception",
95 | "OSError",
96 | "IOError",
97 | "EnvironmentError",
98 | "socket.error",
99 | ]
100 |
101 | [lint.per-file-ignores]
102 | # `__init__.py` specific ignores
103 | "__init__.py" = [
104 | "F401", # {name} imported but unused (NOTE: ignored as imports in `__init__.py` files are almost never used inside the module, but are intended for namespaces)
105 | "I001", # Import block is un-sorted or un-formatted
106 | ]
107 | # `tests` specific ignores
108 | "tests/**/*" = [
109 | "D", # Missing docstrings
110 | "ERA001", # Found commented-out code
111 | "PT006", # Wrong type passed to first argument of `@pytest.mark.parametrize` (NOTE: ignored to allow parameters args as "args_1,arg_2,arg_3,..."
112 | "S101", # Use of assert detected
113 | "PLR2004", # Magic value used in comparison
114 | "ANN201", # Missing return type annotation for public function
115 | "ANN202", # Missing return type annotation for private function
116 | "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. (NOTE: tests are not intended to be a module, __init__.py hence not required.)
117 | "SLF001", # Private member accessed
118 | "TRY004", # Prefer `TypeError` exception for invalid type
119 | ]
120 | # `stubs` specific ignores
121 | "stubs/**/*" = [
122 | "D", # Missing docstrings
123 | "ERA001", # Found commented-out code
124 | "SLF001", # Private member accessed
125 | "F405", # {name} may be undefined, or defined from star imports: {module}
126 | "F403", # from {name} import * used; unable to detect undefined names
127 | "ANN", # Missing type annotation
128 | "N", # Naming violations
129 | "A001", # Variable {name} is shadowing a Python builtin
130 | "A002", # Argument {name} is shadowing a Python builtin
131 | "FBT001", # Boolean-typed positional argument in function definition
132 | "PYI042", # Type alias {name} should be CamelCase
133 | "PYI002", # complex if statement in stub
134 | "PLR0913", # Too many arguments in function definition
135 | ]
136 | # Jupyter notebook specific ignores
137 | "**/*.ipynb" = [
138 | "D103", # Missing docstring in public function
139 | "T201", # `print` found
140 | "PGH003", # Use specific rule codes when ignoring type issues
141 | "TCH002", # Move third-party import into a type-checking block
142 | ]
143 | # `examples` specific ignores
144 | "examples/**/*" = [
145 | "D", # Missing docstrings
146 | "S101", # Use of assert detected
147 | "PLR2004", # Magic value used in comparison
148 | "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. (NOTE: tutorials are not intended to be a module, __init__.py hence not required.)
149 | "T201", # `print` found
150 | "E402", # Module level import not at top of cell
151 | ]
152 |
153 | [lint.pydocstyle]
154 | convention = "numpy"
155 |
156 | [format]
157 | docstring-code-format = true
158 |
--------------------------------------------------------------------------------
/src/mlfmu/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | MLFMU_PACKAGE_PATH = Path().absolute().parent.parent
4 |
--------------------------------------------------------------------------------
/src/mlfmu/cli/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/cli/__init__.py
--------------------------------------------------------------------------------
/src/mlfmu/cli/mlfmu.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | import logging
5 | import textwrap
6 | from pathlib import Path
7 |
8 | from mlfmu.api import MlFmuCommand, run
9 | from mlfmu.utils.logging import configure_logging
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def _argparser() -> argparse.ArgumentParser:
15 | """
16 | Create and return an ArgumentParser object for parsing command line arguments.
17 |
18 | Returns
19 | -------
20 | argparse.ArgumentParser: The ArgumentParser object.
21 | """
22 | parser = argparse.ArgumentParser(
23 | prog="mlfmu",
24 | formatter_class=argparse.RawDescriptionHelpFormatter,
25 | description=textwrap.dedent("""\
26 | mlfmu is a command line tool to build FMUs from ONNX ML models.
27 | Check the README and docs for more info.
28 | You can also run `mlfmu --help` for more info on a specific command."""),
29 | epilog=textwrap.dedent("""\
30 | This tool utilizes cppfmu, source code is available at: https://github.com/viproma/cppfmu
31 | _________________mlfmu___________________
32 | """),
33 | prefix_chars="-",
34 | add_help=True,
35 | )
36 |
37 | common_args_parser = argparse.ArgumentParser(add_help=False)
38 |
39 | console_verbosity = common_args_parser.add_mutually_exclusive_group(required=False)
40 |
41 | _ = console_verbosity.add_argument(
42 | "-q",
43 | "--quiet",
44 | action="store_true",
45 | help=("console output will be quiet."),
46 | default=False,
47 | )
48 |
49 | _ = console_verbosity.add_argument(
50 | "-v",
51 | "--verbose",
52 | action="store_true",
53 | help=("console output will be verbose."),
54 | default=False,
55 | )
56 |
57 | _ = common_args_parser.add_argument(
58 | "-l",
59 | "--log",
60 | action="store",
61 | type=str,
62 | help=(
63 | "name of log file. If specified, this will activate logging to file. "
64 | "If not, it does not log to file, only console."
65 | ),
66 | default=None,
67 | required=False,
68 | )
69 |
70 | _ = common_args_parser.add_argument(
71 | "-ll",
72 | "--log-level",
73 | action="store",
74 | type=str,
75 | help="log level applied to logging to file. Default: WARNING.",
76 | choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
77 | default="WARNING",
78 | required=False,
79 | )
80 |
81 | # Create a sub parser for each command
82 | sub_parsers = parser.add_subparsers(
83 | dest="command",
84 | title="Available commands",
85 | metavar="command",
86 | required=True,
87 | )
88 |
89 | # Main command
90 | # build command to go from config to compiled fmu
91 | build_parser = sub_parsers.add_parser(
92 | MlFmuCommand.BUILD.value,
93 | help="Build FMU from interface and model files",
94 | parents=[common_args_parser],
95 | add_help=True,
96 | )
97 |
98 | # Add options for build command
99 | _ = build_parser.add_argument(
100 | "-i",
101 | "--interface-file",
102 | type=str,
103 | help="JSON file describing the FMU following schema",
104 | )
105 | _ = build_parser.add_argument(
106 | "-m",
107 | "--model-file",
108 | type=str,
109 | help="ONNX file containing the ML Model",
110 | )
111 | _ = build_parser.add_argument(
112 | "-f",
113 | "--fmu-path",
114 | type=str,
115 | help="Path to where the built FMU should be saved",
116 | )
117 |
118 | # Split the main build command into steps for customization
119 | # generate-code command to go from config to generated fmu source code
120 | code_generation_parser = sub_parsers.add_parser(
121 | MlFmuCommand.GENERATE.value,
122 | help="Generate FMU source code from interface and model files",
123 | parents=[common_args_parser],
124 | add_help=True,
125 | )
126 |
127 | # Add options for code generation command
128 | _ = code_generation_parser.add_argument(
129 | "--interface-file",
130 | type=str,
131 | help="json file describing the FMU following schema (e.g. interface.json).",
132 | )
133 | _ = code_generation_parser.add_argument(
134 | "--model-file",
135 | type=str,
136 | help="onnx file containing the ML Model (e.g. example.onnx).",
137 | )
138 | _ = code_generation_parser.add_argument(
139 | "--fmu-source-path",
140 | help=(
141 | "Path to where the generated FMU source code should be saved. "
142 | "Given path/to/folder the files can be found in path/to/folder/[FmuName]"
143 | ),
144 | )
145 |
146 | # build-code command to go from fmu source code to compiled fmu
147 | build_code_parser = sub_parsers.add_parser(
148 | MlFmuCommand.COMPILE.value,
149 | help="Build FMU from FMU source code",
150 | parents=[common_args_parser],
151 | add_help=True,
152 | )
153 |
154 | # Add option for fmu compilation
155 | _ = build_code_parser.add_argument(
156 | "--fmu-source-path",
157 | type=str,
158 | help=(
159 | "Path to the folder where the FMU source code is located. "
160 | "The folder needs to have the same name as the FMU. E.g. path/to/folder/[FmuName]"
161 | ),
162 | )
163 | _ = build_code_parser.add_argument(
164 | "--fmu-path",
165 | type=str,
166 | help="Path to where the built FMU should be saved.",
167 | )
168 |
169 | return parser
170 |
171 |
172 | def main() -> None:
173 | """
174 | Entry point for console script as configured in setup.cfg.
175 |
176 | Runs the command line interface and parses arguments and options entered on the console.
177 | """
178 | parser = _argparser()
179 | args = parser.parse_args()
180 |
181 | # Configure Logging
182 | # ..to console
183 | log_level_console: str = "WARNING"
184 | if any([args.quiet, args.verbose]):
185 | log_level_console = "ERROR" if args.quiet else log_level_console
186 | log_level_console = "INFO" if args.verbose else log_level_console
187 | # ..to file
188 | log_file: Path | None = Path(args.log) if args.log else None
189 | log_level_file: str = args.log_level
190 | configure_logging(log_level_console, log_file, log_level_file)
191 | logger.info("Logging to file: %s", log_file)
192 |
193 | command: MlFmuCommand | None = MlFmuCommand.from_string(args.command)
194 |
195 | if command is None:
196 | raise ValueError(
197 | f"The given command (={args.command}) does not match any of the existing commands "
198 | f"(={[command.value for command in MlFmuCommand]})."
199 | )
200 |
201 | interface_file = args.interface_file if "interface_file" in args else None
202 | model_file = args.model_file if "model_file" in args else None
203 | fmu_path = args.fmu_path if "fmu_path" in args else None
204 | source_folder = args.fmu_source_path if "fmu_source_path" in args else None
205 |
206 | # Invoke API
207 | try:
208 | run(
209 | command=command,
210 | interface_file=interface_file,
211 | model_file=model_file,
212 | fmu_path=fmu_path,
213 | source_folder=source_folder,
214 | )
215 | except Exception:
216 | logger.exception("Unhandled exception in run: %s")
217 |
218 |
219 | if __name__ == "__main__":
220 | try:
221 | main()
222 | except Exception:
223 | logger.exception("Unhandled exception in main: %s")
224 |
--------------------------------------------------------------------------------
/src/mlfmu/cli/publish_docs.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from mlfmu.utils.interface import publish_interface_schema
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 |
8 | def main() -> None:
9 | """Start the publishing process for the mlfmu interface docs, by calling the publish_interface_schema function."""
10 | logger.info("Start publish-interface-docs.py")
11 | publish_interface_schema()
12 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.24)
2 | set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
3 |
4 | project(cpp-fmus)
5 |
6 | option(FMU_USE_STATIC_RUNTIME
7 | "Whether to link the model code against a static C(++) runtime library"
8 | ON)
9 | option(FMU_CHECK_COMPLIANCE
10 | "Whether to test each FMU with the FMU Compliance checker"
11 | OFF)
12 | set(FMU_OUTPUT_DIR "${CMAKE_BINARY_DIR}"
13 | CACHE PATH "Where to put the generated FMUs")
14 | set(FMU_STAGING_DIR "${CMAKE_BINARY_DIR}/fmu-staging"
15 | CACHE PATH "Where to put the generated FMU contents before zipping")
16 | set(FMU_UUID_DIR "${CMAKE_BINARY_DIR}/fmu-uuids"
17 | CACHE PATH "Where to put the headers containing FMU UUIDs")
18 | set(FMU_SOURCE_PATH "${CMAKE_SOURCE_DIR}/src"
19 | CACHE PATH "Where the folder with the FMU source is located")
20 |
21 | # The names of the FMUs built in this project
22 |
23 | # Set variable -DFMU_NAMES from command line instead
24 | set(FMU_NAMES)
25 |
26 | # ==============================================================================
27 | # Compiler/platform specific settings
28 | # ==============================================================================
29 |
30 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
31 | add_compile_options("-Wall" "-Wextra" "-Wpedantic")
32 | add_compile_options("-Wno-parentheses")
33 | if(FMU_USE_STATIC_RUNTIME)
34 | add_compile_options("-static")
35 | endif()
36 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
37 | add_compile_options("/W4")
38 | add_compile_options("/wd4996")
39 | add_definitions("-D_SCL_SECURE_NO_WARNINGS" "-D_CRT_SECURE_NO_WARNINGS")
40 | if(FMU_USE_STATIC_RUNTIME)
41 | foreach(lang "C" "CXX")
42 | string(REPLACE "/MD" "/MT" "CMAKE_${lang}_FLAGS" "${${CMAKE_${lang}_FLAGS}}")
43 | foreach(config ${CMAKE_CONFIGURATION_TYPES})
44 | string(TOUPPER "${config}" configUpper)
45 | set(flagVar "CMAKE_${lang}_FLAGS_${configUpper}")
46 | string(REPLACE "/MD" "/MT" ${flagVar} ${${flagVar}})
47 | endforeach()
48 | endforeach()
49 | endif()
50 | endif()
51 |
52 | if(UNIX)
53 | # Remove the "lib" prefix on the generated .so files
54 | set(CMAKE_SHARED_MODULE_PREFIX)
55 | endif()
56 |
57 | # ==============================================================================
58 | # Build
59 | # ==============================================================================
60 |
61 | # List CPPFMU sources
62 | file(GLOB cppfmuSourceFiles "${CMAKE_SOURCE_DIR}/cppfmu/*.?pp")
63 | file(GLOB onnxfmuSourceFiles "${CMAKE_SOURCE_DIR}/templates/onnx_fmu/*.?pp")
64 | # Find FMU Compliance Checker
65 | if(FMU_CHECK_COMPLIANCE)
66 | enable_testing()
67 | find_package(FMUComplianceChecker REQUIRED)
68 | endif()
69 |
70 | # Detect platform
71 | if("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
72 | set(os "darwin")
73 | elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
74 | set(os "linux")
75 | elseif(WIN32)
76 | set(os "win")
77 | else()
78 | message(FATAL_ERROR "Unknown or unsupported platform: ${CMAKE_SYSTEM_NAME}")
79 | endif()
80 | math(EXPR wordSize 8*${CMAKE_SIZEOF_VOID_P})
81 | set(platformIdentifier "${os}${wordSize}")
82 |
83 | # Prepare names for FMU staging directories.
84 | if(CMAKE_CONFIGURATION_TYPES)
85 | set(configStagingDir "${FMU_STAGING_DIR}/$")
86 | else()
87 | set(configStagingDir "${FMU_STAGING_DIR}")
88 | endif()
89 |
90 | # Create targets
91 | set(includeDirs
92 | )
93 |
94 | find_package (onnxruntime REQUIRED)
95 |
96 | foreach(fmuName IN LISTS FMU_NAMES)
97 | set(fmuLibraryTarget "${fmuName}")
98 | set(fmuGuidTarget "${fmuName}_guid")
99 | set(fmuTarget "${fmuName}_fmu")
100 |
101 | set(fmuSourceDir "${FMU_SOURCE_PATH}/${fmuName}")
102 | set(fmuSourceModelDescription "${fmuSourceDir}/modelDescription.xml")
103 | set(fmuSourceFiles "${fmuSourceDir}/sources/fmu.cpp")
104 |
105 | set(fmuGuidDir "${FMU_UUID_DIR}/${fmuName}")
106 | set(fmuGuidModelDescription "${fmuGuidDir}/modelDescription.xml")
107 | set(fmuGuidHeaderFile "${fmuGuidDir}/fmu-uuid.h")
108 |
109 | set(fmuStagingDir "${configStagingDir}/${fmuName}")
110 | set(fmuStagedBinariesDir "${fmuStagingDir}/binaries/${platformIdentifier}")
111 |
112 | set(fmuOutputFile "${FMU_OUTPUT_DIR}/${fmuName}.fmu")
113 |
114 | # Target to generate GUID
115 | add_custom_command(
116 | OUTPUT "${fmuGuidModelDescription}" "${fmuGuidHeaderFile}"
117 | COMMAND
118 | "${CMAKE_COMMAND}" "-E" "make_directory" "${fmuGuidDir}"
119 | COMMAND
120 | "${CMAKE_COMMAND}"
121 | "-DINPUT_MODEL_DESCRIPTION=${fmuSourceModelDescription}"
122 | "-DINPUT_HEADER=${CMAKE_SOURCE_DIR}/cmake/fmu-uuid.h.in"
123 | "-DOUTPUT_MODEL_DESCRIPTION=${fmuGuidModelDescription}"
124 | "-DOUTPUT_HEADER=${fmuGuidHeaderFile}"
125 | "-DADDITIONAL_INPUT=${fmuSourceFiles}"
126 | "-P" "${CMAKE_SOURCE_DIR}/cmake/GenerateFmuGuid.cmake"
127 | DEPENDS "${fmuSourceModelDescription}"
128 | VERBATIM
129 | )
130 | add_custom_target(${fmuGuidTarget}
131 | DEPENDS "${fmuGuidModelDescription}" "${fmuGuidHeaderFile}"
132 | )
133 |
134 | # Target to build dynamic library
135 | add_library(${fmuLibraryTarget} MODULE
136 | "${fmuSourceModelDescription}"
137 | ${fmuSourceFiles}
138 | ${cppfmuSourceFiles}
139 | ${onnxfmuSourceFiles}
140 | )
141 | add_dependencies(${fmuLibraryTarget} ${fmuGuidTarget})
142 | target_link_libraries(${fmuLibraryTarget} onnxruntime::onnxruntime)
143 | target_include_directories(${fmuLibraryTarget} PRIVATE
144 | "${CMAKE_SOURCE_DIR}/fmi"
145 | "${CMAKE_SOURCE_DIR}/cppfmu"
146 | "${CMAKE_SOURCE_DIR}/templates/onnx_fmu"
147 | "${fmuSourceDir}/sources"
148 | "${fmuGuidDir}"
149 | )
150 |
151 | # Target to generate FMU contents
152 | add_custom_command(OUTPUT "${fmuOutputFile}"
153 | COMMAND "${CMAKE_COMMAND}" "-E" "copy_directory" "${fmuSourceDir}" "${fmuStagingDir}"
154 | COMMAND "${CMAKE_COMMAND}" "-E" "make_directory" "${fmuStagedBinariesDir}"
155 | COMMAND "${CMAKE_COMMAND}" "-E" "copy" "$" "${fmuStagedBinariesDir}/"
156 | COMMAND "${CMAKE_COMMAND}" "-E" "copy" "${fmuGuidModelDescription}" "${fmuStagingDir}/modelDescription.xml"
157 | COMMAND "${CMAKE_COMMAND}" "-E" "copy" "${fmuGuidHeaderFile}" "${fmuStagingDir}/sources/"
158 | COMMAND "${CMAKE_COMMAND}" "-DBASE_DIR=${fmuStagingDir}" "-DOUTPUT_FILE=${fmuOutputFile}" "-P" "${CMAKE_SOURCE_DIR}/cmake/ZipAll.cmake"
159 | DEPENDS ${fmuLibraryTarget} ${fmuGuidTarget}
160 | VERBATIM)
161 | add_custom_target(${fmuTarget} ALL DEPENDS "${fmuOutputFile}")
162 |
163 | # Test compliance
164 | if(FMU_CHECK_COMPLIANCE)
165 | add_fmu_compliance_check("${fmuTarget}_compliance" "${fmuOutputFile}")
166 | endif()
167 | endforeach()
168 |
169 | source_group("Metadata" REGULAR_EXPRESSION "modelDescription.xml")
170 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/cmake/FindFMUComplianceChecker.cmake:
--------------------------------------------------------------------------------
1 | # Finds FMUComplianceChecker
2 | # (https://github.com/modelica-tools/FMUComplianceChecker)
3 | #
4 | # The script sets the following variables:
5 | #
6 | # FMUComplianceChecker_FOUND - TRUE on success, FALSE on failure
7 | # FMUComplianceChecker_EXECUTABLE - The fmuCheck executable, if found.
8 | #
9 | # It also defines the following macro, which adds a new test (using add_test)
10 | # that runs the FMU Compliance Checker on an FMU:
11 | #
12 | # add_fmu_compliance_check(test_name fmu_path)
13 | #
14 |
15 | # Detect platform
16 | if("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
17 | set(_os "darwin")
18 | elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
19 | set(_os "linux")
20 | elseif(WIN32)
21 | set(_os "win")
22 | else()
23 | message(FATAL_ERROR "Unknown or unsupported platform: ${CMAKE_SYSTEM_NAME}")
24 | endif()
25 | math(EXPR _wordSize 8*${CMAKE_SIZEOF_VOID_P})
26 |
27 | find_program(
28 | FMUComplianceChecker_EXECUTABLE
29 | "fmuCheck.${_os}${_wordSize}")
30 |
31 | include(FindPackageHandleStandardArgs)
32 | find_package_handle_standard_args(
33 | FMUComplianceChecker
34 | DEFAULT_MSG
35 | FMUComplianceChecker_EXECUTABLE)
36 |
37 | macro(add_fmu_compliance_check testName fmuPath)
38 | add_test(
39 | NAME "${testName}"
40 | COMMAND "${FMUComplianceChecker_EXECUTABLE}" "${fmuPath}"
41 | WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
42 | endmacro()
43 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/cmake/GenerateFmuGuid.cmake:
--------------------------------------------------------------------------------
1 | # Generates a UUID to be used as the "guid" attribute of an FMU, and writes it
2 | # to the modelDescription.xml file and to a header that can be imported in the
3 | # FMU code.
4 | #
5 | # The script takes a template modelDescription.xml and header file as input,
6 | # and substitutes a placeholder in each of them with the generated UUID before
7 | # writing the output files. The placeholder should be on the form "@FMU_UUID@"
8 | # (the name is configurable, but the @ signs are not).
9 | #
10 | # The UUID will be generated as a hash of the input model description and,
11 | # optionally, other input files (e.g. model source code files).
12 | #
13 | # Mandatory configuration variables:
14 | #
15 | # INPUT_MODEL_DESCRIPTION
16 | # The model description XML file containing a placeholder in the "guid"
17 | # attribute.
18 | #
19 | # INPUT_HEADER
20 | # A header file containing a placeholder for the UUID (typically in a
21 | # #define directive or constant declaration).
22 | #
23 | # OUTPUT_MODEL_DESCRIPTION
24 | # The path to the output model description XML file.
25 | #
26 | # OUTPUT_HEADER
27 | # The path to the output header file.
28 | #
29 | # Optional configuration variables:
30 | #
31 | # ADDITIONAL_INPUT
32 | # Other input files that should be hashed into the UUID.
33 | #
34 | # PLACEHOLDER_NAME
35 | # The name of the placeholder in the input model description and header.
36 | # The default value is "FMU_UUID".
37 | #
38 | cmake_minimum_required(VERSION 3.1)
39 |
40 | if((NOT INPUT_MODEL_DESCRIPTION) OR (NOT INPUT_HEADER) OR (NOT OUTPUT_MODEL_DESCRIPTION) OR (NOT OUTPUT_HEADER))
41 | message(FATAL_ERROR "One or more mandatory configuration variables are not set")
42 | endif()
43 | if(NOT PLACEHOLDER_NAME)
44 | set(PLACEHOLDER_NAME "FMU_UUID")
45 | endif()
46 |
47 | macro(append_file_contents_without_newlines filePath outputVar)
48 | file(READ "${filePath}" contents)
49 | string(REGEX REPLACE "[\r\n]" "" contentsWithoutNewlines "${contents}")
50 | string(APPEND ${outputVar} "${contentsWithoutNewlines}")
51 | endmacro()
52 |
53 | # Read input model description file and any additional input files
54 | set(text)
55 | append_file_contents_without_newlines("${INPUT_MODEL_DESCRIPTION}" text)
56 | foreach(f IN LISTS ADDITIONAL_INPUT)
57 | append_file_contents_without_newlines("${f}" text)
58 | endforeach()
59 |
60 | # Generate UUID
61 | set(uuidNamespace "23d0734c-3983-4f65-a8f7-3bd5c5693546")
62 | string(UUID ${PLACEHOLDER_NAME} NAMESPACE "${uuidNamespace}" NAME "${text}" TYPE "SHA1")
63 |
64 | # Write output files
65 | configure_file("${INPUT_MODEL_DESCRIPTION}" "${OUTPUT_MODEL_DESCRIPTION}" @ONLY)
66 | configure_file("${INPUT_HEADER}" "${OUTPUT_HEADER}" @ONLY)
67 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/cmake/ZipAll.cmake:
--------------------------------------------------------------------------------
1 | # Changes to BASE_DIR and zips all files found there into OUTPUT_FILE.
2 | if(NOT IS_ABSOLUTE "${BASE_DIR}")
3 | message(FATAL_ERROR "BASE_DIR does not contain an absolute path")
4 | endif()
5 | if(NOT IS_DIRECTORY "${BASE_DIR}")
6 | message(FATAL_ERROR "BASE_DIR does not refer to a directory")
7 | endif()
8 | if(NOT IS_ABSOLUTE "${OUTPUT_FILE}")
9 | message(FATAL_ERROR "OUTPUT_FILE does not contain an absolute path")
10 | endif()
11 |
12 | file(GLOB inputFiles RELATIVE "${BASE_DIR}" "${BASE_DIR}/*")
13 | execute_process(
14 | COMMAND "${CMAKE_COMMAND}" "-E" "tar" "cf" "${OUTPUT_FILE}" "--format=zip" ${inputFiles}
15 | WORKING_DIRECTORY "${BASE_DIR}"
16 | RESULT_VARIABLE exitCode
17 | )
18 | if(NOT exitCode EQUAL 0)
19 | message(FATAL_ERROR "Zip command failed")
20 | endif()
21 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/cmake/fmu-uuid.h.in:
--------------------------------------------------------------------------------
1 | #define FMU_UUID "@FMU_UUID@"
2 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/conanfile.txt:
--------------------------------------------------------------------------------
1 | [requires]
2 | onnxruntime/1.18.1
3 |
4 | [options]
5 | *:shared=False
6 | onnx*:disable_static_registration=True
7 |
8 | [generators]
9 | CMakeDeps
10 | CMakeToolchain
11 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/fmi/fmi2FunctionTypes.h:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/fmu_build/fmi/fmi2FunctionTypes.h
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/fmi/fmi2Functions.h:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/fmu_build/fmi/fmi2Functions.h
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/fmi/fmi2TypesPlatform.h:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/fmu_build/fmi/fmi2TypesPlatform.h
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/fmi/fmiFunctions.h:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/fmu_build/fmi/fmiFunctions.h
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/fmi/fmiPlatformTypes.h:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/fmu_build/fmi/fmiPlatformTypes.h
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/templates/fmu/fmu_template.cpp:
--------------------------------------------------------------------------------
1 | #include "fmu-uuid.h"
2 | #include "model_definitions.h"
3 |
4 | #include
5 | #include
6 |
7 |
8 | /**
9 | * \class {FmuName}
10 | * \brief A class representing an {FmuName} FMU.
11 | *
12 | * This class is derived from the OnnxFmu class and provides functionality specific to the {FmuName} FMU.
13 | */
14 | class {FmuName} : public OnnxFmu {{
15 | public :
16 | /**
17 | * \brief Constructs a new {FmuName} object.
18 | *
19 | * \param fmuResourceLocation The location of the resources of the FMU.
20 | */
21 | {FmuName}(cppfmu::FMIString fmuResourceLocation) : OnnxFmu(fmuResourceLocation) {{}}
22 |
23 | private :
24 | // Add private members and functions here
25 | }};
26 |
27 | /**
28 | * \brief Instantiate a `slave` instance of the FMU.
29 | *
30 | * \param instanceName The name of the FMU instance.
31 | * \param fmuGUID The GUID of the FMU.
32 | * \param fmuResourceLocation The location of the FMU resource.
33 | * \param mimeType The MIME type of the FMU.
34 | * \param timeout The timeout value for the instantiation process.
35 | * \param visible Flag indicating whether the FMU should be visible.
36 | * \param interactive Flag indicating whether the FMU should be interactive.
37 | * \param memory The memory to be used for the FMU instance.
38 | * \param logger The logger to be used for logging messages.
39 | * \returns A unique pointer to the instantiated slave instance.
40 | *
41 | * \throws std::runtime_error if the FMU GUID does not match.
42 | */
43 | cppfmu::UniquePtr CppfmuInstantiateSlave(
44 | cppfmu::FMIString /*instanceName*/, cppfmu::FMIString fmuGUID, cppfmu::FMIString fmuResourceLocation,
45 | cppfmu::FMIString /*mimeType*/, cppfmu::FMIReal /*timeout*/, cppfmu::FMIBoolean /*visible*/,
46 | cppfmu::FMIBoolean /*interactive*/, cppfmu::Memory memory, cppfmu::Logger /*logger*/)
47 | {{
48 | if (std::strcmp(fmuGUID, FMU_UUID) != 0) {{
49 | throw std::runtime_error("FMU GUID mismatch");
50 | }}
51 | return cppfmu::AllocateUnique<{FmuName}>(memory, fmuResourceLocation);
52 | }}
53 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/templates/fmu/model_definitions_template.h:
--------------------------------------------------------------------------------
1 | #define NUM_FMU_VARIABLES {numFmuVariables}
2 |
3 | #define NUM_ONNX_INPUTS {numOnnxInputs}
4 | #define NUM_ONNX_FMU_INPUTS {numOnnxFmuInputs}
5 | #define NUM_ONNX_OUTPUTS {numOnnxOutputs}
6 | #define NUM_ONNX_FMU_OUTPUTS {numOnnxFmuOutputs}
7 | #define NUM_ONNX_STATES {numOnnxStates}
8 | #define NUM_ONNX_STATES_OUTPUTS {numOnnxStatesOutputs}
9 | #define NUM_ONNX_STATE_INIT {numOnnxStateInit}
10 |
11 | #define ONNX_INPUT_VALUE_REFERENCES {onnxInputValueReferences}
12 | #define ONNX_OUTPUT_VALUE_REFERENCES {onnxOutputValueReferences}
13 | #define ONNX_STATE_OUTPUT_INDEXES {onnxStateOutputIndexes}
14 | #define ONNX_STATE_INIT_VALUE_REFERENCES {onnxStateInitValueReferences}
15 |
16 | #define ONNX_USE_TIME_INPUT {onnxUsesTime}
17 |
18 | #define ONNX_INPUT_NAME "{onnxInputName}"
19 | #define ONNX_OUTPUT_NAME "{onnxOutputName}"
20 | #define ONNX_STATE_NAME "{onnxStatesName}"
21 | #define ONNX_TIME_INPUT_NAME "{onnxTimeInputName}"
22 | #define ONNX_FILENAME L"{onnxFileName}"
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp:
--------------------------------------------------------------------------------
1 | #include "onnxFmu.hpp"
2 |
3 | #include
4 |
5 | /**
6 | * \brief Constructs an instance of the OnnxFmu class.
7 | *
8 | * Constructs an instance of the OnnxFmu class.
9 | *
10 | * \param fmuResourceLocation The location of the FMU resource.
11 | */
12 | OnnxFmu::OnnxFmu(cppfmu::FMIString fmuResourceLocation)
13 | {
14 | formatOnnxPath(fmuResourceLocation);
15 | try {
16 | CreateSession();
17 | } catch (const std::runtime_error& e) {
18 | std::cerr << e.what() << '\n';
19 | }
20 | OnnxFmu::Reset();
21 | }
22 |
23 |
24 | /**
25 | * \brief Formats the onnx path.
26 | *
27 | * Formats the onnx path by appending the ONNX_FILENAME to the given fmuResourceLocation.
28 | * If the path starts with "file:///", it removes the "file://" prefix.
29 | * This is directly stored to the class var onnxPath_.
30 | *
31 | * \param fmuResourceLocation The location of the FMU resource.
32 | */
33 | void OnnxFmu::formatOnnxPath(cppfmu::FMIString fmuResourceLocation)
34 | {
35 | // Creating complete path to onnx file
36 | std::wostringstream onnxPathStream;
37 | onnxPathStream << fmuResourceLocation;
38 | onnxPathStream << L"/";
39 | onnxPathStream << ONNX_FILENAME;
40 |
41 | // Remove file:// from the path if it is at the beginning
42 | std::wstring path = onnxPathStream.str();
43 | std::wstring startPath = path.substr(0, 8);
44 | std::wstring endPath = path.substr(8);
45 | if (startPath == L"file:///") {
46 | path = endPath;
47 | }
48 | // save to onnxPath_ (wstring for Windows, else string)
49 | #ifdef _WIN32
50 | onnxPath_ = path;
51 | #else
52 | onnxPath_ = std::string(path.begin(), path.end());
53 | #endif
54 | }
55 |
56 | /**
57 | * \brief Creates a onnx runtime session for the model.
58 | *
59 | * This function creates a session to the ONNX model, using the specified ONNX model file.
60 | * This loads the weights of the model such that we can run predictions in the doStep function.
61 | *
62 | * \note The ONNX model file path must be set before calling this function.
63 | * \throws std::runtime_error if the session creation fails.
64 | */
65 | void OnnxFmu::CreateSession()
66 | {
67 | // Create the ONNX environment
68 | session_ = Ort::Session(env, onnxPath_.c_str(), Ort::SessionOptions {nullptr});
69 | }
70 |
71 | /**
72 | * \brief Resets the state of the OnnxFmu.
73 | *
74 | * This function is called to reset the state of the OnnxFmu.
75 | */
76 | void OnnxFmu::Reset()
77 | {
78 | doStateInit_ = true;
79 | return;
80 | }
81 |
82 | /**
83 | * Sets the real values of the specified FMI value references.
84 | *
85 | * \param vr An array of FMI value references.
86 | * \param nvr The number of FMI value references in the array.
87 | * \param value An array of FMI real values to be set.
88 | */
89 | void OnnxFmu::SetReal(const cppfmu::FMIValueReference vr[], std::size_t nvr, const cppfmu::FMIReal value[])
90 | {
91 | for (std::size_t i = 0; i < nvr; ++i) {
92 | OnnxFmu::fmuVariables_[vr[i]].real = value[i];
93 | }
94 | }
95 |
96 |
97 | /**
98 | * \brief Retrieves the real values of the specified FMI value references.
99 | *
100 | * \param vr An array of FMI value references.
101 | * \param nvr The number of FMI value references in the array.
102 | * \param value An array to store the retrieved real values.
103 | */
104 | void OnnxFmu::GetReal(const cppfmu::FMIValueReference vr[], std::size_t nvr, cppfmu::FMIReal value[]) const
105 | {
106 | for (std::size_t i = 0; i < nvr; ++i) {
107 | value[i] = fmuVariables_[vr[i]].real;
108 | }
109 | }
110 |
111 |
112 | /**
113 | * \brief Sets the integer values of the specified FMI value references.
114 | *
115 | * This function sets the integer values of the FMI value references specified in the
116 | * `vr` array to the corresponding values in the `value` array.
117 | *
118 | * \param vr An array of FMI value references.
119 | * \param nvr The number of FMI value references in the `vr` array.
120 | * \param value An array of FMI integer values.
121 | */
122 | void OnnxFmu::SetInteger(const cppfmu::FMIValueReference vr[], std::size_t nvr, const cppfmu::FMIInteger value[])
123 | {
124 | for (std::size_t i = 0; i < nvr; ++i) {
125 | fmuVariables_[vr[i]].integer = value[i];
126 | }
127 | }
128 |
129 |
130 | /**
131 | * \brief Retrieves the integer values of the specified FMI value references.
132 | *
133 | * This function retrieves the integer values of the FMI value references specified in the
134 | * `vr` array and stores them in the `value` array.
135 | *
136 | * \param vr An array of FMI value references.
137 | * \param nvr The number of FMI value references in the `vr` array.
138 | * \param value An array to store the retrieved integer values.
139 | */
140 | void OnnxFmu::GetInteger(const cppfmu::FMIValueReference vr[], std::size_t nvr, cppfmu::FMIInteger value[]) const
141 | {
142 | for (std::size_t i = 0; i < nvr; ++i) {
143 | value[i] = fmuVariables_[vr[i]].integer;
144 | }
145 | }
146 |
147 | /**
148 | * \brief Sets the boolean values for the specified FMI value references.
149 | *
150 | * This function sets the boolean values for the specified FMI value references in the
151 | * OnnxFmu object.
152 | *
153 | * \param vr An array of FMI value references.
154 | * \param nvr The number of FMI value references in the array.
155 | * \param value An array of FMI boolean values.
156 | */
157 | void OnnxFmu::SetBoolean(const cppfmu::FMIValueReference vr[], std::size_t nvr, const cppfmu::FMIBoolean value[])
158 | {
159 | for (std::size_t i = 0; i < nvr; ++i) {
160 | fmuVariables_[vr[i]].boolean = value[i];
161 | }
162 | }
163 |
164 | /**
165 | * \brief Retrieves boolean values for the specified value references.
166 | *
167 | * This function retrieves boolean values for the specified value references from the
168 | * OnnxFmu object.
169 | *
170 | * \param vr An array of FMIValueReference representing the value references.
171 | * \param nvr The number of value references in the vr array.
172 | * \param value An array of FMIBoolean to store the retrieved boolean values.
173 | */
174 | void OnnxFmu::GetBoolean(const cppfmu::FMIValueReference vr[], std::size_t nvr, cppfmu::FMIBoolean value[]) const
175 | {
176 | for (std::size_t i = 0; i < nvr; ++i) {
177 | value[i] = fmuVariables_[vr[i]].boolean;
178 | }
179 | }
180 |
181 | /**
182 | * \brief Sets the ONNX inputs for the ONNX FMU, matching FMU variables with inputs of ONNX model.
183 | *
184 | * This function matches the FMU variables with the inputs to the ONNX model.
185 | * It iterates over the ONNX input value-reference index pairs and assigns the corresponding FMU
186 | * variable's real value to the ONNX input.
187 | *
188 | * \returns `true` if the ONNX inputs are successfully set, `false` otherwise.
189 | */
190 | bool OnnxFmu::SetOnnxInputs()
191 | {
192 | for (int index = 0; index < NUM_ONNX_FMU_INPUTS; index++) {
193 | int inputIndex = onnxInputValueReferenceIndexPairs_[index][0];
194 | int valueReference = onnxInputValueReferenceIndexPairs_[index][1];
195 | FMIVariable var = fmuVariables_[valueReference];
196 | // TODO: Change to handle if the variable is not a real
197 | onnxInputs_[inputIndex] = var.real;
198 | }
199 | return true;
200 | }
201 |
202 | /**
203 | * \brief Sets the ONNX states.
204 | *
205 | * This function sets the ONNX states by assigning the corresponding ONNX outputs to the
206 | * ONNX states array.
207 | *
208 | * \returns `true` if the ONNX states are successfully set, `false` otherwise.
209 | */
210 | bool OnnxFmu::SetOnnxStates()
211 | {
212 | for (int index = 0; index < NUM_ONNX_STATES_OUTPUTS; index++) {
213 | onnxStates_[index] = onnxOutputs_[onnxStateOutputIndexes_[index]];
214 | }
215 | return true;
216 | }
217 |
218 | /**
219 | * \brief Retrieves the ONNX outputs and updates the corresponding FMU variables.
220 | *
221 | * This function iterates over the ONNX output value-reference index pairs and updates
222 | * the FMU variables with the corresponding ONNX outputs.
223 | * The function assumes that the FMU variables are of type `FMIVariable` and Real valued
224 | * and the ONNX outputs are stored in the `onnxOutputs_` array.
225 | *
226 | * \returns `true` if the ONNX outputs are successfully retrieved and FMU variables are
227 | * updated, `false` otherwise.
228 | */
229 | bool OnnxFmu::GetOnnxOutputs()
230 | {
231 | for (int index = 0; index < NUM_ONNX_FMU_OUTPUTS; index++) {
232 | int outputIndex = onnxOutputValueReferenceIndexPairs_[index][0];
233 | int valueReference = onnxOutputValueReferenceIndexPairs_[index][1];
234 | FMIVariable var = fmuVariables_[valueReference];
235 | // TODO: Change to handle if the variable is not a real
236 | var.real = onnxOutputs_[outputIndex];
237 |
238 | fmuVariables_[valueReference] = var;
239 | }
240 | return true;
241 | }
242 |
243 | /**
244 | * \brief Initializes the ONNX states of the ONNX model.
245 | *
246 | * This function initializes the ONNX states of the ONNX FMU by assigning the initial values
247 | * of the ONNX states from the corresponding variables in the FMU.
248 | * The function assumes that the FMU variables are of type `FMIVariable` and Real valued.
249 | *
250 | * \returns `true` if the ONNX states are successfully initialized, `false` otherwise.
251 | */
252 | bool OnnxFmu::InitOnnxStates()
253 | {
254 | for (int index = 0; index < NUM_ONNX_STATE_INIT; index++) {
255 | int stateIndex = onnxStateInitValueReferenceIndexPairs_[index][0];
256 | int valueReference = onnxStateInitValueReferenceIndexPairs_[index][1];
257 | FMIVariable var = fmuVariables_[valueReference];
258 | // TODO: Change to handle if the variable is not a real
259 | onnxStates_[stateIndex] = var.real;
260 | }
261 | return true;
262 | }
263 |
264 | /**
265 | * \brief Runs the ONNX model for the FMU.
266 | *
267 | * This function runs the ONNX model for the FMU using the provided current communication
268 | * point and time step.
269 | * It takes the input data, states (if any), and time inputs (if enabled) and calls Run
270 | * to produce the output data.
271 | *
272 | * \param currentCommunicationPoint The current communication point of the FMU.
273 | * \param dt The time step of the FMU.
274 | * \returns `true` if the ONNX model was successfully run, `false` otherwise.
275 | */
276 | bool OnnxFmu::RunOnnxModel(cppfmu::FMIReal currentCommunicationPoint, cppfmu::FMIReal dt)
277 | {
278 | try {
279 | Ort::MemoryInfo memoryInfo = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
280 |
281 | std::vector inputs;
282 | const char* inputNames[3] = {inputName_.c_str()};
283 | int numInputs = 1;
284 | inputs.emplace_back(Ort::Value::CreateTensor(memoryInfo, onnxInputs_.data(), onnxInputs_.size(),
285 | inputShape_.data(), inputShape_.size()));
286 |
287 | if (NUM_ONNX_STATES > 0) {
288 | inputs.emplace_back(Ort::Value::CreateTensor(memoryInfo, onnxStates_.data(), onnxStates_.size(),
289 | stateShape_.data(), stateShape_.size()));
290 | inputNames[1] = stateName_.c_str();
291 | numInputs++;
292 | }
293 |
294 | if (ONNX_USE_TIME_INPUT) {
295 | onnxTimeInput_[0] = currentCommunicationPoint;
296 | onnxTimeInput_[1] = dt;
297 | inputs.emplace_back(Ort::Value::CreateTensor(memoryInfo, onnxTimeInput_.data(),
298 | onnxTimeInput_.size(), timeInputShape_.data(),
299 | timeInputShape_.size()));
300 | inputNames[2] = timeInputName_.c_str();
301 | numInputs++;
302 | }
303 |
304 | const char* output_names[] = {outputName_.c_str()};
305 | Ort::Value outputs = Ort::Value::CreateTensor(memoryInfo, onnxOutputs_.data(), onnxOutputs_.size(),
306 | outputShape_.data(), outputShape_.size());
307 |
308 | session_.Run(run_options, inputNames, inputs.data(), numInputs, output_names, &outputs, 1);
309 | } catch (const std::exception& /*e*/) {
310 | return false;
311 | }
312 | return true;
313 | }
314 |
315 | /**
316 | * \brief Performs a step in the OnnxFmu simulation.
317 | *
318 | * This function is called by the FMU framework to perform a step in the simulation.
319 | * It initializes the ONNX states if necessary, sets the ONNX inputs, runs the ONNX model,
320 | * gets the ONNX outputs, and sets the ONNX states.
321 | *
322 | * \param currentCommunicationPoint The current communication point in the simulation.
323 | * \param dt The time step size for the simulation.
324 | * \param newStep A boolean indicating whether a new step has started.
325 | * \param endOfStep The end of the current step in the simulation.
326 | * \returns `true` if the step was successful, `false` otherwise.
327 | */
328 | bool OnnxFmu::DoStep(cppfmu::FMIReal currentCommunicationPoint, cppfmu::FMIReal dt, cppfmu::FMIBoolean /*newStep*/,
329 | cppfmu::FMIReal& /*endOfStep*/)
330 | {
331 | if (doStateInit_) {
332 | bool initOnnxStates = InitOnnxStates();
333 | if (!initOnnxStates) {
334 | return false;
335 | }
336 | doStateInit_ = false;
337 | }
338 | bool setOnnxSuccessful = SetOnnxInputs();
339 | if (!setOnnxSuccessful) {
340 | return false;
341 | }
342 | bool runOnnxSuccessful = RunOnnxModel(currentCommunicationPoint, dt);
343 | if (!runOnnxSuccessful) {
344 | return false;
345 | }
346 | bool getOnnxSuccessful = GetOnnxOutputs();
347 | if (!getOnnxSuccessful) {
348 | return false;
349 | }
350 | bool setOnnxStateSuccessful = SetOnnxStates();
351 | if (!setOnnxStateSuccessful) {
352 | return false;
353 | }
354 | return true;
355 | }
356 |
--------------------------------------------------------------------------------
/src/mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.hpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | // Include definitions specific for each FMU using the template
7 | #include "model_definitions.h"
8 |
9 | union FMIVariable {
10 | struct
11 | {
12 | cppfmu::FMIReal real;
13 | };
14 | struct
15 | {
16 | cppfmu::FMIInteger integer;
17 | };
18 | struct
19 | {
20 | cppfmu::FMIString string;
21 | };
22 | struct
23 | {
24 | cppfmu::FMIBoolean boolean;
25 | };
26 | };
27 |
28 | class OnnxFmu : public cppfmu::SlaveInstance
29 | {
30 | public:
31 | OnnxFmu(cppfmu::FMIString fmuResourceLocation);
32 |
33 | void formatOnnxPath(cppfmu::FMIString fmuResourceLocation);
34 |
35 | // New functions for the OnnxTemplate class
36 | void CreateSession();
37 | bool SetOnnxInputs();
38 | bool GetOnnxOutputs();
39 | bool SetOnnxStates();
40 | bool InitOnnxStates();
41 | bool RunOnnxModel(cppfmu::FMIReal currentCommunicationPoint, cppfmu::FMIReal dt);
42 |
43 | // Override functions from cppmu::SlaveInstance
44 | void Reset() override;
45 | void SetReal(const cppfmu::FMIValueReference vr[], std::size_t nvr, const cppfmu::FMIReal value[]) override;
46 | void GetReal(const cppfmu::FMIValueReference vr[], std::size_t nvr, cppfmu::FMIReal value[]) const override;
47 | void SetInteger(const cppfmu::FMIValueReference vr[], std::size_t nvr, const cppfmu::FMIInteger value[]) override;
48 | void GetInteger(const cppfmu::FMIValueReference vr[], std::size_t nvr, cppfmu::FMIInteger value[]) const override;
49 | void SetBoolean(const cppfmu::FMIValueReference vr[], std::size_t nvr, const cppfmu::FMIBoolean value[]) override;
50 | void GetBoolean(const cppfmu::FMIValueReference vr[], std::size_t nvr, cppfmu::FMIBoolean value[]) const override;
51 | bool DoStep(cppfmu::FMIReal, cppfmu::FMIReal, cppfmu::FMIBoolean, cppfmu::FMIReal&) override;
52 |
53 | private:
54 | std::array fmuVariables_;
55 |
56 | Ort::Env env;
57 | Ort::RunOptions run_options;
58 | Ort::Session session_ {nullptr};
59 |
60 | // store path as wstring for Windows or as char * for Linux
61 | #ifdef _WIN32
62 | std::wstring onnxPath_;
63 | #else
64 | std::string onnxPath_;
65 | #endif
66 |
67 | std::string inputName_ {ONNX_INPUT_NAME};
68 | std::array inputShape_ {1, NUM_ONNX_INPUTS};
69 | std::array onnxInputs_ {};
70 | std::array, NUM_ONNX_FMU_INPUTS> onnxInputValueReferenceIndexPairs_ {ONNX_INPUT_VALUE_REFERENCES};
71 |
72 | std::string stateName_ {ONNX_STATE_NAME};
73 | std::array stateShape_ {1, NUM_ONNX_STATES};
74 | std::array onnxStates_ {};
75 | std::array onnxStateOutputIndexes_ {ONNX_STATE_OUTPUT_INDEXES};
76 |
77 | std::string outputName_ {ONNX_OUTPUT_NAME};
78 | std::array outputShape_ {1, NUM_ONNX_OUTPUTS};
79 | std::array onnxOutputs_ {};
80 | std::array, NUM_ONNX_FMU_OUTPUTS> onnxOutputValueReferenceIndexPairs_ {
81 | ONNX_OUTPUT_VALUE_REFERENCES};
82 |
83 | std::string timeInputName_ {ONNX_TIME_INPUT_NAME};
84 | std::array timeInputShape_ {1, 2};
85 | std::array onnxTimeInput_ {0.0, 0.0};
86 |
87 | std::array, NUM_ONNX_STATE_INIT> onnxStateInitValueReferenceIndexPairs_ {
88 | ONNX_STATE_INIT_VALUE_REFERENCES};
89 | bool doStateInit_ = true;
90 | };
91 |
--------------------------------------------------------------------------------
/src/mlfmu/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/py.typed
--------------------------------------------------------------------------------
/src/mlfmu/types/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/src/mlfmu/types/__init__.py
--------------------------------------------------------------------------------
/src/mlfmu/types/onnx_model.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 | from typing import Any
4 |
5 | from onnxruntime import InferenceSession, NodeArg
6 |
7 |
8 | class ONNXModel:
9 | """ONNX Metadata class.
10 |
11 | Represents an ONNX model and provides methods to load inputs and outputs.
12 | Allows to import the ONNX file and figure out the input/output sizes.
13 |
14 | Raises
15 | ------
16 | ValueError
17 | If the ml model has 3 inputs, but the `usesTime` flag is set to `false` in the json interface.
18 | ValueError
19 | If the number of inputs to the ml model is larger than 3.
20 | ValueError
21 | If the number of outputs from the ml model is not exactly 1.
22 | """
23 |
24 | filename: str = "" #: The name of the ONNX file.
25 | states_name: str = "" #: The name of the internal states input.
26 | state_size: int = 0 #: The size of the internal states input.
27 | input_name: str = "" #: The name of the main input.
28 | input_size: int = 0 #: The size of the main input.
29 | output_name: str = "" #: The name of the output.
30 | output_size: int = 0 #: The size of the output.
31 | time_input_name: str = "" #: The name of the time input.
32 | time_input: bool = False #: Flag indicating whether the model uses time input.
33 | __onnx_path: Path #: The path to the ONNX file.
34 | __onnx_session: InferenceSession #: The ONNX runtime inference session.
35 |
36 | def __init__(
37 | self,
38 | onnx_path: str | os.PathLike[str],
39 | *,
40 | time_input: bool = False,
41 | ) -> None:
42 | """
43 | Initialize the ONNXModel object by loading the ONNX file and assigning model parameters.
44 |
45 | Args:
46 | onnx_path (Union[str, os.PathLike[str]]): The path to the ONNX file.
47 | time_input (bool, optional): Flag indicating whether the model uses time input. Defaults to False.
48 | """
49 | # Load ONNX file into memory
50 | self.__onnx_path = onnx_path if isinstance(onnx_path, Path) else Path(onnx_path)
51 | self.__onnx_session = InferenceSession(onnx_path)
52 |
53 | # Assign model parameters
54 | self.filename = f"{self.__onnx_path.stem}.onnx"
55 | self.time_input = time_input
56 |
57 | self.load_inputs()
58 | self.load_outputs()
59 |
60 | def load_inputs(self) -> None:
61 | """Load the inputs from the ONNX file and assign the input name and size."""
62 | # Get inputs from ONNX file
63 | inputs: list[NodeArg] = self.__onnx_session.get_inputs()
64 | input_names = [inp.name for inp in inputs]
65 | input_shapes = [inp.shape for inp in inputs]
66 | self.input_name = input_names[0]
67 | self.input_size = input_shapes[0][1]
68 |
69 | # Number of internal states
70 | num_states = 0
71 |
72 | # Based on number of inputs work out which are INTERNAL STATES, INPUTS and TIME DATA
73 | if len(input_names) == 3: # noqa: PLR2004
74 | self.states_name = input_names[1]
75 | self.time_input_name = input_names[2]
76 | num_states = input_shapes[1][1]
77 | if not self.time_input:
78 | raise ValueError(
79 | "The ml model has 3 inputs, but the `usesTime` flag is set to `false` in the json interface. "
80 | "A model can only have 3 inputs if it uses time input."
81 | )
82 | elif len(input_names) == 2: # noqa: PLR2004
83 | if self.time_input:
84 | self.time_input_name = input_names[1]
85 | else:
86 | self.states_name = input_names[1]
87 | num_states = input_shapes[1][1]
88 |
89 | elif not input_names or len(input_names) > 3: # noqa: PLR2004
90 | raise ValueError(f"The number of inputs to the ml model (={len(input_names)}) must be 1, 2 or 3")
91 |
92 | self.state_size = num_states
93 |
94 | def load_outputs(self) -> None:
95 | """Load the outputs from the ONNX file and assign the output name and size."""
96 | # Get outputs from ONNX file
97 | outputs: list[Any] = self.__onnx_session.get_outputs()
98 | output_names = [out.name for out in outputs]
99 |
100 | if len(output_names) != 1:
101 | raise ValueError(f"The number of outputs from the ml model (={len(output_names)}) must be exactly 1")
102 |
103 | output_shapes = [out.shape for out in outputs]
104 | self.output_name = output_names[0]
105 | self.output_size = output_shapes[0][1]
106 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """Utilities for mlfmu."""
2 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/builder.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import shutil
4 | import subprocess
5 | from pathlib import Path
6 |
7 | from pydantic import ValidationError
8 |
9 | from mlfmu.types.fmu_component import FmiModel, ModelComponent
10 | from mlfmu.types.onnx_model import ONNXModel
11 | from mlfmu.utils.fmi_builder import generate_model_description
12 | from mlfmu.utils.signals import range_list_expanded
13 |
14 | # Paths to files needed for build
15 | path_to_this_file = Path(__file__).resolve()
16 | absolute_path = path_to_this_file.parent.parent
17 | fmu_build_folder = absolute_path / "fmu_build"
18 | template_parent_path = fmu_build_folder / "templates" / "fmu"
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 |
23 | def format_template_file(
24 | template_path: Path,
25 | save_path: Path,
26 | data: dict[str, str],
27 | ) -> None:
28 | """
29 | Replace all the template strings with their corresponding values and save to a new file.
30 |
31 | Args
32 | ----
33 | template_path (Path): The path to the template file.
34 | save_path (Path): The path to save the formatted file.
35 | data (dict[str, str]): The data containing the values to replace in the template.
36 | """
37 | # TODO: Need to check that these calls are safe from a cybersecurity point of view # noqa: TD002
38 | with Path.open(template_path, encoding="utf-8") as template_file:
39 | template_string = template_file.read()
40 |
41 | formatted_string = template_string.format(**data)
42 | with Path.open(save_path, "w", encoding="utf-8") as save_file:
43 | _ = save_file.write(formatted_string)
44 |
45 |
46 | def create_model_description(
47 | fmu: FmiModel,
48 | src_path: Path,
49 | ) -> None:
50 | """
51 | Generate modelDescription.xml structure for FMU, and save it in a file.
52 |
53 | Args
54 | ----
55 | fmu (FmiModel): The FMI model.
56 | src_path (Path): The path to save the model description file.
57 | """
58 | xml_structure = generate_model_description(fmu_model=fmu)
59 |
60 | # Save in file
61 | xml_structure.write(src_path / "modelDescription.xml", encoding="utf-8")
62 |
63 |
64 | def make_fmu_dirs(src_path: Path) -> None:
65 | """
66 | Create all the directories needed to put all the FMU files in.
67 |
68 | Args
69 | ----
70 | src_path (Path): The path to the FMU source directory.
71 | """
72 | sources_path = src_path / "sources"
73 | resources_path = src_path / "resources"
74 | sources_path.mkdir(parents=True, exist_ok=True)
75 | resources_path.mkdir(parents=True, exist_ok=True)
76 |
77 |
78 | def create_files_from_templates(
79 | data: dict[str, str],
80 | fmu_src: Path,
81 | ) -> None:
82 | """
83 | Create and format all needed C++ files for FMU generation.
84 |
85 | Args
86 | ----
87 | data (dict[str, str]): The data containing the values to format the template files.
88 | fmu_src (Path): The path to the FMU source directory.
89 | """
90 | sources_path = fmu_src / "sources"
91 | file_names = ["fmu.cpp", "model_definitions.h"]
92 |
93 | paths = [
94 | (
95 | template_parent_path / "_template.".join(file_name.split(".")),
96 | sources_path / file_name,
97 | )
98 | for file_name in file_names
99 | ]
100 |
101 | for template_path, save_path in paths:
102 | format_template_file(template_path, save_path, data)
103 |
104 |
105 | def format_template_data(onnx: ONNXModel, fmi_model: FmiModel, model_component: ModelComponent) -> dict[str, str]:
106 | """
107 | Generate the key-value pairs needed to format the template files to valid C++.
108 |
109 | Args
110 | ----
111 | onnx (ONNXModel): The ONNX model.
112 | fmi_model (FmiModel): The FMI model.
113 | model_component (ModelComponent): The model component.
114 |
115 | Returns
116 | -------
117 | dict[str, str]: The formatted template data.
118 | """
119 | # Work out template mapping between ONNX and FMU ports
120 | inputs, outputs, state_init = fmi_model.get_template_mapping()
121 | state_output_indexes = [
122 | index for state in model_component.states for index in range_list_expanded(state.agent_output_indexes)
123 | ]
124 |
125 | # Total number of inputs/outputs/internal states
126 | num_fmu_variables = fmi_model.get_total_variable_number()
127 | num_fmu_inputs = len(inputs)
128 | num_fmu_outputs = len(outputs)
129 | num_onnx_states = len(state_output_indexes)
130 | num_onnx_state_init = len(state_init)
131 |
132 | # Checking compatibility between ModelComponent and ONNXModel
133 | if num_fmu_inputs > onnx.input_size:
134 | raise ValueError(
135 | "The number of total input indexes for all inputs and parameter in the interface file "
136 | f"(={num_fmu_inputs}) cannot exceed the input size of the ml model (={onnx.input_size})"
137 | )
138 |
139 | if num_fmu_outputs > onnx.output_size:
140 | raise ValueError(
141 | "The number of total output indexes for all outputs in the interface file "
142 | f"(={num_fmu_outputs}) cannot exceed the output size of the ml model (={onnx.output_size})"
143 | )
144 | if num_onnx_states > onnx.state_size:
145 | raise ValueError(
146 | "The number of total output indexes for all states in the interface file "
147 | f"(={num_onnx_states}) cannot exceed either the state input size (={onnx.state_size})"
148 | )
149 | if num_onnx_state_init > onnx.state_size:
150 | raise ValueError(
151 | "The number of states that are initialized in the interface file "
152 | f"(={num_onnx_state_init}) cannot exceed either the state input size (={onnx.state_size})"
153 | )
154 |
155 | # Flatten vectors to comply with template requirements
156 | # -> onnx-index, variable-reference, onnx-index, variable-reference ...
157 | flattened_input_string = ", ".join(
158 | [str(index) for indexValueReferencePair in inputs for index in indexValueReferencePair]
159 | )
160 | flattened_output_string = ", ".join(
161 | [str(index) for indexValueReferencePair in outputs for index in indexValueReferencePair]
162 | )
163 | flattened_state_string = ", ".join([str(index) for index in state_output_indexes])
164 | flattened_state_init_string = ", ".join(
165 | [str(index) for indexValueReferencePair in state_init for index in indexValueReferencePair]
166 | )
167 |
168 | template_data: dict[str, str] = {
169 | "numFmuVariables": str(num_fmu_variables),
170 | "FmuName": fmi_model.name,
171 | "numOnnxInputs": str(onnx.input_size),
172 | "numOnnxOutputs": str(onnx.output_size),
173 | "numOnnxStates": str(onnx.state_size),
174 | "numOnnxStateInit": str(num_onnx_state_init),
175 | "onnxUsesTime": "true" if onnx.time_input else "false",
176 | "onnxInputName": onnx.input_name,
177 | "onnxStatesName": onnx.states_name,
178 | "onnxTimeInputName": onnx.time_input_name,
179 | "onnxOutputName": onnx.output_name,
180 | "onnxFileName": onnx.filename,
181 | "numOnnxFmuInputs": str(num_fmu_inputs),
182 | "numOnnxFmuOutputs": str(num_fmu_outputs),
183 | "numOnnxStatesOutputs": str(num_onnx_states),
184 | "onnxInputValueReferences": flattened_input_string,
185 | "onnxOutputValueReferences": flattened_output_string,
186 | "onnxStateOutputIndexes": flattened_state_string,
187 | "onnxStateInitValueReferences": flattened_state_init_string,
188 | }
189 |
190 | return template_data
191 |
192 |
193 | def validate_interface_spec(
194 | spec: str,
195 | ) -> tuple[ValidationError | None, ModelComponent]:
196 | """
197 | Parse and validate JSON data from the interface file.
198 |
199 | Args
200 | ----
201 | spec (str): Contents of the JSON file.
202 |
203 | Returns
204 | -------
205 | tuple[Optional[ValidationError], ModelComponent]:
206 | The validation error (if any) and the validated model component.
207 | The pydantic model instance that contains all the interface information.
208 | """
209 | parsed_spec = ModelComponent.model_validate_json(json_data=spec, strict=True)
210 | try:
211 | validated_model = ModelComponent.model_validate(parsed_spec)
212 | except ValidationError as e:
213 | return e, parsed_spec
214 |
215 | return None, validated_model
216 |
217 |
218 | def generate_fmu_files(
219 | fmu_src_path: os.PathLike[str],
220 | onnx_path: os.PathLike[str],
221 | interface_spec_path: os.PathLike[str],
222 | ) -> FmiModel:
223 | """
224 | Generate FMU files based on the FMU source, ONNX model, and interface specification.
225 |
226 | Args
227 | ----
228 | fmu_src_path (os.PathLike[str]): The path to the FMU source directory.
229 | onnx_path (os.PathLike[str]): The path to the ONNX model file.
230 | interface_spec_path (os.PathLike[str]): The path to the interface specification file.
231 |
232 | Returns
233 | -------
234 | FmiModel: The FMI model.
235 | """
236 | # Create Path instances for the path to the spec and ONNX file.
237 | onnx_path = Path(onnx_path)
238 | interface_spec_path = Path(interface_spec_path)
239 |
240 | # Load JSON interface contents
241 | with Path.open(interface_spec_path, encoding="utf-8") as template_file:
242 | interface_contents = template_file.read()
243 |
244 | # Validate the FMU interface spec against expected Schema
245 | error, component_model = validate_interface_spec(interface_contents)
246 |
247 | if error:
248 | # Display error and finish workflow
249 | raise error
250 |
251 | # Create ONNXModel and FmiModel instances -> load some metadata
252 | onnx_model = ONNXModel(onnx_path=onnx_path, time_input=bool(component_model.uses_time))
253 | fmi_model = FmiModel(model=component_model)
254 | fmu_source = Path(fmu_src_path) / fmi_model.name
255 |
256 | template_data = format_template_data(onnx=onnx_model, fmi_model=fmi_model, model_component=component_model)
257 |
258 | # Generate all FMU files
259 | make_fmu_dirs(fmu_source)
260 | create_files_from_templates(data=template_data, fmu_src=fmu_source)
261 | create_model_description(fmu=fmi_model, src_path=fmu_source)
262 |
263 | # Copy ONNX file and save it inside FMU folder
264 | _ = shutil.copyfile(src=onnx_path, dst=fmu_source / "resources" / onnx_model.filename)
265 |
266 | return fmi_model
267 |
268 |
269 | def validate_fmu_source_files(fmu_path: os.PathLike[str]) -> None:
270 | """
271 | Validate the FMU source files.
272 |
273 | Args
274 | ----
275 | fmu_path (os.PathLike[str]): The path to the FMU source directory.
276 |
277 | Raises
278 | ------
279 | FileNotFoundError: If required files are missing in the FMU source directory.
280 | """
281 | fmu_path = Path(fmu_path)
282 |
283 | files_should_exist: list[str] = [
284 | "modelDescription.xml",
285 | "sources/fmu.cpp",
286 | "sources/model_definitions.h",
287 | ]
288 |
289 | if files_not_exists := [file for file in files_should_exist if not (fmu_path / file).is_file()]:
290 | raise FileNotFoundError(
291 | f"The files {files_not_exists} are not contained in the provided FMU source path ({fmu_path})"
292 | )
293 |
294 | resources_dir = fmu_path / "resources"
295 |
296 | num_onnx_files = len(list(resources_dir.glob("*.onnx")))
297 |
298 | if num_onnx_files < 1:
299 | raise FileNotFoundError(
300 | f"There is no *.onnx file in the resource folder in the provided FMU source path ({fmu_path})"
301 | )
302 |
303 |
304 | def build_fmu(
305 | fmu_src_path: os.PathLike[str],
306 | fmu_build_path: os.PathLike[str],
307 | fmu_save_path: os.PathLike[str],
308 | ) -> None:
309 | """
310 | Build the FMU.
311 |
312 | Args
313 | ----
314 | fmu_src_path (os.PathLike[str]): The path to the FMU source directory.
315 | fmu_build_path (os.PathLike[str]): The path to the FMU build directory.
316 | fmu_save_path (os.PathLike[str]): The path to save the built FMU.
317 |
318 | Raises
319 | ------
320 | FileNotFoundError: If required files are missing in the FMU source directory.
321 | """
322 | fmu_src_path = Path(fmu_src_path)
323 | validate_fmu_source_files(fmu_src_path)
324 | fmu_name = fmu_src_path.stem
325 | conan_install_command = [
326 | "conan",
327 | "install",
328 | ".",
329 | "-of",
330 | str(fmu_build_path),
331 | "-u",
332 | "-b",
333 | "missing",
334 | "-o",
335 | "shared=True",
336 | ]
337 | cmake_set_folders = [
338 | f"-DCMAKE_BINARY_DIR={fmu_build_path!s}",
339 | f"-DFMU_OUTPUT_DIR={fmu_save_path!s}",
340 | f"-DFMU_NAMES={fmu_name}",
341 | f"-DFMU_SOURCE_PATH={fmu_src_path.parent!s}",
342 | ]
343 | # Windows vs Linux
344 | conan_preset = "conan-default" if os.name == "nt" else "conan-release"
345 | cmake_command = ["cmake", *cmake_set_folders, "--preset", conan_preset]
346 | cmake_build_command = ["cmake", "--build", ".", "-j", "14", "--config", "Release"]
347 |
348 | # Change directory to the build folder
349 | os.chdir(fmu_build_folder)
350 |
351 | # Run conan install, cmake, cmake build
352 | logger.debug("Builder: Run conan install")
353 | try:
354 | _ = subprocess.run(conan_install_command, check=True) # noqa: S603
355 | except subprocess.CalledProcessError:
356 | logger.exception("Exception in conan install: %s")
357 |
358 | logger.debug("Builder: Run cmake")
359 | try:
360 | _ = subprocess.run(cmake_command, check=True) # noqa: S603
361 | except subprocess.CalledProcessError:
362 | logger.exception("Exception in cmake: %s")
363 |
364 | os.chdir(fmu_build_path)
365 | logger.debug("Builder: Run cmake build")
366 | try:
367 | _ = subprocess.run(cmake_build_command, check=True) # noqa: S603
368 | except subprocess.CalledProcessError:
369 | logger.exception("Exception in cmake build: %s")
370 |
371 | logger.debug("Builder: Done with build_fmu")
372 |
373 | # Return to original working directory (leave build dir)
374 | os.chdir(absolute_path)
375 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/fmi_builder.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import logging
3 | from importlib import metadata
4 | from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
5 |
6 | from mlfmu.types.fmu_component import (
7 | FmiCausality,
8 | FmiModel,
9 | FmiVariability,
10 | FmiVariable,
11 | )
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def requires_start(var: FmiVariable) -> bool:
17 | """
18 | Test if a variable requires a start attribute.
19 |
20 | Returns
21 | -------
22 | True if successful, False otherwise
23 | """
24 | return var.causality in (FmiCausality.INPUT, FmiCausality.PARAMETER) or var.variability == FmiVariability.CONSTANT
25 |
26 |
27 | def generate_model_description(fmu_model: FmiModel) -> ElementTree:
28 | """
29 | Generate FMU modelDescription as XML.
30 |
31 | Args
32 | ----
33 | fmu_model (FmiModel): Object representation of FMI slave instance
34 |
35 | Returns
36 | -------
37 | xml.etree.TreeElement.Element: modelDescription XML representation.
38 | """
39 | t = datetime.datetime.now(datetime.timezone.utc)
40 | date_str = t.isoformat(timespec="seconds")
41 | TOOL_VERSION = metadata.version("mlfmu") # noqa: N806
42 |
43 | # Root tag
44 | model_description = {
45 | "fmiVersion": "2.0",
46 | "modelName": fmu_model.name,
47 | "guid": f"{fmu_model.guid!s}" if fmu_model.guid is not None else "@FMU_UUID@",
48 | "version": fmu_model.version,
49 | "generationDateAndTime": date_str,
50 | "variableNamingConvention": "structured",
51 | "generationTool": f"MLFMU {TOOL_VERSION}",
52 | }
53 |
54 | # Optional props
55 | if fmu_model.copyright is not None:
56 | model_description["copyright"] = fmu_model.copyright
57 | if fmu_model.license is not None:
58 | model_description["license"] = fmu_model.license
59 | if fmu_model.author is not None:
60 | model_description["author"] = fmu_model.author
61 | if fmu_model.description is not None:
62 | model_description["description"] = fmu_model.description
63 |
64 | root = Element("fmiModelDescription", model_description)
65 |
66 | # tag options
67 | cosim_options = {
68 | "modelIdentifier": fmu_model.name,
69 | "canHandleVariableCommunicationStepSize": "true",
70 | }
71 | _ = SubElement(root, "CoSimulation", attrib=cosim_options)
72 |
73 | # tag -> Append inputs/parameters/outputs
74 | variables = SubElement(root, "ModelVariables")
75 |
76 | # tag with tab inside --> Append all outputs
77 | model_structure = SubElement(root, "ModelStructure")
78 | outputs = SubElement(model_structure, "Outputs")
79 | initial_unknowns = SubElement(model_structure, "InitialUnknowns")
80 |
81 | # Get all variables to add them inside the tag
82 | model_variables = fmu_model.get_fmi_model_variables()
83 |
84 | # The variables needs to be added in the order of their valueReference
85 | sorted_model_variables = sorted(model_variables, key=lambda x: x.variable_reference)
86 |
87 | # Add each variable inside the tag
88 | for var in sorted_model_variables:
89 | # XML variable attributes
90 | var_attrs = {
91 | "name": var.name,
92 | "valueReference": str(var.variable_reference),
93 | "causality": var.causality.value,
94 | "description": var.description or "",
95 | "variability": var.variability.value if var.variability else FmiVariability.CONTINUOUS.value,
96 | }
97 | var_elem = SubElement(variables, "ScalarVariable", var_attrs)
98 |
99 | var_type_attrs = {}
100 | if requires_start(var):
101 | var_type_attrs["start"] = str(var.start_value)
102 |
103 | # FMI variable type element
104 | _ = SubElement(var_elem, var.type.value.capitalize(), var_type_attrs)
105 |
106 | # Adding outputs inside
107 | if var.causality == FmiCausality.OUTPUT:
108 | # Index is 1-indexed for tag
109 | unknown_attributes = {"index": str(var.variable_reference + 1)}
110 | # For each output create an tag inside both and
111 | _ = SubElement(outputs, "Unknown", unknown_attributes)
112 | _ = SubElement(initial_unknowns, "Unknown", unknown_attributes)
113 |
114 | # Create XML tree containing root element and pretty format its contents
115 | xml_tree = ElementTree(root)
116 | indent(xml_tree, space="\t", level=0)
117 | return xml_tree
118 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/interface.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from pathlib import Path
4 | from typing import cast
5 |
6 | from dictIO.utils.path import relative_path
7 | from json_schema_for_humans.generate import (
8 | SchemaToRender,
9 | TemplateRenderer,
10 | generate_schemas_doc,
11 | )
12 | from json_schema_for_humans.generation_configuration import GenerationConfiguration
13 | from pydantic import BaseModel
14 | from pydantic._internal._model_construction import ModelMetaclass
15 |
16 | from mlfmu.types.fmu_component import ModelComponent
17 |
18 | __ALL__ = ["publish_interface_schema"]
19 |
20 |
21 | def generate_interface_schema(
22 | model: BaseModel | ModelMetaclass,
23 | schema_dir: str | os.PathLike[str] | None = None,
24 | ) -> None:
25 | """
26 | Generate a JSON interface schema file for the given model.
27 |
28 | Args
29 | ----
30 | model (BaseModel | ModelMetaclass): The pydantic model for which to generate the schema.
31 | schema_dir (str | os.PathLike[str], optional):
32 | The directory where the schema file will be saved. Defaults to None.
33 | """
34 | schema_dir_default = Path.cwd() / "docs/schema"
35 | schema_dir = schema_dir or schema_dir_default
36 | # Make sure schema_dir argument is of type Path. If not, cast it to Path type.
37 | schema_dir = schema_dir if isinstance(schema_dir, Path) else Path(schema_dir)
38 |
39 | # Assert model argument is a pydantic BaseModel
40 | # Background: ModelMetaClass is added just to please static type checking,
41 | # which would otherwise complain.
42 | # Behind the scenes in pdyantic, models always inherit the attributes of BaseModel.
43 | if not hasattr(model, "model_json_schema"):
44 | raise ValueError("model argument must be a pydantic BaseModel")
45 | model = cast(BaseModel, model)
46 |
47 | # Create schema_dir if it does not exist
48 | schema_dir.mkdir(parents=True, exist_ok=True)
49 |
50 | json_file: Path = schema_dir / "schema.json"
51 | schema = json.dumps(model.model_json_schema(by_alias=True), indent=4)
52 |
53 | with Path.open(json_file, "w", encoding="utf-8") as f:
54 | _ = f.write(schema)
55 |
56 | return
57 |
58 |
59 | def generate_interface_docs(
60 | schema_dir: str | os.PathLike[str] | None = None,
61 | docs_dir: str | os.PathLike[str] | None = None,
62 | ) -> None:
63 | """
64 | Generate HTML documentation for the JSON interface schema files in the schema directory.
65 |
66 | Args
67 | ----
68 | schema_dir (str | os.PathLike[str], optional):
69 | The directory where the schema files are located. Defaults to None.
70 | docs_dir (str | os.PathLike[str], optional):
71 | The directory where the documentation files will be saved. Defaults to None.
72 | """
73 | schema_dir_default = Path.cwd() / "docs/schema"
74 | schema_dir = schema_dir or schema_dir_default
75 |
76 | docs_dir_default = Path.cwd() / "docs/interface"
77 | docs_dir = docs_dir or docs_dir_default
78 |
79 | # Make sure schema_dir and docs_dir are of type Path. If not, cast it to Path type.
80 | schema_dir = schema_dir if isinstance(schema_dir, Path) else Path(schema_dir)
81 | docs_dir = docs_dir if isinstance(docs_dir, Path) else Path(docs_dir)
82 |
83 | # Create dirs in case they don't exist
84 | schema_dir.mkdir(parents=True, exist_ok=True)
85 | docs_dir.mkdir(parents=True, exist_ok=True)
86 |
87 | # Collect all schemata in schema dir
88 | pattern: str = "**.json"
89 | schemata: list[Path] = list(schema_dir.glob(pattern))
90 |
91 | # Generate html documentation for schemata
92 | config: GenerationConfiguration = GenerationConfiguration(
93 | template_name="js",
94 | expand_buttons=True,
95 | link_to_reused_ref=False,
96 | show_breadcrumbs=False,
97 | )
98 |
99 | schemas_to_render: list[SchemaToRender] = []
100 |
101 | for schema in schemata:
102 | rel_path: Path = relative_path(from_path=schema_dir, to_path=schema.parent)
103 | name: str = schema.stem
104 | html_file: Path = docs_dir / rel_path / f"{name}.html"
105 | schema_to_render: SchemaToRender = SchemaToRender(
106 | schema_file=schema,
107 | result_file=html_file,
108 | output_dir=None,
109 | )
110 | schemas_to_render.append(schema_to_render)
111 |
112 | _ = generate_schemas_doc(
113 | schemas_to_render=schemas_to_render,
114 | template_renderer=TemplateRenderer(config),
115 | )
116 |
117 |
118 | def publish_interface_schema(
119 | schema_dir: str | os.PathLike[str] | None = None,
120 | docs_dir: str | os.PathLike[str] | None = None,
121 | ) -> None:
122 | """
123 | Publish the JSON schema and HTML documentation for the interface.
124 |
125 | Args
126 | ----
127 | schema_dir (str | os.PathLike[str], optional):
128 | The directory where the schema file will be saved. Defaults to None.
129 | docs_dir (str | os.PathLike[str], optional):
130 | The directory where the documentation files will be saved. Defaults to None.
131 | """
132 | # Generate JSON schema
133 | generate_interface_schema(model=ModelComponent, schema_dir=schema_dir)
134 |
135 | # Generate documentation HTML
136 | generate_interface_docs(schema_dir=schema_dir, docs_dir=docs_dir)
137 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/logging.py:
--------------------------------------------------------------------------------
1 | """Functions to configure logging for the application."""
2 |
3 | import logging
4 | import sys
5 | from pathlib import Path
6 |
7 | __all__ = ["configure_logging"]
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def configure_logging(
13 | log_level_console: str = "WARNING",
14 | log_file: Path | None = None,
15 | log_level_file: str = "WARNING",
16 | ) -> None:
17 | """Configure logging for the application, allowing for both console and file logging.
18 |
19 | Sets the log levels and formats for the output, ensuring that logs are captured as specified.
20 |
21 | Args:
22 | log_level_console (str): The logging level for console output. Defaults to "WARNING".
23 | log_file (Path | None): The path to the log file. If None, file logging is disabled. Defaults to None.
24 | log_level_file (str): The logging level for file output. Defaults to "WARNING".
25 |
26 | Raises
27 | ------
28 | TypeError: If the provided log levels are invalid.
29 |
30 | Examples
31 | --------
32 | configure_logging(log_level_console="INFO", log_file=Path("app.log"), log_level_file="DEBUG")
33 | """
34 | # sourcery skip: extract-duplicate-method, extract-method
35 | log_level_console_numeric = getattr(logging, log_level_console.upper(), None)
36 | if not isinstance(log_level_console_numeric, int):
37 | raise TypeError(f"Invalid log level to console: {log_level_console_numeric}")
38 |
39 | log_level_file_numeric = getattr(logging, log_level_file.upper(), None)
40 | if not isinstance(log_level_file_numeric, int):
41 | raise TypeError(f"Invalid log level to file: {log_level_file_numeric}")
42 |
43 | root_logger = logging.getLogger()
44 | root_logger.setLevel(logging.DEBUG)
45 |
46 | console_handler = logging.StreamHandler(sys.stdout)
47 | console_handler.setLevel(log_level_console_numeric)
48 | console_formatter = logging.Formatter("%(levelname)-8s %(message)s")
49 | console_handler.setFormatter(console_formatter)
50 | root_logger.addHandler(console_handler)
51 |
52 | if log_file:
53 | if not log_file.parent.exists():
54 | log_file.parent.mkdir(parents=True, exist_ok=True)
55 | file_handler = logging.FileHandler(str(log_file.absolute()), "a")
56 | print(f"Logging to: {log_file.absolute()}") # noqa: T201
57 | file_handler.setLevel(log_level_file_numeric)
58 | file_formatter = logging.Formatter("%(asctime)s %(levelname)-8s %(message)s", "%Y-%m-%d %H:%M:%S")
59 | file_handler.setFormatter(file_formatter)
60 | root_logger.addHandler(file_handler)
61 |
62 | logging.getLogger("mlfmu").setLevel(logging.WARNING)
63 |
64 | return
65 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/path.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 |
5 | def find_default_file(
6 | folder: Path,
7 | file_extension: str,
8 | default_name: str | None = None,
9 | ) -> Path | None:
10 | """Return a file inside folder with the file extension that matches file_extension.
11 |
12 | If there are multiple matches it uses the closest match to default_name if given.
13 | Return None if there is no clear match.
14 |
15 | Parameters
16 | ----------
17 | folder : Path
18 | the folder to search in
19 | file_extension : str
20 | file extension to search for
21 | default_name : str | None, optional
22 | file name used to determine "closest match"
23 | in case multiple files match file_extension, by default None
24 |
25 | Returns
26 | -------
27 | Path | None
28 | the path to the file if it is found, otherwise None
29 | """
30 | # Check if there is a file with correct file extension in current working directory. If it exists use it.
31 | matching_files: list[Path] = []
32 |
33 | for file in os.listdir(folder):
34 | file_path = folder / file
35 | if file_path.is_file() and file_path.suffix.lstrip(".") == file_extension:
36 | matching_files.append(file_path)
37 |
38 | if not matching_files:
39 | return None
40 |
41 | if len(matching_files) == 1:
42 | return matching_files[0]
43 |
44 | # If there are more matches on file extension. Use the one that matches the default name
45 | if default_name is None:
46 | return None
47 |
48 | name_matches = [file for file in matching_files if default_name in file.stem]
49 |
50 | if not name_matches:
51 | return None
52 |
53 | if len(name_matches) == 1:
54 | return name_matches[0]
55 |
56 | # If more multiple name matches use the exact match if it exists
57 | name_exact_matches = [file for file in matching_files if default_name == file.stem]
58 |
59 | return name_matches[0] if len(name_exact_matches) == 1 else None
60 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/signals.py:
--------------------------------------------------------------------------------
1 | def range_list_expanded(list_of_ranges: list[str]) -> list[int]:
2 | """
3 | Expand ranges specified in interface. They should be formatted as [starts_index:end_index].
4 |
5 | Args
6 | ----
7 | list_of_ranges (List[str]): List of indexes or ranges.
8 |
9 | Returns
10 | -------
11 | A list of all indexes covered in ranges or individual indexes
12 | """
13 | indexes: list[int] = []
14 |
15 | for val in list_of_ranges:
16 | if ":" in val:
17 | indexes.extend(range(*[int(num) for num in val.split(":")]))
18 | else:
19 | indexes.append(int(val))
20 |
21 | return indexes
22 |
--------------------------------------------------------------------------------
/src/mlfmu/utils/strings.py:
--------------------------------------------------------------------------------
1 | def to_camel(string: str) -> str:
2 | """Change casing of string to CamelCase."""
3 | words = string.split("_")
4 | camel_string: str = words[0] + "".join(word.capitalize() for word in words[1:])
5 | return camel_string
6 |
--------------------------------------------------------------------------------
/stubs/onnxruntime/__init__.pyi:
--------------------------------------------------------------------------------
1 | import os
2 | from collections.abc import Sequence
3 | from typing import Any
4 |
5 | class Dummy: ...
6 |
7 | class NodeArg:
8 | name: str
9 | shape: list[int]
10 |
11 | class Session: ...
12 | class SessionOptions: ...
13 |
14 | class InferenceSession(Session):
15 | def __init__(
16 | self,
17 | path_or_bytes: str | bytes | os.PathLike[str],
18 | sess_options: SessionOptions | None = None,
19 | providers: Sequence[str | tuple[str, dict[Any, Any]]] | None = None,
20 | provider_options: Sequence[dict[Any, Any]] | None = None,
21 | **kwargs: Any,
22 | ) -> None: ...
23 | def get_inputs(self) -> list[NodeArg]: ...
24 | def get_outputs(self) -> list[NodeArg]: ...
25 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | temp_*
2 | ~$temp*
--------------------------------------------------------------------------------
/tests/cli/test_mlfmu_cli.py:
--------------------------------------------------------------------------------
1 | # pyright: reportPrivateUsage=false
2 | import sys
3 | from argparse import ArgumentError
4 | from dataclasses import dataclass, field
5 | from pathlib import Path
6 |
7 | import pytest
8 |
9 | from mlfmu.api import MlFmuCommand
10 | from mlfmu.cli import mlfmu
11 | from mlfmu.cli.mlfmu import _argparser, main
12 |
13 | # *****Test commandline interface (CLI)************************************************************
14 |
15 |
16 | @dataclass()
17 | class CliArgs:
18 | # Expected default values for the CLI arguments when mlfmu gets called via the commandline
19 | quiet: bool = False
20 | verbose: bool = False
21 | log: str | None = None
22 | log_level: str = field(default_factory=lambda: "WARNING")
23 | command: str = ""
24 |
25 |
26 | @pytest.mark.parametrize(
27 | "inputs, expected",
28 | [
29 | ([], ArgumentError),
30 | (["asd"], ArgumentError),
31 | (["build", "-q"], CliArgs(quiet=True, command="build")),
32 | (["build", "--quiet"], CliArgs(quiet=True, command="build")),
33 | (["build", "-v"], CliArgs(verbose=True, command="build")),
34 | (["build", "--verbose"], CliArgs(verbose=True, command="build")),
35 | (["build", "-qv"], ArgumentError),
36 | (["build", "--log", "logFile"], CliArgs(log="logFile", command="build")),
37 | (["build", "--log"], ArgumentError),
38 | (["build", "--log-level", "INFO"], CliArgs(log_level="INFO", command="build")),
39 | (["build", "--log-level"], ArgumentError),
40 | (["build", "-o"], ArgumentError),
41 | ],
42 | )
43 | def test_cli(
44 | inputs: list[str],
45 | expected: CliArgs | type,
46 | monkeypatch: pytest.MonkeyPatch,
47 | ):
48 | """
49 | Test the command-line interface (CLI) of the 'mlfmu' program.
50 |
51 | Args
52 | ----
53 | inputs (List[str]): A list of input arguments to be passed to the CLI.
54 | expected (Union[CliArgs, type]): The expected output of the CLI.
55 | It can be either an instance of the `CliArgs` class or a subclass of `Exception`.
56 | monkeypatch (pytest.MonkeyPatch): A pytest fixture that allows patching of objects at runtime.
57 |
58 | Raises
59 | ------
60 | AssertionError: If the `expected` argument is neither an instance of `CliArgs`
61 | nor a subclass of `Exception`.
62 | """
63 |
64 | # sourcery skip: no-conditionals-in-tests
65 | # sourcery skip: no-loop-in-tests
66 |
67 | # Prepare
68 | monkeypatch.setattr(sys, "argv", ["mlfmu", *inputs])
69 | parser = _argparser()
70 |
71 | # Execute
72 | if isinstance(expected, CliArgs):
73 | args_expected: CliArgs = expected
74 | args = parser.parse_args()
75 |
76 | # Assert args
77 | for key in args_expected.__dataclass_fields__:
78 | assert args.__getattribute__(key) == args_expected.__getattribute__(key)
79 | elif issubclass(expected, Exception):
80 | exception: type = expected
81 |
82 | # Assert that expected exception is raised
83 | with pytest.raises((exception, SystemExit)):
84 | args = parser.parse_args()
85 | else:
86 | raise AssertionError
87 |
88 |
89 | # *****Ensure the CLI correctly configures logging*************************************************
90 |
91 |
92 | @dataclass()
93 | class ConfigureLoggingArgs:
94 | # Values that main() is expected to pass to ConfigureLogging() by default when configuring the logging
95 | log_level_console: str = field(default_factory=lambda: "WARNING")
96 | log_file: Path | None = None
97 | log_level_file: str = field(default_factory=lambda: "WARNING")
98 |
99 |
100 | @pytest.mark.parametrize(
101 | "inputs, expected",
102 | [
103 | ([], ArgumentError),
104 | (["build"], ConfigureLoggingArgs()),
105 | (["build", "-q"], ConfigureLoggingArgs(log_level_console="ERROR")),
106 | (
107 | ["build", "--quiet"],
108 | ConfigureLoggingArgs(log_level_console="ERROR"),
109 | ),
110 | (["build", "-v"], ConfigureLoggingArgs(log_level_console="INFO")),
111 | (
112 | ["build", "--verbose"],
113 | ConfigureLoggingArgs(log_level_console="INFO"),
114 | ),
115 | (["build", "-qv"], ArgumentError),
116 | (
117 | ["build", "--log", "logFile"],
118 | ConfigureLoggingArgs(log_file=Path("logFile")),
119 | ),
120 | (["build", "--log"], ArgumentError),
121 | (
122 | ["build", "--log-level", "INFO"],
123 | ConfigureLoggingArgs(log_level_file="INFO"),
124 | ),
125 | (["build", "--log-level"], ArgumentError),
126 | ],
127 | )
128 | def test_logging_configuration(
129 | inputs: list[str],
130 | expected: ConfigureLoggingArgs | type,
131 | monkeypatch: pytest.MonkeyPatch,
132 | ):
133 | """
134 | Test the logging configuration of the `main` function in the `mlfmu` module.
135 |
136 | Args
137 | ----
138 | inputs (List[str]): The list of input arguments to be passed to the `main` function.
139 | expected (Union[ConfigureLoggingArgs, type]): The expected output of the `main` function.
140 | It can be an instance of `ConfigureLoggingArgs` or a subclass of `Exception`.
141 | monkeypatch (pytest.MonkeyPatch): The monkeypatch fixture provided by pytest.
142 |
143 | Raises
144 | ----
145 | AssertionError: If the `expected` argument is neither an instance of `ConfigureLoggingArgs`
146 | nor a subclass of `Exception`.
147 | """
148 |
149 | # sourcery skip: no-conditionals-in-tests
150 | # sourcery skip: no-loop-in-tests
151 |
152 | # Prepare
153 | monkeypatch.setattr(sys, "argv", ["mlfmu", *inputs])
154 | args: ConfigureLoggingArgs = ConfigureLoggingArgs()
155 |
156 | def fake_configure_logging(
157 | log_level_console: str,
158 | log_file: Path | None,
159 | log_level_file: str,
160 | ):
161 | args.log_level_console = log_level_console
162 | args.log_file = log_file
163 | args.log_level_file = log_level_file
164 |
165 | def fake_run(
166 | command: str,
167 | interface_file: str | None,
168 | model_file: str | None,
169 | fmu_path: str | None,
170 | source_folder: str | None,
171 | ):
172 | pass
173 |
174 | monkeypatch.setattr(mlfmu, "configure_logging", fake_configure_logging)
175 | monkeypatch.setattr(mlfmu, "run", fake_run)
176 | # Execute
177 | if isinstance(expected, ConfigureLoggingArgs):
178 | args_expected: ConfigureLoggingArgs = expected
179 | main()
180 | # Assert args
181 | for key in args_expected.__dataclass_fields__:
182 | assert args.__getattribute__(key) == args_expected.__getattribute__(key)
183 | elif issubclass(expected, Exception):
184 | exception: type = expected
185 | # Assert that expected exception is raised
186 | with pytest.raises((exception, SystemExit)):
187 | main()
188 | else:
189 | raise AssertionError
190 |
191 |
192 | # *****Ensure the CLI correctly invokes the API****************************************************
193 |
194 |
195 | @dataclass()
196 | class ApiArgs:
197 | # Values that main() is expected to pass to run() by default when invoking the API
198 | command: MlFmuCommand | None = None
199 | interface_file: str | None = None
200 | model_file: str | None = None
201 | fmu_path: str | None = None
202 | source_folder: str | None = None
203 |
204 |
205 | @pytest.mark.parametrize(
206 | "inputs, expected",
207 | [
208 | ([], ArgumentError),
209 | (["build"], ApiArgs()),
210 | ],
211 | )
212 | def test_api_invokation(
213 | inputs: list[str],
214 | expected: ApiArgs | type,
215 | monkeypatch: pytest.MonkeyPatch,
216 | ):
217 | # sourcery skip: no-conditionals-in-tests
218 | # sourcery skip: no-loop-in-tests
219 | """
220 | Test the invocation of the API function.
221 |
222 | Args
223 | ----
224 | inputs (List[str]): The list of input arguments.
225 | expected (Union[ApiArgs, type]): The expected output, either an instance of ApiArgs or an exception type.
226 | monkeypatch (pytest.MonkeyPatch): The monkeypatch object for patching sys.argv.
227 |
228 | Raises
229 | ----
230 | AssertionError: If the expected output is neither an instance of ApiArgs nor an exception type.
231 | """
232 |
233 | # Prepare
234 | monkeypatch.setattr(sys, "argv", ["mlfmu", *inputs])
235 | args: ApiArgs = ApiArgs()
236 |
237 | def fake_run(
238 | command: str,
239 | interface_file: str | None,
240 | model_file: str | None,
241 | fmu_path: str | None,
242 | source_folder: str | None,
243 | ):
244 | args.command = MlFmuCommand.from_string(command)
245 | args.interface_file = interface_file
246 | args.model_file = model_file
247 | args.fmu_path = fmu_path
248 | args.source_folder = source_folder
249 |
250 | monkeypatch.setattr(mlfmu, "run", fake_run)
251 | # Execute
252 | if isinstance(expected, ApiArgs):
253 | args_expected: ApiArgs = expected
254 | main()
255 | # Assert args
256 | for key in args_expected.__dataclass_fields__:
257 | assert args.__getattribute__(key) == args_expected.__getattribute__(key)
258 | elif issubclass(expected, Exception):
259 | exception: type = expected
260 | # Assert that expected exception is raised
261 | with pytest.raises((exception, SystemExit)):
262 | main()
263 | else:
264 | raise AssertionError
265 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from pathlib import Path
4 | from shutil import rmtree
5 |
6 | import pytest
7 |
8 |
9 | @pytest.fixture(scope="package", autouse=True)
10 | def chdir() -> None:
11 | """
12 | Fixture that changes the current working directory to the 'test_working_directory' folder.
13 | This fixture is automatically used for the entire package.
14 | """
15 | os.chdir(Path(__file__).parent.absolute() / "test_working_directory")
16 |
17 |
18 | @pytest.fixture(scope="package", autouse=True)
19 | def test_dir() -> Path:
20 | """
21 | Fixture that returns the absolute path of the directory containing the current file.
22 | This fixture is automatically used for the entire package.
23 | """
24 | return Path(__file__).parent.absolute()
25 |
26 |
27 | output_dirs = [
28 | "results",
29 | ]
30 | output_files = [
31 | "*test*.pdf",
32 | ]
33 |
34 |
35 | @pytest.fixture(autouse=True)
36 | def default_setup_and_teardown():
37 | """
38 | Fixture that performs setup and teardown actions before and after each test function.
39 | It removes the output directories and files specified in 'output_dirs' and 'output_files' lists.
40 | """
41 | _remove_output_dirs_and_files()
42 | yield
43 | _remove_output_dirs_and_files()
44 |
45 |
46 | def _remove_output_dirs_and_files() -> None:
47 | """
48 | Helper function that removes the output directories and files specified in 'output_dirs' and 'output_files' lists.
49 | """
50 | for folder in output_dirs:
51 | rmtree(folder, ignore_errors=True)
52 | for pattern in output_files:
53 | for file in Path.cwd().glob(pattern):
54 | _file = Path(file)
55 | _file.unlink(missing_ok=True)
56 |
57 |
58 | @pytest.fixture(autouse=True)
59 | def setup_logging(caplog: pytest.LogCaptureFixture) -> None:
60 | """
61 | Fixture that sets up logging for each test function.
62 | It sets the log level to 'INFO' and clears the log capture.
63 | """
64 | caplog.set_level("INFO")
65 | caplog.clear()
66 |
67 |
68 | @pytest.fixture(autouse=True)
69 | def logger() -> logging.Logger:
70 | """Fixture that returns the logger object."""
71 | return logging.getLogger()
72 |
--------------------------------------------------------------------------------
/tests/data/example.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnv-opensource/mlfmu/4fdbc85ca1a75d5a74caa72f52608a074586d674/tests/data/example.onnx
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | # pyright: reportPrivateUsage=false
2 |
--------------------------------------------------------------------------------
/tests/test_working_directory/.gitignore:
--------------------------------------------------------------------------------
1 | *test*.pdf
2 |
--------------------------------------------------------------------------------
/tests/test_working_directory/test_config_file:
--------------------------------------------------------------------------------
1 | max_number_of_runs 3;
--------------------------------------------------------------------------------
/tests/test_working_directory/test_config_file.json:
--------------------------------------------------------------------------------
1 | {
2 | "max_number_of_runs":3
3 | }
--------------------------------------------------------------------------------
/tests/test_working_directory/test_config_file_empty:
--------------------------------------------------------------------------------
1 | // empty
--------------------------------------------------------------------------------
/tests/test_working_directory/test_config_file_empty.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/tests/types/test_fmu_component.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/utils/test_fmu_template.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 | from pathlib import Path
4 |
5 | import pytest
6 |
7 | from mlfmu.types.fmu_component import FmiModel
8 | from mlfmu.types.onnx_model import ONNXModel
9 | from mlfmu.utils.builder import format_template_data, validate_interface_spec
10 |
11 |
12 | @pytest.fixture(scope="session")
13 | def wind_generator_onnx() -> ONNXModel:
14 | return ONNXModel(Path.cwd().parent / "data" / "example.onnx", time_input=True)
15 |
16 |
17 | def test_valid_template_data(wind_generator_onnx: ONNXModel):
18 | valid_spec = {
19 | "name": "example",
20 | "version": "1.0",
21 | "inputs": [
22 | {"name": "inputs", "description": "My inputs", "agentInputIndexes": ["0:2"], "isArray": True, "length": 2}
23 | ],
24 | "outputs": [
25 | {
26 | "name": "outputs",
27 | "description": "My outputs",
28 | "agentOutputIndexes": ["0:2"],
29 | "isArray": True,
30 | "length": 2,
31 | }
32 | ],
33 | "states": [
34 | {"agentOutputIndexes": ["2:130"]},
35 | {"name": "state1", "startValue": 10.0, "agentOutputIndexes": ["0"]},
36 | {"name": "state2", "startValue": 180.0, "agentOutputIndexes": ["1"]},
37 | ],
38 | }
39 | _, model = validate_interface_spec(json.dumps(valid_spec))
40 | assert model is not None
41 |
42 | fmi_model = FmiModel(model=model)
43 | template_data = format_template_data(onnx=wind_generator_onnx, fmi_model=fmi_model, model_component=model)
44 |
45 | assert template_data["FmuName"] == "example"
46 | assert template_data["numFmuVariables"] == "6"
47 | assert template_data["numOnnxInputs"] == "2"
48 | assert template_data["numOnnxOutputs"] == "130"
49 | assert template_data["numOnnxStates"] == "130"
50 | assert template_data["onnxInputValueReferences"] == "0, 0, 1, 1"
51 | assert template_data["onnxOutputValueReferences"] == "0, 2, 1, 3"
52 |
53 |
54 | def test_template_data_invalid_input_size(wind_generator_onnx: ONNXModel):
55 | valid_spec = {
56 | "name": "example",
57 | "version": "1.0",
58 | "inputs": [
59 | {"name": "inputs", "description": "My inputs", "agentInputIndexes": ["0:2"], "isArray": True, "length": 2},
60 | {
61 | "name": "inputs2",
62 | "description": "My inputs 2",
63 | "agentInputIndexes": ["0:10"],
64 | "isArray": True,
65 | "length": 10,
66 | },
67 | ],
68 | "outputs": [
69 | {"name": "outputs", "description": "My outputs", "agentInputIndexes": ["0:2"], "isArray": True, "length": 2}
70 | ],
71 | "states": [
72 | {"agentOutputIndexes": ["2:130"]},
73 | {"name": "state1", "startValue": 10.0, "agentOutputIndexes": ["0"]},
74 | {"name": "state2", "startValue": 180.0, "agentOutputIndexes": ["1"]},
75 | ],
76 | }
77 |
78 | _, model = validate_interface_spec(json.dumps(valid_spec))
79 | assert model is not None
80 |
81 | fmi_model = FmiModel(model=model)
82 |
83 | with pytest.raises(ValueError) as exc_info:
84 | _ = format_template_data(onnx=wind_generator_onnx, fmi_model=fmi_model, model_component=model)
85 |
86 | assert exc_info.match(
87 | re.escape(
88 | "The number of total input indexes for all inputs and parameter in the interface file (=12) \
89 | cannot exceed the input size of the ml model (=2)"
90 | )
91 | )
92 |
93 |
94 | def test_template_data_invalid_output_size(wind_generator_onnx: ONNXModel):
95 | valid_spec = {
96 | "name": "example",
97 | "version": "1.0",
98 | "inputs": [
99 | {"name": "inputs", "description": "My inputs", "agentInputIndexes": ["0:2"], "isArray": True, "length": 2}
100 | ],
101 | "outputs": [
102 | {
103 | "name": "outputs",
104 | "description": "My outputs",
105 | "agentOutputIndexes": ["0:2"],
106 | "isArray": True,
107 | "length": 2,
108 | },
109 | {
110 | "name": "outputs2",
111 | "description": "My outputs 2",
112 | "agentOutputIndexes": ["0:200"],
113 | "isArray": True,
114 | "length": 200,
115 | },
116 | ],
117 | "states": [
118 | {"agentOutputIndexes": ["2:130"]},
119 | {"name": "state1", "startValue": 10.0, "agentOutputIndexes": ["0"]},
120 | {"name": "state2", "startValue": 180.0, "agentOutputIndexes": ["1"]},
121 | ],
122 | }
123 |
124 | _, model = validate_interface_spec(json.dumps(valid_spec))
125 | fmi_model = FmiModel(model=model)
126 |
127 | with pytest.raises(ValueError) as exc_info:
128 | _ = format_template_data(onnx=wind_generator_onnx, fmi_model=fmi_model, model_component=model)
129 |
130 | assert exc_info.match(
131 | re.escape(
132 | "The number of total output indexes for all outputs in the interface file (=202) \
133 | cannot exceed the output size of the ml model (=130)"
134 | )
135 | )
136 |
137 |
138 | def test_template_data_invalid_state_size(wind_generator_onnx: ONNXModel):
139 | valid_spec = {
140 | "name": "example",
141 | "version": "1.0",
142 | "inputs": [
143 | {"name": "inputs", "description": "My inputs", "agentInputIndexes": ["0:2"], "isArray": True, "length": 2}
144 | ],
145 | "outputs": [
146 | {"name": "outputs", "description": "My outputs", "agentInputIndexes": ["0:2"], "isArray": True, "length": 2}
147 | ],
148 | "states": [
149 | {"agentOutputIndexes": ["2:200"]},
150 | ],
151 | }
152 |
153 | _, model = validate_interface_spec(json.dumps(valid_spec))
154 | assert model is not None
155 |
156 | fmi_model = FmiModel(model=model)
157 |
158 | with pytest.raises(ValueError) as exc_info:
159 | _ = format_template_data(onnx=wind_generator_onnx, fmi_model=fmi_model, model_component=model)
160 |
161 | assert exc_info.match(
162 | re.escape(
163 | "The number of total output indexes for all states in the interface file (=198) \
164 | cannot exceed either the state input size (=130)"
165 | )
166 | )
167 |
--------------------------------------------------------------------------------
/tests/utils/test_interface_validation.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 | from pydantic import ValidationError
5 |
6 | from mlfmu.types.fmu_component import ModelComponent
7 | from mlfmu.utils.builder import validate_interface_spec
8 |
9 |
10 | def test_validate_simple_interface_spec():
11 | # Assuming validate_interface_spec takes a dictionary as input
12 | valid_spec = {
13 | "name": "example",
14 | "version": "1.0",
15 | "inputs": [{"name": "input1", "description": "My input1", "agentInputIndexes": ["0"], "type": "integer"}],
16 | "outputs": [{"name": "output1", "description": "My output1", "agentInputIndexes": ["0"]}],
17 | }
18 | error, model = validate_interface_spec(json.dumps(valid_spec))
19 | assert error is None
20 | assert isinstance(model, ModelComponent)
21 | assert model.name == "example"
22 | assert model.version == "1.0"
23 | assert model.inputs[0].name == "input1"
24 | assert model.inputs[0].type == "integer"
25 | assert model.outputs[0].name == "output1"
26 | assert model.outputs[0].type == "real"
27 |
28 |
29 | def test_validate_interface_spec_wrong_types():
30 | # Assuming validate_interface_spec returns False for invalid specs
31 | invalid_spec = {
32 | "name": "example",
33 | "version": "1.0",
34 | "inputs": [{"name": "input1", "type": "enum"}], # Missing enum type
35 | "outputs": [{"name": "output1", "type": "int"}], # Should be integer
36 | }
37 |
38 | with pytest.raises(ValidationError) as exc_info:
39 | _, _ = validate_interface_spec(json.dumps(invalid_spec))
40 |
41 | # Model type error as it's missing the agentInputIndexes
42 | assert exc_info.match("Input should be 'real', 'integer', 'string' or 'boolean'")
43 |
44 |
45 | def test_validate_unnamed_spec():
46 | invalid_spec = {
47 | "version": "1.0",
48 | "inputs": [{"name": "input1", "description": "My input1", "agentInputIndexes": ["0"], "type": "integer"}],
49 | "outputs": [{"name": "output1", "description": "My output1", "agentInputIndexes": ["0"]}],
50 | }
51 |
52 | with pytest.raises(ValidationError) as exc_info:
53 | _, _ = validate_interface_spec(json.dumps(invalid_spec))
54 |
55 | assert exc_info.match("Field required")
56 |
57 |
58 | def test_validate_invalid_agent_indices():
59 | invalid_spec = {
60 | "name": "example",
61 | "version": "1.0",
62 | "inputs": [
63 | {"name": "input1", "description": "My input1", "type": "integer", "agentInputIndexes": [0, ":10", "10.0"]}
64 | ], # Should be a stringified list of integers
65 | "outputs": [
66 | {"name": "output1", "description": "My output1", "agentOutputIndexes": ["0:a"]}
67 | ], # Should not have letters
68 | }
69 |
70 | with pytest.raises(ValidationError) as exc_info:
71 | _, _ = validate_interface_spec(json.dumps(invalid_spec))
72 |
73 | assert exc_info.match("Input should be a valid string")
74 | assert exc_info.match("String should match pattern")
75 | assert exc_info.match("4 validation errors for ModelComponent")
76 |
77 |
78 | def test_validate_default_parameters():
79 | invalid_spec = {
80 | "name": "example",
81 | "version": "1.0",
82 | "inputs": [
83 | {"name": "input1", "description": "My input1", "type": "integer", "agentInputIndexes": ["10"]}
84 | ], # Should be a stringified list of integers
85 | "outputs": [
86 | {"name": "output1", "description": "My output1", "agentOutputIndexes": ["0:10"]}
87 | ], # Should not have letters
88 | }
89 | error, model = validate_interface_spec(json.dumps(invalid_spec))
90 | assert error is None
91 | assert model is not None
92 |
93 | assert model.uses_time is False
94 | assert model.state_initialization_reuse is False
95 | assert model.name == "example"
96 | assert model.parameters == []
97 |
98 |
99 | def test_validate_internal_states():
100 | invalid_spec = {
101 | "name": "example",
102 | "version": "1.0",
103 | "inputs": [
104 | {"name": "input1", "description": "My input1", "type": "integer", "agentInputIndexes": ["10"]}
105 | ], # Should be a stringified list of integers
106 | "outputs": [
107 | {"name": "output1", "description": "My output1", "agentOutputIndexes": ["0:10"]}
108 | ], # Should not have letters
109 | "states": [
110 | {
111 | "name": "state1",
112 | "description": "My state1",
113 | "startValue": 10,
114 | "initializationVariable": "input1",
115 | "agentOutputIndexes": ["0:10"],
116 | },
117 | {"name": "state2", "description": "My state2", "agentOutputIndexes": ["0:10"]},
118 | {"name": "state3", "initializationVariable": "input1", "agentOutputIndexes": ["0:10"]},
119 | {"description": "My state4", "startValue": 10},
120 | ],
121 | }
122 | with pytest.raises(ValidationError) as exc_info:
123 | _, _ = validate_interface_spec(json.dumps(invalid_spec))
124 |
125 | assert exc_info.match(
126 | "Value error, Only one state initialization method is allowed to be used at a time: \
127 | initialization_variable cannot be set if either start_value or name is set."
128 | )
129 | assert exc_info.match(
130 | "Value error, name is set without start_value being set. \
131 | Both fields need to be set for the state initialization to be valid"
132 | )
133 | assert exc_info.match(
134 | "Value error, Only one state initialization method is allowed to be used at a time: \
135 | initialization_variable cannot be set if either start_value or name is set."
136 | )
137 | assert exc_info.match(
138 | "Value error, start_value is set without name being set. \
139 | Both fields need to be set for the state initialization to be valid"
140 | )
141 |
--------------------------------------------------------------------------------
/tests/utils/test_modelDescription_builder.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from mlfmu.types.fmu_component import FmiModel
4 | from mlfmu.utils.builder import validate_interface_spec
5 | from mlfmu.utils.fmi_builder import generate_model_description
6 |
7 |
8 | def test_generate_simple_model_description():
9 | valid_spec = {
10 | "name": "example",
11 | "version": "1.0",
12 | "inputs": [{"name": "input1", "description": "My input1", "agentInputIndexes": ["0"], "type": "integer"}],
13 | "outputs": [{"name": "output1", "description": "My output1", "agentInputIndexes": ["0"]}],
14 | }
15 | _, model = validate_interface_spec(json.dumps(valid_spec))
16 | assert model is not None
17 |
18 | fmi_model = FmiModel(model=model)
19 | xml_structure = generate_model_description(fmu_model=fmi_model)
20 | variables = xml_structure.findall(".//ScalarVariable")
21 |
22 | assert xml_structure.getroot().tag == "fmiModelDescription"
23 | assert variables[0].attrib["name"] == "input1"
24 | assert variables[0].attrib["causality"] == "input"
25 | assert variables[0].attrib["variability"] == "continuous"
26 | assert variables[0].attrib["description"] == "My input1"
27 | assert variables[0][0].tag == "Integer"
28 |
29 | assert variables[1].attrib["name"] == "output1"
30 | assert variables[1].attrib["causality"] == "output"
31 | assert variables[1].attrib["variability"] == "continuous"
32 | assert variables[1].attrib["description"] == "My output1"
33 | assert variables[1][0].tag == "Real"
34 |
35 |
36 | def test_generate_model_description_with_internal_state_params():
37 | valid_spec = {
38 | "name": "example",
39 | "version": "1.0",
40 | "states": [
41 | {
42 | "name": "state1",
43 | "description": "My state1",
44 | "startValue": 0.0,
45 | "type": "real",
46 | "agentOutputIndexes": ["0"],
47 | }
48 | ],
49 | "outputs": [{"name": "output1", "description": "My output1", "agentInputIndexes": ["0"]}],
50 | }
51 | _, model = validate_interface_spec(json.dumps(valid_spec))
52 | assert model is not None
53 |
54 | fmi_model = FmiModel(model=model)
55 | xml_structure = generate_model_description(fmu_model=fmi_model)
56 | variables = xml_structure.findall(".//ScalarVariable")
57 |
58 | assert xml_structure.getroot().tag == "fmiModelDescription"
59 |
60 | assert variables[0].attrib["name"] == "output1"
61 | assert variables[0].attrib["causality"] == "output"
62 |
63 | assert variables[1].attrib["name"] == "state1"
64 | assert variables[1].attrib["causality"] == "parameter"
65 | assert variables[1][0].tag == "Real"
66 | assert variables[1][0].attrib["start"] == "0.0"
67 |
68 |
69 | def test_generate_vector_ports():
70 | valid_spec = {
71 | "name": "example",
72 | "version": "1.0",
73 | "inputs": [
74 | {
75 | "name": "inputVector",
76 | "description": "My input1",
77 | "agentInputIndexes": ["0:5"],
78 | "type": "real",
79 | "isArray": True,
80 | "length": 5,
81 | }
82 | ],
83 | "outputs": [
84 | {
85 | "name": "outputVector",
86 | "description": "My output1",
87 | "agentInputIndexes": ["0:5"],
88 | "isArray": True,
89 | "length": 5,
90 | }
91 | ],
92 | }
93 | _, model = validate_interface_spec(json.dumps(valid_spec))
94 | assert model is not None
95 |
96 | fmi_model = FmiModel(model=model)
97 | xml_structure = generate_model_description(fmu_model=fmi_model)
98 | variables = xml_structure.findall(".//ScalarVariable")
99 |
100 | assert model
101 | assert variables[0].attrib["name"] == "inputVector[0]"
102 | assert variables[1].attrib["name"] == "inputVector[1]"
103 | assert variables[2].attrib["name"] == "inputVector[2]"
104 | assert variables[3].attrib["name"] == "inputVector[3]"
105 | assert variables[4].attrib["name"] == "inputVector[4]"
106 |
107 | assert variables[5].attrib["name"] == "outputVector[0]"
108 | assert variables[6].attrib["name"] == "outputVector[1]"
109 | assert variables[7].attrib["name"] == "outputVector[2]"
110 | assert variables[8].attrib["name"] == "outputVector[3]"
111 | assert variables[9].attrib["name"] == "outputVector[4]"
112 |
113 |
114 | def test_generate_model_description_with_start_value():
115 | valid_spec = {
116 | "name": "example",
117 | "version": "1.0",
118 | "usesTime": True,
119 | "inputs": [
120 | {
121 | "name": "input1",
122 | "description": "My input1",
123 | "agentInputIndexes": ["0"],
124 | "type": "integer",
125 | "startValue": 10,
126 | },
127 | {
128 | "name": "input2",
129 | "description": "My input2",
130 | "agentOutputIndexes": ["0"],
131 | "type": "boolean",
132 | "startValue": True,
133 | },
134 | {"name": "input3", "description": "My input3", "agentOutputIndexes": ["0"], "startValue": 10.0},
135 | ],
136 | }
137 | _, model = validate_interface_spec(json.dumps(valid_spec))
138 | assert model is not None
139 |
140 | fmi_model = FmiModel(model=model)
141 | xml_structure = generate_model_description(fmu_model=fmi_model)
142 | variables = xml_structure.findall(".//ScalarVariable")
143 |
144 | assert xml_structure.getroot().tag == "fmiModelDescription"
145 | assert variables[0].attrib["name"] == "input1"
146 | assert variables[0].attrib["causality"] == "input"
147 | assert variables[0][0].tag == "Integer"
148 | assert variables[0][0].attrib["start"] == "10"
149 |
150 | assert variables[1].attrib["name"] == "input2"
151 | assert variables[1].attrib["causality"] == "input"
152 | assert variables[1][0].tag == "Boolean"
153 | assert variables[1][0].attrib["start"] == "True"
154 |
155 | assert variables[2].attrib["name"] == "input3"
156 | assert variables[2].attrib["causality"] == "input"
157 | assert variables[2][0].tag == "Real"
158 | assert variables[2][0].attrib["start"] == "10.0"
159 |
160 |
161 | def test_generate_model_description_output():
162 | valid_spec = {
163 | "name": "example",
164 | "version": "1.0",
165 | "usesTime": True,
166 | "inputs": [
167 | {
168 | "name": "input1",
169 | "description": "My input1",
170 | "agentInputIndexes": ["0"],
171 | "type": "integer",
172 | "startValue": 10,
173 | },
174 | {
175 | "name": "input2",
176 | "description": "My input2",
177 | "agentOutputIndexes": ["0"],
178 | "type": "boolean",
179 | "startValue": True,
180 | },
181 | {"name": "input3", "description": "My input3", "agentOutputIndexes": ["0"], "startValue": 10.0},
182 | ],
183 | "outputs": [
184 | {"name": "output1", "description": "My output1", "agentInputIndexes": ["0"], "type": "real"},
185 | {"name": "output1", "description": "My output1", "agentInputIndexes": ["0"], "type": "real"},
186 | ],
187 | }
188 | _, model = validate_interface_spec(json.dumps(valid_spec))
189 | assert model is not None
190 |
191 | fmi_model = FmiModel(model=model)
192 | xml_structure = generate_model_description(fmu_model=fmi_model)
193 | variables = xml_structure.findall(".//ScalarVariable")
194 | output_variables = [var for var in variables if var.attrib.get("causality") == "output"]
195 | outputs_registered = xml_structure.findall(".//Outputs/Unknown")
196 |
197 | # The index should be the valueReference + 1
198 | assert int(output_variables[0].attrib["valueReference"]) + 1 == int(outputs_registered[0].attrib["index"])
199 | assert int(output_variables[1].attrib["valueReference"]) + 1 == int(outputs_registered[1].attrib["index"])
200 |
--------------------------------------------------------------------------------