├── .devcontainer └── devcontainer.json ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── commit.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cliff.toml ├── docs └── FMI_for_ModelExchange_and_CoSimulation_v2.0.pdf ├── fmi-schema ├── Cargo.toml ├── README.md ├── src │ ├── date_time.rs │ ├── fmi2 │ │ ├── attribute_groups.rs │ │ ├── interface_type.rs │ │ ├── mod.rs │ │ ├── model_description.rs │ │ ├── scalar_variable.rs │ │ ├── type.rs │ │ ├── unit.rs │ │ └── variable_dependency.rs │ ├── fmi3 │ │ ├── annotation.rs │ │ ├── attribute_groups.rs │ │ ├── interface_type.rs │ │ ├── mod.rs │ │ ├── model_description.rs │ │ ├── type.rs │ │ ├── unit.rs │ │ ├── variable.rs │ │ └── variable_dependency.rs │ ├── lib.rs │ ├── minimal.rs │ ├── traits.rs │ └── variable_counts.rs └── tests │ ├── FMI2.xml │ ├── FMI3.xml │ ├── test_fmi2.rs │ ├── test_fmi3.rs │ └── test_minimal.rs ├── fmi-sim ├── Cargo.lock ├── Cargo.toml ├── README.md ├── examples │ └── bouncing_ball.rs ├── src │ ├── lib.rs │ ├── main.old.rs │ ├── main.rs │ ├── options.rs │ └── sim │ │ ├── fmi2 │ │ ├── cs.rs │ │ ├── io.rs │ │ ├── me.rs │ │ ├── mod.rs │ │ └── schema.rs │ │ ├── fmi3 │ │ ├── cs.rs │ │ ├── io.rs │ │ ├── me.rs │ │ ├── mod.rs │ │ └── schema.rs │ │ ├── interpolation.rs │ │ ├── io.rs │ │ ├── me.rs │ │ ├── mod.rs │ │ ├── params.rs │ │ ├── solver │ │ ├── euler.rs │ │ └── mod.rs │ │ ├── traits.rs │ │ └── util.rs └── tests │ ├── data │ ├── bouncing_ball_cs_expected.csv │ ├── bouncing_ball_me_expected.csv │ ├── feedthrough_in.csv │ ├── feedthrough_in2.csv │ └── test.ipynb │ └── test_fmi_sim.rs ├── fmi-sys ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── fmi2 │ ├── logger.c │ ├── logger.rs │ └── mod.rs │ ├── fmi3 │ └── mod.rs │ └── lib.rs ├── fmi-test-data ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── registry.txt ├── fmi ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── fmi2 │ ├── import.rs │ ├── instance │ │ ├── co_simulation.rs │ │ ├── common.rs │ │ ├── mod.rs │ │ ├── model_exchange.rs │ │ └── traits.rs │ ├── mod.rs │ └── variable.rs │ ├── fmi3 │ ├── import.rs │ ├── instance │ │ ├── co_simulation.rs │ │ ├── common.rs │ │ ├── mod.rs │ │ ├── model_exchange.rs │ │ ├── scheduled_execution.rs │ │ └── traits.rs │ ├── logger.rs │ ├── mod.rs │ ├── model.rs │ └── model2.rs │ ├── import.rs │ ├── lib.rs │ └── traits.rs ├── renovate.json ├── rustfmt.toml └── tests ├── test_imports.rs ├── test_instances_fmi2.rs └── test_instances_fmi3.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/rust:1": {} 5 | }, 6 | "hostRequirements": { 7 | "cpus": 4 8 | }, 9 | "waitFor": "onCreateCommand", 10 | "updateContentCommand": "sudo apt udpate && sudo apt install liblapack3", 11 | "postCreateCommand": "rustc --version", 12 | "postAttachCommand": {}, 13 | "customizations": { 14 | "codespaces": { 15 | "openFiles": [ 16 | "src/lib.rs" 17 | ] 18 | }, 19 | "vscode": { 20 | "extensions": [ 21 | "rust-lang.rust-analyzer" 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.fmu filter=lfs diff=lfs merge=lfs -text 2 | *.pdf filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | on: 3 | push: 4 | branches: ["main"] 5 | 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | toolchain: [stable, nightly] 17 | os: [windows-latest, ubuntu-latest, macos-latest] 18 | 19 | # Only test nightly on Linux 20 | exclude: 21 | - os: macos-latest 22 | toolchain: nightly 23 | - os: windows-latest 24 | toolchain: nightly 25 | 26 | runs-on: ${{ matrix.os }} 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | lfs: true 32 | submodules: true 33 | 34 | - uses: swatinem/rust-cache@v2 35 | 36 | - name: fetch Rust 37 | uses: dtolnay/rust-toolchain@master 38 | with: 39 | toolchain: ${{ matrix.toolchain }} 40 | 41 | - name: toolchain version 42 | run: cargo -vV 43 | 44 | - uses: giraffate/clippy-action@v1 45 | with: 46 | reporter: 'github-pr-review' 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | clippy_flags: -- -Dwarnings 49 | 50 | - name: build 51 | run: cargo build --all --verbose 52 | 53 | - name: test 54 | run: cargo test --all --verbose -------------------------------------------------------------------------------- /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | 8 | jobs: 9 | commitlint: 10 | runs-on: ubuntu-latest 11 | name: Commitlint 12 | steps: 13 | - name: Run commitlint 14 | uses: opensource-nepal/commitlint@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode/cquery_cached_index -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fmi-standard/fmi2"] 2 | path = fmi-sys/fmi-standard2 3 | url = https://github.com/modelica/fmi-standard.git 4 | [submodule "fmi-standard/fmi3"] 5 | path = fmi-sys/fmi-standard3 6 | url = https://github.com/modelica/fmi-standard.git 7 | -------------------------------------------------------------------------------- /.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 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'fmi_check'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=fmi_check", 15 | "--package=fmi_check" 16 | ], 17 | "filter": { 18 | "name": "fmi_check", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'fmi_check'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=fmi_check", 34 | "--package=fmi_check" 35 | ], 36 | "filter": { 37 | "name": "fmi_check", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug unit tests in library 'fmi'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--lib", 53 | "--package=fmi" 54 | ], 55 | "filter": { 56 | "name": "fmi", 57 | "kind": "lib" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | }, 63 | { 64 | "type": "lldb", 65 | "request": "launch", 66 | "name": "Debug integration test 'test_fmi3'", 67 | "cargo": { 68 | "args": [ 69 | "test", 70 | "--no-run", 71 | "--test=test_fmi3", 72 | "--package=fmi" 73 | ], 74 | "filter": { 75 | "name": "test_fmi3", 76 | "kind": "test" 77 | } 78 | }, 79 | "args": [], 80 | "cwd": "${workspaceFolder}" 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.1] - 2024-10-30 9 | 10 | ### Added 11 | 12 | - Add Windows and MacOS builds to GH CI (#66) 13 | - Add renovate.json (#10) 14 | 15 | ### Changed 16 | 17 | - Add PR Conventional Commit Validation workflow (#68) 18 | - Update Cargo.lock (#64) 19 | - Update Rust crate float-cmp to 0.10 (#60) 20 | - Replace in-repo copies of fmi-standard header files with git submodules. (#62) 21 | - Update Rust crate built to v0.7.4 (#50) 22 | - Update Rust crate bindgen to 0.70 (#53) 23 | - Update Rust crate libloading to v0.8.5 (#48) 24 | - Update Rust crate url to v2.5.2 (#47) 25 | - Update Rust crate cc to v1.1.21 (#46) 26 | - Update Rust crate zip to v2 (#43) 27 | - Use correctly represented resource paths in fmi2 and fmi3. (#54) 28 | - Update Rust crate dependencies (#44) 29 | - Update Rust crate rstest to 0.21 (#42) 30 | - Update Rust crate anyhow to v1.0.86 (#36) 31 | - Update Rust crate cc to v1.0.98 (#35) 32 | - Update Rust crate semver to v1.0.23 (#37) 33 | - Update Rust crate thiserror to v1.0.61 (#38) 34 | - Update Rust crate libc to v0.2.155 (#40) 35 | - Update Rust crate built to v0.7.3 (#41) 36 | - Update Rust crate rstest to 0.19 (#30) 37 | - Update Rust crate thiserror to v1.0.59 (#29) 38 | - Update Rust crate test-log to v0.2.16 (#28) 39 | - Update Rust crate libc to v0.2.154 (#27) 40 | - Update Rust crate chrono to v0.4.38 (#26) 41 | - Update Rust crate cc to v1.0.96 (#25) 42 | - Update Rust crate built to v0.7.2 (#24) 43 | - Update Rust crate assert_cmd to 2.0.14 (#12) 44 | - Update Rust crate anyhow to 1.0.82 (#11) 45 | 46 | ## [0.4.0] - 2024-04-16 47 | 48 | ### Added 49 | 50 | - Support FMI2.0 in `fmi-sim` (#9) 51 | - Support output files in fmi-sim. 52 | - Add functions to query number of continous state and event indicator values 53 | - Add thiserror to crate root 54 | 55 | ### Changed 56 | 57 | - Prepare fmi-sim for release, added bouncing_ball example 58 | - Prepare for release 59 | - Sim mostly working (#8) 60 | - Initial ScheduledExecution interface 61 | - Refactoring and error cleanup 62 | - Almost there 63 | - Almost there 64 | - Switch to clap, ME work-in-progress 65 | - Traits refactor (#7) 66 | - Initial reference testing (#6) 67 | - Fmi-check (#4) 68 | - Total Refactor, support for FMI3 (#3) 69 | - Use lfs in ci checkout 70 | - Install lapack3 in ci workflow 71 | 72 | ### Fixed 73 | 74 | - Fix ci workflow branch 75 | 76 | ## [0.2.2] - 2023-11-02 77 | 78 | ### Added 79 | 80 | - Added workflows, devcontainer, cargo-dist 81 | - Added gitpod config, fix gitlab-ci 82 | - Added rustfmt.toml, applied 83 | - Add CoSim doStep, var getters/setters, enumeration type 84 | 85 | ### Changed 86 | 87 | - 0.2.2 88 | - Bumped deps, removed gitlab config 89 | - Update README.md 90 | - Merge branch 'fixLog' into 'master' 91 | - Don't reuse va_args 92 | - * Determine FMI_PLATFORM path at compile-time, as done in FMILibrary. 93 | - Merge branch '2023-update' into 'master' 94 | - Updates for rust2021 edition, bump deps 95 | - Fixed misnamed .gitpod.dockerfile 96 | - Merge branch 'gitpod-config' into 'master' 97 | - - Moved fmi_check into it's own crate 98 | - Got rid of unecessary use of Rc 99 | - Patch release 0.2.1 100 | - Merge branch 'hugwijst-master-patch-99090' into 'master' 101 | - Fix buffer overflow on large log messages. 102 | - Bump version to 0.2.0 103 | - Merge branch 'wip' into 'master' 104 | - * Additional CS support in fmi_check example 105 | - Ran rustfmt 106 | - Merge branch '1-casting-error-prevent-compilation' into 'master' 107 | - Resolve "casting error prevent compilation" 108 | - Merge branch 'fix_build' into 'master' 109 | - * Fix codecov from copy/paste 110 | - * Added lfs to gitlab ci 111 | - * Added .gitlab-ci.yml 112 | - * Initial Gitlab import 113 | 114 | [0.4.1]: https://github.com///compare/v0.4.0..v0.4.1 115 | [0.4.0]: https://github.com///compare/v0.2.2..v0.4.0 116 | 117 | 118 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["fmi", "fmi-schema", "fmi-sim", "fmi-sys", "fmi-test-data"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | license = "MIT OR Apache-2.0" 7 | edition = "2021" 8 | authors = ["John Hughes "] 9 | repository = "https://github.com/jondo2010/rust-fmi" 10 | keywords = ["model", "ode", "modelica"] 11 | categories = ["science", "simulation"] 12 | homepage = "https://github.com/jondo2010/rust-fmi" 13 | include = [ 14 | "Cargo.toml", 15 | "benches/*.rs", 16 | "examples/*.rs", 17 | "src/**/*.rs", 18 | "tests/*.rs", 19 | "build.rs", 20 | ] 21 | 22 | [workspace.dependencies] 23 | anyhow = { version = "1.0.82" } 24 | arrow = { version = "50.0" } 25 | document-features = "0.2" 26 | fmi = { path = "fmi", version = "0.4.1" } 27 | fmi-schema = { path = "fmi-schema", version = "0.2.1", default_features = false } 28 | fmi-sim = { path = "fmi-sim", version = "0.1.1" } 29 | fmi-sys = { path = "fmi-sys", version = "0.1.2" } 30 | fmi-test-data = { path = "fmi-test-data", version = "0.1.0" } 31 | itertools = "0.14" 32 | libloading = "0.8" 33 | tempfile = "3.1" 34 | test-log = { version = "0.2", features = ["trace"] } 35 | thiserror = "1.0" 36 | zip = "2.0" 37 | 38 | [patch.crates-io] 39 | #arrow = { git = "https://github.com/jondo2010/arrow-rs.git", branch = "fine_grained_integer_inference" } 40 | 41 | [package] 42 | name = "fmi-workspace" 43 | version = "0.0.0" 44 | publish = false 45 | 46 | [dev-dependencies] 47 | fmi = { workspace = true } 48 | fmi-test-data = { workspace = true } 49 | test-log = { workspace = true } 50 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 John Hughes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A native Rust interface to FMI 2 | 3 | [github](https://github.com/jondo2010/rust-fmi) 4 | [crates.io](https://crates.io/crates/fmi) 5 | [docs.rs](https://docs.rs/fmi) 6 | [build status](https://github.com/jondo2010/rust-fmi/actions?query=branch%3Amain) 7 | 8 | A Rust interface to FMUs (Functional Mockup Units) that follow the FMI Standard. 9 | 10 | See [http://www.fmi-standard.org](http://www.fmi-standard.org) 11 | 12 | This repository is composed of the following crates: 13 | 14 | | Crate | Description | Latest API Docs | README | 15 | | --------------- | -------------------------------------------------- | ---------------------------------------------- | ----------------------------- | 16 | | `fmi` | Core functionality for importing and excuting FMUs | [docs.rs](https://docs.rs/fmi/latest) | [(README)][fmi-readme] | 17 | | `fmi-sys` | Raw generated Rust bindings to the FMI API | [docs.rs](https://docs.rs/fmi-sys/latest) | [(README)][fmi-sys-readme] | 18 | | `fmi-schema` | XML parsing of the FMU Model Description | [docs.rs](https://docs.rs/fmi-schema/latest) | [(README)][fmi-schema-readme] | 19 | | `fmi-sim` | Work-in-progress FMU Simulation master | [docs.rs](https://docs.rs/fmi-sim/latest) | [(README)][fmi-sim-readme] | 20 | | `fmi-test-data` | Reference FMUs for testing | [docs.rs](https//docs.rs/fmi-test-data/latest) | [(README)][fmi-test-data] | 21 | 22 | ## License 23 | 24 | Licensed under either of 25 | * Apache License, Version 2.0 26 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 27 | * MIT license 28 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 29 | at your option. 30 | 31 | ## Contribution 32 | 33 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 34 | 35 | [fmi-readme]: fmi/README.md 36 | [fmi-schema-readme]: fmi-schema/README.md 37 | [fmi-sys-readme]: fmi-sys/README.md 38 | [fmi-sim-readme]: fmi-sim/README.md 39 | [fmi-test-data]: fmi-test-data/README.md -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [changelog] 5 | # template for the changelog header 6 | header = """ 7 | # Changelog\n 8 | All notable changes to this project will be documented in this file. 9 | 10 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 11 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n 12 | """ 13 | # template for the changelog body 14 | # https://keats.github.io/tera/docs/#introduction 15 | body = """ 16 | {% if version -%} 17 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 18 | {% else -%} 19 | ## [Unreleased] 20 | {% endif -%} 21 | {% for group, commits in commits | group_by(attribute="group") %} 22 | ### {{ group | upper_first }} 23 | {% for commit in commits %} 24 | - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\ 25 | {% endfor %} 26 | {% endfor %}\n 27 | """ 28 | # template for the changelog footer 29 | footer = """ 30 | {% for release in releases -%} 31 | {% if release.version -%} 32 | {% if release.previous.version -%} 33 | [{{ release.version | trim_start_matches(pat="v") }}]: \ 34 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 35 | /compare/{{ release.previous.version }}..{{ release.version }} 36 | {% endif -%} 37 | {% else -%} 38 | [unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 39 | /compare/{{ release.previous.version }}..HEAD 40 | {% endif -%} 41 | {% endfor %} 42 | 43 | """ 44 | # remove the leading and trailing whitespace from the templates 45 | trim = true 46 | 47 | [git] 48 | # parse the commits based on https://www.conventionalcommits.org 49 | conventional_commits = true 50 | # filter out the commits that are not conventional 51 | filter_unconventional = false 52 | # regex for parsing and grouping commits 53 | commit_parsers = [ 54 | { message = "^[a|A]dd", group = "Added" }, 55 | { message = "^[s|S]upport", group = "Added" }, 56 | { message = "^[r|R]emove", group = "Removed" }, 57 | { message = "^.*: add", group = "Added" }, 58 | { message = "^.*: support", group = "Added" }, 59 | { message = "^.*: remove", group = "Removed" }, 60 | { message = "^.*: delete", group = "Removed" }, 61 | { message = "^test", group = "Fixed" }, 62 | { message = "^fix", group = "Fixed" }, 63 | { message = "^.*: fix", group = "Fixed" }, 64 | { message = "^.*", group = "Changed" }, 65 | ] 66 | # filter out the commits that are not matched by commit parsers 67 | filter_commits = false 68 | # sort the tags topologically 69 | topo_order = false 70 | # sort the commits inside sections by oldest/newest order 71 | sort_commits = "newest" 72 | -------------------------------------------------------------------------------- /docs/FMI_for_ModelExchange_and_CoSimulation_v2.0.pdf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9b4b452310b73e151588968ee0a365cd52b75e198d4223479914c239b2d876cf 3 | size 1836801 4 | -------------------------------------------------------------------------------- /fmi-schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmi-schema" 3 | version = "0.2.1" 4 | description = "XML schema support for FMI 2.0 and 3.0" 5 | readme = "README.md" 6 | authors.workspace = true 7 | categories.workspace = true 8 | edition.workspace = true 9 | homepage.workspace = true 10 | include.workspace = true 11 | keywords.workspace = true 12 | license.workspace = true 13 | publish = true 14 | repository.workspace = true 15 | 16 | [features] 17 | default = ["fmi2", "fmi3"] 18 | ## Enable support for FMI 2.0 19 | fmi2 = [] 20 | ## Enable support for FMI 3.0 21 | fmi3 = [] 22 | ## Enable support for Apache Arrow Schema 23 | arrow = ["dep:arrow"] 24 | 25 | [dependencies] 26 | arrow = { workspace = true, optional = true } 27 | chrono = { version = "0.4" } 28 | document-features = { workspace = true } 29 | itertools = "0.14" 30 | lenient_semver = "0.4" 31 | semver = "1.0" 32 | thiserror = { workspace = true } 33 | yaserde = "0.9.2" 34 | yaserde_derive = "0.9.2" 35 | -------------------------------------------------------------------------------- /fmi-schema/README.md: -------------------------------------------------------------------------------- 1 | # fmi-schema 2 | 3 | [github](https://github.com/jondo2010/rust-fmi) 4 | [crates.io](https://crates.io/crates/fmi-schema) 5 | [docs.rs](https://docs.rs/fmi-schema) 6 | [build status](https://github.com/jondo2010/rust-fmi/actions?query=branch%3Amain) 7 | 8 | XML schema support for FMI 2.0 and 3.0. This crate is part of [rust-fmi](https://github.com/jondo2010/rust-fmi). 9 | 10 | The reference XSI can be found at [https://fmi-standard.org/downloads](https://fmi-standard.org/downloads). 11 | 12 | ## Determining the FMI version 13 | 14 | FMI 2.0 and 3.0 have different XML schemas. 15 | 16 | The FMI version can initially be determined in a non-specific way by using [`minimal::ModelDescription`]. 17 | 18 | ## Example 19 | 20 | ```rust,no_run 21 | # use std::str::FromStr; 22 | let md = fmi_schema::fmi3::Fmi3ModelDescription::from_str( 23 | std::fs::read_to_string("tests/FMI3.xml").unwrap().as_str(), 24 | ) 25 | .unwrap(); 26 | println!("{}", md.model_name); 27 | ``` 28 | 29 | ## License 30 | 31 | Licensed under either of 32 | 33 | * Apache License, Version 2.0 34 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 35 | * MIT license 36 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 37 | 38 | at your option. 39 | 40 | ## Contribution 41 | 42 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 43 | -------------------------------------------------------------------------------- /fmi-schema/src/date_time.rs: -------------------------------------------------------------------------------- 1 | //! DateTime support for FMI schema. 2 | 3 | /// A wrapper around `chrono::DateTime` that implements `FromStr` for `xsd:dateTime`. 4 | #[derive(Debug, Clone, PartialEq)] 5 | pub struct DateTime(chrono::DateTime); 6 | 7 | impl std::fmt::Display for DateTime { 8 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 9 | self.0.to_rfc3339().fmt(f) 10 | } 11 | } 12 | 13 | impl std::str::FromStr for DateTime { 14 | type Err = chrono::format::ParseError; 15 | 16 | // Note: 17 | // `parse_from_rfc3339` parses an RFC 3339 and ISO 8601 date and time string. 18 | // XSD follows ISO 8601, which allows no time zone at the end of literal. 19 | // Since RFC 3339 does not allow such behavior, the function tries to add 20 | // 'Z' (which equals "+00:00") in case there is no timezone provided. 21 | fn from_str(s: &str) -> Result { 22 | let tz_provided = s.ends_with('Z') || s.contains('+') || s.matches('-').count() == 3; 23 | let s_with_timezone = if tz_provided { 24 | s.to_string() 25 | } else { 26 | format!("{}Z", s) 27 | }; 28 | match chrono::DateTime::parse_from_rfc3339(&s_with_timezone) { 29 | Ok(cdt) => Ok(DateTime(cdt)), 30 | Err(err) => Err(err), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi2/attribute_groups.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | #[derive(Default, Debug, PartialEq, YaSerialize, YaDeserialize)] 4 | pub struct RealAttributes { 5 | #[yaserde(attribute)] 6 | pub quantity: Option, 7 | 8 | #[yaserde(attribute)] 9 | pub unit: Option, 10 | 11 | /// Default display unit, provided the conversion of values in "unit" to values in 12 | /// "displayUnit" is defined in UnitDefinitions / Unit / DisplayUnit. 13 | #[yaserde(attribute, rename = "displayUnit")] 14 | pub display_unit: Option, 15 | 16 | /// If relativeQuantity=true, offset for displayUnit must be ignored. 17 | #[yaserde(attribute, rename = "relativeQuantity")] 18 | pub relative_quantity: bool, 19 | 20 | #[yaserde(attribute, rename = "min")] 21 | pub min: Option, 22 | 23 | /// max >= min required 24 | #[yaserde(attribute, rename = "max")] 25 | pub max: Option, 26 | 27 | /// nominal >= min and <= max required 28 | #[yaserde(attribute, rename = "nominal")] 29 | pub nominal: Option, 30 | 31 | /// Set to true, e.g., for crank angle. If true and variable is a state, relative tolerance 32 | /// should be zero on this variable. 33 | #[yaserde(attribute, rename = "unbounded")] 34 | pub unbounded: bool, 35 | } 36 | 37 | #[derive(Default, Debug, PartialEq, YaSerialize, YaDeserialize)] 38 | pub struct IntegerAttributes { 39 | pub quantity: Option, 40 | 41 | #[yaserde(attribute, rename = "min")] 42 | pub min: Option, 43 | 44 | /// max >= min required 45 | #[yaserde(attribute, rename = "max")] 46 | pub max: Option, 47 | } 48 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi2/interface_type.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | #[derive(Default, Debug, YaSerialize, YaDeserialize)] 4 | #[yaserde(tag = "File")] 5 | pub struct File { 6 | /// Name of the file including the path relative to the sources directory, using the forward 7 | /// slash as separator (for example: name = "myFMU.c"; name = "modelExchange/solve.c") 8 | #[yaserde(attribute)] 9 | pub name: String, 10 | } 11 | 12 | #[derive(Default, Debug, YaSerialize, YaDeserialize)] 13 | #[yaserde(tag = "SourceFiles")] 14 | pub struct SourceFiles { 15 | #[yaserde(rename = "File")] 16 | pub files: Vec, 17 | } 18 | 19 | /// The FMU includes a model or the communication to a tool that provides a model. The environment 20 | /// provides the simulation engine for the model. 21 | #[derive(Default, Debug, YaSerialize, YaDeserialize)] 22 | pub struct ModelExchange { 23 | /// Short class name according to C-syntax 24 | #[yaserde(attribute, rename = "modelIdentifier")] 25 | pub model_identifier: String, 26 | 27 | /// If true, a tool is needed to execute the model and the FMU just contains the communication 28 | /// to this tool. 29 | #[yaserde(attribute, rename = "needsExecutionTool")] 30 | pub needs_execution_tool: bool, 31 | 32 | #[yaserde(attribute, rename = "completedIntegratorStepNotNeeded")] 33 | pub completed_integrator_step_not_needed: bool, 34 | 35 | #[yaserde(attribute, rename = "canBeInstantiatedOnlyOncePerProcess")] 36 | pub can_be_instantiated_only_once_per_process: bool, 37 | 38 | #[yaserde(attribute, rename = "canNotUseMemoryManagementFunctions")] 39 | pub can_not_use_memory_management_functions: bool, 40 | 41 | #[yaserde(attribute, rename = "canGetAndSetFMUstate")] 42 | pub can_get_and_set_fmu_state: bool, 43 | 44 | #[yaserde(attribute, rename = "canSerializeFMUstate")] 45 | pub can_serialize_fmu_state: bool, 46 | 47 | /// If true, the directional derivative of the equations can be computed with 48 | /// fmi2GetDirectionalDerivative 49 | #[yaserde(attribute, rename = "providesDirectionalDerivative")] 50 | pub provides_directional_derivative: bool, 51 | 52 | /// List of source file names that are present in the "sources" directory of the FMU and need 53 | /// to be compiled in order to generate the binary of the FMU (only meaningful for source 54 | /// code FMUs). 55 | #[yaserde(rename = "SourceFiles")] 56 | pub source_files: SourceFiles, 57 | } 58 | 59 | #[derive(Default, Debug, YaSerialize, YaDeserialize)] 60 | pub struct CoSimulation { 61 | /// Short class name according to C-syntax 62 | #[yaserde(attribute, rename = "modelIdentifier")] 63 | pub model_identifier: String, 64 | 65 | /// If true, a tool is needed to execute the model and the FMU just contains the communication 66 | /// to this tool. 67 | #[yaserde(attribute, rename = "needsExecutionTool")] 68 | pub needs_execution_tool: bool, 69 | 70 | #[yaserde(attribute, rename = "canHandleVariableCommunicationStepSize")] 71 | pub can_handle_variable_communication_step_size: bool, 72 | 73 | #[yaserde(attribute, rename = "canInterpolateInputs")] 74 | pub can_interpolate_inputs: bool, 75 | 76 | #[yaserde(attribute, rename = "maxOutputDerivativeOrder")] 77 | pub max_output_derivative_order: u32, 78 | 79 | #[yaserde(attribute, rename = "canRunAsynchronuously")] 80 | pub can_run_asynchronuously: bool, 81 | 82 | #[yaserde(attribute, rename = "canBeInstantiatedOnlyOncePerProcess")] 83 | pub can_be_instantiated_only_once_per_process: bool, 84 | 85 | #[yaserde(attribute, rename = "canNotUseMemoryManagementFunctions")] 86 | pub can_not_use_memory_management_functions: bool, 87 | 88 | #[yaserde(attribute, rename = "canGetAndSetFMUstate")] 89 | pub can_get_and_set_fmu_state: bool, 90 | 91 | #[yaserde(attribute, rename = "canSerializeFMUstate")] 92 | pub can_serialize_fmu_state: bool, 93 | 94 | /// Directional derivatives at communication points 95 | #[yaserde(attribute, rename = "providesDirectionalDerivative")] 96 | pub provides_directional_derivative: bool, 97 | 98 | /// List of source file names that are present in the "sources" directory of the FMU and need 99 | /// to be compiled in order to generate the binary of the FMU (only meaningful for source 100 | /// code FMUs). 101 | #[yaserde(rename = "SourceFiles")] 102 | pub source_files: SourceFiles, 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use crate::fmi2::ModelExchange; 108 | 109 | #[test] 110 | fn test_model_exchange() { 111 | let s = r##""##; 112 | let me: ModelExchange = yaserde::de::from_str(s).unwrap(); 113 | assert!(me.model_identifier == "MyLibrary_SpringMassDamper"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi2/type.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | use super::attribute_groups::{IntegerAttributes, RealAttributes}; 4 | 5 | #[derive(Debug, PartialEq, YaSerialize, YaDeserialize)] 6 | pub enum SimpleTypeElement { 7 | #[yaserde(flatten)] 8 | Real(RealAttributes), 9 | #[yaserde(flatten)] 10 | Integer(IntegerAttributes), 11 | #[yaserde()] 12 | Boolean, 13 | #[yaserde()] 14 | String, 15 | #[yaserde()] 16 | Enumeration, 17 | } 18 | 19 | impl Default for SimpleTypeElement { 20 | fn default() -> Self { 21 | Self::Real(RealAttributes::default()) 22 | } 23 | } 24 | 25 | #[derive(Default, Debug, PartialEq, YaSerialize, YaDeserialize)] 26 | #[yaserde()] 27 | /// Type attributes of a scalar variable 28 | pub struct SimpleType { 29 | #[yaserde(flatten)] 30 | pub elem: SimpleTypeElement, 31 | 32 | #[yaserde(attribute)] 33 | /// Name of SimpleType element. "name" must be unique with respect to all other elements of the 34 | /// TypeDefinitions list. Furthermore, "name" of a SimpleType must be different to all 35 | /// "name"s of ScalarVariable. 36 | pub name: String, 37 | 38 | #[yaserde(attribute)] 39 | /// Description of the SimpleType 40 | pub description: Option, 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use crate::fmi2::{RealAttributes, SimpleTypeElement}; 46 | 47 | use super::SimpleType; 48 | 49 | #[test] 50 | fn test_simple_type() { 51 | let xml = r#" 52 | 53 | 54 | "#; 55 | 56 | let simple_type: SimpleType = yaserde::de::from_str(xml).unwrap(); 57 | assert_eq!(simple_type.name, "Acceleration"); 58 | assert_eq!(simple_type.description, None); 59 | assert_eq!( 60 | simple_type.elem, 61 | SimpleTypeElement::Real(RealAttributes { 62 | quantity: Some("Acceleration".to_owned()), 63 | unit: Some("m/s2".to_owned()), 64 | ..Default::default() 65 | }) 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi2/unit.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | // use super::Annotations; 4 | 5 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 6 | #[yaserde(rename = "Unit")] 7 | /// Unit definition (with respect to SI base units) and default display units 8 | pub struct Fmi2Unit { 9 | #[yaserde(attribute)] 10 | pub name: String, 11 | #[yaserde(rename = "BaseUnit")] 12 | /// BaseUnit_value = factor*Unit_value + offset 13 | pub base_unit: Option, 14 | #[yaserde(rename = "DisplayUnit")] 15 | pub display_unit: Vec, 16 | //#[yaserde(= "Annotations")] 17 | // pub annotations: Option, 18 | } 19 | 20 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 21 | pub struct BaseUnit { 22 | #[yaserde(attribute, rename = "kg")] 23 | /// Exponent of SI base unit "kg" 24 | pub kg: Option, 25 | #[yaserde(attribute, rename = "m")] 26 | /// Exponent of SI base unit "m" 27 | pub m: Option, 28 | #[yaserde(attribute, rename = "s")] 29 | pub s: Option, 30 | #[yaserde(attribute, rename = "A")] 31 | pub a: Option, 32 | #[yaserde(attribute, rename = "K")] 33 | pub k: Option, 34 | #[yaserde(attribute, rename = "mol")] 35 | pub mol: Option, 36 | #[yaserde(attribute, rename = "cd")] 37 | pub cd: Option, 38 | #[yaserde(attribute, rename = "rad")] 39 | pub rad: Option, 40 | #[yaserde(attribute, rename = "factor")] 41 | pub factor: Option, 42 | #[yaserde(attribute, rename = "offset")] 43 | pub offset: Option, 44 | } 45 | 46 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 47 | pub struct DisplayUnit { 48 | //#[yaserde(rename = "Annotations")] 49 | // pub annotations: Option, 50 | #[yaserde(attribute, rename = "name")] 51 | pub name: String, 52 | #[yaserde(attribute, rename = "factor")] 53 | pub factor: Option, 54 | #[yaserde(attribute, rename = "offset")] 55 | pub offset: Option, 56 | #[yaserde(attribute, rename = "inverse")] 57 | pub inverse: Option, 58 | } 59 | 60 | #[test] 61 | fn test_dependencies_kind() { 62 | let xml = r#" 63 | 64 | "#; 65 | 66 | let unit: Fmi2Unit = yaserde::de::from_str(xml).unwrap(); 67 | assert_eq!(unit.name, "m/s2"); 68 | assert_eq!( 69 | unit.base_unit, 70 | Some(BaseUnit { 71 | m: Some(1), 72 | s: Some(-2), 73 | ..Default::default() 74 | }) 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi2/variable_dependency.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | /// Dependency of scalar Unknown from Knowns in Continuous-Time and Event Mode (ModelExchange), and 4 | /// at Communication Points (CoSimulation): Unknown=f(Known_1, Known_2, ...). 5 | /// The Knowns are "inputs", "continuous states" and "independent variable" (usually time)". 6 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 7 | pub struct Fmi2VariableDependency { 8 | /// ScalarVariable index of Unknown 9 | #[yaserde(attribute)] 10 | pub index: u32, 11 | 12 | /// Defines the dependency of the Unknown (directly or indirectly via auxiliary variables) on 13 | /// the Knowns in Continuous-Time and Event Mode ([`super::ModelExchange`]) and at 14 | /// Communication Points ([`super::CoSimulation`]). 15 | /// 16 | /// If not present, it must be assumed that the Unknown depends on all Knowns. If present as 17 | /// empty list, the Unknown depends on none of the Knowns. Otherwise the Unknown depends on 18 | /// the Knowns defined by the given [`super::ScalarVariable`] indices. The indices are 19 | /// ordered according to size, starting with the smallest index. 20 | #[yaserde(attribute, rename = "dependencies")] 21 | pub dependencies: Vec, 22 | 23 | /// If not present, it must be assumed that the Unknown depends on the Knowns without a 24 | /// particular structure. Otherwise, the corresponding Known v enters the equation as: 25 | /// 26 | /// * [`DependenciesKind::Dependent`]: no particular structure, f(v) 27 | /// * [`DependenciesKind::Constant`]: constant factor, c*v (only for Real variablse) 28 | /// * [`DependenciesKind::Fixed`]: fixed factor, p*v (only for Real variables) 29 | /// * [`DependenciesKind::Tunable`]: tunable factor, p*v (only for Real variables) 30 | /// * [`DependenciesKind::Discrete`]: discrete factor, d*v (only for Real variables) 31 | /// 32 | /// If [`Self::dependencies_kind`] is present, [`Self::dependencies`] must be present and must 33 | /// have the same number of list elements. 34 | #[yaserde(child, attribute, rename = "dependenciesKind")] 35 | pub dependencies_kind: Vec, 36 | } 37 | 38 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 39 | pub enum DependenciesKind { 40 | #[yaserde(rename = "dependent")] 41 | #[default] 42 | Dependent, 43 | #[yaserde(rename = "constant")] 44 | Constant, 45 | #[yaserde(rename = "fixed")] 46 | Fixed, 47 | #[yaserde(rename = "tunable")] 48 | Tunable, 49 | #[yaserde(rename = "discrete")] 50 | Discrete, 51 | } 52 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/annotation.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 4 | #[yaserde(rename = "Annotations")] 5 | pub struct Fmi3Annotations { 6 | pub annotation: Annotation, 7 | } 8 | 9 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 10 | pub struct Annotation { 11 | #[yaserde(attribute = "type")] 12 | pub r#type: String, 13 | } 14 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/attribute_groups.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 4 | pub struct RealBaseAttributes { 5 | #[yaserde(attribute)] 6 | pub quantity: Option, 7 | #[yaserde(attribute)] 8 | pub unit: Option, 9 | #[yaserde(attribute, rename = "displayUnit")] 10 | pub display_unit: Option, 11 | #[yaserde(attribute, rename = "relativeQuantity")] 12 | pub relative_quantity: bool, 13 | #[yaserde(attribute, rename = "unbounded")] 14 | pub unbounded: bool, 15 | } 16 | 17 | macro_rules! float_attrs { 18 | ($name:ident, $type:ty) => { 19 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 20 | pub struct $name { 21 | #[yaserde(attr)] 22 | pub min: Option<$type>, 23 | #[yaserde(attr)] 24 | pub max: Option<$type>, 25 | #[yaserde(attr)] 26 | pub nominal: Option<$type>, 27 | } 28 | }; 29 | } 30 | float_attrs!(Float32Attributes, f32); 31 | float_attrs!(Float64Attributes, f64); 32 | 33 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 34 | pub struct IntegerBaseAttributes { 35 | #[yaserde(attribute)] 36 | quantity: String, 37 | } 38 | 39 | macro_rules! integer_attrs { 40 | ($name:ident, $type:ty) => { 41 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 42 | #[yaserde(rename = "$name")] 43 | pub struct $name { 44 | #[yaserde(attribute)] 45 | pub min: $type, 46 | #[yaserde(attribute)] 47 | pub max: $type, 48 | } 49 | }; 50 | } 51 | 52 | integer_attrs!(Int8Attributes, i8); 53 | integer_attrs!(UInt8Attributes, u8); 54 | integer_attrs!(Int16Attributes, i16); 55 | integer_attrs!(UInt16Attributes, u16); 56 | integer_attrs!(Int32Attributes, i32); 57 | integer_attrs!(UInt32Attributes, u32); 58 | integer_attrs!(Int64Attributes, i64); 59 | integer_attrs!(UInt64Attributes, u64); 60 | 61 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 62 | pub struct RealVariableAttributes { 63 | #[yaserde(attribute)] 64 | pub derivative: Option, 65 | #[yaserde(attribute)] 66 | pub reinit: bool, 67 | } 68 | 69 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 70 | struct EnumerationAttributes { 71 | #[yaserde(attribute)] 72 | pub min: i64, 73 | #[yaserde(attribute)] 74 | pub max: i64, 75 | } 76 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/interface_type.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | use super::Annotations; 4 | 5 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 6 | #[yaserde(rename = "InterfaceType")] 7 | pub struct Fmi3InterfaceType { 8 | #[yaserde(rename = "Annotations")] 9 | pub annotations: Option, 10 | 11 | #[yaserde(attribute, rename = "modelIdentifier")] 12 | pub model_identifier: String, 13 | 14 | #[yaserde(attribute, rename = "needsExecutionTool")] 15 | pub needs_execution_tool: Option, 16 | 17 | #[yaserde(attribute, rename = "canBeInstantiatedOnlyOncePerProcess")] 18 | pub can_be_instantiated_only_once_per_process: Option, 19 | 20 | #[yaserde(attribute, rename = "canGetAndSetFMUState")] 21 | pub can_get_and_set_fmu_state: Option, 22 | 23 | #[yaserde(attribute, rename = "canSerializeFMUState")] 24 | pub can_serialize_fmu_state: Option, 25 | 26 | #[yaserde(attribute, rename = "providesDirectionalDerivatives")] 27 | pub provides_directional_derivatives: Option, 28 | 29 | #[yaserde(attribute, rename = "providesAdjointDerivatives")] 30 | pub provides_adjoint_derivatives: Option, 31 | 32 | #[yaserde(attribute, rename = "providesPerElementDependencies")] 33 | pub provides_per_element_dependencies: Option, 34 | } 35 | 36 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 37 | #[yaserde(rename = "ModelExchange")] 38 | pub struct Fmi3ModelExchange { 39 | #[yaserde(attribute, rename = "needsCompletedIntegratorStep")] 40 | pub needs_completed_integrator_step: Option, 41 | 42 | #[yaserde(attribute, rename = "providesEvaluateDiscreteStates")] 43 | pub provides_evaluate_discrete_states: Option, 44 | 45 | #[yaserde(rename = "Annotations")] 46 | pub annotations: Option, 47 | 48 | #[yaserde(attribute, rename = "modelIdentifier")] 49 | pub model_identifier: String, 50 | 51 | #[yaserde(attribute, rename = "needsExecutionTool")] 52 | pub needs_execution_tool: Option, 53 | 54 | #[yaserde(attribute, rename = "canBeInstantiatedOnlyOncePerProcess")] 55 | pub can_be_instantiated_only_once_per_process: Option, 56 | 57 | #[yaserde(attribute, rename = "canGetAndSetFMUState")] 58 | pub can_get_and_set_fmu_state: Option, 59 | 60 | #[yaserde(attribute, rename = "canSerializeFMUState")] 61 | pub can_serialize_fmu_state: Option, 62 | 63 | #[yaserde(attribute, rename = "providesDirectionalDerivatives")] 64 | pub provides_directional_derivatives: Option, 65 | 66 | #[yaserde(attribute, rename = "providesAdjointDerivatives")] 67 | pub provides_adjoint_derivatives: Option, 68 | 69 | #[yaserde(attribute, rename = "providesPerElementDependencies")] 70 | pub provides_per_element_dependencies: Option, 71 | } 72 | 73 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 74 | #[yaserde(rename = "CoSimulation")] 75 | pub struct Fmi3CoSimulation { 76 | #[yaserde(attribute, rename = "canHandleVariableCommunicationStepSize")] 77 | pub can_handle_variable_communication_step_size: Option, 78 | 79 | #[yaserde(attribute, rename = "fixedInternalStepSize")] 80 | pub fixed_internal_step_size: Option, 81 | 82 | #[yaserde(attribute, rename = "maxOutputDerivativeOrder")] 83 | pub max_output_derivative_order: Option, 84 | 85 | #[yaserde(attribute, rename = "recommendedIntermediateInputSmoothness")] 86 | pub recommended_intermediate_input_smoothness: Option, 87 | 88 | #[yaserde(attribute, rename = "providesIntermediateUpdate")] 89 | pub provides_intermediate_update: Option, 90 | 91 | #[yaserde(attribute, rename = "mightReturnEarlyFromDoStep")] 92 | pub might_return_early_from_do_step: Option, 93 | 94 | #[yaserde(attribute, rename = "canReturnEarlyAfterIntermediateUpdate")] 95 | pub can_return_early_after_intermediate_update: Option, 96 | 97 | #[yaserde(attribute, rename = "hasEventMode")] 98 | pub has_event_mode: Option, 99 | 100 | #[yaserde(attribute, rename = "providesEvaluateDiscreteStates")] 101 | pub provides_evaluate_discrete_states: Option, 102 | 103 | #[yaserde(rename = "Annotations")] 104 | pub annotations: Option, 105 | 106 | #[yaserde(attribute, rename = "modelIdentifier")] 107 | pub model_identifier: String, 108 | 109 | #[yaserde(attribute, rename = "needsExecutionTool")] 110 | pub needs_execution_tool: Option, 111 | 112 | #[yaserde(attribute, rename = "canBeInstantiatedOnlyOncePerProcess")] 113 | pub can_be_instantiated_only_once_per_process: Option, 114 | 115 | #[yaserde(attribute, rename = "canGetAndSetFMUState")] 116 | pub can_get_and_set_fmu_state: Option, 117 | 118 | #[yaserde(attribute, rename = "canSerializeFMUState")] 119 | pub can_serialize_fmu_state: Option, 120 | 121 | #[yaserde(attribute, rename = "providesDirectionalDerivatives")] 122 | pub provides_directional_derivatives: Option, 123 | 124 | #[yaserde(attribute, rename = "providesAdjointDerivatives")] 125 | pub provides_adjoint_derivatives: Option, 126 | 127 | #[yaserde(attribute, rename = "providesPerElementDependencies")] 128 | pub provides_per_element_dependencies: Option, 129 | } 130 | 131 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 132 | #[yaserde(rename = "ScheduledExecution")] 133 | pub struct Fmi3ScheduledExecution { 134 | #[yaserde(rename = "Annotations")] 135 | pub annotations: Option, 136 | 137 | #[yaserde(attribute, rename = "modelIdentifier")] 138 | pub model_identifier: String, 139 | 140 | #[yaserde(attribute, rename = "needsExecutionTool")] 141 | pub needs_execution_tool: Option, 142 | 143 | #[yaserde(attribute, rename = "canBeInstantiatedOnlyOncePerProcess")] 144 | pub can_be_instantiated_only_once_per_process: Option, 145 | 146 | #[yaserde(attribute, rename = "canGetAndSetFMUState")] 147 | pub can_get_and_set_fmu_state: Option, 148 | 149 | #[yaserde(attribute, rename = "canSerializeFMUState")] 150 | pub can_serialize_fmu_state: Option, 151 | 152 | #[yaserde(attribute, rename = "providesDirectionalDerivatives")] 153 | pub provides_directional_derivatives: Option, 154 | 155 | #[yaserde(attribute, rename = "providesAdjointDerivatives")] 156 | pub provides_adjoint_derivatives: Option, 157 | 158 | #[yaserde(attribute, rename = "providesPerElementDependencies")] 159 | pub provides_per_element_dependencies: Option, 160 | } 161 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/mod.rs: -------------------------------------------------------------------------------- 1 | //! FMI3.0 schema definitions 2 | //! 3 | //! This module contains the definitions of the FMI3.0 XML schema. 4 | 5 | mod annotation; 6 | mod attribute_groups; 7 | mod interface_type; 8 | mod model_description; 9 | mod r#type; 10 | mod unit; 11 | mod variable; 12 | mod variable_dependency; 13 | 14 | use std::str::FromStr; 15 | 16 | pub use annotation::Fmi3Annotations as Annotations; 17 | pub use attribute_groups::*; 18 | pub use interface_type::*; 19 | pub use model_description::*; 20 | pub use r#type::*; 21 | pub use unit::*; 22 | pub use variable::*; 23 | pub use variable_dependency::*; 24 | 25 | use crate::{ 26 | variable_counts::{Counts, VariableCounts}, 27 | Error, 28 | }; 29 | 30 | impl FromStr for Fmi3ModelDescription { 31 | type Err = crate::Error; 32 | 33 | fn from_str(s: &str) -> Result { 34 | yaserde::de::from_str(s).map_err(Error::XmlParse) 35 | } 36 | } 37 | 38 | impl crate::traits::DefaultExperiment for Fmi3ModelDescription { 39 | fn start_time(&self) -> Option { 40 | self.default_experiment 41 | .as_ref() 42 | .and_then(|de| de.start_time) 43 | } 44 | 45 | fn stop_time(&self) -> Option { 46 | self.default_experiment.as_ref().and_then(|de| de.stop_time) 47 | } 48 | 49 | fn tolerance(&self) -> Option { 50 | self.default_experiment.as_ref().and_then(|de| de.tolerance) 51 | } 52 | 53 | fn step_size(&self) -> Option { 54 | self.default_experiment.as_ref().and_then(|de| de.step_size) 55 | } 56 | } 57 | 58 | impl VariableCounts for ModelVariables { 59 | fn model_counts(&self) -> Counts { 60 | let cts = Counts { 61 | num_real_vars: self.float32.len() + self.float64.len(), 62 | num_bool_vars: 0, 63 | num_integer_vars: self.int8.len() 64 | + self.uint8.len() 65 | + self.int16.len() 66 | + self.uint16.len() 67 | + self.int32.len() 68 | + self.uint32.len(), 69 | num_string_vars: 0, 70 | num_enum_vars: 0, 71 | ..Default::default() 72 | }; 73 | 74 | let fl32 = self 75 | .float32 76 | .iter() 77 | .map(|sv| (sv.variability(), sv.causality())); 78 | let fl64 = self 79 | .float64 80 | .iter() 81 | .map(|sv| (sv.variability(), sv.causality())); 82 | let i8 = self 83 | .int8 84 | .iter() 85 | .map(|sv| (sv.variability(), sv.causality())); 86 | let u8 = self 87 | .uint8 88 | .iter() 89 | .map(|sv| (sv.variability(), sv.causality())); 90 | let i16 = self 91 | .int16 92 | .iter() 93 | .map(|sv| (sv.variability(), sv.causality())); 94 | let u16 = self 95 | .uint16 96 | .iter() 97 | .map(|sv| (sv.variability(), sv.causality())); 98 | let i32 = self 99 | .int32 100 | .iter() 101 | .map(|sv| (sv.variability(), sv.causality())); 102 | let u32 = self 103 | .uint32 104 | .iter() 105 | .map(|sv| (sv.variability(), sv.causality())); 106 | 107 | itertools::chain!(fl32, fl64, i8, u8, i16, u16, i32, u32).fold( 108 | cts, 109 | |mut cts, (variability, causality)| { 110 | match variability { 111 | Variability::Constant => { 112 | cts.num_constants += 1; 113 | } 114 | Variability::Continuous => { 115 | cts.num_continuous += 1; 116 | } 117 | Variability::Discrete => { 118 | cts.num_discrete += 1; 119 | } 120 | _ => {} 121 | } 122 | match causality { 123 | Causality::CalculatedParameter => { 124 | cts.num_calculated_parameters += 1; 125 | } 126 | Causality::Parameter => { 127 | cts.num_parameters += 1; 128 | } 129 | Causality::Input => { 130 | cts.num_inputs += 1; 131 | } 132 | Causality::Output => { 133 | cts.num_outputs += 1; 134 | } 135 | Causality::Local => { 136 | cts.num_local += 1; 137 | } 138 | Causality::Independent => { 139 | cts.num_independent += 1; 140 | } 141 | _ => {} 142 | } 143 | cts 144 | }, 145 | ) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/type.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | use super::{Float32Attributes, Float64Attributes, RealBaseAttributes}; 4 | 5 | pub trait BaseTypeTrait { 6 | fn name(&self) -> &str; 7 | fn description(&self) -> Option<&str>; 8 | } 9 | 10 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 11 | pub struct TypeDefinitionBase { 12 | #[yaserde(attribute)] 13 | pub name: String, 14 | #[yaserde(attribute)] 15 | pub description: Option, 16 | } 17 | 18 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 19 | pub struct Float32Type { 20 | #[yaserde(flatten)] 21 | pub base: TypeDefinitionBase, 22 | #[yaserde(flatten)] 23 | pub base_attr: RealBaseAttributes, 24 | #[yaserde(flatten)] 25 | pub attr: Float32Attributes, 26 | } 27 | 28 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 29 | pub struct Float64Type { 30 | #[yaserde(flatten)] 31 | pub base: TypeDefinitionBase, 32 | #[yaserde(flatten)] 33 | pub base_attr: RealBaseAttributes, 34 | #[yaserde(flatten)] 35 | pub attr: Float64Attributes, 36 | } 37 | 38 | impl BaseTypeTrait for Float32Type { 39 | fn name(&self) -> &str { 40 | &self.base.name 41 | } 42 | 43 | fn description(&self) -> Option<&str> { 44 | self.base.description.as_deref() 45 | } 46 | } 47 | 48 | impl BaseTypeTrait for Float64Type { 49 | fn name(&self) -> &str { 50 | &self.base.name 51 | } 52 | 53 | fn description(&self) -> Option<&str> { 54 | self.base.description.as_deref() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/unit.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | use super::Annotations; 4 | 5 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 6 | #[yaserde(rename = "Unit")] 7 | pub struct Fmi3Unit { 8 | #[yaserde(attribute)] 9 | pub name: String, 10 | #[yaserde(rename = "BaseUnit")] 11 | pub base_unit: Option, 12 | #[yaserde(rename = "DisplayUnit")] 13 | pub display_unit: Vec, 14 | #[yaserde(= "Annotations")] 15 | pub annotations: Option, 16 | } 17 | 18 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 19 | pub struct BaseUnit { 20 | #[yaserde(attribute, rename = "kg")] 21 | pub kg: Option, 22 | #[yaserde(attribute, rename = "m")] 23 | pub m: Option, 24 | #[yaserde(attribute, rename = "s")] 25 | pub s: Option, 26 | #[yaserde(attribute, rename = "A")] 27 | pub a: Option, 28 | #[yaserde(attribute, rename = "K")] 29 | pub k: Option, 30 | #[yaserde(attribute, rename = "mol")] 31 | pub mol: Option, 32 | #[yaserde(attribute, rename = "cd")] 33 | pub cd: Option, 34 | #[yaserde(attribute, rename = "rad")] 35 | pub rad: Option, 36 | #[yaserde(attribute, rename = "factor")] 37 | pub factor: Option, 38 | #[yaserde(attribute, rename = "offset")] 39 | pub offset: Option, 40 | } 41 | 42 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 43 | pub struct DisplayUnit { 44 | #[yaserde(rename = "Annotations")] 45 | pub annotations: Option, 46 | #[yaserde(attribute, rename = "name")] 47 | pub name: String, 48 | #[yaserde(attribute, rename = "factor")] 49 | pub factor: Option, 50 | #[yaserde(attribute, rename = "offset")] 51 | pub offset: Option, 52 | #[yaserde(attribute, rename = "inverse")] 53 | pub inverse: Option, 54 | } 55 | 56 | #[test] 57 | fn test_dependencies_kind() { 58 | let xml = r#" 59 | 60 | "#; 61 | 62 | let unit: Fmi3Unit = yaserde::de::from_str(xml).unwrap(); 63 | assert_eq!(unit.name, "m/s2"); 64 | assert_eq!( 65 | unit.base_unit, 66 | Some(BaseUnit { 67 | m: Some(1), 68 | s: Some(-2), 69 | ..Default::default() 70 | }) 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /fmi-schema/src/fmi3/variable_dependency.rs: -------------------------------------------------------------------------------- 1 | use yaserde_derive::{YaDeserialize, YaSerialize}; 2 | 3 | use super::Annotations; 4 | 5 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 6 | pub enum DependenciesKind { 7 | #[yaserde(rename = "dependent")] 8 | #[default] 9 | Dependent, 10 | #[yaserde(rename = "constant")] 11 | Constant, 12 | #[yaserde(rename = "fixed")] 13 | Fixed, 14 | #[yaserde(rename = "tunable")] 15 | Tunable, 16 | #[yaserde(rename = "discrete")] 17 | Discrete, 18 | } 19 | 20 | #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] 21 | #[yaserde(tag = "Fmi3Unknown")] 22 | pub struct Fmi3Unknown { 23 | #[yaserde(rename = "Annotations")] 24 | pub annotations: Option, 25 | #[yaserde(attribute, rename = "valueReference")] 26 | pub value_reference: u32, 27 | #[yaserde(attribute, rename = "dependencies")] 28 | pub dependencies: Vec, 29 | #[yaserde(attribute, rename = "dependenciesKind")] 30 | pub dependencies_kind: Vec, 31 | } 32 | 33 | #[test] 34 | fn test_dependencies_kind() { 35 | let xml = r#" 36 | 37 | "#; 38 | 39 | let x: Fmi3Unknown = yaserde::de::from_str(xml).unwrap(); 40 | assert_eq!(x.value_reference, 1); 41 | assert_eq!(x.dependencies, vec![0, 1, 2]); 42 | assert_eq!( 43 | x.dependencies_kind, 44 | vec![ 45 | DependenciesKind::Dependent, 46 | DependenciesKind::Constant, 47 | DependenciesKind::Fixed 48 | ] 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /fmi-schema/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc=include_str!( "../README.md")] 2 | //! ## Feature flags 3 | #![doc = document_features::document_features!()] 4 | #![deny(unsafe_code)] 5 | #![deny(clippy::all)] 6 | 7 | use thiserror::Error; 8 | 9 | pub mod date_time; 10 | #[cfg(feature = "fmi2")] 11 | pub mod fmi2; 12 | #[cfg(feature = "fmi3")] 13 | pub mod fmi3; 14 | pub mod minimal; 15 | pub mod traits; 16 | pub mod variable_counts; 17 | 18 | /// The major version of the FMI standard 19 | #[derive(Debug, PartialEq, Eq)] 20 | pub enum MajorVersion { 21 | FMI1, 22 | FMI2, 23 | FMI3, 24 | } 25 | 26 | impl ToString for MajorVersion { 27 | fn to_string(&self) -> String { 28 | match self { 29 | MajorVersion::FMI1 => "1.0".to_string(), 30 | MajorVersion::FMI2 => "2.0".to_string(), 31 | MajorVersion::FMI3 => "3.0".to_string(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Debug, Error)] 37 | pub enum Error { 38 | #[error("Variable {0} not found")] 39 | VariableNotFound(String), 40 | 41 | #[error(transparent)] 42 | Semver(#[from] lenient_semver::parser::OwnedError), 43 | 44 | #[error("Error parsing XML: {0}")] 45 | XmlParse(String), 46 | } 47 | -------------------------------------------------------------------------------- /fmi-schema/src/minimal.rs: -------------------------------------------------------------------------------- 1 | //! Minimal FMI definitions for determining FMI version. 2 | //! 3 | //! ```rust 4 | //! # use fmi_schema::{minimal::MinModelDescription, traits::FmiModelDescription}; 5 | //! # use std::str::FromStr; 6 | //! let xml = r#" 7 | //! 8 | //! "#; 9 | //! let md = MinModelDescription::from_str(xml).unwrap(); 10 | //! let version = md.version().unwrap(); 11 | //! assert_eq!(version, semver::Version::new(2, 0, 0)); 12 | //! ``` 13 | 14 | use std::str::FromStr; 15 | 16 | use yaserde_derive::YaDeserialize; 17 | 18 | use crate::traits::FmiModelDescription; 19 | 20 | /// A minimal model description that only contains the FMI version 21 | /// This is used to determine the FMI version of the FMU 22 | #[derive(Default, PartialEq, Debug, YaDeserialize)] 23 | #[yaserde(rename = "fmiModelDescription")] 24 | pub struct MinModelDescription { 25 | #[yaserde(attribute, rename = "fmiVersion")] 26 | pub fmi_version: String, 27 | #[yaserde(attribute, rename = "modelName")] 28 | pub model_name: String, 29 | } 30 | 31 | impl FmiModelDescription for MinModelDescription { 32 | fn model_name(&self) -> &str { 33 | &self.model_name 34 | } 35 | 36 | fn version_string(&self) -> &str { 37 | &self.fmi_version 38 | } 39 | } 40 | 41 | impl FromStr for MinModelDescription { 42 | type Err = crate::Error; 43 | 44 | fn from_str(s: &str) -> Result { 45 | yaserde::de::from_str(s).map_err(crate::Error::XmlParse) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fmi-schema/src/traits.rs: -------------------------------------------------------------------------------- 1 | //! Common traits for FMI schema 2 | 3 | use crate::MajorVersion; 4 | 5 | pub trait DefaultExperiment { 6 | fn start_time(&self) -> Option; 7 | fn stop_time(&self) -> Option; 8 | fn tolerance(&self) -> Option; 9 | fn step_size(&self) -> Option; 10 | } 11 | 12 | pub trait FmiModelDescription { 13 | /// Returns the model name 14 | fn model_name(&self) -> &str; 15 | 16 | /// Returns the FMI version as a string 17 | fn version_string(&self) -> &str; 18 | 19 | /// Returns the parsed FMI version as a semver::Version 20 | fn version(&self) -> Result { 21 | lenient_semver::parse(self.version_string()).map_err(|e| e.owned().into()) 22 | } 23 | 24 | /// Returns the parsed FMI version as a MajorVersion 25 | fn major_version(&self) -> Result { 26 | match self.version()? { 27 | v if v.major == 1 => Ok(MajorVersion::FMI1), 28 | v if v.major == 2 => Ok(MajorVersion::FMI2), 29 | v if v.major == 3 => Ok(MajorVersion::FMI3), 30 | v => panic!("Invalid version {}", v.major), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fmi-schema/src/variable_counts.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | /// Collects counts of variables in the model 4 | #[derive(Debug, Default)] 5 | pub struct Counts { 6 | pub num_constants: usize, 7 | pub num_parameters: usize, 8 | pub num_discrete: usize, 9 | pub num_continuous: usize, 10 | pub num_inputs: usize, 11 | pub num_outputs: usize, 12 | pub num_local: usize, 13 | pub num_independent: usize, 14 | pub num_calculated_parameters: usize, 15 | pub num_real_vars: usize, 16 | pub num_integer_vars: usize, 17 | pub num_enum_vars: usize, 18 | pub num_bool_vars: usize, 19 | pub num_string_vars: usize, 20 | } 21 | 22 | impl Display for Counts { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | f.debug_struct("Variable Counts") 25 | .field("Constants", &self.num_constants) 26 | .field("Parameters", &self.num_parameters) 27 | .field("Discrete", &self.num_discrete) 28 | .field("Continuous", &self.num_continuous) 29 | .field("Inputs", &self.num_inputs) 30 | .field("Outputs", &self.num_outputs) 31 | .field("Local", &self.num_local) 32 | .field("Independent", &self.num_independent) 33 | .field("Calculated parameters", &self.num_calculated_parameters) 34 | .field("Real", &self.num_real_vars) 35 | .field("Integer", &self.num_integer_vars) 36 | .field("Enumeration", &self.num_enum_vars) 37 | .field("Boolean", &self.num_bool_vars) 38 | .field("String", &self.num_string_vars) 39 | .finish() 40 | } 41 | } 42 | 43 | pub trait VariableCounts { 44 | fn model_counts(&self) -> Counts; 45 | } 46 | -------------------------------------------------------------------------------- /fmi-schema/tests/FMI2.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 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 | -------------------------------------------------------------------------------- /fmi-schema/tests/FMI3.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 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 | -------------------------------------------------------------------------------- /fmi-schema/tests/test_fmi2.rs: -------------------------------------------------------------------------------- 1 | //! Test FMI 2.0 schema by parsing the FMI2.xml file. 2 | 3 | use fmi_schema::fmi2::{BaseUnit, Fmi2ModelDescription, SimpleTypeElement}; 4 | 5 | #[test] 6 | #[cfg(feature = "fmi2")] 7 | fn test_fmi2() { 8 | let test_file = std::env::current_dir() 9 | .map(|path| path.join("tests/FMI2.xml")) 10 | .unwrap(); 11 | let file = std::fs::File::open(test_file).unwrap(); 12 | let buf_reader = std::io::BufReader::new(file); 13 | let md: Fmi2ModelDescription = yaserde::de::from_reader(buf_reader).unwrap(); 14 | 15 | assert_eq!(md.fmi_version, "2.0"); 16 | assert_eq!(md.model_name, "BouncingBall"); 17 | assert_eq!( 18 | md.description.as_deref(), 19 | Some("This model calculates the trajectory, over time, of a ball dropped from a height of 1 m.") 20 | ); 21 | assert_eq!(md.guid, "{8c4e810f-3df3-4a00-8276-176fa3c9f003}"); 22 | assert_eq!(md.number_of_event_indicators, 1); 23 | 24 | let me = md.model_exchange.unwrap(); 25 | assert_eq!(me.model_identifier, "BouncingBall"); 26 | assert_eq!(me.can_not_use_memory_management_functions, true); 27 | assert_eq!(me.can_get_and_set_fmu_state, true); 28 | assert_eq!(me.can_serialize_fmu_state, true); 29 | assert_eq!(me.source_files.files.len(), 1); 30 | assert_eq!(me.source_files.files[0].name, "all.c"); 31 | 32 | let cs = md.co_simulation.unwrap(); 33 | assert_eq!(cs.model_identifier, "BouncingBall"); 34 | assert_eq!(cs.can_handle_variable_communication_step_size, true); 35 | assert_eq!(cs.can_not_use_memory_management_functions, true); 36 | assert_eq!(cs.can_get_and_set_fmu_state, true); 37 | assert_eq!(cs.can_serialize_fmu_state, true); 38 | assert_eq!(cs.source_files.files.len(), 1); 39 | assert_eq!(cs.source_files.files[0].name, "all.c"); 40 | 41 | let units = md.unit_definitions.unwrap(); 42 | assert_eq!(units.units.len(), 3); 43 | assert_eq!(units.units[0].name, "m"); 44 | assert_eq!( 45 | units.units[0].base_unit, 46 | Some(BaseUnit { 47 | m: Some(1), 48 | ..Default::default() 49 | }) 50 | ); 51 | 52 | let typedefs = md.type_definitions.unwrap(); 53 | assert_eq!(typedefs.types.len(), 3); 54 | assert_eq!(typedefs.types[0].name, "Position"); 55 | assert!(matches!(typedefs.types[0].elem, SimpleTypeElement::Real(_))); 56 | } 57 | -------------------------------------------------------------------------------- /fmi-schema/tests/test_fmi3.rs: -------------------------------------------------------------------------------- 1 | //! Test FMI 3.0 schema by parsing the FMI3.xml file. 2 | 3 | #[test] 4 | #[cfg(feature = "fmi3")] 5 | fn test_fmi3() { 6 | use fmi_schema::fmi3::{ 7 | AbstractVariableTrait, BaseTypeTrait, BaseUnit, DependenciesKind, Fmi3ModelDescription, 8 | Fmi3ModelExchange, Variability, 9 | }; 10 | 11 | let test_file = std::env::current_dir() 12 | .map(|path| path.join("tests/FMI3.xml")) 13 | .unwrap(); 14 | let file = std::fs::File::open(test_file).unwrap(); 15 | let buf_reader = std::io::BufReader::new(file); 16 | let model: Fmi3ModelDescription = yaserde::de::from_reader(buf_reader).unwrap(); 17 | 18 | let model_exchange = model.model_exchange.unwrap(); 19 | assert_eq!( 20 | model_exchange, 21 | Fmi3ModelExchange { 22 | model_identifier: "BouncingBall".to_owned(), 23 | can_get_and_set_fmu_state: Some(true), 24 | can_serialize_fmu_state: Some(true), 25 | ..Default::default() 26 | } 27 | ); 28 | 29 | let unit_defs = model.unit_definitions.unwrap(); 30 | assert_eq!(unit_defs.units.len(), 3); 31 | assert_eq!(unit_defs.units[0].name, "m"); 32 | assert_eq!( 33 | unit_defs.units[1].base_unit, 34 | Some(BaseUnit { 35 | m: Some(1), 36 | s: Some(-1), 37 | ..Default::default() 38 | }) 39 | ); 40 | 41 | let type_defs = model.type_definitions.unwrap(); 42 | assert_eq!(type_defs.float64types.len(), 3); 43 | assert_eq!(type_defs.float64types[0].name(), "Position"); 44 | // Float64Type { name: "Position", quantity: "Position", unit: "m" }, 45 | 46 | let log_cats = model.log_categories.unwrap(); 47 | assert_eq!(log_cats.categories.len(), 2); 48 | assert_eq!(&log_cats.categories[0].name, "logEvents"); 49 | assert_eq!( 50 | log_cats.categories[0].description.as_deref(), 51 | Some("Log events") 52 | ); 53 | 54 | let default_exp = model.default_experiment.unwrap(); 55 | assert_eq!(default_exp.start_time, Some(0.0)); 56 | assert_eq!(default_exp.stop_time, Some(3.0)); 57 | assert_eq!(default_exp.step_size, Some(1e-3)); 58 | 59 | let model_vars = &model.model_variables; 60 | assert_eq!(model_vars.float64.len(), 7); 61 | assert_eq!(model_vars.float64[4].name(), "der(v)"); 62 | assert_eq!(model_vars.float64[4].value_reference(), 4); 63 | assert_eq!(model_vars.float64[4].variability(), Variability::Continuous); 64 | 65 | let model_structure = &model.model_structure; 66 | assert_eq!(model_structure.outputs.len(), 2); 67 | assert_eq!(model_structure.outputs[0].value_reference, 1); 68 | assert_eq!(model_structure.outputs[1].value_reference, 3); 69 | assert_eq!(model_structure.continuous_state_derivative.len(), 2); 70 | assert_eq!(model_structure.initial_unknown.len(), 2); 71 | assert_eq!(model_structure.initial_unknown[0].value_reference, 2); 72 | assert_eq!(model_structure.initial_unknown[0].dependencies, vec![3]); 73 | assert_eq!( 74 | model_structure.initial_unknown[0].dependencies_kind, 75 | vec![DependenciesKind::Constant] 76 | ); 77 | assert_eq!(model_structure.event_indicator.len(), 1); 78 | } 79 | -------------------------------------------------------------------------------- /fmi-schema/tests/test_minimal.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr as _; 2 | 3 | use fmi_schema::traits::FmiModelDescription; 4 | use semver::{BuildMetadata, Prerelease}; 5 | 6 | #[test] 7 | fn test_minimal() -> Result<(), Box> { 8 | let test_file = std::env::current_dir().map(|path| path.join("tests/FMI2.xml"))?; 9 | let data = std::fs::read_to_string(test_file)?; 10 | let md = fmi_schema::minimal::MinModelDescription::from_str(&data)?; 11 | assert_eq!(md.major_version()?, fmi_schema::MajorVersion::FMI2); 12 | assert_eq!(md.version()?, semver::Version::new(2, 0, 0)); 13 | assert_eq!(md.model_name, "BouncingBall"); 14 | 15 | let test_file = std::env::current_dir().map(|path| path.join("tests/FMI3.xml"))?; 16 | let data = std::fs::read_to_string(test_file)?; 17 | let md = fmi_schema::minimal::MinModelDescription::from_str(&data)?; 18 | assert_eq!(md.major_version()?, fmi_schema::MajorVersion::FMI3); 19 | assert_eq!( 20 | md.version()?, 21 | semver::Version { 22 | major: 3, 23 | minor: 0, 24 | patch: 0, 25 | pre: Prerelease::new("beta.2").unwrap(), 26 | build: BuildMetadata::EMPTY, 27 | } 28 | ); 29 | assert_eq!(md.model_name, "BouncingBall"); 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /fmi-sim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmi-sim" 3 | version = "0.1.1" 4 | description = "A pure Rust FMI simulator" 5 | readme = "README.md" 6 | publish = true 7 | authors.workspace = true 8 | categories.workspace = true 9 | edition.workspace = true 10 | homepage.workspace = true 11 | include.workspace = true 12 | keywords.workspace = true 13 | license.workspace = true 14 | repository.workspace = true 15 | 16 | [features] 17 | default = ["fmi2", "fmi3", "cs", "me"] 18 | ## Enable support for FMI 2.0 19 | fmi2 = ["fmi/fmi2"] 20 | ## Enable support for FMI 3.0 21 | fmi3 = ["fmi/fmi3"] 22 | ## Enable support for Model Exchange 23 | me = [] 24 | ## Enable support for Co-Simulation 25 | cs = [] 26 | ## Enable support for Scheduled Execution 27 | se = [] 28 | 29 | 30 | [dependencies] 31 | anyhow = { workspace = true } 32 | arrow = { workspace = true, features = ["csv", "prettyprint"] } 33 | clap = { version = "4.5", features = ["derive"] } 34 | comfy-table = "7.1" 35 | document-features = { workspace = true } 36 | env_logger = "0.11" 37 | fmi = { workspace = true, features = ["arrow"] } 38 | itertools = { workspace = true } 39 | log = "0.4" 40 | num-traits = "0.2" 41 | sensible-env-logger = "0.3" 42 | thiserror = { workspace = true } 43 | 44 | [dev-dependencies] 45 | assert_cmd = "2.0.14" 46 | float-cmp = { version = "0.10", features = ["std"] } 47 | fmi-test-data = { workspace = true } 48 | rstest = "0.21" 49 | test-log = { workspace = true } 50 | -------------------------------------------------------------------------------- /fmi-sim/README.md: -------------------------------------------------------------------------------- 1 | # fmi-sim 2 | 3 | [github](https://github.com/jondo2010/rust-fmi) 4 | [crates.io](https://crates.io/crates/fmi-sim) 5 | [docs.rs](https://docs.rs/fmi-sim) 6 | [build status](https://github.com/jondo2010/rust-fmi/actions?query=branch%3Amain) 7 | 8 | A pure-Rust FMI simulator framework. This crate is a work-in-progress. 9 | 10 | ## Scope 11 | 12 | The purpose of `fmi-sim` is to simulate a single `FMI 2.0` or `FMI 3.0` FMU in ME/CS/SE modes as a way to drive testing and API completeness of the `rust-fmi` crates. The simulation algorithms are heavily inspired by those in [fmusim](https://github.com/modelica/Reference-FMUs/tree/main/fmusim). 13 | 14 | ## Running 15 | 16 | ```bash 17 | ➜ cargo run -p fmi-sim -- --help 18 | Finished dev [unoptimized + debuginfo] target(s) in 0.08s 19 | Running `target/debug/fmi-sim --help` 20 | Error: A pure Rust FMI simulator 21 | 22 | Usage: fmi-sim [OPTIONS] --model 23 | 24 | Commands: 25 | model-exchange Perform a ModelExchange simulation 26 | co-simulation Perform a CoSimulation simulation 27 | help Print this message or the help of the given subcommand(s) 28 | 29 | Options: 30 | --model The FMU model to read 31 | -i, --input-file Name of the CSV file name with input data 32 | -o, --output-file Simulation result output CSV file name. Default is to use standard output 33 | -c Separator to be used in CSV input/output [default: ,] 34 | -m Mangle variable names to avoid quoting (needed for some CSV importing applications, but not according to the CrossCheck rules) 35 | -h, --help Print help 36 | -V, --version Print version 37 | ``` 38 | 39 | ## License 40 | 41 | Licensed under either of 42 | 43 | * Apache License, Version 2.0 44 | ([LICENSE-APACHE](LICENSE-APACHE) or ) 45 | * MIT license 46 | ([LICENSE-MIT](LICENSE-MIT) or ) 47 | 48 | at your option. 49 | 50 | ## Contribution 51 | 52 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 53 | -------------------------------------------------------------------------------- /fmi-sim/examples/bouncing_ball.rs: -------------------------------------------------------------------------------- 1 | //! The following example demonstrates how to load and simulate an FMU model using the `fmi-sim` crate. 2 | 3 | use fmi::schema::MajorVersion; 4 | use fmi_sim::options::{CoSimulationOptions, CommonOptions, FmiSimOptions, Interface}; 5 | use fmi_test_data::ReferenceFmus; 6 | 7 | fn main() -> Result<(), Box> { 8 | // Load the FMU model 9 | let mut ref_fmus = ReferenceFmus::new().unwrap(); 10 | let fmu_file = ref_fmus 11 | .extract_reference_fmu("BouncingBall", MajorVersion::FMI3) 12 | .unwrap(); 13 | 14 | // Set the simulation options 15 | let interface = Interface::CoSimulation(CoSimulationOptions { 16 | common: CommonOptions { 17 | start_time: Some(0.0), 18 | output_interval: Some(0.1), 19 | ..Default::default() 20 | }, 21 | event_mode_used: true, 22 | ..Default::default() 23 | }); 24 | 25 | let options = FmiSimOptions { 26 | interface, 27 | model: fmu_file.path().to_path_buf(), 28 | ..Default::default() 29 | }; 30 | 31 | // Simulate the FMU model 32 | let (outputs, stats) = fmi_sim::simulate(&options)?; 33 | 34 | // Print the simulation results 35 | println!("Simulation statistics: {stats:?}"); 36 | println!( 37 | "{}", 38 | arrow::util::pretty::pretty_format_batches(&[outputs]).unwrap() 39 | ); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /fmi-sim/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc=include_str!( "../README.md")] 2 | //! ## Feature flags 3 | #![doc = document_features::document_features!()] 4 | #![deny(unsafe_code)] 5 | #![deny(clippy::all)] 6 | 7 | use arrow::array::RecordBatch; 8 | use fmi::schema::{traits::FmiModelDescription, MajorVersion}; 9 | use sim::SimStats; 10 | 11 | pub mod options; 12 | pub mod sim; 13 | 14 | /// Sim error 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum Error { 17 | #[error(transparent)] 18 | FmiError(#[from] fmi::Error), 19 | 20 | #[error(transparent)] 21 | SolverError(#[from] sim::solver::SolverError), 22 | 23 | #[error(transparent)] 24 | ArrowError(#[from] arrow::error::ArrowError), 25 | 26 | #[error(transparent)] 27 | Other(#[from] anyhow::Error), 28 | } 29 | 30 | /// Simulate an FMI model parameterized by the given top-level options. 31 | /// 32 | /// # Returns 33 | /// A tuple of the record batch of the simulation results and the statistics of the simulation. 34 | pub fn simulate(options: &options::FmiSimOptions) -> Result<(RecordBatch, SimStats), Error> { 35 | let mini_descr = fmi::import::peek_descr_path(&options.model)?; 36 | let version = mini_descr.major_version().map_err(fmi::Error::from)?; 37 | 38 | log::debug!("Loaded {:?}", mini_descr); 39 | 40 | // Read optional input data 41 | let input_data = options 42 | .input_file 43 | .as_ref() 44 | .inspect(|p| log::debug!("Reading input data from {}", p.display())) 45 | .map(sim::util::read_csv_file) 46 | .transpose()?; 47 | 48 | match version { 49 | MajorVersion::FMI1 => Err(fmi::Error::UnsupportedFmiVersion(version).into()), 50 | 51 | #[cfg(feature = "fmi2")] 52 | MajorVersion::FMI2 => { 53 | let import: fmi::fmi2::import::Fmi2Import = fmi::import::from_path(&options.model)?; 54 | sim::simulate_with(input_data, &options.interface, import) 55 | } 56 | 57 | #[cfg(feature = "fmi3")] 58 | MajorVersion::FMI3 => { 59 | let import: fmi::fmi3::import::Fmi3Import = fmi::import::from_path(&options.model)?; 60 | sim::simulate_with(input_data, &options.interface, import) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /fmi-sim/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | fn main() -> anyhow::Result<()> { 4 | sensible_env_logger::try_init_timed!()?; 5 | 6 | let options = fmi_sim::options::FmiSimOptions::try_parse()?; 7 | let (outputs, stats) = fmi_sim::simulate(&options)?; 8 | 9 | log::info!( 10 | "Simulation finished at t = {:.1} after {} steps.", 11 | stats.end_time, 12 | stats.num_steps 13 | ); 14 | 15 | if let Some(output_file) = options.output_file { 16 | let file = std::fs::File::create(output_file).unwrap(); 17 | arrow::csv::writer::WriterBuilder::new() 18 | .with_delimiter(options.separator as _) 19 | .with_header(true) 20 | .build(file) 21 | .write(&outputs)?; 22 | } else { 23 | println!( 24 | "Outputs:\n{}", 25 | arrow::util::pretty::pretty_format_batches(&[outputs]).unwrap() 26 | ); 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /fmi-sim/src/options.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | #[derive(Default, Debug, Clone, clap::ValueEnum)] 4 | pub enum SolverArg { 5 | /// Euler solver 6 | #[clap(name = "euler")] 7 | #[default] 8 | Euler, 9 | } 10 | 11 | #[derive(Default, Debug, clap::Args)] 12 | /// Perform a ModelExchange simulation 13 | pub struct ModelExchangeOptions { 14 | #[command(flatten)] 15 | pub common: CommonOptions, 16 | 17 | /// The solver to use 18 | #[arg(long, default_value = "euler")] 19 | pub solver: SolverArg, 20 | } 21 | 22 | #[derive(Default, Debug, clap::Args)] 23 | /// Perform a CoSimulation simulation 24 | pub struct CoSimulationOptions { 25 | #[command(flatten)] 26 | pub common: CommonOptions, 27 | 28 | /// Use event mode 29 | #[arg(long)] 30 | pub event_mode_used: bool, 31 | 32 | /// Support early-return in Co-Simulation. 33 | #[arg(long)] 34 | pub early_return_allowed: bool, 35 | } 36 | 37 | #[derive(Debug, clap::Subcommand)] 38 | pub enum Interface { 39 | #[cfg(feature = "me")] 40 | #[command(alias = "me")] 41 | ModelExchange(ModelExchangeOptions), 42 | 43 | #[cfg(feature = "cs")] 44 | #[command(alias = "cs")] 45 | CoSimulation(CoSimulationOptions), 46 | 47 | /// Perform a ScheduledExecution simulation 48 | #[cfg(feature = "se")] 49 | ScheduledExecution(CommonOptions), 50 | } 51 | 52 | impl Default for Interface { 53 | fn default() -> Self { 54 | // if only CS is enabled, use CS as default 55 | #[cfg(all(not(feature = "me"), feature = "cs"))] 56 | { 57 | Self::CoSimulation(Default::default()) 58 | } 59 | 60 | // if both ME and CS are enabled, use CS as default 61 | #[cfg(all(feature = "me", feature = "cs"))] 62 | { 63 | Self::CoSimulation(Default::default()) 64 | } 65 | 66 | // if only ME is enabled, use ME as default 67 | #[cfg(all(feature = "me", not(feature = "cs")))] 68 | { 69 | Self::ModelExchange(Default::default()) 70 | } 71 | } 72 | } 73 | 74 | impl Display for Interface { 75 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 76 | match self { 77 | #[cfg(feature = "me")] 78 | Self::ModelExchange(_) => write!(f, "ModelExchange"), 79 | #[cfg(feature = "cs")] 80 | Self::CoSimulation(_) => write!(f, "CoSimulation"), 81 | #[cfg(feature = "se")] 82 | Self::ScheduledExecution(_) => write!(f, "ScheduledExecution"), 83 | } 84 | } 85 | } 86 | 87 | #[derive(Default, Debug, clap::Args)] 88 | pub struct CommonOptions { 89 | /// File containing initial serialized FMU state. 90 | #[arg(long)] 91 | pub initial_fmu_state_file: Option, 92 | 93 | /// File to write final serialized FMU state. 94 | #[arg(long)] 95 | pub final_fmu_state_file: Option, 96 | 97 | /// List of initial values to set before simulation starts. The format is 98 | /// "variableName=value", where variableName is the name of the variable and value is the 99 | /// value to set. The value must be of the same type as the variable. The variable name must 100 | /// be a valid FMI variable name, i.e. it must be a valid identifier and it must be unique. 101 | #[arg(short = 'v')] 102 | pub initial_values: Vec, 103 | 104 | /// Print also left limit values at event points to the output file to investigate event 105 | /// behaviour. Default is to only print values after event handling. 106 | #[arg(short = 'd')] 107 | pub print_left_limit: bool, 108 | 109 | /// Print all variables to the output file. Default is to only print outputs. 110 | #[arg(long = "print-all")] 111 | pub print_all_variables: bool, 112 | 113 | /// For ME simulation: Decides step size to use in forward Euler. 114 | /// For CS simulation: Decides communication step size for the stepping. 115 | /// Observe that if a small stepSize is used the number of saved outputs will still be limited 116 | /// by the number of output points. Default is to calculated a step size from the number of 117 | /// output points. See the -n option for how the number of outputs is set. 118 | #[arg(long = "ss")] 119 | pub step_size: Option, 120 | 121 | #[arg(long = "output-interval")] 122 | pub output_interval: Option, 123 | 124 | /// Maximum number of output points. "-n 0" means output at every step and the number of 125 | /// outputs are decided by the -h option. Observe that no interpolation is used, output points 126 | /// are taken at the steps. 127 | #[arg(short = 'n', default_value = "500")] 128 | pub num_steps: usize, 129 | 130 | /// Simulation start time, default is to use information from 'DefaultExperiment' as specified 131 | /// in the model description XML. 132 | #[arg(short = 's')] 133 | pub start_time: Option, 134 | 135 | /// Simulation stop time, default is to use information from 'DefaultExperiment' as specified 136 | /// in the model description XML. 137 | #[arg(short = 'f')] 138 | pub stop_time: Option, 139 | 140 | /// Relative tolerance 141 | #[arg(long)] 142 | pub tolerance: Option, 143 | } 144 | 145 | /// Simulate an FMU 146 | #[derive(Default, Debug, clap::Parser)] 147 | #[command(version, about)] 148 | pub struct FmiSimOptions { 149 | /// Which FMI interface to use 150 | #[command(subcommand)] 151 | pub interface: Interface, 152 | /// The FMU model to read 153 | #[arg(long)] 154 | pub model: std::path::PathBuf, 155 | /// Name of the CSV file name with input data. 156 | #[arg(short = 'i', long)] 157 | pub input_file: Option, 158 | /// Simulation result output CSV file name. Default is to use standard output. 159 | #[arg(short = 'o', long)] 160 | pub output_file: Option, 161 | /// Separator to be used in CSV input/output. 162 | #[arg(short = 'c', default_value = ",")] 163 | pub separator: char, 164 | /// Mangle variable names to avoid quoting (needed for some CSV importing applications, but not 165 | /// according to the CrossCheck rules). 166 | #[arg(short = 'm')] 167 | pub mangle_names: bool, 168 | } 169 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi2/cs.rs: -------------------------------------------------------------------------------- 1 | use fmi::{ 2 | fmi2::{ 3 | import::Fmi2Import, 4 | instance::{CoSimulation, InstanceCS}, 5 | Fmi2Error, 6 | }, 7 | traits::{FmiInstance, FmiStatus}, 8 | }; 9 | 10 | use crate::{ 11 | sim::{ 12 | interpolation::Linear, 13 | io::StartValues, 14 | params::SimParams, 15 | traits::{InstRecordValues, InstSetValues, SimApplyStartValues}, 16 | InputState, RecorderState, SimState, SimStateTrait, SimStats, 17 | }, 18 | Error, 19 | }; 20 | 21 | impl<'a> SimStateTrait<'a, InstanceCS<'a>> for SimState> { 22 | fn new( 23 | import: &'a Fmi2Import, 24 | sim_params: SimParams, 25 | input_state: InputState>, 26 | recorder_state: RecorderState>, 27 | ) -> Result { 28 | log::trace!("Instantiating CS Simulation: {sim_params:#?}"); 29 | let inst = import.instantiate_cs("inst1", true, true)?; 30 | Ok(Self { 31 | sim_params, 32 | input_state, 33 | recorder_state, 34 | inst, 35 | next_event_time: None, 36 | }) 37 | } 38 | } 39 | 40 | impl SimApplyStartValues> for SimState> { 41 | fn apply_start_values( 42 | &mut self, 43 | start_values: &StartValues<::ValueRef>, 44 | ) -> Result<(), Error> { 45 | start_values.variables.iter().for_each(|(vr, ary)| { 46 | self.inst.set_array(&[*vr], ary); 47 | }); 48 | Ok(()) 49 | } 50 | } 51 | 52 | impl<'a> SimState> { 53 | /// Main loop of the co-simulation 54 | pub fn main_loop(&mut self) -> Result { 55 | let mut stats = SimStats::default(); 56 | 57 | loop { 58 | let time = self.sim_params.start_time 59 | + stats.num_steps as f64 * self.sim_params.output_interval; 60 | 61 | self.inst 62 | .record_outputs(time, &mut self.recorder_state) 63 | .unwrap(); 64 | self.input_state 65 | .apply_input::(time, &mut self.inst, true, true, false) 66 | .unwrap(); 67 | 68 | if time >= self.sim_params.stop_time { 69 | stats.end_time = time; 70 | break; 71 | } 72 | 73 | match self 74 | .inst 75 | .do_step(time, self.sim_params.output_interval, true) 76 | .ok() 77 | { 78 | Err(Fmi2Error::Discard) => { 79 | if self.inst.terminated()? { 80 | let time = self.inst.last_successful_time()?; 81 | 82 | self.inst 83 | .record_outputs(time, &mut self.recorder_state) 84 | .unwrap(); 85 | 86 | stats.end_time = time; 87 | break; 88 | } 89 | } 90 | Err(e) => return Err(e), 91 | _ => {} 92 | } 93 | 94 | stats.num_steps += 1; 95 | } 96 | 97 | //TODO save final FMU state 98 | 99 | self.inst.terminate().ok()?; 100 | 101 | Ok(stats) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi2/io.rs: -------------------------------------------------------------------------------- 1 | //! FMI2-specific input and output implementation 2 | 3 | use arrow::{ 4 | array::{ 5 | downcast_array, ArrayRef, AsArray, BooleanBuilder, Float64Array, Float64Builder, 6 | Int32Builder, 7 | }, 8 | datatypes::{DataType, Float64Type, Int32Type}, 9 | }; 10 | use fmi::{ 11 | fmi2::instance::Common, 12 | traits::{FmiInstance, FmiStatus}, 13 | }; 14 | use itertools::Itertools; 15 | 16 | use crate::sim::{ 17 | interpolation::{Interpolate, PreLookup}, 18 | io::Recorder, 19 | traits::{InstRecordValues, InstSetValues}, 20 | RecorderState, 21 | }; 22 | 23 | macro_rules! impl_recorder { 24 | ($getter:ident, $builder_type:ident, $inst:expr, $vr:ident, $builder:ident) => {{ 25 | let mut value = [std::default::Default::default()]; 26 | $inst.$getter(&[*$vr], &mut value).ok()?; 27 | $builder 28 | .as_any_mut() 29 | .downcast_mut::<$builder_type>() 30 | .expect(concat!("column is not ", stringify!($builder_type))) 31 | .append_value(value[0]); 32 | }}; 33 | } 34 | 35 | macro_rules! impl_record_values { 36 | ($inst:ty) => { 37 | impl InstRecordValues for $inst { 38 | fn record_outputs( 39 | &mut self, 40 | time: f64, 41 | recorder: &mut RecorderState, 42 | ) -> anyhow::Result<()> { 43 | log::trace!("Recording variables at time {}", time); 44 | 45 | recorder.time.append_value(time); 46 | for Recorder { 47 | field, 48 | value_reference: vr, 49 | builder, 50 | } in &mut recorder.recorders 51 | { 52 | match field.data_type() { 53 | DataType::Boolean => { 54 | let mut value = [std::default::Default::default()]; 55 | self.get_boolean(&[*vr], &mut value).ok()?; 56 | builder 57 | .as_any_mut() 58 | .downcast_mut::() 59 | .expect(concat!("column is not ", stringify!($builder_type))) 60 | .append_value(value[0] > 0); 61 | } 62 | DataType::Int32 => { 63 | impl_recorder!(get_integer, Int32Builder, self, vr, builder) 64 | } 65 | DataType::Float64 => { 66 | impl_recorder!(get_real, Float64Builder, self, vr, builder) 67 | } 68 | _ => unimplemented!("Unsupported data type: {:?}", field.data_type()), 69 | } 70 | } 71 | Ok(()) 72 | } 73 | } 74 | }; 75 | } 76 | 77 | macro_rules! impl_set_values { 78 | ($t:ty) => { 79 | impl InstSetValues for $t { 80 | fn set_array(&mut self, vrs: &[Self::ValueRef], values: &ArrayRef) { 81 | match values.data_type() { 82 | DataType::Boolean => { 83 | let values = values 84 | .as_boolean() 85 | .iter() 86 | .map(|x| x.unwrap() as i32) 87 | .collect_vec(); 88 | self.set_boolean(vrs, &values); 89 | } 90 | DataType::Int32 => { 91 | self.set_integer(vrs, values.as_primitive::().values()); 92 | } 93 | DataType::Float64 => { 94 | self.set_real(vrs, values.as_primitive::().values()); 95 | } 96 | DataType::Utf8 => { 97 | self.set_string(vrs, values.as_string::().iter().flatten()); 98 | } 99 | _ => unimplemented!("Unsupported data type"), 100 | } 101 | } 102 | 103 | fn set_interpolated( 104 | &mut self, 105 | vr: ::ValueRef, 106 | pl: &PreLookup, 107 | array: &ArrayRef, 108 | ) -> anyhow::Result<()> { 109 | match array.data_type() { 110 | DataType::Boolean => todo!(), 111 | DataType::Int32 => { 112 | let array = array.as_primitive::(); 113 | let value = I::interpolate(pl, &array); 114 | self.set_integer(&[vr], &[value]).ok()?; 115 | } 116 | DataType::Float64 => { 117 | let array: Float64Array = downcast_array(&array); 118 | let value = I::interpolate(pl, &array); 119 | self.set_real(&[vr], &[value]).ok()?; 120 | } 121 | _ => unimplemented!("Unsupported data type: {:?}", array.data_type()), 122 | } 123 | Ok(()) 124 | } 125 | } 126 | }; 127 | } 128 | 129 | #[cfg(feature = "cs")] 130 | impl_set_values!(fmi::fmi2::instance::InstanceCS<'_>); 131 | #[cfg(feature = "cs")] 132 | impl_record_values!(fmi::fmi2::instance::InstanceCS<'_>); 133 | 134 | #[cfg(feature = "me")] 135 | impl_set_values!(fmi::fmi2::instance::InstanceME<'_>); 136 | #[cfg(feature = "me")] 137 | impl_record_values!(fmi::fmi2::instance::InstanceME<'_>); 138 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi2/me.rs: -------------------------------------------------------------------------------- 1 | use fmi::{ 2 | fmi2::{import::Fmi2Import, instance::InstanceME}, 3 | traits::FmiInstance, 4 | }; 5 | 6 | use crate::{ 7 | sim::{ 8 | io::StartValues, 9 | params::SimParams, 10 | traits::{InstSetValues, SimApplyStartValues}, 11 | InputState, RecorderState, SimState, SimStateTrait, 12 | }, 13 | Error, 14 | }; 15 | 16 | impl<'a> SimStateTrait<'a, InstanceME<'a>> for SimState> { 17 | fn new( 18 | import: &'a Fmi2Import, 19 | sim_params: SimParams, 20 | input_state: InputState>, 21 | recorder_state: RecorderState>, 22 | ) -> Result { 23 | log::trace!("Instantiating ME Simulation: {sim_params:#?}"); 24 | let inst = import.instantiate_me("inst1", true, true)?; 25 | Ok(Self { 26 | sim_params, 27 | input_state, 28 | recorder_state, 29 | inst, 30 | next_event_time: None, 31 | }) 32 | } 33 | } 34 | 35 | impl SimApplyStartValues> for SimState> { 36 | fn apply_start_values( 37 | &mut self, 38 | start_values: &StartValues<::ValueRef>, 39 | ) -> Result<(), Error> { 40 | start_values.variables.iter().for_each(|(vr, ary)| { 41 | self.inst.set_array(&[*vr], ary); 42 | }); 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi2/mod.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::RecordBatch; 2 | 3 | use fmi::fmi2::import::Fmi2Import; 4 | 5 | use crate::{ 6 | options::{CoSimulationOptions, ModelExchangeOptions}, 7 | sim::{ 8 | traits::{ImportSchemaBuilder, SimInitialize}, 9 | InputState, RecorderState, SimState, SimStateTrait, 10 | }, 11 | Error, 12 | }; 13 | 14 | use super::{params::SimParams, traits::FmiSim, SimStats}; 15 | 16 | #[cfg(feature = "cs")] 17 | mod cs; 18 | mod io; 19 | #[cfg(feature = "me")] 20 | mod me; 21 | mod schema; 22 | 23 | impl FmiSim for Fmi2Import { 24 | #[cfg(feature = "me")] 25 | fn simulate_me( 26 | &self, 27 | options: &ModelExchangeOptions, 28 | input_data: Option, 29 | ) -> Result<(RecordBatch, SimStats), Error> { 30 | use crate::sim::{solver, traits::SimMe}; 31 | use fmi::{fmi2::instance::InstanceME, traits::FmiImport}; 32 | 33 | let sim_params = 34 | SimParams::new_from_options(&options.common, self.model_description(), true, false); 35 | 36 | let start_values = self.parse_start_values(&options.common.initial_values)?; 37 | let input_state = InputState::new(self, input_data)?; 38 | let output_state = RecorderState::new(self, &sim_params); 39 | 40 | let mut sim_state = 41 | SimState::::new(self, sim_params, input_state, output_state)?; 42 | sim_state.initialize(start_values, options.common.initial_fmu_state_file.as_ref())?; 43 | let stats = sim_state.main_loop::(())?; 44 | 45 | Ok((sim_state.recorder_state.finish(), stats)) 46 | } 47 | 48 | #[cfg(feature = "cs")] 49 | fn simulate_cs( 50 | &self, 51 | options: &CoSimulationOptions, 52 | input_data: Option, 53 | ) -> Result<(RecordBatch, SimStats), Error> { 54 | use fmi::{fmi2::instance::InstanceCS, traits::FmiImport}; 55 | 56 | let sim_params = SimParams::new_from_options( 57 | &options.common, 58 | self.model_description(), 59 | options.event_mode_used, 60 | options.early_return_allowed, 61 | ); 62 | 63 | let start_values = self.parse_start_values(&options.common.initial_values)?; 64 | let input_state = InputState::new(self, input_data)?; 65 | let recorder_state = RecorderState::new(self, &sim_params); 66 | 67 | let mut sim_state = 68 | SimState::::new(self, sim_params, input_state, recorder_state)?; 69 | sim_state.initialize(start_values, options.common.initial_fmu_state_file.as_ref())?; 70 | let stats = sim_state.main_loop().map_err(fmi::Error::from)?; 71 | 72 | Ok((sim_state.recorder_state.finish(), stats)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi2/schema.rs: -------------------------------------------------------------------------------- 1 | use arrow::{ 2 | array::StringArray, 3 | datatypes::{Field, Fields, Schema}, 4 | }; 5 | use fmi::{ 6 | fmi2::{ 7 | import::Fmi2Import, 8 | schema::{Causality, Variability}, 9 | }, 10 | traits::FmiImport, 11 | }; 12 | 13 | use crate::sim::{io::StartValues, traits::ImportSchemaBuilder}; 14 | 15 | impl ImportSchemaBuilder for Fmi2Import 16 | where 17 | Self::ValueRef: From, 18 | { 19 | fn inputs_schema(&self) -> Schema { 20 | let input_fields = self 21 | .model_description() 22 | .model_variables 23 | .variables 24 | .iter() 25 | .filter(|v| v.causality == Causality::Input) 26 | .map(|v| Field::new(&v.name, v.elem.data_type(), false)) 27 | .collect::(); 28 | 29 | Schema::new(input_fields) 30 | } 31 | 32 | fn outputs_schema(&self) -> Schema { 33 | let time = Field::new("time", arrow::datatypes::DataType::Float64, false); 34 | let output_fields = self 35 | .model_description() 36 | .model_variables 37 | .variables 38 | .iter() 39 | .filter(|v| v.causality == Causality::Output) 40 | .map(|v| Field::new(&v.name, v.elem.data_type(), false)) 41 | .chain(std::iter::once(time)) 42 | .collect::(); 43 | 44 | Schema::new(output_fields) 45 | } 46 | 47 | fn continuous_inputs(&self) -> impl Iterator + '_ { 48 | self.model_description() 49 | .model_variables 50 | .variables 51 | .iter() 52 | .filter(|v| v.causality == Causality::Input && v.variability == Variability::Continuous) 53 | .map(|v| { 54 | ( 55 | Field::new(&v.name, v.elem.data_type(), false), 56 | v.value_reference, 57 | ) 58 | }) 59 | } 60 | 61 | fn discrete_inputs(&self) -> impl Iterator + '_ { 62 | self.model_description() 63 | .model_variables 64 | .variables 65 | .iter() 66 | .filter(|v| { 67 | v.causality == Causality::Input 68 | && (v.variability == Variability::Discrete 69 | || v.variability == Variability::Tunable) 70 | }) 71 | .map(|v| { 72 | ( 73 | Field::new(&v.name, v.elem.data_type(), false), 74 | v.value_reference, 75 | ) 76 | }) 77 | } 78 | 79 | fn outputs(&self) -> impl Iterator + '_ { 80 | self.model_description() 81 | .model_variables 82 | .variables 83 | .iter() 84 | .filter(|v| v.causality == Causality::Output) 85 | .map(|v| { 86 | ( 87 | Field::new(&v.name, v.elem.data_type(), false), 88 | v.value_reference, 89 | ) 90 | }) 91 | } 92 | 93 | fn parse_start_values( 94 | &self, 95 | start_values: &[String], 96 | ) -> anyhow::Result> { 97 | let mut variables = vec![]; 98 | 99 | for start_value in start_values { 100 | let (name, value) = start_value 101 | .split_once('=') 102 | .ok_or_else(|| anyhow::anyhow!("Invalid start value: {}", start_value))?; 103 | 104 | let var = self 105 | .model_description() 106 | .model_variables 107 | .variables 108 | .iter() 109 | .find(|v| v.name == name) 110 | .ok_or_else(|| { 111 | anyhow::anyhow!( 112 | "Invalid variable name: {name}. Valid variables are: {valid:?}", 113 | valid = self 114 | .model_description() 115 | .model_variables 116 | .variables 117 | .iter() 118 | .map(|v| &v.name) 119 | .collect::>() 120 | ) 121 | })?; 122 | 123 | let dt = var.elem.data_type(); 124 | let ary = StringArray::from(vec![value.to_string()]); 125 | let ary = arrow::compute::cast(&ary, &dt) 126 | .map_err(|e| anyhow::anyhow!("Error casting type: {e}"))?; 127 | 128 | variables.push((var.value_reference, ary)); 129 | } 130 | 131 | Ok(StartValues { 132 | structural_parameters: vec![], 133 | variables, 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi3/cs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use fmi::{ 3 | fmi3::{ 4 | import::Fmi3Import, 5 | instance::{CoSimulation, InstanceCS}, 6 | }, 7 | traits::{FmiInstance, FmiStatus}, 8 | }; 9 | 10 | use crate::{ 11 | sim::{ 12 | interpolation::Linear, 13 | params::SimParams, 14 | traits::{InstRecordValues, SimHandleEvents}, 15 | InputState, RecorderState, SimState, SimStateTrait, SimStats, 16 | }, 17 | Error, 18 | }; 19 | 20 | impl<'a> SimStateTrait<'a, InstanceCS<'a>> for SimState> { 21 | fn new( 22 | import: &'a Fmi3Import, 23 | sim_params: SimParams, 24 | input_state: InputState>, 25 | output_state: RecorderState>, 26 | ) -> Result { 27 | let inst = import.instantiate_cs( 28 | "inst1", 29 | true, 30 | true, 31 | sim_params.event_mode_used, 32 | sim_params.early_return_allowed, 33 | &[], 34 | )?; 35 | Ok(Self { 36 | sim_params, 37 | input_state, 38 | recorder_state: output_state, 39 | inst, 40 | next_event_time: None, 41 | }) 42 | } 43 | } 44 | 45 | impl<'a> SimState> { 46 | /// Main loop of the co-simulation 47 | pub fn main_loop(&mut self) -> Result { 48 | let mut stats = SimStats::default(); 49 | 50 | if self.sim_params.event_mode_used { 51 | self.inst.enter_step_mode().ok().map_err(fmi::Error::from)?; 52 | } 53 | 54 | let mut time = self.sim_params.start_time; 55 | 56 | loop { 57 | self.inst.record_outputs(time, &mut self.recorder_state)?; 58 | 59 | if time >= self.sim_params.stop_time { 60 | break; 61 | } 62 | 63 | // calculate next time point 64 | let next_regular_point = self.sim_params.start_time 65 | + (stats.num_steps + 1) as f64 * self.sim_params.output_interval; 66 | let next_input_event_time = self.input_state.next_input_event(time); 67 | // use `next_input_event` if it is earlier than `next_regular_point` 68 | let next_communication_point = next_input_event_time.min(next_regular_point); 69 | let input_event = next_regular_point > next_input_event_time; 70 | 71 | let step_size = next_communication_point - time; 72 | 73 | let mut event_encountered = false; 74 | let mut terminate_simulation = false; 75 | let mut early_return = false; 76 | let mut last_successful_time = 0.0; 77 | 78 | if self.sim_params.event_mode_used { 79 | self.input_state 80 | .apply_input::(time, &mut self.inst, false, true, false)?; 81 | } else { 82 | self.input_state 83 | .apply_input::(time, &mut self.inst, true, true, true)?; 84 | } 85 | 86 | self.inst 87 | .do_step( 88 | time, 89 | step_size, 90 | true, 91 | &mut event_encountered, 92 | &mut terminate_simulation, 93 | &mut early_return, 94 | &mut last_successful_time, 95 | ) 96 | .ok() 97 | .context("do_step")?; 98 | 99 | if early_return && !self.sim_params.early_return_allowed { 100 | panic!("Early return is not allowed."); 101 | } 102 | 103 | if terminate_simulation { 104 | break; 105 | } 106 | 107 | if early_return && last_successful_time < next_communication_point { 108 | time = last_successful_time; 109 | } else { 110 | time = next_communication_point; 111 | } 112 | 113 | if time == next_regular_point { 114 | stats.num_steps += 1; 115 | } 116 | 117 | if self.sim_params.event_mode_used && (input_event || event_encountered) { 118 | log::trace!("Event encountered at t = {time}"); 119 | self.handle_events(time, input_event, &mut terminate_simulation)?; 120 | 121 | self.inst 122 | .enter_step_mode() 123 | .ok() 124 | .context("enter_step_mode")?; 125 | } 126 | } 127 | 128 | self.inst.terminate().ok().context("terminate")?; 129 | 130 | stats.end_time = time; 131 | Ok(stats) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi3/me.rs: -------------------------------------------------------------------------------- 1 | use fmi::fmi3::{import::Fmi3Import, instance::InstanceME}; 2 | 3 | use crate::{ 4 | sim::{params::SimParams, InputState, RecorderState, SimState, SimStateTrait}, 5 | Error, 6 | }; 7 | 8 | impl<'a> SimStateTrait<'a, InstanceME<'a>> for SimState> { 9 | fn new( 10 | import: &'a Fmi3Import, 11 | sim_params: SimParams, 12 | input_state: InputState>, 13 | recorder_state: RecorderState>, 14 | ) -> Result { 15 | let inst = import.instantiate_me("inst1", true, true)?; 16 | Ok(Self { 17 | sim_params, 18 | input_state, 19 | recorder_state, 20 | inst, 21 | next_event_time: None, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi3/mod.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::RecordBatch; 2 | 3 | use fmi::{ 4 | fmi3::{import::Fmi3Import, instance::Common}, 5 | traits::{FmiImport, FmiInstance, FmiStatus}, 6 | }; 7 | 8 | use crate::{ 9 | options::{CoSimulationOptions, ModelExchangeOptions}, 10 | sim::{ 11 | params::SimParams, 12 | traits::{ImportSchemaBuilder, SimInitialize}, 13 | InputState, RecorderState, SimState, SimStateTrait, 14 | }, 15 | Error, 16 | }; 17 | 18 | use super::{ 19 | io::StartValues, 20 | traits::{FmiSim, InstSetValues}, 21 | SimStats, 22 | }; 23 | 24 | #[cfg(feature = "cs")] 25 | mod cs; 26 | mod io; 27 | #[cfg(feature = "me")] 28 | mod me; 29 | mod schema; 30 | 31 | macro_rules! impl_sim_apply_start_values { 32 | ($inst:ty) => { 33 | impl super::traits::SimApplyStartValues<$inst> for super::SimState<$inst> { 34 | fn apply_start_values( 35 | &mut self, 36 | start_values: &StartValues<<$inst as FmiInstance>::ValueRef>, 37 | ) -> Result<(), Error> { 38 | if !start_values.structural_parameters.is_empty() { 39 | self.inst 40 | .enter_configuration_mode() 41 | .ok() 42 | .map_err(fmi::Error::from)?; 43 | for (vr, ary) in &start_values.structural_parameters { 44 | //log::trace!("Setting structural parameter `{}`", (*vr).into()); 45 | self.inst.set_array(&[(*vr)], ary); 46 | } 47 | self.inst 48 | .exit_configuration_mode() 49 | .ok() 50 | .map_err(fmi::Error::from)?; 51 | } 52 | 53 | start_values.variables.iter().for_each(|(vr, ary)| { 54 | self.inst.set_array(&[*vr], ary); 55 | }); 56 | 57 | Ok(()) 58 | } 59 | } 60 | }; 61 | } 62 | 63 | #[cfg(feature = "me")] 64 | impl_sim_apply_start_values!(fmi::fmi3::instance::InstanceME<'_>); 65 | #[cfg(feature = "cs")] 66 | impl_sim_apply_start_values!(fmi::fmi3::instance::InstanceCS<'_>); 67 | 68 | impl FmiSim for Fmi3Import { 69 | #[cfg(feature = "me")] 70 | fn simulate_me( 71 | &self, 72 | options: &ModelExchangeOptions, 73 | input_data: Option, 74 | ) -> Result<(RecordBatch, SimStats), Error> { 75 | use crate::sim::{solver, traits::SimMe}; 76 | use fmi::fmi3::instance::InstanceME; 77 | 78 | let sim_params = 79 | SimParams::new_from_options(&options.common, self.model_description(), true, false); 80 | 81 | let start_values = self.parse_start_values(&options.common.initial_values)?; 82 | let input_state = InputState::new(self, input_data)?; 83 | let recorder_state = RecorderState::new(self, &sim_params); 84 | 85 | let mut sim_state = 86 | SimState::::new(self, sim_params, input_state, recorder_state)?; 87 | sim_state.initialize(start_values, options.common.initial_fmu_state_file.as_ref())?; 88 | let stats = sim_state.main_loop::(())?; 89 | 90 | Ok((sim_state.recorder_state.finish(), stats)) 91 | } 92 | 93 | #[cfg(feature = "cs")] 94 | fn simulate_cs( 95 | &self, 96 | options: &CoSimulationOptions, 97 | input_data: Option, 98 | ) -> Result<(RecordBatch, SimStats), Error> { 99 | use fmi::fmi3::instance::InstanceCS; 100 | 101 | let sim_params = SimParams::new_from_options( 102 | &options.common, 103 | self.model_description(), 104 | options.event_mode_used, 105 | options.early_return_allowed, 106 | ); 107 | 108 | let start_values = self.parse_start_values(&options.common.initial_values)?; 109 | let input_state = InputState::new(self, input_data)?; 110 | let output_state = RecorderState::new(self, &sim_params); 111 | 112 | let mut sim_state = 113 | SimState::::new(self, sim_params, input_state, output_state)?; 114 | sim_state.initialize(start_values, options.common.initial_fmu_state_file.as_ref())?; 115 | let stats = sim_state.main_loop()?; 116 | 117 | Ok((sim_state.recorder_state.finish(), stats)) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/fmi3/schema.rs: -------------------------------------------------------------------------------- 1 | use arrow::{ 2 | array::{ArrayRef, StringArray}, 3 | datatypes::{DataType, Field, Fields, Schema}, 4 | }; 5 | use fmi::{ 6 | fmi3::{import::Fmi3Import, schema::Causality}, 7 | schema::fmi3::Variability, 8 | traits::FmiImport, 9 | }; 10 | 11 | use crate::sim::{io::StartValues, traits::ImportSchemaBuilder}; 12 | 13 | impl ImportSchemaBuilder for Fmi3Import 14 | where 15 | Self::ValueRef: From, 16 | { 17 | fn inputs_schema(&self) -> Schema { 18 | let input_fields = self 19 | .model_description() 20 | .model_variables 21 | .iter_abstract() 22 | .filter(|v| v.causality() == Causality::Input) 23 | .map(|v| Field::new(v.name(), v.data_type().into(), false)) 24 | .collect::(); 25 | 26 | Schema::new(input_fields) 27 | } 28 | 29 | fn outputs_schema(&self) -> Schema { 30 | let time = Field::new("time", DataType::Float64, false); 31 | let output_fields = self 32 | .model_description() 33 | .model_variables 34 | .iter_abstract() 35 | .filter(|v| v.causality() == Causality::Output) 36 | .map(|v| Field::new(v.name(), v.data_type().into(), false)) 37 | .chain(std::iter::once(time)) 38 | .collect::(); 39 | 40 | Schema::new(output_fields) 41 | } 42 | 43 | fn continuous_inputs(&self) -> impl Iterator + '_ { 44 | self.model_description() 45 | .model_variables 46 | .iter_abstract() 47 | .filter(|v| { 48 | v.causality() == Causality::Input 49 | && v.variability() == fmi::fmi3::schema::Variability::Continuous 50 | }) 51 | .map(|v| { 52 | ( 53 | Field::new(v.name(), v.data_type().into(), false), 54 | v.value_reference(), 55 | ) 56 | }) 57 | } 58 | 59 | fn discrete_inputs(&self) -> impl Iterator + '_ { 60 | self.model_description() 61 | .model_variables 62 | .iter_abstract() 63 | .filter(|v| { 64 | v.causality() == Causality::Input 65 | && (v.variability() == Variability::Discrete 66 | || v.variability() == Variability::Tunable) 67 | }) 68 | .map(|v| { 69 | ( 70 | Field::new(v.name(), v.data_type().into(), false), 71 | v.value_reference(), 72 | ) 73 | }) 74 | } 75 | 76 | fn outputs(&self) -> impl Iterator { 77 | self.model_description() 78 | .model_variables 79 | .iter_abstract() 80 | .filter(|v| v.causality() == Causality::Output) 81 | .map(|v| { 82 | ( 83 | Field::new(v.name(), v.data_type().into(), false), 84 | v.value_reference(), 85 | ) 86 | }) 87 | } 88 | 89 | fn parse_start_values( 90 | &self, 91 | start_values: &[String], 92 | ) -> anyhow::Result> { 93 | let mut structural_parameters: Vec<(Self::ValueRef, ArrayRef)> = vec![]; 94 | let mut variables: Vec<(Self::ValueRef, ArrayRef)> = vec![]; 95 | 96 | for start_value in start_values { 97 | let (name, value) = start_value 98 | .split_once('=') 99 | .ok_or_else(|| anyhow::anyhow!("Invalid start value"))?; 100 | 101 | let var = self 102 | .model_description() 103 | .model_variables 104 | .iter_abstract() 105 | .find(|v| v.name() == name) 106 | .ok_or_else(|| { 107 | anyhow::anyhow!( 108 | "Invalid variable name: {name}. Valid variables are: {valid:?}", 109 | valid = self 110 | .model_description() 111 | .model_variables 112 | .iter_abstract() 113 | .map(|v| v.name()) 114 | .collect::>() 115 | ) 116 | })?; 117 | 118 | let dt = arrow::datatypes::DataType::from(var.data_type()); 119 | let ary = StringArray::from(vec![value.to_string()]); 120 | let ary = arrow::compute::cast(&ary, &dt) 121 | .map_err(|e| anyhow::anyhow!("Error casting type: {e}"))?; 122 | 123 | if var.causality() == Causality::StructuralParameter { 124 | structural_parameters.push((var.value_reference(), ary)); 125 | } else { 126 | variables.push((var.value_reference(), ary)); 127 | } 128 | } 129 | 130 | Ok(StartValues { 131 | structural_parameters, 132 | variables, 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/me.rs: -------------------------------------------------------------------------------- 1 | //! Model-Exchange simulation generic across FMI versions. 2 | 3 | use fmi::traits::{FmiEventHandler, FmiInstance, FmiModelExchange, FmiStatus}; 4 | 5 | use crate::Error; 6 | 7 | use super::{ 8 | interpolation::Linear, 9 | solver::Solver, 10 | traits::{ImportSchemaBuilder, InstRecordValues, InstSetValues, SimHandleEvents, SimMe}, 11 | SimState, SimStats, 12 | }; 13 | 14 | impl SimMe for SimState 15 | where 16 | Inst: FmiInstance + FmiModelExchange + InstSetValues + InstRecordValues + FmiEventHandler, 17 | Inst::Import: ImportSchemaBuilder, 18 | { 19 | fn main_loop(&mut self, solver_params: S::Params) -> Result 20 | where 21 | S: Solver, 22 | { 23 | let mut stats = SimStats::default(); 24 | self.inst 25 | .enter_continuous_time_mode() 26 | .ok() 27 | .map_err(Into::into)?; 28 | 29 | let nx = self.inst.get_number_of_continuous_state_values(); 30 | let nz = self.inst.get_number_of_event_indicator_values(); 31 | 32 | let mut solver = S::new( 33 | self.sim_params.start_time, 34 | self.sim_params.tolerance.unwrap_or_default(), 35 | nx, 36 | nz, 37 | solver_params, 38 | ); 39 | 40 | let mut time = self.sim_params.start_time; 41 | 42 | loop { 43 | self.inst.record_outputs(time, &mut self.recorder_state)?; 44 | 45 | if time >= self.sim_params.stop_time { 46 | break; 47 | } 48 | 49 | // calculate next time point 50 | let next_regular_point = self.sim_params.start_time 51 | + (stats.num_steps + 1) as f64 * self.sim_params.output_interval; 52 | let next_input_event_time = self.input_state.next_input_event(time); 53 | 54 | let input_event = next_regular_point >= next_input_event_time; 55 | let time_event = next_regular_point >= self.next_event_time.unwrap_or(f64::INFINITY); 56 | 57 | // Use the earliest of [next_input_event, next_event_time, and next_regular_point] 58 | let next_communication_point = if input_event || time_event { 59 | next_input_event_time.min(self.next_event_time.unwrap_or(f64::INFINITY)) 60 | } else { 61 | next_regular_point 62 | }; 63 | 64 | let (time_reached, state_event) = 65 | solver.step(&mut self.inst, next_communication_point)?; 66 | time = time_reached; 67 | 68 | self.inst.set_time(time).ok().map_err(Into::into)?; 69 | 70 | self.input_state 71 | .apply_input::(time, &mut self.inst, false, true, false)?; 72 | 73 | if time == next_regular_point { 74 | stats.num_steps += 1; 75 | } 76 | 77 | let mut step_event = false; 78 | let mut terminate = false; 79 | 80 | self.inst 81 | .completed_integrator_step(true, &mut step_event, &mut terminate) 82 | .ok() 83 | .map_err(Into::into)?; 84 | 85 | if terminate { 86 | log::info!("Termination requested by FMU"); 87 | break; 88 | } 89 | 90 | if input_event || time_event || state_event || step_event { 91 | log::trace!("Event encountered at t = {time}. [INPUT/TIME/STATE/STEP] = [{input_event}/{time_event}/{state_event}/{step_event}]"); 92 | stats.num_events += 1; 93 | let mut terminate = false; 94 | let reset_solver = self.handle_events(time, input_event, &mut terminate)?; 95 | 96 | if terminate { 97 | break; 98 | } 99 | 100 | self.inst 101 | .enter_continuous_time_mode() 102 | .ok() 103 | .map_err(Into::into)?; 104 | 105 | if reset_solver { 106 | solver.reset(&mut self.inst, time)?; 107 | } 108 | } 109 | } 110 | 111 | self.inst.terminate().ok().map_err(Into::into)?; 112 | 113 | Ok(stats) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/params.rs: -------------------------------------------------------------------------------- 1 | use fmi::schema::traits::DefaultExperiment; 2 | 3 | use crate::options::CommonOptions; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct SimParams { 7 | /// Start time of the simulation 8 | pub start_time: f64, 9 | /// Stop time of the simulation 10 | pub stop_time: f64, 11 | /// Output interval 12 | pub output_interval: f64, 13 | /// Tolerance 14 | pub tolerance: Option, 15 | /// Use event mode 16 | pub event_mode_used: bool, 17 | /// Support early-return in Co-Simulation. 18 | pub early_return_allowed: bool, 19 | } 20 | 21 | impl SimParams { 22 | /// Create a new `SimParams` from the given `SimOptions` and `DefaultExperiment`. 23 | /// 24 | /// Values from `SimOptions` take precedence over values from `DefaultExperiment`. 25 | pub fn new_from_options( 26 | options: &CommonOptions, 27 | default_experiment: &DE, 28 | event_mode_used: bool, 29 | early_return_allowed: bool, 30 | ) -> Self 31 | where 32 | DE: DefaultExperiment, 33 | { 34 | let start_time = options 35 | .start_time 36 | .or(default_experiment.start_time()) 37 | .unwrap_or(0.0); 38 | 39 | let stop_time = options 40 | .stop_time 41 | .or(default_experiment.stop_time()) 42 | .unwrap_or(1.0); 43 | 44 | let output_interval = options 45 | .output_interval 46 | .or(default_experiment.step_size()) 47 | .unwrap_or_else(|| (stop_time - start_time) / 500.0); 48 | 49 | if output_interval <= 0.0 { 50 | panic!("`output_interval` must be positive."); 51 | } 52 | 53 | let tolerance = options.tolerance.or(default_experiment.tolerance()); 54 | 55 | Self { 56 | start_time, 57 | stop_time, 58 | output_interval, 59 | tolerance, 60 | event_mode_used, 61 | early_return_allowed, 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/solver/euler.rs: -------------------------------------------------------------------------------- 1 | use super::{Model, Solver, SolverError}; 2 | 3 | pub struct Euler { 4 | /// Current time 5 | time: f64, 6 | /// Continuous states 7 | x: Vec, 8 | /// Derivatives of continuous states 9 | dx: Vec, 10 | /// Event indicators 11 | z: Vec, 12 | /// Previous event indicators 13 | prez: Vec, 14 | } 15 | 16 | impl Solver for Euler { 17 | type Params = (); 18 | 19 | fn new(start_time: f64, _tol: f64, nx: usize, nz: usize, _solver_params: Self::Params) -> Self { 20 | Self { 21 | time: start_time, 22 | x: vec![0.0; nx], 23 | dx: vec![0.0; nx], 24 | z: vec![0.0; nz], 25 | prez: vec![0.0; nz], 26 | } 27 | } 28 | 29 | fn step(&mut self, model: &mut M, next_time: f64) -> Result<(f64, bool), SolverError> { 30 | let dt = next_time - self.time; 31 | 32 | if !self.x.is_empty() { 33 | model.get_continuous_states(&mut self.x); 34 | model.get_continuous_state_derivatives(&mut self.dx); 35 | 36 | for i in 0..self.x.len() { 37 | self.x[i] += self.dx[i] * dt; 38 | } 39 | 40 | model.set_continuous_states(&self.x); 41 | } 42 | 43 | let mut state_event = false; 44 | 45 | if !self.z.is_empty() { 46 | model.get_event_indicators(&mut self.z); 47 | 48 | self.z 49 | .iter() 50 | .zip(self.prez.iter_mut()) 51 | .for_each(|(z, prez)| { 52 | // crossed zero going positive 53 | let cross_pos = *prez <= 0.0 && *z > 0.0; 54 | // crossed zero going negative 55 | let cross_neg = *prez > 0.0 && *z <= 0.0; 56 | state_event = cross_pos || cross_neg || state_event; 57 | *prez = *z; 58 | }); 59 | } 60 | self.time = next_time; 61 | 62 | Ok((self.time, state_event)) 63 | } 64 | 65 | fn reset(&mut self, model: &mut M, _time: f64) -> Result<(), SolverError> { 66 | if !self.z.is_empty() { 67 | model.get_event_indicators(&mut self.z); 68 | } 69 | Ok(()) 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | 77 | struct SimpleModel; 78 | 79 | impl Model for SimpleModel { 80 | fn get_continuous_states(&mut self, x: &mut [f64]) { 81 | x[0] = 0.0; 82 | } 83 | 84 | fn set_continuous_states(&mut self, states: &[f64]) { 85 | assert_eq!(states[0], 1.0); 86 | } 87 | 88 | fn get_continuous_state_derivatives(&mut self, dx: &mut [f64]) { 89 | dx[0] = 1.0; 90 | } 91 | 92 | fn get_event_indicators(&mut self, z: &mut [f64]) { 93 | z[0] = 0.0; 94 | } 95 | } 96 | 97 | #[test] 98 | fn test_euler() { 99 | let mut euler = >::new(0.0, 1e-6, 1, 1, ()); 100 | let (time, state_event) = euler.step(&mut SimpleModel, 1.0).unwrap(); 101 | assert_eq!(time, 1.0); 102 | assert_eq!(state_event, false); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/solver/mod.rs: -------------------------------------------------------------------------------- 1 | mod euler; 2 | 3 | pub use euler::Euler; 4 | use fmi::traits::FmiModelExchange; 5 | 6 | pub trait Model { 7 | fn get_continuous_states(&mut self, x: &mut [f64]); 8 | fn set_continuous_states(&mut self, states: &[f64]); 9 | fn get_continuous_state_derivatives(&mut self, dx: &mut [f64]); 10 | fn get_event_indicators(&mut self, z: &mut [f64]); 11 | } 12 | 13 | impl Model for Inst { 14 | fn get_continuous_states(&mut self, x: &mut [f64]) { 15 | FmiModelExchange::get_continuous_states(self, x); 16 | } 17 | 18 | fn set_continuous_states(&mut self, states: &[f64]) { 19 | FmiModelExchange::set_continuous_states(self, states); 20 | } 21 | 22 | fn get_continuous_state_derivatives(&mut self, dx: &mut [f64]) { 23 | FmiModelExchange::get_continuous_state_derivatives(self, dx); 24 | } 25 | 26 | fn get_event_indicators(&mut self, z: &mut [f64]) { 27 | FmiModelExchange::get_event_indicators(self, z); 28 | } 29 | } 30 | 31 | #[derive(Debug, thiserror::Error)] 32 | pub enum SolverError { 33 | #[error("Step error")] 34 | StepError, 35 | } 36 | 37 | pub trait Solver { 38 | /// Solver parameters 39 | type Params; 40 | 41 | /// Create a new Solver instance. 42 | /// # Arguments 43 | /// * `nx` - The number of continuous states. 44 | /// * `nz` - The number of event indicators. 45 | fn new(start_time: f64, tolerance: f64, nx: usize, nz: usize, params: Self::Params) -> Self; 46 | 47 | /// Perform a single step of the solver. 48 | /// 49 | /// # Arguments 50 | /// * `model` - The model to be simulated. 51 | /// * `next_time` - The time at which the simulation should stop. 52 | /// 53 | /// # Returns 54 | /// A tuple of (`time_reached`, `state_event`) 55 | fn step(&mut self, model: &mut M, next_time: f64) -> Result<(f64, bool), SolverError>; 56 | 57 | /// Reset the solver 58 | fn reset(&mut self, model: &mut M, time: f64) -> Result<(), SolverError>; 59 | } 60 | 61 | /// A dummy solver that does nothing. 62 | pub struct DummySolver; 63 | 64 | impl Solver for DummySolver { 65 | type Params = (); 66 | fn new( 67 | _start_time: f64, 68 | _tolerance: f64, 69 | _nx: usize, 70 | _nz: usize, 71 | _params: Self::Params, 72 | ) -> Self { 73 | Self 74 | } 75 | 76 | fn step(&mut self, _model: &mut M, _next_time: f64) -> Result<(f64, bool), SolverError> { 77 | Ok((0.0, false)) 78 | } 79 | 80 | fn reset(&mut self, _model: &mut M, _time: f64) -> Result<(), SolverError> { 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/traits.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use arrow::{ 4 | array::{ArrayRef, RecordBatch}, 5 | datatypes::{Field, Schema}, 6 | }; 7 | use fmi::traits::{FmiImport, FmiInstance}; 8 | 9 | use crate::{ 10 | options::{CoSimulationOptions, ModelExchangeOptions}, 11 | Error, 12 | }; 13 | 14 | use super::{ 15 | interpolation::{Interpolate, PreLookup}, 16 | io::StartValues, 17 | solver::Solver, 18 | RecorderState, SimStats, 19 | }; 20 | 21 | /// Interface for building the Arrow schema for the inputs and outputs of an FMU. 22 | pub trait ImportSchemaBuilder: FmiImport { 23 | /// Build the schema for the inputs of the model. 24 | fn inputs_schema(&self) -> Schema; 25 | /// Build the schema for the outputs of the model. 26 | fn outputs_schema(&self) -> Schema; 27 | /// Build a list of (Field, ValueReference) for the continuous inputs. 28 | fn continuous_inputs(&self) -> impl Iterator + '_; 29 | /// Build a list of Schema column (index, ValueReference) for the discrete inputs. 30 | fn discrete_inputs(&self) -> impl Iterator + '_; 31 | /// Build a list of Schema column (index, ValueReference) for the outputs. 32 | fn outputs(&self) -> impl Iterator + '_; 33 | /// Parse a list of "var=value" strings. 34 | /// 35 | /// # Returns 36 | /// A tuple of two lists of (ValueReference, Array) tuples. The first list contains any variable with 37 | /// `Causality = StructuralParameter` and the second list contains regular parameters. 38 | fn parse_start_values( 39 | &self, 40 | start_values: &[String], 41 | ) -> anyhow::Result>; 42 | } 43 | 44 | pub trait InstSetValues: FmiInstance { 45 | fn set_array( 46 | &mut self, 47 | vrs: &[::ValueRef], 48 | values: &arrow::array::ArrayRef, 49 | ); 50 | fn set_interpolated( 51 | &mut self, 52 | vr: ::ValueRef, 53 | pl: &PreLookup, 54 | array: &ArrayRef, 55 | ) -> anyhow::Result<()>; 56 | } 57 | 58 | pub trait InstRecordValues: FmiInstance + Sized { 59 | fn record_outputs( 60 | &mut self, 61 | time: f64, 62 | recorder: &mut RecorderState, 63 | ) -> anyhow::Result<()>; 64 | } 65 | 66 | /// Interface for handling events in the simulation. 67 | /// Implemented by ME in fmi2 and ME+CS in fmi3. 68 | pub trait SimHandleEvents { 69 | fn handle_events( 70 | &mut self, 71 | time: f64, 72 | input_event: bool, 73 | terminate_simulation: &mut bool, 74 | ) -> Result; 75 | } 76 | 77 | pub trait SimMe { 78 | /// Main loop of the model-exchange simulation 79 | fn main_loop(&mut self, solver_params: S::Params) -> Result 80 | where 81 | S: Solver; 82 | } 83 | 84 | pub trait SimDefaultInitialize { 85 | fn default_initialize(&mut self) -> Result<(), Error>; 86 | } 87 | 88 | pub trait SimApplyStartValues { 89 | fn apply_start_values( 90 | &mut self, 91 | start_values: &StartValues, 92 | ) -> Result<(), Error>; 93 | } 94 | 95 | pub trait SimInitialize: SimDefaultInitialize { 96 | fn initialize>( 97 | &mut self, 98 | start_values: StartValues, 99 | fmu_state_file: Option

