Skip to content

Commit 18f07ce

Browse files
committed
Add FMUFieldFunction
1 parent 85ffb50 commit 18f07ce

File tree

5 files changed

+216
-1
lines changed

5 files changed

+216
-1
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2026-?? - release 0.18 (wip)
2+
- Add FMUFieldFunction (field -> field)
3+
14
2026-01-08 - release 0.17.1
25
- Default function export mode changed to cxx
36
- Fix multiple function export and cpython mode with conda envs

doc/api.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ the output consisting of the values at the final simulation time.
2828

2929
FMUFunction
3030

31+
The class **FMUFieldFunction** wraps the FMU evaluation in an :py:class:`openturns.FieldFunction`.
32+
Its input is a :py:class:`openturns.Field` and its output is a
33+
:py:class:`openturns.Field` gathering the outputs as function of time.
34+
35+
.. autosummary::
36+
:toctree: _generated/
37+
:template: class.rst_t
38+
39+
FMUFieldFunction
40+
3141
The class **FMUFieldToPointFunction** wraps the FMU evaluation in an :py:class:`openturns.FieldToPointFunction`.
3242
Its input is a :py:class:`openturns.Field` and its output is a vector
3343
(:py:class:`openturns.Point`) consisting of the values at the final simulation time.

otfmi/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.17.1"
1+
__version__ = "0.18"
22

33
from .otfmi import (
44
FMUFunction,
@@ -7,11 +7,14 @@
77
OpenTURNSFMUPointToFieldFunction,
88
FMUFieldToPointFunction,
99
OpenTURNSFMUFieldToPointFunction,
10+
FMUFieldFunction,
11+
OpenTURNSFMUFieldFunction,
1012
)
1113
from .function_exporter import FunctionExporter
1214
from .mo2fmu import mo2fmu
1315

1416
__all__ = [FMUFunction, OpenTURNSFMUFunction,
1517
FMUPointToFieldFunction, OpenTURNSFMUPointToFieldFunction,
1618
FMUFieldToPointFunction, OpenTURNSFMUFieldToPointFunction,
19+
FMUFieldFunction, OpenTURNSFMUFieldFunction,
1720
FunctionExporter, mo2fmu]

