Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bffb626
One of the tests had a return value, which pytest will not allow in t…
eisDNV Apr 30, 2025
78f5646
Added a separate object 'Unit' to variable, so that units are handled…
eisDNV May 15, 2025
d90e671
Fully tested the changes related to the separate Unit class. So far l…
eisDNV May 18, 2025
fb5d077
With structured variable derivatives der(<name>,int), no space is all…
eisDNV May 22, 2025
653a65a
Defined return of Variable.getter() uniquely as list (not Sequence)
eisDNV May 22, 2025
f582a47
Draft changes with respect to using the ECCO algorithm within libcosimpy
eisDNV Jun 5, 2025
1fc34f7
Added a few missing (renamed) files
eisDNV Jun 5, 2025
8fe58ca
Changes related to using libcosimpy with the ECCO algorithm. Test is …
eisDNV Jun 11, 2025
353a6cc
Intermediate update to demonstrate a problem
eisDNV Jun 29, 2025
6375582
Updates with respect to oscillator_6dof and PythonFMU issues
eisDNV Jul 4, 2025
c59b1cb
Fix ruff and mypy issues
Jorgelmh Jul 4, 2025
66f0fa5
Added ForcedOscillator6D.xml to the project
eisDNV Jul 4, 2025
35f655c
Minor chagnes in relation with updates on the crane-fmu package
eisDNV Oct 23, 2025
2da5395
General updates in connection with crane-fmu
eisDNV Dec 2, 2025
dcad3f0
GitHub workflows: replaced --extra 'modelTest' by --extra 'tests'
ClaasRostock Dec 2, 2025
339c3c6
GitHub workflows: Change Python versions to test against to 3.11 - 3.13
ClaasRostock Dec 2, 2025
10dc6e9
Updated version trying to fix problems with libcosimpy. test_oscillat…
eisDNV Dec 2, 2025
ac3e4df
Adjust diverged branch
eisDNV Dec 2, 2025
bb4c6b1
Updates in connection with finding pytest failures. This still fails …
eisDNV Dec 4, 2025
ed53c06
Added the controls module and temporarily removed usage of libcosimpy
eisDNV Dec 12, 2025
9ba156c
Fix floating point number missmatch and upgrade pythonfmu
Jorgelmh Dec 12, 2025
10a22fd
Revert pythonfmu version
Jorgelmh Dec 12, 2025
c519591
Fix last test
Jorgelmh Dec 12, 2025
a2960dd
Fix Pint version to support python 3.13
Jorgelmh Dec 12, 2025
b11e143
Fix tests and mypy errors
Jorgelmh Dec 12, 2025
64b79b0
Fix ruff format issue
Jorgelmh Dec 12, 2025
4613f0f
Upgrade numpy
Jorgelmh Dec 12, 2025
3bc75c9
Remove unnecessary numpy from table FMU
Jorgelmh Dec 12, 2025
cd2d788
Rollback changes and try to fix tests
Jorgelmh Dec 12, 2025
6c0302b
Fix mypy issue
Jorgelmh Dec 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/_code_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ jobs:
python-version: "3.12.8"
python-version-file: "pyproject.toml"
- name: Install the project
run: uv sync --upgrade
run: uv sync --upgrade --extra tests
- name: Run mypy
run: uv run mypy
4 changes: 2 additions & 2 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
- runner: windows-latest
# - runner: macos-latest
python:
- version: '3.10'
- version: '3.11'
- version: '3.12'
- version: '3.13'
steps:
- uses: actions/checkout@v4
- name: Install uv
Expand All @@ -28,7 +28,7 @@ jobs:
with:
python-version: ${{ matrix.python.version }}
- name: Install the project
run: uv sync --upgrade -p ${{ matrix.python.version }} --no-dev --extra modelTest
run: uv sync --upgrade -p ${{ matrix.python.version }} --no-dev --extra tests
- name: Install pytest
run: |
uv pip install pytest
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/_test_future.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Unit Tests (py313)
# Test also with Python 3.13 (experimental; workflow will not fail on error.)
name: Unit Tests (py314)
# Test also with Python 3.14 (experimental; workflow will not fail on error.)

on: workflow_call