, 100 | ) -> Result<(), Error>; 101 | } 102 | 103 | pub trait FmiSim: FmiImport + ImportSchemaBuilder { 104 | /// Simulate the model using Model Exchange. 105 | #[cfg(feature = "me")] 106 | fn simulate_me( 107 | &self, 108 | options: &ModelExchangeOptions, 109 | input_data: Option, 110 | ) -> Result<(RecordBatch, SimStats), Error>; 111 | 112 | /// Simulate the model using Co-Simulation. 113 | #[cfg(feature = "cs")] 114 | fn simulate_cs( 115 | &self, 116 | options: &CoSimulationOptions, 117 | input_data: Option, 118 | ) -> Result<(RecordBatch, SimStats), Error>; 119 | } 120 | -------------------------------------------------------------------------------- /fmi-sim/src/sim/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Seek}, 3 | path::Path, 4 | sync::Arc, 5 | }; 6 | 7 | use arrow::{ 8 | csv::{reader::Format, ReaderBuilder}, 9 | datatypes::{Field, Schema, SchemaRef}, 10 | record_batch::RecordBatch, 11 | }; 12 | use comfy_table::Table; 13 | use itertools::Itertools; 14 | 15 | pub fn read_csv_file>(path: P) -> anyhow::Result { 16 | let mut file = std::fs::File::open(&path)?; 17 | log::debug!("Reading CSV file {:?}", path.as_ref()); 18 | read_csv(&mut file) 19 | } 20 | 21 | /// Read a CSV file into a single RecordBatch. 22 | pub fn read_csv(reader: &mut R) -> anyhow::Result 23 | where 24 | R: Read + Seek, 25 | { 26 | // Infer the schema with the first 100 records 27 | let (file_schema, _) = Format::default() 28 | .with_header(true) 29 | .infer_schema(reader.by_ref(), Some(100))?; 30 | reader.rewind()?; 31 | 32 | log::debug!( 33 | "Inferred schema: {:?}", 34 | file_schema 35 | .fields() 36 | .iter() 37 | .map(|f| f.name()) 38 | .collect::>() 39 | ); 40 | 41 | let _time = Arc::new(arrow::datatypes::Field::new( 42 | "time", 43 | arrow::datatypes::DataType::Float64, 44 | false, 45 | )); 46 | 47 | // Create a non-nullible schema from the file schema 48 | let file_schema = Arc::new(Schema::new( 49 | file_schema 50 | .fields() 51 | .iter() 52 | .map(|f| Arc::new(Field::new(f.name(), f.data_type().clone(), false)) as Arc) 53 | .collect::>(), 54 | )); 55 | 56 | let reader = ReaderBuilder::new(file_schema) 57 | .with_header(true) 58 | .build(reader)?; 59 | 60 | let batches = reader.collect::, _>>()?; 61 | 62 | Ok(arrow::compute::concat_batches( 63 | &batches[0].schema(), 64 | &batches, 65 | )?) 66 | } 67 | 68 | /// Format the projected fields in a human-readable format 69 | pub fn pretty_format_projection( 70 | input_data_schema: Arc, 71 | model_input_schema: Arc, 72 | time_field: Arc, 73 | ) -> impl std::fmt::Display { 74 | let mut table = Table::new(); 75 | table.load_preset(comfy_table::presets::ASCII_BORDERS_ONLY_CONDENSED); 76 | table.set_header(vec!["Variable", "Input Type", "Model Type"]); 77 | let rows_iter = input_data_schema.fields().iter().map(|input_field| { 78 | let model_field_name = model_input_schema 79 | .fields() 80 | .iter() 81 | .chain(std::iter::once(&time_field)) 82 | .find(|model_field| model_field.name() == input_field.name()) 83 | .map(|model_field| model_field.data_type()); 84 | vec![ 85 | input_field.name().to_string(), 86 | input_field.data_type().to_string(), 87 | model_field_name 88 | .map(|t| t.to_string()) 89 | .unwrap_or("-None-".to_string()), 90 | ] 91 | }); 92 | table.add_rows(rows_iter); 93 | table 94 | } 95 | 96 | /// Transform the `input_data` to match the `model_input_schema`. Input data columns are projected and 97 | /// cast to the corresponding input schema columns. 98 | /// 99 | /// This is necessary because the `input_data` may have extra columns or have different datatypes. 100 | pub fn project_input_data( 101 | input_data: &RecordBatch, 102 | model_input_schema: SchemaRef, 103 | ) -> anyhow::Result { 104 | let input_data_schema = input_data.schema(); 105 | 106 | let time_field = Arc::new(Field::new( 107 | "time", 108 | arrow::datatypes::DataType::Float64, 109 | false, 110 | )); 111 | 112 | // Create an iterator over the fields of the input data, starting with the time field 113 | let fields_iter = std::iter::once(&time_field).chain(model_input_schema.fields().iter()); 114 | 115 | let (projected_fields, projected_columns): (Vec<_>, Vec<_>) = fields_iter 116 | .filter_map(|field| { 117 | input_data.column_by_name(field.name()).map(|col| { 118 | arrow::compute::cast(col, field.data_type()) 119 | .map(|col| (field.clone(), col)) 120 | .map_err(|_| anyhow::anyhow!("Error casting type")) 121 | }) 122 | }) 123 | .process_results(|pairs| pairs.unzip())?; 124 | 125 | log::debug!( 126 | "Projected input data schema:\n{}", 127 | pretty_format_projection(input_data_schema, model_input_schema, time_field) 128 | ); 129 | 130 | let input_data_schema = Arc::new(Schema::new(projected_fields)); 131 | RecordBatch::try_new(input_data_schema, projected_columns).map_err(anyhow::Error::from) 132 | } 133 | -------------------------------------------------------------------------------- /fmi-sim/tests/data/feedthrough_in.csv: -------------------------------------------------------------------------------- 1 | time,Float64_continuous_input,Float64_discrete_input,Int32_input 2 | 0,3,3,1 3 | 1,3,3,1 4 | 1,2,2,1 5 | 2,3,3,1 6 | 3,3,3,2 7 | 4,3,3,2 -------------------------------------------------------------------------------- /fmi-sim/tests/data/feedthrough_in2.csv: -------------------------------------------------------------------------------- 1 | time,Int8_input,UInt8_input,Int16_input,UInt16_input,Int32_input,UInt32_input,Int64_input,UInt64_input 2 | 0.0,-128,0,-32768,0,-2147483648,0,-9223372036854775808,0 3 | 1.0,127,255,32767,65535,2147483647,4294967295,9223372036854775807,18446744073709551615 -------------------------------------------------------------------------------- /fmi-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmi-sys" 3 | version = "0.1.2" 4 | description = "Raw bindgen bindings to FMI 2.0 and 3.0" 5 | readme = "README.md" 6 | authors.workspace = true 7 | categories.workspace = true 8 | edition.workspace = true 9 | homepage.workspace = true 10 | keywords.workspace = true 11 | license.workspace = true 12 | publish = true 13 | repository.workspace = true 14 | 15 | [features] 16 | default = ["fmi2", "fmi3"] 17 | ## Enable support for FMI 2.0 18 | fmi2 = ["dep:log"] 19 | ## Enable support for FMI 3.0 20 | fmi3 = [] 21 | 22 | [dependencies] 23 | document-features = { workspace = true } 24 | libloading = { workspace = true } 25 | log = { version = "0.4", optional = true } 26 | 27 | [build-dependencies] 28 | bindgen = "0.70" 29 | cc = "1.0" 30 | -------------------------------------------------------------------------------- /fmi-sys/README.md: -------------------------------------------------------------------------------- 1 | # fmi-sys 2 | 3 | [github](https://github.com/jondo2010/rust-fmi) 4 | [crates.io](https://crates.io/crates/fmi-sys) 5 | [docs.rs](https://docs.rs/fmi-sys) 6 | [build status](https://github.com/jondo2010/rust-fmi/actions?query=branch%3Amain) 7 | 8 | Raw Rust bindings to FMI 2.0 and 3.0, generated by [bindgen](https://github.com/rust-lang/rust-bindgen). This crate is part of [rust-fmi](https://github.com/jondo2010/rust-fmi). 9 | 10 | A C compiler such as `gcc` or `clang` is required at build-time. 11 | 12 | See [http://www.fmi-standard.org](http://www.fmi-standard.org) 13 | 14 | ## License 15 | 16 | Licensed under either of 17 | 18 | * Apache License, Version 2.0 19 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 20 | * MIT license 21 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 22 | 23 | at your option. 24 | 25 | ## Contribution 26 | 27 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 28 | -------------------------------------------------------------------------------- /fmi-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | fn main() { 4 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 5 | 6 | #[cfg(feature = "fmi2")] 7 | { 8 | cc::Build::new() 9 | .file("src/fmi2/logger.c") 10 | .include("fmi-standard2/headers") 11 | .compile("liblogger.a"); 12 | 13 | let bindings = bindgen::Builder::default() 14 | .header("fmi-standard2/headers/fmi2Functions.h") 15 | .dynamic_link_require_all(false) 16 | .dynamic_library_name("Fmi2Binding") 17 | .allowlist_function("fmi2.*") 18 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 19 | .generate() 20 | .expect("Unable to generate bindings"); 21 | 22 | bindings 23 | .write_to_file(out_path.join("fmi2_bindings.rs")) 24 | .expect("Couldn't write bindings!"); 25 | } 26 | 27 | #[cfg(feature = "fmi3")] 28 | { 29 | let bindings = bindgen::Builder::default() 30 | .header("fmi-standard3/headers/fmi3Functions.h") 31 | .dynamic_link_require_all(false) 32 | .dynamic_library_name("Fmi3Binding") 33 | .allowlist_function("fmi3.*") 34 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 35 | .generate() 36 | .expect("Unable to generate bindings"); 37 | 38 | bindings 39 | .write_to_file(out_path.join("fmi3_bindings.rs")) 40 | .expect("Couldn't write bindings!"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fmi-sys/src/fmi2/logger.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "fmi2FunctionTypes.h" 6 | 7 | extern void callback_log(fmi2ComponentEnvironment componentEnvironment, 8 | fmi2String instanceName, fmi2Status status, 9 | fmi2String category, fmi2String message); 10 | 11 | void callback_logger_handler(fmi2ComponentEnvironment componentEnvironment, 12 | fmi2String instanceName, fmi2Status status, 13 | fmi2String category, fmi2String message, ...) { 14 | va_list args; 15 | 16 | va_start(args, message); 17 | int buffer_size = vsnprintf(NULL, 0, message, args); 18 | va_end(args); 19 | if (buffer_size > 0) { 20 | // vsnprintf return value doesn't include the terminating null-byte 21 | char *buffer = malloc(buffer_size + 1); 22 | 23 | if (buffer) { 24 | va_start(args, message); 25 | vsprintf(buffer, message, args); 26 | va_end(args); 27 | 28 | callback_log(componentEnvironment, instanceName, status, category, 29 | buffer); 30 | 31 | free(buffer); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fmi-sys/src/fmi2/logger.rs: -------------------------------------------------------------------------------- 1 | use crate::fmi2 as binding; 2 | 3 | /// This function gets called from logger.c 4 | #[no_mangle] 5 | extern "C" fn callback_log( 6 | _component_environment: binding::fmi2ComponentEnvironment, 7 | instance_name: binding::fmi2String, 8 | status: binding::fmi2Status, 9 | category: binding::fmi2String, 10 | message: binding::fmi2String, 11 | ) { 12 | let instance_name = unsafe { std::ffi::CStr::from_ptr(instance_name) } 13 | .to_str() 14 | .unwrap_or("NULL"); 15 | 16 | let level = match status { 17 | binding::fmi2Status_fmi2OK => log::Level::Info, 18 | binding::fmi2Status_fmi2Warning => log::Level::Warn, 19 | binding::fmi2Status_fmi2Pending => unreachable!("Pending status is not allowed in logger"), 20 | binding::fmi2Status_fmi2Discard => log::Level::Trace, 21 | binding::fmi2Status_fmi2Error => log::Level::Error, 22 | binding::fmi2Status_fmi2Fatal => log::Level::Error, 23 | _ => unreachable!("Invalid status"), 24 | }; 25 | 26 | let _category = unsafe { std::ffi::CStr::from_ptr(category) } 27 | .to_str() 28 | .unwrap_or("NULL"); 29 | 30 | let message = unsafe { std::ffi::CStr::from_ptr(message) } 31 | .to_str() 32 | .unwrap_or("NULL"); 33 | 34 | log::logger().log( 35 | &log::Record::builder() 36 | .args(format_args!("{}", message)) 37 | .level(level) 38 | .module_path(Some("logger")) 39 | .target(instance_name) 40 | .build(), 41 | ); 42 | } 43 | 44 | #[link(name = "logger", kind = "static")] 45 | extern "C" { 46 | /// This function is implemented in logger.c 47 | /// Note: This can be re-implemented in pure Rust once the `c_variadics` feature stabilizes. 48 | /// See: https://doc.rust-lang.org/beta/unstable-book/language-features/c-variadic.html 49 | pub fn callback_logger_handler( 50 | componentEnvironment: binding::fmi2ComponentEnvironment, 51 | instanceName: binding::fmi2String, 52 | status: binding::fmi2Status, 53 | category: binding::fmi2String, 54 | message: binding::fmi2String, 55 | ... 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /fmi-sys/src/fmi2/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(clippy::all)] 5 | include!(concat!(env!("OUT_DIR"), "/fmi2_bindings.rs")); 6 | 7 | pub mod logger; 8 | 9 | impl Default for fmi2EventInfo { 10 | fn default() -> Self { 11 | fmi2EventInfo { 12 | newDiscreteStatesNeeded: 0, 13 | terminateSimulation: 0, 14 | nominalsOfContinuousStatesChanged: 0, 15 | valuesOfContinuousStatesChanged: 0, 16 | nextEventTimeDefined: 0, 17 | nextEventTime: 0.0, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fmi-sys/src/fmi3/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(clippy::all)] 5 | include!(concat!(env!("OUT_DIR"), "/fmi3_bindings.rs")); 6 | -------------------------------------------------------------------------------- /fmi-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc=include_str!( "../README.md")] 2 | //! ## Feature flags 3 | #![doc = document_features::document_features!()] 4 | #![deny(clippy::all)] 5 | 6 | #[cfg(feature = "fmi2")] 7 | pub mod fmi2; 8 | #[cfg(feature = "fmi3")] 9 | pub mod fmi3; 10 | -------------------------------------------------------------------------------- /fmi-test-data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmi-test-data" 3 | version = "0.1.0" 4 | description = "Utilities for fetching test data from Modelica's Reference-FMUs repository." 5 | license.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | keywords.workspace = true 10 | categories.workspace = true 11 | publish = true 12 | 13 | [dependencies] 14 | anyhow = { workspace = true } 15 | fetch-data = "0.1" 16 | fmi = { workspace = true, features = ["fmi2", "fmi3"] } 17 | tempfile = { workspace = true } 18 | zip = { workspace = true } 19 | -------------------------------------------------------------------------------- /fmi-test-data/README.md: -------------------------------------------------------------------------------- 1 | # fmi-test-data 2 | 3 | [github](https://github.com/jondo2010/rust-fmi) 4 | [crates.io](https://crates.io/crates/fmi) 5 | [docs.rs](https://docs.rs/fmi) 6 | [build status](https://github.com/jondo2010/rust-fmi/actions?query=branch%3Amain) 7 | 8 | Utilities for fetching test data from Modelica's Reference-FMUs repository. 9 | 10 | See [https://github.com/modelica/Reference-FMUs](https://github.com/modelica/Reference-FMUs) 11 | 12 | ## License 13 | 14 | Licensed under either of 15 | 16 | * Apache License, Version 2.0 17 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 18 | * MIT license 19 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 20 | 21 | at your option. 22 | 23 | ## Contribution 24 | 25 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 26 | -------------------------------------------------------------------------------- /fmi-test-data/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc=include_str!( "../README.md")] 2 | #![deny(unsafe_code)] 3 | #![deny(clippy::all)] 4 | 5 | use anyhow::Context; 6 | use fetch_data::{ctor, FetchData}; 7 | use fmi::{schema::MajorVersion, traits::FmiImport}; 8 | use std::{ 9 | fs::File, 10 | io::{Cursor, Read}, 11 | }; 12 | use tempfile::NamedTempFile; 13 | 14 | const REF_ARCHIVE: &str = "Reference-FMUs-0.0.29.zip"; 15 | const REF_URL: &str = "https://github.com/modelica/Reference-FMUs/releases/download/v0.0.29/"; 16 | 17 | #[ctor] 18 | static STATIC_FETCH_DATA: FetchData = FetchData::new( 19 | include_str!("registry.txt"), 20 | REF_URL, 21 | "FMU_DATA_DIR", 22 | "org", 23 | "modelica", 24 | "reference-fmus", 25 | ); 26 | 27 | /// A Rust interface to the Modelica Reference-FMUs, downloaded as an archive using `fetch_data` 28 | pub struct ReferenceFmus { 29 | archive: zip::ZipArchive, 30 | } 31 | 32 | impl std::fmt::Debug for ReferenceFmus { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | f.debug_struct("ReferenceFmus") 35 | .field("archive", &self.archive.comment()) 36 | .finish() 37 | } 38 | } 39 | 40 | impl ReferenceFmus { 41 | /// Fetch the released Modelica Reference-FMUs file 42 | pub fn new() -> anyhow::Result { 43 | let path = STATIC_FETCH_DATA 44 | .fetch_file(REF_ARCHIVE) 45 | .context(format!("Fetch {REF_ARCHIVE}"))?; 46 | let f = std::fs::File::open(&path).context(format!("Open {:?}", path))?; 47 | let archive = zip::ZipArchive::new(f)?; 48 | Ok(Self { archive }) 49 | } 50 | 51 | pub fn get_reference_fmu(&mut self, name: &str) -> anyhow::Result { 52 | let version = Imp::MAJOR_VERSION.to_string(); 53 | let mut f = self.archive.by_name(&format!("{version}/{name}.fmu"))?; 54 | // Read f into a Vec that can be used to create a new Import 55 | let mut buf = Vec::new(); 56 | f.read_to_end(buf.as_mut())?; 57 | Ok(fmi::import::new(Cursor::new(buf))?) 58 | } 59 | 60 | /// Extract a reference FMU from the reference archive into a temporary file 61 | pub fn extract_reference_fmu( 62 | &mut self, 63 | name: &str, 64 | version: MajorVersion, 65 | ) -> anyhow::Result { 66 | let version = version.to_string(); 67 | let filename = format!("{version}/{name}.fmu"); 68 | let mut fin = self.archive.by_name(&filename).context("Open {filename}")?; 69 | let mut fout = tempfile::NamedTempFile::new()?; 70 | std::io::copy(fin.by_ref(), fout.as_file_mut()) 71 | .context("Extracting {path:?} to tempfile")?; 72 | Ok(fout) 73 | } 74 | } 75 | 76 | #[test] 77 | fn test_reference_fmus() { 78 | use fmi::traits::FmiImport; 79 | let mut reference_fmus = ReferenceFmus::new().unwrap(); 80 | let fmu: fmi::fmi2::import::Fmi2Import = 81 | reference_fmus.get_reference_fmu("BouncingBall").unwrap(); 82 | assert_eq!(fmu.model_description().fmi_version, "2.0"); 83 | assert_eq!(fmu.model_description().model_name, "BouncingBall"); 84 | let fmu: fmi::fmi3::import::Fmi3Import = 85 | reference_fmus.get_reference_fmu("BouncingBall").unwrap(); 86 | assert_eq!(fmu.model_description().fmi_version, "3.0"); 87 | assert_eq!(fmu.model_description().model_name, "BouncingBall"); 88 | } 89 | 90 | #[cfg(feature = "disabled")] 91 | #[test] 92 | fn print_registry_contents() { 93 | let registry_contents = STATIC_FETCH_DATA 94 | .gen_registry_contents([REF_ARCHIVE]) 95 | .unwrap(); 96 | println!("{registry_contents}"); 97 | } 98 | -------------------------------------------------------------------------------- /fmi-test-data/src/registry.txt: -------------------------------------------------------------------------------- 1 | Reference-FMUs-0.0.29.zip cf17e4e8ca0db0965afc5d4c968ab1a94d6328c8941e20496e69e5c0ee6836f1 -------------------------------------------------------------------------------- /fmi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmi" 3 | version = "0.4.1" 4 | description = "A Rust interface to FMUs (Functional Mockup Units) that follow the FMI Standard. See http://www.fmi-standard.org/" 5 | readme = "README.md" 6 | publish = true 7 | authors.workspace = true 8 | categories.workspace = true 9 | edition.workspace = true 10 | homepage.workspace = true 11 | include.workspace = true 12 | keywords.workspace = true 13 | license.workspace = true 14 | repository.workspace = true 15 | 16 | [features] 17 | default = ["fmi2", "fmi3", "arrow"] 18 | ## Enable support for FMI 2.0 19 | fmi2 = ["fmi-schema/fmi2", "dep:libc", "dep:url"] 20 | ## Enable support for FMI 3.0 21 | fmi3 = ["fmi-schema/fmi3"] 22 | ## Enable support for Apache Arrow Schema 23 | arrow = ["dep:arrow", "fmi-schema/arrow"] 24 | 25 | [dependencies] 26 | arrow = { workspace = true, optional = true } 27 | document-features = { workspace = true } 28 | fmi-schema = { workspace = true, default_features = false } 29 | fmi-sys = { workspace = true } 30 | itertools = { workspace = true } 31 | # Note: libc is only used for FMI 2.0 support, needed for alloc 32 | libc = { version = "0.2", features = ["align"], optional = true } 33 | libloading = { workspace = true } 34 | log = { version = "0.4", features = ["std", "serde"] } 35 | tempfile = { workspace = true } 36 | thiserror = { workspace = true } 37 | url = { version = "2.2", optional = true } 38 | zip = { workspace = true } 39 | 40 | [build-dependencies] 41 | built = "0.7" 42 | -------------------------------------------------------------------------------- /fmi/README.md: -------------------------------------------------------------------------------- 1 | # fmi 2 | 3 | [github](https://github.com/jondo2010/rust-fmi) 4 | [crates.io](https://crates.io/crates/fmi) 5 | [docs.rs](https://docs.rs/fmi) 6 | [build status](https://github.com/jondo2010/rust-fmi/actions?query=branch%3Amain) 7 | 8 | A Rust interface to FMUs (Functional Mockup Units) that follow the FMI Standard. 9 | 10 | See [http://www.fmi-standard.org](http://www.fmi-standard.org) 11 | 12 | ## License 13 | 14 | Licensed under either of 15 | 16 | * Apache License, Version 2.0 17 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 18 | * MIT license 19 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 20 | 21 | at your option. 22 | 23 | ## Contribution 24 | 25 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 26 | -------------------------------------------------------------------------------- /fmi/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | built::write_built_file().expect("Failed to acquire build-time information"); 3 | } 4 | -------------------------------------------------------------------------------- /fmi/src/fmi2/import.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use super::{ 4 | binding, 5 | instance::{Instance, CS, ME}, 6 | }; 7 | use crate::{traits::FmiImport, Error}; 8 | 9 | use fmi_schema::{fmi2 as schema, MajorVersion}; 10 | 11 | #[derive(Debug)] 12 | pub struct Fmi2Import { 13 | /// Path to the unzipped FMU on disk 14 | dir: tempfile::TempDir, 15 | /// Parsed raw-schema model description 16 | model_description: schema::Fmi2ModelDescription, 17 | } 18 | 19 | impl FmiImport for Fmi2Import { 20 | const MAJOR_VERSION: MajorVersion = MajorVersion::FMI2; 21 | type ModelDescription = schema::Fmi2ModelDescription; 22 | type Binding = binding::Fmi2Binding; 23 | type ValueRef = binding::fmi2ValueReference; 24 | 25 | fn new(dir: tempfile::TempDir, schema_xml: &str) -> Result { 26 | let schema = schema::Fmi2ModelDescription::from_str(schema_xml)?; 27 | Ok(Self { 28 | dir, 29 | model_description: schema, 30 | }) 31 | } 32 | 33 | #[inline] 34 | fn archive_path(&self) -> &std::path::Path { 35 | self.dir.path() 36 | } 37 | 38 | /// Get the path to the shared library 39 | fn shared_lib_path(&self, model_identifier: &str) -> Result { 40 | let platform_folder = match (std::env::consts::OS, std::env::consts::ARCH) { 41 | ("windows", "x86_64") => "win64", 42 | ("windows", "x86") => "win32", 43 | ("linux", "x86_64") => "linux64", 44 | ("linux", "x86") => "linux32", 45 | ("macos", "x86_64") => "darwin64", 46 | ("macos", "x86") => "darwin32", 47 | _ => { 48 | return Err(Error::UnsupportedPlatform { 49 | os: std::env::consts::OS.to_string(), 50 | arch: std::env::consts::ARCH.to_string(), 51 | }); 52 | } 53 | }; 54 | let fname = format!("{model_identifier}{}", std::env::consts::DLL_SUFFIX); 55 | Ok(std::path::PathBuf::from("binaries") 56 | .join(platform_folder) 57 | .join(fname)) 58 | } 59 | 60 | fn model_description(&self) -> &Self::ModelDescription { 61 | &self.model_description 62 | } 63 | 64 | /// Load the plugin shared library and return the raw bindings. 65 | fn binding(&self, model_identifier: &str) -> Result { 66 | let lib_path = self 67 | .dir 68 | .path() 69 | .join(self.shared_lib_path(model_identifier)?); 70 | log::trace!("Loading shared library {:?}", lib_path); 71 | unsafe { binding::Fmi2Binding::new(lib_path).map_err(Error::from) } 72 | } 73 | 74 | /// Get a `String` representation of the resources path for this FMU 75 | /// 76 | /// As per the FMI standard, the resource location is a IETF URI to the resources directory. 77 | fn canonical_resource_path_string(&self) -> String { 78 | let resource_path = 79 | std::path::absolute(self.resource_path()).expect("Invalid resource path"); 80 | url::Url::from_file_path(resource_path) 81 | .map(|url| url.as_str().to_owned()) 82 | .expect("Error converting path to URL") 83 | } 84 | } 85 | 86 | impl Fmi2Import { 87 | /// Create a new instance of the FMU for Model-Exchange 88 | pub fn instantiate_me( 89 | &self, 90 | instance_name: &str, 91 | visible: bool, 92 | logging_on: bool, 93 | ) -> Result, Error> { 94 | Instance::<'_, ME>::new(self, instance_name, visible, logging_on) 95 | } 96 | 97 | /// Create a new instance of the FMU for Co-Simulation 98 | pub fn instantiate_cs( 99 | &self, 100 | instance_name: &str, 101 | visible: bool, 102 | logging_on: bool, 103 | ) -> Result, Error> { 104 | Instance::<'_, CS>::new(self, instance_name, visible, logging_on) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /fmi/src/fmi2/instance/co_simulation.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | 3 | use crate::{ 4 | fmi2::{import, CallbackFunctions, Fmi2Error, Fmi2Status}, 5 | traits::{FmiImport, FmiStatus}, 6 | Error, 7 | }; 8 | 9 | use super::{binding, traits, Instance, CS}; 10 | 11 | impl<'a> Instance<'a, CS> { 12 | /// Initialize a new Instance from an Import 13 | pub fn new( 14 | import: &'a import::Fmi2Import, 15 | instance_name: &str, 16 | visible: bool, 17 | logging_on: bool, 18 | ) -> Result { 19 | let schema = import.model_description(); 20 | 21 | let co_simulation = schema 22 | .co_simulation 23 | .as_ref() 24 | .ok_or(Error::UnsupportedFmuType("CoSimulation".to_owned()))?; 25 | 26 | let binding = import.binding(&co_simulation.model_identifier)?; 27 | 28 | let callbacks = Box::::default(); 29 | //.check_consistency(&import, &cs.common)?; 30 | 31 | let name = instance_name.to_owned(); 32 | 33 | let instance_name = CString::new(instance_name).expect("Error building CString"); 34 | let guid = CString::new(schema.guid.as_bytes()).expect("Error building CString"); 35 | let resource_url = 36 | CString::new(import.canonical_resource_path_string()).expect("Invalid resource path"); 37 | 38 | let component = unsafe { 39 | let callback_functions = &*callbacks as *const CallbackFunctions; 40 | binding.fmi2Instantiate( 41 | instance_name.as_ptr(), 42 | binding::fmi2Type_fmi2CoSimulation, 43 | guid.as_ptr(), // guid 44 | resource_url.as_ptr(), // fmu_resource_location 45 | callback_functions as _, // functions 46 | visible as binding::fmi2Boolean, // visible 47 | logging_on as binding::fmi2Boolean, // logging_on 48 | ) 49 | }; 50 | if component.is_null() { 51 | return Err(Error::Instantiation); 52 | } 53 | log::trace!("Created FMI2.0 CS component {:?}", component); 54 | 55 | Ok(Self { 56 | binding, 57 | component, 58 | model_description: schema, 59 | callbacks, 60 | name, 61 | saved_states: Vec::new(), 62 | _tag: std::marker::PhantomData, 63 | }) 64 | } 65 | } 66 | 67 | impl<'a> traits::CoSimulation for Instance<'a, CS> { 68 | fn do_step( 69 | &self, 70 | current_communication_point: f64, 71 | communication_step_size: f64, 72 | new_step: bool, 73 | ) -> Fmi2Status { 74 | unsafe { 75 | self.binding.fmi2DoStep( 76 | self.component, 77 | current_communication_point, 78 | communication_step_size, 79 | new_step as _, 80 | ) 81 | } 82 | .into() 83 | } 84 | 85 | fn cancel_step(&self) -> Fmi2Status { 86 | unsafe { self.binding.fmi2CancelStep(self.component) }.into() 87 | } 88 | 89 | fn do_step_status(&mut self) -> Result { 90 | let mut ret = binding::fmi2Status_fmi2OK; 91 | Fmi2Status(unsafe { 92 | self.binding.fmi2GetStatus( 93 | self.component, 94 | binding::fmi2StatusKind_fmi2DoStepStatus, 95 | &mut ret, 96 | ) 97 | }) 98 | .ok() 99 | .map(|_| Fmi2Status(ret)) 100 | } 101 | 102 | fn pending_status(&mut self) -> Result<&str, Fmi2Error> { 103 | let str_ret = CStr::from_bytes_with_nul(b"\0").unwrap(); 104 | Fmi2Status(unsafe { 105 | self.binding.fmi2GetStringStatus( 106 | self.component, 107 | binding::fmi2StatusKind_fmi2PendingStatus, 108 | &mut str_ret.as_ptr(), 109 | ) 110 | }) 111 | .ok() 112 | .map(|_| str_ret.to_str().unwrap()) 113 | } 114 | 115 | fn last_successful_time(&mut self) -> Result { 116 | let mut ret = 0.0; 117 | Fmi2Status(unsafe { 118 | self.binding.fmi2GetRealStatus( 119 | self.component, 120 | binding::fmi2StatusKind_fmi2LastSuccessfulTime, 121 | &mut ret, 122 | ) 123 | }) 124 | .ok() 125 | .map(|_| ret) 126 | } 127 | 128 | fn terminated(&mut self) -> Result { 129 | let mut ret = 0i32; 130 | Fmi2Status(unsafe { 131 | self.binding.fmi2GetBooleanStatus( 132 | self.component, 133 | binding::fmi2StatusKind_fmi2Terminated, 134 | &mut ret, 135 | ) 136 | }) 137 | .ok() 138 | .map(|_| ret != 0) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /fmi/src/fmi2/mod.rs: -------------------------------------------------------------------------------- 1 | //! FMI 2.0 API 2 | 3 | pub mod import; 4 | pub mod instance; 5 | // Re-export 6 | pub use fmi_schema::fmi2 as schema; 7 | pub use fmi_sys::fmi2 as binding; 8 | 9 | use crate::traits::FmiStatus; 10 | 11 | #[repr(C)] 12 | #[derive(Debug, Copy, Clone)] 13 | pub struct CallbackFunctions { 14 | pub logger: binding::fmi2CallbackLogger, 15 | pub allocate_memory: binding::fmi2CallbackAllocateMemory, 16 | pub free_memory: binding::fmi2CallbackFreeMemory, 17 | pub step_finished: binding::fmi2StepFinished, 18 | pub component_environment: binding::fmi2ComponentEnvironment, 19 | } 20 | 21 | #[repr(C)] 22 | #[derive(Debug)] 23 | pub enum StatusKind { 24 | /// Can be called when the fmi2DoStep function returned fmi2Pending. The function delivers 25 | /// fmi2Pending if the computation is not finished. Otherwise the function returns the result 26 | /// of the asynchronously executed fmi2DoStep call 27 | DoStepStatus = binding::fmi2StatusKind_fmi2DoStepStatus as _, 28 | /// Can be called when the fmi2DoStep function returned fmi2Pending. The function delivers a 29 | /// string which informs about the status of the currently running asynchronous fmi2DoStep 30 | /// computation. 31 | PendingStatus = binding::fmi2StatusKind_fmi2PendingStatus as _, 32 | /// Returns the end time of the last successfully completed communication step. Can be called 33 | /// after fmi2DoStep(...) returned fmi2Discard. 34 | LastSuccessfulTime = binding::fmi2StatusKind_fmi2LastSuccessfulTime as _, 35 | /// Returns true, if the slave wants to terminate the simulation. Can be called after 36 | /// fmi2DoStep(...) returned fmi2Discard. Use fmi2LastSuccessfulTime to determine the time 37 | /// instant at which the slave terminated 38 | Terminated = binding::fmi2StatusKind_fmi2Terminated as _, 39 | } 40 | 41 | #[derive(Debug)] 42 | pub enum Fmi2Res { 43 | /// All well 44 | OK, 45 | /// Things are not quite right, but the computation can continue. Function “logger” was called 46 | /// in the model (see below), and it is expected that this function has shown the prepared 47 | /// information message to the user. 48 | Warning, 49 | /// This status is returned only from the co-simulation interface, if the slave executes the 50 | /// function in an asynchronous way. That means the slave starts to compute but returns 51 | /// immediately. 52 | /// 53 | /// The master has to call [`instance::traits::CoSimulation::get_status`](..., 54 | /// fmi2DoStepStatus) to determine if the slave has finished the computation. Can be 55 | /// returned only by [`instance::traits::CoSimulation::do_step`] 56 | /// and by [`instance::traits::CoSimulation::get_status`]. 57 | Pending, 58 | } 59 | 60 | #[derive(Debug, thiserror::Error)] 61 | pub enum Fmi2Error { 62 | #[error("TypesPlatform of loaded API ({0}) doesn't match expected (default)")] 63 | TypesPlatformMismatch(String), 64 | 65 | /// For “model exchange”: It is recommended to perform a smaller step size and evaluate the 66 | /// model equations again, for example because an iterative solver in the model did not 67 | /// converge or because a function is outside of its domain (for example sqrt()). If this is not possible, the simulation has to be terminated. 69 | /// 70 | /// For “co-simulation”: [`Fmi2Err::Discard`] is returned also if the slave is not able to 71 | /// return the required status information. The master has to decide if the simulation run 72 | /// can be continued. 73 | /// 74 | /// In both cases, function “logger” was called in the FMU (see below) and it is expected that 75 | /// this function has shown the prepared information message to the user if the FMU was 76 | /// called in debug mode (loggingOn = true). Otherwise, “logger” should not show a message. 77 | #[error("Discard")] 78 | Discard, 79 | /// The FMU encountered an error. The simulation cannot be continued with this FMU instance. If 80 | /// one of the functions returns [`Fmi2Err::Error`], it can be tried to restart the 81 | /// simulation from a formerly stored FMU state by 82 | /// calling [`instance::traits::Common::set_fmu_state`]. This can be done if the capability 83 | /// flag `can_get_and_set_fmu_state` is true and 84 | /// [`instance::traits::Common::get_fmu_state`] was called before in non-erroneous state. If 85 | /// not, the simulation cannot be continued and [`instance::traits::Common::reset`] must be 86 | /// called afterwards. 87 | #[error("Error")] 88 | Error, 89 | /// The model computations are irreparably corrupted for all FMU instances. 90 | #[error("Fatal")] 91 | Fatal, 92 | } 93 | 94 | #[derive(Debug)] 95 | pub struct Fmi2Status(binding::fmi2Status); 96 | 97 | impl FmiStatus for Fmi2Status { 98 | type Res = Fmi2Res; 99 | 100 | type Err = Fmi2Error; 101 | 102 | /// Convert to [`Result`] 103 | #[inline] 104 | fn ok(self) -> Result { 105 | self.into() 106 | } 107 | 108 | #[inline] 109 | fn is_error(&self) -> bool { 110 | self.0 == binding::fmi2Status_fmi2Error || self.0 == binding::fmi2Status_fmi2Fatal 111 | } 112 | } 113 | 114 | impl From for Fmi2Status { 115 | fn from(status: binding::fmi2Status) -> Self { 116 | Self(status) 117 | } 118 | } 119 | 120 | impl From for Result { 121 | fn from(Fmi2Status(status): Fmi2Status) -> Self { 122 | match status { 123 | binding::fmi2Status_fmi2OK => Ok(Fmi2Res::OK), 124 | binding::fmi2Status_fmi2Warning => Ok(Fmi2Res::Warning), 125 | binding::fmi2Status_fmi2Discard => Err(Fmi2Error::Discard), 126 | binding::fmi2Status_fmi2Error => Err(Fmi2Error::Error), 127 | binding::fmi2Status_fmi2Fatal => Err(Fmi2Error::Fatal), 128 | _ => unreachable!("Invalid status"), 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /fmi/src/fmi2/variable.rs: -------------------------------------------------------------------------------- 1 | use crate::FmiStatus; 2 | 3 | use super::FmiResult; 4 | use derive_more::Display; 5 | use std::cmp::Ordering; 6 | 7 | // Re-exports 8 | pub use super::fmi2::meta::{Causality, Initial, ScalarVariableElementBase, Variability}; 9 | 10 | #[derive(Display, Debug)] 11 | pub enum Value { 12 | Real(fmi2::fmi2Real), 13 | Integer(fmi2::fmi2Integer), 14 | Boolean(fmi2::fmi2Boolean), 15 | String(String), 16 | Enumeration(fmi2::fmi2Integer), 17 | } 18 | 19 | impl From<&Value> for ScalarVariableElementBase { 20 | fn from(value: &Value) -> Self { 21 | match value { 22 | Value::Real(_) => Self::Real, 23 | Value::Integer(_) => Self::Integer, 24 | Value::Boolean(_) => Self::Boolean, 25 | Value::String(_) => Self::String, 26 | Value::Enumeration(_) => Self::Enumeration, 27 | } 28 | } 29 | } 30 | 31 | /// Var wraps access to an underlying ScalarVariable on an Instance 32 | #[derive(Display, Debug)] 33 | #[display(fmt = "Var {}.{}", "self.instance.name()", "self.name()")] 34 | pub struct Var<'inst, I: instance::Common> { 35 | /// An owned-copy of the ScalarVariable data 36 | sv: meta::ScalarVariable, // only 120 bytes 37 | instance: &'inst I, 38 | } 39 | 40 | impl<'inst, I: instance::Common> std::hash::Hash for Var<'inst, I> { 41 | fn hash(&self, state: &mut H) { 42 | self.instance.hash(state); 43 | self.sv.hash(state); 44 | } 45 | } 46 | 47 | impl<'inst, I: instance::Common> Ord for Var<'inst, I> { 48 | fn cmp(&self, other: &Self) -> Ordering { 49 | (self.instance.name(), &self.sv.name).cmp(&(other.instance.name(), &other.sv.name)) 50 | } 51 | } 52 | 53 | impl<'inst, I: instance::Common> PartialOrd for Var<'inst, I> { 54 | fn partial_cmp(&self, other: &Self) -> Option { 55 | Some(self.cmp(other)) 56 | } 57 | } 58 | 59 | impl<'inst, I: instance::Common> PartialEq for Var<'inst, I> { 60 | fn eq(&self, other: &Self) -> bool { 61 | (self.instance.name(), &self.sv.name) == (other.instance.name(), &other.sv.name) 62 | } 63 | } 64 | 65 | impl<'inst, I: instance::Common> Eq for Var<'inst, I> {} 66 | 67 | impl<'inst, I: instance::Common> Var<'inst, I> { 68 | /// Create a new Var from an Instance and a ScalarVariable 69 | pub fn from_scalar_variable(instance: &'inst I, sv: &meta::ScalarVariable) -> Self { 70 | Var { 71 | instance, 72 | sv: sv.clone(), 73 | } 74 | } 75 | 76 | /// Create a new Var from an Instance given a variable name 77 | #[cfg(feature = "disable")] 78 | pub fn from_name>(instance: &'inst I, name: S) -> Result { 79 | let sv: &meta::ScalarVariable = instance 80 | .import() 81 | .descr() 82 | .get_model_variables() 83 | .find(|(_vr, sv)| sv.name == name.as_ref()) 84 | .map(|(_vr, sv)| sv) 85 | .ok_or_else(|| meta::ModelDescriptionError::VariableNotFound { 86 | model: instance.import().descr().model_name().to_owned(), 87 | name: name.as_ref().into(), 88 | })?; 89 | 90 | let instance = instance.clone(); 91 | 92 | Ok(Var { 93 | instance, 94 | sv: sv.clone(), 95 | }) 96 | } 97 | 98 | pub fn name(&self) -> &str { 99 | &self.sv.name 100 | } 101 | 102 | pub fn scalar_variable(&self) -> &meta::ScalarVariable { 103 | &self.sv 104 | } 105 | 106 | pub fn instance(&self) -> &I { 107 | self.instance 108 | } 109 | 110 | pub fn get(&self) -> Result { 111 | match self.sv.elem { 112 | meta::ScalarVariableElement::Real { .. } => { 113 | self.instance.get_real(&self.sv).map(Value::Real) 114 | } 115 | meta::ScalarVariableElement::Integer { .. } => { 116 | self.instance.get_integer(&self.sv).map(Value::Integer) 117 | } 118 | meta::ScalarVariableElement::Boolean { .. } => { 119 | self.instance.get_boolean(&self.sv).map(Value::Boolean) 120 | } 121 | meta::ScalarVariableElement::String { .. } => { 122 | unimplemented!("String variables not supported yet.") 123 | } 124 | meta::ScalarVariableElement::Enumeration { .. } => { 125 | self.instance.get_integer(&self.sv).map(Value::Enumeration) 126 | } 127 | } 128 | } 129 | 130 | pub fn set(&self, value: &Value) -> Result { 131 | match (&self.sv.elem, value) { 132 | (meta::ScalarVariableElement::Real { .. }, Value::Real(x)) => { 133 | self.instance.set_real(&[self.sv.value_reference], &[*x]) 134 | } 135 | (meta::ScalarVariableElement::Integer { .. }, Value::Integer(x)) => { 136 | self.instance.set_integer(&[self.sv.value_reference], &[*x]) 137 | } 138 | (meta::ScalarVariableElement::Boolean { .. }, Value::Boolean(x)) => self 139 | .instance 140 | .set_boolean(&[self.sv.value_reference.0], &[*x]), 141 | (meta::ScalarVariableElement::String { .. }, Value::String(_x)) => { 142 | unimplemented!("String variables not supported yet.") 143 | } 144 | (meta::ScalarVariableElement::Enumeration { .. }, Value::Enumeration(x)) => { 145 | self.instance.set_integer(&[self.sv.value_reference], &[*x]) 146 | } 147 | _ => Err(meta::ModelDescriptionError::VariableTypeMismatch( 148 | value.into(), 149 | ScalarVariableElementBase::from(&self.sv.elem), 150 | ) 151 | .into()), 152 | } 153 | } 154 | } 155 | 156 | // trait SetAll { 157 | // fn set_all(self) -> Result<()>; 158 | // } 159 | // 160 | // impl<'a, I> SetAll for I where I: IntoIterator + Clone { 161 | // fn set_all(self) -> Result<()> { 162 | // let x = self.into_iter().map(|val: &Value| (val.sv.elem)); 163 | // 164 | // Ok(()) 165 | // } 166 | // } 167 | // 168 | // pub fn set2<'a, T>(&self, vals: T) -> Result<()> 169 | // where 170 | // T: IntoIterator, 171 | // { 172 | // let q = vals.into_iter().map(|val| { 173 | // match (&self.sv.elem, val) { 174 | // (meta::ScalarVariableElement::real {..}, Value::Real(x)) => { 175 | // (&self.sv.value_reference, *x) 176 | // } 177 | // _ => Err(format_err!("Type mismatch")), 178 | // } 179 | // }); 180 | // let (left, right): (Vec<_>, Vec<_>) = vec![(1,2), (3,4)].iter().cloned().unzip(); 181 | // 182 | // .collect::<(Vec<_>, Vec<_>)>(); 183 | // 184 | // Ok(()) 185 | // } 186 | // 187 | -------------------------------------------------------------------------------- /fmi/src/fmi3/import.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use fmi_schema::MajorVersion; 4 | use tempfile::TempDir; 5 | 6 | use crate::{traits::FmiImport, Error}; 7 | 8 | use super::{ 9 | binding, 10 | instance::{Instance, CS, ME}, 11 | schema, 12 | }; 13 | 14 | /// FMU import for FMI 3.0 15 | #[derive(Debug)] 16 | pub struct Fmi3Import { 17 | /// Path to the unzipped FMU on disk 18 | dir: tempfile::TempDir, 19 | /// Parsed raw-schema model description 20 | model_description: schema::Fmi3ModelDescription, 21 | } 22 | 23 | impl FmiImport for Fmi3Import { 24 | const MAJOR_VERSION: MajorVersion = MajorVersion::FMI3; 25 | type ModelDescription = schema::Fmi3ModelDescription; 26 | type Binding = binding::Fmi3Binding; 27 | type ValueRef = binding::fmi3ValueReference; 28 | 29 | /// Create a new FMI 3.0 import from a directory containing the unzipped FMU 30 | fn new(dir: TempDir, schema_xml: &str) -> Result { 31 | let model_description = schema::Fmi3ModelDescription::from_str(schema_xml)?; 32 | Ok(Self { 33 | dir, 34 | model_description, 35 | }) 36 | } 37 | 38 | #[inline] 39 | fn archive_path(&self) -> &std::path::Path { 40 | self.dir.path() 41 | } 42 | 43 | /// Get the path to the shared library 44 | fn shared_lib_path(&self, model_identifier: &str) -> Result { 45 | use std::env::consts::{ARCH, OS}; 46 | let platform_folder = match (OS, ARCH) { 47 | ("windows", "x86_64") => "x86_64-windows", 48 | ("windows", "x86") => "x86-windows", 49 | ("linux", "x86_64") => "x86_64-linux", 50 | ("linux", "x86") => "x86-linux", 51 | ("macos", "x86_64") => "x86-64-darwin", 52 | ("macos", "x86") => "x86-darwin", 53 | ("macos", "aarch64") => "aarch64-darwin", 54 | _ => panic!("Unsupported platform: {OS} {ARCH}"), 55 | }; 56 | let fname = format!("{model_identifier}{}", std::env::consts::DLL_SUFFIX); 57 | Ok(std::path::PathBuf::from("binaries") 58 | .join(platform_folder) 59 | .join(fname)) 60 | } 61 | 62 | /// Get the parsed raw-schema model description 63 | fn model_description(&self) -> &Self::ModelDescription { 64 | &self.model_description 65 | } 66 | 67 | /// Load the plugin shared library and return the raw bindings. 68 | fn binding(&self, model_identifier: &str) -> Result { 69 | let lib_path = self 70 | .dir 71 | .path() 72 | .join(self.shared_lib_path(model_identifier)?); 73 | log::debug!("Loading shared library {:?}", lib_path); 74 | unsafe { binding::Fmi3Binding::new(lib_path).map_err(Error::from) } 75 | } 76 | 77 | /// Get a `String` representation of the resources path for this FMU 78 | /// 79 | /// As per the FMI3.0 standard, `resourcePath` is the absolute file path (with a trailing file separator) of the 80 | /// resources directory of the extracted FMU archive. 81 | fn canonical_resource_path_string(&self) -> String { 82 | std::path::absolute(self.resource_path()) 83 | .expect("Invalid resource path") 84 | .to_str() 85 | .expect("Invalid resource path") 86 | .to_owned() 87 | } 88 | } 89 | 90 | impl Fmi3Import { 91 | /// Build a derived model description from the raw-schema model description 92 | #[cfg(feature = "disabled")] 93 | pub fn model(&self) -> &model::ModelDescription { 94 | &self.model 95 | } 96 | 97 | /// Create a new instance of the FMU for Model-Exchange 98 | /// 99 | /// See [`Instance::::new()`] for more information. 100 | pub fn instantiate_me( 101 | &self, 102 | instance_name: &str, 103 | visible: bool, 104 | logging_on: bool, 105 | ) -> Result, Error> { 106 | Instance::<'_, ME>::new(self, instance_name, visible, logging_on) 107 | } 108 | 109 | /// Create a new instance of the FMU for Co-Simulation 110 | /// 111 | /// See [`Instance::::new()`] for more information. 112 | pub fn instantiate_cs( 113 | &self, 114 | instance_name: &str, 115 | visible: bool, 116 | logging_on: bool, 117 | event_mode_used: bool, 118 | early_return_allowed: bool, 119 | required_intermediate_variables: &[binding::fmi3ValueReference], 120 | ) -> Result, Error> { 121 | Instance::<'_, CS>::new( 122 | self, 123 | instance_name, 124 | visible, 125 | logging_on, 126 | event_mode_used, 127 | early_return_allowed, 128 | required_intermediate_variables, 129 | ) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /fmi/src/fmi3/instance/scheduled_execution.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | use crate::{ 4 | fmi3::{binding, import, logger}, 5 | traits::FmiImport, 6 | Error, 7 | }; 8 | 9 | use super::{Instance, ScheduledExecution, SE}; 10 | 11 | unsafe extern "C" fn clock_update(_instance_environment: binding::fmi3InstanceEnvironment) { 12 | todo!(); 13 | } 14 | unsafe extern "C" fn lock_preemption() { 15 | todo!(); 16 | } 17 | unsafe extern "C" fn unlock_preemption() { 18 | todo!(); 19 | } 20 | 21 | impl<'a> Instance<'a, SE> { 22 | pub fn new( 23 | import: &'a import::Fmi3Import, 24 | instance_name: &str, 25 | visible: bool, 26 | logging_on: bool, 27 | ) -> Result { 28 | let schema = import.model_description(); 29 | 30 | let name = instance_name.to_owned(); 31 | 32 | let scheduled_execution = schema 33 | .scheduled_execution 34 | .as_ref() 35 | .ok_or(Error::UnsupportedFmuType("ScheduledExecution".to_owned()))?; 36 | 37 | log::debug!( 38 | "Instantiating ME: {} '{name}'", 39 | scheduled_execution.model_identifier 40 | ); 41 | 42 | let binding = import.binding(&scheduled_execution.model_identifier)?; 43 | 44 | let instance_name = CString::new(instance_name).expect("Invalid instance name"); 45 | let instantiation_token = CString::new(schema.instantiation_token.as_bytes()) 46 | .expect("Invalid instantiation token"); 47 | let resource_path = 48 | CString::new(import.canonical_resource_path_string()).expect("Invalid resource path"); 49 | 50 | let instance = unsafe { 51 | binding.fmi3InstantiateScheduledExecution( 52 | instance_name.as_ptr(), 53 | instantiation_token.as_ptr(), 54 | resource_path.as_ptr() as binding::fmi3String, 55 | visible, 56 | logging_on, 57 | std::ptr::null_mut() as binding::fmi3InstanceEnvironment, 58 | Some(logger::callback_log), 59 | Some(clock_update), 60 | Some(lock_preemption), 61 | Some(unlock_preemption), 62 | ) 63 | }; 64 | 65 | if instance.is_null() { 66 | return Err(Error::Instantiation); 67 | } 68 | 69 | Ok(Self { 70 | binding, 71 | ptr: instance, 72 | model_description: schema, 73 | name, 74 | _tag: std::marker::PhantomData, 75 | }) 76 | } 77 | } 78 | 79 | impl<'a> ScheduledExecution for Instance<'a, SE> { 80 | fn activate_model_partition( 81 | &mut self, 82 | clock_reference: Self::ValueRef, 83 | activation_time: f64, 84 | ) -> crate::fmi3::Fmi3Status { 85 | unsafe { 86 | self.binding 87 | .fmi3ActivateModelPartition(self.ptr, clock_reference, activation_time) 88 | } 89 | .into() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /fmi/src/fmi3/logger.rs: -------------------------------------------------------------------------------- 1 | use super::{binding, Fmi3Status}; 2 | 3 | /// Callback function for logging 4 | pub(crate) unsafe extern "C" fn callback_log( 5 | _instance_environment: binding::fmi3InstanceEnvironment, 6 | status: binding::fmi3Status, 7 | category: binding::fmi3String, 8 | message: binding::fmi3String, 9 | ) { 10 | let status = Fmi3Status::from(status); 11 | let category = std::ffi::CStr::from_ptr(category) 12 | .to_str() 13 | .unwrap_or("INVALID"); 14 | let message = std::ffi::CStr::from_ptr(message) 15 | .to_str() 16 | .unwrap_or("INVALID"); 17 | 18 | let (status, level) = match status.0 { 19 | binding::fmi3Status_fmi3OK => ("fmi3OK", log::Level::Info), 20 | binding::fmi3Status_fmi3Warning => ("fmi3Warning", log::Level::Warn), 21 | binding::fmi3Status_fmi3Discard => ("fmi3Discard", log::Level::Warn), 22 | binding::fmi3Status_fmi3Error => ("fmi3Error", log::Level::Error), 23 | binding::fmi3Status_fmi3Fatal => ("fmi3Fatal", log::Level::Error), 24 | _ => unreachable!("Invalid status"), 25 | }; 26 | 27 | log::log!(target: category, level, "[{status}], {message}"); 28 | } 29 | -------------------------------------------------------------------------------- /fmi/src/fmi3/mod.rs: -------------------------------------------------------------------------------- 1 | //! FMI 3.0 API 2 | 3 | pub mod import; 4 | pub mod instance; 5 | pub(crate) mod logger; 6 | #[cfg(feature = "disabled")] 7 | pub mod model; 8 | // Re-export 9 | pub use fmi_schema::fmi3 as schema; 10 | pub use fmi_sys::fmi3 as binding; 11 | 12 | use crate::traits::FmiStatus; 13 | 14 | #[derive(Debug)] 15 | pub enum Fmi3Res { 16 | /// The call was successful. The output argument values are defined. 17 | OK, 18 | /// A non-critical problem was detected, but the computation may continue. The output argument 19 | /// values are defined. Function logMessage should be called by the FMU with further 20 | /// information before returning this status, respecting the current logging settings. 21 | Warning, 22 | } 23 | 24 | #[derive(Debug, thiserror::Error)] 25 | pub enum Fmi3Error { 26 | /// The call was not successful and the FMU is in the same state as before the call. The output 27 | /// argument values are undefined, but the computation may continue. Function logMessage should 28 | /// be called by the FMU with further information before returning this status, respecting the 29 | /// current logging settings. Advanced importers may try alternative approaches to continue the 30 | /// simulation by calling the function with different arguments or calling another function - 31 | /// except in FMI for Scheduled Execution where repeating failed function calls is not allowed. 32 | /// Otherwise the simulation algorithm must treat this return code like [`Fmi3Error::Error`] 33 | /// and must terminate the simulation. 34 | /// 35 | /// [Examples for usage of `Discard` are handling of min/max violation, or signal numerical 36 | /// problems during model evaluation forcing smaller step sizes.] 37 | #[error("Discard")] 38 | Discard, 39 | /// The call failed. The output argument values are undefined and the simulation must not be 40 | /// continued. Function logMessage should be called by the FMU with further information before 41 | /// returning this status, respecting the current logging settings. If a function returns 42 | /// [`Fmi3Error::Error`], it is possible to restore a previously retrieved FMU state by calling 43 | /// [`set_fmu_state`]`. Otherwise [`FreeInstance`] or `Reset` must be called. When detecting 44 | /// illegal arguments or a function call not allowed in the current state according to the 45 | /// respective state machine, the FMU must return fmi3Error. Other instances of this FMU are 46 | /// not affected by the error. 47 | #[error("Error")] 48 | Error, 49 | #[error("Fatal")] 50 | Fatal, 51 | } 52 | 53 | #[derive(Debug)] 54 | pub struct Fmi3Status(binding::fmi3Status); 55 | 56 | impl FmiStatus for Fmi3Status { 57 | type Res = Fmi3Res; 58 | type Err = Fmi3Error; 59 | 60 | /// Convert to [`Result`] 61 | #[inline] 62 | fn ok(self) -> Result { 63 | self.into() 64 | } 65 | 66 | #[inline] 67 | fn is_error(&self) -> bool { 68 | self.0 == binding::fmi3Status_fmi3Error || self.0 == binding::fmi3Status_fmi3Fatal 69 | } 70 | } 71 | 72 | impl From for Fmi3Status { 73 | fn from(status: binding::fmi3Status) -> Self { 74 | Self(status) 75 | } 76 | } 77 | 78 | impl From for Result { 79 | fn from(Fmi3Status(status): Fmi3Status) -> Self { 80 | match status { 81 | binding::fmi3Status_fmi3OK => Ok(Fmi3Res::OK), 82 | binding::fmi3Status_fmi3Warning => Ok(Fmi3Res::Warning), 83 | binding::fmi3Status_fmi3Discard => Err(Fmi3Error::Discard), 84 | binding::fmi3Status_fmi3Error => Err(Fmi3Error::Error), 85 | binding::fmi3Status_fmi3Fatal => Err(Fmi3Error::Fatal), 86 | _ => unreachable!("Invalid status"), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /fmi/src/import.rs: -------------------------------------------------------------------------------- 1 | //! Import is responsible for extracting the FMU, parsing the modelDescription XML and loading the shared library. 2 | 3 | use std::{ 4 | io::{Read, Seek}, 5 | path::Path, 6 | str::FromStr, 7 | }; 8 | 9 | use crate::{traits::FmiImport, Error}; 10 | 11 | use fmi_schema::minimal::MinModelDescription as MinModel; 12 | 13 | const MODEL_DESCRIPTION: &str = "modelDescription.xml"; 14 | 15 | /// Peek at the modelDescription XML without extracting the FMU 16 | pub fn peek_descr_path(path: impl AsRef) -> Result { 17 | let file = std::fs::File::open(path.as_ref())?; 18 | peek_descr(file) 19 | } 20 | 21 | /// Peek at the modelDescription XML without extracting the FMU 22 | pub fn peek_descr(reader: R) -> Result { 23 | let mut archive = zip::ZipArchive::new(reader)?; 24 | let mut descr_file = archive 25 | .by_name(MODEL_DESCRIPTION) 26 | .map_err(|e| Error::ArchiveStructure(e.to_string()))?; 27 | let mut descr_xml = String::new(); 28 | descr_file.read_to_string(&mut descr_xml)?; 29 | let descr = MinModel::from_str(&descr_xml)?; 30 | log::debug!( 31 | "Found FMI {} named '{}'", 32 | descr.fmi_version, 33 | descr.model_name 34 | ); 35 | Ok(descr) 36 | } 37 | 38 | /// Creates a new Import by extracting the FMU and parsing the modelDescription XML 39 | pub fn from_path(path: impl AsRef) -> Result { 40 | let file = std::fs::File::open(path.as_ref())?; 41 | log::debug!("Opening FMU file {:?}", path.as_ref()); 42 | new(file) 43 | } 44 | 45 | /// Creates a new Import by extracting the FMU and parsing the modelDescription XML 46 | pub fn new(reader: R) -> Result { 47 | let mut archive = zip::ZipArchive::new(reader)?; 48 | let temp_dir = tempfile::Builder::new().prefix("fmi-rs").tempdir()?; 49 | log::debug!("Extracting into {temp_dir:?}"); 50 | archive.extract(&temp_dir)?; 51 | 52 | // Open and read the modelDescription XML into a string 53 | let descr_file_path = temp_dir.path().join(MODEL_DESCRIPTION); 54 | let descr_xml = std::fs::read_to_string(descr_file_path)?; 55 | 56 | Imp::new(temp_dir, &descr_xml) 57 | } 58 | -------------------------------------------------------------------------------- /fmi/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The `fmi` crate implements a Rust interface to FMUs (Functional Mockup Units) that follow FMI 2 | //! Standard. This version of the library supports FMI 2.0 and 3.0. See http://www.fmi-standard.org/ 3 | //! 4 | //! ## Examples 5 | //! 6 | //! ```rust,no_run,ignore 7 | //! use fmi::{FmiImport as _, FmiInstance as _}; 8 | //! let import = fmi::Import::new("Modelica_Blocks_Sources_Sine.fmu") 9 | //! .unwrap() 10 | //! .as_fmi2() 11 | //! .unwrap(); 12 | //! assert_eq!(import.model_description().fmi_version, "2.0"); 13 | //! let me = import.instantiate_me("inst1", false, true).unwrap(); 14 | //! assert_eq!(me.get_version(), "2.0"); 15 | //! ``` 16 | #![doc = document_features::document_features!()] 17 | #![deny(clippy::all)] 18 | 19 | // Re-export the fmi-schema crate 20 | pub use fmi_schema as schema; 21 | 22 | use schema::MajorVersion; 23 | 24 | #[cfg(feature = "fmi2")] 25 | pub mod fmi2; 26 | #[cfg(feature = "fmi3")] 27 | pub mod fmi3; 28 | pub mod import; 29 | pub mod traits; 30 | 31 | pub mod built_info { 32 | // The file has been placed there by the build script. 33 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 34 | } 35 | 36 | #[derive(Debug, thiserror::Error)] 37 | pub enum Error { 38 | #[error("Error instantiating import")] 39 | Instantiation, 40 | 41 | #[error("Unknown variable: {}", name)] 42 | UnknownVariable { name: String }, 43 | 44 | #[error("Model type {0} not supported by this FMU")] 45 | UnsupportedFmuType(String), 46 | 47 | #[error("Unsupported platform {os}/{arch}")] 48 | UnsupportedPlatform { os: String, arch: String }, 49 | 50 | #[error("Unsupported FMI version: {0:?}")] 51 | UnsupportedFmiVersion(MajorVersion), 52 | 53 | #[error("Unsupported Interface type: {0}")] 54 | UnsupportedInterface(String), 55 | 56 | #[error("FMI version of loaded API ({found}) doesn't match expected ({expected})")] 57 | FmiVersionMismatch { found: String, expected: String }, 58 | 59 | #[error("FMU archive structure is not as expected: {0}")] 60 | ArchiveStructure(String), 61 | 62 | #[error(transparent)] 63 | Io(#[from] std::io::Error), 64 | 65 | #[error(transparent)] 66 | Zip(#[from] zip::result::ZipError), 67 | 68 | #[error(transparent)] 69 | Schema(#[from] fmi_schema::Error), 70 | 71 | #[error(transparent)] 72 | Utf8Error(#[from] std::str::Utf8Error), 73 | 74 | #[error(transparent)] 75 | LibLoading { 76 | #[from] 77 | source: libloading::Error, 78 | }, 79 | 80 | #[cfg(feature = "fmi2")] 81 | #[error(transparent)] 82 | Fmi2Error(#[from] fmi2::Fmi2Error), 83 | 84 | #[cfg(feature = "fmi3")] 85 | #[error(transparent)] 86 | Fmi3Error(#[from] fmi3::Fmi3Error), 87 | } 88 | -------------------------------------------------------------------------------- /fmi/src/traits.rs: -------------------------------------------------------------------------------- 1 | use fmi_schema::{ 2 | traits::{DefaultExperiment, FmiModelDescription}, 3 | MajorVersion, 4 | }; 5 | 6 | use crate::Error; 7 | 8 | /// Generic FMI import trait 9 | pub trait FmiImport: Sized { 10 | /// The type of the major version 11 | const MAJOR_VERSION: MajorVersion; 12 | 13 | /// The raw parsed XML schema type 14 | type ModelDescription: FmiModelDescription + DefaultExperiment; 15 | 16 | /// The raw FMI bindings type 17 | type Binding; 18 | 19 | /// The type of the value reference used by the FMI API. 20 | type ValueRef; 21 | 22 | /// Create a new FMI import from a directory containing the unzipped FMU 23 | fn new(dir: tempfile::TempDir, schema_xml: &str) -> Result; 24 | 25 | /// Return the path to the extracted FMU 26 | fn archive_path(&self) -> &std::path::Path; 27 | 28 | /// Get the path to the shared library 29 | fn shared_lib_path(&self, model_identifier: &str) -> Result; 30 | 31 | /// Return the path to the resources directory 32 | fn resource_path(&self) -> std::path::PathBuf { 33 | self.archive_path().join("resources") 34 | } 35 | 36 | /// Return a canonical string representation of the resource path 37 | fn canonical_resource_path_string(&self) -> String; 38 | 39 | /// Get a reference to the raw-schema model description 40 | fn model_description(&self) -> &Self::ModelDescription; 41 | 42 | /// Load the plugin shared library and return the raw bindings. 43 | fn binding(&self, model_identifier: &str) -> Result; 44 | } 45 | 46 | /// FMI status trait 47 | pub trait FmiStatus { 48 | type Res; 49 | type Err: Into + std::fmt::Debug; 50 | /// Convert to [`Result`] 51 | fn ok(self) -> Result; 52 | /// Check if the status is an error 53 | fn is_error(&self) -> bool; 54 | } 55 | 56 | /// Generic FMI instance trait 57 | pub trait FmiInstance { 58 | type ModelDescription: FmiModelDescription + DefaultExperiment; 59 | type Import: FmiImport; 60 | type ValueRef: Copy + From + Into; 61 | type Status: FmiStatus; 62 | 63 | /// Get the instance name 64 | fn name(&self) -> &str; 65 | 66 | /// Get the version of the FMU 67 | fn get_version(&self) -> &str; 68 | 69 | /// Get the model description of the FMU 70 | fn model_description(&self) -> &Self::ModelDescription; 71 | 72 | /// The function controls the debug logging that is output by the FMU 73 | /// 74 | /// See 75 | fn set_debug_logging(&mut self, logging_on: bool, categories: &[&str]) -> Self::Status; 76 | 77 | fn enter_initialization_mode( 78 | &mut self, 79 | tolerance: Option, 80 | start_time: f64, 81 | stop_time: Option, 82 | ) -> Self::Status; 83 | 84 | fn exit_initialization_mode(&mut self) -> Self::Status; 85 | 86 | /// Changes state to [`Terminated`](https://fmi-standard.org/docs/3.0.1/#Terminated). 87 | /// 88 | /// See 89 | fn terminate(&mut self) -> Self::Status; 90 | 91 | /// Is called by the environment to reset the FMU after a simulation run. 92 | /// The FMU goes into the same state as if newly created. All variables have their default 93 | /// values. Before starting a new run [`Common::enter_initialization_mode()`] has to be called. 94 | /// 95 | /// See 96 | fn reset(&mut self) -> Self::Status; 97 | 98 | /// Get the number of values required to store the continuous states. Array dimensions are expanded. 99 | fn get_number_of_continuous_state_values(&mut self) -> usize; 100 | 101 | /// Get the number of values required to store the event indicators. Array dimensions are expanded. 102 | fn get_number_of_event_indicator_values(&mut self) -> usize; 103 | } 104 | 105 | /// Generic FMI ModelExchange trait 106 | pub trait FmiModelExchange: FmiInstance { 107 | fn enter_continuous_time_mode(&mut self) -> Self::Status; 108 | 109 | fn enter_event_mode(&mut self) -> Self::Status; 110 | 111 | /// This function is called to signal a converged solution at the current super-dense time 112 | /// instant. `update_discrete_states` must be called at least once per super-dense time 113 | /// instant. 114 | /// 115 | /// See 116 | fn update_discrete_states( 117 | &mut self, 118 | discrete_states_need_update: &mut bool, 119 | terminate_simulation: &mut bool, 120 | nominals_of_continuous_states_changed: &mut bool, 121 | values_of_continuous_states_changed: &mut bool, 122 | next_event_time: &mut Option, 123 | ) -> Self::Status; 124 | 125 | fn completed_integrator_step( 126 | &mut self, 127 | no_set_fmu_state_prior: bool, 128 | enter_event_mode: &mut bool, 129 | terminate_simulation: &mut bool, 130 | ) -> Self::Status; 131 | 132 | fn set_time(&mut self, time: f64) -> Self::Status; 133 | 134 | fn get_continuous_states(&mut self, continuous_states: &mut [f64]) -> Self::Status; 135 | fn set_continuous_states(&mut self, states: &[f64]) -> Self::Status; 136 | 137 | fn get_continuous_state_derivatives(&mut self, derivatives: &mut [f64]) -> Self::Status; 138 | fn get_nominals_of_continuous_states(&mut self, nominals: &mut [f64]) -> Self::Status; 139 | 140 | fn get_event_indicators(&mut self, event_indicators: &mut [f64]) -> Self::Status; 141 | fn get_number_of_event_indicators( 142 | &self, 143 | number_of_event_indicators: &mut usize, 144 | ) -> Self::Status; 145 | } 146 | 147 | /// Event handling interface for ME in FMI2.0 and both ME and CS interfaces in FMI3.0 148 | pub trait FmiEventHandler: FmiInstance { 149 | fn enter_event_mode(&mut self) -> Self::Status; 150 | 151 | fn update_discrete_states( 152 | &mut self, 153 | discrete_states_need_update: &mut bool, 154 | terminate_simulation: &mut bool, 155 | nominals_of_continuous_states_changed: &mut bool, 156 | values_of_continuous_states_changed: &mut bool, 157 | next_event_time: &mut Option, 158 | ) -> Self::Status; 159 | } 160 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | wrap_comments = true 2 | comment_width = 120 3 | normalize_comments = true 4 | -------------------------------------------------------------------------------- /tests/test_imports.rs: -------------------------------------------------------------------------------- 1 | use fmi::traits::{FmiImport, FmiInstance as _}; 2 | use fmi_test_data::ReferenceFmus; 3 | 4 | extern crate fmi; 5 | extern crate fmi_test_data; 6 | 7 | const FMU2_NAMES: [&str; 6] = [ 8 | "BouncingBall", 9 | "Dahlquist", 10 | "Feedthrough", 11 | "Resource", 12 | "Stair", 13 | "VanDerPol", 14 | ]; 15 | 16 | const FMU3_NAMES: [&str; 8] = [ 17 | "BouncingBall", 18 | "Clocks", 19 | "Dahlquist", 20 | "Feedthrough", 21 | "Resource", 22 | "Stair", 23 | "StateSpace", 24 | "VanDerPol", 25 | ]; 26 | 27 | #[test] 28 | fn test_fmi2_imports() { 29 | let mut ref_fmus = ReferenceFmus::new().unwrap(); 30 | 31 | for &name in FMU2_NAMES.iter() { 32 | let import: fmi::fmi2::import::Fmi2Import = ref_fmus 33 | .get_reference_fmu(name) 34 | .expect("Expected FMI2 import"); 35 | assert_eq!(import.model_description().fmi_version, "2.0"); 36 | 37 | #[cfg(target_os = "linux")] 38 | { 39 | if import.model_description().model_exchange.is_some() { 40 | let me = import.instantiate_me("inst1", false, true).unwrap(); 41 | assert_eq!(me.get_version(), "2.0"); 42 | } 43 | 44 | if import.model_description().co_simulation.is_some() { 45 | let cs = import.instantiate_cs("inst1", false, true).unwrap(); 46 | assert_eq!(cs.get_version(), "2.0"); 47 | } 48 | } 49 | } 50 | } 51 | 52 | #[test_log::test] 53 | fn test_fmi3_imports() { 54 | let mut ref_fmus = ReferenceFmus::new().unwrap(); 55 | 56 | for &name in FMU3_NAMES.iter() { 57 | let import: fmi::fmi3::import::Fmi3Import = ref_fmus 58 | .get_reference_fmu(name) 59 | .expect("Expected FMI3 import"); 60 | assert_eq!(import.model_description().fmi_version, "3.0"); 61 | 62 | if import.model_description().model_exchange.is_some() { 63 | let me = import.instantiate_me("inst1", false, true).unwrap(); 64 | assert_eq!(me.get_version(), "3.0"); 65 | } 66 | 67 | if import.model_description().co_simulation.is_some() { 68 | let cs = import 69 | .instantiate_cs("inst1", false, true, false, false, &[]) 70 | .unwrap(); 71 | assert_eq!(cs.get_version(), "3.0"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/test_instances_fmi2.rs: -------------------------------------------------------------------------------- 1 | //! Test the FMI2.0 instance API. 2 | 3 | #[cfg(target_os = "linux")] 4 | use fmi::{ 5 | fmi2::instance::{CoSimulation as _, Common as _, Instance, CS, ME}, 6 | traits::{FmiImport as _, FmiStatus}, 7 | }; 8 | #[cfg(target_os = "linux")] 9 | use fmi_test_data::ReferenceFmus; 10 | 11 | extern crate fmi; 12 | extern crate fmi_test_data; 13 | 14 | #[cfg(target_os = "linux")] 15 | #[test] 16 | fn test_instance_me() { 17 | let mut ref_fmus = ReferenceFmus::new().unwrap(); 18 | let import = ref_fmus.get_reference_fmu("Dahlquist").unwrap(); 19 | let mut instance1 = Instance::::new(&import, "inst1", false, true).unwrap(); 20 | assert_eq!(instance1.get_version(), "2.0"); 21 | 22 | let categories = &import 23 | .model_description() 24 | .log_categories 25 | .as_ref() 26 | .unwrap() 27 | .categories 28 | .iter() 29 | .map(|cat| cat.name.as_ref()) 30 | .collect::>(); 31 | 32 | instance1 33 | .set_debug_logging(true, categories) 34 | .ok() 35 | .expect("set_debug_logging"); 36 | instance1 37 | .setup_experiment(Some(1.0e-6_f64), 0.0, None) 38 | .ok() 39 | .expect("setup_experiment"); 40 | instance1 41 | .enter_initialization_mode() 42 | .ok() 43 | .expect("enter_initialization_mode"); 44 | instance1 45 | .exit_initialization_mode() 46 | .ok() 47 | .expect("exit_initialization_mode"); 48 | instance1.terminate().ok().expect("terminate"); 49 | instance1.reset().ok().expect("reset"); 50 | } 51 | 52 | #[cfg(target_os = "linux")] 53 | #[test] 54 | fn test_instance_cs() { 55 | let mut ref_fmus = ReferenceFmus::new().unwrap(); 56 | let import = ref_fmus.get_reference_fmu("Dahlquist").unwrap(); 57 | 58 | let mut instance1 = Instance::::new(&import, "inst1", false, true).unwrap(); 59 | assert_eq!(instance1.get_version(), "2.0"); 60 | 61 | instance1 62 | .setup_experiment(Some(1.0e-6_f64), 0.0, None) 63 | .ok() 64 | .expect("setup_experiment"); 65 | 66 | instance1 67 | .enter_initialization_mode() 68 | .ok() 69 | .expect("enter_initialization_mode"); 70 | 71 | let sv = import 72 | .model_description() 73 | .model_variable_by_name("k") 74 | .unwrap(); 75 | 76 | instance1 77 | .set_real(&[sv.value_reference], &[2.0f64]) 78 | .ok() 79 | .expect("set k parameter"); 80 | 81 | instance1 82 | .exit_initialization_mode() 83 | .ok() 84 | .expect("exit_initialization_mode"); 85 | 86 | let sv = import 87 | .model_description() 88 | .model_variable_by_name("x") 89 | .unwrap(); 90 | 91 | let mut x = [0.0]; 92 | 93 | instance1 94 | .get_real(&[sv.value_reference], &mut x) 95 | .ok() 96 | .unwrap(); 97 | 98 | assert_eq!(x, [1.0]); 99 | 100 | instance1.do_step(0.0, 0.125, false).ok().expect("do_step"); 101 | 102 | instance1 103 | .get_real(&[sv.value_reference], &mut x) 104 | .ok() 105 | .unwrap(); 106 | 107 | assert_eq!(x, [0.8]); 108 | } 109 | -------------------------------------------------------------------------------- /tests/test_instances_fmi3.rs: -------------------------------------------------------------------------------- 1 | //! Test the FMI3.0 instance API. 2 | 3 | use fmi::{ 4 | fmi3::{ 5 | import::Fmi3Import, 6 | instance::{Common as _, ModelExchange as _}, 7 | }, 8 | traits::{FmiImport as _, FmiStatus}, 9 | }; 10 | use fmi_test_data::ReferenceFmus; 11 | 12 | extern crate fmi; 13 | extern crate fmi_test_data; 14 | 15 | #[test] 16 | fn test_instance() { 17 | let mut ref_fmus = ReferenceFmus::new().unwrap(); 18 | let import: Fmi3Import = ref_fmus.get_reference_fmu("Dahlquist").unwrap(); 19 | let mut inst1 = import.instantiate_me("inst1", true, true).unwrap(); 20 | assert_eq!(inst1.get_version(), "3.0"); 21 | let log_cats: Vec<_> = import 22 | .model_description() 23 | .log_categories 24 | .as_ref() 25 | .unwrap() 26 | .categories 27 | .iter() 28 | .map(|x| x.name.as_str()) 29 | .collect(); 30 | inst1.set_debug_logging(true, &log_cats).ok().unwrap(); 31 | 32 | inst1.enter_configuration_mode().ok().unwrap(); 33 | inst1.exit_configuration_mode().ok().unwrap(); 34 | 35 | inst1 36 | .enter_initialization_mode(None, 0.0, None) 37 | .ok() 38 | .unwrap(); 39 | inst1.exit_initialization_mode().ok().unwrap(); 40 | inst1.set_time(1234.0).ok().unwrap(); 41 | 42 | inst1.enter_continuous_time_mode().ok().unwrap(); 43 | 44 | let states = (0..import 45 | .model_description() 46 | .model_structure 47 | .continuous_state_derivative 48 | .len()) 49 | .map(|x| x as f64) 50 | .collect::>(); 51 | 52 | inst1.set_continuous_states(&states).ok().unwrap(); 53 | let mut enter_event_mode = false; 54 | let mut terminate_simulation = false; 55 | inst1 56 | .completed_integrator_step(false, &mut enter_event_mode, &mut terminate_simulation) 57 | .ok() 58 | .unwrap(); 59 | assert_eq!(enter_event_mode, false); 60 | assert_eq!(terminate_simulation, false); 61 | 62 | let mut ders = vec![0.0; states.len()]; 63 | inst1 64 | .get_continuous_state_derivatives(ders.as_mut_slice()) 65 | .ok() 66 | .unwrap(); 67 | assert_eq!(ders, vec![-0.0]); 68 | } 69 | --------------------------------------------------------------------------------