Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e
* -/-


## [0.3.2] - 2026-01-16
## [0.3.2] - 2026-02-02

### Added
* Added new module unit.py, containing class `Unit`, a helper class to store and manage units and display units. One `Unit` object represents one scalar variable.
* Added new module range.py, containing class `Range`, a utility class to store and handle the variable range of a single-valued variable.
* Sphinx documentation:
* Added docs for modules variable_naming.py, enums.py, analytic.py and plotter.py
* Added docs for modules variable_naming.py, unit.py, range.py, enums.py and analytic.py
* Added Visual Studio Code settings

### Removed
* Removed module plotter.py

### Changed
* Updated code base with latest changes in python_project_template v0.2.6
* pyproject.toml:
Expand Down
3 changes: 2 additions & 1 deletion docs/source/component_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Modules
component_model.model
component_model.variable
component_model.variable_naming
component_model.unit
component_model.range
component_model.enums
component_model.analytic
component_model.plotter
Binary file modified examples/DrivingForce.fmu
Binary file not shown.
Binary file modified examples/DrivingForce6D.fmu
Binary file not shown.
Binary file modified examples/HarmonicOscillator.fmu
Binary file not shown.
Binary file modified examples/HarmonicOscillator6D.fmu
Binary file not shown.
21 changes: 10 additions & 11 deletions examples/axle.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def init_drive(self):
self.wheels[0].pos = np.array([0.0, 0.0], float)
self.wheels[1].pos = np.array([self.a, 0.0], float)
for i in range(2):
self.wheels[i].track = None
self.wheels[i].track_reset()
self.times = []

def drive(self, time: float, dt: float):
Expand Down Expand Up @@ -90,22 +90,21 @@ def pos(self) -> np.ndarray:
return self._pos

@pos.setter
def pos(self, newpos: list[float] | np.ndarray):
self.track = newpos
def pos(self, newpos: np.ndarray):
self.track_add(newpos)
self._pos = np.array(newpos, float)

@property
def track(self) -> list[list[float]]:
return self._track

@track.setter
def track(self, newpos: list[float] | np.ndarray | None):
"""Remember the track. None: start new track."""
if newpos is None: # reset
self._track = [[], []]
else:
for i in range(2):
self._track[i].append(newpos[i])
def track_reset(self):
self._track = [[], []]

def track_add(self, newpos: np.ndarray):
"""Remember the track."""
for i in range(2):
self._track[i].append(newpos[i])


class Motor:
Expand Down
22 changes: 12 additions & 10 deletions examples/bouncing_ball_3d.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# pyright: reportAttributeAccessIssue=false
# pyright: reportOptionalMemberAccess=false
from math import sqrt
from typing import Any

import numpy as np

Expand Down Expand Up @@ -30,26 +29,29 @@ class BouncingBall3D(Model):
def __init__(
self,
name: str = "BouncingBall3D",
description="Another Python-based BouncingBall model, using Model and Variable to construct a FMU",
pos: tuple = ("0 m", "0 m", "10 inch"),
speed: tuple = ("1 m/s", "0 m/s", "0 m/s"),
g="9.81 m/s^2",
e=0.9,
description: str = "Another Python-based BouncingBall model, using Model and Variable to construct a FMU",
pos: tuple[str | float, ...] = ("0 m", "0 m", "10 inch"),
speed: tuple[str | float, ...] = ("1 m/s", "0 m/s", "0 m/s"),
g: str | float = "9.81 m/s^2",
e: float = 0.9,
min_speed_z: float = 1e-6,
**kwargs,
**kwargs: Any,
):
super().__init__(name, description, author="DNV, SEACo project", **kwargs)
self.pos: np.ndarray
self._pos = self._interface("pos", pos)
self._speed = self._interface("speed", speed)
self.g: float
self._g = self._interface("g", g)
self.a = np.array((0, 0, -self.g), float)
self.e: float
self._e = self._interface("e", e)
self.min_speed_z = min_speed_z
self.stopped = False
self.time = 0.0
self._p_bounce = self._interface("p_bounce", ("0m", "0m", "0m")) # Note: 3D, but z always 0
# provoke an update at simulation start:
self.t_bounce, self.p_bounce = (-1.0, self.pos) # type: ignore
self.t_bounce, self.p_bounce = (-1.0, self.pos)

def do_step(self, current_time: float, step_size: float) -> bool:
"""Perform a simulation step from `self.time` to `self.time + step_size`.
Expand Down Expand Up @@ -110,7 +112,7 @@ def exit_initialization_mode(self):
super().exit_initialization_mode()
self.a = np.array((0, 0, -self.g), float)

def _interface(self, name: str, start: str | float | tuple) -> Variable:
def _interface(self, name: str, start: str | float | tuple[str | float, ...]) -> Variable:
"""Define a FMU2 interface variable, using the variable interface.

Args:
Expand Down
5 changes: 3 additions & 2 deletions examples/bouncing_ball_3d_pythonfmu.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from math import sqrt
from typing import Any