jobs:
test313:
test314:
name: Test on ${{matrix.python.version}}-${{matrix.platform.runner}} (experimental)
continue-on-error: true
runs-on: ${{ matrix.platform.runner }}
Expand All @@ -14,8 +14,8 @@ jobs:
- runner: ubuntu-latest
- runner: windows-latest
python:
- version: '3.13.0-alpha - 3.13.0'
uvpy: '3.13'
- version: '3.14.0-alpha - 3.14.0'
uvpy: '3.14'
steps:
- uses: actions/checkout@v4
- name: Install uv
Expand All @@ -28,7 +28,7 @@ jobs:
with:
python-version: ${{ matrix.python.version }}
- name: Install the project
run: uv sync --upgrade -p ${{ matrix.python.uvpy }} --no-dev --extra modelTest
run: uv sync --upgrade -p ${{ matrix.python.uvpy }} --no-dev --extra tests
- name: Install pytest
run: |
uv pip install pytest
Expand Down
2 changes: 1 addition & 1 deletion docs/_ext/get_from_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def run(self):
par = nodes.paragraph(text=text)
if self.typ is None: # need to parse the extracted text
parNode = nodes.paragraph(text="") # use an empty paragraph node as parent
self.state.nested_parse(par, 0, parNode) # here the content of the retrieved text is parsed
self.state.nested_parse(par, 0, parNode) # type: ignore #here the content of the retrieved text is parsed
else: # use the text as is
parNode = par
return [parNode]
Expand Down
82 changes: 82 additions & 0 deletions docs/source/component-development-process.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
*******************************************************
Guideline on how to develop a FMU using component-model
*******************************************************

The development process follows the steps

#. Develop a functional model using Python (>=3.10.) as a Python class. Called *basic model* here.
#. Thoroughly test the basic model.
#. Define the FMU interface using component-model functions
#. Build the FMU calling `Model.build()`, optionally overwriting optional argument of the model class.
#. Test the FMU standalone, e.g. using the `FMPy` package or in conjunction with other FMUs using the `sim-explorer` package.

Develop a basic model
=====================
Define a functional model as a Python class. We refer to this model as the *basic model*.
At this stage the emerging model class does not need to refer to the `component-model` package.
In fact it is **not recommended** to derive the basic model from `Model`class of component-model.
The basic model might import any Python package (e.g. numpy), as needed to satisfy the functionality.

Testing the basic model
=======================
The basic model should be thoroughly tested.
This cannot be emphasised too much, as test possibilities and feadback is limited in the FMU domain,
while Python offers proper test and debugging facilities.


Defining the FMU interface
==========================
A FMU interface must be added to the model prior to package the model as FMU. This concerns basically

* component model parameters, i.e. settings which can be changed prior to a simulation run, but are typically constant during the run
* component model input variables, i.e. variables which can be changed based on the output from other component models
* component model output variables, i.e. results which are provided to any connected component model, or the system.

Defining the interface is done like

.. code-block:: Python

class <basic-model>_FMU(Model, <basic-model>):
def __init__(self, <basic_model_args, **kwargs):
Model.__init__(self,name,description,author,version, kwargs)
<basic-model>.__init__(<basic-model-args>)


Virtual derivatives
-------------------
Running component models in scenarios it is often necessary to change variables during the simulation run.
As in the reality it is often not a good idea to step values in huge steps, as this resembles a 'hammer stroke',
which the system might not tolerate. The simulation dynamics does often not handle such a situation properly either.
It is therefore often necessary to ramp up or down values to the desired final values,
i.e. changing the derivative of the input variable to a non-zero value
until the desired value of the parent variable is reached and then setting the derivative back to zero.

It is cumbersome to introduce derivative variables for every parent variable which might be changed during the simulation.
Therefore. component-model introduces the concept of *virtual derivatives*,
which are derivative interface variables which are not linked to variables in the basic model.
When defining such variables and setting them to non-zero values,
the parent variable is changed with the given slope at every time step,
i.e.

`<parent-variable> += d<parent-variable>/dt * <step-size>`

where `d<parent-variable>/dt` is the non-zero derivative value (slope).

In practical terms, virtual derivatives are defined using FMI structured variables syntax:

.. code-block:: Python

Variable( name='der(<parent-variable-name>)', causality='input', variability='continuous', ...)

Explicit specification of the arguments `owner` and `local_name` should be avoided.
Specification of `local_name` changes the virtual derivative into a non-virtual derivative,
i.e. the variable is expected to exist in the basic model.
The `on_step` argument is automatically set to change the parent variable at every time step if the derivative is non-zero.
Explicitly overwriting the automatic `on_step` function is allowed at one's own expense.


Building the FMU
================