otfmi/otfmi.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,118 @@ def _exec(self, value_input, **kwargs):
711711
712712
"""
713713
return self.base.simulate(value_input=value_input, **kwargs)
714+
715+
716+
class FMUFieldFunction(ot.FieldFunction):
717+
"""
718+
Define a FieldFunction from a FMU file.
719+
720+
Parameters
721+
----------
722+
path_fmu : str, path to the FMU file.
723+
724+
input_mesh : :class:`openturns.Mesh`, default=None
725+
Time grid of the input variables, has to be included in the start/final time defined in the FMU.
726+
727+
output_mesh : :class:`openturns.Mesh`, default=None
728+
Time grid of the output variables, has to be included in the start/final time defined in the FMU.
729+
730+
inputs_fmu : Sequence of str, default=None
731+
Names of the variable from the fmu to be used as input variables.
732+
By default assigns variables with FMI causality INPUT.
733+
734+
outputs_fmu : Sequence of str, default=None
735+
Names of the variable from the fmu to be used as output variables.
736+
By default assigns variables with FMI causality OUTPUT.
737+
738+
initialization_script : str, default=None
739+
Path to the initialization script.
740+
741+
kind : str, default=None
742+
Either "ME" (model exchange) or "CS" (co-simulation)
743+
Select a kind of FMU if both are available.
744+
Note:
745+
Contrary to pyfmi, the default here is "CS" (co-simulation). The
746+
rationale behind this choice is that co-simulation may be used to
747+
impose a solver not available in pyfmi.
748+
749+
start_time : float, default=None
750+
The FMU simulation start time.
751+
The default behavior is to use the default start time defined the FMU.
752+
753+
final_time : float, default=None
754+
The FMU simulation stop time.
755+
The default behavior is to use the default stop time defined the FMU.
756+
757+
"""
758+
759+
def __new__(
760+
self,
761+
path_fmu,
762+
input_mesh=None,
763+
output_mesh=None,
764+
inputs_fmu=None,
765+
outputs_fmu=None,
766+
kind=None,
767+
initialization_script=None,
768+
start_time=None,
769+
final_time=None,
770+
):
771+
lowlevel = OpenTURNSFMUFieldFunction(
772+
path_fmu=path_fmu,
773+
input_mesh=input_mesh,
774+
output_mesh=output_mesh,
775+
inputs_fmu=inputs_fmu,
776+
outputs_fmu=outputs_fmu,
777+
kind=kind,
778+
initialization_script=initialization_script,
779+
start_time=start_time,
780+
final_time=final_time,
781+
)
782+
783+
highlevel = ot.FieldFunction(lowlevel)
784+
# highlevel._model = lowlevel.model
785+
return highlevel
786+
787+
788+
class OpenTURNSFMUFieldFunction(ot.OpenTURNSPythonFieldFunction):
789+
"""Define a FieldFunction from a FMU file."""
790+
791+
def __init__(
792+
self,
793+
path_fmu,
794+
input_mesh=None,
795+
output_mesh=None,
796+
inputs_fmu=None,
797+
outputs_fmu=None,
798+
initialization_script=None,
799+
kind=None,
800+
start_time=None,
801+
final_time=None,
802+
**kwargs
803+
):
804+
self.base = _FMUBaseFunction(path_fmu, kind=kind,
805+
inputs_fmu=inputs_fmu, outputs_fmu=outputs_fmu,
806+
start_time=start_time, final_time=final_time,
807+
initialization_script=initialization_script,
808+
field_input=True, input_mesh=input_mesh,
809+
output_mesh=output_mesh, field_output=True)
810+
811+
super().__init__(
812+
self.base.get_input_mesh(), len(self.base.get_inputs_fmu()),
813+
self.base.get_output_mesh(), len(self.base.get_outputs_fmu())
814+
)
815+
self.setInputDescription(self.base.get_inputs_fmu())
816+
self.setOutputDescription(self.base.get_outputs_fmu())
817+
818+
def _exec(self, value_input, **kwargs):
819+
"""Simulate the FMU for a given set of input values.
820+
821+
Parameters
822+
----------
823+
value_input : Vector or array-like with time steps as rows.
824+
825+
See the 'simulate' method for additional keyword arguments.
826+
827+
"""
828+
return self.base.simulate(value_input=value_input, **kwargs)

test/test_f2f.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python
2+
3+
import openturns as ot
4+
import openturns.testing as ott
5+
import otfmi
6+
import otfmi.example.utility
7+
import pytest
8+
9+
10+
@pytest.fixture
11+
def path_fmu():
12+
"""Load FMU and setup pure python reference."""
13+
return otfmi.example.utility.get_path_fmu("epid")
14+
15+
16+
@pytest.fixture
17+
def input_mesh():
18+
return ot.RegularGrid(2.0, 0.5, 50)
19+
20+
21+
def test_default_mesh(path_fmu):
22+
f = otfmi.FMUFieldFunction(
23+
path_fmu,
24+
inputs_fmu=["infection_rate", "healing_rate"],
25+
outputs_fmu=["infected"],
26+
)
27+
input_mesh = f.getInputMesh()
28+
start_time = input_mesh.getVertices().getMin()[0]
29+
end_time = input_mesh.getVertices().getMax()[0]
30+
ott.assert_almost_equal(start_time, 0.0)
31+
ott.assert_almost_equal(end_time, 200.0)
32+
33+
output_mesh = f.getOutputMesh()
34+
start_time = output_mesh.getVertices().getMin()[0]
35+
end_time = output_mesh.getVertices().getMax()[0]
36+
ott.assert_almost_equal(start_time, 0.0)
37+
ott.assert_almost_equal(end_time, 200.0)
38+
39+
40+
def test_start_time_coherence(path_fmu, input_mesh):
41+
"""Check if incoherent start time raises an error"""
42+
with pytest.raises(ValueError):
43+
_ = otfmi.FMUFieldFunction(
44+
path_fmu,
45+
input_mesh,
46+
inputs_fmu=["infection_rate", "healing_rate"],
47+
outputs_fmu=["infected"],
48+
start_time=10,
49+
)
50+
51+
52+
def test_start_time(path_fmu, input_mesh):
53+
"""Check if start times are taken into account."""
54+
model_fmu_1 = otfmi.FMUFieldFunction(
55+
path_fmu,
56+
input_mesh,
57+
inputs_fmu=["infection_rate", "healing_rate"],
58+
outputs_fmu=["infected"],
59+
start_time=0,
60+
)
61+
model_fmu_2 = otfmi.FMUFieldFunction(
62+
path_fmu,
63+
input_mesh,
64+
inputs_fmu=["infection_rate", "healing_rate"],
65+
outputs_fmu=["infected"],
66+
start_time=1,
67+
)
68+
n = input_mesh.getVerticesNumber()
69+
input_value = [[0.007, 0.02]] * n
70+
y1 = model_fmu_1(input_value)[-1]
71+
y2 = model_fmu_2(input_value)[-1]
72+
assert y2[0] - y1[0] != 0.0
73+
74+
75+
def test_final_time_coherence(path_fmu, input_mesh):
76+
"""Check if incoherent final time raises an error."""
77+
with pytest.raises(ValueError):
78+
_ = otfmi.FMUFieldFunction(
79+
path_fmu,
80+
input_mesh,
81+
inputs_fmu=["infection_rate", "healing_rate"],
82+
outputs_fmu=["infected"],
83+
final_time=10,
84+
)

0 commit comments

Comments
 (0)