import numpy as np
from pythonfmu import Fmi2Causality, Fmi2Slave, Real # type: ignore[import-untyped]
Expand All @@ -16,7 +17,7 @@ class BouncingBall3D(Fmi2Slave):
* Internal units are assumed as SI (m,s,rad)
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs: Any):
super().__init__(
name="BouncingBall3D",
description="Another Python-based BouncingBall model, using Model and Variable to construct a FMU",
Expand Down Expand Up @@ -63,7 +64,7 @@ def __init__(self, **kwargs):
self.accelerationY = 0.0
self.accelerationZ = -self.g

def do_step(self, current_time, step_size) -> bool:
def do_step(self, current_time: float, step_size: float) -> bool:
"""Perform a simulation step from `self.time` to `self.time + step_size`.

With respect to bouncing (self.t_bounce should be initialized to a negative value)
Expand Down
6 changes: 3 additions & 3 deletions examples/driving_force_fmu.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(
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.func: Callable # type: ignore[reportMissingTypeArgument] ## kwargs
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)
Expand Down Expand Up @@ -102,7 +102,7 @@ def exit_initialization_mode(self):
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!
omega=np.array(2 * np.pi * self.freq, float),
d_omega=np.array(2 * np.pi * self.d_freq, float),
)
logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}, d_freq={self.d_freq}")
4 changes: 2 additions & 2 deletions examples/oscillator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(
c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0),
m: float | tuple[float, ...] = 1.0,
tolerance: float = 1e-5,
f_func: Callable | None = None,
f_func: Callable | None = None, # type: ignore[reportMissingTypeArgument] ## kwargs
):
self.dim = len(k)
self.k = np.array(k, float)
Expand Down Expand Up @@ -106,7 +106,7 @@ class Force:
func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector
"""

def __init__(self, func: Callable):
def __init__(self, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs
self.func = func
self.out = np.array((0, 0, 0), float)

Expand Down
4 changes: 2 additions & 2 deletions examples/oscillator_6d.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(
c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0),
m: float | tuple[float, ...] = 1.0,
tolerance: float = 1e-5,
f_func: Callable | None = None,
f_func: Callable | None = None, # type: ignore[reportMissingTypeArgument] ## kwargs
):
self.dim = len(k)
self.k = np.array(k, float)
Expand Down Expand Up @@ -106,7 +106,7 @@ class Force:
func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector
"""

def __init__(self, func: Callable):
def __init__(self, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs
self.func = func
self.out = np.array((0, 0, 0), float)

Expand Down
4 changes: 2 additions & 2 deletions examples/oscillator_xd.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ class Force:
denoting the force dependencies
"""

def __init__(self, dim: int, func: Callable):
def __init__(self, dim: int, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs
self.dim = dim
self.func = func
self.current_time = 0.0
self.dt = 0
self.out = np.array((0,) * self.dim)

def __call__(self, **kwargs):
def __call__(self, **kwargs: Any):
"""Calculate the force in dependence on keyword arguments 't', 'x' or 'v'."""
if "t" in kwargs:
t = kwargs["t"]
Expand Down
24 changes: 13 additions & 11 deletions examples/time_table.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence
from typing import Any

import numpy as np
from scipy.interpolate import make_interp_spline
Expand All @@ -21,16 +21,18 @@ class TimeTable:

def __init__(
self,
data: Sequence = (
(0.0, 1, 0, 0),
(1.0, 1, 1, 1),
(3.0, 1, 3, 9),
(7.0, 1, 7, 49),
), # default data set useful for testing
header: Sequence[str] | None = None,
data: list[list[int | float]] | None = None,
header: list[str] | None = None,
interpolate: int = 1,
**kwargs,
**kwargs: Any,
):
if data is None:
data = [ # default data set useful for testing
[0.0, 1, 0, 0],
[1.0, 1, 1, 1],
[3.0, 1, 3, 9],
[7.0, 1, 7, 49],
]
self._rows = len(data)
assert self._rows > 0, "Empty lookup table detected, which does not make sense"
self._cols = len(data[0]) - 1
Expand All @@ -46,15 +48,15 @@ def __init__(
assert len(header) == self._cols, "Number of header elements does not match number of columns in data"
self.header = tuple(header)
self.outs = self.data[0] # initial values
self.interpolate = self.set_interpolate(interpolate)
self.interpolate = self.set_interpolate(int(interpolate))

def set_interpolate(self, interpolate: int):
assert 0 <= interpolate <= 4, f"Erroneous interpolation exponent {self.interpolate}"
self._bspl = make_interp_spline(self.times, self.data, k=int(interpolate))
self.interpolate = interpolate
return interpolate

def lookup(self, time):
def lookup(self, time: float):
"""Do a simulation step of size 'stepSize at time 'time."""
self.outs = self._bspl(time)
return self.outs
12 changes: 7 additions & 5 deletions examples/time_table_fmu.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Sequence
from typing import Any

import numpy as np # noqa

Expand Down Expand Up @@ -29,12 +29,14 @@ class TimeTableFMU(Model, TimeTable): # refer to Model first!

def __init__(
self,
data: Sequence = ((0.0, 1, 0, 0), (1.0, 1, 1, 1), (3.0, 1, 3, 9), (7.0, 1, 7, 49)), # data useful for testing
header: Sequence[str] | None = None,
data: list[list[int | float]] | None = None,
header: list[str] | None = None,
interpolate: int = 1,
default_experiment: dict[str, float] | None = None,
**kwargs,
**kwargs: Any,
):
if data is None:
data = [[0.0, 1, 0, 0], [1.0, 1, 1, 1], [3.0, 1, 3, 9], [7.0, 1, 7, 49]] # ex.data
TimeTable.__init__(self, data, header, interpolate)
if default_experiment is None:
default_experiment = {"startTime": 0, "stopTime": 10.0, "stepSize": 0.1, "tolerance": 1e-5}
Expand All @@ -58,7 +60,7 @@ def __init__(
initial="exact",
start=interpolate,
rng=(0, 4),
on_set=self.set_interpolate,
on_set=self.set_interpolate, # type: ignore
) # the interpolation type can be set as parameter
self._outs = Variable(
self,
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ typeCheckingMode = "basic"
useLibraryCodeForTypes = true
reportMissingParameterType = "error"
reportUnknownParameterType = "warning"
reportUnknownMemberType = "warning" # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning
reportUnknownMemberType = false # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning
reportMissingTypeArgument = "error"
reportPropertyTypeMismatch = "error"
reportFunctionMemberAccess = "warning"
Expand All @@ -165,7 +165,7 @@ reportUnnecessaryIsInstance = "information"
reportUnnecessaryCast = "warning"
reportUnnecessaryComparison = "warning"
reportUnnecessaryContains = "warning"
reportUnusedCallResult = "warning"
reportUnusedCallResult = false # was "warning"
reportUnusedExpression = "warning"
reportMatchNotExhaustive = "warning"
reportUntypedFunctionDecorator = "warning"
Expand Down
42 changes: 25 additions & 17 deletions src/component_model/enums.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
"""Additional Enum objects for component-model and enum-related utilities."""

import logging
from enum import Enum, IntFlag, EnumType
from enum import Enum, EnumType, IntFlag

# import pythonfmu.enums # type: ignore
from pythonfmu.enums import Fmi2Causality as Causality # type: ignore
from pythonfmu.enums import Fmi2Initial as Initial # type: ignore
from pythonfmu.enums import Fmi2Variability as Variability # type: ignore
from pythonfmu.enums import Fmi2Causality as Causality
from pythonfmu.enums import Fmi2Initial as Initial
from pythonfmu.enums import Fmi2Variability as Variability

logger = logging.getLogger(__name__)


def ensure_enum(org: str | EnumType | None, default: EnumType | None) -> Enum | None:
def ensure_enum(org: str | Enum | None, default: Enum | EnumType | None) -> Enum | None:
"""Ensure that we have an Enum, based on the input as str, Enum or None."""
if org is None:
if org is None and default is None:
return None
raise ValueError("org and default shall not both be None") from None
elif org is None:
assert isinstance(default, Enum), f"Need an Enum (member) as default if org=None. Found {type(default)}"
return default
elif isinstance(org, str):
assert isinstance(default, Enum), "Need a default Enum here"
if org in type(default).__members__:
return type(default)[org]
elif default is None:
assert isinstance(org, Enum), "When no default is provided, org must be an Enum."
return org
elif isinstance(org, str): # both provided and org is a string
_default = default if isinstance(default, EnumType) else type(default) # need the Enum itself
if org in _default.__members__:
e: Enum = _default[org] # type: ignore[reportAssignmentType]
assert isinstance(e, Enum)
return e
else:
raise Exception(f"The value {org} is not compatible with the Enum {type(default)}") from None
else: # expect already an Enum
assert default is None or isinstance(org, type(default)), f"{org} is not member of the Enum {type(default)}"
raise Exception(f"The value {org} is not compatible with the Enum {_default}") from None
else: # expect already an EnumType
assert isinstance(org, type(default)), f"{org} is not member of the Enum {type(default)}"
return org


Expand Down Expand Up @@ -85,14 +93,14 @@ def check_causality_variability_initial(
variability: str | Enum | None, # EnumType | None,
initial: str | Enum | None,
) -> tuple[Causality | None, Variability | None, Initial | None]:
_causality = ensure_enum(causality, Causality.parameter) # type: ignore
_variability = ensure_enum(variability, Variability.constant) # type: ignore
_causality = ensure_enum(causality, Causality.parameter)
_variability = ensure_enum(variability, Variability.constant)
res = combination(_variability, _causality) # type: ignore
if res in ("a", "b", "c", "d", "e"): # combination is not allowed
logger.info(f"(causality {_causality}, variability {variability}) is not allowed: {explanations[res]}")
return (None, None, None)
else: # allowed
_initial = ensure_enum(initial, initial_default[res][0]) # type: ignore
_initial = ensure_enum(initial, initial_default[res][0])
if _initial not in initial_default[res][1]:
logger.info(f"(Causality {_causality}, variability {_variability}, Initial {_initial}) is not allowed")
return (None, None, None)
Expand Down
Loading