Testing the FMU
===============
Binary file modified docs/source/component-model.pptx
Binary file not shown.
9 changes: 9 additions & 0 deletions examples/BouncingBallStructure.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<OspSystemStructure xmlns="http://opensimulationplatform.com/MSMI/OSPSystemStructure" version="0.1">
<StartTime>0.0</StartTime>
<BaseStepSize>0.1</BaseStepSize>
<Simulators>
<Simulator name="bb" source="BouncingBall3D.fmu" stepSize="0.1" />
</Simulators>
<Functions />
<Connections />
</OspSystemStructure>
Binary file added examples/DrivingForce.fmu
Binary file not shown.
24 changes: 20 additions & 4 deletions examples/ForcedOscillator.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
<OspSystemStructure xmlns="http://opensimulationplatform.com/MSMI/OSPSystemStructure" version="0.1">
<StartTime>0.0</StartTime>
<BaseStepSize>0.01</BaseStepSize>
<Algorithm>fixedStep</Algorithm>
<Simulators>
<Simulator name="osc" source="HarmonicOscillator.fmu" stepSize="0.01" />
<Simulator name="drv" source="DrivingForce.fmu" stepSize="0.01" />
</Simulators>
<Functions />
<Connections>
<VariableConnection>
<Variable simulator="drv" name="f[2]" />
<Variable simulator="osc" name="f[2]" />
<VariableConnection powerBond="force-velocity">
<Variable simulator="drv" name="f[2]" causality="output"/>
<Variable simulator="osc" name="f[2]" causality="input"/>
</VariableConnection>
<VariableConnection powerBond="force-velocity">
<Variable simulator="osc" name="v[2]" causality="output"/>
<Variable simulator="drv" name="v_osc[2]" causality="input"/>
</VariableConnection>
</Connections>
<EccoConfiguration>
<SafetyFactor>0.99</SafetyFactor>
<StepSize>0.0001</StepSize>
<MinimumStepSize>0.00001</MinimumStepSize>
<MaximumStepSize>0.01</MaximumStepSize>
<MinimumChangeRate>0.2</MinimumChangeRate>
<MaximumChangeRate>1.5</MaximumChangeRate>
<ProportionalGain>0.2</ProportionalGain>
<IntegralGain>0.15</IntegralGain>
<RelativeTolerance>1e-6</RelativeTolerance>
<AbsoluteTolerance>1e-6</AbsoluteTolerance>
</EccoConfiguration>
</OspSystemStructure>
31 changes: 31 additions & 0 deletions examples/ForcedOscillator6D.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<OspSystemStructure xmlns="http://opensimulationplatform.com/MSMI/OSPSystemStructure" version="0.1">
<StartTime>0.0</StartTime>
<BaseStepSize>0.01</BaseStepSize>
<Algorithm>fixedStep</Algorithm>
<Simulators>
<Simulator name="osc" source="HarmonicOscillator6D.fmu" stepSize="0.01" />
<Simulator name="drv" source="DrivingForce6D.fmu" stepSize="0.01" />
</Simulators>
<Connections>
<VariableConnection powerBond="force-velocity">
<Variable simulator="drv" name="f[5]" causality="output"/>
<Variable simulator="osc" name="f[5]" causality="input"/>
</VariableConnection>
<VariableConnection powerBond="force-velocity">
<Variable simulator="osc" name="v[5]" causality="output"/>
<Variable simulator="drv" name="v_osc[5]" causality="input"/>
</VariableConnection>
</Connections>
<EccoConfiguration>
<SafetyFactor>0.99</SafetyFactor>
<StepSize>0.0001</StepSize>
<MinimumStepSize>0.00001</MinimumStepSize>
<MaximumStepSize>0.01</MaximumStepSize>
<MinimumChangeRate>0.2</MinimumChangeRate>
<MaximumChangeRate>1.5</MaximumChangeRate>
<ProportionalGain>0.2</ProportionalGain>
<IntegralGain>0.15</IntegralGain>
<RelativeTolerance>1e-6</RelativeTolerance>
<AbsoluteTolerance>1e-6</AbsoluteTolerance>
</EccoConfiguration>
</OspSystemStructure>
Binary file added examples/HarmonicOscillator.fmu
Binary file not shown.
Binary file added examples/HarmonicOscillator6D.fmu
Binary file not shown.
1 change: 1 addition & 0 deletions examples/TimeTableStructure.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<OspSystemStructure xmlns="http://opensimulationplatform.com/MSMI/OSPSystemStructure" version="0.1">
<StartTime>0.0</StartTime>
<BaseStepSize>0.1</BaseStepSize>
<Algorithm>fixedStep</Algorithm>
<Simulators>
<Simulator name="tab" source="TimeTableFMU.fmu" stepSize="0.1" />
</Simulators>
Expand Down
2 changes: 1 addition & 1 deletion examples/bouncing_ball_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def _interface(self, name: str, start: str | float | tuple) -> Variable:
variability="continuous",
initial="exact",
start=start,
rng=((0, "100 m"), None, (0, "10 m")),
rng=((0, "100 m"), None, (0, "39.371 inch")),
)
elif name == "speed":
return Variable(
Expand Down
78 changes: 60 additions & 18 deletions examples/driving_force_fmu.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import logging
from collections.abc import Callable
from functools import partial
from math import pi, sin
from typing import Any
from typing import Any, Callable

import numpy as np

Expand All @@ -15,27 +13,40 @@
# 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: float = 1.0, omega: float = 0.1):
return np.array((0, 0, ampl * sin(omega * time)), float)
def func(time: float, ampl: np.ndarray, omega: np.ndarray, d_omega: np.ndarray):
"""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 all(_do == 0.0 for _do in d_omega):
return ampl * np.sin(omega * time)
else:
return ampl * np.sin((omega + d_omega * time) * time)


class DrivingForce(Model):
"""A driving force in 3 dimensions which produces an ouput per time and can be connected to the oscillator.

Note1: the FMU model is made directly (without a basic python class model), which is not recommended!
Note2: the speed of the connected oscillator is added as additional connector.
Since the driving is forced, the input speed is ignored, but it is needed for the ECCO algorithm (power bonds).

Note: This completely replaces DrivingForce (do_step and other functions are not re-used).

Args:
func (callable)=func: The driving force function f(t).
Note: The func can currently not really be handled as parameter and must be hard-coded here (see above).
Soon to come: Model.build() function which honors parameters, such that function can be supplied from
outside and the FMU can be re-build without changing the class.
ampl (float|tuple) = 1.0: the amplitude of the (sinusoidal) driving force. Same for all D if float.
Optional with units.
freq (float|tuple) = 1.0: the frequency of the (sinusoidal) driving force. Same for all D if float.
Optional with units.
d_freq (float) = 0.0: Optional frequency change per time unit (for frequency sweep experiments).
"""

def __init__(
self,
func: Callable[..., Any] = func,
ampl: float = 1.0,
freq: float = 1.0,
ampl: float | tuple[float] | tuple[str] = 1.0,
freq: float | tuple[float] | tuple[str] = 1.0,
d_freq: float | tuple[float] | tuple[str] = 0.0,
**kwargs: Any,
):
super().__init__(
Expand All @@ -44,23 +55,54 @@ def __init__(
"Siegfried Eisinger",
**kwargs,
)
# interface Variables
self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=ampl)
self._freq = Variable(self, "freq", "The frequency of the force in 1/s", start=freq)
# interface Variables. We define first their values, to help pyright, since the basic model is missing
_ampl = ampl if isinstance(ampl, tuple) else (ampl,)
self.dim = len(_ampl)
_freq = freq if isinstance(freq, tuple) else (freq,)
assert len(_freq) == self.dim, f"ampl and freq are expected of same length. Found {ampl}, {freq}"
_d_freq = d_freq if isinstance(d_freq, tuple) else (d_freq,) * self.dim
assert len(_d_freq) == self.dim, f"d_freq expected as float or has same length as ampl:{ampl}. Found {d_freq}"
self.ampl = np.array((1.0,) * self.dim, float)
self.freq = np.array((1.0,) * self.dim, float)
self.d_freq = np.array((0.0,) * self.dim, float)
self.function = func
self.func: Callable
self.f = np.array((0.0,) * self.dim, float)
self.v_osc = (0.0,) * self.dim
self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=_ampl)
self._freq = Variable(self, "freq", "The frequency of the force in 1/s", start=_freq)
self._d_freq = Variable(self, "d_freq", "Change of frequency of the force in 1/s**2", start=_d_freq)
self._f = Variable(
self,
"f",
"Output connector for the driving force f(t) in N",
causality="output",
variability="continuous",
start=np.array((0, 0, 0), float),
start=(0.0,) * self.dim,
)
self._v_osc = Variable(
self,
"v_osc",
"Input connector for the speed of the connected element in m/s",
causality="input",
variability="continuous",
start=(0.0,) * self.dim,
)

def do_step(self, current_time: float, step_size: float):
self.f = self.func(current_time)
self.f = self.func(current_time + step_size)
return True # very important!

def exit_initialization_mode(self):
"""Set internal state after initial variables are set."""
self.func = partial(func, ampl=self.ampl, omega=2 * pi * self.freq) # type: ignore[reportAttributeAccessIssue]
logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}") # type: ignore[reportAttributeAccessIssue]
assert isinstance(self.ampl, np.ndarray)
assert isinstance(self.freq, np.ndarray)
assert isinstance(self.d_freq, np.ndarray)

self.func = partial(
self.function,
ampl=np.array(self.ampl, float),
omega=np.array(2 * np.pi * self.freq, float), # type: ignore # it is an ndarray!
d_omega=np.array(2 * np.pi * self.d_freq, float), # type: ignore # it is an ndarray!
)
logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}, d_freq={self.d_freq}")
Loading
Loading