Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e
* -/-


## [0.3.2] - 2026-01-23
## [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.
Expand Down
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ Development Setup

1. Install uv
^^^^^^^^^^^^^
This project uses `uv` as package manager.
This project uses ``uv`` as package manager.

If you haven't already, install `uv <https://docs.astral.sh/uv/>`_, preferably using it's `"Standalone installer" <https://docs.astral.sh/uv/getting-started/installation/#__tabbed_1_2/>`_ method:

Expand All @@ -319,7 +319,7 @@ If you haven't already, install `uv <https://docs.astral.sh/uv/>`_, preferably u

(see `docs.astral.sh/uv <https://docs.astral.sh/uv/getting-started/installation//>`_ for all / alternative installation methods.)

Once installed, you can update `uv` to its latest version, anytime, by running:
Once installed, you can update ``uv`` to its latest version, anytime, by running:

.. code:: sh

Expand Down Expand Up @@ -381,7 +381,7 @@ Run ``uv sync -U`` to create a virtual environment and install all project depen
**Note**: ``uv`` will create a new virtual environment called
``.venv`` in the project root directory when running ``uv sync -U``
the first time. Optionally, you can create your own virtual
environment using e.g. ``uv venv``, before running ``uv sync -U``.
environment using e.g. ``uv venv``, before running ``uv sync -U``.


