From fe99d8904dca96110c51d45b8ff7acb2ce1f5bb0 Mon Sep 17 00:00:00 2001 From: coder Date: Sun, 8 Feb 2026 08:42:42 -0800 Subject: [PATCH] add readme --- README.md | 158 ++++++++++++++++++++++++++++++++++ tests/test_readme_examples.py | 139 ++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 tests/test_readme_examples.py diff --git a/README.md b/README.md index e69de29..fab4b8c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,158 @@ +# fmuloader โš™๏ธ + +A lightweight, **zero-dependency** Python library for loading and calling FMI 2.0 +and 3.0 shared-library binaries via ctypes. + +[![PyPI version](https://badge.fury.io/py/fmuloader.svg)](https://badge.fury.io/py/fmuloader) + +## Installation ๐Ÿ“ฆ + +Add `fmuloader` to your project with `uv`: + +```bash +uv add fmuloader +``` + +> To install `uv`, see + +## How to use ๐Ÿš€ + +### FMI 2.0 Co-Simulation + +```python +from fmuloader.fmi2 import Fmi2Slave, Fmi2Type + +slave = Fmi2Slave("model.fmu", model_identifier="MyModel") + +slave.instantiate("instance1", Fmi2Type.CO_SIMULATION, guid="{...}") +slave.setup_experiment(start_time=0.0, stop_time=10.0) +slave.enter_initialization_mode() +slave.exit_initialization_mode() + +t, dt = 0.0, 0.01 +while t < 10.0: + slave.do_step(t, dt) + t += dt + +values = slave.get_real([1, 2]) +slave.terminate() +slave.free_instance() +``` + +### FMI 3.0 Co-Simulation + +```python +from fmuloader.fmi3 import Fmi3Slave + +slave = Fmi3Slave("model.fmu", model_identifier="MyModel") + +slave.instantiate_co_simulation("instance1", instantiation_token="{...}") +slave.enter_initialization_mode(start_time=0.0, stop_time=10.0) +slave.exit_initialization_mode() + +t, dt = 0.0, 0.01 +while t < 10.0: + result = slave.do_step(t, dt) + t += dt + +values = slave.get_float64([1, 2]) +slave.terminate() +slave.free_instance() +``` + +### FMI 3.0 Model Exchange + +```python +from fmuloader.fmi3 import Fmi3Slave + +slave = Fmi3Slave("model.fmu", model_identifier="MyModel") + +slave.instantiate_model_exchange("instance1", instantiation_token="{...}") +slave.enter_initialization_mode(start_time=0.0) +slave.exit_initialization_mode() + +result = slave.update_discrete_states() +while result.discrete_states_need_update: + result = slave.update_discrete_states() +slave.enter_continuous_time_mode() + +nx = slave.get_number_of_continuous_states() +slave.set_time(0.0) +derivs = slave.get_continuous_state_derivatives(nx) + +slave.terminate() +slave.free_instance() +``` + +### FMI 3.0 Scheduled Execution + +```python +from fmuloader.fmi3 import Fmi3Slave + +slave = Fmi3Slave("model.fmu", model_identifier="MyModel") + +slave.instantiate_scheduled_execution("instance1", instantiation_token="{...}") +slave.enter_initialization_mode(start_time=0.0) +slave.exit_initialization_mode() + +slave.activate_model_partition(clock_reference=1001, activation_time=0.0) + +slave.terminate() +slave.free_instance() +``` + +## Features โœจ + +- Load and call FMI **2.0** and **3.0** shared-library binaries via ctypes +- Full Co-Simulation, Model Exchange, and Scheduled Execution support +- All FMI 3.0 data types: `Float32`, `Float64`, `Int8`โ€“`Int64`, `UInt8`โ€“`UInt64`, `Boolean`, `String`, `Binary`, `Clock` +- FMU state management: get, set, serialize, and deserialize +- Directional and adjoint derivatives +- Clock interval and shift functions +- Context manager support for automatic cleanup +- Automatic platform detection (macOS, Linux, Windows; x86, x86_64, aarch64) + +## Design philosophy ๐Ÿ’ก + +**fmuloader intentionally separates binary loading from modelDescription.xml parsing.** + +Most FMI libraries tightly couple XML parsing with binary invocation, pulling in +heavy dependencies and making it hard to use one without the other. fmuloader +takes a different approach: + +- **fmuloader** handles only the **binary loading** โ€” extracting the shared + library from an `.fmu` archive, binding every C function via ctypes, and + exposing thin Python wrappers. +- **modelDescription.xml parsing** is left to the user or to a dedicated library + like [fmureader](https://github.com/time-integral/fmureader). + +This means: + +- **Zero runtime dependencies** โ€” fmuloader uses only the Python standard library. +- **Bring your own parser** โ€” use fmureader, FMPy, lxml, or anything else to + read GUIDs, value references, and variable metadata. Then pass them straight + to fmuloader. +- **Minimal surface area** โ€” each module (`fmi2`, `fmi3`) is a single file you + can vendor into your own project if needed. + +```python +# Example: combine fmureader (parsing) with fmuloader (execution) +import fmureader.fmi3 as reader +from fmuloader.fmi3 import Fmi3Slave + +md = reader.read_model_description("model.fmu") + +slave = Fmi3Slave("model.fmu", model_identifier=md.co_simulation.model_identifier) +slave.instantiate_co_simulation("inst", instantiation_token=md.instantiation_token) +# ... use md.model_variables to look up value references, then call slave.get_float64() etc. +``` + +## Related projects ๐Ÿ”— + +- [fmureader](https://github.com/time-integral/fmureader) โ€” Lightweight Pydantic-based modelDescription.xml parser for FMI 2.0 and 3.0 +- [FMPy](https://github.com/CATIA-Systems/FMPy) โ€” Full-featured FMI library with simulation, GUI, and more + +## Licensing ๐Ÿ“„ + +The code in this project is licensed under MIT license. +See the [LICENSE](LICENSE) file for details. diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py new file mode 100644 index 0000000..bffe1b1 --- /dev/null +++ b/tests/test_readme_examples.py @@ -0,0 +1,139 @@ +"""Tests that mirror the README.md code examples using real reference FMUs. + +Each test corresponds to a code block in the README, ensuring the +documented API actually works. +""" + +import platform + +import pytest +from fmuloader.fmi2 import Fmi2Slave, Fmi2Type +from fmuloader.fmi3 import Fmi3Slave + +# FMI 2.0 reference FMUs don't ship aarch64-darwin binaries +skip_arm64 = pytest.mark.skipif( + platform.machine() == "arm64", + reason="FMI 2.0 reference FMU binaries not available on ARM64", +) + + +class TestReadmeFmi2CoSimulation: + """README ยง FMI 2.0 Co-Simulation.""" + + @skip_arm64 + def test_fmi2_co_simulation(self, reference_fmus_dir): + # -- mirrors README example: FMI 2.0 Co-Simulation -- + from fmuloader.fmi2 import Fmi2Slave, Fmi2Type + + slave = Fmi2Slave( + reference_fmus_dir / "2.0/BouncingBall.fmu", + model_identifier="BouncingBall", + ) + + slave.instantiate( + "instance1", + Fmi2Type.CO_SIMULATION, + guid="{1AE5E10D-9521-4DE3-80B9-D0EAAA7D5AF1}", + ) + slave.setup_experiment(start_time=0.0, stop_time=10.0) + slave.enter_initialization_mode() + slave.exit_initialization_mode() + + t, dt = 0.0, 0.01 + while t < 1.0: # shortened from 10.0 for speed + slave.do_step(t, dt) + t += dt + + values = slave.get_real([1, 3]) + assert len(values) == 2 + slave.terminate() + slave.free_instance() + + +class TestReadmeFmi3CoSimulation: + """README ยง FMI 3.0 Co-Simulation.""" + + def test_fmi3_co_simulation(self, reference_fmus_dir): + # -- mirrors README example: FMI 3.0 Co-Simulation -- + from fmuloader.fmi3 import Fmi3Slave + + slave = Fmi3Slave( + reference_fmus_dir / "3.0/BouncingBall.fmu", + model_identifier="BouncingBall", + ) + + slave.instantiate_co_simulation( + "instance1", + instantiation_token="{1AE5E10D-9521-4DE3-80B9-D0EAAA7D5AF1}", + ) + slave.enter_initialization_mode(start_time=0.0, stop_time=10.0) + slave.exit_initialization_mode() + + t, dt = 0.0, 0.01 + while t < 1.0: # shortened from 10.0 for speed + result = slave.do_step(t, dt) + t += dt + + values = slave.get_float64([1, 3]) + assert len(values) == 2 + slave.terminate() + slave.free_instance() + + +class TestReadmeFmi3ModelExchange: + """README ยง FMI 3.0 Model Exchange.""" + + def test_fmi3_model_exchange(self, reference_fmus_dir): + # -- mirrors README example: FMI 3.0 Model Exchange -- + from fmuloader.fmi3 import Fmi3Slave + + slave = Fmi3Slave( + reference_fmus_dir / "3.0/VanDerPol.fmu", + model_identifier="VanDerPol", + ) + + slave.instantiate_model_exchange( + "instance1", + instantiation_token="{BD403596-3166-4232-ABC2-132BDF73E644}", + ) + slave.enter_initialization_mode(start_time=0.0) + slave.exit_initialization_mode() + + result = slave.update_discrete_states() + while result.discrete_states_need_update: + result = slave.update_discrete_states() + slave.enter_continuous_time_mode() + + nx = slave.get_number_of_continuous_states() + assert nx == 2 + slave.set_time(0.0) + derivs = slave.get_continuous_state_derivatives(nx) + assert len(derivs) == nx + + slave.terminate() + slave.free_instance() + + +class TestReadmeFmi3ScheduledExecution: + """README ยง FMI 3.0 Scheduled Execution.""" + + def test_fmi3_scheduled_execution(self, reference_fmus_dir): + # -- mirrors README example: FMI 3.0 Scheduled Execution -- + from fmuloader.fmi3 import Fmi3Slave + + slave = Fmi3Slave( + reference_fmus_dir / "3.0/Clocks.fmu", + model_identifier="Clocks", + ) + + slave.instantiate_scheduled_execution( + "instance1", + instantiation_token="{C5F142BA-B849-42DA-B4A1-4745BFF3BE28}", + ) + slave.enter_initialization_mode(start_time=0.0) + slave.exit_initialization_mode() + + slave.activate_model_partition(clock_reference=1001, activation_time=0.0) + + slave.terminate() + slave.free_instance()