From e0f9f6764f1cc1781948d01e708ed1c596c68d3e Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Tue, 4 Aug 2026 10:21:51 +0200 Subject: [PATCH 01/20] Re-structured project to the standard src/pythonfmu3/ structure and moving tests/ out. Ran ruff check --fix and ruff format on project --- README.md | 68 +++- docs/index.md | 10 +- docs/usage.md | 135 +++++-- docs/usageMX.md | 237 +++++++++--- examples/Enum.py | 40 +- examples/alltypes.py | 85 +++-- examples/arraytypes.py | 100 +++-- examples/bouncingBall.py | 97 ++++- examples/bouncingBall_me.py | 124 ++++-- examples/dahlquist_me.py | 52 ++- examples/dahlquist_me_cs.py | 55 ++- examples/demoslave.py | 40 +- examples/linearTransform.py | 49 ++- examples/mldemo.py | 37 +- examples/robertson_me.py | 81 +++- examples/sympyslave.py | 31 +- .../sin-approximation.ipynb | 161 ++++---- examples/vanderpol_me.py | 74 +++- pythonfmu3/default_experiment.py | 7 - pythonfmu3/tests/conftest.py | 6 - pythonfmu3/tests/slaves/pythonslave.py | 52 --- pythonfmu3/tests/slaves/pythonslaveMX.py | 27 -- pythonfmu3/tests/slaves/pythonslaveMXCS.py | 32 -- .../tests/slaves/pythonslave_arraytypes.py | 80 ---- .../tests/slaves/pythonslave_read_file.py | 23 -- pythonfmu3/tests/utils.py | 13 - {pythonfmu3 => src/pythonfmu3}/__init__.py | 17 +- {pythonfmu3 => src/pythonfmu3}/__main__.py | 8 +- {pythonfmu3 => src/pythonfmu3}/_version.py | 0 {pythonfmu3 => src/pythonfmu3}/builder.py | 65 ++-- .../pythonfmu3}/cosimulation.py | 4 +- src/pythonfmu3/default_experiment.py | 12 + {pythonfmu3 => src/pythonfmu3}/deploy.py | 48 ++- {pythonfmu3 => src/pythonfmu3}/enums.py | 2 + {pythonfmu3 => src/pythonfmu3}/fmi3slave.py | 259 ++++++++----- {pythonfmu3 => src/pythonfmu3}/logmsg.py | 7 +- .../pythonfmu3}/modelexchange.py | 21 +- {pythonfmu3 => src/pythonfmu3}/osutil.py | 7 +- .../pythonfmu-export/.clang-format | 0 .../pythonfmu-export/CMakeLists.txt | 0 .../pythonfmu-export/build_unix.sh | 0 .../pythonfmu3}/pythonfmu-export/build_win.sh | 0 .../pythonfmu-export/src/CMakeLists.txt | 0 .../src/cppfmu/cppfmu_common.hpp | 0 .../pythonfmu-export/src/cppfmu/cppfmu_cs.cpp | 0 .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 0 .../src/cppfmu/fmi_functions.cpp | 0 .../src/fmi/fmi3FunctionTypes.h | 0 .../pythonfmu-export/src/fmi/fmi3Functions.h | 0 .../src/fmi/fmi3PlatformTypes.h | 0 .../src/pythonfmu/IPyState.hpp | 0 .../src/pythonfmu/PySlaveInstance.cpp | 0 .../src/pythonfmu/PySlaveInstance.hpp | 0 .../src/pythonfmu/PyState.hpp | 0 {pythonfmu3 => src/pythonfmu3}/unit.py | 57 +-- .../pythonfmu3}/variable_types.py | 37 +- {pythonfmu3 => src/pythonfmu3}/variables.py | 210 ++++++---- {pythonfmu3/tests => tests}/__init__.py | 0 tests/conftest.py | 4 + {pythonfmu3/tests => tests}/data/csvdemo.csv | 0 {pythonfmu3/tests => tests}/data/hello.txt | 0 .../slaves/PythonSlaveWithException.py | 11 +- .../tests => tests}/slaves/localmodule.py | 4 +- tests/slaves/pythonslave.py | 91 +++++ tests/slaves/pythonslaveMX.py | 57 +++ tests/slaves/pythonslaveMXCS.py | 65 ++++ tests/slaves/pythonslave_arraytypes.py | 103 +++++ tests/slaves/pythonslave_read_file.py | 39 ++ .../tests => tests}/slaves/slavewithdep.py | 19 +- {pythonfmu3/tests => tests}/test_builder.py | 15 +- {pythonfmu3/tests => tests}/test_deploy.py | 14 +- {pythonfmu3/tests => tests}/test_docs.py | 68 ++-- {pythonfmu3/tests => tests}/test_fmi3slave.py | 92 ++--- .../tests => tests}/test_integration.py | 139 ++++--- {pythonfmu3/tests => tests}/test_logger.py | 67 ++-- .../tests => tests}/test_multiple_fmus.py | 20 +- {pythonfmu3/tests => tests}/test_variables.py | 359 +++++++++++------- tests/utils.py | 17 + 78 files changed, 2406 insertions(+), 1248 deletions(-) delete mode 100644 pythonfmu3/default_experiment.py delete mode 100644 pythonfmu3/tests/conftest.py delete mode 100644 pythonfmu3/tests/slaves/pythonslave.py delete mode 100644 pythonfmu3/tests/slaves/pythonslaveMX.py delete mode 100644 pythonfmu3/tests/slaves/pythonslaveMXCS.py delete mode 100644 pythonfmu3/tests/slaves/pythonslave_arraytypes.py delete mode 100644 pythonfmu3/tests/slaves/pythonslave_read_file.py delete mode 100644 pythonfmu3/tests/utils.py rename {pythonfmu3 => src/pythonfmu3}/__init__.py (55%) rename {pythonfmu3 => src/pythonfmu3}/__main__.py (87%) rename {pythonfmu3 => src/pythonfmu3}/_version.py (100%) rename {pythonfmu3 => src/pythonfmu3}/builder.py (88%) rename {pythonfmu3 => src/pythonfmu3}/cosimulation.py (92%) create mode 100644 src/pythonfmu3/default_experiment.py rename {pythonfmu3 => src/pythonfmu3}/deploy.py (75%) rename {pythonfmu3 => src/pythonfmu3}/enums.py (99%) rename {pythonfmu3 => src/pythonfmu3}/fmi3slave.py (74%) rename {pythonfmu3 => src/pythonfmu3}/logmsg.py (64%) rename {pythonfmu3 => src/pythonfmu3}/modelexchange.py (89%) rename {pythonfmu3 => src/pythonfmu3}/osutil.py (92%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/.clang-format (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/CMakeLists.txt (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/build_unix.sh (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/build_win.sh (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/CMakeLists.txt (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/cppfmu/cppfmu_common.hpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/cppfmu/fmi_functions.cpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/fmi/fmi3FunctionTypes.h (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/fmi/fmi3Functions.h (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/fmi/fmi3PlatformTypes.h (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/pythonfmu/IPyState.hpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp (100%) rename {pythonfmu3 => src/pythonfmu3}/pythonfmu-export/src/pythonfmu/PyState.hpp (100%) rename {pythonfmu3 => src/pythonfmu3}/unit.py (50%) rename {pythonfmu3 => src/pythonfmu3}/variable_types.py (78%) rename {pythonfmu3 => src/pythonfmu3}/variables.py (76%) rename {pythonfmu3/tests => tests}/__init__.py (100%) create mode 100644 tests/conftest.py rename {pythonfmu3/tests => tests}/data/csvdemo.csv (100%) rename {pythonfmu3/tests => tests}/data/hello.txt (100%) rename {pythonfmu3/tests => tests}/slaves/PythonSlaveWithException.py (59%) rename {pythonfmu3/tests => tests}/slaves/localmodule.py (79%) create mode 100644 tests/slaves/pythonslave.py create mode 100644 tests/slaves/pythonslaveMX.py create mode 100644 tests/slaves/pythonslaveMXCS.py create mode 100644 tests/slaves/pythonslave_arraytypes.py create mode 100644 tests/slaves/pythonslave_read_file.py rename {pythonfmu3/tests => tests}/slaves/slavewithdep.py (51%) rename {pythonfmu3/tests => tests}/test_builder.py (94%) rename {pythonfmu3/tests => tests}/test_deploy.py (85%) rename {pythonfmu3/tests => tests}/test_docs.py (66%) rename {pythonfmu3/tests => tests}/test_fmi3slave.py (77%) rename {pythonfmu3/tests => tests}/test_integration.py (86%) rename {pythonfmu3/tests => tests}/test_logger.py (78%) rename {pythonfmu3/tests => tests}/test_multiple_fmus.py (92%) rename {pythonfmu3/tests => tests}/test_variables.py (52%) create mode 100644 tests/utils.py diff --git a/README.md b/README.md index b295c8b..6ab0ef2 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,10 @@ optional arguments: #### Write the script ```python - from pythonfmu3 import Fmi3Causality, Fmi3Slave, Boolean, Int32, Float64, String class PythonSlave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -114,16 +112,17 @@ class PythonSlave(Fmi3Slave): self.register_variable(Float64("time", causality=Fmi3Causality.independent)) self.register_variable(Int32("intOut", causality=Fmi3Causality.output)) self.register_variable(Float64("realOut", causality=Fmi3Causality.output)) - self.register_variable(Boolean("booleanVariable", causality=Fmi3Causality.local)) + self.register_variable( + Boolean("booleanVariable", causality=Fmi3Causality.local) + ) self.register_variable(String("stringVariable", causality=Fmi3Causality.local)) - + # Note: # it is also possible to explicitly define getters and setters as lambdas in case the variable is not backed by a Python field. # self.register_variable(Float64("myReal", causality=Fmi3Causality.output, getter=lambda: self.realOut, setter=lambda v: set_real_out(v)) def do_step(self, current_time, step_size): return True - ``` #### Create the FMU @@ -144,13 +143,11 @@ To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of #### Write the script ```python - from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange from typing import List class PythonSlaveMX(Fmi3SlaveBase, ModelExchange): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -164,20 +161,59 @@ class PythonSlaveMX(Fmi3SlaveBase, ModelExchange): self.derx0 = 0.0 self.derx1 = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x0", causality=Fmi3Causality.output, start=2, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("x1", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx0", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("derx1", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=2)) - self.register_variable(Float64("mu", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x0", + causality=Fmi3Causality.output, + start=2, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "x1", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx0", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "derx1", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=2, + ) + ) + self.register_variable( + Float64( + "mu", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) def get_continuous_state_derivatives(self) -> List[float]: self.derx0 = self.x1 self.derx1 = self.mu * ((1 - self.x0**2) * self.x1) - self.x0 return [self.derx0, self.derx1] - - ``` Note: There is a hard requirement that we have a `self.time` member in the derived class. diff --git a/docs/index.md b/docs/index.md index 5adf2f9..7e3347e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,18 +56,19 @@ pip install pythonfmu3 ```python from pythonfmu3 import Fmi3Slave, Fmi3Causality, Float64 + class MyModel(Fmi3Slave): def __init__(self, **kwargs): super().__init__(**kwargs) - + # Register variables self.register_variable(Float64("input", causality=Fmi3Causality.input)) self.register_variable(Float64("output", causality=Fmi3Causality.output)) - + # Initialize values self.input = 0.0 self.output = 0.0 - + def do_step(self, current_time, step_size): # Your model logic here self.output = 2.0 * self.input @@ -82,7 +83,8 @@ pythonfmu3 build -f my_model.py **3. Use in simulation:** ```python import fmpy -result = fmpy.simulate_fmu('MyModel.fmu', stop_time=10.0) + +result = fmpy.simulate_fmu("MyModel.fmu", stop_time=10.0) ``` ## Architecture diff --git a/docs/usage.md b/docs/usage.md index fb758c7..9396b73 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -29,14 +29,22 @@ Co-simulation FMUs support various variable types and causalities: ```python # Input variables (set by master algorithm) -self.register_variable(Float64("input_signal", causality=Fmi3Causality.input, start=0.0)) +self.register_variable( + Float64("input_signal", causality=Fmi3Causality.input, start=0.0) +) # Output variables (read by master algorithm) self.register_variable(Float64("output_signal", causality=Fmi3Causality.output)) # Parameters (configurable, usually fixed during simulation) -self.register_variable(Float64("gain", causality=Fmi3Causality.parameter, - variability=Fmi3Variability.tunable, start=1.0)) +self.register_variable( + Float64( + "gain", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + start=1.0, + ) +) ``` @@ -44,6 +52,7 @@ self.register_variable(Float64("gain", causality=Fmi3Causality.parameter, ```python from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64 + class PIDController(Fmi3Slave): """PID Controller implementation""" @@ -56,14 +65,14 @@ class PIDController(Fmi3Slave): # Inputs self.setpoint = 0.0 self.measurement = 0.0 - + # Outputs self.control_output = 0.0 # Parameters self.kp = 1.0 # Proportional gain self.ki = 0.1 # Integral gain - self.kd = 0.01 # Derivative gain + self.kd = 0.01 # Derivative gain # Internal states self.integral_error = 0.0 @@ -71,12 +80,22 @@ class PIDController(Fmi3Slave): self.first_step = True # Register variables - self.register_variable(Float64("setpoint", causality=Fmi3Causality.input, start=0.0)) - self.register_variable(Float64("measurement", causality=Fmi3Causality.input, start=0.0)) + self.register_variable( + Float64("setpoint", causality=Fmi3Causality.input, start=0.0) + ) + self.register_variable( + Float64("measurement", causality=Fmi3Causality.input, start=0.0) + ) self.register_variable(Float64("output", causality=Fmi3Causality.output)) - self.register_variable(Float64("Kp", causality=Fmi3Causality.parameter, start=1.0)) - self.register_variable(Float64("Ki", causality=Fmi3Causality.parameter, start=0.1)) - self.register_variable(Float64("Kd", causality=Fmi3Causality.parameter, start=0.01)) + self.register_variable( + Float64("Kp", causality=Fmi3Causality.parameter, start=1.0) + ) + self.register_variable( + Float64("Ki", causality=Fmi3Causality.parameter, start=0.1) + ) + self.register_variable( + Float64("Kd", causality=Fmi3Causality.parameter, start=0.01) + ) def do_step(self, current_time, step_size): # Calculate error @@ -103,7 +122,6 @@ class PIDController(Fmi3Slave): self.previous_error = error return True - ``` ### Units @@ -113,6 +131,7 @@ Units can be added to this example through the `Unit` class along with `register ```python from pythonfmu3 import Unit + velocity_unit = Unit(name="m/s", m=1, s=-1) ``` which can now be utilized in the register variables call, @@ -127,12 +146,19 @@ self.register_variable(Float64("v", causality=Fmi3Causality.output, start=0, var If numpy is installed, arrays may be used with, ```python -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3Slave, Float64, UInt64, Fmi3Initial, Dimension +from pythonfmu3 import ( + Fmi3Causality, + Fmi3Variability, + Fmi3Slave, + Float64, + UInt64, + Fmi3Initial, + Dimension, +) import numpy as np class LinearTransform(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -150,17 +176,56 @@ class LinearTransform(Fmi3Slave): self.A = np.array([1.0, 1.0, 2.0, 1.0]).reshape((self.m, self.n)) self.y = np.array([0.0, 0.0]).reshape((self.m, 1)) - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(UInt64("m", causality=Fmi3Causality.structuralParameter, variability=Fmi3Variability.tunable, start=2)) - self.register_variable(Float64("scalar", causality=Fmi3Causality.input, start=2.0)) - self.register_variable(Float64("u", causality=Fmi3Causality.input, dimensions=[Dimension(start=f"{self.n}")])) - self.register_variable(Float64("offset", causality=Fmi3Causality.input, dimensions=[Dimension(start=f"{self.m}")])) - self.register_variable(Float64("A", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable, dimensions=[Dimension(start=f"{self.m}"), Dimension(start=f"{self.n}")])) - self.register_variable(Float64("y", causality=Fmi3Causality.output, dimensions=[Dimension(valueReference="1")])) + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + UInt64( + "m", + causality=Fmi3Causality.structuralParameter, + variability=Fmi3Variability.tunable, + start=2, + ) + ) + self.register_variable( + Float64("scalar", causality=Fmi3Causality.input, start=2.0) + ) + self.register_variable( + Float64( + "u", + causality=Fmi3Causality.input, + dimensions=[Dimension(start=f"{self.n}")], + ) + ) + self.register_variable( + Float64( + "offset", + causality=Fmi3Causality.input, + dimensions=[Dimension(start=f"{self.m}")], + ) + ) + self.register_variable( + Float64( + "A", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + dimensions=[Dimension(start=f"{self.m}"), Dimension(start=f"{self.n}")], + ) + ) + self.register_variable( + Float64( + "y", + causality=Fmi3Causality.output, + dimensions=[Dimension(valueReference="1")], + ) + ) def do_step(self, current_time, step_size): - self.y = self.scalar*self.A.dot(self.u) + self.offset + self.y = self.scalar * self.A.dot(self.u) + self.offset return True ``` @@ -170,6 +235,7 @@ The `do_step` function can also return a tuple of flags, corresponding to those ```python from pythonfmu3 import Fmi3Status, Fmi3StepResult + def do_step(self, current_time, step_size): self.y = 1 + 2 + 3 + 4 + ... terminate = False @@ -199,34 +265,27 @@ import matplotlib.pyplot as plt # Simulate the PID controller FMU result = simulate_fmu( - filename='PIDController.fmu', + filename="PIDController.fmu", start_time=0.0, stop_time=10.0, step_size=0.01, - start_values={ - 'Kp': 2.0, - 'Ki': 0.5, - 'Kd': 0.1, - 'setpoint': 1.0 - }, - input={ - 'measurement': [(0.0, 0.0), (1.0, 0.3), (5.0, 0.8), (10.0, 1.0)] - } + start_values={"Kp": 2.0, "Ki": 0.5, "Kd": 0.1, "setpoint": 1.0}, + input={"measurement": [(0.0, 0.0), (1.0, 0.3), (5.0, 0.8), (10.0, 1.0)]}, ) # Plot results plt.figure(figsize=(12, 6)) plt.subplot(2, 1, 1) -plt.plot(result['time'], result['setpoint'], label='Setpoint') -plt.plot(result['time'], result['measurement'], label='Measurement') -plt.ylabel('Value') +plt.plot(result["time"], result["setpoint"], label="Setpoint") +plt.plot(result["time"], result["measurement"], label="Measurement") +plt.ylabel("Value") plt.legend() plt.grid(True) plt.subplot(2, 1, 2) -plt.plot(result['time'], result['output'], label='Control Output') -plt.xlabel('Time [s]') -plt.ylabel('Control Signal') +plt.plot(result["time"], result["output"], label="Control Output") +plt.xlabel("Time [s]") +plt.ylabel("Control Signal") plt.legend() plt.grid(True) plt.show() diff --git a/docs/usageMX.md b/docs/usageMX.md index f91f090..f73a0c3 100644 --- a/docs/usageMX.md +++ b/docs/usageMX.md @@ -24,22 +24,34 @@ Model Exchange FMUs require specific variable declarations: ```python # Independent variable (time) -self.register_variable(Float64("time", - causality=Fmi3Causality.independent, - variability=Fmi3Variability.continuous)) +self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) +) # States with initial values -self.register_variable(Float64("x0", - causality=Fmi3Causality.output, - start=2.0, # Initial value - variability=Fmi3Variability.continuous, - initial=Fmi3Initial.exact)) # Must be exact +self.register_variable( + Float64( + "x0", + causality=Fmi3Causality.output, + start=2.0, # Initial value + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) +) # Must be exact # Derivatives (local variables) -self.register_variable(Float64("derx0", - causality=Fmi3Causality.local, - variability=Fmi3Variability.continuous, - derivative=1)) # Derivative of variable 1 (x0) +self.register_variable( + Float64( + "derx0", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) +) # Derivative of variable 1 (x0) ``` @@ -62,13 +74,23 @@ def get_continuous_state_derivatives(self): ## Example ```python - -from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import ( + Fmi3Causality, + ModelExchange, + Fmi3Variability, + Fmi3SlaveBase, + Fmi3Status, + Float64, + Fmi3Initial, + Unit, + Float64Type, + Fmi3StepResult, +) from typing import List -class VanDerPol(Fmi3SlaveBase, ModelExchange): +class VanDerPol(Fmi3SlaveBase, ModelExchange): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -82,22 +104,62 @@ class VanDerPol(Fmi3SlaveBase, ModelExchange): self.derx0 = 0.0 self.derx1 = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x0", causality=Fmi3Causality.output, start=2, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("x1", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx0", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("derx1", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=2)) - self.register_variable(Float64("mu", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x0", + causality=Fmi3Causality.output, + start=2, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "x1", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx0", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "derx1", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=2, + ) + ) + self.register_variable( + Float64( + "mu", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) def get_continuous_state_derivatives(self) -> List[float]: self.derx0 = self.x1 self.derx1 = self.mu * ((1 - self.x0**2) * self.x1) - self.x0 return [self.derx0, self.derx1] - + def get_nominals_of_continuous_states(self, nStates: int) -> List[float]: return [1.0, 1.0] - ``` @@ -125,15 +187,16 @@ result = fmpy.simulate_fmu( fmu_filename, stop_time=20.0, step_size=0.01, - start_values={'mu': 1.0, 'x0': 2.0, 'x1': 0.0} + start_values={"mu": 1.0, "x0": 2.0, "x1": 0.0}, ) # Plot results import matplotlib.pyplot as plt -plt.plot(result['time'], result['x0'], label='x0') -plt.plot(result['time'], result['x1'], label='x1') -plt.xlabel('Time') -plt.ylabel('State Values') + +plt.plot(result["time"], result["x0"], label="x0") +plt.plot(result["time"], result["x1"], label="x1") +plt.xlabel("Time") +plt.ylabel("State Values") plt.legend() plt.grid(True) plt.show() @@ -147,7 +210,16 @@ For systems with discrete events. Firstly, mark the event indicators in register ```python -self.register_variable(Float64("h", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact), has_event_indicator=True) +self.register_variable( + Float64( + "h", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ), + has_event_indicator=True, +) ``` @@ -158,6 +230,7 @@ Then define the event methods, `get_event_indicators()` and `update_discrete_sta def get_event_indicators(self): return [0.2] + def update_discrete_states(self): """Handle discrete state updates after events""" fdsr = Fmi3UpdateDiscreteStatesResult() @@ -169,7 +242,15 @@ def update_discrete_states(self): ### Example ```python -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3SlaveBase, ModelExchange, Float64, Fmi3Initial, Fmi3UpdateDiscreteStatesResult +from pythonfmu3 import ( + Fmi3Causality, + Fmi3Variability, + Fmi3SlaveBase, + ModelExchange, + Float64, + Fmi3Initial, + Fmi3UpdateDiscreteStatesResult, +) from typing import List @@ -177,8 +258,8 @@ import sys EVENT_EPS = 1e-12 -class BouncingBall(Fmi3SlaveBase, ModelExchange): +class BouncingBall(Fmi3SlaveBase, ModelExchange): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -194,50 +275,94 @@ class BouncingBall(Fmi3SlaveBase, ModelExchange): self.e = 0.7 self.v_min = 0.1 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - - self.register_variable(Float64("h", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact), - has_event_indicator=True) - self.register_variable(Float64("derh", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("v", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derv", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=3)) - - self.register_variable(Float64("g", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - self.register_variable(Float64("e", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable(Float64("v_min", variability=Fmi3Variability.constant, start=0.1)) - - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + + self.register_variable( + Float64( + "h", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ), + has_event_indicator=True, + ) + self.register_variable( + Float64( + "derh", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "v", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derv", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=3, + ) + ) + + self.register_variable( + Float64( + "g", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) + self.register_variable( + Float64( + "e", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable( + Float64("v_min", variability=Fmi3Variability.constant, start=0.1) + ) def get_continuous_state_derivatives(self) -> List[float]: self.derh = self.v self.derv = self.g return [self.derh, self.derv] - + def get_event_indicators(self) -> List[float]: z = [self.h] - if self.h > -EVENT_EPS and self.h <=0 and self.v > 0: + if self.h > -EVENT_EPS and self.h <= 0 and self.v > 0: z[0] = -EVENT_EPS return z - + def update_discrete_states(self): fdsr = Fmi3UpdateDiscreteStatesResult() if self.h <= 0 and self.v < 0: self.h = sys.float_info.min self.v = -self.v * self.e - + if self.v < self.v_min: - self.v = 0.0; - self.g = 0.0; - + self.v = 0.0 + self.g = 0.0 + fdsr.valuesOfContinuousStatesChanged = True return fdsr - - - - ``` ## Build the FMU diff --git a/examples/Enum.py b/examples/Enum.py index 967a212..95c3525 100644 --- a/examples/Enum.py +++ b/examples/Enum.py @@ -1,10 +1,17 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3Slave, Float64, Enumeration, Fmi3Initial, EnumerationType - import enum +from pythonfmu3 import ( + Enumeration, + EnumerationType, + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3Slave, + Fmi3Variability, +) -class Enum(Fmi3Slave): +class Enum(Fmi3Slave): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -14,14 +21,31 @@ def __init__(self, **kwargs): self.time = 0.0 self.x = 0.0 - Options = enum.Enum('Options', {'Option_1': 1, 'Option_2': 2}) + Options = enum.Enum("Options", {"Option_1": 1, "Option_2": 2}) enum_type = EnumerationType(name="Option", values=Options) self.option = Options.Option_1 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x", causality=Fmi3Causality.output, initial=Fmi3Initial.calculated)) - self.register_variable(Enumeration("option", declared_type="Option", start=1, causality=Fmi3Causality.input, - getter=lambda: self.option.value, setter=lambda v: setattr(self, "option", Options(v))), var_type=enum_type) + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64("x", causality=Fmi3Causality.output, initial=Fmi3Initial.calculated) + ) + self.register_variable( + Enumeration( + "option", + declared_type="Option", + start=1, + causality=Fmi3Causality.input, + getter=lambda: self.option.value, + setter=lambda v: setattr(self, "option", Options(v)), + ), + var_type=enum_type, + ) def do_step(self, current_time, step_size): self.x += 1.0 diff --git a/examples/alltypes.py b/examples/alltypes.py index 5bad6b9..75492e9 100644 --- a/examples/alltypes.py +++ b/examples/alltypes.py @@ -1,52 +1,67 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Dimension, Fmi3Slave, Fmi3Status, Float64, Int32, Int64, UInt64, String, Boolean - +from pythonfmu3 import ( + Boolean, + Float64, + Fmi3Causality, + Fmi3Slave, + Fmi3Status, + Fmi3Variability, + Int32, + Int64, + UInt64, +) TYPES = [UInt64, Float64, Boolean, Int32, Int64] CAUSALITY = [Fmi3Causality.output, Fmi3Causality.input] -type_map = { - UInt64: int, - Float64: float, - Int32: int, - Int64: int, - Boolean: bool -} +type_map = {UInt64: int, Float64: float, Int32: int, Int64: int, Boolean: bool} + def var_names(var_type, causality): return f"{var_type.__name__.lower()}_{causality.name.lower()}" - + + def init_var(var_type, causality): return type_map[var_type]() - + + def create_vars(self): for var_type in TYPES: - for causality in CAUSALITY: - name = var_names(var_type, causality) - if var_type == Float64: - var = var_type(name, causality=causality, variability=Fmi3Variability.continuous) - else: - var = var_type(name, causality=causality, variability=Fmi3Variability.discrete) - setattr(self, name, init_var(var_type, causality)) - self.register_variable(var) + for causality in CAUSALITY: + name = var_names(var_type, causality) + if var_type == Float64: + var = var_type( + name, causality=causality, variability=Fmi3Variability.continuous + ) + else: + var = var_type( + name, causality=causality, variability=Fmi3Variability.discrete + ) + setattr(self, name, init_var(var_type, causality)) + self.register_variable(var) + class AllTypes(Fmi3Slave): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Stephen Smith" + self.description = "All types example" - def __init__(self, **kwargs): - super().__init__(**kwargs) + self.time = 0.0 - self.author = "Stephen Smith" - self.description = "All types example" - - self.time = 0.0 + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + create_vars(self) - create_vars(self) - + def do_step(self, current_time: float, step_size: float) -> Fmi3Status: + # feedthrough + for var_type in TYPES: + input_var = getattr(self, var_names(var_type, Fmi3Causality.input)) + setattr(self, var_names(var_type, Fmi3Causality.output), input_var) - def do_step(self, current_time: float, step_size: float) -> Fmi3Status: - # feedthrough - for var_type in TYPES: - input_var = getattr(self, var_names(var_type, Fmi3Causality.input)) - setattr(self, var_names(var_type, Fmi3Causality.output), input_var) - - return True + return True diff --git a/examples/arraytypes.py b/examples/arraytypes.py index b04b4ff..313769e 100644 --- a/examples/arraytypes.py +++ b/examples/arraytypes.py @@ -1,8 +1,18 @@ - -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Dimension, Fmi3Slave, Fmi3Status, Float64, Int32, Int64, UInt64, String, Boolean - import numpy as np +from pythonfmu3 import ( + Boolean, + Dimension, + Float64, + Fmi3Causality, + Fmi3Slave, + Fmi3Status, + Fmi3Variability, + Int32, + Int64, + UInt64, +) + SIZE = 10 TYPES = [UInt64, Float64, Boolean, Int32, Int64] @@ -13,9 +23,10 @@ Float64: np.float64, Int32: np.int32, Int64: np.int64, - Boolean: bool + Boolean: bool, } + def var_names(var_type, causality): return f"{var_type.__name__.lower()}_{causality.name.lower()}" @@ -30,45 +41,64 @@ def init_var(var_type, causality, array=True): def create_vars(self): dimensions = [Dimension(start=str(SIZE))] for var_type in TYPES: - for causality in CAUSALITY: - name = var_names(var_type, causality) - if var_type == Float64: - var = var_type(name, causality=causality, variability=Fmi3Variability.continuous, dimensions=dimensions) - else: - var = var_type(name, causality=causality, variability=Fmi3Variability.discrete, dimensions=dimensions) - setattr(self, name, init_var(var_type, causality)) - self.register_variable(var) + for causality in CAUSALITY: + name = var_names(var_type, causality) + if var_type == Float64: + var = var_type( + name, + causality=causality, + variability=Fmi3Variability.continuous, + dimensions=dimensions, + ) + else: + var = var_type( + name, + causality=causality, + variability=Fmi3Variability.discrete, + dimensions=dimensions, + ) + setattr(self, name, init_var(var_type, causality)) + self.register_variable(var) def generate_random_data(self): for var_type in TYPES: - causality = Fmi3Causality.output - name = var_names(var_type, causality) - var = getattr(self, name) - if var_type == Boolean: - setattr(self, name, np.random.choice(a=[False, True], size=SIZE).astype(bool)) - elif var_type == Int32 or var_type == Int64 or var_type == UInt64: - setattr(self, name, np.random.randint(0, 100, size=SIZE).astype(TYPE_MAP[var_type])) - else: - setattr(self, name, np.random.rand(SIZE).astype(TYPE_MAP[var_type])) + causality = Fmi3Causality.output + name = var_names(var_type, causality) + var = getattr(self, name) + if var_type == Boolean: + setattr( + self, name, np.random.choice(a=[False, True], size=SIZE).astype(bool) + ) + elif var_type == Int32 or var_type == Int64 or var_type == UInt64: + setattr( + self, + name, + np.random.randint(0, 100, size=SIZE).astype(TYPE_MAP[var_type]), + ) + else: + setattr(self, name, np.random.rand(SIZE).astype(TYPE_MAP[var_type])) -class ArrayTypes(Fmi3Slave): - def __init__(self, **kwargs): - super().__init__(**kwargs) +class ArrayTypes(Fmi3Slave): + def __init__(self, **kwargs): + super().__init__(**kwargs) - self.author = "Stephen Smith" - self.description = "All types example" + self.author = "Stephen Smith" + self.description = "All types example" - self.time = 0.0 + self.time = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - - create_vars(self) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) - def do_step(self, current_time: float, step_size: float) -> Fmi3Status: - generate_random_data(self) - return True + create_vars(self) - + def do_step(self, current_time: float, step_size: float) -> Fmi3Status: + generate_random_data(self) + return True diff --git a/examples/bouncingBall.py b/examples/bouncingBall.py index d95ed60..ec44975 100644 --- a/examples/bouncingBall.py +++ b/examples/bouncingBall.py @@ -1,7 +1,16 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3Slave, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import ( + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3Slave, + Fmi3Status, + Fmi3StepResult, + Fmi3Variability, + Unit, +) -class BouncingBall(Fmi3Slave): +class BouncingBall(Fmi3Slave): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -23,21 +32,75 @@ def __init__(self, **kwargs): unit3 = Unit(name="m/s2", m=1, s=-2) self.register_units([unit1, unit2, unit3]) + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + self.register_variable( + Float64( + "ball.h", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + unit=unit1.name, + ), + nested=False, + ) + self.register_variable( + Float64( + "ball.derh", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + unit=unit2.name, + ), + nested=False, + ) + self.register_variable( + Float64( + "ball.v", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + unit=unit2.name, + ), + nested=False, + ) + self.register_variable( + Float64( + "ball.derv", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=3, + unit=unit3.name, + ), + nested=False, + ) - self.register_variable(Float64("ball.h", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=unit1.name), - nested=False) - self.register_variable(Float64("ball.derh", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1, unit=unit2.name), - nested=False) - self.register_variable(Float64("ball.v", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=unit2.name), - nested=False) - self.register_variable(Float64("ball.derv", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=3, unit=unit3.name), - nested=False) - - self.register_variable(Float64("g", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed, unit=unit3.name)) - self.register_variable(Float64("e", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable(Float64("v_min", variability=Fmi3Variability.constant, start=0.1)) + self.register_variable( + Float64( + "g", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + unit=unit3.name, + ) + ) + self.register_variable( + Float64( + "e", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable( + Float64("v_min", variability=Fmi3Variability.constant, start=0.1) + ) def do_step(self, current_time, step_size): self.derv = self.g @@ -47,9 +110,9 @@ def do_step(self, current_time, step_size): if self.h <= 0 and self.v < 0: self.h = 1e-12 - self.v = -self.v*self.e + self.v = -self.v * self.e if self.v < self.v_min: self.v = 0 self.g = 0 - + return Fmi3StepResult(status=Fmi3Status.ok) diff --git a/examples/bouncingBall_me.py b/examples/bouncingBall_me.py index e98e7c7..d93df6f 100644 --- a/examples/bouncingBall_me.py +++ b/examples/bouncingBall_me.py @@ -1,13 +1,20 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3SlaveBase, ModelExchange, Fmi3Status, Float64, Fmi3Initial, Unit, Fmi3UpdateDiscreteStatesResult - -from typing import List - import sys +from pythonfmu3 import ( + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3UpdateDiscreteStatesResult, + Fmi3Variability, + ModelExchange, + Unit, +) + EVENT_EPS = 1e-12 -class BouncingBall(Fmi3SlaveBase, ModelExchange): +class BouncingBall(Fmi3SlaveBase, ModelExchange): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -29,47 +36,100 @@ def __init__(self, **kwargs): unit3 = Unit(name="m/s2", m=1, s=-2) self.register_units([unit1, unit2, unit3]) - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - - self.register_variable(Float64("ball.h", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=unit1.name), - nested=False, has_event_indicator=True) - self.register_variable(Float64("ball.derh", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1, unit=unit2.name), - nested=False) - self.register_variable(Float64("ball.v", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=unit2.name), - nested=False) - self.register_variable(Float64("ball.derv", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=3, unit=unit3.name), - nested=False) - - self.register_variable(Float64("g", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed, unit=unit3.name)) - self.register_variable(Float64("e", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable(Float64("v_min", variability=Fmi3Variability.constant, start=0.1)) - - - - def get_continuous_state_derivatives(self) -> List[float]: + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + + self.register_variable( + Float64( + "ball.h", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + unit=unit1.name, + ), + nested=False, + has_event_indicator=True, + ) + self.register_variable( + Float64( + "ball.derh", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + unit=unit2.name, + ), + nested=False, + ) + self.register_variable( + Float64( + "ball.v", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + unit=unit2.name, + ), + nested=False, + ) + self.register_variable( + Float64( + "ball.derv", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=3, + unit=unit3.name, + ), + nested=False, + ) + + self.register_variable( + Float64( + "g", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + unit=unit3.name, + ) + ) + self.register_variable( + Float64( + "e", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable( + Float64("v_min", variability=Fmi3Variability.constant, start=0.1) + ) + + def get_continuous_state_derivatives(self) -> list[float]: self.derh = self.v self.derv = self.g return [self.derh, self.derv] - - def get_event_indicators(self) -> List[float]: + + def get_event_indicators(self) -> list[float]: z = [self.h] - if self.h > -EVENT_EPS and self.h <=0 and self.v > 0: + if self.h > -EVENT_EPS and self.h <= 0 and self.v > 0: z[0] = -EVENT_EPS return z - + def update_discrete_states(self): fdsr = Fmi3UpdateDiscreteStatesResult() if self.h <= 0 and self.v < 0: self.h = sys.float_info.min self.v = -self.v * self.e - + if self.v < self.v_min: - self.v = 0.0; - self.g = 0.0; - + self.v = 0.0 + self.g = 0.0 + fdsr.valuesOfContinuousStatesChanged = True return fdsr diff --git a/examples/dahlquist_me.py b/examples/dahlquist_me.py index 763127a..41a46e6 100644 --- a/examples/dahlquist_me.py +++ b/examples/dahlquist_me.py @@ -1,9 +1,14 @@ -from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import ( + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3Variability, + ModelExchange, +) -from typing import List class Dahlquist(Fmi3SlaveBase, ModelExchange): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -15,13 +20,38 @@ def __init__(self, **kwargs): self.x = 1.0 self.derx = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("k", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - - - def get_continuous_state_derivatives(self) -> List[float]: + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "k", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) + + def get_continuous_state_derivatives(self) -> list[float]: self.derx = -self.k * self.x return [self.derx] - \ No newline at end of file diff --git a/examples/dahlquist_me_cs.py b/examples/dahlquist_me_cs.py index 01d20f8..e13e6df 100644 --- a/examples/dahlquist_me_cs.py +++ b/examples/dahlquist_me_cs.py @@ -1,9 +1,16 @@ -from pythonfmu3 import Fmi3Causality, CoSimulation, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import ( + CoSimulation, + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3StepResult, + Fmi3Variability, + ModelExchange, +) -from typing import List class Dahlquist(Fmi3SlaveBase, ModelExchange, CoSimulation): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -15,17 +22,43 @@ def __init__(self, **kwargs): self.x = 1.0 self.derx = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("k", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "k", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) - def get_continuous_state_derivatives(self) -> List[float]: + def get_continuous_state_derivatives(self) -> list[float]: self.derx = -self.k * self.x return [self.derx] - + def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult: self.derx = -self.k * self.x self.x += self.derx * step_size - return True \ No newline at end of file + return True diff --git a/examples/demoslave.py b/examples/demoslave.py index 7744801..941629b 100644 --- a/examples/demoslave.py +++ b/examples/demoslave.py @@ -1,9 +1,7 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3Slave, Float64, String +from pythonfmu3 import Float64, Fmi3Causality, Fmi3Slave, Fmi3Variability, String class Resistor(Fmi3Slave): - - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -12,27 +10,45 @@ def __init__(self, **kwargs): self.time = 0.0 - self.positive_pin_v = 20. + self.positive_pin_v = 20.0 self.positive_pin_i = 0.001 - self.negative_pin_v = 10. + self.negative_pin_v = 10.0 self.negative_pin_i = 0.001 - self.delta_v = 10. + self.delta_v = 10.0 self.i = 0.001 - self.R = 10000. + self.R = 10000.0 self.stringVariable = "mine" - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("R", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "R", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) self.register_variable(Float64("positive_pin_v", causality=Fmi3Causality.input)) - self.register_variable(Float64("positive_pin_i", causality=Fmi3Causality.output)) + self.register_variable( + Float64("positive_pin_i", causality=Fmi3Causality.output) + ) self.register_variable(Float64("negative_pin_v", causality=Fmi3Causality.input)) - self.register_variable(Float64("negative_pin_i", causality=Fmi3Causality.output)) + self.register_variable( + Float64("negative_pin_i", causality=Fmi3Causality.output) + ) self.register_variable(Float64("delta_v", causality=Fmi3Causality.local)) self.register_variable(Float64("i", causality=Fmi3Causality.local)) - self.register_variable(String("stringVariable", causality=Fmi3Causality.parameter)) + self.register_variable( + String("stringVariable", causality=Fmi3Causality.parameter) + ) def do_step(self, current_time, step_size): self.delta_v = self.positive_pin_v - self.negative_pin_v diff --git a/examples/linearTransform.py b/examples/linearTransform.py index bd1b837..7cb744b 100644 --- a/examples/linearTransform.py +++ b/examples/linearTransform.py @@ -1,9 +1,9 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3Slave, Float64, UInt64, Fmi3Initial, Dimension import numpy as np +from pythonfmu3 import Dimension, Float64, Fmi3Causality, Fmi3Slave, Fmi3Variability -class LinearTransformFixed(Fmi3Slave): +class LinearTransformFixed(Fmi3Slave): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -22,15 +22,42 @@ def __init__(self, **kwargs): self.A = np.array([1.0, 1.0, 2.0, 1.0]).reshape((self.m, self.n)) self.y = np.array([0.0, 0.0]).reshape((self.n, 1)) - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("scalar", causality=Fmi3Causality.input, start=2.0)) - self.register_variable(Float64("u", causality=Fmi3Causality.input, dimensions=[Dimension(start="2")])) - self.register_variable(Float64("offset", causality=Fmi3Causality.input, dimensions=[Dimension(start="2")])) - self.register_variable(Float64("A", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable, dimensions=[Dimension(start="2"), Dimension(start="2")])) - self.register_variable(Float64("y", causality=Fmi3Causality.output, dimensions=[Dimension(start="2")])) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64("scalar", causality=Fmi3Causality.input, start=2.0) + ) + self.register_variable( + Float64( + "u", causality=Fmi3Causality.input, dimensions=[Dimension(start="2")] + ) + ) + self.register_variable( + Float64( + "offset", + causality=Fmi3Causality.input, + dimensions=[Dimension(start="2")], + ) + ) + self.register_variable( + Float64( + "A", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + dimensions=[Dimension(start="2"), Dimension(start="2")], + ) + ) + self.register_variable( + Float64( + "y", causality=Fmi3Causality.output, dimensions=[Dimension(start="2")] + ) + ) def do_step(self, current_time, step_size): - self.y = self.scalar*self.A.dot(self.u) + self.offset + self.y = self.scalar * self.A.dot(self.u) + self.offset return True diff --git a/examples/mldemo.py b/examples/mldemo.py index 7cdc2f9..3f7345b 100644 --- a/examples/mldemo.py +++ b/examples/mldemo.py @@ -1,16 +1,17 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64 - import pathlib # pathlib is included in the Python Standard Library +from pythonfmu3 import Float64, Fmi3Causality, Fmi3Slave + try: - import tensorflow as tf import numpy as np -except ImportError: # Trick to be able to generate the FMU without TensorFlow and NumPy installed + import tensorflow as tf +except ( + ImportError +): # Trick to be able to generate the FMU without TensorFlow and NumPy installed np, tf = None, None class MLDemo(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -28,22 +29,32 @@ def __init__(self, **kwargs): # Fetch saved model from directory included in the FMU self.model = tf.keras.models.load_model(model_dir_path) except AttributeError: - print("Unable to load model from directory. Has TensorFlow been included in the environment?") + print( + "Unable to load model from directory. Has TensorFlow been included in the environment?" + ) except OSError: - print("Unable to load model from directory. Has the correct directory been specified?") + print( + "Unable to load model from directory. Has the correct directory been specified?" + ) else: - print("No model directory found. Has the directory been included in the FMU when building?") + print( + "No model directory found. Has the directory been included in the FMU when building?" + ) - self.sin_input = 0. - self.sin_output_tf = 0. - self.sin_output_ref = 0. + self.sin_input = 0.0 + self.sin_output_tf = 0.0 + self.sin_output_ref = 0.0 self.register_variable(Float64("sin_input", causality=Fmi3Causality.input)) self.register_variable(Float64("sin_output_tf", causality=Fmi3Causality.output)) - self.register_variable(Float64("sin_output_ref", causality=Fmi3Causality.output)) + self.register_variable( + Float64("sin_output_ref", causality=Fmi3Causality.output) + ) def do_step(self, current_time, step_size): # Similar to model.predict() with less performance overhead - self.sin_output_tf = self.model(np.array([self.sin_input]), training=False).numpy()[0][0] + self.sin_output_tf = self.model( + np.array([self.sin_input]), training=False + ).numpy()[0][0] self.sin_output_ref = np.sin([self.sin_input])[0] return True diff --git a/examples/robertson_me.py b/examples/robertson_me.py index 5f1766d..f9a612c 100644 --- a/examples/robertson_me.py +++ b/examples/robertson_me.py @@ -1,9 +1,14 @@ -from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import ( + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3Variability, + ModelExchange, +) -from typing import List class Robertson(Fmi3SlaveBase, ModelExchange): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -18,17 +23,67 @@ def __init__(self, **kwargs): self.dery2 = 0.04 self.dery3 = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("y1", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("y2", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("y3", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("dery1", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("dery2", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=2)) - self.register_variable(Float64("dery3", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=3)) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "y1", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "y2", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "y3", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "dery1", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "dery2", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=2, + ) + ) + self.register_variable( + Float64( + "dery3", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=3, + ) + ) - def get_continuous_state_derivatives(self) -> List[float]: + def get_continuous_state_derivatives(self) -> list[float]: self.dery1 = -0.04 * self.y1 + 1e4 * self.y2 * self.y3 self.dery2 = 0.04 * self.y1 - 1e4 * self.y2 * self.y3 - 3e7 * self.y2**2 self.dery3 = 3e7 * self.y2**2 - return [self.dery1, self.dery2, self.dery3] \ No newline at end of file + return [self.dery1, self.dery2, self.dery3] diff --git a/examples/sympyslave.py b/examples/sympyslave.py index 3b4b3e1..a5d1ffb 100644 --- a/examples/sympyslave.py +++ b/examples/sympyslave.py @@ -1,13 +1,14 @@ -from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Fmi3Variability, Float64 +from pythonfmu3.fmi3slave import Float64, Fmi3Causality, Fmi3Slave, Fmi3Variability + try: - from sympy import symbols, exp + from sympy import exp, symbols except ImportError: # Trick to be able to generate the FMU without sympy installed symbols, exp = None, None class SympySlave(Fmi3Slave): """This class is an example to demonstrate installing new Python dependencies. - + The code is not efficient. """ @@ -16,15 +17,29 @@ def __init__(self, **kwargs): self.realIn = 22.0 self.realOut = 0.0 - self.tau = 2. - self.a = 5. + self.tau = 2.0 + self.a = 5.0 self.register_variable(Float64("realIn", causality=Fmi3Causality.input)) - self.register_variable(Float64("a", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable(Float64("tau", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) + self.register_variable( + Float64( + "a", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable( + Float64( + "tau", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) self.register_variable(Float64("realOut", causality=Fmi3Causality.output)) def do_step(self, current_time, step_size): i, a, t, tau = symbols("i, a, t, tau") expr = i * a * (1 - exp(-1 * t / tau)) - self.realOut = expr.evalf(subs={i: self.realIn, a: self.a, t: current_time + step_size, tau: self.tau}) + self.realOut = expr.evalf( + subs={i: self.realIn, a: self.a, t: current_time + step_size, tau: self.tau} + ) return True diff --git a/examples/tensorflow-model-export/sin-approximation.ipynb b/examples/tensorflow-model-export/sin-approximation.ipynb index 3dedcf4..fe7e08b 100644 --- a/examples/tensorflow-model-export/sin-approximation.ipynb +++ b/examples/tensorflow-model-export/sin-approximation.ipynb @@ -8,51 +8,63 @@ }, "outputs": [], "source": [ + "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import tensorflow as tf\n", - "import matplotlib.pyplot as plt" + "import tensorflow as tf" ] }, { "cell_type": "code", "execution_count": 4, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Parameters\n", "BATCH_SIZE = 32\n", "SAMPLES_PER_BATCH = 1000" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 5, "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 5, + }, "outputs": [], "source": [ "# Generate training and output data\n", - "x_train = tf.random.uniform(shape=[BATCH_SIZE*SAMPLES_PER_BATCH,], minval=-2.0 * np.pi, maxval=2.0 * np.pi)\n", + "x_train = tf.random.uniform(\n", + " shape=[\n", + " BATCH_SIZE * SAMPLES_PER_BATCH,\n", + " ],\n", + " minval=-2.0 * np.pi,\n", + " maxval=2.0 * np.pi,\n", + ")\n", "y_train = tf.math.sin(x_train)" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 6, "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 6, + }, "outputs": [ { "data": { - "text/plain": "
", - "image/png": "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\n" + "image/png": "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\n", + "text/plain": "
" }, "metadata": { "needs_background": "light" @@ -62,54 +74,55 @@ ], "source": [ "# Create visualization of the training data\n", - "plt.plot(x_train, y_train, 'bo')\n", + "plt.plot(x_train, y_train, \"bo\")\n", "plt.show()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "code", "execution_count": 7, - "outputs": [], - "source": [ - "# Create model\n", - "model = tf.keras.models.Sequential([\n", - " tf.keras.layers.Dense(16, activation='relu', input_shape=(1,)),\n", - " tf.keras.layers.Dense(16, activation='relu'),\n", - " tf.keras.layers.Dense(1)\n", - "])" - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [], + "source": [ + "# Create model\n", + "model = tf.keras.models.Sequential(\n", + " [\n", + " tf.keras.layers.Dense(16, activation=\"relu\", input_shape=(1,)),\n", + " tf.keras.layers.Dense(16, activation=\"relu\"),\n", + " tf.keras.layers.Dense(1),\n", + " ]\n", + ")" + ] }, { "cell_type": "code", "execution_count": 8, - "outputs": [], - "source": [ - "# Compile model\n", - "model.compile(optimizer='adam',\n", - " loss='mse')" - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [], + "source": [ + "# Compile model\n", + "model.compile(optimizer=\"adam\", loss=\"mse\")" + ] }, { "cell_type": "code", "execution_count": 9, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "name": "stdout", @@ -169,17 +182,17 @@ "source": [ "# Train the model\n", "model.fit(x_train, y_train, batch_size=BATCH_SIZE, epochs=20)" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 10, "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 10, + }, "outputs": [ { "name": "stdout", @@ -190,8 +203,8 @@ }, { "data": { - "text/plain": "
", - "image/png": "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\n" + "image/png": "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\n", + "text/plain": "
" }, "metadata": { "needs_background": "light" @@ -205,20 +218,20 @@ "test_outputs = model.predict(test_inputs)\n", "reference = tf.math.sin(test_inputs)\n", "\n", - "plt.plot(test_inputs, test_outputs, 'bo')\n", - "plt.plot(test_inputs, reference, 'ro')\n", + "plt.plot(test_inputs, test_outputs, \"bo\")\n", + "plt.plot(test_inputs, reference, \"ro\")\n", "plt.show()" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 11, "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 11, + }, "outputs": [ { "name": "stderr", @@ -245,32 +258,32 @@ "source": [ "# Export model\n", "model.save(\"stored-model\")" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 12, "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 12, + }, "outputs": [], "source": [ "# Load in the saved model\n", "loaded_model = tf.keras.models.load_model(\"stored-model\")" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 13, "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 13, + }, "outputs": [ { "name": "stdout", @@ -295,13 +308,7 @@ "print(model.predict(np.array([loaded_test_input]), verbose=0)[0][0])\n", "print(\"Reference:\")\n", "print(np.sin([loaded_test_input])[0])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] } ], "metadata": { diff --git a/examples/vanderpol_me.py b/examples/vanderpol_me.py index 1aa52f6..329345d 100644 --- a/examples/vanderpol_me.py +++ b/examples/vanderpol_me.py @@ -1,9 +1,14 @@ -from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import ( + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3Variability, + ModelExchange, +) -from typing import List class VanDerPol(Fmi3SlaveBase, ModelExchange): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -17,18 +22,59 @@ def __init__(self, **kwargs): self.derx0 = 0.0 self.derx1 = 0.0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x0", causality=Fmi3Causality.output, start=2, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("x1", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx0", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("derx1", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=2)) - self.register_variable(Float64("mu", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x0", + causality=Fmi3Causality.output, + start=2, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "x1", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx0", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "derx1", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=2, + ) + ) + self.register_variable( + Float64( + "mu", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) - def get_continuous_state_derivatives(self) -> List[float]: + def get_continuous_state_derivatives(self) -> list[float]: self.derx0 = self.x1 self.derx1 = self.mu * ((1 - self.x0**2) * self.x1) - self.x0 return [self.derx0, self.derx1] - - def get_nominals_of_continuous_states(self, nStates: int) -> List[float]: - return [1.0, 1.0] \ No newline at end of file + + def get_nominals_of_continuous_states(self, nStates: int) -> list[float]: + return [1.0, 1.0] diff --git a/pythonfmu3/default_experiment.py b/pythonfmu3/default_experiment.py deleted file mode 100644 index c0aabb2..0000000 --- a/pythonfmu3/default_experiment.py +++ /dev/null @@ -1,7 +0,0 @@ -class DefaultExperiment: - - def __init__(self, start_time: float = None, stop_time: float = None, step_size: float = None, tolerance: float = None): - self.start_time = start_time - self.stop_time = stop_time - self.step_size = step_size - self.tolerance = tolerance diff --git a/pythonfmu3/tests/conftest.py b/pythonfmu3/tests/conftest.py deleted file mode 100644 index 212955b..0000000 --- a/pythonfmu3/tests/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - -def pytest_configure(config): - config.addinivalue_line( - "markers", "requirements(pkg): mark test as requiring the specified package" - ) \ No newline at end of file diff --git a/pythonfmu3/tests/slaves/pythonslave.py b/pythonfmu3/tests/slaves/pythonslave.py deleted file mode 100644 index 3696792..0000000 --- a/pythonfmu3/tests/slaves/pythonslave.py +++ /dev/null @@ -1,52 +0,0 @@ -from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Fmi3Variability, Int32, Float64, Boolean, String - - -class Container: - pass - - -class PythonSlave(Fmi3Slave): - - author = "John Doe" - description = "A simple description" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.intParam = 42 - self.intOut = 23 - self.realOut = 3.0 - self.booleanVariable = True - self.stringVariable = "Hello World!" - self.realIn = 2. / 3. - self.booleanParameter = False - self.stringParameter = "dog" - self.time = 0 - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable( - Int32("intParam", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable(Float64("realIn", causality=Fmi3Causality.input)) - self.register_variable( - Boolean("booleanParameter", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable( - String("stringParameter", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - - self.register_variable(Int32("intOut", causality=Fmi3Causality.output)) - self.register_variable(Float64("realOut", causality=Fmi3Causality.output)) - self.register_variable(Boolean("booleanVariable", causality=Fmi3Causality.local)) - self.register_variable(String("stringVariable", causality=Fmi3Causality.local)) - - self.container = Container() - self.container.someReal = 99.0 - self.container.subContainer = sub = Container() - sub.someInteger = -15 - self.register_variable( - Float64("container.someReal", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) - self.register_variable( - Int32("container.subContainer.someInteger", causality=Fmi3Causality.parameter, - variability=Fmi3Variability.tunable)) - - def do_step(self, current_time, step_size): - self.realOut = current_time + step_size - return True diff --git a/pythonfmu3/tests/slaves/pythonslaveMX.py b/pythonfmu3/tests/slaves/pythonslaveMX.py deleted file mode 100644 index 4d22672..0000000 --- a/pythonfmu3/tests/slaves/pythonslaveMX.py +++ /dev/null @@ -1,27 +0,0 @@ -from pythonfmu3 import Fmi3Causality, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult - -from typing import List - -class Dahlquist(Fmi3SlaveBase, ModelExchange): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.author = "Stephen Smith" - self.description = "Dahlquist's test problem for model exchange FMUs" - - self.time = 0.0 - self.k = 1.0 - self.x = 1.0 - self.derx = 0.0 - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("k", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - - - def get_continuous_state_derivatives(self) -> List[float]: - self.derx = -self.k * self.x - return [self.derx] - \ No newline at end of file diff --git a/pythonfmu3/tests/slaves/pythonslaveMXCS.py b/pythonfmu3/tests/slaves/pythonslaveMXCS.py deleted file mode 100644 index 27ca784..0000000 --- a/pythonfmu3/tests/slaves/pythonslaveMXCS.py +++ /dev/null @@ -1,32 +0,0 @@ -from pythonfmu3 import Fmi3Causality, CoSimulation, ModelExchange, Fmi3Variability, Fmi3SlaveBase, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult - -from typing import List - -class Dahlquist(Fmi3SlaveBase, ModelExchange, CoSimulation): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.author = "Stephen Smith" - self.description = "Dahlquist's test problem for model exchange FMUs" - - self.time = 0.0 - self.k = 1.0 - self.x = 1.0 - self.derx = 0.0 - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable(Float64("x", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact)) - self.register_variable(Float64("derx", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1)) - self.register_variable(Float64("k", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed)) - - - def get_continuous_state_derivatives(self) -> List[float]: - self.derx = -self.k * self.x - return [self.derx] - - def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult: - self.time = current_time + step_size - self.derx = -self.k * self.x - self.x += self.derx * step_size - return True \ No newline at end of file diff --git a/pythonfmu3/tests/slaves/pythonslave_arraytypes.py b/pythonfmu3/tests/slaves/pythonslave_arraytypes.py deleted file mode 100644 index c1a67d5..0000000 --- a/pythonfmu3/tests/slaves/pythonslave_arraytypes.py +++ /dev/null @@ -1,80 +0,0 @@ - -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Dimension, Fmi3Slave, Fmi3Status, Float64, Int32, Int64, UInt64, String, Boolean - -import numpy as np - -SIZE = 10 - -TYPES = [UInt64, Float64, Boolean, Int32, Int64] -CAUSALITY = [Fmi3Causality.output, Fmi3Causality.input] - -TYPE_MAP = { - UInt64: np.uint64, - Float64: np.float64, - Int32: np.int32, - Int64: np.int64, - Boolean: bool -} - -def var_names(var_type, causality): - return f"{var_type.__name__.lower()}_{causality.name.lower()}" - - -def init_var(var_type, causality, array=True): - if array: - return np.zeros(SIZE, dtype=TYPE_MAP[var_type]) - else: - return TYPE_MAP[var_type]() - - -def create_vars(self): - dimensions = [Dimension(start=str(SIZE))] - for var_type in TYPES: - for causality in CAUSALITY: - name = var_names(var_type, causality) - if var_type == Float64: - var = var_type(name, causality=causality, variability=Fmi3Variability.continuous, dimensions=dimensions) - else: - var = var_type(name, causality=causality, variability=Fmi3Variability.discrete, dimensions=dimensions) - setattr(self, name, init_var(var_type, causality)) - self.register_variable(var) - - -def generate_data(self): - for var_type in TYPES: - causality = Fmi3Causality.output - name = var_names(var_type, causality) - var = getattr(self, name) - if var_type == Boolean: - data = [False, True, False, True, True, False, False, True, False, True] - setattr(self, name, np.array(data).astype(bool)) - elif var_type == Int32 or var_type == Int64 or var_type == UInt64: - data = [1,2,3,4,5,6,7,8,9,10] - setattr(self, name, np.array(data).astype(TYPE_MAP[var_type])) - else: - data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] - setattr(self, name, np.array(data).astype(TYPE_MAP[var_type])) - -class ArrayTypes(Fmi3Slave): - - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.author = "Tester" - self.description = "FMU with all basic types as arrays" - - self.time = 0.0 - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - - create_vars(self) - - generate_data(self) - - - def do_step(self, current_time: float, step_size: float) -> Fmi3Status: - generate_data(self) - return True - - diff --git a/pythonfmu3/tests/slaves/pythonslave_read_file.py b/pythonfmu3/tests/slaves/pythonslave_read_file.py deleted file mode 100644 index f5afd82..0000000 --- a/pythonfmu3/tests/slaves/pythonslave_read_file.py +++ /dev/null @@ -1,23 +0,0 @@ -from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Fmi3Variability, String, DefaultExperiment, Float64 - - -class PythonSlaveReadFile(Fmi3Slave): - - default_experiment = DefaultExperiment(start_time=0) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - with (open(f'{self.resources}/hello.txt', 'r')) as file: - data = file.read() - - self.time = 0 - - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) - self.register_variable( - String("file_content", getter=lambda: data, - causality=Fmi3Causality.output, - variability=Fmi3Variability.constant)) - - def do_step(self, current_time, step_size): - return True diff --git a/pythonfmu3/tests/utils.py b/pythonfmu3/tests/utils.py deleted file mode 100644 index 218a0c9..0000000 --- a/pythonfmu3/tests/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from pythonfmu3.variables import Boolean, Int32, UInt64, Float64, ModelVariable, String - -class UInt64ValType(int): pass - - -FMI2PY = dict(( - (Boolean, bool), - (Int32, int), - (Float64, float), - (String, str), - (UInt64, UInt64ValType), -)) -PY2FMI = dict([(v, k) for k, v in FMI2PY.items()]) diff --git a/pythonfmu3/__init__.py b/src/pythonfmu3/__init__.py similarity index 55% rename from pythonfmu3/__init__.py rename to src/pythonfmu3/__init__.py index d5e7048..265c23b 100644 --- a/pythonfmu3/__init__.py +++ b/src/pythonfmu3/__init__.py @@ -1,10 +1,19 @@ from ._version import __version__ from .builder import FmuBuilder from .cosimulation import CoSimulation -from .modelexchange import ModelExchange, Fmi3UpdateDiscreteStatesResult +from .default_experiment import DefaultExperiment from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability from .fmi3slave import Fmi3Slave, Fmi3SlaveBase, Fmi3StepResult -from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, String, Dimension -from .default_experiment import DefaultExperiment -from .variable_types import Float64Type, EnumerationType +from .modelexchange import Fmi3UpdateDiscreteStatesResult, ModelExchange from .unit import BaseUnit, Unit +from .variable_types import EnumerationType, Float64Type +from .variables import ( + Boolean, + Dimension, + Enumeration, + Float64, + Int32, + Int64, + String, + UInt64, +) diff --git a/pythonfmu3/__main__.py b/src/pythonfmu3/__main__.py similarity index 87% rename from pythonfmu3/__main__.py rename to src/pythonfmu3/__main__.py index da58e8d..e7289d7 100644 --- a/pythonfmu3/__main__.py +++ b/src/pythonfmu3/__main__.py @@ -1,6 +1,7 @@ import argparse from pythonfmu3 import builder, deploy + from ._version import __version__ @@ -16,13 +17,14 @@ def default_execution(**kwargs): parser.set_defaults(execute=default_execution) subparsers = parser.add_subparsers( - title="Subcommands", description="Call `pythonfmu _command_ -h` to get more help." + title="Subcommands", + description="Call `pythonfmu _command_ -h` to get more help.", ) build_parser = subparsers.add_parser( "build", description="Build an FMU from a Python script.", - help="Build an FMU from a Python script." + help="Build an FMU from a Python script.", ) builder.create_command_parser(build_parser) @@ -39,7 +41,7 @@ def default_execution(**kwargs): The tool assume the Python environment in which the FMU should be executed is the current one. """, - help="Install Python FMU dependencies." + help="Install Python FMU dependencies.", ) deploy.create_command_parser(deploy_parser) diff --git a/pythonfmu3/_version.py b/src/pythonfmu3/_version.py similarity index 100% rename from pythonfmu3/_version.py rename to src/pythonfmu3/_version.py diff --git a/pythonfmu3/builder.py b/src/pythonfmu3/builder.py similarity index 88% rename from pythonfmu3/builder.py rename to src/pythonfmu3/builder.py index 58be085..782c17f 100644 --- a/pythonfmu3/builder.py +++ b/src/pythonfmu3/builder.py @@ -1,4 +1,5 @@ """Python FMU builder""" + import argparse import importlib import itertools @@ -8,12 +9,19 @@ import sys import tempfile import zipfile +from collections.abc import Iterable from pathlib import Path -from typing import Iterable, Optional, Tuple, Union +from typing import Union from xml.dom.minidom import parseString -from xml.etree.ElementTree import Element, SubElement, tostring +from xml.etree.ElementTree import Element, tostring + +from .fmi3slave import ( + FMI3_MODEL_OPTIONS_COMMON, + FMI3_MODEL_OPTIONS_COSIM, + FMI3_MODEL_OPTIONS_MX, + Fmi3SlaveBase, +) from .osutil import get_lib_extension, get_platform -from .fmi3slave import FMI3_MODEL_OPTIONS_COMMON, FMI3_MODEL_OPTIONS_COSIM, FMI3_MODEL_OPTIONS_MX, Fmi3Slave, Fmi3SlaveBase FilePath = Union[str, Path] HERE = Path(__file__).parent @@ -22,12 +30,14 @@ def get_class_name(file_name: Path) -> str: - with open(str(file_name), 'r') as file: + with open(str(file_name), "r") as file: data = file.read() - return re.search(r'class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:', data).group(1) + return re.search( + r"class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:", data + ).group(1) -def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Element]: +def get_model_description(filepath: Path, module_name: str) -> tuple[str, Element]: """Extract the FMU model description as XML. Args: @@ -46,7 +56,9 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen spec.loader.exec_module(fmu_interface) # Instantiate the interface class_name = get_class_name(filepath) - instance = getattr(fmu_interface, class_name)(instance_name="dummyInstance", resources=str(filepath.parent)) + instance = getattr(fmu_interface, class_name)( + instance_name="dummyInstance", resources=str(filepath.parent) + ) finally: sys.path.remove(str(filepath.parent)) # remove inserted temporary path @@ -59,14 +71,13 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen class FmuBuilder: - @staticmethod def build_FMU( script_file: FilePath, dest: FilePath = ".", project_files: Iterable[FilePath] = set(), - documentation_folder: Optional[FilePath] = None, - terminals : Optional[FilePath] = None, + documentation_folder: FilePath | None = None, + terminals: FilePath | None = None, **options, ) -> Path: script_file = Path(script_file) @@ -86,7 +97,7 @@ def build_FMU( raise ValueError( f"The documentation folder does not exists {documentation_folder!s}" ) - + if terminals is not None: terminals = Path(terminals) if not terminals.exists(): @@ -107,7 +118,9 @@ def build_FMU( # Embed pythonfmu in the FMU so it does not need to be included dep_folder = temp_dir / "pythonfmu3" dep_folder.mkdir() - for dep in HERE.glob('*.py'): # Find all python files at the same level as this one + for dep in HERE.glob( + "*.py" + ): # Find all python files at the same level as this one shutil.copy2(dep, dep_folder) for file_ in project_files: if file_ == script_file.parent: @@ -154,7 +167,6 @@ def build_FMU( type_node.set(option, str(value).lower()) with zipfile.ZipFile(dest_file, "w") as zip_fmu: - resource = Path("resources") # Add files copied in temporary directory @@ -174,7 +186,7 @@ def build_FMU( ): relative_f = f.relative_to(src) if "build" in relative_f.parts: - continue; + continue zip_fmu.write(f, arcname=(sources / relative_f)) # Add FMI API wrapping Python class library @@ -205,7 +217,6 @@ def build_FMU( terminalsFolder = Path("terminalsAndIcons") relative_f = terminals.relative_to(terminals.parent) zip_fmu.write(f, arcname=(terminalsFolder / relative_f)) - # Add the model description xml_str = parseString(tostring(xml, "UTF-8")) @@ -219,9 +230,13 @@ def build_FMU( def has_binary() -> bool: """Does the binary for this platform exits?""" binary_folder = get_platform() - lib_ext = get_lib_extension() or "*" # if library extension is unknown, it will look for '*.*' in src_binaries + lib_ext = ( + get_lib_extension() or "*" + ) # if library extension is unknown, it will look for '*.*' in src_binaries src_binaries = HERE / "resources" / "binaries" / binary_folder - return src_binaries.exists() and len(list(src_binaries.glob(f"*.{lib_ext}"))) >= 1 + return ( + src_binaries.exists() and len(list(src_binaries.glob(f"*.{lib_ext}"))) >= 1 + ) def create_command_parser(parser: argparse.ArgumentParser): @@ -230,7 +245,7 @@ def create_command_parser(parser: argparse.ArgumentParser): "--file", dest="script_file", help="Path to the Python script.", - required=True + required=True, ) parser.add_argument( @@ -241,14 +256,14 @@ def create_command_parser(parser: argparse.ArgumentParser): "--doc", dest="documentation_folder", help="Documentation folder to include in the FMU.", - default=None + default=None, ) parser.add_argument( "--terminals", dest="terminals", help="Terminals file (terminalsAndIcons.xml) to include in the FMU.", - default=None + default=None, ) for option in FMI3_MODEL_OPTIONS_COMMON: @@ -257,7 +272,7 @@ def create_command_parser(parser: argparse.ArgumentParser): f"--{option.cli}", dest=option.name, help=f"If given, {option.name}={action[6:]}", - action=action + action=action, ) for option in FMI3_MODEL_OPTIONS_COSIM: @@ -266,16 +281,16 @@ def create_command_parser(parser: argparse.ArgumentParser): f"--{option.cli}", dest=option.name, help=f"If given, {option.name}={action[6:]}", - action=action + action=action, ) - + for option in FMI3_MODEL_OPTIONS_MX: action = "store_false" if option.value else "store_true" parser.add_argument( f"--{option.cli}", dest=option.name, help=f"If given, {option.name}={action[6:]}", - action=action + action=action, ) parser.add_argument( @@ -283,7 +298,7 @@ def create_command_parser(parser: argparse.ArgumentParser): metavar="Project files", nargs="*", help="Additional project files required by the Python script.", - default=set() + default=set(), ) parser.set_defaults(execute=FmuBuilder.build_FMU) diff --git a/pythonfmu3/cosimulation.py b/src/pythonfmu3/cosimulation.py similarity index 92% rename from pythonfmu3/cosimulation.py rename to src/pythonfmu3/cosimulation.py index 84665e1..f496a30 100644 --- a/pythonfmu3/cosimulation.py +++ b/src/pythonfmu3/cosimulation.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod + # co-simulation mixin class CoSimulation(ABC): - @abstractmethod def do_step(self, current_time: float, step_size: float): - pass \ No newline at end of file + pass diff --git a/src/pythonfmu3/default_experiment.py b/src/pythonfmu3/default_experiment.py new file mode 100644 index 0000000..3b6ed4f --- /dev/null +++ b/src/pythonfmu3/default_experiment.py @@ -0,0 +1,12 @@ +class DefaultExperiment: + def __init__( + self, + start_time: float = None, + stop_time: float = None, + step_size: float = None, + tolerance: float = None, + ): + self.start_time = start_time + self.stop_time = stop_time + self.step_size = step_size + self.tolerance = tolerance diff --git a/pythonfmu3/deploy.py b/src/pythonfmu3/deploy.py similarity index 75% rename from pythonfmu3/deploy.py rename to src/pythonfmu3/deploy.py index 830926a..8dc793e 100644 --- a/pythonfmu3/deploy.py +++ b/src/pythonfmu3/deploy.py @@ -1,4 +1,5 @@ """CLI command to deploy a FMU.""" + import argparse import os import subprocess @@ -6,26 +7,24 @@ import zipfile from pathlib import Path from tempfile import TemporaryDirectory -from typing import Union from .enums import PackageManager - # Dictionary of ("default file name", "associated package manager") ENVIRONMENT_FILES = { "requirements.txt": PackageManager.pip, "environment.yaml": PackageManager.conda, - "environment.yml": PackageManager.conda + "environment.yml": PackageManager.conda, } def deploy( - fmu: Union[str, Path], - environment: Union[str, Path, None] = None, - package_manager: Union[str, PackageManager, None] = None + fmu: str | Path, + environment: str | Path | None = None, + package_manager: str | PackageManager | None = None, ) -> None: """Install Python dependency packages from requirement file shipped within the FMU. - + Args: fmu (str or pathlib.Path) : FMU file path environment (str or pathlib.Path) : optional, requirements file within the `resources` folder of the FMU @@ -50,11 +49,15 @@ def deploy( manager = manager or ENVIRONMENT_FILES[spec] break if environment_file is None: - raise ValueError("Unable to find requirement file in the FMU resources folder.") + raise ValueError( + "Unable to find requirement file in the FMU resources folder." + ) else: environment_file = Path("resources") / environment if environment_file.as_posix() not in names: - raise ValueError(f"Unable to find requirement file {environment!s} in the FMU resources folder.") + raise ValueError( + f"Unable to find requirement file {environment!s} in the FMU resources folder." + ) if manager is None: if environment in ENVIRONMENT_FILES: @@ -75,10 +78,19 @@ def deploy( if manager == PackageManager.pip: subprocess.run( - [sys.executable, "-m", "pip", "install", "-r", f"{copy_env!s}", "--progress-bar", "off"], + [ + sys.executable, + "-m", + "pip", + "install", + "-r", + f"{copy_env!s}", + "--progress-bar", + "off", + ], stdout=sys.stdout, stderr=sys.stderr, - check=True + check=True, ) elif manager == PackageManager.conda: conda_exe = os.environ.get("CONDA_EXE", "conda") @@ -86,18 +98,14 @@ def deploy( [conda_exe, "env", "update", f"--file={copy_env!s}", "--quiet"], stdout=sys.stdout, stderr=sys.stderr, - check=True + check=True, ) def create_command_parser(parser: argparse.ArgumentParser): parser.add_argument( - "-f", - "--file", - dest="fmu", - help="Path to the Python FMU.", - required=True + "-f", "--file", dest="fmu", help="Path to the Python FMU.", required=True ) parser.add_argument( @@ -105,14 +113,14 @@ def create_command_parser(parser: argparse.ArgumentParser): "--env", dest="environment", help="Requirements or environment file.", - default=None + default=None, ) parser.add_argument( choices=["pip", "conda"], dest="package_manager", - nargs='?', - help="Python packages manager" + nargs="?", + help="Python packages manager", ) parser.set_defaults(execute=deploy) diff --git a/pythonfmu3/enums.py b/src/pythonfmu3/enums.py similarity index 99% rename from pythonfmu3/enums.py rename to src/pythonfmu3/enums.py index 1311aec..cd178a8 100644 --- a/pythonfmu3/enums.py +++ b/src/pythonfmu3/enums.py @@ -1,4 +1,5 @@ """FMI enumerations""" + from enum import Enum, IntEnum @@ -44,5 +45,6 @@ class Fmi3Status(IntEnum): class PackageManager(Enum): """Enumeration of Python packages manager.""" + pip = "pip" conda = "conda" diff --git a/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py similarity index 74% rename from pythonfmu3/fmi3slave.py rename to src/pythonfmu3/fmi3slave.py index 2a95dc1..8ef5350 100644 --- a/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -1,38 +1,46 @@ """Define the abstract facade class.""" -import json + import ctypes import datetime -from abc import ABC, abstractmethod +import json from collections import OrderedDict, namedtuple -from pathlib import Path -from typing import Any, ClassVar, Dict, List, NamedTuple, Optional +from typing import Any, NamedTuple from uuid import uuid1 from xml.etree.ElementTree import Element, SubElement -from .logmsg import LogMsg -from .default_experiment import DefaultExperiment +from ._version import __version__ as VERSION from .cosimulation import CoSimulation +from .default_experiment import DefaultExperiment +from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability +from .logmsg import LogMsg from .modelexchange import ModelExchange -from ._version import __version__ as VERSION -from .enums import Fmi3Type, Fmi3Status, Fmi3Causality, Fmi3Initial, Fmi3Variability -from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, ModelVariable, String -from .variable_types import VariableType from .unit import Unit +from .variable_types import VariableType +from .variables import ( + Boolean, + Enumeration, + Float64, + Int32, + Int64, + ModelVariable, + String, + UInt64, +) ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"]) -FMI3_MODEL_OPTIONS_COMMON: List[ModelOptions] = [ +FMI3_MODEL_OPTIONS_COMMON: list[ModelOptions] = [ ModelOptions("needsExecutionTool", True, "no-external-tool"), ModelOptions("canBeInstantiatedOnlyOncePerProcess", False, "only-one-per-process"), ModelOptions("canGetAndSetFMUState", False, "handle-state"), - ModelOptions("canSerializeFMUState", False, "serialize-state") + ModelOptions("canSerializeFMUState", False, "serialize-state"), ] -FMI3_MODEL_OPTIONS_COSIM: List[ModelOptions] = [ +FMI3_MODEL_OPTIONS_COSIM: list[ModelOptions] = [ ModelOptions("canHandleVariableCommunicationStepSize", True, "no-variable-step"), ] -FMI3_MODEL_OPTIONS_MX: List[ModelOptions] = [ +FMI3_MODEL_OPTIONS_MX: list[ModelOptions] = [ ModelOptions("needsCompletedIntegratorStep", False, "needs-completed-step"), ] @@ -44,49 +52,49 @@ class Fmi3StepResult(NamedTuple): earlyReturn: bool = False -class Fmi3SlaveBase(object): +class Fmi3SlaveBase: """Abstract facade class to execute Python through FMI standard.""" # Dictionary of (category, description) entries - log_categories: Dict[str, str] = { + log_categories: dict[str, str] = { "logStatusWarning": "Log messages with fmi3Warning status.", "logStatusDiscard": "Log messages with fmi3Discard status.", "logStatusError": "Log messages with fmi3Error status.", "logStatusFatal": "Log messages with fmi3Fatal status.", - "logAll": "Log all messages." + "logAll": "Log all messages.", } def __init__(self, **kwargs): self.vars = OrderedDict() - self.event_indicators: List[int] = [] + self.event_indicators: list[int] = [] self.instance_name = kwargs["instance_name"] self.resources = kwargs.get("resources", None) self.visible = kwargs.get("visible", False) self.log_queue = [] self.guid = uuid1() - self.author: Optional[str] = None - self.license: Optional[str] = None - self.version: Optional[str] = None - self.copyright: Optional[str] = None - self.modelName: Optional[str] = self.__class__.__name__ - self.description: Optional[str] = None - self.default_experiment: Optional[DefaultExperiment] = None - - self.type_definitions: Dict[str, VariableType] = {} - self.units: Dict[str, Unit] = {} - - def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: + self.author: str | None = None + self.license: str | None = None + self.version: str | None = None + self.copyright: str | None = None + self.modelName: str | None = self.__class__.__name__ + self.description: str | None = None + self.default_experiment: DefaultExperiment | None = None + + self.type_definitions: dict[str, VariableType] = {} + self.units: dict[str, Unit] = {} + + def to_xml(self, model_options: dict[str, str] = dict()) -> Element: """Build the XML representation of the model. - + Args: model_options (Dict[str, str]) : FMU model options - + Returns: (xml.etree.TreeElement.Element) XML description of the FMU """ - t = datetime.datetime.now(datetime.timezone.utc) + t = datetime.datetime.now(datetime.UTC) date_str = t.isoformat(timespec="seconds") attrib = dict( @@ -95,7 +103,7 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: instantiationToken=f"{self.guid!s}", generationTool=f"PythonFMU3 {VERSION}", generationDateAndTime=date_str, - variableNamingConvention="structured" + variableNamingConvention="structured", ) if self.description is not None: attrib["description"] = self.description @@ -114,13 +122,13 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: for option in FMI3_MODEL_OPTIONS_COMMON: value = model_options.get(option.name, option.value) options[option.name] = str(value).lower() - + options["modelIdentifier"] = self.modelName options_cs = options.copy() for option in FMI3_MODEL_OPTIONS_COSIM: options_cs[option.name] = str(value).lower() - + options_me = options.copy() for option in FMI3_MODEL_OPTIONS_MX: options_me[option.name] = str(value).lower() @@ -128,7 +136,7 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: # check if we have cosim mixin or model exchange mixin if isinstance(self, ModelExchange): SubElement(root, "ModelExchange", attrib=options_me) - + if isinstance(self, CoSimulation): SubElement(root, "CoSimulation", attrib=options_cs) @@ -163,7 +171,7 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: if self.default_experiment.tolerance is not None: attrib["tolerance"] = str(self.default_experiment.tolerance) SubElement(root, "DefaultExperiment", attrib) - + variables = SubElement(root, "ModelVariables") for v in self.vars.values(): if ModelVariable.requires_start(v): @@ -176,25 +184,50 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: ) continuous_state_derivatives = list( - filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values()) + filter( + lambda v: ( + v.variability == Fmi3Variability.continuous + and (isinstance(v, Float64) and v.derivative is not None) + ), + self.vars.values(), + ) ) allowed_variability = [None, Fmi3Initial.approx, Fmi3Initial.calculated] initial_unknown = list( - filter(lambda v: (v.causality == Fmi3Causality.output and (v.initial in allowed_variability)) - or v.causality == Fmi3Causality.calculatedParameter - or v in continuous_state_derivatives and v.initial in allowed_variability, self.vars.values()) + filter( + lambda v: ( + ( + v.causality == Fmi3Causality.output + and (v.initial in allowed_variability) + ) + or v.causality == Fmi3Causality.calculatedParameter + or v in continuous_state_derivatives + and v.initial in allowed_variability + ), + self.vars.values(), + ) ) for v in outputs: - SubElement(structure, "Output", attrib=dict(valueReference=str(v.value_reference))) + SubElement( + structure, "Output", attrib=dict(valueReference=str(v.value_reference)) + ) for v in continuous_state_derivatives: - SubElement(structure, "ContinuousStateDerivative", attrib=dict(valueReference=str(v.value_reference))) + SubElement( + structure, + "ContinuousStateDerivative", + attrib=dict(valueReference=str(v.value_reference)), + ) for v in initial_unknown: - SubElement(structure, "InitialUnknown", attrib=dict(valueReference=str(v.value_reference))) - + SubElement( + structure, + "InitialUnknown", + attrib=dict(valueReference=str(v.value_reference)), + ) + for v in self.event_indicators: SubElement(structure, "EventIndicator", attrib=dict(valueReference=str(v))) @@ -218,9 +251,15 @@ def __apply_start_value(self, var: ModelVariable): raise Exception(f"Unsupported type {type(var)}!") var.start = refs if len(getattr(var, "dimensions", [])) > 0 else refs[0] - def register_variable(self, var: ModelVariable, nested: bool = True, var_type: Any = None, has_event_indicator: bool = False): + def register_variable( + self, + var: ModelVariable, + nested: bool = True, + var_type: Any = None, + has_event_indicator: bool = False, + ): """Register a variable as FMU interface. - + Args: var (ModelVariable): The variable to be registered nested (bool): Optional, does the "." in the variable name reflect an object hierarchy to access it? Default True @@ -240,17 +279,26 @@ def register_variable(self, var: ModelVariable, nested: bool = True, var_type: A var.getter = lambda: getattr(owner, var.local_name).flatten().tolist() else: var.getter = lambda: getattr(owner, var.local_name) - if var.setter is None and hasattr(owner, var.local_name) and var.variability != Fmi3Variability.constant: + if ( + var.setter is None + and hasattr(owner, var.local_name) + and var.variability != Fmi3Variability.constant + ): if hasattr(var, "dimensions") and len(var.dimensions) > 0: import numpy as np - var.setter = lambda v: setattr(owner, var.local_name, np.reshape(v, newshape=getattr(owner, var.local_name).shape)) + + var.setter = lambda v: setattr( + owner, + var.local_name, + np.reshape(v, newshape=getattr(owner, var.local_name).shape), + ) else: var.setter = lambda v: setattr(owner, var.local_name, v) - + if var_type: self.type_definitions[var_type.name] = var_type var.declared_type = var_type.name - + if has_event_indicator: self.register_event_indicator(var.value_reference) @@ -260,7 +308,7 @@ def register_event_indicator(self, vr): def setup_experiment(self, start_time: float): pass - def register_units(self, units: List[Unit]): + def register_units(self, units: list[Unit]): for unit in units: self.units[unit.name] = unit @@ -276,7 +324,7 @@ def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult: def terminate(self): pass - def get_int32(self, vrs: List[int]) -> List[int]: + def get_int32(self, vrs: list[int]) -> list[int]: refs = list() for vr in vrs: var = self.vars[vr] @@ -290,8 +338,8 @@ def get_int32(self, vrs: List[int]) -> List[int]: f"Variable with valueReference={vr} is not of type Int32!" ) return refs - - def get_int64(self, vrs: List[int]) -> List[int]: + + def get_int64(self, vrs: list[int]) -> list[int]: refs = list() for vr in vrs: var = self.vars[vr] @@ -306,14 +354,18 @@ def get_int64(self, vrs: List[int]) -> List[int]: ) return refs - def get_uint64(self, vrs: List[int]) -> List[ctypes.c_uint64]: + def get_uint64(self, vrs: list[int]) -> list[ctypes.c_uint64]: refs = list() for vr in vrs: var = self.vars[vr] if isinstance(var, UInt64): if len(var.dimensions) == 0: val = var.getter() - refs.append(ctypes.c_uint64(val) if not isinstance(val, ctypes.c_uint64) else val) + refs.append( + ctypes.c_uint64(val) + if not isinstance(val, ctypes.c_uint64) + else val + ) else: refs.extend(map(ctypes.c_uint64, var.getter())) else: @@ -322,7 +374,7 @@ def get_uint64(self, vrs: List[int]) -> List[ctypes.c_uint64]: ) return refs - def get_float64(self, vrs: List[int]) -> List[float]: + def get_float64(self, vrs: list[int]) -> list[float]: refs = list() for vr in vrs: var = self.vars[vr] @@ -337,7 +389,7 @@ def get_float64(self, vrs: List[int]) -> List[float]: ) return refs - def get_boolean(self, vrs: List[int]) -> List[bool]: + def get_boolean(self, vrs: list[int]) -> list[bool]: refs = list() for vr in vrs: var = self.vars[vr] @@ -346,14 +398,14 @@ def get_boolean(self, vrs: List[int]) -> List[bool]: refs.append(bool(var.getter())) else: refs.extend(var.getter()) - + else: raise TypeError( f"Variable with valueReference={vr} is not of type Boolean!" ) return refs - def get_string(self, vrs: List[int]) -> List[str]: + def get_string(self, vrs: list[int]) -> list[str]: refs = list() for vr in vrs: var = self.vars[vr] @@ -365,14 +417,14 @@ def get_string(self, vrs: List[int]) -> List[str]: ) return refs - def set_int32(self, vrs: List[int], values: List[int]): + def set_int32(self, vrs: list[int], values: list[int]): offset = 0 for vr in vrs: var = self.vars[vr] if isinstance(var, Int32): size = var.size(self.vars) if size > 1: - var.setter(values[offset:offset+size]) + var.setter(values[offset : offset + size]) else: var.setter(values[offset]) offset += size @@ -380,15 +432,15 @@ def set_int32(self, vrs: List[int], values: List[int]): raise TypeError( f"Variable with valueReference={vr} is not of type Int32!" ) - - def set_int64(self, vrs: List[int], values: List[int]): + + def set_int64(self, vrs: list[int], values: list[int]): offset = 0 for vr in vrs: var = self.vars[vr] if isinstance(var, (Enumeration, Int64)): size = var.size(self.vars) if size > 1: - var.setter(values[offset:offset+size]) + var.setter(values[offset : offset + size]) else: var.setter(values[offset]) offset += size @@ -397,14 +449,14 @@ def set_int64(self, vrs: List[int], values: List[int]): f"Variable with valueReference={vr} is not of type Int64!" ) - def set_uint64(self, vrs: List[int], values: List[int]): + def set_uint64(self, vrs: list[int], values: list[int]): offset = 0 for vr in vrs: var = self.vars[vr] if isinstance(var, UInt64): size = var.size(self.vars) if size > 1: - var.setter(values[offset:offset+size]) + var.setter(values[offset : offset + size]) else: var.setter(values[offset]) offset += size @@ -413,14 +465,14 @@ def set_uint64(self, vrs: List[int], values: List[int]): f"Variable with valueReference={vr} is not of type UInt64!" ) - def set_float64(self, vrs: List[int], values: List[float]): + def set_float64(self, vrs: list[int], values: list[float]): offset = 0 for vr in vrs: var = self.vars[vr] if isinstance(var, Float64): size = var.size(self.vars) if size > 1: - var.setter(values[offset:offset+size]) + var.setter(values[offset : offset + size]) else: var.setter(values[offset]) offset += size @@ -429,14 +481,14 @@ def set_float64(self, vrs: List[int], values: List[float]): f"Variable with valueReference={vr} is not of type Float64!" ) - def set_boolean(self, vrs: List[int], values: List[bool]): + def set_boolean(self, vrs: list[int], values: list[bool]): offset = 0 for vr in vrs: var = self.vars[vr] if isinstance(var, Boolean): size = var.size(self.vars) if size > 1: - var.setter(values[offset:offset+size]) + var.setter(values[offset : offset + size]) else: var.setter(values[offset]) offset += size @@ -445,7 +497,7 @@ def set_boolean(self, vrs: List[int], values: List[bool]): f"Variable with valueReference={vr} is not of type Boolean!" ) - def set_string(self, vrs: List[int], values: List[str]): + def set_string(self, vrs: list[int], values: list[str]): for vr, value in zip(vrs, values): var = self.vars[vr] if isinstance(var, String): @@ -455,13 +507,13 @@ def set_string(self, vrs: List[int], values: List[str]): f"Variable with valueReference={vr} is not of type String!" ) - def _get_fmu_state(self) -> Dict[str, Any]: + def _get_fmu_state(self) -> dict[str, Any]: state = dict() for var in self.vars.values(): state[var.name] = var.getter() return state - def _set_fmu_state(self, state: Dict[str, Any]): + def _set_fmu_state(self, state: dict[str, Any]): vars_by_name = dict([(v.name, v) for v in self.vars.values()]) for name, value in state.items(): if name not in vars_by_name: @@ -474,31 +526,43 @@ def _set_fmu_state(self, state: Dict[str, Any]): def get_number_of_event_indicators(self) -> int: return len(self.event_indicators) - def set_continuous_states(self, values: List[float]): + def set_continuous_states(self, values: list[float]): offset = 0 continuous_state_derivatives = list( - filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values()) + filter( + lambda v: ( + v.variability == Fmi3Variability.continuous + and (isinstance(v, Float64) and v.derivative is not None) + ), + self.vars.values(), + ) ) vrs = [v.derivative for v in continuous_state_derivatives] - + for vr in vrs: var = self.vars[vr] size = var.size(self.vars) if size > 1: - var.setter(values[offset:offset+size]) + var.setter(values[offset : offset + size]) else: var.setter(values[offset]) offset += size - - def get_continuous_states(self) -> List[float]: + + def get_continuous_states(self) -> list[float]: offset = 0 continuous_state_derivatives = list( - filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values()) + filter( + lambda v: ( + v.variability == Fmi3Variability.continuous + and (isinstance(v, Float64) and v.derivative is not None) + ), + self.vars.values(), + ) ) vrs = [v.derivative for v in continuous_state_derivatives] - + refs = list() for vr in vrs: var = self.vars[vr] @@ -506,24 +570,30 @@ def get_continuous_states(self) -> List[float]: refs.append(float(var.getter())) else: refs.extend(var.getter()) - + return refs - + def get_number_of_continuous_states(self) -> int: continuous_state_derivatives = list( - filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values()) + filter( + lambda v: ( + v.variability == Fmi3Variability.continuous + and (isinstance(v, Float64) and v.derivative is not None) + ), + self.vars.values(), + ) ) return len(continuous_state_derivatives) - + def set_time(self, time: float): self.time = time @staticmethod - def _fmu_state_to_bytes(state: Dict[str, Any]) -> bytes: + def _fmu_state_to_bytes(state: dict[str, Any]) -> bytes: return json.dumps(state).encode("utf-8") @staticmethod - def _fmu_state_from_bytes(state: bytes) -> Dict[str, Any]: + def _fmu_state_from_bytes(state: bytes) -> dict[str, Any]: return json.loads(state.decode("utf-8")) def _get_log_queue(self): @@ -533,11 +603,11 @@ def log( self, msg: str, status: Fmi3Status = Fmi3Status.ok, - category: Optional[str] = None, - debug: bool = False + category: str | None = None, + debug: bool = False, ): """Log a message to the FMU logger. - + Args: msg (str) : Log message status (Fmi3Status) : Optional, message status (default ok) @@ -551,5 +621,6 @@ def log( log_msg = LogMsg(status, category, msg, debug) self.log_queue.append(log_msg) + class Fmi3Slave(Fmi3SlaveBase, CoSimulation): - pass \ No newline at end of file + pass diff --git a/pythonfmu3/logmsg.py b/src/pythonfmu3/logmsg.py similarity index 64% rename from pythonfmu3/logmsg.py rename to src/pythonfmu3/logmsg.py index ad28489..d3151d5 100644 --- a/pythonfmu3/logmsg.py +++ b/src/pythonfmu3/logmsg.py @@ -1,6 +1,4 @@ - class LogMsg: - def __init__(self, status: int, category: str, msg: str, debug: bool): self.status = status self.category = category @@ -8,7 +6,4 @@ def __init__(self, status: int, category: str, msg: str, debug: bool): self.debug = debug def __str__(self) -> str: - return "LogMsg(status={}, category={}, msg={}, debug={}".format(self.status, self.category, self.msg, self.debug) - - - + return f"LogMsg(status={self.status}, category={self.category}, msg={self.msg}, debug={self.debug}" diff --git a/pythonfmu3/modelexchange.py b/src/pythonfmu3/modelexchange.py similarity index 89% rename from pythonfmu3/modelexchange.py rename to src/pythonfmu3/modelexchange.py index 7848b24..eb3328d 100644 --- a/pythonfmu3/modelexchange.py +++ b/src/pythonfmu3/modelexchange.py @@ -1,6 +1,7 @@ from abc import ABC, ABCMeta, abstractmethod from dataclasses import dataclass + @dataclass class Fmi3UpdateDiscreteStatesResult: discreteStateNeedsUpdate: bool = False @@ -10,14 +11,19 @@ class Fmi3UpdateDiscreteStatesResult: nextEventTimeDefined: bool = False nextEventTime: float = 0.0 + class RequireTimeMeta(ABCMeta): def __init__(cls, name, bases, namespace): super().__init__(name, bases, namespace) orig_init = cls.__init__ + def new_init(self, *args, **kwargs): orig_init(self, *args, **kwargs) - if not hasattr(self, 'time'): - raise AttributeError(f"{cls.__name__} must define 'self.time' in __init__") + if not hasattr(self, "time"): + raise AttributeError( + f"{cls.__name__} must define 'self.time' in __init__" + ) + cls.__init__ = new_init @@ -25,7 +31,7 @@ def new_init(self, *args, **kwargs): class ModelExchange(ABC, metaclass=RequireTimeMeta): """ Classes derived from ModelExchange must define a 'self.time' member variable. - + Required methods to override: - `get_continuous_state_derivatives`: Must return a list of continuous state derivatives. @@ -33,20 +39,19 @@ class ModelExchange(ABC, metaclass=RequireTimeMeta): - `get_event_indicators`: Should return a list of event indicators. - `update_discrete_states`: Signify converged solution at current super-dense time instant. """ - + @abstractmethod def get_continuous_state_derivatives(self): """Return the continuous state derivatives of the model.""" - pass - + def get_event_indicators(self): """Return the event indicators of the model.""" return [] - + def update_discrete_states(self): """Update the discrete states of the model.""" return Fmi3UpdateDiscreteStatesResult() def get_nominals_of_continuous_states(self, size: int) -> list[float]: """Return the nominal values of the continuous states.""" - return [1.0] * size \ No newline at end of file + return [1.0] * size diff --git a/pythonfmu3/osutil.py b/src/pythonfmu3/osutil.py similarity index 92% rename from pythonfmu3/osutil.py rename to src/pythonfmu3/osutil.py index 568350e..81fb6f3 100644 --- a/pythonfmu3/osutil.py +++ b/src/pythonfmu3/osutil.py @@ -1,15 +1,15 @@ -import sys - import platform + def get_fmu_arch(): - #is_64bits = sys.maxsize > 2 ** 32 + # is_64bits = sys.maxsize > 2 ** 32 system = platform.system() is_x86 = platform.machine() in ["i386", "AMD64", "x86_64"] platforms = {"Windows": "windows", "Linux": "linux", "Darwin": "darwin"} arch = "x86_64" if is_x86 else "x86" return arch + "-" + platforms.get(system, "unknown") + def get_platform() -> str: """Get FMU binary platform folder name.""" return get_fmu_arch() @@ -19,4 +19,3 @@ def get_lib_extension() -> str: """Get FMU library platform extension.""" platforms = {"Darwin": "dylib", "Linux": "so", "Windows": "dll"} return platforms.get(platform.system(), "") - diff --git a/pythonfmu3/pythonfmu-export/.clang-format b/src/pythonfmu3/pythonfmu-export/.clang-format similarity index 100% rename from pythonfmu3/pythonfmu-export/.clang-format rename to src/pythonfmu3/pythonfmu-export/.clang-format diff --git a/pythonfmu3/pythonfmu-export/CMakeLists.txt b/src/pythonfmu3/pythonfmu-export/CMakeLists.txt similarity index 100% rename from pythonfmu3/pythonfmu-export/CMakeLists.txt rename to src/pythonfmu3/pythonfmu-export/CMakeLists.txt diff --git a/pythonfmu3/pythonfmu-export/build_unix.sh b/src/pythonfmu3/pythonfmu-export/build_unix.sh similarity index 100% rename from pythonfmu3/pythonfmu-export/build_unix.sh rename to src/pythonfmu3/pythonfmu-export/build_unix.sh diff --git a/pythonfmu3/pythonfmu-export/build_win.sh b/src/pythonfmu3/pythonfmu-export/build_win.sh similarity index 100% rename from pythonfmu3/pythonfmu-export/build_win.sh rename to src/pythonfmu3/pythonfmu-export/build_win.sh diff --git a/pythonfmu3/pythonfmu-export/src/CMakeLists.txt b/src/pythonfmu3/pythonfmu-export/src/CMakeLists.txt similarity index 100% rename from pythonfmu3/pythonfmu-export/src/CMakeLists.txt rename to src/pythonfmu3/pythonfmu-export/src/CMakeLists.txt diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp rename to src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp rename to src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp rename to src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp rename to src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp diff --git a/pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h b/src/pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h similarity index 100% rename from pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h rename to src/pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h diff --git a/pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h b/src/pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h similarity index 100% rename from pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h rename to src/pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h diff --git a/pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h b/src/pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h similarity index 100% rename from pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h rename to src/pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp rename to src/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp rename to src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp rename to src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp similarity index 100% rename from pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp rename to src/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp diff --git a/pythonfmu3/unit.py b/src/pythonfmu3/unit.py similarity index 50% rename from pythonfmu3/unit.py rename to src/pythonfmu3/unit.py index cb8640f..49cd3cb 100644 --- a/pythonfmu3/unit.py +++ b/src/pythonfmu3/unit.py @@ -1,6 +1,7 @@ from xml.etree.ElementTree import Element -class BaseUnit(object): + +class BaseUnit: tag = "BaseUnit" base_units = { "kg": 0, @@ -23,22 +24,22 @@ def units(self): return {key: str(value) for key, value in self._units.items() if value != 0} def to_xml(self): - """Convert the variable to XML node. - - Returns - xml.etree.ElementTree.Element: XML node - """ - attrib = dict() - if self._factor != 1.0: - attrib["factor"] = str(self._factor) - if self._offset != 0.0: - attrib["offset"] = str(self._offset) - attrib.update(self.units) - ele = Element(self.tag, attrib) - return ele - - -class Unit(object): + """Convert the variable to XML node. + + Returns + xml.etree.ElementTree.Element: XML node + """ + attrib = dict() + if self._factor != 1.0: + attrib["factor"] = str(self._factor) + if self._offset != 0.0: + attrib["offset"] = str(self._offset) + attrib.update(self.units) + ele = Element(self.tag, attrib) + return ele + + +class Unit: tag = "Unit" def __init__(self, name, **kwargs): @@ -47,16 +48,16 @@ def __init__(self, name, **kwargs): @property def name(self) -> str: - return self._name + return self._name def to_xml(self) -> Element: - """Convert the variable to XML node. - - Returns - xml.etree.ElementTree.Element: XML node - """ - attrib = dict() - attrib["name"] = self._name - ele = Element(self.tag, attrib) - ele.append(self._base_unit.to_xml()) - return ele + """Convert the variable to XML node. + + Returns + xml.etree.ElementTree.Element: XML node + """ + attrib = dict() + attrib["name"] = self._name + ele = Element(self.tag, attrib) + ele.append(self._base_unit.to_xml()) + return ele diff --git a/pythonfmu3/variable_types.py b/src/pythonfmu3/variable_types.py similarity index 78% rename from pythonfmu3/variable_types.py rename to src/pythonfmu3/variable_types.py index 34286e8..124d1a3 100644 --- a/pythonfmu3/variable_types.py +++ b/src/pythonfmu3/variable_types.py @@ -1,16 +1,15 @@ from enum import Enum -from typing import Optional from xml.etree.ElementTree import Element -class VariableType(object): +class VariableType: def __init__(self, tag, name, **kwargs): self._tag = tag self._attrs = { "name": name, } self._attrs.update(**kwargs) - + @property def name(self) -> str: """str: Variable name""" @@ -28,6 +27,7 @@ def to_xml(self) -> Element: attrib[key] = str(value.name if isinstance(value, Enum) else value) return Element(self._tag, attrib) + class Float64Type(VariableType): tag = "Float64Type" @@ -35,15 +35,11 @@ def __init__(self, name, **kwargs): super().__init__(self.tag, name, **kwargs) -class Item(object): +class Item: tag = "Item" def __init__(self, name: str, value: int, description: str = "", **kwargs): - self.__attrs = { - "name": name, - "value": str(value), - "description": description - } + self.__attrs = {"name": name, "value": str(value), "description": description} def name(self) -> str: return self.__attrs["name"] @@ -53,7 +49,7 @@ def value(self) -> str: return self.__attrs["value"] @property - def description(self) -> Optional[str]: + def description(self) -> str | None: return self.__attrs["description"] def to_xml(self) -> Element: @@ -69,24 +65,27 @@ def to_xml(self) -> Element: return Element(self.tag, attrib) def __repr__(self) -> str: - return f"{self.__class__.__name__}" \ - f"(name={self.name}, " \ - f"value={self.value}, " \ - f"description={self.description})" + return ( + f"{self.__class__.__name__}" + f"(name={self.name}, " + f"value={self.value}, " + f"description={self.description})" + ) + class EnumerationType(VariableType): tag = "EnumerationType" - def __init__(self, name: str, values: Optional[Enum] = None, **kwargs): + def __init__(self, name: str, values: Enum | None = None, **kwargs): super().__init__(self.tag, name, **kwargs) self._items = {} - + for val in values: - self.add_value(name = val.name, value = val.value) + self.add_value(name=val.name, value=val.value) def add_value(self, name: str, value: int, description: str = "", **kwargs): self._items[name] = Item(name, value, description, **kwargs) - + def get_value(self, name): return self._items[name] @@ -99,4 +98,4 @@ def to_xml(self) -> Element: parent = super().to_xml() for _, item in self._items.items(): parent.append(item.to_xml()) - return parent \ No newline at end of file + return parent diff --git a/pythonfmu3/variables.py b/src/pythonfmu3/variables.py similarity index 76% rename from pythonfmu3/variables.py rename to src/pythonfmu3/variables.py index c2ee44e..0beb9bd 100644 --- a/pythonfmu3/variables.py +++ b/src/pythonfmu3/variables.py @@ -1,17 +1,19 @@ """Classes describing interface variables.""" -from abc import ABC -from enum import Enum + import importlib -from typing import Any, Optional, List -from xml.etree.ElementTree import Element, SubElement -from collections.abc import Iterable +from abc import ABC from collections import ChainMap -from functools import reduce +from collections.abc import Iterable +from enum import Enum +from functools import reduce +from typing import Any +from xml.etree.ElementTree import Element from .enums import Fmi3Causality, Fmi3Initial, Fmi3Variability MAX_LENGTH = 1000 + def flatten(lst): for item in lst: if isinstance(item, Iterable) and not isinstance(item, (str, bytes)): @@ -19,11 +21,15 @@ def flatten(lst): else: yield item + def check_numpy(): try: - importlib.import_module('numpy') + importlib.import_module("numpy") except ModuleNotFoundError: - raise ModuleNotFoundError("Numpy is not installed, it is required for Array support") + raise ModuleNotFoundError( + "Numpy is not installed, it is required for Array support" + ) + class ModelVariable(ABC): """Abstract FMI model variable definition. @@ -35,25 +41,25 @@ class ModelVariable(ABC): initial (:obj:`Fmi3Initial`, optional): Variable initial status variability (:obj:`Fmi3Variability`, optional): Variable variability """ + def __init__( self, name: str, - causality: Optional[Fmi3Causality] = None, - description: Optional[str] = None, - initial: Optional[Fmi3Initial] = None, - variability: Optional[Fmi3Variability] = None, - declared_type: Optional[str] = None, + causality: Fmi3Causality | None = None, + description: str | None = None, + initial: Fmi3Initial | None = None, + variability: Fmi3Variability | None = None, + declared_type: str | None = None, getter: Any = None, - setter: Any = None + setter: Any = None, ): self.getter = getter self.setter = setter self._type = None self.local_name = name.split(".")[-1] - - # demanagle names - self.local_name = self.local_name.lstrip('_') + # demanagle names + self.local_name = self.local_name.lstrip("_") self.__attrs = { "name": name, @@ -68,17 +74,17 @@ def __init__( self._extras = {} @property - def causality(self) -> Optional[Fmi3Causality]: + def causality(self) -> Fmi3Causality | None: """:obj:`Fmi3Causality` or None: Variable causality - None if not set""" return self.__attrs["causality"] @property - def description(self) -> Optional[str]: + def description(self) -> str | None: """str or None: Variable description - None if not set""" return self.__attrs["description"] @property - def initial(self) -> Optional[Fmi3Initial]: + def initial(self) -> Fmi3Initial | None: """:obj:`Fmi3Initial` or None: Variable initial status - None if not set""" return self.__attrs["initial"] @@ -108,14 +114,14 @@ def declared_type(self, value: str): if self.__attrs["declaredType"] is not None: raise RuntimeError("Declared type already set.") self.__attrs["declaredType"] = value - + @property - def variability(self) -> Optional[Fmi3Variability]: + def variability(self) -> Fmi3Variability | None: """:obj:`Fmi3Variability` or None: Variable variability - None if not set""" return self.__attrs["variability"] - + @staticmethod - def requires_start(v: 'ModelVariable') -> bool: + def requires_start(v: "ModelVariable") -> bool: """Test if a variable requires a start attribute Returns: @@ -142,33 +148,46 @@ def to_xml(self) -> Element: return Element(self._type, attrib) def __repr__(self) -> str: - return f"{self.__class__.__name__}" \ - f"(name={self.name}, " \ - f"causality={self.causality}, " \ - f"variability={self.variability})" + return ( + f"{self.__class__.__name__}" + f"(name={self.name}, " + f"causality={self.causality}, " + f"variability={self.variability})" + ) + -class Start(object): +class Start: def __init__(self, startValue): self.value = startValue - + def to_xml(self) -> Element: attrib = dict() attrib["value"] = self.value return Element("Start", attrib) -class Dimension(object): + +class Dimension: def __init__(self, start: str = "", valueReference: str = ""): if start and valueReference and any((start, valueReference)): - raise RuntimeError("start and valueReference attributes are mutally exclusive for Dimension element") - + raise RuntimeError( + "start and valueReference attributes are mutally exclusive for Dimension element" + ) + self.start = start self.value_reference = valueReference - def size(self, vars : List[ModelVariable]): + def size(self, vars: list[ModelVariable]): if self.start: return self.start else: - result = next((value for vr, value in vars.items() if vr == int(self.value_reference)), None) + result = next( + ( + value + for vr, value in vars.items() + if vr == int(self.value_reference) + ), + None, + ) return result.getter() def to_xml(self) -> Element: @@ -177,18 +196,19 @@ def to_xml(self) -> Element: if self.start: attrib["start"] = self.start else: - attrib['valueReference'] = self.value_reference + attrib["valueReference"] = self.value_reference ele = Element("Dimension", attrib) return ele -class Arrayable(object): - def __init__(self, dimensions : List[Dimension] = [], **kwargs): + +class Arrayable: + def __init__(self, dimensions: list[Dimension] = [], **kwargs): if dimensions: check_numpy() self._dimensions = dimensions - + @property - def dimensions(self) -> List[Dimension]: + def dimensions(self) -> list[Dimension]: return self._dimensions def get_start_str(self, value, formatter): @@ -199,15 +219,24 @@ def get_start_str(self, value, formatter): return output else: return formatter(value) - - def dimensions_xml(self) -> List[Element]: + + def dimensions_xml(self) -> list[Element]: return [dim.to_xml() for dim in self._dimensions] def size(self, vars): return reduce(lambda x, dim: x * int(dim.size(vars)), self._dimensions, 1) + class Float64(ModelVariable, Arrayable): - def __init__(self, name: str, start: Optional[Any] = None, derivative: Optional[Any] = None, dimensions: List[Dimension] = [], unit: Optional[str] = None, **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + derivative: Any | None = None, + dimensions: list[Dimension] = [], + unit: str | None = None, + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) Arrayable.__init__(self, dimensions) self.__attrs = {"start": start, "derivative": derivative} @@ -215,15 +244,15 @@ def __init__(self, name: str, start: Optional[Any] = None, derivative: Optional[ self._unit = unit @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self.__attrs["start"] @start.setter def start(self, value: float): self.__attrs["start"] = value - + @property - def unit(self) -> Optional[Any]: + def unit(self) -> Any | None: return self._unit @unit.setter @@ -252,17 +281,23 @@ def to_xml(self) -> Element: parent.extend(self.dimensions_xml()) return parent - + class Int32(ModelVariable, Arrayable): - def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + dimensions: list[Dimension] = [], + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} - self._type = "Int32"; + self._type = "Int32" @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self.__attrs["start"] @start.setter @@ -276,20 +311,27 @@ def to_xml(self) -> Element: attrib[key] = self.get_start_str(value, lambda v: str(v)) self._extras = attrib parent = super().to_xml() - + parent.extend(self.dimensions_xml()) return parent - + + class Int64(ModelVariable, Arrayable): - def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + dimensions: list[Dimension] = [], + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} - self._type = "Int64"; + self._type = "Int64" @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self.__attrs["start"] @start.setter @@ -308,15 +350,22 @@ def to_xml(self) -> Element: return parent + class UInt64(ModelVariable, Arrayable): - def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + dimensions: list[Dimension] = [], + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} - self._type = "UInt64"; + self._type = "UInt64" @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self.__attrs["start"] @start.setter @@ -334,16 +383,23 @@ def to_xml(self) -> Element: parent.extend(self.dimensions_xml()) return parent - + + class Boolean(ModelVariable, Arrayable): - def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + dimensions: list[Dimension] = [], + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} self._type = "Boolean" @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self.__attrs["start"] @start.setter @@ -357,21 +413,27 @@ def to_xml(self) -> Element: attrib[key] = self.get_start_str(value, lambda v: str(v).lower()) self._extras = attrib parent = super().to_xml() - + parent.extend(self.dimensions_xml()) return parent class String(ModelVariable): - def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + dimensions: list[Dimension] = [], + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) self.__attrs = dict() self._type = "String" self._start = Start(start) @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self._start.value @start.setter @@ -385,21 +447,29 @@ def to_xml(self) -> Element: attrib[key] = str(value, lambda v: str(v)) self._extras = attrib parent = super().to_xml() - + if self.start is not None: parent.append(self._start.to_xml()) - + return parent - + + class Enumeration(ModelVariable, Arrayable): - def __init__(self, name: str, start: Optional[Any] = None, declared_type: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + def __init__( + self, + name: str, + start: Any | None = None, + declared_type: Any | None = None, + dimensions: list[Dimension] = [], + **kwargs, + ): ModelVariable.__init__(self, name, **kwargs) Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start, "declaredType": declared_type} self._type = "Enumeration" @property - def start(self) -> Optional[Any]: + def start(self) -> Any | None: return self.__attrs["start"] @start.setter @@ -407,7 +477,7 @@ def start(self, value: float): self.__attrs["start"] = value @property - def declared_type(self) -> Optional[Any]: + def declared_type(self) -> Any | None: return self.__attrs["declaredType"] @declared_type.setter diff --git a/pythonfmu3/tests/__init__.py b/tests/__init__.py similarity index 100% rename from pythonfmu3/tests/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..53ec4a0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +def pytest_configure(config): + config.addinivalue_line( + "markers", "requirements(pkg): mark test as requiring the specified package" + ) diff --git a/pythonfmu3/tests/data/csvdemo.csv b/tests/data/csvdemo.csv similarity index 100% rename from pythonfmu3/tests/data/csvdemo.csv rename to tests/data/csvdemo.csv diff --git a/pythonfmu3/tests/data/hello.txt b/tests/data/hello.txt similarity index 100% rename from pythonfmu3/tests/data/hello.txt rename to tests/data/hello.txt diff --git a/pythonfmu3/tests/slaves/PythonSlaveWithException.py b/tests/slaves/PythonSlaveWithException.py similarity index 59% rename from pythonfmu3/tests/slaves/PythonSlaveWithException.py rename to tests/slaves/PythonSlaveWithException.py index 41fd133..be2f3f5 100644 --- a/pythonfmu3/tests/slaves/PythonSlaveWithException.py +++ b/tests/slaves/PythonSlaveWithException.py @@ -1,8 +1,7 @@ -from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Float64, Fmi3Variability +from pythonfmu3.fmi3slave import Float64, Fmi3Causality, Fmi3Slave, Fmi3Variability class PythonSlaveWithException(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) @@ -10,7 +9,13 @@ def __init__(self, **kwargs): self.realOut = 0.0 self.time = 0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) self.register_variable(Float64("realIn", causality=Fmi3Causality.input)) self.register_variable(Float64("realOut", causality=Fmi3Causality.output)) diff --git a/pythonfmu3/tests/slaves/localmodule.py b/tests/slaves/localmodule.py similarity index 79% rename from pythonfmu3/tests/slaves/localmodule.py rename to tests/slaves/localmodule.py index fc7f52a..ec14542 100644 --- a/pythonfmu3/tests/slaves/localmodule.py +++ b/tests/slaves/localmodule.py @@ -1,7 +1,5 @@ - - def get_amplitude(): - return 5. + return 5.0 def get_time_constant(): diff --git a/tests/slaves/pythonslave.py b/tests/slaves/pythonslave.py new file mode 100644 index 0000000..7386103 --- /dev/null +++ b/tests/slaves/pythonslave.py @@ -0,0 +1,91 @@ +from pythonfmu3.fmi3slave import ( + Boolean, + Float64, + Fmi3Causality, + Fmi3Slave, + Fmi3Variability, + Int32, + String, +) + + +class Container: + pass + + +class PythonSlave(Fmi3Slave): + author = "John Doe" + description = "A simple description" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.intParam = 42 + self.intOut = 23 + self.realOut = 3.0 + self.booleanVariable = True + self.stringVariable = "Hello World!" + self.realIn = 2.0 / 3.0 + self.booleanParameter = False + self.stringParameter = "dog" + self.time = 0 + + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Int32( + "intParam", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable(Float64("realIn", causality=Fmi3Causality.input)) + self.register_variable( + Boolean( + "booleanParameter", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable( + String( + "stringParameter", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + + self.register_variable(Int32("intOut", causality=Fmi3Causality.output)) + self.register_variable(Float64("realOut", causality=Fmi3Causality.output)) + self.register_variable( + Boolean("booleanVariable", causality=Fmi3Causality.local) + ) + self.register_variable(String("stringVariable", causality=Fmi3Causality.local)) + + self.container = Container() + self.container.someReal = 99.0 + self.container.subContainer = sub = Container() + sub.someInteger = -15 + self.register_variable( + Float64( + "container.someReal", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + self.register_variable( + Int32( + "container.subContainer.someInteger", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + ) + ) + + def do_step(self, current_time, step_size): + self.realOut = current_time + step_size + return True diff --git a/tests/slaves/pythonslaveMX.py b/tests/slaves/pythonslaveMX.py new file mode 100644 index 0000000..41a46e6 --- /dev/null +++ b/tests/slaves/pythonslaveMX.py @@ -0,0 +1,57 @@ +from pythonfmu3 import ( + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3Variability, + ModelExchange, +) + + +class Dahlquist(Fmi3SlaveBase, ModelExchange): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Stephen Smith" + self.description = "Dahlquist's test problem for model exchange FMUs" + + self.time = 0.0 + self.k = 1.0 + self.x = 1.0 + self.derx = 0.0 + + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "k", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) + + def get_continuous_state_derivatives(self) -> list[float]: + self.derx = -self.k * self.x + return [self.derx] diff --git a/tests/slaves/pythonslaveMXCS.py b/tests/slaves/pythonslaveMXCS.py new file mode 100644 index 0000000..d38dd07 --- /dev/null +++ b/tests/slaves/pythonslaveMXCS.py @@ -0,0 +1,65 @@ +from pythonfmu3 import ( + CoSimulation, + Float64, + Fmi3Causality, + Fmi3Initial, + Fmi3SlaveBase, + Fmi3StepResult, + Fmi3Variability, + ModelExchange, +) + + +class Dahlquist(Fmi3SlaveBase, ModelExchange, CoSimulation): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Stephen Smith" + self.description = "Dahlquist's test problem for model exchange FMUs" + + self.time = 0.0 + self.k = 1.0 + self.x = 1.0 + self.derx = 0.0 + + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + Float64( + "x", + causality=Fmi3Causality.output, + start=1, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + ) + ) + self.register_variable( + Float64( + "derx", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + ) + ) + self.register_variable( + Float64( + "k", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + ) + ) + + def get_continuous_state_derivatives(self) -> list[float]: + self.derx = -self.k * self.x + return [self.derx] + + def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult: + self.time = current_time + step_size + self.derx = -self.k * self.x + self.x += self.derx * step_size + return True diff --git a/tests/slaves/pythonslave_arraytypes.py b/tests/slaves/pythonslave_arraytypes.py new file mode 100644 index 0000000..cd56192 --- /dev/null +++ b/tests/slaves/pythonslave_arraytypes.py @@ -0,0 +1,103 @@ +import numpy as np + +from pythonfmu3 import ( + Boolean, + Dimension, + Float64, + Fmi3Causality, + Fmi3Slave, + Fmi3Status, + Fmi3Variability, + Int32, + Int64, + UInt64, +) + +SIZE = 10 + +TYPES = [UInt64, Float64, Boolean, Int32, Int64] +CAUSALITY = [Fmi3Causality.output, Fmi3Causality.input] + +TYPE_MAP = { + UInt64: np.uint64, + Float64: np.float64, + Int32: np.int32, + Int64: np.int64, + Boolean: bool, +} + + +def var_names(var_type, causality): + return f"{var_type.__name__.lower()}_{causality.name.lower()}" + + +def init_var(var_type, causality, array=True): + if array: + return np.zeros(SIZE, dtype=TYPE_MAP[var_type]) + else: + return TYPE_MAP[var_type]() + + +def create_vars(self): + dimensions = [Dimension(start=str(SIZE))] + for var_type in TYPES: + for causality in CAUSALITY: + name = var_names(var_type, causality) + if var_type == Float64: + var = var_type( + name, + causality=causality, + variability=Fmi3Variability.continuous, + dimensions=dimensions, + ) + else: + var = var_type( + name, + causality=causality, + variability=Fmi3Variability.discrete, + dimensions=dimensions, + ) + setattr(self, name, init_var(var_type, causality)) + self.register_variable(var) + + +def generate_data(self): + for var_type in TYPES: + causality = Fmi3Causality.output + name = var_names(var_type, causality) + var = getattr(self, name) + if var_type == Boolean: + data = [False, True, False, True, True, False, False, True, False, True] + setattr(self, name, np.array(data).astype(bool)) + elif var_type == Int32 or var_type == Int64 or var_type == UInt64: + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + setattr(self, name, np.array(data).astype(TYPE_MAP[var_type])) + else: + data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + setattr(self, name, np.array(data).astype(TYPE_MAP[var_type])) + + +class ArrayTypes(Fmi3Slave): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Tester" + self.description = "FMU with all basic types as arrays" + + self.time = 0.0 + + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + + create_vars(self) + + generate_data(self) + + def do_step(self, current_time: float, step_size: float) -> Fmi3Status: + generate_data(self) + return True diff --git a/tests/slaves/pythonslave_read_file.py b/tests/slaves/pythonslave_read_file.py new file mode 100644 index 0000000..c9abaeb --- /dev/null +++ b/tests/slaves/pythonslave_read_file.py @@ -0,0 +1,39 @@ +from pythonfmu3.fmi3slave import ( + DefaultExperiment, + Float64, + Fmi3Causality, + Fmi3Slave, + Fmi3Variability, + String, +) + + +class PythonSlaveReadFile(Fmi3Slave): + default_experiment = DefaultExperiment(start_time=0) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + with open(f"{self.resources}/hello.txt", "r") as file: + data = file.read() + + self.time = 0 + + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) + self.register_variable( + String( + "file_content", + getter=lambda: data, + causality=Fmi3Causality.output, + variability=Fmi3Variability.constant, + ) + ) + + def do_step(self, current_time, step_size): + return True diff --git a/pythonfmu3/tests/slaves/slavewithdep.py b/tests/slaves/slavewithdep.py similarity index 51% rename from pythonfmu3/tests/slaves/slavewithdep.py rename to tests/slaves/slavewithdep.py index 1b30a30..8b29822 100644 --- a/pythonfmu3/tests/slaves/slavewithdep.py +++ b/tests/slaves/slavewithdep.py @@ -1,10 +1,11 @@ import math -from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Float64, Fmi3Variability + from localmodule import get_amplitude, get_time_constant +from pythonfmu3.fmi3slave import Float64, Fmi3Causality, Fmi3Slave, Fmi3Variability -class PythonSlaveWithDep(Fmi3Slave): +class PythonSlaveWithDep(Fmi3Slave): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -12,10 +13,20 @@ def __init__(self, **kwargs): self.realOut = 0.0 self.time = 0 - self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + self.register_variable( + Float64( + "time", + causality=Fmi3Causality.independent, + variability=Fmi3Variability.continuous, + ) + ) self.register_variable(Float64("realIn", causality=Fmi3Causality.input)) self.register_variable(Float64("realOut", causality=Fmi3Causality.output)) def do_step(self, current_time, step_size): - self.realOut = self.realIn * get_amplitude() * math.exp((current_time + step_size) / get_time_constant()) + self.realOut = ( + self.realIn + * get_amplitude() + * math.exp((current_time + step_size) / get_time_constant()) + ) return True diff --git a/pythonfmu3/tests/test_builder.py b/tests/test_builder.py similarity index 94% rename from pythonfmu3/tests/test_builder.py rename to tests/test_builder.py index 09de7df..d3ddeec 100644 --- a/pythonfmu3/tests/test_builder.py +++ b/tests/test_builder.py @@ -65,7 +65,9 @@ def test_zip_content(tmp_path): if "test" not in relative_f: assert "/".join(("resources", "pythonfmu3", relative_f)) in names - assert len(names) >= nfiles # Library + python script + XML + module name + sources + assert ( + len(names) >= nfiles + ) # Library + python script + XML + module name + sources with files.open(module_file) as myfile: assert myfile.read() == b"pythonslave" @@ -75,6 +77,7 @@ def test_zip_content(tmp_path): def test_project_files(tmp_path, pfiles): pfiles = map(Path, pfiles) script_file = Path(__file__).parent / "slaves/pythonslave.py" + def build(): with tempfile.TemporaryDirectory() as project_dir: project_dir = Path(project_dir) @@ -100,7 +103,9 @@ def build(): project_files.remove(to_remove) project_files.add(full_name.parent) - return FmuBuilder.build_FMU(script_file, dest=tmp_path, project_files=project_files) + return FmuBuilder.build_FMU( + script_file, dest=tmp_path, project_files=project_files + ) fmu = build() with zipfile.ZipFile(fmu) as files: @@ -160,6 +165,7 @@ def build(): def test_documentation(tmp_path): script_file = Path(__file__).parent / "slaves/pythonslave.py" + def build(): with tempfile.TemporaryDirectory() as documentation_dir: doc_dir = Path(documentation_dir) @@ -168,7 +174,9 @@ def build(): license_file.write_text("Dummy license") index_file = doc_dir / "index.html" index_file.write_text("dummy index") - return FmuBuilder.build_FMU(script_file, dest=tmp_path, documentation_folder=doc_dir) + return FmuBuilder.build_FMU( + script_file, dest=tmp_path, documentation_folder=doc_dir + ) fmu = build() with zipfile.ZipFile(fmu) as files: @@ -180,6 +188,7 @@ def build(): def test_terminals(tmp_path): script_file = Path(__file__).parent / "slaves/pythonslave.py" + def build(): with tempfile.TemporaryDirectory() as terminals_dir: terminals_dir = Path(terminals_dir) diff --git a/pythonfmu3/tests/test_deploy.py b/tests/test_deploy.py similarity index 85% rename from pythonfmu3/tests/test_deploy.py rename to tests/test_deploy.py index 90df2f3..8bffb36 100644 --- a/pythonfmu3/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -9,9 +9,11 @@ PYTHON_SLAVE = Path(__file__).parent / "slaves/pythonslave.py" + @pytest.mark.parametrize("test_manager", [None, "pip", "conda"]) @pytest.mark.parametrize( - "requirements, test_requirements, expected", [ + "requirements, test_requirements, expected", + [ (None, None, ValueError), (None, "requirements.txt", ValueError), (None, "environment.yaml", ValueError), @@ -20,7 +22,7 @@ ("environment.yml", None, "conda"), ("req.txt", "req.txt", "pip"), ("env.yml", "env.yml", "conda"), - ] + ], ) def test_deploy(tmp_path, test_manager, requirements, test_requirements, expected): @@ -31,7 +33,13 @@ def test_deploy(tmp_path, test_manager, requirements, test_requirements, expecte with TemporaryDirectory() as tempd: requirements_file = Path(tempd) / requirements requirements_file.write_text(dummy_requirements) - fmu = FmuBuilder.build_FMU(PYTHON_SLAVE, dest=tmp_path, project_files=[requirements_file, ]) + fmu = FmuBuilder.build_FMU( + PYTHON_SLAVE, + dest=tmp_path, + project_files=[ + requirements_file, + ], + ) else: fmu = FmuBuilder.build_FMU(PYTHON_SLAVE, dest=tmp_path) assert fmu.exists() diff --git a/pythonfmu3/tests/test_docs.py b/tests/test_docs.py similarity index 66% rename from pythonfmu3/tests/test_docs.py rename to tests/test_docs.py index 2c8907c..7a2f2f8 100644 --- a/pythonfmu3/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,31 +1,33 @@ -import pytest import pathlib import re -import tempfile + +import pytest + def extract_python_code_from_md(md_file): """Extract Python code blocks from markdown, excluding skip blocks""" content = pathlib.Path(md_file).read_text() - + # Find all python code blocks that aren't marked as skip python_blocks = [] - + # Pattern to match python blocks but not python skip - pattern = r'```python(?!\s+skip)(?:[^\n]*)\n(.*?)\n```' + pattern = r"```python(?!\s+skip)(?:[^\n]*)\n(.*?)\n```" matches = re.findall(pattern, content, re.DOTALL) - + for match in matches: # Skip blocks that are wrapped in skip-test comments - if '' not in content.split(match)[0][-100:]: + if "" not in content.split(match)[0][-100:]: python_blocks.append(match) - + return python_blocks + @pytest.mark.doc def test_readme(): test_dir = pathlib.Path(__file__).parent fpath = test_dir.parent.parent / "README.md" - + # extract Python code code_blocks = extract_python_code_from_md(fpath) @@ -35,11 +37,9 @@ def test_readme(): for i, code in enumerate(code_blocks): try: exec(code, namespace) - print(f"✅ Code block {i+1} executed successfully") + print(f"✅ Code block {i + 1} executed successfully") except Exception as e: - pytest.fail(f"Code block {i+1} failed: {e}\nCode:\n{code}") - - + pytest.fail(f"Code block {i + 1} failed: {e}\nCode:\n{code}") @pytest.mark.doc @@ -47,28 +47,28 @@ def test_cosimulation_examples(): """Test that all Python examples in usage.md actually work""" test_dir = pathlib.Path(__file__).parent usage_file = test_dir.parent.parent / "docs" / "usage.md" - + # Extract Python code code_blocks = extract_python_code_from_md(usage_file) - + # Create a test namespace namespace = {} exec("from pythonfmu3 import *", namespace) - + # Execute each code block for i, code in enumerate(code_blocks): try: exec(code, namespace) - print(f"✅ Code block {i+1} executed successfully") + print(f"✅ Code block {i + 1} executed successfully") except Exception as e: - pytest.fail(f"Code block {i+1} failed: {e}\nCode:\n{code}") - - assert 'LinearTransform' in namespace, "LinearTransform class not found" - assert 'PIDController' in namespace, "PIDController class not found" + pytest.fail(f"Code block {i + 1} failed: {e}\nCode:\n{code}") + + assert "LinearTransform" in namespace, "LinearTransform class not found" + assert "PIDController" in namespace, "PIDController class not found" # Test instantiation - lt = namespace['LinearTransform'](instance_name="test") - pc = namespace['PIDController'](instance_name="test") + lt = namespace["LinearTransform"](instance_name="test") + pc = namespace["PIDController"](instance_name="test") @pytest.mark.doc @@ -76,23 +76,23 @@ def test_model_exchange_examples(): """Test Model Exchange examples""" test_dir = pathlib.Path(__file__).parent mx_file = test_dir.parent.parent / "docs" / "usageMX.md" - + code_blocks = extract_python_code_from_md(mx_file) - + namespace = {} exec("from pythonfmu3 import *", namespace) - + for i, code in enumerate(code_blocks): try: exec(code, namespace) except Exception as e: - pytest.fail(f"MX Code block {i+1} failed: {e}") - + pytest.fail(f"MX Code block {i + 1} failed: {e}") + # Test that classes were actually created - assert 'VanDerPol' in namespace, "VanDerPol class not found" - assert 'BouncingBall' in namespace, "BouncingBall class not found" - + assert "VanDerPol" in namespace, "VanDerPol class not found" + assert "BouncingBall" in namespace, "BouncingBall class not found" + # Test instantiation - vdp = namespace['VanDerPol'](instance_name="test") - - bb = namespace['BouncingBall'](instance_name="test") \ No newline at end of file + vdp = namespace["VanDerPol"](instance_name="test") + + bb = namespace["BouncingBall"](instance_name="test") diff --git a/pythonfmu3/tests/test_fmi3slave.py b/tests/test_fmi3slave.py similarity index 77% rename from pythonfmu3/tests/test_fmi3slave.py rename to tests/test_fmi3slave.py index b288c27..4c8327a 100644 --- a/pythonfmu3/tests/test_fmi3slave.py +++ b/tests/test_fmi3slave.py @@ -12,7 +12,6 @@ def test_Fmi3Slave_constructor(model): class Slave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) if model is not None: @@ -35,24 +34,18 @@ def test_Fmi3Slave_generation_tool(): class Slave(Fmi3Slave): def do_step(self, t, dt): return True - + slave = Slave(instance_name="instance") xml = slave.to_xml() - assert xml.attrib['generationTool'] == f"PythonFMU3 {VERSION}" + assert xml.attrib["generationTool"] == f"PythonFMU3 {VERSION}" @pytest.mark.parametrize("fmi_type", FMI2PY) -@pytest.mark.parametrize("value", [ - False, - 22, - 2./3., - "hello_world" -]) +@pytest.mark.parametrize("value", [False, 22, 2.0 / 3.0, "hello_world"]) def test_Fmi3Slave_getters(fmi_type, value): - - class Slave(Fmi3Slave): + class Slave(Fmi3Slave): def __init__(self, **kwargs): super().__init__(**kwargs) self.var = value @@ -60,7 +53,7 @@ def __init__(self, **kwargs): def do_step(self, t, dt): return True - + py_type = FMI2PY[fmi_type] fmi_type_name = fmi_type.__qualname__.lower() @@ -73,16 +66,18 @@ def do_step(self, t, dt): @pytest.mark.parametrize("fmi_type", FMI2PY) -@pytest.mark.parametrize("value", [ - False, - 22, - 2./3., - "hello_world", -]) +@pytest.mark.parametrize( + "value", + [ + False, + 22, + 2.0 / 3.0, + "hello_world", + ], +) def test_Fmi3Slave_setters(fmi_type, value): class Slave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) self.var = None @@ -90,7 +85,7 @@ def __init__(self, **kwargs): def do_step(self, t, dt): return True - + slave = Slave(instance_name="slaveInstance") py_type = FMI2PY[fmi_type] fmi_type_name = fmi_type.__qualname__.lower() @@ -109,36 +104,44 @@ def test_Fmi3Slave_log_categories(): class Slave(Fmi3Slave): def do_step(self, t, dt): return True - + slave = Slave(instance_name="instance") xml = slave.to_xml() categories = xml.find("LogCategories") assert len(categories) == len(Slave.log_categories) for category, description in Slave.log_categories.items(): - assert categories.find(f"Category[@name='{category}'][@description='{description}']") is not None - - -@pytest.mark.parametrize("new_categories", [ - dict(), - { - "logStatusWarning": "Log messages with fmi3Warning status.", - "logStatusError": "Log messages with fmi3Error status.", - "logStatusFatal": "Log messages with fmi3Fatal status.", - "logAll": "Log all messages." - }, - { - "logCustom1": "My first custom log category", - "logCustom2": "My second custom log category" - } -]) + assert ( + categories.find( + f"Category[@name='{category}'][@description='{description}']" + ) + is not None + ) + + +@pytest.mark.parametrize( + "new_categories", + [ + dict(), + { + "logStatusWarning": "Log messages with fmi3Warning status.", + "logStatusError": "Log messages with fmi3Error status.", + "logStatusFatal": "Log messages with fmi3Fatal status.", + "logAll": "Log all messages.", + }, + { + "logCustom1": "My first custom log category", + "logCustom2": "My second custom log category", + }, + ], +) def test_Fmi3Slave_customized_log_categories(new_categories): class Slave(Fmi3Slave): log_categories = new_categories def do_step(self, t, dt): return True - + slave = Slave(instance_name="instance") xml = slave.to_xml() @@ -147,25 +150,28 @@ def do_step(self, t, dt): if len(new_categories) > 0: assert len(categories) == len(Slave.log_categories) for category, description in Slave.log_categories.items(): - assert categories.find(f"Category[@name='{category}'][@description='{description}']") is not None + assert ( + categories.find( + f"Category[@name='{category}'][@description='{description}']" + ) + is not None + ) else: assert categories is None def test_Fmi3Slave_model_exchange(): class Slave(Fmi3Slave, ModelExchange): - def __init__(self, **kwargs): super().__init__(**kwargs) self.time = 0.0 def get_continuous_state_derivatives(self, vals): return [] - + slave = Slave(instance_name="instance") xml = slave.to_xml() categories = xml.find("ModelExchange") - - assert categories is not None, "ModelExchange category should be present in the XML" + assert categories is not None, "ModelExchange category should be present in the XML" diff --git a/pythonfmu3/tests/test_integration.py b/tests/test_integration.py similarity index 86% rename from pythonfmu3/tests/test_integration.py rename to tests/test_integration.py index cc8a1b7..e5e3463 100644 --- a/pythonfmu3/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,6 +1,6 @@ +import ctypes import math from pathlib import Path -import ctypes import pytest @@ -47,25 +47,26 @@ def test_integration_solve_MX(tmp_path): md = fmpy.read_model_description(str(fmu)) unzipdir = fmpy.extract(str(fmu)) - model = fmpy.fmi3.FMU3Model(guid=md.guid, - unzipDirectory=unzipdir, - modelIdentifier=md.modelExchange.modelIdentifier, - instanceName="instance" - ) + model = fmpy.fmi3.FMU3Model( + guid=md.guid, + unzipDirectory=unzipdir, + modelIdentifier=md.modelExchange.modelIdentifier, + instanceName="instance", + ) model.instantiate() model.enterInitializationMode() model.exitInitializationMode() - + # forward euler t = 0.0 step_size = 0.1 - + values = (ctypes.c_double * 1)(0.0) - states = (ctypes.c_double *1)(0.0) + states = (ctypes.c_double * 1)(0.0) model.getContinuousStateDerivatives(values, 1) model.getContinuousStates(states, 1) - + assert values[0] == pytest.approx(-1.0, rel=1e-7) der = values[0] @@ -73,20 +74,21 @@ def test_integration_solve_MX(tmp_path): model.setContinuousStates((ctypes.c_double * 1)(new_state), 1) model.setTime(t + step_size) - + model.getContinuousStates(states, 1) - + assert states[0] == pytest.approx(new_state, rel=1e-7) + @pytest.mark.integration def test_integration_demo_CS_MX(tmp_path): script_file = Path(__file__).parent / "slaves/pythonslaveMXCS.py" fmu = FmuBuilder.build_FMU(script_file, dest=tmp_path, needsExecutionTool="false") assert fmu.exists() res = fmpy.simulate_fmu(str(fmu), stop_time=0.5, fmi_type="ModelExchange") - + res1 = fmpy.simulate_fmu(str(fmu), stop_time=0.5, fmi_type="CoSimulation") - + assert res[-1][0] == pytest.approx(res1[-1][0], rel=1e-7) assert res[-1][1] == pytest.approx(res1[-1][1], rel=0.1) @@ -99,11 +101,12 @@ def test_integration_reset(tmp_path): md = fmpy.read_model_description(str(fmu)) unzipdir = fmpy.extract(str(fmu)) - model = fmpy.fmi3.FMU3Slave(guid=md.guid, - unzipDirectory=unzipdir, - modelIdentifier=md.coSimulation.modelIdentifier, - instanceName="instance" - ) + model = fmpy.fmi3.FMU3Slave( + guid=md.guid, + unzipDirectory=unzipdir, + modelIdentifier=md.coSimulation.modelIdentifier, + instanceName="instance", + ) model.instantiate() model.enterInitializationMode() model.exitInitializationMode() @@ -132,16 +135,18 @@ def test_integration_get_state(tmp_path): script_file, dest=tmp_path, needsExecutionTool="false", - canGetAndSetFMUstate="true") + canGetAndSetFMUstate="true", + ) assert fmu.exists() md = fmpy.read_model_description(str(fmu)) unzipdir = fmpy.extract(str(fmu)) - model = fmpy.fmi3.FMU3Slave(guid=md.guid, - unzipDirectory=unzipdir, - modelIdentifier=md.coSimulation.modelIdentifier, - instanceName="instance" - ) + model = fmpy.fmi3.FMU3Slave( + guid=md.guid, + unzipDirectory=unzipdir, + modelIdentifier=md.coSimulation.modelIdentifier, + instanceName="instance", + ) model.instantiate() model.enterInitializationMode() model.exitInitializationMode() @@ -179,7 +184,8 @@ def test_integration_get_serialize_state(tmp_path): script_file, dest=tmp_path, canGetAndSetFMUstate="true", - canSerializeFMUstate="true") + canSerializeFMUstate="true", + ) assert fmu.exists() md = fmpy.read_model_description(fmu) @@ -189,7 +195,8 @@ def test_integration_get_serialize_state(tmp_path): guid=md.guid, unzipDirectory=unzip_dir, modelIdentifier=md.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model.instantiate() model.enterInitializationMode() @@ -240,7 +247,8 @@ def test_integration_get(tmp_path): guid=md.guid, unzipDirectory=unzip_dir, modelIdentifier=md.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model.instantiate() model.enterInitializationMode() @@ -256,7 +264,7 @@ def test_integration_get(tmp_path): "booleanParameter": False, "stringParameter": "dog", "container.someReal": 99.0, - "container.subContainer.someInteger": -15 + "container.subContainer.someInteger": -15, } model_value = None @@ -280,6 +288,7 @@ def test_integration_get(tmp_path): model.terminate() model.freeInstance() + @pytest.mark.integration def test_integration_get_array(tmp_path): script_file = Path(__file__).parent / "slaves/pythonslave_arraytypes.py" @@ -293,18 +302,30 @@ def test_integration_get_array(tmp_path): guid=md.guid, unzipDirectory=unzip_dir, modelIdentifier=md.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model.instantiate() model.enterInitializationMode() model.exitInitializationMode() to_test = { - "int32_output": [1,2,3,4,5,6,7,8,9,10], - "int64_output": [1,2,3,4,5,6,7,8,9,10], - "uint64_output": [1,2,3,4,5,6,7,8,9,10], - "float64_output": [1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0], - "boolean_output": [False, True, False, True, True, False, False, True, False, True] + "int32_output": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "int64_output": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "uint64_output": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "float64_output": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "boolean_output": [ + False, + True, + False, + True, + True, + False, + False, + True, + False, + True, + ], } model_value = None @@ -327,7 +348,7 @@ def test_integration_get_array(tmp_path): assert len(model_values) == len(value) assert model_values == value - + model.terminate() model.freeInstance() @@ -336,7 +357,12 @@ def test_integration_get_array(tmp_path): def test_integration_read_from_file(tmp_path): script_file = Path(__file__).parent / "slaves/pythonslave_read_file.py" project_file = Path(__file__).parent / "data/hello.txt" - fmu = FmuBuilder.build_FMU(script_file, project_files=[project_file], dest=tmp_path, needsExecutionTool="false") + fmu = FmuBuilder.build_FMU( + script_file, + project_files=[project_file], + dest=tmp_path, + needsExecutionTool="false", + ) assert fmu.exists() md = fmpy.read_model_description(fmu) @@ -346,7 +372,8 @@ def test_integration_read_from_file(tmp_path): guid=md.guid, unzipDirectory=unzip_dir, modelIdentifier=md.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model.instantiate() model.enterInitializationMode() @@ -356,7 +383,7 @@ def test_integration_read_from_file(tmp_path): var = variables["file_content"] model_value = model.getString([var.valueReference])[0] - with (open(project_file, 'r')) as file: + with open(project_file, "r") as file: data = file.read() assert model_value == data @@ -378,7 +405,8 @@ def test_integration_set(tmp_path): guid=md.guid, unzipDirectory=unzip_dir, modelIdentifier=md.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model.instantiate() model.enterInitializationMode() @@ -390,7 +418,7 @@ def test_integration_set(tmp_path): "booleanParameter": True, "stringParameter": "cat", "container.someReal": 42.0, - "container.subContainer.someInteger": 421 + "container.subContainer.someInteger": 421, } model_value = None @@ -418,6 +446,7 @@ def test_integration_set(tmp_path): model.terminate() model.freeInstance() + @pytest.mark.integration def test_integration_set_array(tmp_path): script_file = Path(__file__).parent / "slaves/pythonslave_arraytypes.py" @@ -431,18 +460,30 @@ def test_integration_set_array(tmp_path): guid=md.guid, unzipDirectory=unzip_dir, modelIdentifier=md.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model.instantiate() model.enterInitializationMode() model.exitInitializationMode() to_test = { - "int32_input": [1,2,3,4,5,6,7,8,9,10], - "int64_input": [1,2,3,4,5,6,7,8,9,10], - "uint64_input": [1,2,3,4,5,6,7,8,9,10], - "float64_input": [1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0], - "boolean_input": [False, True, False, True, True, False, False, True, False, True] + "int32_input": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "int64_input": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "uint64_input": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "float64_input": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + "boolean_input": [ + False, + True, + False, + True, + True, + False, + False, + True, + False, + True, + ], } model_value = None @@ -461,13 +502,13 @@ def test_integration_set_array(tmp_path): model_value = model.getUInt64(vrs, nValues=10) elif var.type == "Float64": model.setFloat64(vrs, value) - model_value = model.getFloat64(vrs,nValues=10) + model_value = model.getFloat64(vrs, nValues=10) elif var.type == "Boolean": model.setBoolean(vrs, value) model_value = model.getBoolean(vrs, nValues=10) else: pytest.xfail("Unsupported type") - + assert list(model_value) == value model.terminate() diff --git a/pythonfmu3/tests/test_logger.py b/tests/test_logger.py similarity index 78% rename from pythonfmu3/tests/test_logger.py rename to tests/test_logger.py index 3dbcee0..9449ea8 100644 --- a/pythonfmu3/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,7 +1,7 @@ -import sys import itertools +from unittest.mock import MagicMock, call + import pytest -from unittest.mock import call, MagicMock from pythonfmu3.builder import FmuBuilder from pythonfmu3.enums import Fmi3Status @@ -26,17 +26,16 @@ def test_logger(tmp_path, debug_logging): message = "log message" log_calls = [ - ( - f"{status.name.upper()} - {debug} - {message}", - status, - category, - debug - ) for debug, status in itertools.product([True, False], Fmi3Status) + (f"{status.name.upper()} - {debug} - {message}", status, category, debug) + for debug, status in itertools.product([True, False], Fmi3Status) ] - fmu_calls = "\n".join([ - ' self.log("{}", Fmi3Status.{}, "{}", {})'.format(c[0], c[1].name, c[2], c[3]) for c in log_calls - ]) + fmu_calls = "\n".join( + [ + f' self.log("{c[0]}", Fmi3Status.{c[1].name}, "{c[2]}", {c[3]})' + for c in log_calls + ] + ) slave_code = f"""from pythonfmu3.Fmi3Slave import Fmi3Slave, Fmi3Status, Fmi3Causality, Int32, Float64, Boolean, String @@ -71,7 +70,7 @@ def do_step(self, current_time, step_size): stop_time=1e-3, output_interval=1e-3, logger=logger, - debug_logging=debug_logging + debug_logging=debug_logging, ) expected_calls = [ @@ -80,10 +79,11 @@ def do_step(self, current_time, step_size): bytes(name, encoding="utf-8"), int(c[1]), bytes(c[2], encoding="utf-8"), - bytes(c[0], encoding="utf-8") - ) for c in filter(lambda c: debug_logging or not c[3], log_calls) + bytes(c[0], encoding="utf-8"), + ) + for c in filter(lambda c: debug_logging or not c[3], log_calls) ] - + assert logger.call_count == len(Fmi3Status) * (1 + int(debug_logging)) logger.assert_has_calls(expected_calls) @@ -96,16 +96,16 @@ def test_log_categories(tmp_path, debug_logging, categories): message = "log message" log_calls = [ - ( - f"{status.name.upper()} - {debug} - {message}", - status, - debug - ) for debug, status in itertools.product([True, False], Fmi3Status) + (f"{status.name.upper()} - {debug} - {message}", status, debug) + for debug, status in itertools.product([True, False], Fmi3Status) ] - fmu_calls = "\n".join([ - ' self.log("{}", Fmi3Status.{}, None, {})'.format(c[0], c[1].name, c[2]) for c in log_calls - ]) + fmu_calls = "\n".join( + [ + f' self.log("{c[0]}", Fmi3Status.{c[1].name}, None, {c[2]})' + for c in log_calls + ] + ) slave_code = f"""from pythonfmu3.Fmi3Slave import Fmi3Slave, Fmi3Status, Fmi3Causality, Int32, Float64, Boolean, String @@ -148,7 +148,8 @@ def do_step(self, current_time, step_size): guid=model_description.guid, unzipDirectory=unzip_dir, modelIdentifier=model_description.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) # Instantiate the model model.instantiate(callbacks=callbacks) model.setDebugLogging(debug_logging, categories) @@ -156,7 +157,7 @@ def do_step(self, current_time, step_size): model.enterInitializationMode() model.exitInitializationMode() # Execute the model - model.doStep(0., 0.1) + model.doStep(0.0, 0.1) # Clean the model model.terminate() @@ -166,13 +167,15 @@ def do_step(self, current_time, step_size): if category not in Fmi3Slave.log_categories: category = "logAll" if len(categories) == 0 or category in categories: - expected_calls.append(call( - logger.call_args[0][0], # Don't test the first argument - b'instance1', - int(c[1]), - bytes(category, encoding="utf-8"), - bytes(c[0], encoding="utf-8") - )) + expected_calls.append( + call( + logger.call_args[0][0], # Don't test the first argument + b"instance1", + int(c[1]), + bytes(category, encoding="utf-8"), + bytes(c[0], encoding="utf-8"), + ) + ) n_calls = len(Fmi3Status) if len(categories) == 0 else len(categories) diff --git a/pythonfmu3/tests/test_multiple_fmus.py b/tests/test_multiple_fmus.py similarity index 92% rename from pythonfmu3/tests/test_multiple_fmus.py rename to tests/test_multiple_fmus.py index 93e621c..8b80e1a 100644 --- a/pythonfmu3/tests/test_multiple_fmus.py +++ b/tests/test_multiple_fmus.py @@ -1,4 +1,5 @@ import math + import pytest from pythonfmu3.builder import FmuBuilder @@ -68,21 +69,13 @@ def do_step(self, current_time, step_size): script1_file.parent.mkdir(parents=True, exist_ok=True) script1_file.write_text(slave1_code) - fmu1 = FmuBuilder.build_FMU( - script1_file, - dest=tmp_path, - needsExecutionTool="false" - ) + fmu1 = FmuBuilder.build_FMU(script1_file, dest=tmp_path, needsExecutionTool="false") assert fmu1.exists() script2_file = tmp_path / "orig" / "slave2.py" script2_file.write_text(slave2_code) - fmu2 = FmuBuilder.build_FMU( - script2_file, - dest=tmp_path, - needsExecutionTool="false" - ) + fmu2 = FmuBuilder.build_FMU(script2_file, dest=tmp_path, needsExecutionTool="false") assert fmu2.exists() md1 = fmpy.read_model_description(fmu1) @@ -92,7 +85,8 @@ def do_step(self, current_time, step_size): guid=md1.guid, unzipDirectory=unzip_dir, modelIdentifier=md1.coSimulation.modelIdentifier, - instanceName='instance1') + instanceName="instance1", + ) model1.instantiate() model1.enterInitializationMode() @@ -105,7 +99,8 @@ def do_step(self, current_time, step_size): guid=md2.guid, unzipDirectory=unzip_dir, modelIdentifier=md2.coSimulation.modelIdentifier, - instanceName='instance2') + instanceName="instance2", + ) variables1 = mapped(md1) variables2 = mapped(md2) @@ -126,7 +121,6 @@ def do_step(self, current_time, step_size): step_size = 0.025 while time < stop_time: - model2.setFloat64([realIn.valueReference], [value]) model1.doStep(time, step_size) diff --git a/pythonfmu3/tests/test_variables.py b/tests/test_variables.py similarity index 52% rename from pythonfmu3/tests/test_variables.py rename to tests/test_variables.py index 142b8a2..9908786 100644 --- a/pythonfmu3/tests/test_variables.py +++ b/tests/test_variables.py @@ -2,21 +2,34 @@ from random import randint import pytest -import ctypes - -from xml.etree import ElementTree from pythonfmu3 import Fmi3Slave from pythonfmu3.enums import Fmi3Causality, Fmi3Initial, Fmi3Variability -from pythonfmu3.variables import flatten, Boolean, Int32, UInt64, Float64, ModelVariable, String, Dimension +from pythonfmu3.variables import ( + Boolean, + Dimension, + Float64, + Int32, + ModelVariable, + String, + UInt64, + flatten, +) from .utils import PY2FMI, UInt64ValType -MODEL_VARIABLE_ATTRIBUTES = ["name", "valueReference", "description", "causality", "variability", "initial"] +MODEL_VARIABLE_ATTRIBUTES = [ + "name", + "valueReference", + "description", + "causality", + "variability", + "initial", +] def test_ModelVariable_reference_set_once_only(): - v = ModelVariable('variable') + v = ModelVariable("variable") v.value_reference = 22 with pytest.raises(RuntimeError): @@ -26,10 +39,13 @@ def test_ModelVariable_reference_set_once_only(): @pytest.mark.parametrize("causality", list(Fmi3Causality) + [None]) @pytest.mark.parametrize("initial", list(Fmi3Initial) + [None]) @pytest.mark.parametrize("variability", list(Fmi3Variability) + [None]) -@pytest.mark.parametrize("name, description", [ - ("var", None), - ("var", "description of var"), -]) +@pytest.mark.parametrize( + "name, description", + [ + ("var", None), + ("var", "description of var"), + ], +) def test_ModelVariable_constructor(causality, initial, variability, name, description): var = ModelVariable(name, causality, description, initial, variability) @@ -41,21 +57,25 @@ def test_ModelVariable_constructor(causality, initial, variability, name, descri assert var.variability == variability -@pytest.mark.parametrize("fmi_type,value", [ - (Boolean, False), - (Int32, 22), - (UInt64, UInt64ValType(23)), - (Float64, 2./3.), - (String, "hello_world"), -]) +@pytest.mark.parametrize( + "fmi_type,value", + [ + (Boolean, False), + (Int32, 22), + (UInt64, UInt64ValType(23)), + (Float64, 2.0 / 3.0), + (String, "hello_world"), + ], +) def test_ModelVariable_getter(fmi_type, value): class Slave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) self.var = [value] - self.register_variable(PY2FMI[type(value)]("var", getter=lambda: self.var[0])) + self.register_variable( + PY2FMI[type(value)]("var", getter=lambda: self.var[0]) + ) def do_step(self, t, dt): return True @@ -63,20 +83,25 @@ def do_step(self, t, dt): fmi_type_name = fmi_type.__qualname__.lower() slave = Slave(instance_name="slaveInstance") - assert [getattr(val, "value", val) for val in getattr(slave, f"get_{fmi_type_name}")([0])] == [value] - - -@pytest.mark.parametrize("fmi_type,value", [ - (Boolean, False), - (Int32, 22), - (UInt64, UInt64ValType(23)), - (Float64, 2./3.), - (String, "hello_world") -]) + assert [ + getattr(val, "value", val) + for val in getattr(slave, f"get_{fmi_type_name}")([0]) + ] == [value] + + +@pytest.mark.parametrize( + "fmi_type,value", + [ + (Boolean, False), + (Int32, 22), + (UInt64, UInt64ValType(23)), + (Float64, 2.0 / 3.0), + (String, "hello_world"), + ], +) def test_ModelVariable_setter(fmi_type, value): class Slave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) self.var = [None] @@ -84,7 +109,7 @@ def __init__(self, **kwargs): PY2FMI[type(value)]( "var", getter=lambda: self.var[0], - setter=lambda v: self.var.__setitem__(0, v) + setter=lambda v: self.var.__setitem__(0, v), ) ) @@ -95,23 +120,40 @@ def do_step(self, t, dt): fmi_type_name = fmi_type.__qualname__.lower() set_method = getattr(slave, f"set_{fmi_type_name}") - set_method([0, ], [value, ]) - assert [getattr(val, "value", val) for val in getattr(slave, f"get_{fmi_type_name}")([0])] == [value] - -@pytest.mark.requirements("numpy") -@pytest.mark.parametrize("fmi_type,value,dims", [ - (Float64, [1.,2.,3.,4.], [4]), - (Float64, [1.,2.,3.,4.], [2, 2]), -]) + set_method( + [ + 0, + ], + [ + value, + ], + ) + assert [ + getattr(val, "value", val) + for val in getattr(slave, f"get_{fmi_type_name}")([0]) + ] == [value] + + +@pytest.mark.requirements("numpy") +@pytest.mark.parametrize( + "fmi_type,value,dims", + [ + (Float64, [1.0, 2.0, 3.0, 4.0], [4]), + (Float64, [1.0, 2.0, 3.0, 4.0], [2, 2]), + ], +) def test_ModelVariable_getter_array(fmi_type, value, dims): class Slave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) self.var = value dimensions = [Dimension(start=val) for val in dims] - self.register_variable(PY2FMI[type(value[0])]("var", getter=lambda: self.var, dimensions=dimensions)) + self.register_variable( + PY2FMI[type(value[0])]( + "var", getter=lambda: self.var, dimensions=dimensions + ) + ) def do_step(self, t, dt): return True @@ -119,28 +161,33 @@ def do_step(self, t, dt): fmi_type_name = fmi_type.__qualname__.lower() slave = Slave(instance_name="slaveInstance") - assert [getattr(val, "value", val) for val in getattr(slave, f"get_{fmi_type_name}")([0])] == value - - -@pytest.mark.requirements("numpy") -@pytest.mark.parametrize("fmi_type,value,dims", [ - (Float64, [1.,2.,3.,4.], [4]), - (Float64, [1.,2.,3.,4.], [2, 2]), -]) + assert [ + getattr(val, "value", val) + for val in getattr(slave, f"get_{fmi_type_name}")([0]) + ] == value + + +@pytest.mark.requirements("numpy") +@pytest.mark.parametrize( + "fmi_type,value,dims", + [ + (Float64, [1.0, 2.0, 3.0, 4.0], [4]), + (Float64, [1.0, 2.0, 3.0, 4.0], [2, 2]), + ], +) def test_ModelVariable_setter_array(fmi_type, value, dims): class Slave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) - self.var = [None]*len(value) + self.var = [None] * len(value) dimensions = [Dimension(start=val) for val in dims] self.register_variable( PY2FMI[type(value[0])]( "var", getter=lambda: self.var, - setter=lambda v: setattr(self, 'var', v), - dimensions=dimensions + setter=lambda v: setattr(self, "var", v), + dimensions=dimensions, ) ) @@ -151,16 +198,28 @@ def do_step(self, t, dt): fmi_type_name = fmi_type.__qualname__.lower() set_method = getattr(slave, f"set_{fmi_type_name}") - set_method([0, ], value) - assert [getattr(val, "value", val) for val in getattr(slave, f"get_{fmi_type_name}")([0])] == value + set_method( + [ + 0, + ], + value, + ) + assert [ + getattr(val, "value", val) + for val in getattr(slave, f"get_{fmi_type_name}")([0]) + ] == value + @pytest.mark.parametrize("causality", list(Fmi3Causality) + [None]) @pytest.mark.parametrize("initial", list(Fmi3Initial) + [None]) @pytest.mark.parametrize("variability", list(Fmi3Variability) + [None]) -@pytest.mark.parametrize("name, description", [ - ("var", None), - ("var", "description of var"), -]) +@pytest.mark.parametrize( + "name, description", + [ + ("var", None), + ("var", "description of var"), + ], +) def test_ModelVariable_to_xml(causality, initial, variability, name, description): var = ModelVariable(name, causality, description, initial, variability) valueReference = randint(0, 25000) @@ -178,23 +237,32 @@ def test_ModelVariable_to_xml(causality, initial, variability, name, description assert node.attrib[attr] == str(value) -@pytest.mark.parametrize("var_type, value", [ - (Boolean, True), - (Int32, 23), - (UInt64, UInt64ValType(23)), - (Float64, 15.), - (String, "hello")]) +@pytest.mark.parametrize( + "var_type, value", + [ + (Boolean, True), + (Int32, 23), + (UInt64, UInt64ValType(23)), + (Float64, 15.0), + (String, "hello"), + ], +) @pytest.mark.parametrize("causality", list(Fmi3Causality) + [None]) @pytest.mark.parametrize("initial", list(Fmi3Initial) + [None]) @pytest.mark.parametrize("variability", list(Fmi3Variability) + [None]) def test_ModelVariable_start(var_type, value, causality, initial, variability): - var_obj = var_type("var", causality=causality, description="a variable", initial=initial, variability=variability) + var_obj = var_type( + "var", + causality=causality, + description="a variable", + initial=initial, + variability=variability, + ) class PySlave(Fmi3Slave): - def __init__(self, **kwargs): super().__init__(**kwargs) - setattr(self, "var", value) + self.var = value self.register_variable(var_obj) def do_step(self, current_time: float, step_size: float): @@ -213,123 +281,160 @@ def do_step(self, current_time: float, step_size: float): assert var_obj.start is None -@pytest.mark.parametrize("name,start", [ - ("boolean_name", None), - ("boolean_another_name", False), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("boolean_name", None), + ("boolean_another_name", False), + ], +) def test_Boolean_constructor(name, start): r = Boolean(name, start) assert r.start == start -@pytest.mark.parametrize("name,start", [ - ("boolean_name", None), - ("boolean_another_name", True), - ("boolean_yet_another", False), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("boolean_name", None), + ("boolean_another_name", True), + ("boolean_yet_another", False), + ], +) def test_Boolean_to_xml(name, start): r = Boolean(name, start) xml = r.to_xml() if start is not None: - assert xml.attrib['start'] == str(start).lower() + assert xml.attrib["start"] == str(start).lower() -@pytest.mark.parametrize("name,start", [ - ("integer_name", None), - ("integer_another_name", 42), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("integer_name", None), + ("integer_another_name", 42), + ], +) def test_Integer_constructor(name, start): r = Int32(name, start) assert r.start == start -@pytest.mark.parametrize("name,start", [ - ("integer_name", None), - ("integer_another_name", 42), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("integer_name", None), + ("integer_another_name", 42), + ], +) def test_Integer_to_xml(name, start): r = Int32(name, start) xml = r.to_xml() if start is not None: - assert xml.attrib['start'] == str(start) + assert xml.attrib["start"] == str(start) -@pytest.mark.parametrize("name,start", [ - ("real_name", None), - ("real_another_name", 22.), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("real_name", None), + ("real_another_name", 22.0), + ], +) def test_Real_constructor(name, start): r = Float64(name, start) assert r.start == start -@pytest.mark.parametrize("name,start", [ - ("real_name", None), - ("real_another_name", 22.), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("real_name", None), + ("real_another_name", 22.0), + ], +) def test_Real_to_xml(name, start): r = Float64(name, start) xml = r.to_xml() if start is not None: - assert xml.attrib['start'] == f"{start:.16g}" + assert xml.attrib["start"] == f"{start:.16g}" -@pytest.mark.parametrize("name,start", [ - ("string_name", None), - ("string_another_name", "dummy"), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("string_name", None), + ("string_another_name", "dummy"), + ], +) def test_String_constructor(name, start): r = String(name, start) assert r.start == start -@pytest.mark.parametrize("name,start", [ - ("string_name", None), - ("string_another_name", "dummy"), -]) +@pytest.mark.parametrize( + "name,start", + [ + ("string_name", None), + ("string_another_name", "dummy"), + ], +) def test_String_to_xml(name, start): r = String(name, start) xml = r.to_xml() if start is not None: - start_elements = xml.findall('.//Start') + start_elements = xml.findall(".//Start") assert len(start_elements) == 1 - assert start_elements[0].attrib['value'] == str(start) + assert start_elements[0].attrib["value"] == str(start) -@pytest.mark.requirements("numpy") -@pytest.mark.parametrize("name,start,dims", [ - ("array1", [1.,2.,3.,4.], [4]), - ("array2", [1.,2.,3.,4.], [2, 2]) -]) +@pytest.mark.requirements("numpy") +@pytest.mark.parametrize( + "name,start,dims", + [("array1", [1.0, 2.0, 3.0, 4.0], [4]), ("array2", [1.0, 2.0, 3.0, 4.0], [2, 2])], +) def test_array_to_xml(name, start, dims): r = Float64(name, start, dimensions=[Dimension(start=val) for val in dims]) xml = r.to_xml() if start is not None: - assert xml.attrib['start'] == " ".join([f"{val:.16g}" for val in flatten(start)]) + assert xml.attrib["start"] == " ".join( + [f"{val:.16g}" for val in flatten(start)] + ) if dims is not None: - xml_dims = xml.findall('.//Dimension') + xml_dims = xml.findall(".//Dimension") assert len(xml_dims) == len(dims) - assert [xml_dim.attrib['start'] for xml_dim in xml_dims] == dims - - -@pytest.mark.requirements("numpy") -@pytest.mark.parametrize("name,start,dims", [ - ("array1", [1.,2.,3.,4.], [4]), - ("array2", [1.,2.,3.,4.], [2, 2]), - ("array3", [1.,2.,3.,4.,5.,6.,7.,8.], [2, 2, 2]) -]) + assert [xml_dim.attrib["start"] for xml_dim in xml_dims] == dims + + +@pytest.mark.requirements("numpy") +@pytest.mark.parametrize( + "name,start,dims", + [ + ("array1", [1.0, 2.0, 3.0, 4.0], [4]), + ("array2", [1.0, 2.0, 3.0, 4.0], [2, 2]), + ("array3", [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], [2, 2, 2]), + ], +) def test_array_output_to_xml(name, start, dims): import numpy as np - start = np.reshape(start, newshape=dims) - r = Float64(name, start, dimensions=[Dimension(start=val) for val in dims], causality=Fmi3Causality.output) + + start = np.reshape(start, newshape=dims) + r = Float64( + name, + start, + dimensions=[Dimension(start=val) for val in dims], + causality=Fmi3Causality.output, + ) xml = r.to_xml() if start is not None: - assert xml.attrib['start'] == " ".join([f"{val:.16g}" for val in flatten(start)]) + assert xml.attrib["start"] == " ".join( + [f"{val:.16g}" for val in flatten(start)] + ) if dims is not None: - xml_dims = xml.findall('.//Dimension') + xml_dims = xml.findall(".//Dimension") assert len(xml_dims) == len(dims) - assert [xml_dim.attrib['start'] for xml_dim in xml_dims] == dims \ No newline at end of file + assert [xml_dim.attrib["start"] for xml_dim in xml_dims] == dims diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..6852c26 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,17 @@ +from pythonfmu3.variables import Boolean, Float64, Int32, String, UInt64 + + +class UInt64ValType(int): + pass + + +FMI2PY = dict( + ( + (Boolean, bool), + (Int32, int), + (Float64, float), + (String, str), + (UInt64, UInt64ValType), + ) +) +PY2FMI = dict([(v, k) for k, v in FMI2PY.items()]) From f8cb221349c24813842096b1a207861358d7afd0 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Tue, 4 Aug 2026 10:43:17 +0200 Subject: [PATCH 02/20] Fixed tests so that ============================= test session starts ============================== platform linux -- Python 3.13.12, pytest-9.1.1, pluggy-1.6.0 rootdir: /home/se/DNV/osp/packages/PythonFMU3 configfile: pyproject.toml plugins: cov-7.1.0 collected 1867 items / 1 skipped tests/test_builder.py ........... [ 0%] tests/test_deploy.py ........................ [ 1%] tests/test_docs.py ... [ 2%] tests/test_fmi3slave.py ................................................ [ 4%] [ 4%] tests/test_integration.py sssssssssssssss [ 5%] tests/test_multiple_fmus.py s [ 5%] tests/test_variables.py ................................................ [ 8%] ........................................................................ [ 11%] ........................................................................ [ 15%] ........................................................................ [ 19%] ........................................................................ [ 23%] ........................................................................ [ 27%] ........................................................................ [ 31%] ........................................................................ [ 35%] ........................................................................ [ 38%] ........................................................................ [ 42%] ........................................................................ [ 46%] ........................................................................ [ 50%] ........................................................................ [ 54%] ........................................................................ [ 58%] ........................................................................ [ 62%] ........................................................................ [ 65%] ........................................................................ [ 69%] ........................................................................ [ 73%] ........................................................................ [ 77%] ........................................................................ [ 81%] ........................................................................ [ 85%] ........................................................................ [ 89%] ........................................................................ [ 92%] ........................................................................ [ 96%] ............................................................. [100%] ======================= 1851 passed, 17 skipped in 1.96s ======================= passes all 1851 tests --- pyproject.toml | 7 ++++++- tests/test_docs.py | 12 ++++++------ tests/test_variables.py | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 612f1a6..d175831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,5 +92,10 @@ required-environments = [ # "sys_platform == 'darwin'", ] - +[tool.pytest] +addopts = ["--strict-markers"] +markers = [ + "integration: marks integration tests", + "doc: marks documentation tests" +] diff --git a/tests/test_docs.py b/tests/test_docs.py index 7a2f2f8..d0dc7a9 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -25,8 +25,8 @@ def extract_python_code_from_md(md_file): @pytest.mark.doc def test_readme(): - test_dir = pathlib.Path(__file__).parent - fpath = test_dir.parent.parent / "README.md" + test_dir = pathlib.Path(__file__).parent.resolve() + fpath = test_dir.parent / "README.md" # extract Python code code_blocks = extract_python_code_from_md(fpath) @@ -45,8 +45,8 @@ def test_readme(): @pytest.mark.doc def test_cosimulation_examples(): """Test that all Python examples in usage.md actually work""" - test_dir = pathlib.Path(__file__).parent - usage_file = test_dir.parent.parent / "docs" / "usage.md" + test_dir = pathlib.Path(__file__).parent.resolve() + usage_file = test_dir.parent / "docs" / "usage.md" # Extract Python code code_blocks = extract_python_code_from_md(usage_file) @@ -74,8 +74,8 @@ def test_cosimulation_examples(): @pytest.mark.doc def test_model_exchange_examples(): """Test Model Exchange examples""" - test_dir = pathlib.Path(__file__).parent - mx_file = test_dir.parent.parent / "docs" / "usageMX.md" + test_dir = pathlib.Path(__file__).parent.resolve() + mx_file = test_dir.parent / "docs" / "usageMX.md" code_blocks = extract_python_code_from_md(mx_file) diff --git a/tests/test_variables.py b/tests/test_variables.py index 9908786..3d47924 100644 --- a/tests/test_variables.py +++ b/tests/test_variables.py @@ -422,7 +422,7 @@ def test_array_to_xml(name, start, dims): def test_array_output_to_xml(name, start, dims): import numpy as np - start = np.reshape(start, newshape=dims) + start = np.reshape(start, shape=dims) r = Float64( name, start, From c042b82594f26ea139fefdc987498e1925746ee5 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Tue, 4 Aug 2026 13:09:49 +0200 Subject: [PATCH 03/20] Added the PID-controller example to /examples. Needed fixes. Changed therefore docs/usage.md and adapted the documentation build process (adding conf.py, /_static and /_templates) --- docs/_static/PythonFMU3.svg | 56 +++++++++++++++++++++ docs/_templates/custom-class.rst | 30 +++++++++++ docs/_templates/custom-module.rst | 67 +++++++++++++++++++++++++ docs/conf.py | 83 +++++++++++++++++++++++++++++++ docs/usage.md | 28 ++++------- examples/pid_fmu.py | 77 ++++++++++++++++++++++++++++ examples/pid_use.py | 39 +++++++++++++++ 7 files changed, 361 insertions(+), 19 deletions(-) create mode 100755 docs/_static/PythonFMU3.svg create mode 100755 docs/_templates/custom-class.rst create mode 100755 docs/_templates/custom-module.rst create mode 100755 docs/conf.py create mode 100644 examples/pid_fmu.py create mode 100644 examples/pid_use.py diff --git a/docs/_static/PythonFMU3.svg b/docs/_static/PythonFMU3.svg new file mode 100755 index 0000000..44c0465 --- /dev/null +++ b/docs/_static/PythonFMU3.svg @@ -0,0 +1,56 @@ + +PythonFMU3 diff --git a/docs/_templates/custom-class.rst b/docs/_templates/custom-class.rst new file mode 100755 index 0000000..2d7c003 --- /dev/null +++ b/docs/_templates/custom-class.rst @@ -0,0 +1,30 @@ +{{ name | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :show-inheritance: + + + {% block methods %} + .. automethod:: __init__ + {% if methods %} + .. rubric:: {{ _('Methods') }} + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/_templates/custom-module.rst b/docs/_templates/custom-module.rst new file mode 100755 index 0000000..fc1123b --- /dev/null +++ b/docs/_templates/custom-module.rst @@ -0,0 +1,67 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + :members: + {%- if classes %} + :exclude-members: {% for item in classes %}{{ item }}{{','}}{%- endfor %} + {% endif %} + + + {% block attributes %} + {%- if attributes %} + .. rubric:: {{ _('Module Attributes') }} + .. autosummary:: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {%- endblock %} + + + {%- block functions %} + {%- if functions %} + .. rubric:: {{ _('Functions') }} + .. autosummary:: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {%- endblock %} + + + {%- block classes %} + {%- if classes %} + .. rubric:: {{ _('Classes') }} + .. autosummary:: + :toctree: + :template: custom-class.rst + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {%- endblock %} + + + {%- block exceptions %} + {%- if exceptions %} + .. rubric:: {{ _('Exceptions') }} + .. autosummary:: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {%- endblock %} + + +{%- block modules %} +{%- if modules %} +.. rubric:: Modules +.. autosummary:: + :toctree: + :template: custom-module.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{%- endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..8f75f52 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,83 @@ +# mypy: ignore-errors + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import sys +from pathlib import Path + +sys.path.insert(0, str(Path("../../src").absolute())) + + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "PythonFMU3" +author = "Lars Hatledal, Stephen Smith and Siegfried Eisinger" + +# The full version, including alpha/beta/rc tags +release = "0.4.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_argparse_cli", + "sphinx.ext.mathjax", + "sphinx.ext.autosummary", + "sphinx.ext.todo", + "sphinxcontrib.mermaid", +] + +# Extenstion for myst_parser +myst_enable_extensions = [ + "dollarmath", + "attrs_inline", +] + +# The file extensions of source files. +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_title = f"PythonFMU3 {release}" +html_theme = "furo" +html_static_path = ["_static"] +html_logo = "_static/PythonFMU3.svg" +autodoc_default_options = { + "member-order": "groupwise", + "undoc-members": True, + "exclude-members": "__weakref__", +} +autodoc_preserve_defaults = True + +myst_heading_anchors = 3 + +todo_include_todos = False + +# add markdown mermaid support +myst_fence_as_directive = ["mermaid"] diff --git a/docs/usage.md b/docs/usage.md index 9396b73..9318375 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -52,7 +52,6 @@ self.register_variable( ```python from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64 - class PIDController(Fmi3Slave): """PID Controller implementation""" @@ -67,12 +66,12 @@ class PIDController(Fmi3Slave): self.measurement = 0.0 # Outputs - self.control_output = 0.0 + self.output = 0.0 # Parameters self.kp = 1.0 # Proportional gain self.ki = 0.1 # Integral gain - self.kd = 0.01 # Derivative gain + self.kd = 0.01 # Derivative gain # Internal states self.integral_error = 0.0 @@ -80,22 +79,13 @@ class PIDController(Fmi3Slave): self.first_step = True # Register variables - self.register_variable( - Float64("setpoint", causality=Fmi3Causality.input, start=0.0) - ) - self.register_variable( - Float64("measurement", causality=Fmi3Causality.input, start=0.0) - ) + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self.register_variable(Float64("setpoint", causality=Fmi3Causality.input, start=0.0)) + self.register_variable(Float64("measurement", causality=Fmi3Causality.input, start=0.0)) self.register_variable(Float64("output", causality=Fmi3Causality.output)) - self.register_variable( - Float64("Kp", causality=Fmi3Causality.parameter, start=1.0) - ) - self.register_variable( - Float64("Ki", causality=Fmi3Causality.parameter, start=0.1) - ) - self.register_variable( - Float64("Kd", causality=Fmi3Causality.parameter, start=0.01) - ) + self.register_variable(Float64("kp", causality=Fmi3Causality.parameter, start=1.0)) + self.register_variable(Float64("ki", causality=Fmi3Causality.parameter, start=0.1)) + self.register_variable(Float64("kd", causality=Fmi3Causality.parameter, start=0.01)) def do_step(self, current_time, step_size): # Calculate error @@ -116,7 +106,7 @@ class PIDController(Fmi3Slave): d_term = self.kd * (error - self.previous_error) / step_size # PID output - self.control_output = p_term + i_term + d_term + self.output = p_term + i_term + d_term # Store error for next iteration self.previous_error = error diff --git a/examples/pid_fmu.py b/examples/pid_fmu.py new file mode 100644 index 0000000..597599d --- /dev/null +++ b/examples/pid_fmu.py @@ -0,0 +1,77 @@ +from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64 + +class PIDController(Fmi3Slave): + """PID Controller implementation""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "PythonFMU3 Team" + self.description = "PID Controller" + + # Inputs + self.setpoint = 0.0 + self.measurement = 0.0 + + # Outputs + self.output = 0.0 + + # Parameters + self.kp = 1.0 # Proportional gain + self.ki = 0.1 # Integral gain + self.kd = 0.01 # Derivative gain + + # Internal states + self.integral_error = 0.0 + self.previous_error = 0.0 + self.first_step = True + + # Register variables + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self.register_variable(Float64("setpoint", causality=Fmi3Causality.input, start=0.0)) + self.register_variable(Float64("measurement", causality=Fmi3Causality.input, start=0.0)) + self.register_variable(Float64("output", causality=Fmi3Causality.output)) + self.register_variable(Float64("kp", causality=Fmi3Causality.parameter, start=1.0)) + self.register_variable(Float64("ki", causality=Fmi3Causality.parameter, start=0.1)) + self.register_variable(Float64("kd", causality=Fmi3Causality.parameter, start=0.01)) + + def do_step(self, current_time, step_size): + # Calculate error + error = self.setpoint - self.measurement + + # Proportional term + p_term = self.kp * error + + # Integral term + self.integral_error += error * step_size + i_term = self.ki * self.integral_error + + # Derivative term + if self.first_step: + d_term = 0.0 + self.first_step = False + else: + d_term = self.kd * (error - self.previous_error) / step_size + + # PID output + self.output = p_term + i_term + d_term + + # Store error for next iteration + self.previous_error = error + + return True + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") \ No newline at end of file diff --git a/examples/pid_use.py b/examples/pid_use.py new file mode 100644 index 0000000..955738d --- /dev/null +++ b/examples/pid_use.py @@ -0,0 +1,39 @@ +# Example of co-simulation setup +import fmpy +from fmpy import simulate_fmu +import matplotlib.pyplot as plt +import numpy as np + +# Simulate the PID controller FMU +result = simulate_fmu( + filename='PIDController.fmu', + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'kp': 2.0, + 'ki': 0.5, + 'kd': 0.1, + 'setpoint': 1.0 + }, + input= np.array( [(0.0, 0.0, 1.0), (1.0, 0.3, 1.0), (5.0, 0.8, 1.0), (10.0, 1.0, 1.0)], + dtype=[('time', np.double),('measurement', np.double),('setpoint', np.double)]), + output = ['output','setpoint','measurement'], +) + +# Plot results +plt.figure(figsize=(12, 6)) +plt.subplot(2, 1, 1) +plt.plot(result['time'], result['setpoint'], label='Setpoint') +plt.plot(result['time'], result['measurement'], label='Measurement') +plt.ylabel('Value') +plt.legend() +plt.grid(True) + +plt.subplot(2, 1, 2) +plt.plot(result['time'], result['output'], label='Control Output') +plt.xlabel('Time [s]') +plt.ylabel('Control Signal') +plt.legend() +plt.grid(True) +plt.show() \ No newline at end of file From 914b037bf58a51addaa475a3864e6ae7e3a12342 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Thu, 6 Aug 2026 08:55:49 +0200 Subject: [PATCH 04/20] Added the implementation of EnterEventMode fmi3 function to the Cpp code. Successfull compilation. Added a basic discrete event example. --- examples/two_state.py | 75 +++++++++++++++++++ examples/two_state_use.py | 36 +++++++++ .../pythonfmu-export/src/cppfmu/cppfmu_cs.cpp | 5 ++ .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 4 +- .../src/pythonfmu/PySlaveInstance.cpp | 12 +++ .../src/pythonfmu/PySlaveInstance.hpp | 3 +- 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 examples/two_state.py create mode 100644 examples/two_state_use.py diff --git a/examples/two_state.py b/examples/two_state.py new file mode 100644 index 0000000..1d186f8 --- /dev/null +++ b/examples/two_state.py @@ -0,0 +1,75 @@ +"""Implements a two-state discrete event system with states `working=1` and 'failed=0` + The MTTF and MTTR are fixed parameters and the actual TTF is exponentially distributed, + while TTR is modelled as constant time interval. + + When `enterEventMode` is called, the state changes and a new time interval is calculated, + which is transmitted to the orchestrator with a `getIntervalDecimal` call. +""" +import numpy as np +from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64, Int32 + +class TwoState(Fmi3Slave): + """Two-state discrete event implementation""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Siegfried Eisinger" + self.description = "Two-state discrete event system." + + # Parameters + self.mttf = 1.0 # Mean time to failure (ttf exponentially distributed) + self.mttr = 0.01 # Mean time to repair (ttr constant) + self.seed = 1 + + # Inputs + + # Outputs + self.state = 1 + + # Internal states + self.rng = np.random.default_rng(self.seed) + self.next_interval = self.get_next_interval() + self.time = 0.0 + self.histogram : list[ tuple[float,int]] + + # Register variables + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self.register_variable(Float64("mttf", causality=Fmi3Causality.parameter, start=1.0)) + self.register_variable(Float64("mttr", causality=Fmi3Causality.parameter, start=0.01)) + self.register_variable(Int32("seed", causality=Fmi3Causality.parameter, start=1)) + self.register_variable(Int32("state", causality=Fmi3Causality.output)) + + def get_next_interval(self): + if self.state==0: + return self.mttr + elif self.state==1: + return self.rng.exponential(self.mttf) + else: + raise ValueError(f"Unallowed state {self.state}") from None + + def enter_event_mode(self): + """Signal from orchestrator that an event has occured.""" + self.state = 1 - self.state + self.next_interval = self.get_next_interval() + + def do_step(self, current_time, step_size): + self.time = current_time + self.histogram.append( (current_time, self.state)) + + return True + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") \ No newline at end of file diff --git a/examples/two_state_use.py b/examples/two_state_use.py new file mode 100644 index 0000000..5e9eeb9 --- /dev/null +++ b/examples/two_state_use.py @@ -0,0 +1,36 @@ +# Example of co-simulation setup +import fmpy +from fmpy import simulate_fmu +import matplotlib.pyplot as plt +import numpy as np + +# Simulate the PID controller FMU +result = simulate_fmu( + filename='TwoState.fmu', + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'mttf': 1.0, + 'mttr': 0.01, + 'seed': 1, + }, + output = ['state'], +) + +# Plot results +plt.figure(figsize=(12, 6)) +plt.subplot(2, 1, 1) +plt.plot(result['time'], result['state'], label='State') +#plt.plot(result['time'], result['measurement'], label='Measurement') +plt.ylabel('Value') +plt.legend() +plt.grid(True) + +plt.subplot(2, 1, 2) +plt.plot(result['time'], result['state'], label='State') +plt.xlabel('Time [s]') +plt.ylabel('Control Signal') +plt.legend() +plt.grid(True) +plt.show() \ No newline at end of file diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp index ecc0855..b594b42 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp @@ -50,6 +50,11 @@ void SlaveInstance::Reset() // Do nothing } +void SlaveInstance::EnterEventMode() +{ + // Do nothing +} + void SlaveInstance::SetFloat64( const FMIValueReference /*vr*/[], diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 61bc2cb..436a529 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -68,6 +68,8 @@ class SlaveInstance /* Called from fmi3SetXxx()/fmiSetXxx(). * Throws std::logic_error by default. */ + virtual void EnterEventMode(); + virtual void SetFloat64( const FMIValueReference vr[], std::size_t nvr, @@ -159,7 +161,7 @@ class SlaveInstance cppfmu::FMIBoolean* valuesOfContinuousStatesChanged, cppfmu::FMIBoolean* nextEventTimeDefined, cppfmu::FMIFloat64* nextEventTime) = 0; - + virtual void GetFMUstate(fmi3FMUState& state) = 0; virtual void SetFMUstate(const fmi3FMUState& state) = 0; diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index 59508be..656ca84 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -262,6 +262,18 @@ void PySlaveInstance::Terminate() }); } +void PySlaveInstance::EnterEventMode() +{ + py_safe_run([this](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "enter_event_mode", nullptr); + if (f == nullptr) { + handle_py_exception("[enterEventMode] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + void PySlaveInstance::SetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* values, std::size_t nValues) { py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 609ca61..4fa9f2b 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -25,6 +25,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance void ExitInitializationMode() override; void Terminate() override; void Reset() override; + void EnterEventMode() override; cppfmu::FMIStatus DoStep(cppfmu::FMIFloat64 currentCommunicationPoint, cppfmu::FMIFloat64 communicationStepSize, cppfmu::FMIBoolean newStep, @@ -50,7 +51,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance void GetFMUstate(fmi3FMUState& State) override; void SetFMUstate(const fmi3FMUState& State) override; void FreeFMUstate(fmi3FMUState& State) override; - + // Model Exchange methods void GetContinuousStates(cppfmu::FMIFloat64* continuousStates, std::size_t nStates) const override; void GetNumberOfContinuousStates(std::size_t& nStates) const override; From 4e078bbb33dcfea54b731b6dff720af72a69494f Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Thu, 6 Aug 2026 09:28:29 +0200 Subject: [PATCH 05/20] Updated the documentation of ModelVariables class and added since this is not only in use for ME, but also for CS in event mode. It is set to True per default as per FMI spec --- src/pythonfmu3/variable_types.py | 1 + src/pythonfmu3/variables.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pythonfmu3/variable_types.py b/src/pythonfmu3/variable_types.py index 124d1a3..dd5287f 100644 --- a/src/pythonfmu3/variable_types.py +++ b/src/pythonfmu3/variable_types.py @@ -36,6 +36,7 @@ def __init__(self, name, **kwargs): class Item: + """Helper class for EnumerationType definitions.""" tag = "Item" def __init__(self, name: str, value: int, description: str = "", **kwargs): diff --git a/src/pythonfmu3/variables.py b/src/pythonfmu3/variables.py index 0beb9bd..7cb92eb 100644 --- a/src/pythonfmu3/variables.py +++ b/src/pythonfmu3/variables.py @@ -40,6 +40,10 @@ class ModelVariable(ABC): description (str, optional): Variable description initial (:obj:`Fmi3Initial`, optional): Variable initial status variability (:obj:`Fmi3Variability`, optional): Variable variability + declared_type: optional reference to a previously declared type (see VariableType) + getter: optional explicit definition of a getter function + setter: optional explicit definition of a setter function + canHandleMultipleSetPerTimeInstant: boolean flag. See FMI spec. """ def __init__( @@ -52,6 +56,7 @@ def __init__( declared_type: str | None = None, getter: Any = None, setter: Any = None, + can_handle_multiple_set_per_time_instant: bool = True ): self.getter = getter self.setter = setter @@ -69,7 +74,7 @@ def __init__( "variability": variability, "initial": initial, "declaredType": declared_type, - # 'canHandleMultipleSetPerTimeInstant': # Only for ME + "canHandleMultipleSetPerTimeInstant": can_handle_multiple_set_per_time_instant, } self._extras = {} From 65ab30db87dcff884ddacbdb0b99f63776c1cc8b Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Thu, 6 Aug 2026 12:51:54 +0200 Subject: [PATCH 06/20] Added a variable type 'Clock' to ModelVariables. Added some documentation also to other variable types and added ModelVariables.to_str as formatter for python types to xml strings --- examples/two_state.py | 8 +++- src/pythonfmu3/__init__.py | 3 +- src/pythonfmu3/enums.py | 8 ++++ src/pythonfmu3/variables.py | 88 ++++++++++++++++++++++++++++++++----- 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/examples/two_state.py b/examples/two_state.py index 1d186f8..9cd8976 100644 --- a/examples/two_state.py +++ b/examples/two_state.py @@ -6,7 +6,9 @@ which is transmitted to the orchestrator with a `getIntervalDecimal` call. """ import numpy as np -from pythonfmu3 import Fmi3Causality, Fmi3Slave, Float64, Int32 +from pythonfmu3 import Fmi3Slave +from pythonfmu3.enums import Fmi3IntervalVariability, Fmi3Causality +from pythonfmu3.variables import Float64, Int32, Clock class TwoState(Fmi3Slave): """Two-state discrete event implementation""" @@ -39,6 +41,10 @@ def __init__(self, **kwargs): self.register_variable(Float64("mttr", causality=Fmi3Causality.parameter, start=0.01)) self.register_variable(Int32("seed", causality=Fmi3Causality.parameter, start=1)) self.register_variable(Int32("state", causality=Fmi3Causality.output)) + self.register_variable(Clock("next_interval", + interval_variability=Fmi3IntervalVariability.triggered, + + )) def get_next_interval(self): if self.state==0: diff --git a/src/pythonfmu3/__init__.py b/src/pythonfmu3/__init__.py index 265c23b..273753d 100644 --- a/src/pythonfmu3/__init__.py +++ b/src/pythonfmu3/__init__.py @@ -2,7 +2,7 @@ from .builder import FmuBuilder from .cosimulation import CoSimulation from .default_experiment import DefaultExperiment -from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability +from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability, Fmi3IntervalVariability from .fmi3slave import Fmi3Slave, Fmi3SlaveBase, Fmi3StepResult from .modelexchange import Fmi3UpdateDiscreteStatesResult, ModelExchange from .unit import BaseUnit, Unit @@ -16,4 +16,5 @@ Int64, String, UInt64, + Clock, ) diff --git a/src/pythonfmu3/enums.py b/src/pythonfmu3/enums.py index cd178a8..302b336 100644 --- a/src/pythonfmu3/enums.py +++ b/src/pythonfmu3/enums.py @@ -34,6 +34,14 @@ class Fmi3Variability(Enum): discrete = 3 continuous = 4 +class Fmi3IntervalVariability(Enum): + constant = 0 + fixed = 1 + tunable = 2 + changing = 3 + countdown = 4 + triggered = 5 + class Fmi3Status(IntEnum): ok = 0 diff --git a/src/pythonfmu3/variables.py b/src/pythonfmu3/variables.py index 7cb92eb..a2d01c7 100644 --- a/src/pythonfmu3/variables.py +++ b/src/pythonfmu3/variables.py @@ -9,7 +9,7 @@ from typing import Any from xml.etree.ElementTree import Element -from .enums import Fmi3Causality, Fmi3Initial, Fmi3Variability +from .enums import Fmi3Causality, Fmi3Initial, Fmi3Variability, Fmi3IntervalVariability MAX_LENGTH = 1000 @@ -149,8 +149,20 @@ def to_xml(self) -> Element: attrib = dict() for key, value in ChainMap(self._extras, self.__attrs).items(): if value is not None: - attrib[key] = str(value.name if isinstance(value, Enum) else value) + attrib[key] = ModelVariable.to_str(value) return Element(self._type, attrib) + + @staticmethod + def to_str(val:Any): + """Helper method to translate python values to proper strings for use in xml.""" + if isinstance(val, bool): + return str(val).lower() + elif isinstance(val, float): + return f"{val:.16g}" + elif isinstance(val, Enum): + return val.name + else: + return str(val) def __repr__(self) -> str: return ( @@ -167,7 +179,7 @@ def __init__(self, startValue): def to_xml(self) -> Element: attrib = dict() - attrib["value"] = self.value + attrib["value"] = ModelVariable.to_str(self.value) return Element("Start", attrib) @@ -216,7 +228,9 @@ def __init__(self, dimensions: list[Dimension] = [], **kwargs): def dimensions(self) -> list[Dimension]: return self._dimensions - def get_start_str(self, value, formatter): + def get_start_str(self, value, formatter=None): + if formatter is None: + formatter = ModelVariable.to_str if len(self.dimensions) > 0: output = " ".join([formatter(val) for val in flatten(value)]) if len(output) > MAX_LENGTH: @@ -274,7 +288,7 @@ def to_xml(self) -> Element: if value is not None: # In order to not loose precision, a number of this type should be # stored on an XML file with at least 16 significant digits - output = self.get_start_str(value, lambda v: f"{v:.16g}") + output = self.get_start_str(value) attrib[key] = output if self.unit: @@ -313,7 +327,7 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = self.get_start_str(value, lambda v: str(v)) + attrib[key] = self.get_start_str(value) self._extras = attrib parent = super().to_xml() @@ -347,7 +361,7 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = self.get_start_str(value, lambda v: str(v)) + attrib[key] = self.get_start_str(value) self._extras = attrib parent = super().to_xml() @@ -381,7 +395,7 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = self.get_start_str(value, lambda v: str(v)) + attrib[key] = self.get_start_str(value) self._extras = attrib parent = super().to_xml() @@ -415,7 +429,7 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = self.get_start_str(value, lambda v: str(v).lower()) + attrib[key] = self.get_start_str(value) self._extras = attrib parent = super().to_xml() @@ -449,7 +463,7 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value, lambda v: str(v)) + attrib[key] = str(value) self._extras = attrib parent = super().to_xml() @@ -458,6 +472,60 @@ def to_xml(self) -> Element: return parent +class Clock(ModelVariable): + """Clock variable definition. See also fmi3Variable.xsd and fmi3AttributeGroups.xsd/fmi3ClockAttributes + + Args: + name: name of the clock unique within FMU + can_be_deactivated: boolean flag + priority: optional explicit priority as unsigned int + interval_variability: required enumeration attribute, similar to variability + interval_decimal: optional initial specification of decimal interval + field + shift_decimal: optional delay of first clock activations. Default 0 + supports_fraction: optional possibility to provide shifts as integer fractions + resolution: optional initial denominator of interval fraction + interval_counter: optional initial nominator of interval fraction + shift_counter: optional initial nominator of shift (first clock activation) + """ + def __init__( + self, + name: str, + can_be_deactivated: bool = False, + priority: int|None = None, + interval_variability: Fmi3IntervalVariability = Fmi3IntervalVariability.constant, + interval_decimal: float|None = None, + shift_decimal: float = 0.0, + supports_fraction: bool = False, + resolution: int|None = None, + interval_counter: int|None = None, + shift_counter: int = 0, + **kwargs, + ): + ModelVariable.__init__(self, name, **kwargs) + self.__attrs = {'canBeDeactivated': can_be_deactivated, + 'priority': priority, + 'intervalVariability': interval_variability, + 'intervalDecimal': interval_decimal, + 'shiftDecimal': shift_decimal, + 'supportsFraction': supports_fraction, + } + if supports_fraction: + self.__attrs.update( {'resolution': resolution, + 'intervalCounter': interval_counter, + 'shiftCounter': shift_counter, + }) + self._type = "Clock" + + def to_xml(self) -> Element: + attrib = dict() + for key, value in self.__attrs.items(): + if value is not None: + attrib[key] = ModelVariable.to_str(value) + self._extras = attrib + parent = super().to_xml() + + return parent + class Enumeration(ModelVariable, Arrayable): def __init__( From c3673ee9af33d8bf4a86f54cc61a574ae473152d Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Sun, 9 Aug 2026 18:39:52 +0200 Subject: [PATCH 07/20] Implemented all event-related functions in cppfmu/ and pythonfmu/. Code compiles, but is so far not yet tested. --- examples/two_state.py | 2 +- .../src/cppfmu/cppfmu_common.hpp | 6 + .../pythonfmu-export/src/cppfmu/cppfmu_cs.cpp | 97 +++++- .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 36 +- .../src/cppfmu/fmi_functions.cpp | 289 +++++++++++----- .../src/pythonfmu/PySlaveInstance.cpp | 325 +++++++++++++++++- .../src/pythonfmu/PySlaveInstance.hpp | 36 +- .../src/pythonfmu/PyState.hpp | 6 +- 8 files changed, 680 insertions(+), 117 deletions(-) diff --git a/examples/two_state.py b/examples/two_state.py index 9cd8976..036d1bf 100644 --- a/examples/two_state.py +++ b/examples/two_state.py @@ -43,7 +43,7 @@ def __init__(self, **kwargs): self.register_variable(Int32("state", causality=Fmi3Causality.output)) self.register_variable(Clock("next_interval", interval_variability=Fmi3IntervalVariability.triggered, - + interval_decimal=self.next_interval, )) def get_next_interval(self): diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp index 4855b3f..aa4e8ba 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp @@ -40,11 +40,13 @@ typedef fmi3Int64 FMIInt64; typedef fmi3UInt64 FMIUInt64; typedef fmi3Boolean FMIBoolean; typedef fmi3String FMIString; +typedef fmi3Clock FMIClock; typedef fmi3LogMessageCallback FMICallbackLogger; typedef fmi3Instance FMIComponent; typedef fmi3InstanceEnvironment FMIComponentEnvironment; typedef fmi3Status FMIStatus; typedef fmi3ValueReference FMIValueReference; +typedef fmi3IntervalQualifier FMIIntervalQualifier; const FMIBoolean FMIFalse = fmi3False; const FMIBoolean FMITrue = fmi3True; @@ -55,6 +57,10 @@ const FMIStatus FMIDiscard = fmi3Discard; const FMIStatus FMIError = fmi3Error; const FMIStatus FMIFatal = fmi3Fatal; +const FMIIntervalQualifier FMIIntervalNotYetKnown = fmi3IntervalNotYetKnown; +const FMIIntervalQualifier FMIIntervalUnchanged = fmi3IntervalUnchanged; +const FMIIntervalQualifier FMIIntervalChanged = fmi3IntervalChanged; + using String = std::string; diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp index b594b42..dc70155 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp @@ -50,12 +50,6 @@ void SlaveInstance::Reset() // Do nothing } -void SlaveInstance::EnterEventMode() -{ - // Do nothing -} - - void SlaveInstance::SetFloat64( const FMIValueReference /*vr*/[], std::size_t nvr, @@ -67,7 +61,6 @@ void SlaveInstance::SetFloat64( } } - void SlaveInstance::SetInt32( const FMIValueReference /*vr*/[], std::size_t nvr, @@ -101,7 +94,6 @@ void SlaveInstance::SetUInt64( } } - void SlaveInstance::SetBoolean( const FMIValueReference /*vr*/[], std::size_t nvr, @@ -113,7 +105,6 @@ void SlaveInstance::SetBoolean( } } - void SlaveInstance::SetString( const FMIValueReference /*vr*/[], std::size_t nvr, @@ -197,6 +188,94 @@ void SlaveInstance::GetString( } } +// Event- and Clock-related functions +void SlaveInstance::EnterEventMode() +{ + // Do nothing +} +void SlaveInstance::GetClock( const FMIValueReference /*vr*/[], std::size_t nvr, FMIClock /*values*/[]) const +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::SetClock( const FMIValueReference /*vr*/[], std::size_t nvr, const FMIClock /*values*/[]) +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::GetIntervalDecimal( + const FMIValueReference /*vr*/[], + std::size_t nvr, + FMIFloat64 /*intervals*/[], + FMIIntervalQualifier /*qualifiers*/[]) const +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::SetIntervalDecimal(const FMIValueReference /*vr*/[], std::size_t nvr, const FMIFloat64 /*intervals*/[]) +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::GetShiftDecimal( const FMIValueReference /*vr*/[], std::size_t nvr, FMIFloat64 /*shifts*/[]) const +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::SetShiftDecimal( const FMIValueReference /*vr*/[], std::size_t nvr, const FMIFloat64 /*shifts*/[]) +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::GetIntervalFraction( + const FMIValueReference /*vr*/[], + std::size_t nvr, + FMIUInt64 /*intervalCounters*/[], + FMIUInt64 /*resolutions*/[], + FMIIntervalQualifier /*qualifiers*/[]) const +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::SetIntervalFraction( + const FMIValueReference /*vr*/[], + std::size_t nvr, + const FMIUInt64 /*intervalCounters*/[], + const FMIUInt64 /*resolutions*/[]) +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::GetShiftFraction( + const FMIValueReference /*vr*/[], + std::size_t nvr, + FMIUInt64 /*shiftCounters*/[], + FMIUInt64 /*resolutions*/[]) const +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} +void SlaveInstance::SetShiftFraction( + const FMIValueReference /*vr*/[], + std::size_t nvr, + const FMIUInt64 /*shiftCounters*/[], + const FMIUInt64 /*resolutions*/[]) +{ + if (nvr != 0) { + throw std::logic_error("Attempted to set nonexistent variable"); + } +} + + SlaveInstance::~SlaveInstance() CPPFMU_NOEXCEPT { diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 436a529..2543760 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -68,8 +68,6 @@ class SlaveInstance /* Called from fmi3SetXxx()/fmiSetXxx(). * Throws std::logic_error by default. */ - virtual void EnterEventMode(); - virtual void SetFloat64( const FMIValueReference vr[], std::size_t nvr, @@ -145,6 +143,40 @@ class SlaveInstance FMIBoolean* earlyReturn, FMIFloat64& endOfStep) = 0; + // Event- and Clock-related functions + virtual void EnterEventMode(); + virtual void GetClock( const FMIValueReference vr[], std::size_t nvr, FMIClock values[]) const; + virtual void SetClock( const FMIValueReference vr[], std::size_t nvr, const FMIClock values[]); + virtual void GetIntervalDecimal( + const FMIValueReference vr[], + std::size_t nvr, + FMIFloat64 intervals[], + FMIIntervalQualifier qualifiers[]) const; + virtual void SetIntervalDecimal(const FMIValueReference vr[], std::size_t nvr, const FMIFloat64 intervals[]); + virtual void GetShiftDecimal( const FMIValueReference vr[], std::size_t nvr, FMIFloat64 shifts[]) const; + virtual void SetShiftDecimal( const FMIValueReference vr[], std::size_t nvr, const FMIFloat64 shifts[]); + virtual void GetIntervalFraction( + const FMIValueReference vr[], + std::size_t nvr, + FMIUInt64 intervalCounters[], + FMIUInt64 resolutions[], + FMIIntervalQualifier qualifiers[]) const; + virtual void SetIntervalFraction( + const FMIValueReference vr[], + std::size_t nvr, + const FMIUInt64 intervalCounters[], + const FMIUInt64 resolutions[]); + virtual void GetShiftFraction( + const FMIValueReference vr[], + std::size_t nvr, + FMIUInt64 shiftCounters[], + FMIUInt64 resolutions[]) const; + virtual void SetShiftFraction( + const FMIValueReference vr[], + std::size_t nvr, + const FMIUInt64 shiftCounters[], + const FMIUInt64 resolutions[]); + // Model Exchange methods virtual void GetContinuousStates(cppfmu::FMIFloat64* continuousStates, std::size_t nStates) const = 0; virtual void GetContinuousStateDerivatives(cppfmu::FMIFloat64* derivatives, std::size_t nStates) const = 0; diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp index 6cd61d4..35d36bc 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp @@ -16,14 +16,14 @@ const fmi3ValueReference vr[], \ size_t nValueReferences, \ var values[], \ - size_t nValues) { NOT_IMPLEMENTED;} + size_t nValues) { NOT_IMPLEMENTED;} #define NOT_IMPLEMENTED_SETTER(suffix, var) fmi3Status fmi3Set##suffix ( \ fmi3Instance c, \ const fmi3ValueReference vr[], \ size_t nValueReferences, \ const var values[], \ - size_t nValues) { NOT_IMPLEMENTED;} + size_t nValues) { NOT_IMPLEMENTED;} namespace { @@ -238,7 +238,7 @@ fmi3Status fmi3ExitInitializationMode(fmi3Instance c) fmi3Status fmi3EnterConfigurationMode(fmi3Instance c) { - + const auto component = reinterpret_cast(c); component->state = Component::State::ConfigurationMode; return fmi3OK; @@ -554,7 +554,7 @@ fmi3Status fmi3GetContinuousStates( auto component = reinterpret_cast(c); try { component->slave->GetContinuousStates(continuousStates, nStates); - return fmi3OK; + return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); return fmi3Fatal; @@ -643,7 +643,7 @@ fmi3Status fmi3GetNominalsOfContinuousStates( const auto component = reinterpret_cast(c); try { component->slave->GetNominalsOfContinuousStates(nominalContinuousStates, nStates); - return fmi3OK; + return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); return fmi3Fatal; @@ -654,7 +654,7 @@ fmi3Status fmi3GetNominalsOfContinuousStates( } fmi3Status fmi3GetNumberOfContinuousStates( - fmi3Instance c, + fmi3Instance c, std::size_t * nContinuousStates) { const auto component = reinterpret_cast(c); @@ -671,7 +671,7 @@ fmi3Status fmi3GetNumberOfContinuousStates( } fmi3Status fmi3GetNumberOfEventIndicators( - fmi3Instance c, + fmi3Instance c, std::size_t * nEventIndicators) { const auto component = reinterpret_cast(c); @@ -728,7 +728,7 @@ fmi3Status fmi3CompletedIntegratorStep( { *enterEventMode = false; *terminateSimulation = false; - + return fmi3OK; } @@ -912,8 +912,200 @@ fmi3Status fmi3DoStep( return fmi3Error; } } + +// Clock-related functions + +fmi3Status fmi3GetClock( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + fmi3Clock values[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->GetClock(vr, nvr, values); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} + +} +fmi3Status fmi3SetClock( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + const fmi3Clock values[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->SetClock(vr, nvr, values); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} + +fmi3Status fmi3GetIntervalDecimal( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + fmi3Float64 intervals[], + fmi3IntervalQualifier qualifiers[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->GetIntervalDecimal(vr, nvr, intervals, qualifiers); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} +fmi3Status fmi3SetIntervalDecimal( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + const fmi3Float64 intervals[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->SetIntervalDecimal(vr, nvr, intervals); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} +fmi3Status fmi3GetShiftDecimal( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + fmi3Float64 shifts[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->GetShiftDecimal(vr, nvr, shifts); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} +fmi3Status fmi3SetShiftDecimal( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + const fmi3Float64 shifts[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->SetShiftDecimal(vr, nvr, shifts); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} +fmi3Status fmi3GetIntervalFraction( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + fmi3UInt64 intervalCounters[], + fmi3UInt64 resolutions[], + fmi3IntervalQualifier qualifiers[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->GetIntervalFraction(vr, nvr, intervalCounters, resolutions, qualifiers); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} +fmi3Status fmi3SetIntervalFraction( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + const fmi3UInt64 intervalCounters[], + const fmi3UInt64 resolutions[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->SetIntervalFraction(vr, nvr, intervalCounters, resolutions); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } } +fmi3Status fmi3GetShiftFraction( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + fmi3UInt64 shiftCounters[], + fmi3UInt64 resolutions[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->GetShiftFraction(vr, nvr, shiftCounters, resolutions); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} + +fmi3Status fmi3SetShiftFraction( + fmi3Instance c, + const fmi3ValueReference vr[], + size_t nvr, + const fmi3UInt64 shiftCounters[], + const fmi3UInt64 resolutions[]) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->SetShiftFraction(vr, nvr, shiftCounters, resolutions); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } +} // NOT IMPLEMENTED NOT_IMPLEMENTED_GETTER(Float32, fmi3Float32); @@ -932,22 +1124,6 @@ NOT_IMPLEMENTED_GETTER(UInt32, fmi3UInt32); NOT_IMPLEMENTED_SETTER(UInt32, fmi3UInt32); -fmi3Status fmi3GetClock(fmi3Instance instance, - const fmi3ValueReference valueReferences[], - size_t nValueReferences, - fmi3Clock values[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status fmi3SetClock(fmi3Instance instance, - const fmi3ValueReference valueReferences[], - size_t nValueReferences, - const fmi3Clock values[]) -{ - NOT_IMPLEMENTED; -} - fmi3Status fmi3GetBinary(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, @@ -1000,71 +1176,6 @@ fmi3Status fmi3GetAdjointDerivative(fmi3Instance instance, NOT_IMPLEMENTED; } -fmi3Status fmi3GetIntervalDecimal( - fmi3Instance instance, - const fmi3ValueReference valueReferences[], - size_t nValueReferences, - fmi3Float64 intervals[], - fmi3IntervalQualifier qualifiers[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status fmi3GetIntervalFraction( - fmi3Instance instance, - const fmi3ValueReference valueReferences[], - size_t nValueReferences, - fmi3UInt64 intervalCounters[], - fmi3UInt64 resolutions[], - fmi3IntervalQualifier qualifiers[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status -fmi3GetShiftDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], - size_t nValueReferences, fmi3Float64 shifts[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status -fmi3GetShiftFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], - size_t nValueReferences, fmi3UInt64 shiftCounters[], fmi3UInt64 resolutions[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status -fmi3SetIntervalDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], - size_t nValueReferences, const fmi3Float64 intervals[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status -fmi3SetIntervalFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], - size_t nValueReferences, const fmi3UInt64 intervalCounters[], - const fmi3UInt64 resolutions[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status -fmi3SetShiftDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], - size_t nValueReferences, const fmi3Float64 shifts[]) -{ - NOT_IMPLEMENTED; -} - -fmi3Status -fmi3SetShiftFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], - size_t nValueReferences, const fmi3UInt64 shiftCounters[], - const fmi3UInt64 resolutions[]) -{ - NOT_IMPLEMENTED; -} - fmi3Status fmi3EvaluateDiscreteStates(fmi3Instance instance) { diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index 656ca84..f090c4e 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -262,18 +262,6 @@ void PySlaveInstance::Terminate() }); } -void PySlaveInstance::EnterEventMode() -{ - py_safe_run([this](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "enter_event_mode", nullptr); - if (f == nullptr) { - handle_py_exception("[enterEventMode] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - void PySlaveInstance::SetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* values, std::size_t nValues) { py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { @@ -817,6 +805,319 @@ void PySlaveInstance::DeSerializeFMUstate(const fmi3Byte bytes[], size_t size, f }); } +// ================================ +// Event- and Clock-related methods +void PySlaveInstance::EnterEventMode() +{ + py_safe_run([this](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "enter_event_mode", nullptr); + if (f == nullptr) { + handle_py_exception("[enterEventMode] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + +void PySlaveInstance::GetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIClock* values) const +{ + py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + auto refs = PyObject_CallMethod(pInstance_, "get_clock", "O", vrs); + Py_DECREF(vrs); + if (refs == nullptr) { + handle_py_exception("[getClock] PyObject_CallMethod", gilState); + } + + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(refs, i); + values[i] = PyObject_IsTrue(value); + } + Py_DECREF(refs); + clearLogBuffer(); + }); +} +void PySlaveInstance::SetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIClock* values) +{ + py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + PyObject* refs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs, i, PyBool_FromLong(values[i])); + } + + auto f = PyObject_CallMethod(pInstance_, "set_clock", "(OO)", vrs, refs); + Py_DECREF(vrs); + Py_DECREF(refs); + if (f == nullptr) { + handle_py_exception("[setClock] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} +void PySlaveInstance::GetIntervalDecimal( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + cppfmu::FMIFloat64* intervals, + cppfmu::FMIIntervalQualifier* qualifiers) const +{ + py_safe_run([this, &vr, nvr, &intervals, &qualifiers](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + + auto refs = PyObject_CallMethod(pInstance_, "get_interval_decimal", "O", vrs); + Py_DECREF(vrs); + if (refs == nullptr) { + handle_py_exception("[getIntervalDecimal] PyObject_CallMethod", gilState); + } + if (PySequence_Size(refs)!=2) { + handle_py_exception("[getIntervalDecimal] !=2 return objects", gilState); + } + PyObject* ref0 = PyList_GetItem(refs, 0); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref0, i); + intervals[i] = PyFloat_AsDouble(value); + } + Py_DECREF(ref0); + PyObject* ref1 = PyList_GetItem(refs, 1); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref1, i); + if (value && PyLong_Check(value)) + qualifiers[i] = static_cast(PyLong_AsLong(value)); + else + handle_py_exception("[getIntervalDecimal] invalid interval qualifier", gilState); + } + Py_DECREF(ref1); + Py_DECREF(refs); + clearLogBuffer(); + }); +} + +void PySlaveInstance::SetIntervalDecimal(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* intervals) +{ + py_safe_run([this, &vr, nvr, &intervals](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + PyObject* refs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs, i, Py_BuildValue("d", intervals[i])); + } + + auto f = PyObject_CallMethod(pInstance_, "set_interval_decimal", "(OO)", vrs, refs); + Py_DECREF(vrs); + Py_DECREF(refs); + if (f == nullptr) { + handle_py_exception("[setFloat64] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} +void PySlaveInstance::GetShiftDecimal(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIFloat64* shifts) const +{ + py_safe_run([this, &vr, nvr, &shifts](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + auto refs = PyObject_CallMethod(pInstance_, "get_shift_decimal", "O", vrs); + Py_DECREF(vrs); + if (refs == nullptr) { + handle_py_exception("[getShiftDecimal] PyObject_CallMethod", gilState); + } + + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(refs, i); + shifts[i] = PyFloat_AsDouble(value); + } + Py_DECREF(refs); + clearLogBuffer(); + }); +} + +void PySlaveInstance::SetShiftDecimal(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* shifts) +{ + py_safe_run([this, &vr, nvr, &shifts](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + PyObject* refs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs, i, Py_BuildValue("d", shifts[i])); + } + + auto f = PyObject_CallMethod(pInstance_, "set_shift_decimal", "(OO)", vrs, refs); + Py_DECREF(vrs); + Py_DECREF(refs); + if (f == nullptr) { + handle_py_exception("[setShiftDecimal] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + +void PySlaveInstance::GetIntervalFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + cppfmu::FMIUInt64* intervalCounters, + cppfmu::FMIUInt64* resolutions, + cppfmu::FMIIntervalQualifier* qualifiers) const +{ + py_safe_run([this, &vr, nvr, &intervalCounters, &resolutions, &qualifiers](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + + auto refs = PyObject_CallMethod(pInstance_, "get_interval_fraction", "O", vrs); + Py_DECREF(vrs); + if (refs == nullptr) { + handle_py_exception("[getIntervalFraction] PyObject_CallMethod", gilState); + } + if (PySequence_Size(refs)!=3) { + handle_py_exception("[getIntervalFraction] !=3 return objects", gilState); + } + PyObject* ref0 = PyList_New(nvr); + ref0 = PyList_GetItem( refs, 0); + //auto ref0 = PyList_GetItem(refs, 0); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref0, i); + intervalCounters[i] = static_cast(PyLong_AsUnsignedLongLong(value)); + } + Py_DECREF(ref0); + auto ref1 = PyList_GetItem(refs, 1); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref1, i); + resolutions[i] = static_cast(PyLong_AsUnsignedLongLong(value)); + } + Py_DECREF(ref1); + auto ref2 = PyList_GetItem(refs, 2); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref2, i); + qualifiers[i] = static_cast(PyLong_AsLong(value)); + } + Py_DECREF(ref2); + Py_DECREF(refs); + clearLogBuffer(); + }); +} + +void PySlaveInstance::SetIntervalFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + const cppfmu::FMIUInt64* intervalCounters, + const cppfmu::FMIUInt64* resolutions) +{ + py_safe_run([this, &vr, nvr, &intervalCounters, &resolutions](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + PyObject* refs1 = PyList_New(nvr); + PyObject* refs2 = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs1, i, Py_BuildValue("K", intervalCounters[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs2, i, Py_BuildValue("K", resolutions[i])); + } + + auto f = PyObject_CallMethod(pInstance_, "set_interval_fraction", "(OO)", vrs, refs1, refs2); + Py_DECREF(vrs); + Py_DECREF(refs1); + Py_DECREF(refs2); + if (f == nullptr) { + handle_py_exception("[setIntervalFraction] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + +void PySlaveInstance::GetShiftFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + cppfmu::FMIUInt64* shiftCounters, + cppfmu::FMIUInt64* resolutions) const +{ + py_safe_run([this, &vr, nvr, &shiftCounters, &resolutions](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + auto refs = PyObject_CallMethod(pInstance_, "get_shift_fraction", "O", vrs); + Py_DECREF(vrs); + if (refs == nullptr) { + handle_py_exception("[getShiftFraction] PyObject_CallMethod", gilState); + } + if (PySequence_Size(refs)!=2) { + handle_py_exception("[getShiftFraction] !=2 return objects", gilState); + } + auto ref0 = PyList_GetItem(refs, 0); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref0, i); + shiftCounters[i] = static_cast(PyLong_AsUnsignedLongLong(value)); + } + Py_DECREF(ref0); + auto ref1 = PyList_GetItem(refs, 1); + for (int i = 0; i < nvr; i++) { + PyObject* value = PyList_GetItem(ref1, i); + resolutions[i] = static_cast(PyLong_AsUnsignedLongLong(value)); + } + Py_DECREF(ref1); + Py_DECREF(refs); + clearLogBuffer(); + }); +} + +void PySlaveInstance::SetShiftFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + const cppfmu::FMIUInt64* shiftCounters, + const cppfmu::FMIUInt64* resolutions) +{ + py_safe_run([this, &vr, nvr, &shiftCounters, &resolutions](PyGILState_STATE gilState) { + PyObject* vrs = PyList_New(nvr); + PyObject* refs1 = PyList_New(nvr); + PyObject* refs2 = PyList_New(nvr); + for (int i = 0; i < nvr; i++) { + PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs1, i, Py_BuildValue("K", shiftCounters[i])); + } + for (int i = 0; i < nvr; i++) { + PyList_SetItem(refs2, i, Py_BuildValue("K", resolutions[i])); + } + + auto f = PyObject_CallMethod(pInstance_, "set_shift_fraction", "(OO)", vrs, refs1, refs2); + Py_DECREF(vrs); + Py_DECREF(refs1); + Py_DECREF(refs2); + if (f == nullptr) { + handle_py_exception("[setShiftFraction] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + +// ============== +// Helper methods + void PySlaveInstance::handle_py_exception(const std::string& what, PyGILState_STATE gilState) const { auto err = PyErr_Occurred(); diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 4fa9f2b..3a3c215 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -25,7 +25,6 @@ class PySlaveInstance : public cppfmu::SlaveInstance void ExitInitializationMode() override; void Terminate() override; void Reset() override; - void EnterEventMode() override; cppfmu::FMIStatus DoStep(cppfmu::FMIFloat64 currentCommunicationPoint, cppfmu::FMIFloat64 communicationStepSize, cppfmu::FMIBoolean newStep, @@ -52,6 +51,41 @@ class PySlaveInstance : public cppfmu::SlaveInstance void SetFMUstate(const fmi3FMUState& State) override; void FreeFMUstate(fmi3FMUState& State) override; + // Event- and Clock-related methods + void EnterEventMode() override; + void GetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIClock* values) const override; + void SetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIClock* values) override; + void GetIntervalDecimal( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + cppfmu::FMIFloat64* intervals, + cppfmu::FMIIntervalQualifier* qualifiers) const override; + void SetIntervalDecimal(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* intervals) override; + void GetShiftDecimal(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIFloat64* shifts) const override; + void SetShiftDecimal(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* shifts) override; + void GetIntervalFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + cppfmu::FMIUInt64* intervalCounters, + cppfmu::FMIUInt64* resolutions, + cppfmu::FMIIntervalQualifier* qualifiers) const override; + void SetIntervalFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + const cppfmu::FMIUInt64* intervalCounters, + const cppfmu::FMIUInt64* resolutions) override; + void GetShiftFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + cppfmu::FMIUInt64* shiftCounters, + cppfmu::FMIUInt64* resolutions) const override; + void SetShiftFraction( + const cppfmu::FMIValueReference* vr, + std::size_t nvr, + const cppfmu::FMIUInt64* shiftCounters, + const cppfmu::FMIUInt64* resolutions) override; + + // Model Exchange methods void GetContinuousStates(cppfmu::FMIFloat64* continuousStates, std::size_t nStates) const override; void GetNumberOfContinuousStates(std::size_t& nStates) const override; diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp index ae64364..b7bc1de 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp @@ -33,10 +33,10 @@ namespace pythonfmu private: - // In accordance to the documentation https://docs.python.org/3/c-api/init.html#c.Py_FinalizeEx + // In accordance to the documentation https://docs.python.org/3/c-api/init.html#c.Py_FinalizeEx // the Py_Initialize/Py_Finalize should be called from the same // thread. The FMI standard allows to call fmi functions from different threads, also different threads - // could be used for loading unloading the FMU libraries. There is a deadlock, when the simulation tool + // could be used for loading unloading the FMU libraries. There is a deadlock, when the simulation tool // unloads the FMU library from the thread which is differs from the thread where the Py_Initialize was called. // Create a new thread which is used for calling Py_Initialize and Py_Deinitialize. The thread waits for // notification from destructor and then calls Py_Deinitialize. @@ -82,4 +82,4 @@ namespace pythonfmu } // namespace pythonfmu -#endif //PYTHONFMU_PYTHONSTATE_HPP \ No newline at end of file +#endif //PYTHONFMU_PYTHONSTATE_HPP From ae48cae1168324d8933573dbe9eba00bc286e9a6 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Fri, 14 Aug 2026 10:48:11 +0200 Subject: [PATCH 08/20] Added the function EnterStepMode, which was completely missing, such that no switch back to step mode was possible. Compilation ok. --- .../pythonfmu-export/src/cppfmu/cppfmu_cs.cpp | 15 +++++++++++++++ .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 1 + .../src/pythonfmu/PySlaveInstance.cpp | 12 ++++++++++++ .../src/pythonfmu/PySlaveInstance.hpp | 1 + 4 files changed, 29 insertions(+) diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp index dc70155..3f2f298 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp @@ -193,6 +193,21 @@ void SlaveInstance::EnterEventMode() { // Do nothing } +void SlaveInstance::EnterStepMode() +{ + // Do nothing +} +void SlaveInstance::UpdateDiscreteStates( + cppfmu::FMIBoolean* discreteStatesNeedUpdate, + cppfmu::FMIBoolean* terminateSimulation, + cppfmu::FMIBoolean* nominalContinuousStatesChanged, + cppfmu::FMIBoolean* valuesOfContinuousStatesChanged, + cppfmu::FMIBoolean* nextEventTimeDefined, + cppfmu::FMIFloat64* nextEventTime) +{ + // Do nothing +} + void SlaveInstance::GetClock( const FMIValueReference /*vr*/[], std::size_t nvr, FMIClock /*values*/[]) const { if (nvr != 0) { diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 2543760..10097f9 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -145,6 +145,7 @@ class SlaveInstance // Event- and Clock-related functions virtual void EnterEventMode(); + virtual void EnterStepMode(); virtual void GetClock( const FMIValueReference vr[], std::size_t nvr, FMIClock values[]) const; virtual void SetClock( const FMIValueReference vr[], std::size_t nvr, const FMIClock values[]); virtual void GetIntervalDecimal( diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index f090c4e..ce6f928 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -819,6 +819,18 @@ void PySlaveInstance::EnterEventMode() }); } +void PySlaveInstance::EnterStepMode() +{ + py_safe_run([this](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "enter_step_mode", nullptr); + if (f == nullptr) { + handle_py_exception("[enterStepMode] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + void PySlaveInstance::GetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIClock* values) const { py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 3a3c215..0b0d0b0 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -53,6 +53,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance // Event- and Clock-related methods void EnterEventMode() override; + void EnterStepMode() override; void GetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIClock* values) const override; void SetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIClock* values) override; void GetIntervalDecimal( From 200a59edd910e12148c1c92657e0172e32c7fd9a Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Fri, 14 Aug 2026 13:58:48 +0200 Subject: [PATCH 09/20] In order to receive results from UpdateDiscreteStates() a dedicated NamedTuple needs to be defined and used. --- examples/two_state.py | 60 ++++++++++++++++++++++++++++++++----- src/pythonfmu3/fmi3slave.py | 9 +++++- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/examples/two_state.py b/examples/two_state.py index 036d1bf..9511aa4 100644 --- a/examples/two_state.py +++ b/examples/two_state.py @@ -6,9 +6,13 @@ which is transmitted to the orchestrator with a `getIntervalDecimal` call. """ import numpy as np +from collections import namedtuple from pythonfmu3 import Fmi3Slave -from pythonfmu3.enums import Fmi3IntervalVariability, Fmi3Causality -from pythonfmu3.variables import Float64, Int32, Clock +from pythonfmu3.enums import Fmi3IntervalVariability, Fmi3Causality, Fmi3Status +from pythonfmu3.variables import Float64, Int32, Clock +from pythonfmu3.fmi3slave import Fmi3StepResult, Fmi3UpdateDiscreteStatesResult + +EPS = 1e-14 class TwoState(Fmi3Slave): """Two-state discrete event implementation""" @@ -33,7 +37,7 @@ def __init__(self, **kwargs): self.rng = np.random.default_rng(self.seed) self.next_interval = self.get_next_interval() self.time = 0.0 - self.histogram : list[ tuple[float,int]] + self.histogram : list[ tuple[float,int]] = [] # Register variables self.register_variable(Float64("time", causality=Fmi3Causality.independent)) @@ -56,15 +60,57 @@ def get_next_interval(self): def enter_event_mode(self): """Signal from orchestrator that an event has occured.""" - self.state = 1 - self.state - self.next_interval = self.get_next_interval() + print(f"EnterEventMode @{self.time}") + + def enter_step_mode(self): + """Signal from orchestrator that events are dealt with and switching back to step mode.""" + print(f"EnterStepMode @{self.time}") + + def exit_initialization_mode(self): + print(f"Initialization finished") def do_step(self, current_time, step_size): - self.time = current_time + self.time = current_time + step_size self.histogram.append( (current_time, self.state)) - return True + return Fmi3StepResult(status=Fmi3Status.ok, + eventHandlingNeeded=(self.time>self.next_time-EPS)) + def update_discrete_states(self): + """Call by Orchestrator to update discrete states in super-dense time instant, + returning info to the Orchestrator on e.g. nextEventTime + + Returns: + discreteStatesNeedUpdate: True; the Orchestrator must stay in Event Mode for another event iteration, + starting a new super-dense time instant. + terminateSimulation: True; the FMU requests to stop the simulation. + nominalsOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: True; use only for ME! + nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. + nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. + The importer must compute up to nextEventTime (or if needed slightly further) + and then enter Event Mode using EnterEventMode. + The FMU must handle this time event during the Event Mode that is entered + by the first call to EnterEventMode, at or after nextEventTime. + If another (e.g. state event) event happens before that nextEventTime, + the previous definition of nextEventTime becomes obsolete. + """ + self.state = 1 if self.time == 0.0 else 1 - self.state + self.next_interval = self.get_next_interval() + self.next_time = self.time + self.next_interval + print(f"UpdateDiscreteStates @ {self.time} => {self.state}, {self.next_time}") + +# dict does not work +# return ( False, # discreteStatesNeedUpdate +# False, # terminateSimulation +# False, # nominalContinuousStatesChanged (only ME) +# False, # valuesOfContinuousStatesChanged +# True, # nextEventTimeDefined, +# self.next_interval, #nextEventTime +# ) + return Fmi3UpdateDiscreteStatesResult( nextEventTimeDefined=True, nextEventTime=self.next_interval) + + if __name__ == "__main__": from pathlib import Path from pythonfmu3.builder import FmuBuilder diff --git a/src/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py index 8ef5350..0bb1eeb 100644 --- a/src/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -44,13 +44,20 @@ ModelOptions("needsCompletedIntegratorStep", False, "needs-completed-step"), ] - class Fmi3StepResult(NamedTuple): status: Fmi3Status = Fmi3Status.ok eventHandlingNeeded: bool = False terminateSimulation: bool = False earlyReturn: bool = False +class Fmi3UpdateDiscreteStatesResult(NamedTuple): + discreteStatesNeedUpdate: bool = False + terminateSimulation: bool = False + nominalContinuousStatesChanged: bool = False # (only ME) + valuesOfContinuousStatesChanged: bool = False + nextEventTimeDefined: bool = False + nextEventTime: Float64 = 0.0 + class Fmi3SlaveBase: """Abstract facade class to execute Python through FMI standard.""" From e78660d7b9ece713b8bdbf08818845375068aea0 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Tue, 18 Aug 2026 15:42:27 +0200 Subject: [PATCH 10/20] Ensure all FMI3 model options, so that the system can work with FMI3 features. Options are now implemented as dataclass and it is possible to provide options directly to the Slave (not only through cli). Tested features. --- examples/two_state.py | 55 +++++--- src/pythonfmu3/builder.py | 83 ++++++------ src/pythonfmu3/fmi3slave.py | 128 +++++++++++++----- .../pythonfmu-export/src/cppfmu/cppfmu_cs.cpp | 16 ++- .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 6 +- .../src/cppfmu/fmi_functions.cpp | 18 ++- .../src/pythonfmu/PySlaveInstance.cpp | 44 +++++- .../src/pythonfmu/PySlaveInstance.hpp | 7 +- tests/test_builder.py | 91 ++++++++++++- 9 files changed, 334 insertions(+), 114 deletions(-) diff --git a/examples/two_state.py b/examples/two_state.py index 9511aa4..c61f5dd 100644 --- a/examples/two_state.py +++ b/examples/two_state.py @@ -9,8 +9,12 @@ from collections import namedtuple from pythonfmu3 import Fmi3Slave from pythonfmu3.enums import Fmi3IntervalVariability, Fmi3Causality, Fmi3Status -from pythonfmu3.variables import Float64, Int32, Clock -from pythonfmu3.fmi3slave import Fmi3StepResult, Fmi3UpdateDiscreteStatesResult +from pythonfmu3.variables import Boolean, Float64, Int32, Clock +from pythonfmu3.fmi3slave import ( + Fmi3StepResult, + Fmi3UpdateDiscreteStatesResult, + Fmi3ModelOptionsCS, + ) EPS = 1e-14 @@ -18,15 +22,21 @@ class TwoState(Fmi3Slave): """Two-state discrete event implementation""" def __init__(self, **kwargs): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + mightReturnEarlyFromDoStep = True, + hasEventMode = True), + 'model_options_me' : None, + }) super().__init__(**kwargs) self.author = "Siegfried Eisinger" self.description = "Two-state discrete event system." # Parameters - self.mttf = 1.0 # Mean time to failure (ttf exponentially distributed) + self.mttf = 0.1 # Mean time to failure (ttf exponentially distributed) self.mttr = 0.01 # Mean time to repair (ttr constant) self.seed = 1 + self.deterministic = False # Inputs @@ -44,6 +54,7 @@ def __init__(self, **kwargs): self.register_variable(Float64("mttf", causality=Fmi3Causality.parameter, start=1.0)) self.register_variable(Float64("mttr", causality=Fmi3Causality.parameter, start=0.01)) self.register_variable(Int32("seed", causality=Fmi3Causality.parameter, start=1)) + self.register_variable(Boolean("deterministic", causality=Fmi3Causality.parameter, start=False)) self.register_variable(Int32("state", causality=Fmi3Causality.output)) self.register_variable(Clock("next_interval", interval_variability=Fmi3IntervalVariability.triggered, @@ -54,27 +65,38 @@ def get_next_interval(self): if self.state==0: return self.mttr elif self.state==1: - return self.rng.exponential(self.mttf) + if self.deterministic: + return self.mttf + else: + return self.rng.exponential(self.mttf) else: raise ValueError(f"Unallowed state {self.state}") from None def enter_event_mode(self): """Signal from orchestrator that an event has occured.""" - print(f"EnterEventMode @{self.time}") + # print(f"EnterEventMode @{self.time}") + pass def enter_step_mode(self): """Signal from orchestrator that events are dealt with and switching back to step mode.""" - print(f"EnterStepMode @{self.time}") + # print(f"EnterStepMode @{self.time}") + pass def exit_initialization_mode(self): - print(f"Initialization finished") + # print(f"Initialization finished") + pass - def do_step(self, current_time, step_size): + def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): self.time = current_time + step_size self.histogram.append( (current_time, self.state)) - - return Fmi3StepResult(status=Fmi3Status.ok, - eventHandlingNeeded=(self.time>self.next_time-EPS)) + if self.time >= self.next_time-EPS: + ret = Fmi3StepResult(eventHandlingNeeded = True, + lastSuccessfullTime = self.next_time) + # print(f"DoStep@{current_time}: {ret}") + else: + ret = Fmi3StepResult(eventHandlingNeeded = False, + lastSuccessfullTime = self.time) + return ret def update_discrete_states(self): """Call by Orchestrator to update discrete states in super-dense time instant, @@ -98,16 +120,7 @@ def update_discrete_states(self): self.state = 1 if self.time == 0.0 else 1 - self.state self.next_interval = self.get_next_interval() self.next_time = self.time + self.next_interval - print(f"UpdateDiscreteStates @ {self.time} => {self.state}, {self.next_time}") - -# dict does not work -# return ( False, # discreteStatesNeedUpdate -# False, # terminateSimulation -# False, # nominalContinuousStatesChanged (only ME) -# False, # valuesOfContinuousStatesChanged -# True, # nextEventTimeDefined, -# self.next_interval, #nextEventTime -# ) + # print(f"UpdateDiscreteStates @ {self.time} => {self.state}, {self.next_time}") return Fmi3UpdateDiscreteStatesResult( nextEventTimeDefined=True, nextEventTime=self.next_interval) diff --git a/src/pythonfmu3/builder.py b/src/pythonfmu3/builder.py index 782c17f..ae88916 100644 --- a/src/pythonfmu3/builder.py +++ b/src/pythonfmu3/builder.py @@ -15,12 +15,8 @@ from xml.dom.minidom import parseString from xml.etree.ElementTree import Element, tostring -from .fmi3slave import ( - FMI3_MODEL_OPTIONS_COMMON, - FMI3_MODEL_OPTIONS_COSIM, - FMI3_MODEL_OPTIONS_MX, - Fmi3SlaveBase, -) +from pythonfmu3.fmi3slave import Fmi3ModelOptionsCS, Fmi3ModelOptionsME, Fmi3SlaveBase + from .osutil import get_lib_extension, get_platform FilePath = Union[str, Path] @@ -59,6 +55,7 @@ def get_model_description(filepath: Path, module_name: str) -> tuple[str, Elemen instance = getattr(fmu_interface, class_name)( instance_name="dummyInstance", resources=str(filepath.parent) ) + model_options_cs = instance.model_options_cs.to_dict() finally: sys.path.remove(str(filepath.parent)) # remove inserted temporary path @@ -67,7 +64,7 @@ def get_model_description(filepath: Path, module_name: str) -> tuple[str, Elemen f"The provided class '{class_name}' does not inherit from {Fmi3SlaveBase.__qualname__}" ) # Produce the xml - return instance.modelName, instance.to_xml() + return instance.modelName, instance.to_xml(model_options_cs) class FmuBuilder: @@ -143,26 +140,21 @@ def build_FMU( else: shutil.copy2(file_, temp_dir) - model_identifier, xml = get_model_description( - temp_dir.absolute() / script_file.name, module_name - ) + model_identifier, xml = get_model_description(temp_dir.absolute() / script_file.name, module_name) dest_file = dest / f"{model_identifier}.fmu" type_node = xml.find("CoSimulation") - option_names = [opt.name for opt in FMI3_MODEL_OPTIONS_COMMON] - cosim_option_names = option_names.copy() - cosim_option_names.extend(opt.name for opt in FMI3_MODEL_OPTIONS_COSIM) + cs_option_names = list(Fmi3ModelOptionsCS().to_dict().keys()) if type_node is not None: - for option, value in options.items(): - if option in cosim_option_names: + for option, value in options['options'].items(): + if option in cs_option_names: type_node.set(option, str(value).lower()) type_node = xml.find("ModelExchange") - mx_option_names = option_names.copy() - mx_option_names.extend(opt.name for opt in FMI3_MODEL_OPTIONS_MX) + me_option_names = list(Fmi3ModelOptionsME().to_dict().keys()) if type_node is not None: - for option, value in options.items(): + for option, value in options['options'].items(): if option in mx_option_names: type_node.set(option, str(value).lower()) @@ -266,32 +258,35 @@ def create_command_parser(parser: argparse.ArgumentParser): default=None, ) - for option in FMI3_MODEL_OPTIONS_COMMON: - action = "store_false" if option.value else "store_true" - parser.add_argument( - f"--{option.cli}", - dest=option.name, - help=f"If given, {option.name}={action[6:]}", - action=action, - ) - - for option in FMI3_MODEL_OPTIONS_COSIM: - action = "store_false" if option.value else "store_true" - parser.add_argument( - f"--{option.cli}", - dest=option.name, - help=f"If given, {option.name}={action[6:]}", - action=action, - ) - - for option in FMI3_MODEL_OPTIONS_MX: - action = "store_false" if option.value else "store_true" - parser.add_argument( - f"--{option.cli}", - dest=option.name, - help=f"If given, {option.name}={action[6:]}", - action=action, - ) + # Boolean options are added using their FMI name, making them implicitly True. + # For other options their (non-default) number is added after a space. + for name, default in Fmi3ModelOptionsCS().to_dict().items(): + if default in (True,False): + parser.add_argument( + f"--{name}", + dest=name, + help=f"If given, {name}={not default}", + action="store_true" + ) + elif name in ("fixedInternalStepSize", "maxOutputDerivativeOrder", "recommendedIntermediateInputSmoothness"): + parser.add_argument(f"--{name}", dest=name, + help=f"If given, {name}= number", + ) + else: + raise ValueError(f"Something wrong with option {name}, default:{default}") from None + + cs_keys = list(Fmi3ModelOptionsCS().to_dict().keys()) + for name, default in Fmi3ModelOptionsME().to_dict().items(): + if name not in cs_keys: + if default in (True,False): + parser.add_argument( + f"--{name}", + dest=name, + help=f"If given, {name}={not default}", + action="store_true" + ) + else: + raise ValueError(f"Something wrong with option {name}, default:{default}") from None parser.add_argument( "project_files", diff --git a/src/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py index 0bb1eeb..5782886 100644 --- a/src/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -4,6 +4,7 @@ import datetime import json from collections import OrderedDict, namedtuple +from dataclasses import dataclass, fields from typing import Any, NamedTuple from uuid import uuid1 from xml.etree.ElementTree import Element, SubElement @@ -27,28 +28,90 @@ UInt64, ) -ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"]) - -FMI3_MODEL_OPTIONS_COMMON: list[ModelOptions] = [ - ModelOptions("needsExecutionTool", True, "no-external-tool"), - ModelOptions("canBeInstantiatedOnlyOncePerProcess", False, "only-one-per-process"), - ModelOptions("canGetAndSetFMUState", False, "handle-state"), - ModelOptions("canSerializeFMUState", False, "serialize-state"), -] - -FMI3_MODEL_OPTIONS_COSIM: list[ModelOptions] = [ - ModelOptions("canHandleVariableCommunicationStepSize", True, "no-variable-step"), -] - -FMI3_MODEL_OPTIONS_MX: list[ModelOptions] = [ - ModelOptions("needsCompletedIntegratorStep", False, "needs-completed-step"), -] +#Note: all Bool options are false by default and the name expresses the option as True. +# The cli uses therefor the original name to add the option as implicit True (non-default) +# other options are added with their (non-default) numbers +@dataclass(kw_only=True, frozen=True, slots=True) +class Fmi3ModelOptionsCommon: + """Model options common for Co-Simulation and Model Exchange.""" + needsExecutionTool: bool = False + canBeInstantiatedOnlyOncePerProcess: bool = False + canGetAndSetFMUState: bool = False + canSerializeFMUState: bool = False + providesDirectionalDerivatives: bool = False + providesAdjointDerivatives: bool = False + providesPerElementDependencies: bool = False + providesEvaluateDiscreteStates: bool = False + + def to_dict(self, *, w_default:bool= True, w_none: bool = True): + """A special version of 'asdict', which also allows for choice what to include. + Note: self.do_dict() == asdict(self) if the default value is None. + + Args: + w_defaults: include also the elements with default value + w_none: include also elements with a 'None' value + """ + dct: dict[str, bool|int|float] = {} + for _f in fields(self): + value = getattr(self,_f.name) + if ((value is not None and value != _f.default) or + (value is not None and value == _f.default and w_default) or + (value is None and value == _f.default and w_default and w_none) or + (value is None and value != _f.default and w_none) + ): + dct.update({_f.name : value}) + return dct + +@dataclass(kw_only=True, frozen=True, slots=True) +class Fmi3ModelOptionsCS(Fmi3ModelOptionsCommon): + """Model options for Co-Simulation.""" + canHandleVariableCommunicationStepSize: bool = False + fixedInternalStepSize: float|None = None + maxOutputDerivativeOrder: int = 0 # uInt + recommendedIntermediateInputSmoothness: int = 0 + providesIntermediateUpdate: bool = False + mightReturnEarlyFromDoStep: bool = False + canReturnEarlyAfterIntermediateUpdate: bool = False + hasEventMode: bool = False + +@dataclass(kw_only=True, frozen=True, slots=True) +class Fmi3ModelOptionsME(Fmi3ModelOptionsCommon): + """Model options for Model Exchange.""" + needsCompletedIntegratorStep: bool = False + + +# +# FMI3_MODEL_OPTIONS_COMMON: list[ModelOptions] = [ +# ModelOptions("needsExecutionTool", False), +# ModelOptions("canBeInstantiatedOnlyOncePerProcess", False), +# ModelOptions("canGetAndSetFMUState", False), +# ModelOptions("canSerializeFMUState", False), +# ModelOptions("providesDirectionalDerivatives", False), +# ModelOptions("providesAdjointDerivatives", False), +# ModelOptions("providesPerElementDependencies", False), +# ModelOptions("providesEvaluateDiscreteStates", False), +# ] +# +# FMI3_MODEL_OPTIONS_CS: list[ModelOptions] = [ +# ModelOptions("canHandleVariableCommunicationStepSize", True), +# ModelOptions("fixedInternalStepSize", None), # double +# ModelOptions("maxOutputDerivativeOrder", 0), # uInt +# ModelOptions("recommendedIntermediateInputSmoothness", 0), # int +# ModelOptions("providesIntermediateUpdate", False), +# ModelOptions("mightReturnEarlyFromDoStep", False), +# ModelOptions("canReturnEarlyAfterIntermediateUpdate", False), +# ModelOptions("hasEventMode", False), +# ] +# +# FMI3_MODEL_OPTIONS_ME: list[ModelOptions] = [ +# ModelOptions("needsCompletedIntegratorStep", False), +# ] class Fmi3StepResult(NamedTuple): - status: Fmi3Status = Fmi3Status.ok eventHandlingNeeded: bool = False terminateSimulation: bool = False earlyReturn: bool = False + lastSuccessfullTime: Float64 = 0.0 class Fmi3UpdateDiscreteStatesResult(NamedTuple): discreteStatesNeedUpdate: bool = False @@ -77,6 +140,8 @@ def __init__(self, **kwargs): self.instance_name = kwargs["instance_name"] self.resources = kwargs.get("resources", None) self.visible = kwargs.get("visible", False) + self.model_options_cs = kwargs.get("model_options_cs", Fmi3ModelOptionsCS()) + self.model_options_me = kwargs.get("model_options_me", Fmi3ModelOptionsME()) self.log_queue = [] self.guid = uuid1() @@ -100,7 +165,6 @@ def to_xml(self, model_options: dict[str, str] = dict()) -> Element: Returns: (xml.etree.TreeElement.Element) XML description of the FMU """ - t = datetime.datetime.now(datetime.UTC) date_str = t.isoformat(timespec="seconds") @@ -125,20 +189,20 @@ def to_xml(self, model_options: dict[str, str] = dict()) -> Element: root = Element("fmiModelDescription", attrib) - options = dict() - for option in FMI3_MODEL_OPTIONS_COMMON: - value = model_options.get(option.name, option.value) - options[option.name] = str(value).lower() - - options["modelIdentifier"] = self.modelName - - options_cs = options.copy() - for option in FMI3_MODEL_OPTIONS_COSIM: - options_cs[option.name] = str(value).lower() - - options_me = options.copy() - for option in FMI3_MODEL_OPTIONS_MX: - options_me[option.name] = str(value).lower() + options_cs = {"modelIdentifier": self.modelName} + for name, default in Fmi3ModelOptionsCS().to_dict().items(): + if name in model_options: + value = model_options.get(name, default) + if value is not None: + options_cs[name] = str(value).lower() + options_cs["modelIdentifier"] = self.modelName + + options_me = {"modelIdentifier": self.modelName} + for name, default in Fmi3ModelOptionsME().to_dict().items(): + if name in model_options: + value = model_options.get(name, default) + if value is not None: + options_me[name] = str(value).lower() # check if we have cosim mixin or model exchange mixin if isinstance(self, ModelExchange): diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp index 3f2f298..d26fdd3 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp @@ -188,6 +188,18 @@ void SlaveInstance::GetString( } } +void SlaveInstance::DoStep( + FMIFloat64 currentCommunicationPoint, + FMIFloat64 communicationStepSize, + FMIBoolean noSetFMUStatePriorToCurrentPoint, + FMIBoolean* eventHandlingNeeded, + FMIBoolean* terminateSimulation, + FMIBoolean* earlyReturn, + FMIFloat64* lastSuccessfullTime) +{ + // Do nothing +} + // Event- and Clock-related functions void SlaveInstance::EnterEventMode() { @@ -197,7 +209,7 @@ void SlaveInstance::EnterStepMode() { // Do nothing } -void SlaveInstance::UpdateDiscreteStates( +/*void SlaveInstance::UpdateDiscreteStates( cppfmu::FMIBoolean* discreteStatesNeedUpdate, cppfmu::FMIBoolean* terminateSimulation, cppfmu::FMIBoolean* nominalContinuousStatesChanged, @@ -207,7 +219,7 @@ void SlaveInstance::UpdateDiscreteStates( { // Do nothing } - +*/ void SlaveInstance::GetClock( const FMIValueReference /*vr*/[], std::size_t nvr, FMIClock /*values*/[]) const { if (nvr != 0) { diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 10097f9..50da143 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -134,14 +134,14 @@ class SlaveInstance std::size_t nValues) const; // Called from fmi3DoStep()/fmiDoStep(). Must be implemented in model code. - virtual FMIStatus DoStep( + virtual void DoStep( FMIFloat64 currentCommunicationPoint, FMIFloat64 communicationStepSize, - FMIBoolean newStep, + FMIBoolean noSetFMUStatePriorToCurrentPoint, FMIBoolean* eventHandlingNeeded, FMIBoolean* terminateSimulation, FMIBoolean* earlyReturn, - FMIFloat64& endOfStep) = 0; + FMIFloat64* lastSuccessfullTime); // Event- and Clock-related functions virtual void EnterEventMode(); diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp index 35d36bc..5fb8644 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp @@ -883,7 +883,21 @@ fmi3Status fmi3DoStep( fmi3Boolean* eventHandlingNeeded , fmi3Boolean* terminateSimulation, fmi3Boolean* earlyReturn, - fmi3Float64* lastSuccessfulTime) + fmi3Float64* lastSuccessfullTime) +{ + const auto component = reinterpret_cast(c); + try { + component->slave->DoStep( + currentCommunicationPoint, + communicationStepSize, + noSetFmuStatePriorToCurrentPoint, + eventHandlingNeeded, + terminateSimulation, + earlyReturn, + lastSuccessfullTime); + return fmi3OK; +/* Changed that back to the original definition leaving the logic to the components + fmi3Float64* lastSuccessfulTime) { const auto component = reinterpret_cast(c); try { @@ -903,7 +917,7 @@ fmi3Status fmi3DoStep( } else { *lastSuccessfulTime = endTime; return status; - } + }*/ } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); return fmi3Fatal; diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index ce6f928..5c47d94 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace pythonfmu { @@ -201,21 +202,27 @@ void PySlaveInstance::ExitInitializationMode() }); } -cppfmu::FMIStatus PySlaveInstance::DoStep(cppfmu::FMIFloat64 currentTime, +//cppfmu::FMIStatus PySlaveInstance::DoStep( +void PySlaveInstance::DoStep( + cppfmu::FMIFloat64 currentTime, cppfmu::FMIFloat64 stepSize, cppfmu::FMIBoolean noSetFmuStatePriorToCurrentPoint, cppfmu::FMIBoolean* eventHandlingNeeded, cppfmu::FMIBoolean* terminateSimulation, cppfmu::FMIBoolean* earlyReturn, - cppfmu::FMIFloat64& endOfStep) + cppfmu::FMIFloat64* lastSuccessfullTime) { + /* Changed back to the original definition of DoStep cppfmu::FMIStatus fmuStatus = cppfmu::FMIOK; - py_safe_run([this, &fmuStatus, currentTime, stepSize, terminateSimulation](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "do_step", "(dd)", currentTime, stepSize); + py_safe_run([this, &fmuStatus, currentTime, stepSize, eventHandlingNeeded, terminateSimulation, earlyReturn](PyGILState_STATE gilState) { + */ + py_safe_run([this, currentTime, stepSize, noSetFmuStatePriorToCurrentPoint, eventHandlingNeeded, terminateSimulation, earlyReturn, lastSuccessfullTime](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "do_step", "(dd)", currentTime, stepSize, noSetFmuStatePriorToCurrentPoint); if (f == nullptr) { handle_py_exception("[doStep] PyObject_CallMethod", gilState); } +/* // optional result according to the old definition if (PyObject_HasAttrString(f, "status")) { PyObject* pyStatus = PyObject_GetAttrString(f, "status"); if (pyStatus) { @@ -228,7 +235,16 @@ cppfmu::FMIStatus PySlaveInstance::DoStep(cppfmu::FMIFloat64 currentTime, fmuStatus = cppfmu::FMIDiscard; } } - +*/ + if (PyObject_HasAttrString(f, "eventHandlingNeeded")) { + PyObject* pyEventHandlingNeeded = PyObject_GetAttrString(f, "eventHandlingNeeded"); + if (pyEventHandlingNeeded) { + *eventHandlingNeeded = static_cast(PyObject_IsTrue(pyEventHandlingNeeded)); + Py_DECREF(pyEventHandlingNeeded); + } + } else { + *eventHandlingNeeded = false; + } if (PyObject_HasAttrString(f, "terminateSimulation")) { PyObject* pyTerminateSimulation = PyObject_GetAttrString(f, "terminateSimulation"); if (pyTerminateSimulation) { @@ -236,11 +252,27 @@ cppfmu::FMIStatus PySlaveInstance::DoStep(cppfmu::FMIFloat64 currentTime, Py_DECREF(pyTerminateSimulation); } } + if (PyObject_HasAttrString(f, "earlyReturn")) { + PyObject* pyEarlyReturn = PyObject_GetAttrString(f, "earlyReturn"); + if (pyEarlyReturn) { + *earlyReturn = static_cast(PyObject_IsTrue(pyEarlyReturn)); + Py_DECREF(pyEarlyReturn); + } + } else { + *earlyReturn = false; + } + if (PyObject_HasAttrString(f, "lastSuccessfullTime")) { + PyObject* pyLastSuccessfullTime = PyObject_GetAttrString(f, "lastSuccessfullTime"); + if (pyLastSuccessfullTime) { + *lastSuccessfullTime = PyFloat_AsDouble(pyLastSuccessfullTime); + Py_DECREF(pyLastSuccessfullTime); + } + } Py_DECREF(f); clearLogBuffer(); }); - return fmuStatus; +// return fmuStatus; } void PySlaveInstance::Reset() diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 0b0d0b0..980fd04 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -25,13 +25,14 @@ class PySlaveInstance : public cppfmu::SlaveInstance void ExitInitializationMode() override; void Terminate() override; void Reset() override; - cppfmu::FMIStatus DoStep(cppfmu::FMIFloat64 currentCommunicationPoint, +// cppfmu::FMIStatus DoStep(cppfmu::FMIFloat64 currentCommunicationPoint, + void DoStep(cppfmu::FMIFloat64 currentCommunicationPoint, cppfmu::FMIFloat64 communicationStepSize, - cppfmu::FMIBoolean newStep, + cppfmu::FMIBoolean noSetFMUStatePriorToCurrentPoint, cppfmu::FMIBoolean* eventHandlingNeeded, cppfmu::FMIBoolean* terminateSimulation, cppfmu::FMIBoolean* earlyReturn, - cppfmu::FMIFloat64& endOfStep) override; + cppfmu::FMIFloat64* lastSuccessfullTime) override; void SetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* value, std::size_t nValues) override; void SetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt32* value, std::size_t nValues) override; diff --git a/tests/test_builder.py b/tests/test_builder.py index d3ddeec..2414200 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,13 +1,18 @@ +import argparse import itertools import platform import tempfile import zipfile +from attrs import asdict +from fmpy import read_model_description from pathlib import Path import pytest import pythonfmu3 +from pythonfmu3 import builder, deploy from pythonfmu3.builder import FmuBuilder, get_platform +from pythonfmu3.fmi3slave import Fmi3ModelOptionsCS, Fmi3ModelOptionsME PROJECT_TEST_CASES = [ ("dummy.txt",), @@ -200,5 +205,89 @@ def build(): fmu = build() with zipfile.ZipFile(fmu) as files: names = files.namelist() - assert "terminalsAndIcons/terminalsAndIcons.xml" in names + +def test_cli(): + def default_execution(**kwargs): + print("A subcommand must be provided.\n") + parser.print_help() + + parser = argparse.ArgumentParser(prog="pythonfmu") + parser.add_argument("-V", "--version", action="version", version="9.9.9") + + parser.set_defaults(execute=default_execution) + + subparsers = parser.add_subparsers( + title="Subcommands", + description="Call `pythonfmu _command_ -h` to get more help.", + ) + + build_parser = subparsers.add_parser( + "build", + description="Build an FMU from a Python script.", + help="Build an FMU from a Python script.", + ) + builder.create_command_parser(build_parser) + + deploy_parser = subparsers.add_parser( + "deploy", + description="""Deploy a Python FMU. + + The command will look in the `resources` folder for one of the following files: + `requirements.txt` or `environment.yml`. + + If you specify a environment file but no package manager, `conda` will be selected + for `.yaml` and `.yml` otherwise `pip` will be used. + + The tool assume the Python environment in which the FMU should be executed + is the current one. + """, + help="Install Python FMU dependencies.", + ) + deploy.create_command_parser(deploy_parser) + + options = vars(parser.parse_args()) + + + py_file = Path(__file__).parent.resolve().parent / "examples/two_state.py" + #cmd = f" build -f {py_file}" + #print("CMD", cmd) + args = parser.parse_args(["build", "-f", str(py_file), "--hasEventMode", "--fixedInternalStepSize", "0.01"]) + assert args.script_file == str(py_file) + assert not args.needsExecutionTool + assert args.hasEventMode + + assert isinstance(vars(args), dict), "Arguments converted to dict, which is also used in cli" + + dest = builder.FmuBuilder.build_FMU( + script_file = py_file, + dest = Path(__file__).parent.resolve() / "test_working_directory", + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + options = {'hasEventMode':False, 'canHandleVariableCommunicationStepSize':False} + ) + md = read_model_description(dest, True, True, True) + dct = asdict(md.coSimulation) + assert not dct['hasEventMode'], f"'hasEventMode' is {dct['hasEventMode']}!" + assert not dct['canHandleVariableCommunicationStepSize'], f"{dct['canHandleVariableCommunicationStepSize']}!" + print(f"Generated {dest}. Validated modelDescription.") + +def test_model_options(): + mcs = Fmi3ModelOptionsCS(hasEventMode=True, fixedInternalStepSize=0.01) + assert mcs.hasEventMode + assert mcs.fixedInternalStepSize == 0.01 + assert not mcs.providesAdjointDerivatives + assert not mcs.to_dict()['needsExecutionTool'], "Can access all options as dict" + mcs = Fmi3ModelOptionsCS(hasEventMode=True) + assert 'fixedInternalStepSize' not in mcs.to_dict(w_none=False), "Not listed because it is None" + assert 'needsExecutionTool' in mcs.to_dict(), "Listed even if default value" + assert 'needsExecutionTool' not in mcs.to_dict(w_default=False), "Not listed because default value" + + +if __name__ == "__main__": + """Possibility to run single or all tests from the IDE.""" + retcode = 0#pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "True", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + test_cli() + #test_model_options() From bfc7144e594f3282743cc792007416b776d88c6b Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Wed, 19 Aug 2026 22:57:37 +0200 Subject: [PATCH 11/20] Change in builder.py to include model options properly --- src/pythonfmu3/builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pythonfmu3/builder.py b/src/pythonfmu3/builder.py index ae88916..6339193 100644 --- a/src/pythonfmu3/builder.py +++ b/src/pythonfmu3/builder.py @@ -146,14 +146,14 @@ def build_FMU( type_node = xml.find("CoSimulation") cs_option_names = list(Fmi3ModelOptionsCS().to_dict().keys()) - if type_node is not None: + if type_node is not None and 'options' in options: for option, value in options['options'].items(): if option in cs_option_names: type_node.set(option, str(value).lower()) type_node = xml.find("ModelExchange") me_option_names = list(Fmi3ModelOptionsME().to_dict().keys()) - if type_node is not None: + if type_node is not None and 'options' in options: for option, value in options['options'].items(): if option in mx_option_names: type_node.set(option, str(value).lower()) From 7432e2df9e58cca37497d85003c3b7d1155265e2 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Wed, 19 Aug 2026 23:02:33 +0200 Subject: [PATCH 12/20] Corrected errors in fmi3slave.py and variables.py leading to errors in FMU building and FMU usage with respect to variables with dimensions --- src/pythonfmu3/fmi3slave.py | 8 +++++--- src/pythonfmu3/variables.py | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py index 5782886..361f085 100644 --- a/src/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -332,8 +332,10 @@ def register_variable( """Register a variable as FMU interface. Args: - var (ModelVariable): The variable to be registered - nested (bool): Optional, does the "." in the variable name reflect an object hierarchy to access it? Default True + var: The variable to be registered + nested: Optional, does the "." in the variable name reflect an object hierarchy to access it? Default True + var_type: + has_event_indicator: """ variable_reference = len(self.vars) self.vars[variable_reference] = var @@ -361,7 +363,7 @@ def register_variable( var.setter = lambda v: setattr( owner, var.local_name, - np.reshape(v, newshape=getattr(owner, var.local_name).shape), + np.reshape(v, shape=getattr(owner, var.local_name).shape), ) else: var.setter = lambda v: setattr(owner, var.local_name, v) diff --git a/src/pythonfmu3/variables.py b/src/pythonfmu3/variables.py index a2d01c7..6506763 100644 --- a/src/pythonfmu3/variables.py +++ b/src/pythonfmu3/variables.py @@ -211,9 +211,9 @@ def to_xml(self) -> Element: attrib = dict() if self.start: - attrib["start"] = self.start + attrib["start"] = str(self.start) else: - attrib["valueReference"] = self.value_reference + attrib["valueReference"] = str(self.value_reference) ele = Element("Dimension", attrib) return ele @@ -469,7 +469,6 @@ def to_xml(self) -> Element: if self.start is not None: parent.append(self._start.to_xml()) - return parent class Clock(ModelVariable): From bdc012ab8d2e808e98a755e3747c29a8c548aef1 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Wed, 19 Aug 2026 23:04:25 +0200 Subject: [PATCH 13/20] Added an FMI3 FMU 'function'. Tested basic usage. --- examples/function.py | 150 +++++++++++++++++++++++++++++++++++++++ examples/function_use.py | 35 +++++++++ examples/two_state.py | 7 +- 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 examples/function.py create mode 100644 examples/function_use.py diff --git a/examples/function.py b/examples/function.py new file mode 100644 index 0000000..ddaa304 --- /dev/null +++ b/examples/function.py @@ -0,0 +1,150 @@ +"""Implements a function component with n scalar inputs and one output. + The function name can be specified. + + The function triggers only on input events and calculates its output, + i.e. it triggers on update_discrete_states, updating the output, not setting new event times. + do_step only keeps track of time. +""" +import numpy as np +from collections import namedtuple +from pythonfmu3 import Fmi3Slave +from pythonfmu3.enums import ( + Fmi3IntervalVariability, + Fmi3Causality, + Fmi3Variability, + Fmi3Status) +from pythonfmu3.variables import Float64, UInt64, String, Dimension +from pythonfmu3.fmi3slave import ( + Fmi3StepResult, + Fmi3UpdateDiscreteStatesResult, + Fmi3ModelOptionsCS, + ) + +EPS = 1e-14 + +class Function(Fmi3Slave): + """Discrete event Function implementation.""" + + def __init__(self, **kwargs): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True), + 'model_options_me' : None, + }) + super().__init__(**kwargs) + + self.author = "Siegfried Eisinger" + self.description = "Function of its inputs, updating the output" + + # Parameters + self.n_inputs = 2 + self.func_name = "Sum" + + # Inputs + self.inputs: np.ndarray = np.array([0.0]*self.n_inputs, float) + # Outputs + self.output: float = 0.0 + + # Internal states + self.time = 0.0 + self.histogram : list[ tuple[float,int]] = [] + + # Register variables + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self._n_inputs = UInt64("n_inputs", + variability=Fmi3Variability.constant, + causality=Fmi3Causality.local, + start=2) + self.register_variable(self._n_inputs) + self.register_variable(String("func_name", causality=Fmi3Causality.parameter, start="Sum")) + + self.register_variable(Float64("inputs", + causality=Fmi3Causality.input, + dimensions=[Dimension(valueReference=self._n_inputs.value_reference)], + start=1.0)) + self.register_variable(Float64("output", causality=Fmi3Causality.output)) + + + def calc(self)-> float: + """Re-calculate the function.""" + if self.func_name == "sum": + return sum( x for x in self.inputs) + elif self.func_name == "and": + res = True + for x in self.inputs: + if not x: + return 0.0 + return 1.0 + elif self.func_name == "or": + res = False + for x in self.inputs: + if x: + return 1.0 + return 0.0 + elif self.func_name == "min": + return min(self.inputs) + elif self.func_name == "max": + return max(self.inputs) + else: + raise NotImplementedError(f"The function {self.func_name} is so far not included.") from None + + def enter_event_mode(self): + """Signal from orchestrator that an event has occured.""" + # print(f"EnterEventMode @{self.time}") + pass + + def enter_step_mode(self): + """Signal from orchestrator that events are dealt with and switching back to step mode.""" + # print(f"EnterStepMode @{self.time}") + pass + + def exit_initialization_mode(self): + # print(f"Initialization finished") + pass + + def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): + self.time = current_time + step_size + self.histogram.append( (current_time, self.output)) + self.output = self.calc() + return Fmi3StepResult(eventHandlingNeeded = False, lastSuccessfullTime = self.time) + + def update_discrete_states(self): + """Call by Orchestrator to update discrete states in super-dense time instant, + returning info to the Orchestrator + + Returns: + discreteStatesNeedUpdate: True; the Orchestrator must stay in Event Mode for another event iteration, + starting a new super-dense time instant. + terminateSimulation: True; the FMU requests to stop the simulation. + nominalsOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: True; use only for ME! + nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. + nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. + The importer must compute up to nextEventTime (or if needed slightly further) + and then enter Event Mode using EnterEventMode. + The FMU must handle this time event during the Event Mode that is entered + by the first call to EnterEventMode, at or after nextEventTime. + If another (e.g. state event) event happens before that nextEventTime, + the previous definition of nextEventTime becomes obsolete. + """ + before_update = self.output + after_update = self.calc() + if before_update != after_update: + self.output = after_update + # make sure that all inputs are updated and loops are managed. + return Fmi3UpdateDiscreteStatesResult( discreteStatesNeedUpdate=True) + else: + return Fmi3UpdateDiscreteStatesResult() + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") \ No newline at end of file diff --git a/examples/function_use.py b/examples/function_use.py new file mode 100644 index 0000000..7df2633 --- /dev/null +++ b/examples/function_use.py @@ -0,0 +1,35 @@ +# Example of co-simulation setup +import fmpy +from fmpy import simulate_fmu +import matplotlib.pyplot as plt +import numpy as np + +# Simulate the PID controller FMU +result = simulate_fmu( + filename='Function.fmu', + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'func_name' : "sum", + 'inputs' : np.array([1.0, 2.0], float), + }, + output = ['output'], +) + +# Plot results +plt.figure(figsize=(12, 6)) +plt.subplot(2, 1, 1) +plt.plot(result['time'], result['output'], label='Output') +#plt.plot(result['time'], result['measurement'], label='Measurement') +plt.ylabel('Value') +plt.legend() +plt.grid(True) + +# plt.subplot(2, 1, 2) +# plt.plot(result['time'], result['state'], label='State') +# plt.xlabel('Time [s]') +# plt.ylabel('Control Signal') +# plt.legend() +# plt.grid(True) +plt.show() \ No newline at end of file diff --git a/examples/two_state.py b/examples/two_state.py index c61f5dd..5f483f4 100644 --- a/examples/two_state.py +++ b/examples/two_state.py @@ -1,9 +1,10 @@ -"""Implements a two-state discrete event system with states `working=1` and 'failed=0` +"""Implements a two-state discrete event component with states `working=1` and 'failed=0` The MTTF and MTTR are fixed parameters and the actual TTF is exponentially distributed, while TTR is modelled as constant time interval. - When `enterEventMode` is called, the state changes and a new time interval is calculated, - which is transmitted to the orchestrator with a `getIntervalDecimal` call. + When `update_discrete_states` is called, the state changes and a new time interval is calculated, + which is transmitted to the orchestrator as one of its return values. + Clocks are not used in this version. """ import numpy as np from collections import namedtuple From 1f96296be7da4e6b855c222bcbf08a6537ee44b9 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Fri, 28 Aug 2026 07:59:58 +0200 Subject: [PATCH 14/20] Adapted function signatures to be as close as possible to FMI3 standard. SetupExperiment deprecated, EnterInitializationMode updated, EnterContinuousTimeMode and CompletedIntegratorStep added. Recompiled and basic tests. --- src/pythonfmu3/fmi3slave.py | 12 +++- .../pythonfmu-export/src/cppfmu/cppfmu_cs.cpp | 35 +++++++--- .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 18 +++-- .../src/cppfmu/fmi_functions.cpp | 28 ++++++-- .../src/pythonfmu/PySlaveInstance.cpp | 69 +++++++++++++++++-- .../src/pythonfmu/PySlaveInstance.hpp | 14 +++- 6 files changed, 151 insertions(+), 25 deletions(-) diff --git a/src/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py index 361f085..80729b2 100644 --- a/src/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -379,19 +379,27 @@ def register_event_indicator(self, vr): self.event_indicators.append(vr) def setup_experiment(self, start_time: float): + self.log("setup_experiment() is not defined within FMI3. Use enter_initialization_mode()") pass def register_units(self, units: list[Unit]): for unit in units: self.units[unit.name] = unit - def enter_initialization_mode(self): + def enter_initialization_mode(self, + tolerance : float|None = None, + startTime : float = 0.0, + stopTime : float|None = None): pass def exit_initialization_mode(self): pass - def do_step(self, current_time: float, step_size: float) -> Fmi3StepResult: + def do_step(self, + current_time: float, + step_size: float, + *, + noSetFmuStatePriorToCurrentPoint:bool = False) -> Fmi3StepResult: pass def terminate(self): diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp index d26fdd3..4cae4cc 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp @@ -16,23 +16,30 @@ namespace cppfmu // ============================================================================= +/* not defined in FMI3. Changed EnterInitializationMode takes this role! void SlaveInstance::SetupExperiment( - FMIBoolean /*toleranceDefined*/, - FMIFloat64 /*tolerance*/, - FMIFloat64 /*tStart*/, - FMIBoolean /*stopTimeDefined*/, - FMIFloat64 /*tStop*/) + FMIBoolean, //toleranceDefined, + FMIFloat64, //tolerance + FMIFloat64, //tStart + FMIBoolean, //stopTimeDefined + FMIFloat64, //tStop + ) { // Do nothing } +*/ - -void SlaveInstance::EnterInitializationMode() +void SlaveInstance::EnterInitializationMode( + FMIBoolean, //toleranceDefined, + FMIFloat64, //tolerance + FMIFloat64, //tStart + FMIBoolean, //stopTimeDefined + FMIFloat64 //stopTime + ) { // Do nothing } - void SlaveInstance::ExitInitializationMode() { // Do nothing @@ -209,6 +216,18 @@ void SlaveInstance::EnterStepMode() { // Do nothing } +void SlaveInstance::EnterContinuousTimeMode() +{ + + // Do nothing +} +void SlaveInstance::CompletedIntegratorStep( + fmi3Boolean noSetFMUStatePriorToCurrentPoint, + fmi3Boolean* enterEventMode, + fmi3Boolean* terminateSimulation) +{ + // Do nothing +} /*void SlaveInstance::UpdateDiscreteStates( cppfmu::FMIBoolean* discreteStatesNeedUpdate, cppfmu::FMIBoolean* terminateSimulation, diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 50da143..3e0205e 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -36,6 +36,7 @@ class SlaveInstance * (FMI 1.0). * Does nothing by default. */ + /* not in FMI3: virtual void SetupExperiment( FMIBoolean toleranceDefined, FMIFloat64 tolerance, @@ -43,14 +44,18 @@ class SlaveInstance FMIBoolean stopTimeDefined, FMIFloat64 tStop); - /* Called from fmi3EnterInitializationMode() (FMI 2.0) or + * Called from fmi3EnterInitializationMode() (FMI 2.0) or * fmiInitializeSlave() (FMI 1.0). * Does nothing by default. */ - virtual void EnterInitializationMode(); + virtual void EnterInitializationMode( + FMIBoolean toleranceDefined, + FMIFloat64 tolerance, + FMIFloat64 tStart, + FMIBoolean stopTimeDefined, + FMIFloat64 stopTime); - /* Called from fmi3ExitInitializationMode() (FMI 2.0) or - * fmiInitializeSlave() (FMI 1.0). + /* Changed signature in FMI3! * Does nothing by default. */ virtual void ExitInitializationMode(); @@ -145,6 +150,11 @@ class SlaveInstance // Event- and Clock-related functions virtual void EnterEventMode(); + virtual void EnterContinuousTimeMode(); + virtual void CompletedIntegratorStep( + fmi3Boolean noSetFMUStatePriorToCurrentPoint, + fmi3Boolean* enterEventMode, + fmi3Boolean* terminateSimulation); virtual void EnterStepMode(); virtual void GetClock( const FMIValueReference vr[], std::size_t nvr, FMIClock values[]) const; virtual void SetClock( const FMIValueReference vr[], std::size_t nvr, const FMIClock values[]); diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp index 5fb8644..ba529da 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp @@ -205,11 +205,17 @@ fmi3Status fmi3SetDebugLogging( return fmi3OK; } -fmi3Status fmi3EnterInitializationMode(fmi3Instance c, fmi3Boolean, fmi3Float64, fmi3Float64, fmi3Boolean, fmi3Float64) +fmi3Status fmi3EnterInitializationMode(fmi3Instance c, + fmi3Boolean toleranceDefined, + fmi3Float64 tolerance, + fmi3Float64 startTime, + fmi3Boolean stopTimeDefined, + fmi3Float64 stopTime + ) { const auto component = reinterpret_cast(c); try { - component->slave->EnterInitializationMode(); + component->slave->EnterInitializationMode(toleranceDefined, tolerance, startTime, stopTimeDefined, stopTime); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -717,6 +723,8 @@ fmi3Status fmi3UpdateDiscreteStates(fmi3Instance c, fmi3Status fmi3EnterContinuousTimeMode(fmi3Instance c) { + const auto component = reinterpret_cast(c); + component->state = Component::State::ContinuousTimeMode; return fmi3OK; } @@ -726,8 +734,20 @@ fmi3Status fmi3CompletedIntegratorStep( fmi3Boolean* enterEventMode, fmi3Boolean* terminateSimulation) { - *enterEventMode = false; - *terminateSimulation = false; + const auto component = reinterpret_cast(c); + try { + component->slave->CompletedIntegratorStep( + noSetFMUStatePriorToCurrentPoint, + enterEventMode, + terminateSimulation); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } return fmi3OK; } diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index 5c47d94..b36571d 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -166,6 +166,7 @@ void PySlaveInstance::initialize(PyGILState_STATE gilState) pMessages_ = PyObject_CallMethod(pInstance_, "_get_log_queue", nullptr); } +/* not included in FMI3: void PySlaveInstance::SetupExperiment(cppfmu::FMIBoolean, cppfmu::FMIFloat64, cppfmu::FMIFloat64 startTime, cppfmu::FMIBoolean, cppfmu::FMIFloat64) { py_safe_run([this, startTime](PyGILState_STATE gilState) { @@ -177,11 +178,23 @@ void PySlaveInstance::SetupExperiment(cppfmu::FMIBoolean, cppfmu::FMIFloat64, cp clearLogBuffer(); }); } - -void PySlaveInstance::EnterInitializationMode() + cppfmu::FMIBoolean toleranceDefined, + cppfmu::FMIFloat64 tolerance, + cppfmu::FMIFloat64 tStart, + cppfmu::FMIBoolean stopTimeDefined, + cppfmu::FMIFloat64 tStop) override; + */ +// As defined in FMI3 standard: +void PySlaveInstance::EnterInitializationMode( + cppfmu::FMIBoolean toleranceDefined, + cppfmu::FMIFloat64 tolerance, + cppfmu::FMIFloat64 startTime, + cppfmu::FMIBoolean stopTimeDefined, + cppfmu::FMIFloat64 stopTime + ) { - py_safe_run([this](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "enter_initialization_mode", nullptr); + py_safe_run([this, toleranceDefined, tolerance, startTime, stopTimeDefined, stopTime](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "enter_initialization_mode", "(ddd)", tolerance, startTime, stopTime); if (f == nullptr) { handle_py_exception("[enterInitializationMode] PyObject_CallMethod", gilState); } @@ -217,7 +230,7 @@ void PySlaveInstance::DoStep( py_safe_run([this, &fmuStatus, currentTime, stepSize, eventHandlingNeeded, terminateSimulation, earlyReturn](PyGILState_STATE gilState) { */ py_safe_run([this, currentTime, stepSize, noSetFmuStatePriorToCurrentPoint, eventHandlingNeeded, terminateSimulation, earlyReturn, lastSuccessfullTime](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "do_step", "(dd)", currentTime, stepSize, noSetFmuStatePriorToCurrentPoint); + auto f = PyObject_CallMethod(pInstance_, "do_step", "(ddi)", currentTime, stepSize, noSetFmuStatePriorToCurrentPoint); if (f == nullptr) { handle_py_exception("[doStep] PyObject_CallMethod", gilState); } @@ -851,6 +864,52 @@ void PySlaveInstance::EnterEventMode() }); } +void PySlaveInstance::EnterContinuousTimeMode() +{ + py_safe_run([this](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "enter_continuous_time_mode", nullptr); + if (f == nullptr) { + handle_py_exception("[enterContinuousTimeMode] PyObject_CallMethod", gilState); + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + +void PySlaveInstance::CompletedIntegratorStep( + cppfmu::FMIBoolean noSetFMUStatePriorToCurrentPoint, + cppfmu::FMIBoolean* enterEventMode, + cppfmu::FMIBoolean* terminateSimulation) +{ + py_safe_run([this, noSetFMUStatePriorToCurrentPoint, &enterEventMode, &terminateSimulation](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "completed_integrator_step", "(p)", noSetFMUStatePriorToCurrentPoint); + if (f == nullptr) { + handle_py_exception("[completedIntegratorStep] PyObject_CallMethod", gilState); + } + if (PyObject_HasAttrString(f, "enterEventMode")) { + PyObject* pyEnterEventMode = PyObject_GetAttrString(f, "enterEventMode"); + if (pyEnterEventMode) { + *enterEventMode = static_cast(PyObject_IsTrue(pyEnterEventMode)); + Py_DECREF(pyEnterEventMode); + } + } else { + *enterEventMode = false; + } + if (PyObject_HasAttrString(f, "terminateSimulation")) { + PyObject* pyTerminateSimulation = PyObject_GetAttrString(f, "terminateSimulation"); + if (pyTerminateSimulation) { + *terminateSimulation = static_cast(PyObject_IsTrue(pyTerminateSimulation)); + Py_DECREF(pyTerminateSimulation); + } + } else { + *terminateSimulation = false; + } + Py_DECREF(f); + clearLogBuffer(); + }); +} + + void PySlaveInstance::EnterStepMode() { py_safe_run([this](PyGILState_STATE gilState) { diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 980fd04..724e268 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -20,8 +20,13 @@ class PySlaveInstance : public cppfmu::SlaveInstance void initialize(PyGILState_STATE gilState); - void SetupExperiment(cppfmu::FMIBoolean toleranceDefined, cppfmu::FMIFloat64 tolerance, cppfmu::FMIFloat64 tStart, cppfmu::FMIBoolean stopTimeDefined, cppfmu::FMIFloat64 tStop) override; - void EnterInitializationMode() override; + // not defined: void SetupExperiment(cppfmu::FMIBoolean toleranceDefined, cppfmu::FMIFloat64 tolerance, cppfmu::FMIFloat64 tStart, cppfmu::FMIBoolean stopTimeDefined, cppfmu::FMIFloat64 tStop) override; + void EnterInitializationMode( + cppfmu::FMIBoolean toleranceDefined, + cppfmu::FMIFloat64 tolerance, + cppfmu::FMIFloat64 startTime, + cppfmu::FMIBoolean stopTimeDefined, + cppfmu::FMIFloat64 stopTime) override; void ExitInitializationMode() override; void Terminate() override; void Reset() override; @@ -54,6 +59,11 @@ class PySlaveInstance : public cppfmu::SlaveInstance // Event- and Clock-related methods void EnterEventMode() override; + void EnterContinuousTimeMode() override; + void CompletedIntegratorStep( + cppfmu::FMIBoolean noSetFMUStatePriorToCurrentPoint, + cppfmu::FMIBoolean* enterEventMode, + cppfmu::FMIBoolean* terminateSimulation); void EnterStepMode() override; void GetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIClock* values) const override; void SetClock(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIClock* values) override; From 8581c7c46e5ef06e1e0384ec814ac08614840987 Mon Sep 17 00:00:00 2001 From: Siegfried Eisinger Date: Mon, 7 Sep 2026 23:25:46 +0200 Subject: [PATCH 15/20] Added the arguments 'eventModeUsed' and 'earlyReturnAllowed' in InstantiateCosimulation, which where so far not included. Related changes in fmi3slave are also implemented. Compiles and test works. IntermediateUpdate management is still not included. --- src/pythonfmu3/__init__.py | 3 +- src/pythonfmu3/builder.py | 3 +- src/pythonfmu3/cosimulation.py | 8 +- src/pythonfmu3/fmi3slave.py | 126 +++--------------- src/pythonfmu3/model_options.py | 58 ++++++++ src/pythonfmu3/model_returns.py | 27 ++++ src/pythonfmu3/modelexchange.py | 11 +- .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 6 +- .../src/cppfmu/fmi_functions.cpp | 14 +- .../src/pythonfmu/PySlaveInstance.cpp | 25 +++- .../src/pythonfmu/PySlaveInstance.hpp | 13 +- 11 files changed, 158 insertions(+), 136 deletions(-) create mode 100644 src/pythonfmu3/model_options.py create mode 100644 src/pythonfmu3/model_returns.py diff --git a/src/pythonfmu3/__init__.py b/src/pythonfmu3/__init__.py index 273753d..de25639 100644 --- a/src/pythonfmu3/__init__.py +++ b/src/pythonfmu3/__init__.py @@ -3,7 +3,8 @@ from .cosimulation import CoSimulation from .default_experiment import DefaultExperiment from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability, Fmi3IntervalVariability -from .fmi3slave import Fmi3Slave, Fmi3SlaveBase, Fmi3StepResult +from .fmi3slave import Fmi3Slave, Fmi3SlaveBase +from .cosimulation import Fmi3StepResult from .modelexchange import Fmi3UpdateDiscreteStatesResult, ModelExchange from .unit import BaseUnit, Unit from .variable_types import EnumerationType, Float64Type diff --git a/src/pythonfmu3/builder.py b/src/pythonfmu3/builder.py index 6339193..fe312d8 100644 --- a/src/pythonfmu3/builder.py +++ b/src/pythonfmu3/builder.py @@ -15,7 +15,8 @@ from xml.dom.minidom import parseString from xml.etree.ElementTree import Element, tostring -from pythonfmu3.fmi3slave import Fmi3ModelOptionsCS, Fmi3ModelOptionsME, Fmi3SlaveBase +from pythonfmu3.fmi3slave import Fmi3SlaveBase +from pythonfmu3.model_options import Fmi3ModelOptionsCS, Fmi3ModelOptionsME from .osutil import get_lib_extension, get_platform diff --git a/src/pythonfmu3/cosimulation.py b/src/pythonfmu3/cosimulation.py index f496a30..9dbc7cb 100644 --- a/src/pythonfmu3/cosimulation.py +++ b/src/pythonfmu3/cosimulation.py @@ -1,8 +1,12 @@ from abc import ABC, abstractmethod - +from pythonfmu3.model_returns import Fmi3StepResult # co-simulation mixin class CoSimulation(ABC): @abstractmethod - def do_step(self, current_time: float, step_size: float): + def do_step(self, + current_time: float, + step_size: float, + *, + noSetFmuStatePriorToCurrentPoint:bool = False) -> Fmi3StepResult: pass diff --git a/src/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py index 80729b2..e12e766 100644 --- a/src/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -9,15 +9,16 @@ from uuid import uuid1 from xml.etree.ElementTree import Element, SubElement -from ._version import __version__ as VERSION -from .cosimulation import CoSimulation -from .default_experiment import DefaultExperiment -from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability -from .logmsg import LogMsg -from .modelexchange import ModelExchange -from .unit import Unit -from .variable_types import VariableType -from .variables import ( +from pythonfmu3._version import __version__ as VERSION +from pythonfmu3.cosimulation import CoSimulation +from pythonfmu3.default_experiment import DefaultExperiment +from pythonfmu3.enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability +from pythonfmu3.logmsg import LogMsg +from pythonfmu3.modelexchange import ModelExchange +from pythonfmu3.model_options import Fmi3ModelOptionsCS, Fmi3ModelOptionsME +from pythonfmu3.unit import Unit +from pythonfmu3.variable_types import VariableType +from pythonfmu3.variables import ( Boolean, Enumeration, Float64, @@ -28,99 +29,6 @@ UInt64, ) -#Note: all Bool options are false by default and the name expresses the option as True. -# The cli uses therefor the original name to add the option as implicit True (non-default) -# other options are added with their (non-default) numbers -@dataclass(kw_only=True, frozen=True, slots=True) -class Fmi3ModelOptionsCommon: - """Model options common for Co-Simulation and Model Exchange.""" - needsExecutionTool: bool = False - canBeInstantiatedOnlyOncePerProcess: bool = False - canGetAndSetFMUState: bool = False - canSerializeFMUState: bool = False - providesDirectionalDerivatives: bool = False - providesAdjointDerivatives: bool = False - providesPerElementDependencies: bool = False - providesEvaluateDiscreteStates: bool = False - - def to_dict(self, *, w_default:bool= True, w_none: bool = True): - """A special version of 'asdict', which also allows for choice what to include. - Note: self.do_dict() == asdict(self) if the default value is None. - - Args: - w_defaults: include also the elements with default value - w_none: include also elements with a 'None' value - """ - dct: dict[str, bool|int|float] = {} - for _f in fields(self): - value = getattr(self,_f.name) - if ((value is not None and value != _f.default) or - (value is not None and value == _f.default and w_default) or - (value is None and value == _f.default and w_default and w_none) or - (value is None and value != _f.default and w_none) - ): - dct.update({_f.name : value}) - return dct - -@dataclass(kw_only=True, frozen=True, slots=True) -class Fmi3ModelOptionsCS(Fmi3ModelOptionsCommon): - """Model options for Co-Simulation.""" - canHandleVariableCommunicationStepSize: bool = False - fixedInternalStepSize: float|None = None - maxOutputDerivativeOrder: int = 0 # uInt - recommendedIntermediateInputSmoothness: int = 0 - providesIntermediateUpdate: bool = False - mightReturnEarlyFromDoStep: bool = False - canReturnEarlyAfterIntermediateUpdate: bool = False - hasEventMode: bool = False - -@dataclass(kw_only=True, frozen=True, slots=True) -class Fmi3ModelOptionsME(Fmi3ModelOptionsCommon): - """Model options for Model Exchange.""" - needsCompletedIntegratorStep: bool = False - - -# -# FMI3_MODEL_OPTIONS_COMMON: list[ModelOptions] = [ -# ModelOptions("needsExecutionTool", False), -# ModelOptions("canBeInstantiatedOnlyOncePerProcess", False), -# ModelOptions("canGetAndSetFMUState", False), -# ModelOptions("canSerializeFMUState", False), -# ModelOptions("providesDirectionalDerivatives", False), -# ModelOptions("providesAdjointDerivatives", False), -# ModelOptions("providesPerElementDependencies", False), -# ModelOptions("providesEvaluateDiscreteStates", False), -# ] -# -# FMI3_MODEL_OPTIONS_CS: list[ModelOptions] = [ -# ModelOptions("canHandleVariableCommunicationStepSize", True), -# ModelOptions("fixedInternalStepSize", None), # double -# ModelOptions("maxOutputDerivativeOrder", 0), # uInt -# ModelOptions("recommendedIntermediateInputSmoothness", 0), # int -# ModelOptions("providesIntermediateUpdate", False), -# ModelOptions("mightReturnEarlyFromDoStep", False), -# ModelOptions("canReturnEarlyAfterIntermediateUpdate", False), -# ModelOptions("hasEventMode", False), -# ] -# -# FMI3_MODEL_OPTIONS_ME: list[ModelOptions] = [ -# ModelOptions("needsCompletedIntegratorStep", False), -# ] - -class Fmi3StepResult(NamedTuple): - eventHandlingNeeded: bool = False - terminateSimulation: bool = False - earlyReturn: bool = False - lastSuccessfullTime: Float64 = 0.0 - -class Fmi3UpdateDiscreteStatesResult(NamedTuple): - discreteStatesNeedUpdate: bool = False - terminateSimulation: bool = False - nominalContinuousStatesChanged: bool = False # (only ME) - valuesOfContinuousStatesChanged: bool = False - nextEventTimeDefined: bool = False - nextEventTime: Float64 = 0.0 - class Fmi3SlaveBase: """Abstract facade class to execute Python through FMI standard.""" @@ -140,6 +48,8 @@ def __init__(self, **kwargs): self.instance_name = kwargs["instance_name"] self.resources = kwargs.get("resources", None) self.visible = kwargs.get("visible", False) + self.eventModeUsed = kwargs.get("eventModeUsed", False) + self.earlyReturnAllowed = kwargs.get("earlyReturnAllowed", False) self.model_options_cs = kwargs.get("model_options_cs", Fmi3ModelOptionsCS()) self.model_options_me = kwargs.get("model_options_me", Fmi3ModelOptionsME()) self.log_queue = [] @@ -395,12 +305,12 @@ def enter_initialization_mode(self, def exit_initialization_mode(self): pass - def do_step(self, - current_time: float, - step_size: float, - *, - noSetFmuStatePriorToCurrentPoint:bool = False) -> Fmi3StepResult: - pass +# def do_step(self, +# current_time: float, +# step_size: float, +# *, +# noSetFmuStatePriorToCurrentPoint:bool = False) -> Fmi3StepResult: +# pass def terminate(self): pass diff --git a/src/pythonfmu3/model_options.py b/src/pythonfmu3/model_options.py new file mode 100644 index 0000000..d0c3267 --- /dev/null +++ b/src/pythonfmu3/model_options.py @@ -0,0 +1,58 @@ +"""Contains modelOptions for both CoSimulation and ModelExchange, defined as dataclass. +These are included in several modules and should therefore be defined separately +""" +from dataclasses import dataclass, fields +from typing import NamedTuple + +#Note: all Bool options are false by default and the name expresses the option as True. +# The cli uses therefore the original name to add the option as implicit True (non-default) +# other options are added with their (non-default) numbers +@dataclass(kw_only=True, frozen=True, slots=True) +class Fmi3ModelOptionsCommon: + """Model options common for Co-Simulation and Model Exchange.""" + needsExecutionTool: bool = False + canBeInstantiatedOnlyOncePerProcess: bool = False + canGetAndSetFMUState: bool = False + canSerializeFMUState: bool = False + providesDirectionalDerivatives: bool = False + providesAdjointDerivatives: bool = False + providesPerElementDependencies: bool = False + providesEvaluateDiscreteStates: bool = False + + def to_dict(self, *, w_default:bool= True, w_none: bool = True): + """A special version of 'asdict', which also allows for choice what to include. + Note: self.do_dict() == asdict(self) if the default value is None. + + Args: + w_defaults: include also the elements with default value + w_none: include also elements with a 'None' value + """ + dct: dict[str, bool|int|float] = {} + for _f in fields(self): + value = getattr(self,_f.name) + if ((value is not None and value != _f.default) or + (value is not None and value == _f.default and w_default) or + (value is None and value == _f.default and w_default and w_none) or + (value is None and value != _f.default and w_none) + ): + dct.update({_f.name : value}) + return dct + +@dataclass(kw_only=True, frozen=True, slots=True) +class Fmi3ModelOptionsCS(Fmi3ModelOptionsCommon): + """Model options for Co-Simulation.""" + canHandleVariableCommunicationStepSize: bool = False + fixedInternalStepSize: float|None = None + maxOutputDerivativeOrder: int = 0 # uInt + recommendedIntermediateInputSmoothness: int = 0 + providesIntermediateUpdate: bool = False + mightReturnEarlyFromDoStep: bool = False + canReturnEarlyAfterIntermediateUpdate: bool = False + hasEventMode: bool = False + + +@dataclass(kw_only=True, frozen=True, slots=True) +class Fmi3ModelOptionsME(Fmi3ModelOptionsCommon): + """Model options for Model Exchange.""" + needsCompletedIntegratorStep: bool = False + diff --git a/src/pythonfmu3/model_returns.py b/src/pythonfmu3/model_returns.py new file mode 100644 index 0000000..9752a6f --- /dev/null +++ b/src/pythonfmu3/model_returns.py @@ -0,0 +1,27 @@ +"""Contains model return classes for both CoSimulation and ModelExchange, +i.e. FMI3 function returns as NamedTuple +Defined selarately, so that they can be imported where needed. +""" +from typing import NamedTuple + + +class Fmi3StepResult(NamedTuple): + eventHandlingNeeded: bool = False + terminateSimulation: bool = False + earlyReturn: bool = False + lastSuccessfullTime: "Float64" = 0.0 + +class Fmi3UpdateDiscreteStatesResult(NamedTuple): + discreteStatesNeedUpdate: bool = False + terminateSimulation: bool = False + nominalContinuousStatesChanged: bool = False # (only ME) + valuesOfContinuousStatesChanged: bool = False + nextEventTimeDefined: bool = False + nextEventTime: float = 0.0 + + +class Fmi3StepResult(NamedTuple): + eventHandlingNeeded: bool = False + terminateSimulation: bool = False + earlyReturn: bool = False + lastSuccessfullTime: "Float64" = 0.0 diff --git a/src/pythonfmu3/modelexchange.py b/src/pythonfmu3/modelexchange.py index eb3328d..21065d2 100644 --- a/src/pythonfmu3/modelexchange.py +++ b/src/pythonfmu3/modelexchange.py @@ -1,15 +1,6 @@ from abc import ABC, ABCMeta, abstractmethod from dataclasses import dataclass - - -@dataclass -class Fmi3UpdateDiscreteStatesResult: - discreteStateNeedsUpdate: bool = False - terminateSimulation: bool = False - nominalsOfContinuousStatesChanged: bool = False - valuesOfContinuousStatesChanged: bool = False - nextEventTimeDefined: bool = False - nextEventTime: float = 0.0 +from pythonfmu3.model_returns import Fmi3UpdateDiscreteStatesResult class RequireTimeMeta(ABCMeta): diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 3e0205e..e8b2833 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -252,7 +252,9 @@ std::unique_ptr CppfmuInstantiateSlave( cppfmu::FMIFloat64 timeout, cppfmu::FMIBoolean visible, cppfmu::FMIBoolean interactive, - const cppfmu::Logger& logger); - + const cppfmu::Logger& logger, + cppfmu::FMIBoolean eventModeUsed, + cppfmu::FMIBoolean earlyReturnAllowed + ); #endif // header guard diff --git a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp index ba529da..fc7c37e 100644 --- a/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp @@ -103,11 +103,13 @@ fmi3Instance fmi3InstantiateCoSimulation( instanceName, instantiationToken, fmuResourceLocation, - "application/x-fmu-sharedlibrary", - 0.0, + "application/x-fmu-sharedlibrary", // mimeType + 0.0, // timeout visible, - cppfmu::FMIFalse, - component->logger); + cppfmu::FMIFalse, // interactive + component->logger, + eventModeUsed, + earlyReturnAllowed); return component; } catch (const cppfmu::FatalError& e) { if (logMessage) @@ -146,7 +148,9 @@ fmi3Instance fmi3InstantiateModelExchange( 0.0, visible, cppfmu::FMIFalse, - component->logger); + component->logger, + cppfmu::FMIFalse, // dummy added for compatibility with CoSimulation + cppfmu::FMIFalse); // dummy added for compatibility with CoSimulation return component; } catch (const cppfmu::FatalError& e) { if (logMessage) diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index b36571d..db7aca4 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -47,12 +47,21 @@ inline void py_safe_run(const std::function& f) PyGILState_Release(gil_state); } -PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible, std::shared_ptr pyState) +PySlaveInstance::PySlaveInstance(std::string instanceName, + std::string resources, + const cppfmu::Logger& logger, + const bool visible, + const bool eventModeUsed, + const bool earlyReturnAllowed, + std::shared_ptr pyState + ) : pyState_{ std::move(pyState) } , instanceName_(std::move(instanceName)) , resources_(std::move(resources)) , logger_(logger) , visible_(visible) + , eventModeUsed_(eventModeUsed) + , earlyReturnAllowed_(earlyReturnAllowed) { py_safe_run([this](PyGILState_STATE gilState) { // Append resources path to python sys path @@ -152,11 +161,14 @@ void PySlaveInstance::initialize(PyGILState_STATE gilState) Py_XDECREF(pMessages_); PyObject* args = PyTuple_New(0); - PyObject* kwargs = Py_BuildValue("{ss,ss,sn,si}", + PyObject* kwargs = Py_BuildValue("{ss,ss,sn,si,si,si}", "instance_name", instanceName_.c_str(), "resources", resources_.c_str(), "logger", &logger_, - "visible", visible_); + "visible", visible_, + "eventModeUsed", eventModeUsed_, + "earlyReturnAllowed", earlyReturnAllowed_ + ); pInstance_ = PyObject_Call(pClass_, args, kwargs); Py_DECREF(args); Py_DECREF(kwargs); @@ -1278,7 +1290,10 @@ std::unique_ptr CppfmuInstantiateSlave( cppfmu::FMIFloat64, cppfmu::FMIBoolean visible, cppfmu::FMIBoolean, - const cppfmu::Logger& logger) + const cppfmu::Logger& logger, + cppfmu::FMIBoolean eventModeUsed, + cppfmu::FMIBoolean earlyReturnAllowed + ) { // Convert URI %20 to space @@ -1301,7 +1316,7 @@ std::unique_ptr CppfmuInstantiateSlave( ensurePyStateAlive(); return std::make_unique( - instanceName, resources, logger, visible, pyState); + instanceName, resources, logger, visible, eventModeUsed, earlyReturnAllowed, pyState); } } diff --git a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 724e268..250c6dd 100644 --- a/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/src/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -16,7 +16,15 @@ class PySlaveInstance : public cppfmu::SlaveInstance { public: - PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, bool visible, std::shared_ptr pyState); + PySlaveInstance( + std::string instanceName, + std::string resources, + const cppfmu::Logger& logger, + bool visible, + bool eventModeUsed, + bool earlyReturnAllowed, + std::shared_ptr pyState + ); void initialize(PyGILState_STATE gilState); @@ -131,10 +139,11 @@ class PySlaveInstance : public cppfmu::SlaveInstance PyObject* pMessages_{}; const bool visible_; + const bool eventModeUsed_; + const bool earlyReturnAllowed_; const std::string instanceName_; const std::string resources_; const cppfmu::Logger& logger_; - mutable std::vector strBuffer; mutable std::vector logStrBuffer; From fd4ab8327fc149c7fee2d3e3d378b5222d499511 Mon Sep 17 00:00:00 2001 From: Eisinger Date: Fri, 9 Oct 2026 12:34:35 +0200 Subject: [PATCH 16/20] Some last changes as part of moving back to windows. Windows compilation not yet included. --- examples/bouncingBall.py | 220 ++++++++++------ examples/driving_force3.py | 125 +++++++++ examples/function.py | 47 ++-- examples/function_use.py | 35 --- examples/linear_transform.py | 112 ++++++++ examples/oscillator3.py | 159 +++++++++++ examples/pid_use.py | 39 --- examples/table.py | 162 ++++++++++++ examples/two_state.py | 38 +-- examples/two_state_use.py | 36 --- pyproject.toml | 2 +- src/pythonfmu3/fmi3slave.py | 14 +- src/pythonfmu3/pythonfmu-export/build_unix.sh | 0 tests/conftest.py | 11 + tests/simulation.py | 247 ++++++++++++++++++ tests/test_bouncing_ball.py | 71 +++++ tests/test_cli.py | 167 ++++++++++++ tests/test_event_functions.py | 43 +++ tests/test_fmus.py | 176 +++++++++++++ tests/test_oscillator3.py | 94 +++++++ tests/test_working_directory/.gitignore | 4 + 21 files changed, 1572 insertions(+), 230 deletions(-) create mode 100644 examples/driving_force3.py delete mode 100644 examples/function_use.py create mode 100644 examples/linear_transform.py create mode 100644 examples/oscillator3.py delete mode 100644 examples/pid_use.py create mode 100644 examples/table.py delete mode 100644 examples/two_state_use.py mode change 100755 => 100644 src/pythonfmu3/pythonfmu-export/build_unix.sh create mode 100644 tests/simulation.py create mode 100644 tests/test_bouncing_ball.py create mode 100755 tests/test_cli.py create mode 100644 tests/test_event_functions.py create mode 100644 tests/test_fmus.py create mode 100644 tests/test_oscillator3.py create mode 100755 tests/test_working_directory/.gitignore diff --git a/examples/bouncingBall.py b/examples/bouncingBall.py index ec44975..3dfd96d 100644 --- a/examples/bouncingBall.py +++ b/examples/bouncingBall.py @@ -1,21 +1,32 @@ -from pythonfmu3 import ( - Float64, - Fmi3Causality, - Fmi3Initial, - Fmi3Slave, - Fmi3Status, - Fmi3StepResult, - Fmi3Variability, - Unit, -) +import logging +import numpy as np + +from pythonfmu3 import Fmi3Slave +from pythonfmu3.cosimulation import Fmi3StepResult +from pythonfmu3.enums import Fmi3Causality, Fmi3Initial, Fmi3Variability +from pythonfmu3.model_options import Fmi3ModelOptionsCS +from pythonfmu3.model_returns import Fmi3UpdateDiscreteStatesResult +from pythonfmu3.unit import Unit +from pythonfmu3.variables import Float64 + +LOGGER = logging.getLogger(__name__) + +EPS = 1e-13 class BouncingBall(Fmi3Slave): def __init__(self, **kwargs): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + mightReturnEarlyFromDoStep = True, + hasEventMode = True), + 'model_options_me' : None, + }) + super().__init__(**kwargs) + self.log(f"FMU Function instantiating: visible:{self.visible}, use events?{self.eventModeUsed}") self.author = "..." - self.description = "Bouncing Ball" + self.description = "Bouncing Ball as FMI3 component with event handling" self.time = 0.0 self.h = 1.0 @@ -25,6 +36,8 @@ def __init__(self, **kwargs): self.g = -9.81 self.e = 0.7 self.v_min = 0.1 + self.next_bounce: float + self.energy_min: float # define units unit1 = Unit(name="m", m=1) @@ -33,86 +46,139 @@ def __init__(self, **kwargs): self.register_units([unit1, unit2, unit3]) self.register_variable( - Float64( - "time", - causality=Fmi3Causality.independent, - variability=Fmi3Variability.continuous, - ) + Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous) ) self.register_variable( - Float64( - "ball.h", - causality=Fmi3Causality.output, - start=1, - variability=Fmi3Variability.continuous, - initial=Fmi3Initial.exact, - unit=unit1.name, - ), - nested=False, - ) + Float64("h", + causality=Fmi3Causality.output, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + unit=unit1.name, + start=1, + )) self.register_variable( - Float64( - "ball.derh", - causality=Fmi3Causality.local, - variability=Fmi3Variability.continuous, - derivative=1, - unit=unit2.name, - ), - nested=False, - ) + Float64("derh", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=1, + unit=unit2.name, + )) self.register_variable( - Float64( - "ball.v", - causality=Fmi3Causality.output, - start=0, - variability=Fmi3Variability.continuous, - initial=Fmi3Initial.exact, - unit=unit2.name, - ), - nested=False, - ) + Float64("v", + causality=Fmi3Causality.output, + start=0, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + unit=unit2.name, + )) self.register_variable( - Float64( - "ball.derv", - causality=Fmi3Causality.local, - variability=Fmi3Variability.continuous, - derivative=3, - unit=unit3.name, - ), - nested=False, - ) + Float64("derv", + causality=Fmi3Causality.local, + variability=Fmi3Variability.continuous, + derivative=3, + unit=unit3.name, + )) self.register_variable( - Float64( - "g", - causality=Fmi3Causality.parameter, - variability=Fmi3Variability.fixed, - unit=unit3.name, - ) - ) + Float64("g", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + unit=unit3.name, + start="-9.81" + )) self.register_variable( - Float64( - "e", - causality=Fmi3Causality.parameter, - variability=Fmi3Variability.tunable, - ) - ) + Float64("e", + causality=Fmi3Causality.parameter, + variability=Fmi3Variability.tunable, + start="0.7" + )) self.register_variable( - Float64("v_min", variability=Fmi3Variability.constant, start=0.1) + Float64("v_min", causality=Fmi3Causality.parameter, + variability=Fmi3Variability.fixed, + start=0.1) ) + + def calc_next_bounce(self): + """Calculate the next bounce time as interval from current time.""" + if self.h == 0.0 and self.v == 0.0: # bouncing stopped + return float('inf') + return -self.v/self.g + np.sqrt((self.v/self.g)**2 - 2*self.h/self.g) + + def exit_initialization_mode(self): + self.derv = self.g # constant + self.next_bounce = self.time + self.calc_next_bounce() + self.energy_min = 0.5* self.v_min* self.v_min - def do_step(self, current_time, step_size): - self.derv = self.g + def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): + self.time = current_time + step_size + assert not self.eventModeUsed or self.time < self.next_bounce + EPS, ( + f"Time {self.time} > {self.next_bounce}" + ) self.derh = self.v - self.h += self.derh * step_size + self.h += (self.derh + 0.5*self.derv* step_size)*step_size self.v += self.derv * step_size - - if self.h <= 0 and self.v < 0: + + event_handling_needed: bool = False + if self.eventModeUsed and abs(self.time - self.next_bounce) < EPS: + event_handling_needed = True # calculate the next bounce + + elif self.h <= 0 and self.v < 0: # when eventMode is not used self.h = 1e-12 self.v = -self.v * self.e - if self.v < self.v_min: - self.v = 0 - self.g = 0 + + if not self.eventModeUsed and 0.5*self.v**2 - self.g*self.h < self.energy_min: # stop the bouncing + self.v = 0 + self.derv = 0 + ret = Fmi3StepResult(eventHandlingNeeded = event_handling_needed, + lastSuccessfullTime = self.time) + return ret + + def update_discrete_states(self): + """Call by Orchestrator to update discrete states in super-dense time instant, + returning info to the Orchestrator on e.g. nextEventTime + + Returns: + discreteStatesNeedUpdate: True; the Orchestrator must stay in Event Mode for another event iteration, + starting a new super-dense time instant. + terminateSimulation: True; the FMU requests to stop the simulation. + nominalsOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: False; use only for configuration changes + nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. + nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. + The importer must compute up to nextEventTime (or if needed slightly further) + and then enter Event Mode using EnterEventMode. + The FMU must handle this time event during the Event Mode that is entered + by the first call to EnterEventMode, at or after nextEventTime. + If another (e.g. state event) event happens before that nextEventTime, + the previous definition of nextEventTime becomes obsolete. + """ + if self.time > 0.0: + self.h = 0.0 # avoid numerical issues. Bounce always at ground + if abs(self.v) < self.v_min: + self.v = 0.0 + self.derv = 0.0 + else: + self.v = -self.v * self.e + + self.next_bounce = self.time + self.calc_next_bounce() + return Fmi3UpdateDiscreteStatesResult(discreteStatesNeedUpdate=False, + nextEventTimeDefined=True, + nextEventTime=self.next_bounce) + +if __name__ == "__main__": + from pathlib import Path + + from fmpy import read_model_description + + from pythonfmu3.builder import FmuBuilder - return Fmi3StepResult(status=Fmi3Status.ok) + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") \ No newline at end of file diff --git a/examples/driving_force3.py b/examples/driving_force3.py new file mode 100644 index 0000000..bddac40 --- /dev/null +++ b/examples/driving_force3.py @@ -0,0 +1,125 @@ +import logging +from functools import partial +from typing import Any, Callable +import numpy as np +import numpy.typing as npt +from pythonfmu3 import Fmi3Slave +from pythonfmu3.cosimulation import Fmi3StepResult +from pythonfmu3.enums import Fmi3Variability, Fmi3Causality, Fmi3Initial, Fmi3Status +from pythonfmu3.variables import Boolean, Float64, Int32, UInt64, Dimension +from pythonfmu3.model_returns import Fmi3UpdateDiscreteStatesResult +from pythonfmu3.model_options import Fmi3ModelOptionsCS + +LOGGER = logging.getLogger(__name__) + + +# Note: PythonFMU (which component-model is built on) works on files and thus only one Model class allowed per file + + +def func(time: float, ampl: npt.NDArray[np.float64], omega: npt.NDArray[np.float64], d_omega: float = 0.0): + """Generate a harmonic oscillating force function. + Optionally it is possible to linearly change the angular frequency as omega + d_omega*time. + The function is intended to be initialized through partial, so that only 'time' is left as variable. + """ + if d_omega == 0.0: + return ampl * np.sin(omega * time) + else: + _omega = omega + np.ones_like(omega)* d_omega* time + return ampl * np.sin(_omega * time) + + +class DrivingForce(Fmi3Slave): + """A driving force in 3 dimensions which produces an ouput per time and can be connected to the oscillator.""" + + def __init__(self, **kwargs: Any): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + mightReturnEarlyFromDoStep = False, + hasEventMode = False), + 'model_options_me' : None, + }) + super().__init__(**kwargs) + self.log(f"FMU instantiating: visible:{self.visible}, use events?{self.eventModeUsed}") + self.author = "Siegfried Eisinger" + self.description = "FMI3 version of the oscillator component model" + + self.time = 0.0 + self.ampl = np.array((1.0,) * 3, np.float64) + self.dim = len(self.ampl) + self.freq = np.array( (1.0,) * self.dim, np.float64) + self.d_freq = 0.0 + + self.function = func + self.func: Callable + self.f = np.array((0.0,) * self.dim, np.float64) + self.v_osc = np.array((0.0,) * self.dim, np.float64) + + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self._dim = UInt64("dim", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.structuralParameter, + initial=Fmi3Initial.exact, + start=self.dim) + self.register_variable(self._dim) + self.register_variable(Float64("ampl", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="1.0 1.0 1.0")) + self.register_variable(Float64("freq", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="1.0 1.0 1.0")) + self.register_variable(Float64("d_freq", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + start="0.0")) + self.register_variable(Float64("f", + causality=Fmi3Causality.output, + dimensions=[Dimension(valueReference=self._dim.value_reference)])) + self.register_variable(Float64("v_osc", + causality=Fmi3Causality.input, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="0.0 0.0 0.0")) + + def do_step(self, + current_time: float, + step_size: float, + noSetFmuStatePriorToCurrentPoint:bool=True) -> Fmi3StepResult: + self.time = current_time + step_size + self.f = self.func(self.time) + ret = Fmi3StepResult(eventHandlingNeeded = False, + lastSuccessfullTime = self.time) + return ret + + def exit_initialization_mode(self): + """Set internal state after initial variables are set.""" + assert isinstance(self.ampl, np.ndarray) + assert isinstance(self.freq, np.ndarray) + assert isinstance(self.d_freq, float) + self.func = partial( + self.function, + ampl=np.array(self.ampl, np.float64), + omega=np.array(2 * np.pi * self.freq, np.float64), + d_omega=2 * np.pi * self.d_freq + ) + self.f = self.func(self.time) + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") + diff --git a/examples/function.py b/examples/function.py index ddaa304..26e6475 100644 --- a/examples/function.py +++ b/examples/function.py @@ -12,13 +12,12 @@ Fmi3IntervalVariability, Fmi3Causality, Fmi3Variability, + Fmi3Initial, Fmi3Status) from pythonfmu3.variables import Float64, UInt64, String, Dimension -from pythonfmu3.fmi3slave import ( - Fmi3StepResult, - Fmi3UpdateDiscreteStatesResult, - Fmi3ModelOptionsCS, - ) +from pythonfmu3.model_returns import Fmi3StepResult, Fmi3UpdateDiscreteStatesResult +from pythonfmu3.model_options import Fmi3ModelOptionsCS + EPS = 1e-14 @@ -26,17 +25,20 @@ class Function(Fmi3Slave): """Discrete event Function implementation.""" def __init__(self, **kwargs): - kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True), + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + hasEventMode=True), 'model_options_me' : None, }) super().__init__(**kwargs) + self.log(f"FMU Function instantiating: visible:{self.visible}, use events?{self.eventModeUsed}") self.author = "Siegfried Eisinger" self.description = "Function of its inputs, updating the output" # Parameters - self.n_inputs = 2 - self.func_name = "Sum" + self.n_inputs : int = 2 + self.func_name : str = "sum" + self.par : float = 1.0 # Inputs self.inputs: np.ndarray = np.array([0.0]*self.n_inputs, float) @@ -50,23 +52,28 @@ def __init__(self, **kwargs): # Register variables self.register_variable(Float64("time", causality=Fmi3Causality.independent)) self._n_inputs = UInt64("n_inputs", - variability=Fmi3Variability.constant, - causality=Fmi3Causality.local, + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.structuralParameter, + initial=Fmi3Initial.exact, start=2) self.register_variable(self._n_inputs) - self.register_variable(String("func_name", causality=Fmi3Causality.parameter, start="Sum")) - + self.register_variable(String("func_name", causality=Fmi3Causality.parameter, start="sum")) + self.register_variable(Float64("par", causality=Fmi3Causality.parameter, start=1.0)) self.register_variable(Float64("inputs", - causality=Fmi3Causality.input, + variability=Fmi3Variability.continuous, + causality=Fmi3Causality.input, dimensions=[Dimension(valueReference=self._n_inputs.value_reference)], - start=1.0)) + start=(1.0, 1.0) )) self.register_variable(Float64("output", causality=Fmi3Causality.output)) def calc(self)-> float: """Re-calculate the function.""" if self.func_name == "sum": - return sum( x for x in self.inputs) + if self.par == 1.0: + return sum( x for x in self.inputs) + else: + return self.par* sum( x for x in self.inputs) elif self.func_name == "and": res = True for x in self.inputs: @@ -83,8 +90,7 @@ def calc(self)-> float: return min(self.inputs) elif self.func_name == "max": return max(self.inputs) - else: - raise NotImplementedError(f"The function {self.func_name} is so far not included.") from None + raise NotImplementedError(f"The function {self.func_name} is so far not included.") from None def enter_event_mode(self): """Signal from orchestrator that an event has occured.""" @@ -102,8 +108,9 @@ def exit_initialization_mode(self): def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): self.time = current_time + step_size - self.histogram.append( (current_time, self.output)) - self.output = self.calc() + if not self.eventModeUsed: + self.histogram.append( (current_time, self.output)) + self.output = self.calc() return Fmi3StepResult(eventHandlingNeeded = False, lastSuccessfullTime = self.time) def update_discrete_states(self): @@ -115,7 +122,7 @@ def update_discrete_states(self): starting a new super-dense time instant. terminateSimulation: True; the FMU requests to stop the simulation. nominalsOfContinuousStatesChanged: True; use only for ME! - valuesOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: False; use only for configuration changes nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. The importer must compute up to nextEventTime (or if needed slightly further) diff --git a/examples/function_use.py b/examples/function_use.py deleted file mode 100644 index 7df2633..0000000 --- a/examples/function_use.py +++ /dev/null @@ -1,35 +0,0 @@ -# Example of co-simulation setup -import fmpy -from fmpy import simulate_fmu -import matplotlib.pyplot as plt -import numpy as np - -# Simulate the PID controller FMU -result = simulate_fmu( - filename='Function.fmu', - start_time=0.0, - stop_time=10.0, - step_size=0.01, - start_values={ - 'func_name' : "sum", - 'inputs' : np.array([1.0, 2.0], float), - }, - output = ['output'], -) - -# Plot results -plt.figure(figsize=(12, 6)) -plt.subplot(2, 1, 1) -plt.plot(result['time'], result['output'], label='Output') -#plt.plot(result['time'], result['measurement'], label='Measurement') -plt.ylabel('Value') -plt.legend() -plt.grid(True) - -# plt.subplot(2, 1, 2) -# plt.plot(result['time'], result['state'], label='State') -# plt.xlabel('Time [s]') -# plt.ylabel('Control Signal') -# plt.legend() -# plt.grid(True) -plt.show() \ No newline at end of file diff --git a/examples/linear_transform.py b/examples/linear_transform.py new file mode 100644 index 0000000..56d3497 --- /dev/null +++ b/examples/linear_transform.py @@ -0,0 +1,112 @@ +"""Implements a function component with n scalar inputs and one output. + The function name can be specified. + + The function triggers only on input events and calculates its output, + i.e. it triggers on update_discrete_states, updating the output, not setting new event times. + do_step only keeps track of time. +""" +import numpy as np +from collections import namedtuple +from pythonfmu3 import Fmi3Slave +from pythonfmu3.enums import ( + Fmi3IntervalVariability, + Fmi3Causality, + Fmi3Variability, + Fmi3Status) +from pythonfmu3.variables import Float64, UInt64, String, Dimension +from pythonfmu3.model_returns import Fmi3StepResult, Fmi3UpdateDiscreteStatesResult +from pythonfmu3.model_options import Fmi3ModelOptionsCS + +EPS = 1e-14 + +class LinearTransform(Fmi3Slave): + """Linear transformation of a scalar input value.""" + + def __init__(self, **kwargs): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + hasEventMode = True), + 'model_options_me' : None, + }) + super().__init__(**kwargs) + + self.author = "Siegfried Eisinger" + self.description = "Linear transformation of a scalar input value: y = a + b*x" + + # Internal states + self.time = 0.0 # actually independent of time + + # Parameters + self.a : float = 0.0 # intercept + self.b : float = 1.0 # slope + + # Inputs + self.x: float = 0.0 + # Outputs + self.y: float = self.transform(self.x) + + # Register variables + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self.register_variable(Float64("a", causality=Fmi3Causality.parameter, start=0.0)) + self.register_variable(Float64("b", causality=Fmi3Causality.parameter, start=1.0)) + + self.register_variable(Float64("x", causality=Fmi3Causality.input, start=0.0)) + self.register_variable(Float64("y", causality=Fmi3Causality.output)) + + def enter_event_mode(self): + """Signal from orchestrator that an event has occured.""" + # print(f"EnterEventMode @{self.time}") + pass + + def enter_step_mode(self): + """Signal from orchestrator that events are dealt with and switching back to step mode.""" + # print(f"EnterStepMode @{self.time}") + pass + + def exit_initialization_mode(self): + # print(f"Initialization finished") + pass + + def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): + self.time = current_time + step_size + self.y = self.transform(self.x) + return Fmi3StepResult(eventHandlingNeeded = False, lastSuccessfullTime = self.time) + + def update_discrete_states(self): + """Call by Orchestrator to update discrete states in super-dense time instant, + returning info to the Orchestrator + + Returns: + discreteStatesNeedUpdate: True; the Orchestrator must stay in Event Mode for another event iteration, + starting a new super-dense time instant. + terminateSimulation: True; the FMU requests to stop the simulation. + nominalsOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: False; use only for configuration changes + nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. + nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. + The importer must compute up to nextEventTime (or if needed slightly further) + and then enter Event Mode using EnterEventMode. + The FMU must handle this time event during the Event Mode that is entered + by the first call to EnterEventMode, at or after nextEventTime. + If another (e.g. state event) event happens before that nextEventTime, + the previous definition of nextEventTime becomes obsolete. + """ + self.y = self.transform(self.x) + return Fmi3UpdateDiscreteStatesResult(discreteStatesNeedUpdate = True) + + def transform(self, x:float): + return self.a + self.b* x + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") \ No newline at end of file diff --git a/examples/oscillator3.py b/examples/oscillator3.py new file mode 100644 index 0000000..4b21cb5 --- /dev/null +++ b/examples/oscillator3.py @@ -0,0 +1,159 @@ +import logging +import numpy as np +import numpy.typing as npt +from scipy import integrate +from pythonfmu3 import Fmi3Slave +from pythonfmu3.cosimulation import Fmi3StepResult +from pythonfmu3.enums import Fmi3Variability, Fmi3Causality, Fmi3Initial, Fmi3Status +from pythonfmu3.variables import Boolean, Float64, Int32, UInt64, Dimension +from pythonfmu3.model_returns import Fmi3UpdateDiscreteStatesResult +from pythonfmu3.model_options import Fmi3ModelOptionsCS + +LOGGER = logging.getLogger(__name__) + + +class Oscillator(Fmi3Slave): + """Construct a simple model of a general harmonic oscillator, potentially driven by a force. + + The system obeys the equation F(t) - k*x - c*dx/dt = m*d^2x/dt^2 + See also `Wikipedia `_ + + where x shall be a 3D vector with an initial position. F(t)=0 as long as there is not external driving force. + + We use the scipy.integrate.solve_ivp algorithm for the integration (see do_step) + + Args: + k (tuple)=(1.0, 1.0, 1.0): spring constant in N/m. May vary in 3D + c (tuple)=(0.0, 0.0, 0.0): Viscous damping coefficient in N.s/m. May vary in 3D + m (float | tuple)=1.0: Mass of the spring load (spring mass negligible) in kg. Same in all dim. if float + tolerance (float)=1e-5: Optional tolerance in m, i.e. maximum uncertainty in displacement x. + """ + + def __init__(self, **kwargs): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + mightReturnEarlyFromDoStep = False, + hasEventMode = False), + 'model_options_me' : None, + }) + super().__init__(**kwargs) + self.log(f"FMU instantiating: visible:{self.visible}, use events?{self.eventModeUsed}") + self.author = "Siegfried Eisinger" + self.description = "FMI3 version of the oscillator component model" + + self.time = 0.0 + self.k = np.array((1.0, 1.0, 1.0), np.float64) + self.dim = len(self.k) # internal variable + self.c = np.array((0.0, 0.0, 0.0), np.float64) + self.m = np.array((1.0,) * self.dim, np.float64) + self.tolerance = 1e-5 + self.x = np.array((0,) * self.dim, np.float64) + self.v = np.array((0,) * self.dim, np.float64) + self.f = np.array((0,) * self.dim, np.float64) + self.ode : npt.NDArray[np.float64] # Defined after initialization: + + # Register variables + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self._dim = UInt64("dim", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.structuralParameter, + initial=Fmi3Initial.exact, + start=self.dim) + self.register_variable(self._dim) + self.register_variable(Float64("k", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="1.0 1.0 1.0")) + self.register_variable(Float64("c", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="0.0 0.0 0.0")) + self.register_variable(Float64("x", + causality=Fmi3Causality.output, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="1.0 0.0 0.0")) + self.register_variable(Float64("v", + causality=Fmi3Causality.output, + variability=Fmi3Variability.continuous, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._dim.value_reference)], + start="0.0 0.0 0.0")) + self.register_variable(Float64("f", + causality=Fmi3Causality.input, + dimensions=[Dimension(valueReference=self._dim.value_reference)])) + + + def exit_initialization_mode(self): + self.ode = [ # standard ODE matrix (homogeneous system). + np.array(((-self.c[i] / self.m[i], -self.k[i] / self.m[i]), (1, 0)), float) for i in range(self.dim) + ] + + + def ode_func( + self, + t: float, # scalar time + y: np.ndarray, # combined array of position and speed for component i + i: int, # dimension + f: float, + ) -> np.ndarray: # force for component i + res = self.ode[i].dot(y) + if f != 0: + res += np.array((f, 0), float) + return res + + def do_step(self, + current_time: float, + step_size: float, + noSetFmuStatePriorToCurrentPoint:bool=True) -> Fmi3StepResult: + """Do one simulation step of size dt. + + We implement a very simplistic algoritm based on difference calculus. + """ + self.time = current_time + step_size + for i in range(self.dim): # this is a xD oscillator + if self.x[i] != 0 or self.v[i] != 0 or self.f[i] != 0: + y0 = np.array([self.v[i], self.x[i]], float) + sol = integrate.solve_ivp( + fun=self.ode_func, + t_span=[current_time, current_time + step_size], + y0=y0, + args=(i, self.f[i]), # axis and force as extra arguments to fun + atol=self.tolerance, + ) + self.x[i] = sol.y[1][-1] + self.v[i] = sol.y[0][-1] + ret = Fmi3StepResult(eventHandlingNeeded = False, + lastSuccessfullTime = self.time) + return ret + + @property + def period(self): + """Calculate the natural period of the oscillator (without damping an).""" + w2 = [] + for i in range(self.dim): + w2i = self.k[i] / self.m[i] - (self.c[i] / 2 / self.m[i]) ** 2 + if w2i > 0: + w2.append(2 * np.pi / np.sqrt(w2i)) + else: + w2.append(float("nan")) # critically or over-damped. There is no period + return w2 + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") diff --git a/examples/pid_use.py b/examples/pid_use.py deleted file mode 100644 index 955738d..0000000 --- a/examples/pid_use.py +++ /dev/null @@ -1,39 +0,0 @@ -# Example of co-simulation setup -import fmpy -from fmpy import simulate_fmu -import matplotlib.pyplot as plt -import numpy as np - -# Simulate the PID controller FMU -result = simulate_fmu( - filename='PIDController.fmu', - start_time=0.0, - stop_time=10.0, - step_size=0.01, - start_values={ - 'kp': 2.0, - 'ki': 0.5, - 'kd': 0.1, - 'setpoint': 1.0 - }, - input= np.array( [(0.0, 0.0, 1.0), (1.0, 0.3, 1.0), (5.0, 0.8, 1.0), (10.0, 1.0, 1.0)], - dtype=[('time', np.double),('measurement', np.double),('setpoint', np.double)]), - output = ['output','setpoint','measurement'], -) - -# Plot results -plt.figure(figsize=(12, 6)) -plt.subplot(2, 1, 1) -plt.plot(result['time'], result['setpoint'], label='Setpoint') -plt.plot(result['time'], result['measurement'], label='Measurement') -plt.ylabel('Value') -plt.legend() -plt.grid(True) - -plt.subplot(2, 1, 2) -plt.plot(result['time'], result['output'], label='Control Output') -plt.xlabel('Time [s]') -plt.ylabel('Control Signal') -plt.legend() -plt.grid(True) -plt.show() \ No newline at end of file diff --git a/examples/table.py b/examples/table.py new file mode 100644 index 0000000..8a81263 --- /dev/null +++ b/examples/table.py @@ -0,0 +1,162 @@ +"""Implements a function component with n scalar inputs and one output. + The function name can be specified. + + The function triggers only on input events and calculates its output, + i.e. it triggers on update_discrete_states, updating the output, not setting new event times. + do_step only keeps track of time. +""" +import numpy as np +from collections import namedtuple +from pythonfmu3 import Fmi3Slave +from pythonfmu3.enums import ( + Fmi3IntervalVariability, + Fmi3Causality, + Fmi3Variability, + Fmi3Initial, + Fmi3Status) +from pythonfmu3.variables import Float64, UInt64, String, Dimension +from pythonfmu3.model_returns import Fmi3StepResult, Fmi3UpdateDiscreteStatesResult +from pythonfmu3.model_options import Fmi3ModelOptionsCS + +EPS = 1e-14 + +class Table(Fmi3Slave): + """Input table changing values with time.""" + + def __init__(self, **kwargs): + kwargs.update({'model_options_cs' : Fmi3ModelOptionsCS(canHandleVariableCommunicationStepSize = True, + hasEventMode = True), + 'model_options_me' : None, + }) + super().__init__(**kwargs) + self.log(f"FMU instantiating: visible:{self.visible}, use events?{self.eventModeUsed}") + + self.author = "Siegfried Eisinger" + self.description = "Table with time values in first column changing row with time." + + # Parameters + self.times = np.array([t for t in np.arange(0,10, 0.9)], float) + self.values = np.array([[t,t**2] for t in self.times], float) + self.n_rows : int = len(self.times) + self.n_cols : int = len(self.values[0]) + + # Inputs + # Outputs + self.outputs: np.ndarray = np.array([0.0]*self.n_cols, float) + + # Internal states + self.time = 0.0 + self.row: int = -1 + self.next_time = self.times[0] + + # Register variables + self.register_variable(Float64("time", causality=Fmi3Causality.independent)) + self._n_rows = UInt64("n_rows", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.structuralParameter, + initial=Fmi3Initial.exact, + start=self.n_rows) + self.register_variable(self._n_rows) + self._n_cols = UInt64("n_cols", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.structuralParameter, + initial=Fmi3Initial.exact, + start=self.n_cols) + self.register_variable(self._n_cols) + self.register_variable(Float64("times", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._n_rows.value_reference)], + #start = str(self.times.flatten()).strip('[').strip(']') + )) + self.register_variable(Float64("values", + variability=Fmi3Variability.fixed, + causality=Fmi3Causality.parameter, + initial=Fmi3Initial.exact, + dimensions=[Dimension(valueReference=self._n_rows.value_reference), + Dimension(valueReference=self._n_cols.value_reference)], + start = str(self.values.flatten()).strip('[').strip(']') + )) + self.register_variable(Float64("outputs", + causality=Fmi3Causality.output, + dimensions=[Dimension(valueReference=self._n_cols.value_reference)])) + + def enter_event_mode(self): + """Signal from orchestrator that an event has occured.""" + # print(f"EnterEventMode @{self.time}") + pass + + def enter_step_mode(self): + """Signal from orchestrator that events are dealt with and switching back to step mode.""" + # print(f"EnterStepMode @{self.time}") + pass + + def exit_initialization_mode(self): + self.row = -1 + # print(f"Initialization finished") + pass + + def enter_configuration_mode(self): + print(f"Start configuration mode") + + def exit_configuration_mode(self): + print(f"Start configuration mode") + + def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): + self.time = current_time + step_size + if self.time >= self.next_time: + if not self.eventModeUsed: + _ = self.update_discrete_states() + ret = Fmi3StepResult(eventHandlingNeeded = False, lastSuccessfullTime = self.time) + else: + return Fmi3StepResult(eventHandlingNeeded = True, lastSuccessfullTime = self.next_time) + else: + return Fmi3StepResult(eventHandlingNeeded = False, lastSuccessfullTime = self.time) + + def update_discrete_states(self): + """Call by Orchestrator to update discrete states in super-dense time instant, + returning info to the Orchestrator + + Returns: + discreteStatesNeedUpdate: True; the Orchestrator must stay in Event Mode for another event iteration, + starting a new super-dense time instant. + terminateSimulation: True; the FMU requests to stop the simulation. + nominalsOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: False; use only for configuration changes + nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. + nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. + The importer must compute up to nextEventTime (or if needed slightly further) + and then enter Event Mode using EnterEventMode. + The FMU must handle this time event during the Event Mode that is entered + by the first call to EnterEventMode, at or after nextEventTime. + If another (e.g. state event) event happens before that nextEventTime, + the previous definition of nextEventTime becomes obsolete. + """ + self.row += 1 + if self.row < len(self.values): + if self.row+1 < len(self.values): + self.next_time = self.times[self.row+1] + else: + self.next_time = float('inf') + self.outputs = self.values[self.row] + return Fmi3UpdateDiscreteStatesResult( discreteStatesNeedUpdate = True, + nextEventTimeDefined = (self.next_time is not None), + nextEventTime = self.next_time) + else: + return Fmi3UpdateDiscreteStatesResult() + +if __name__ == "__main__": + from pathlib import Path + from pythonfmu3.builder import FmuBuilder + from fmpy import read_model_description + + dest = FmuBuilder.build_FMU( + script_file = Path(__file__), + dest = Path(__file__).parent.resolve(), + #project_files: Iterable[FilePath] = set(), + #documentation_folder: Optional[FilePath] = None, + #terminals : Optional[FilePath] = None, + ) + read_model_description(dest, True, True, True) + print(f"Generated {dest}. Validated modelDescription.") \ No newline at end of file diff --git a/examples/two_state.py b/examples/two_state.py index 5f483f4..930aff9 100644 --- a/examples/two_state.py +++ b/examples/two_state.py @@ -6,16 +6,16 @@ which is transmitted to the orchestrator as one of its return values. Clocks are not used in this version. """ +import logging import numpy as np -from collections import namedtuple from pythonfmu3 import Fmi3Slave +from pythonfmu3.cosimulation import Fmi3StepResult from pythonfmu3.enums import Fmi3IntervalVariability, Fmi3Causality, Fmi3Status from pythonfmu3.variables import Boolean, Float64, Int32, Clock -from pythonfmu3.fmi3slave import ( - Fmi3StepResult, - Fmi3UpdateDiscreteStatesResult, - Fmi3ModelOptionsCS, - ) +from pythonfmu3.model_returns import Fmi3UpdateDiscreteStatesResult +from pythonfmu3.model_options import Fmi3ModelOptionsCS + +LOGGER = logging.getLogger(__name__) EPS = 1e-14 @@ -29,7 +29,7 @@ def __init__(self, **kwargs): 'model_options_me' : None, }) super().__init__(**kwargs) - + self.log(f"FMU instantiating: visible:{self.visible}, use events?{self.eventModeUsed}") self.author = "Siegfried Eisinger" self.description = "Two-state discrete event system." @@ -48,6 +48,7 @@ def __init__(self, **kwargs): self.rng = np.random.default_rng(self.seed) self.next_interval = self.get_next_interval() self.time = 0.0 + self.next_time = 0.0 self.histogram : list[ tuple[float,int]] = [] # Register variables @@ -72,7 +73,7 @@ def get_next_interval(self): return self.rng.exponential(self.mttf) else: raise ValueError(f"Unallowed state {self.state}") from None - + def enter_event_mode(self): """Signal from orchestrator that an event has occured.""" # print(f"EnterEventMode @{self.time}") @@ -84,16 +85,21 @@ def enter_step_mode(self): pass def exit_initialization_mode(self): - # print(f"Initialization finished") + self.log(f"Initialization finished. hasEventMode:{self.model_options_cs.hasEventMode}. UseEvents? {getattr(self, 'eventModeUsed', False)}") pass def do_step(self, current_time:float, step_size:float, noSetFmuStatePriorToCurrentPoint:bool=True): self.time = current_time + step_size self.histogram.append( (current_time, self.state)) if self.time >= self.next_time-EPS: - ret = Fmi3StepResult(eventHandlingNeeded = True, - lastSuccessfullTime = self.next_time) - # print(f"DoStep@{current_time}: {ret}") + if not self.eventModeUsed: + _ = self.update_discrete_states() + ret = Fmi3StepResult(eventHandlingNeeded = False, + lastSuccessfullTime = self.time) + else: + ret = Fmi3StepResult(eventHandlingNeeded = True, + lastSuccessfullTime = self.next_time) + #print(f"DoStep@{current_time}: {ret}") else: ret = Fmi3StepResult(eventHandlingNeeded = False, lastSuccessfullTime = self.time) @@ -108,7 +114,7 @@ def update_discrete_states(self): starting a new super-dense time instant. terminateSimulation: True; the FMU requests to stop the simulation. nominalsOfContinuousStatesChanged: True; use only for ME! - valuesOfContinuousStatesChanged: True; use only for ME! + valuesOfContinuousStatesChanged: False; use only for configuration changes nextEventTimeDefined: True; the next time event is known and nextEventTime has a valid value. nextEventTime: contains the absolute time of the next time event if nextEventTimeDefined = True. The importer must compute up to nextEventTime (or if needed slightly further) @@ -121,8 +127,10 @@ def update_discrete_states(self): self.state = 1 if self.time == 0.0 else 1 - self.state self.next_interval = self.get_next_interval() self.next_time = self.time + self.next_interval - # print(f"UpdateDiscreteStates @ {self.time} => {self.state}, {self.next_time}") - return Fmi3UpdateDiscreteStatesResult( nextEventTimeDefined=True, nextEventTime=self.next_interval) + #print(f"UpdateDiscreteStates @ {self.time} => {self.state}, {self.next_time}") + return Fmi3UpdateDiscreteStatesResult(discreteStatesNeedUpdate=True, + nextEventTimeDefined=True, + nextEventTime=self.next_time) if __name__ == "__main__": diff --git a/examples/two_state_use.py b/examples/two_state_use.py deleted file mode 100644 index 5e9eeb9..0000000 --- a/examples/two_state_use.py +++ /dev/null @@ -1,36 +0,0 @@ -# Example of co-simulation setup -import fmpy -from fmpy import simulate_fmu -import matplotlib.pyplot as plt -import numpy as np - -# Simulate the PID controller FMU -result = simulate_fmu( - filename='TwoState.fmu', - start_time=0.0, - stop_time=10.0, - step_size=0.01, - start_values={ - 'mttf': 1.0, - 'mttr': 0.01, - 'seed': 1, - }, - output = ['state'], -) - -# Plot results -plt.figure(figsize=(12, 6)) -plt.subplot(2, 1, 1) -plt.plot(result['time'], result['state'], label='State') -#plt.plot(result['time'], result['measurement'], label='Measurement') -plt.ylabel('Value') -plt.legend() -plt.grid(True) - -plt.subplot(2, 1, 2) -plt.plot(result['time'], result['state'], label='State') -plt.xlabel('Time [s]') -plt.ylabel('Control Signal') -plt.legend() -plt.grid(True) -plt.show() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d175831..9ef2a65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pythonfmu3" -version = "0.3.4" +version = "0.4.0" description = "A lightweight framework that enables the packaging of Python3.x code as co-simulation FMUs with (partial) FMI3 features support." readme = "README.md" license = { file = "LICENSE" } diff --git a/src/pythonfmu3/fmi3slave.py b/src/pythonfmu3/fmi3slave.py index e12e766..d9e57e5 100644 --- a/src/pythonfmu3/fmi3slave.py +++ b/src/pythonfmu3/fmi3slave.py @@ -10,7 +10,7 @@ from xml.etree.ElementTree import Element, SubElement from pythonfmu3._version import __version__ as VERSION -from pythonfmu3.cosimulation import CoSimulation +from pythonfmu3.cosimulation import CoSimulation, Fmi3StepResult from pythonfmu3.default_experiment import DefaultExperiment from pythonfmu3.enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability from pythonfmu3.logmsg import LogMsg @@ -305,12 +305,12 @@ def enter_initialization_mode(self, def exit_initialization_mode(self): pass -# def do_step(self, -# current_time: float, -# step_size: float, -# *, -# noSetFmuStatePriorToCurrentPoint:bool = False) -> Fmi3StepResult: -# pass + def do_step(self, + current_time: float, + step_size: float, + *, + noSetFmuStatePriorToCurrentPoint: bool = False) -> Fmi3StepResult: + pass def terminate(self): pass diff --git a/src/pythonfmu3/pythonfmu-export/build_unix.sh b/src/pythonfmu3/pythonfmu-export/build_unix.sh old mode 100755 new mode 100644 diff --git a/tests/conftest.py b/tests/conftest.py index 53ec4a0..ec4ba7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,15 @@ +import pytest + def pytest_configure(config): config.addinivalue_line( "markers", "requirements(pkg): mark test as requiring the specified package" ) + +def pytest_addoption(parser: pytest.Parser): + parser.addoption("--show", action="store", default=False) + + +@pytest.fixture(scope="session") +def show(request: pytest.FixtureRequest): + return request.config.getoption("--show") == "False" + diff --git a/tests/simulation.py b/tests/simulation.py new file mode 100644 index 0000000..a55955c --- /dev/null +++ b/tests/simulation.py @@ -0,0 +1,247 @@ +import shutil +from math import isclose + +from fmpy.fmi1 import * +from fmpy.fmi1 import _FMU1, _FMU +from fmpy.fmi2 import * +from fmpy.fmi2 import _FMU2 +from fmpy import fmi3 +from fmpy import extract +from fmpy.util import auto_interval, add_remoting +from fmpy.simulation import ( + Input, + Recorder, + apply_start_values, + settable_in_instantiated, + settable_in_initialization_mode, + ) + + +import numpy as np +from time import time as current_time +from typing import Union, Any, Dict, Sequence, Callable + +def _simulateCS(model_description, + fmu, + start_time, + stop_time, + relative_tolerance, + start_values, + apply_default_start_values, + input_signals, + output, + output_interval, + timeout, + step_finished, + set_input_derivatives, + use_event_mode, + early_return_allowed, + validate, + initialize, + terminate, + set_stop_time): + """Copy of fmpy.simulation.simulateCS() function, + where we put in hooks to test the implemented event functions. + """ + print(f"RUNNING ADAPTED SIMULATE. events?{use_event_mode}") + nextEventTime : float|None = None + if set_input_derivatives and not model_description.coSimulation.canInterpolateInputs: + raise Exception("Parameter set_input_derivatives is True but the FMU cannot interpolate inputs.") + + if output_interval is None: + output_interval = auto_interval(stop_time - start_time) + + sim_start = current_time() + + is_fmi1 = model_description.fmiVersion == '1.0' + is_fmi2 = model_description.fmiVersion == '2.0' + + can_handle_variable_step_size = model_description.coSimulation.canHandleVariableCommunicationStepSize + + input = Input(fmu=fmu, modelDescription=model_description, signals=input_signals, + set_input_derivatives=set_input_derivatives) + + time = start_time + + if initialize: + + # initialize the model + if is_fmi1: + start_values = apply_start_values(fmu, model_description, start_values, settable=has_start_value) + input.apply(time) + fmu.initialize(tStart=time, stopTime=stop_time if set_stop_time else None) + elif is_fmi2: + fmu.setupExperiment(tolerance=relative_tolerance, startTime=time, + stopTime=stop_time if set_stop_time else None) + start_values = apply_start_values(fmu, model_description, start_values, + settable=settable_in_instantiated) + fmu.enterInitializationMode() + start_values = apply_start_values(fmu, model_description, start_values, + settable=settable_in_initialization_mode) + input.apply(time) + fmu.exitInitializationMode() + else: + start_values = apply_start_values(fmu, model_description, start_values, + settable=settable_in_instantiated) + fmu.enterInitializationMode(tolerance=relative_tolerance, startTime=time, + stopTime=stop_time if set_stop_time else None) + start_values = apply_start_values(fmu, model_description, start_values, + settable=settable_in_initialization_mode) + input.apply(time) + fmu.exitInitializationMode() + + if use_event_mode: + + update_discrete_states = True + + while update_discrete_states: + (update_discrete_states, + terminate_simulation, + _, _, _nETD, _nET) = fmu.updateDiscreteStates() + + if terminate_simulation: + raise Exception("The FMU requested to terminate the simulation during initialization.") + if _nETD: + nextEventTime = time + _nET + fmu.enterStepMode() + + if validate and len(start_values) > 0: + raise Exception("The start values for the following variables could not be set: " + + ', '.join(start_values.keys())) + + recorder = Recorder(fmu=fmu, + modelDescription=model_description, + variableNames=output, + interval=output_interval) + + recorder.sample(time, force=True) + + n_steps = 0 + + input_applied = False + + # simulation loop + while True: + + if timeout is not None and (current_time() - sim_start) > timeout: + break + + if time > stop_time or isclose(time, stop_time): + break + + next_regular_point = start_time + (n_steps + 1) * output_interval + + next_communication_point = next_regular_point + + next_input_event_time = input.nextEvent(time) + + if (can_handle_variable_step_size and + next_communication_point > next_input_event_time and + not isclose(next_communication_point, next_input_event_time)): + next_communication_point = next_input_event_time + + if (next_communication_point > stop_time and + not isclose(next_communication_point, stop_time)): + + if can_handle_variable_step_size: + next_communication_point = stop_time + else: + break + + input_event = isclose(next_communication_point, next_input_event_time) + + step_size = next_communication_point - time + + if is_fmi1: + + input.apply(time, continuous=True, discrete=True, after_event=True) + + fmu.doStep(currentCommunicationPoint=time, communicationStepSize=step_size) + + time = next_communication_point + + recorder.sample(time) + + elif is_fmi2: + + input.apply(time, continuous=True, discrete=True, after_event=True) + + try: + fmu.doStep(currentCommunicationPoint=time, communicationStepSize=step_size) + + time = next_communication_point + + except FMICallException as exception: + + if exception.status == fmi2Discard: + + terminate_simulation = fmu.getBooleanStatus(fmi2Terminated) + + if terminate_simulation: + time = fmu.getRealStatus(fmi2LastSuccessfulTime) + recorder.sample(time, force=True) + break + else: + raise exception + + recorder.sample(time) + + else: + + input.apply(time, + continuous=not input_applied, + discrete=not input_applied, + after_event=not use_event_mode) + + (event_encountered, + terminate_simulation, + early_return, + last_successful_time) = fmu.doStep(currentCommunicationPoint=time, + communicationStepSize=step_size) + + if early_return and not early_return_allowed: + raise Exception("FMU returned early from doStep() but Early Return is not allowed.") + + if early_return and last_successful_time < next_communication_point: + time = last_successful_time + else: + time = next_communication_point + + recorder.sample(time) + + if terminate_simulation: + break + + if use_event_mode and (input_event or event_encountered): + + fmu.enterEventMode() + + input.apply(time, after_event=True) + + update_discrete_states = True + + while update_discrete_states: + + update_discrete_states, terminate_simulation, _, _, _, _ = fmu.updateDiscreteStates() + + if terminate_simulation: + break + + fmu.enterStepMode() + + recorder.sample(time) + + input_applied = True + else: + input_applied = False + + if isclose(time, next_regular_point): + n_steps += 1 + + if step_finished is not None and not step_finished(time, recorder): + break + + if terminate: + fmu.terminate() + + return recorder.result() \ No newline at end of file diff --git a/tests/test_bouncing_ball.py b/tests/test_bouncing_ball.py new file mode 100644 index 0000000..38fb0d1 --- /dev/null +++ b/tests/test_bouncing_ball.py @@ -0,0 +1,71 @@ +"""Test bouncingBall using fmpy.""" +import fmpy +from fmpy import simulate_fmu, instantiate_fmu +from fmpy.model_description import read_model_description +import matplotlib.pyplot as plt +import logging +import numpy as np +from pathlib import Path +import shutil +import zipfile + + +from simulation import _simulateCS +fmpy.simulation.simulateCS = _simulateCS + +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + +EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" + +def do_show( x:list|np.ndarray, + y:list|np.ndarray, + label:str, + x2:list|np.ndarray|None = None, + y2:list|np.ndarray|None = None, + label2:str=""): + plt.figure(figsize=(12, 6)) + plt.subplot(2, 1, 1) + plt.plot(x, y, label=label) + plt.xlabel('Time') + plt.ylabel('Value') + plt.legend() + plt.grid(True) + + if x2 is not None and y2 is not None: + plt.subplot(2, 1, 2) + plt.plot(x2, y2, label=label2) + plt.xlabel('Time') + plt.ylabel('Value2') + plt.legend() + plt.grid(True) + plt.show() + + +def test_bouncing(*, show: bool): + """Test the Function.fmu in isolation.""" + fmu_file = EXAMPLES_DIR / 'BouncingBall.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=4.0, + step_size=0.5, + start_values={ + 'h' : 1.0, + 'v' : 1.0, + 'g' : -9.81, + 'e' : 0.7, + }, + output = ['h','v'], + ) + if show: + do_show(result['time'], result['h'], label='Height', + x2=result['time'], y2=result['v'], label2='Speed') + + +if __name__ == "__main__": + retcode = 0#pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + test_bouncing(show=True) + \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100755 index 0000000..b5d7655 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,167 @@ +import importlib +import os +import subprocess +import sys + +# from types import SimpleNamespace as Namespace +from importlib.metadata import version +from pathlib import Path +from typing import Any + +import pytest + + +def run_in_subprocess( + *args: Any, # noqa: ANN401 + **kwargs: Any, # noqa: ANN401 +) -> subprocess.CompletedProcess[str]: + """Runs a command in a subprocess. + + Returns a CompletedProcess object capturing return code as well as stdout and stderr. + + When running under debugpy and the CLI entry point shall be executed, + then indirect the call to the Python executable, and let Python run the CLI script + as a module. This allows the debugger to follow into the subprocess. + """ + CLI_ENTRY_POINT = "sim-explorer" + is_debug_session = "VSCODE_DEBUGPY_ADAPTER_ENDPOINTS" in os.environ + + if is_debug_session and args and args[0] == CLI_ENTRY_POINT: + cmd_args = [sys.executable, "-m", "sim_explorer.cli.__main__", *args[1:]] + else: + cmd_args = list(args) + + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + + result = subprocess.run( + args=cmd_args, + shell=False, # Must be False for debugger attachment + capture_output=True, + check=False, + # encoding="utf-8", + encoding="cp437", # Use cp437 to avoid decoding errors on Windows with non-UTF-8 locale + errors="replace", # Replace undecodable bytes to avoid errors on Windows with non-UTF-8 locale + env=env, + **kwargs, + ) + return result + + +def test_libcosimpy_import(): + """Test that libcosimpy can be imported without errors.""" + try: + import libcosimpy.CosimLibrary # noqa: F401, PLC0415 + except ImportError as e: + pytest.fail(f"Failed to import libcosimpy: {e}") + + +def test_libcosimpy_import_and_reload(): + """Test that libcosimpy can be imported without errors.""" + try: + import libcosimpy.CosimLibrary # noqa: PLC0415 + except ImportError as e: + pytest.fail(f"Failed to import libcosimpy: {e}") + try: + _module = importlib.reload(libcosimpy.CosimLibrary) + except ImportError as e: + pytest.fail(f"Failed to re-import libcosimpy: {e}") + + +def test_entrypoint(): + result = run_in_subprocess("pythonfmu3", "--help") + assert result.returncode == 0 + + +def test_info(): + """Does info display the correct information.""" + cases = Path(__file__).parent / "data" / "BouncingBall3D" / "BouncingBall3D.cases" + # "Cases BouncingBall3D. Simple sim explorer with the 3D BouncingBall FMU (3D position and speed\r\nSystem spec 'OspSystemStructure.xml'.\r\nbase\r\n restitution\r\n restitutionAndGravity\r\n gravity\r\n\r\n" + result = run_in_subprocess("sim-explorer", str(cases), "--info") + assert result.returncode == 0 + assert result.stdout.startswith("Cases BouncingBall3D. Simple sim explorer with the 3D BouncingBall FMU (3D") + assert "'OspSystemStructure.xml'" in result.stdout + assert "base" in result.stdout + assert "restitution" in result.stdout + assert "restitutionAndGravity" in result.stdout + assert "gravity" in result.stdout + + +def test_help(): + """Does info display the correct information.""" + result = run_in_subprocess("sim-explorer", "--help") + assert result.returncode == 0 + assert result.stdout.startswith("usage: sim-explorer cases [options [args]]") + assert "sim-explorer cases --info" in result.stdout + assert "cases The sim-explorer specification file." in result.stdout + assert "-h, --help show this help message and exit" in result.stdout + assert "--info Display the structure of the defined cases." in result.stdout + assert "--run run Run a single case." in result.stdout + assert "--Run Run Run a case and all its sub-cases." in result.stdout + assert "-q, --quiet console output will be quiet." in result.stdout + assert "-v, --verbose console output will be verbose." in result.stdout + assert "--log LOG name of log file. If specified, this will activate" in result.stdout + assert "--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}" in result.stdout + assert "-V, --version show program's version number and exit" in result.stdout + + +def test_version(): + """Does info display the correct information.""" + result = run_in_subprocess("sim-explorer", "--version") + assert result.returncode == 0 + expected = version("sim-explorer") + assert result.returncode == 0 + assert result.stdout.strip() == expected + + +def test_run(): + """Test running single case.""" + case = "gravity" + path = Path(__file__).parent / "data" / "BouncingBall3D" + cases = path / "BouncingBall3D.cases" + res = Path(f"{case}.js5") + log = Path("test.log") + if res.exists(): + res.unlink() + if log.exists(): + log.unlink() + result = run_in_subprocess("sim-explorer", str(cases), "--run", case, "--log", "test.log", "--log-level", "DEBUG") + assert result.returncode == 1 + assert case in result.stdout + assert "6@A(g==9.81): Check wrong gravity." in result.stdout + assert "Error: Assertion has failed" in result.stdout + assert "1 tests failed" in result.stdout + assert res.exists(), f"No results file {res} produced" + assert log.exists(), f"log file {log} was not produced as requested" + # print(result) + + +def test_Run(): + """Test running single case.""" + case = "restitution" + path = Path(__file__).parent / "data" / "BouncingBall3D" + cases = path / "BouncingBall3D.cases" + res = Path(f"{case}.js5") + res2 = Path("restitutionAndGravity.js5") + if res.exists(): + res.unlink() + if res2.exists(): + res2.unlink() + result = run_in_subprocess("sim-explorer", str(cases), "--Run", case) + assert result.returncode == 0 + assert case in result.stdout, "Note: only the results from restitutionAndGravity are in stdout!" + assert res.exists(), f"No results file {res} produced" + assert res2.exists(), f"No results file {res2} produced" + + +if __name__ == "__main__": + retcode = 0#pytest.main(["-rA", "-v", __file__, "--show"]) + assert retcode == 0, f"Non-zero return code {retcode}" + # os.chdir(Path(__file__).parent.absolute() / "test_working_directory") + test_entrypoint() + # test_help() + # test_version() + # test_info() + # test_run() + # test_Run() + # test_cli() diff --git a/tests/test_event_functions.py b/tests/test_event_functions.py new file mode 100644 index 0000000..ae51461 --- /dev/null +++ b/tests/test_event_functions.py @@ -0,0 +1,43 @@ +# Example of co-simulation setup +import fmpy +import pytest +from fmpy import simulate_fmu +import matplotlib.pyplot as plt +import numpy as np +from pathlib import Path +from simulation import _simulateCS +fmpy.simulation.simulateCS = _simulateCS + +def test_run(show:bool): + # Simulate the PID controller FMU + result = simulate_fmu( + filename=Path(__file__).parent.resolve().parent / 'examples' / 'TwoState.fmu', + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'mttf': 0.9005, + 'mttr': 0.0995, + 'seed': 1, + 'deterministic': True + }, + output = ['state'], + use_event_mode = True, + ) + + if show: + # Plot results + plt.figure(figsize=(12, 6)) + plt.subplot() + plt.plot(result['time'], result['state'], label='State') + #plt.plot(result['time'], result['measurement'], label='Measurement') + plt.ylabel('Value') + plt.legend() + plt.grid(True) + plt.show() + +if __name__ == "__main__": + #retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "True", __file__]) + #assert retcode == 0, f"Non-zero return code {retcode}" + test_run(show=True) + # test_py_type() \ No newline at end of file diff --git a/tests/test_fmus.py b/tests/test_fmus.py new file mode 100644 index 0000000..0bd4467 --- /dev/null +++ b/tests/test_fmus.py @@ -0,0 +1,176 @@ +"""Test some FMUs using fmpy.""" +import fmpy +from fmpy import simulate_fmu, instantiate_fmu +from fmpy.model_description import read_model_description +import matplotlib.pyplot as plt +import logging +import numpy as np +from pathlib import Path +import shutil +import zipfile + + +from simulation import _simulateCS +fmpy.simulation.simulateCS = _simulateCS + +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + +EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" + +def do_show( x:list|np.ndarray, + y:list|np.ndarray, + label:str, + x2:list|np.ndarray|None = None, + y2:list|np.ndarray|None = None, + label2:str=""): + plt.figure(figsize=(12, 6)) + plt.subplot(2, 1, 1) + plt.plot(x, y, label=label) + plt.xlabel('Time') + plt.ylabel('Value') + plt.legend() + plt.grid(True) + + if x2 is not None and y2 is not None: + plt.subplot(2, 1, 2) + plt.plot(x2, y2, label=label2) + plt.xlabel('Time') + plt.ylabel('Value2') + plt.legend() + plt.grid(True) + plt.show() + + +def test_function(*, show: bool): + """Test the Function.fmu in isolation.""" + fmu_file = EXAMPLES_DIR / 'Function.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'func_name' : "sum", + 'inputs' : "1.0 5.0", + }, + #input= np.array( [(0.0, 0.0, 1.0), (1.0, 0.3, 1.0), (5.0, 0.8, 1.0), (10.0, 1.0, 1.0)], + # dtype=[('time', np.double),('inputs[0]', np.double), ('inputs[1]', np.double)]), + output = ['output'], + ) + assert all( _sum == 6 for _sum in result['output'][2:]) + LOGGER.warning("TODO: find out how to provide input to array inputs") + if show: + do_show(result['time'], result['output'], label='Output') + +def test_table(*, show: bool): + """Test the Table.fmu in isolation.""" + fmu_file = EXAMPLES_DIR / 'Table.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + }, + #input= np.array( [(0.0, 0.0, 1.0), (1.0, 0.3, 1.0), (5.0, 0.8, 1.0), (10.0, 1.0, 1.0)], + # dtype=[('time', np.double),('inputs[0]', np.double), ('inputs[1]', np.double)]), + output = ['outputs'], + use_event_mode = True, + ) + if show: + do_show(result['time'], result['outputs'], label='Outputs') + + +def test_function(*, show: bool): + """Test the Function.fmu in isolation.""" + fmu_file = EXAMPLES_DIR / 'Function.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'func_name' : "sum", + 'inputs' : "1.0 5.0", + }, + #input= np.array( [(0.0, 0.0, 1.0), (1.0, 0.3, 1.0), (5.0, 0.8, 1.0), (10.0, 1.0, 1.0)], + # dtype=[('time', np.double),('inputs[0]', np.double), ('inputs[1]', np.double)]), + output = ['output'], + ) + assert all( _sum == 6 for _sum in result['output'][2:]) + + +def test_linear_transform(): + fmu_file = EXAMPLES_DIR / 'LinearTransform.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=10.0, + step_size=0.1, + start_values={ + 'a': 2.0, + 'b': 3.0, + 'x': 9.0, + }, + output = ['y'], + use_event_mode = True, + ) + assert all( _y == 2.0+3.0*9.0 for _y in result['y'][2:]) + + +def test_two_state(*, show: bool): + """Test the TwoState.fmu simulation in isolation through fmpy.""" + fmu_file = EXAMPLES_DIR / 'TwoState.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=10.0, + step_size=0.1, + start_values={ + 'mttf': 0.5, + 'mttr': 0.1, + 'deterministic': True, + 'seed': 1, + }, + output = ['state'], + use_event_mode = True, + ) + if show: + do_show(result['time'], result['state'], label='State') + +def test_pid(*, show:bool): + fmu_file = EXAMPLES_DIR / 'PIDController.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=10.0, + step_size=0.01, + start_values={ + 'kp': 2.0, + 'ki': 0.5, + 'kd': 0.1, + 'setpoint': 1.0 + }, + input= np.array( [(0.0, 0.0, 1.0), (1.0, 0.3, 1.0), (5.0, 0.8, 1.0), (10.0, 1.0, 1.0)], + dtype=[('time', np.double),('measurement', np.double),('setpoint', np.double)]), + output = ['output','setpoint','measurement'], + ) + if show: + do_show( result['time'], result['measurement'], 'Measurement', + result['time'], result['output'], 'Control Output') + +if __name__ == "__main__": + retcode = 0#pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + # test_linear_transform() + # test_function(show=True) + # test_two_state(show=True) + test_table(show=True) + # test_pid( show=True) \ No newline at end of file diff --git a/tests/test_oscillator3.py b/tests/test_oscillator3.py new file mode 100644 index 0000000..d302cb9 --- /dev/null +++ b/tests/test_oscillator3.py @@ -0,0 +1,94 @@ +"""Test oscillator and driver using fmpy.""" +import fmpy +from fmpy import simulate_fmu, instantiate_fmu +from fmpy.model_description import read_model_description +import matplotlib.pyplot as plt +import logging +import numpy as np +from pathlib import Path +import pytest +import shutil +import zipfile + + +from simulation import _simulateCS +fmpy.simulation.simulateCS = _simulateCS + +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + +EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" + +def do_show( x:list|np.ndarray, + y:list|np.ndarray, + label:str, + x2:list|np.ndarray|None = None, + y2:list|np.ndarray|None = None, + label2:str=""): + plt.figure(figsize=(12, 6)) + plt.subplot(2, 1, 1) + plt.plot(x, y, label=label) + plt.xlabel('Time') + plt.ylabel('Value') + plt.legend() + plt.grid(True) + + if x2 is not None and y2 is not None: + plt.subplot(2, 1, 2) + plt.plot(x2, y2, label=label2) + plt.xlabel('Time') + plt.ylabel('Value2') + plt.legend() + plt.grid(True) + plt.show() + + +def test_oscillator(*, show: bool): + """Test the Function.fmu in isolation.""" + fmu_file = EXAMPLES_DIR / 'Oscillator.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=100.0, + step_size=0.01, + start_values={ + 'c' : "0.1 0 0", + 'x' : "1.0 0 0", + 'v' : "0 0 0", + }, + output = ['x','v'], + ) + if show: + do_show(result['time'], result['x'], label='Position', + x2=result['time'], y2=result['v'], label2='Speed') + +def test_driving_force(*, show: bool): + """Test the DrivingForce.fmu in isolation.""" + fmu_file = EXAMPLES_DIR / 'DrivingForce.fmu' + assert fmu_file.exists() + result = simulate_fmu( + filename=fmu_file, + start_time=0.0, + stop_time=100.0, + step_size=0.01, + start_values={ + 'ampl' : "1 1 1", + 'freq' : "0.1 0.1 0.1", + 'd_freq' : 0.1, + }, + output = ['f'], + ) + if show: + do_show(result['time'], result['f'], label='Force', + ) + + +if __name__ == "__main__": + retcode = 1 + if retcode == 0: + retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + #test_oscillator(show=True) + test_driving_force(show=True) + \ No newline at end of file diff --git a/tests/test_working_directory/.gitignore b/tests/test_working_directory/.gitignore new file mode 100755 index 0000000..53f6747 --- /dev/null +++ b/tests/test_working_directory/.gitignore @@ -0,0 +1,4 @@ +*test*.pdf +*.xml +*.fmu +*.dat From 5075170dc8b993f3d1fa78c0e3bbad1d9720afb1 Mon Sep 17 00:00:00 2001 From: Eisinger Date: Fri, 9 Oct 2026 15:06:22 +0200 Subject: [PATCH 17/20] Added windows support to the package. The dynamic libraries and the generated FMUs have been added to the git package (.gitignore changed). --- .gitignore | 10 +++++----- .python-version | 1 + docs/_static/PythonFMU3.svg | 0 docs/_templates/custom-class.rst | 0 docs/_templates/custom-module.rst | 0 docs/conf.py | 0 examples/BouncingBall.fmu | Bin 0 -> 1268135 bytes examples/DrivingForce.fmu | Bin 0 -> 1266237 bytes examples/Function.fmu | Bin 0 -> 1266643 bytes examples/LinearTransform.fmu | Bin 0 -> 1264480 bytes examples/Oscillator.fmu | Bin 0 -> 1267966 bytes examples/PIDController.fmu | Bin 0 -> 551338 bytes examples/Table.fmu | Bin 0 -> 1267969 bytes examples/TwoState.fmu | Bin 0 -> 1267021 bytes pyproject.toml | 5 ++++- .../x86_64-linux/libpythonfmu-export.so | Bin 0 -> 432352 bytes .../x86_64-windows/pythonfmu-export.dll | Bin 0 -> 628736 bytes tests/test_cli.py | 0 tests/test_fmus.py | 12 +++++++----- tests/test_working_directory/.gitignore | 0 20 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 .python-version mode change 100755 => 100644 docs/_static/PythonFMU3.svg mode change 100755 => 100644 docs/_templates/custom-class.rst mode change 100755 => 100644 docs/_templates/custom-module.rst mode change 100755 => 100644 docs/conf.py create mode 100644 examples/BouncingBall.fmu create mode 100644 examples/DrivingForce.fmu create mode 100644 examples/Function.fmu create mode 100644 examples/LinearTransform.fmu create mode 100644 examples/Oscillator.fmu create mode 100644 examples/PIDController.fmu create mode 100644 examples/Table.fmu create mode 100644 examples/TwoState.fmu create mode 100644 src/pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so create mode 100644 src/pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll mode change 100755 => 100644 tests/test_cli.py mode change 100755 => 100644 tests/test_working_directory/.gitignore diff --git a/.gitignore b/.gitignore index 91dd3fc..fdd7247 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ # binaries -*.dll -*.dll.manifest -*.so +#*.dll +#*.dll.manifest +#*.so # pythonfmu binaries folder -/**/pythonfmu3/resources/binaries +#/**/pythonfmu3/resources/binaries # CLion /**/cmake-build-debug @@ -27,7 +27,7 @@ build/ # test and examples artifacts /**/*_log.txt /**/*_result.mat -/**/*.fmu +#/**/*.fmu # Generated TensorFlow model /examples/tensorflow-model-export/stored-model/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/docs/_static/PythonFMU3.svg b/docs/_static/PythonFMU3.svg old mode 100755 new mode 100644 diff --git a/docs/_templates/custom-class.rst b/docs/_templates/custom-class.rst old mode 100755 new mode 100644 diff --git a/docs/_templates/custom-module.rst b/docs/_templates/custom-module.rst old mode 100755 new mode 100644 diff --git a/docs/conf.py b/docs/conf.py old mode 100755 new mode 100644 diff --git a/examples/BouncingBall.fmu b/examples/BouncingBall.fmu new file mode 100644 index 0000000000000000000000000000000000000000..7ad6e3f16b9b8c4a2846071b8fca3e803bdc4b93 GIT binary patch literal 1268135 zcmeFaUyPhtvM1!(Ws$ZP30PJOR|4+W=}SAS+D^OMef!R>?(&SgT<*5U|JGIRnTy9{ zXJvj>mE)|;ocvREO^xp(6518KthoCKyMlK>NC@$Qga9pq2PAkw2qc~mZ}5b8Sbo2V zIRCysSy|<>+pX)A?8^MUbK=B_6DLlbh&XW`|KhLyQ-A9cKdoQfd;X99tN-Bt`rW^B z=@S3_0iI3r>1aOb=F{uDqxrDgAMStM84OzEIcGd6T@*q`+~1O4$`XVUNN_W8C1=y24_2kp_AZ6^R9@Wms2 zzPVjfZ<5dEleFH`aj!GW@ARi#yv(;}_%W^N#2iSvK0L+uQX@x#_#h964Mv^W7oV@J z+<)}VH(O7#4L1_4{d{&GgOX30?e?&9n77-jD=S-%w^8KN{Mx5CS5~@%&UBi69gSzx z56SBKN`_A_-^<$VK2T}5o6~%-cQspEd-1X}*`HbomeL2UxpcI7awQ>QyF0y-t;-ms z6l``o!*4so-XPcE804Fy!$XjuE91V6vEJ_gE(g9llX-qs@L_A`{2cc84`xqfc(yu| z!SP)%<(&~*xu_NnI@7JAd^lq_4d!Ys+65vV<|NX?(XhUwuU9<@{Fmu`i~(M4`7!dN zvl7Z+*jkvM&ArCm2T!xRn7%r%hSOPRh)FTS#9hyh`qTdIAYVV>Ft_a2>#Ny(nrAt? zHT{{sk{|5j(`nv&-3VAo#m*ele1h&bTCG+iMWUBu4)kRPv4G>ejG~0H;_iccH?!^t z5@m??vX}kYfi|5TkWYYSqtLBc|B$4<(YlegbbyDS79So}mF(pc7A`@+^UCu3D17Zt zTR;6YZBViJll0+H`>;P0;Dyq|{MD>2f;3;x_Be&piuo{~>>snXd+py}=A7&g^Ng}$ z>L2shr)Y{&qsi&M(Ku{e%?>v{4dcTypB2hnpK7VA*>vMtq3q34+0UZFp9R1&@RNMM zKb_^1wjkDgCK!Qz*r@)w`5Y3+&EB+*lWR@?p5^YUaBDU=%xJpnMEVBhlo#-={;-#i zbNn})UCk7f$aX2F!bN^#T>-xj=cD;FAb(z*4jR>SyAH|Gd^VoXs_QI=b9G$`XDn>e zS6k#-`KwO1rYVlchKx;%Y*Gyqf?+b-_%waF8fbQ=!Q%`&BdrFbE|%(q7aZTb38{cd z{|MslC@=QSLHE@8te!gOrNhw~iLi)abbxMLJc-WBO>co-l)z^s+1uxvo7E)ud~