5. (Optional) Activate the virtual environment
Expand Down
Binary file removed examples/DrivingForce.fmu
Binary file not shown.
Binary file removed examples/DrivingForce6D.fmu
Binary file not shown.
Binary file removed examples/HarmonicOscillator.fmu
Binary file not shown.
Binary file removed examples/HarmonicOscillator6D.fmu
Binary file not shown.
8 changes: 4 additions & 4 deletions examples/axle_fmu.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,23 @@ def __init__(
self._rpm0 = Variable(
self,
"wheels[0].motor.rpm",
"Angualar speed of wheel 1 in rad/s:",
"Angular speed of wheel 1 in rad/s:",
causality="input",
variability="continuous",
start=f"{rpm1} 1/s",
)
self._rpm1 = Variable(
self,
"wheels[1].motor.rpm",
"Angualar speed of wheel 2 in rad/s:",
"Angular speed of wheel 2 in rad/s:",
causality="input",
variability="continuous",
start=f"{rpm2} 1/s",
)
self._acc0 = Variable(
self,
"der(wheels[0].motor.rpm)",
"Angualar acceleration of wheel 1 in rad/s**2:",
"Angular acceleration of wheel 1 in rad/s**2:",
causality="input",
variability="continuous",
start="0.0 1/s**2",
Expand All @@ -90,7 +90,7 @@ def __init__(
Variable(
self,
"der(wheels[1].motor.rpm)",
"Angualar acceleration of wheel 2 in rad/s**2:",
"Angular acceleration of wheel 2 in rad/s**2:",
causality="input",
variability="continuous",
start="0.0 1/s**2",
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,12 @@ dependencies = [
"jsonpath-ng>=1.7.0",
"pythonfmu>=0.7.0",
"flexparser>=0.4",
"scipy-stubs>=1.17.0.0",
"scipy>=1.16",
]

[project.optional-dependencies]
test = [
"fmpy==0.3.21", # version 0.3.22 does so far (25.3.25) not workwhen installing through pip
"scipy>=1.16",
"fmpy>=0.3.21", # version 0.3.22 does so far (25.3.25) not workwhen installing through pip
"matplotlib>=3.10",
"plotly>=6.3",
"libcosimpy>=0.0.5",
Expand Down Expand Up @@ -103,6 +102,7 @@ dev = [
"sphinxcontrib-mermaid>=1.0.0",
"myst-parser>=4.0",
"furo>=2025.9",
"scipy-stubs>=1.17.0.0",
]

[tool.uv]
Expand Down
22 changes: 16 additions & 6 deletions src/component_model/utils/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@


class RW(Protocol):
"""Defines the read/write access function for a single controll, i.e. the .rw property.
"""Defines the read/write access function for a single control.

Since it is desired to have a default empty argument list (for read access),
a Protocol must be used for proper type checking. Implement as
A function passed as `rw` argument when instantiating a `Control` instance must satisfy this protocol.
The function is then accessible through the .rw property of the instantiated `Control` instance.

Implement either through a new class, implementing the full __call__ method or use functool.partial().
See `tests/test_controls.py` for examples
Using `Protocol` to define the signature of the read/write function allows for proper type checking
while still providing flexibility in the implementation of the read/write function.
It also allows the argument list to be empty when called for read access, which is desired in this case.

Implement the `RW` protocol either through a class, implementing the `__call__()` method, or use functools.partial()
to tweak the signature of an existing function to match the signature of `__call__()`, and return that.
See `tests/test_controls.py` for examples.
"""

def __call__(self, val: float | None = None, /) -> float: ...
Expand Down Expand Up @@ -55,12 +60,17 @@ def __init__(
):
self.name = name
self._limits = Control._prepare_limits(limits)
self.rw = rw
self._rw = rw
self.started: bool = False
self.goal: list[tuple[float, float]] = []
self.speed: float = 0.0 # may be used during goal tracking
self.acc: float = 0.0 # may be used during goal tracking

@property
def rw(self) -> RW:
"""Read-only property to access the read/write function."""
return self._rw

@staticmethod
def _prepare_limits(
limits: tuple[tuple[float | None, float | None] | float | None, ...],
Expand Down
2 changes: 0 additions & 2 deletions src/component_model/utils/fmu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
but which can be useful to work with fmu files (e.g. retrieving and working with a modelDefinition.xml file),
to make an OSP system structure file, or to reverse-engineer the interface of a model
(e.g. when making a surrogate model).


"""

import xml.etree.ElementTree as ET # noqa: N817
Expand Down
6 changes: 5 additions & 1 deletion src/component_model/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ def __init__(
self.local_name = f"der{parsed.der}_{parsed.var}"
if not hasattr(self.owner, self.local_name): # a virtual derivative
basevar = self.model.add_derivative(
self.name, parsed.as_string(("parent", "var", "der"), primitive=True)
dername=self.name,
basename=parsed.as_string(
include=("parent", "var", "der"),
primitive=True,
),
)
assert isinstance(basevar, Variable), f"The primitive of {self.name} must be a Variable object"
assert basevar.typ is float, f"The primitive of {self.name} shall be float. Found {basevar.typ}"
Expand Down
8 changes: 4 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,20 @@ def register_all_namespaces(filename: Path):
# return


@pytest.fixture(scope="package", autouse=True)
@pytest.fixture(scope="session", autouse=True)
def chdir() -> None:
"""
Fixture that changes the current working directory to the 'test_working_directory' folder.
This fixture is automatically used for the entire package.
This fixture is automatically used for the entire session.
"""
os.chdir(Path(__file__).parent.absolute() / "test_working_directory")


@pytest.fixture(scope="package", autouse=True)
@pytest.fixture(scope="session", autouse=True)
def test_dir() -> Path:
"""
Fixture that returns the absolute path of the directory containing the current file.
This fixture is automatically used for the entire package.
This fixture is automatically used for the entire session.
"""
return Path(__file__).parent.absolute()

Expand Down
2 changes: 1 addition & 1 deletion tests/test_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def do_goal(
value: float,
current: np.ndarray | None = None,
t_end: float = 10.0,
change: tuple[float, int, float]|None = None,
change: tuple[float, int, float] | None = None,
):
if change is not None:
t1, order1, val1 = change
Expand Down
4 changes: 2 additions & 2 deletions tests/test_oscillator_6dof_fmu.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _oscillator_6d_fmu():
assert src.exists(), f"Model file {src} not found."
fmu_path = Model.build(
script=str(src),
dest=Path(__file__).parent.parent / "examples" / "HarmonicOscillator6D.fmu",
dest=Path.cwd() / "HarmonicOscillator6D.fmu",
newargs={
"k": ("1N/m", "1N/m", "1N/m", "1N*m/rad", "1N*m/rad", "1N*m/rad"),
"c": ("0.1N*s/m", "0.1N*s/m", "0.1N*s/m", "0.1N*m*s/rad", "0.1N*m*s/rad", "0.1N*m*s/rad"),
Expand All @@ -45,7 +45,7 @@ def _driver_6d_fmu():
assert src.exists(), f"Model file {src} not found."
fmu_path = Model.build(
script=str(src),
dest=Path(__file__).parent.parent / "examples" / "DrivingForce6D.fmu",
dest=Path.cwd() / "DrivingForce6D.fmu",
newargs={"ampl": ("1.0N", "1.0N", "1.0N", "1.0N*m", "1.0N*m", "1.0N*m"), "freq": ("1.0Hz",) * 6},
)
return fmu_path
Expand Down
4 changes: 2 additions & 2 deletions tests/test_oscillator_fmu.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def _oscillator_fmu():
"""Make FMU and return .fmu file with path."""
fmu_path = Model.build(
script=Path(__file__).parent.parent / "examples" / "oscillator_fmu.py",
dest=Path(__file__).parent.parent / "examples" / "HarmonicOscillator.fmu",
dest=Path.cwd() / "HarmonicOscillator.fmu",
)
return fmu_path

Expand All @@ -33,7 +33,7 @@ def _driver_fmu():
"""Make FMU and return .fmu file with path."""
fmu_path = Model.build(
script=Path(__file__).parent.parent / "examples" / "driving_force_fmu.py",
dest=Path(__file__).parent.parent / "examples" / "DrivingForce.fmu",
dest=Path.cwd() / "DrivingForce.fmu",
newargs={"ampl": ("3N", "2N", "1N"), "freq": ("3Hz", "2Hz", "1Hz")},
)
return fmu_path
Expand Down
3 changes: 2 additions & 1 deletion tests/test_working_directory/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*test*.pdf
*.xml
*.fmu
*.fmu
*.dat
Loading