Repository: CATIA-Systems/FMPy (language: python)

Issue:
Add support for String parameters to Modelica FMU import



Discussion / hints:

Fix patch:
diff --git a/fmpy/modelica/FMI/FMI2/Functions/FMI2SetString.mo b/fmpy/modelica/FMI/FMI2/Functions/FMI2SetString.mo
new file mode 100644
index 00000000..52b0d725
--- /dev/null
+++ b/fmpy/modelica/FMI/FMI2/Functions/FMI2SetString.mo
@@ -0,0 +1,8 @@
+within FMI.FMI2.Functions;
+impure function FMI2SetString
+  input Internal.ExternalFMU externalFMU;
+    input Integer vr[nvr];
+    input Integer nvr;
+    input String value[nvr];
+    external"C" FMU_FMI2SetString(externalFMU, vr, nvr, value) annotation (Library="ModelicaFMI");
+end FMI2SetString;
diff --git a/fmpy/modelica/FMI/FMI2/Functions/package.order b/fmpy/modelica/FMI/FMI2/Functions/package.order
index d28feb6a..c7f726ca 100644
--- a/fmpy/modelica/FMI/FMI2/Functions/package.order
+++ b/fmpy/modelica/FMI/FMI2/Functions/package.order
@@ -8,6 +8,7 @@ FMI2GetBooleanScalar
 FMI2SetReal
 FMI2SetInteger
 FMI2SetBoolean
+FMI2SetString
 FMI2EnterInitializationMode
 FMI2ExitInitializationMode
 FMI2DoStep
diff --git a/fmpy/modelica/FMI/FMI3/Functions/FMI3SetString.mo b/fmpy/modelica/FMI/FMI3/Functions/FMI3SetString.mo
new file mode 100644
index 00000000..ee715105
--- /dev/null
+++ b/fmpy/modelica/FMI/FMI3/Functions/FMI3SetString.mo
@@ -0,0 +1,8 @@
+within FMI.FMI3.Functions;
+impure function FMI3SetString
+  input Internal.ExternalFMU externalFMU;
+    input Integer valueReferences[nValueReferences];
+    input Integer nValueReferences;
+    input String values[nValueReferences];
+    external"C" FMU_FMI3SetString(externalFMU, valueReferences, nValueReferences, values) annotation (Library="ModelicaFMI");
+end FMI3SetString;
diff --git a/fmpy/modelica/FMI/FMI3/Functions/package.order b/fmpy/modelica/FMI/FMI3/Functions/package.order
index 7e91a4a8..8a4375c7 100644
--- a/fmpy/modelica/FMI/FMI3/Functions/package.order
+++ b/fmpy/modelica/FMI/FMI3/Functions/package.order
@@ -11,6 +11,7 @@ FMI3GetBooleanScalar
 FMI3SetFloat64
 FMI3SetInt32
 FMI3SetBoolean
+FMI3SetString
 FMI3UpdateDiscreteStates
 FMI3EnterContinuousTimeMode
 FMI3SetTime
diff --git a/fmpy/modelica/__init__.py b/fmpy/modelica/__init__.py
index e5e70514..8b84abf9 100644
--- a/fmpy/modelica/__init__.py
+++ b/fmpy/modelica/__init__.py
@@ -90,7 +90,7 @@ def import_fmu_to_modelica(fmu_path, model_path, interface_type):
 
     for variable in model_description.modelVariables:
 
-        if variable.type not in {'Float64', 'Int32', 'Real', 'Integer', 'Boolean'}:
+        if variable.type not in {'Float64', 'Int32', 'Real', 'Integer', 'Boolean', 'String'}:
             continue
 
         if variable.causality == 'parameter':
@@ -133,6 +133,8 @@ def as_quoted_array(values, default):
     def start_value(variable):
         if variable.type == 'Boolean':
             return 'true' if variable.start in ['true', '1'] else 'false'
+        elif variable.type == 'String':
+            return f'"{variable.start}"'
         else:
             return str(variable.start)
 
diff --git a/src/modelica/ModelicaFMI2.c b/src/modelica/ModelicaFMI2.c
index 53ecec99..3ab38a78 100644
--- a/src/modelica/ModelicaFMI2.c
+++ b/src/modelica/ModelicaFMI2.c
@@ -36,6 +36,10 @@ void FMU_FMI2SetBoolean(void* instance, const int vr[], int nvr, const int value
     if (nvr > 0) CALL(FMI2SetBoolean((FMIInstance*)instance, vr, nvr, value));
 }
 
+void FMU_FMI2SetString(void* instance, const int vr[], int nvr, const char* value[]) {
+    if (nvr > 0) CALL(FMI2SetString((FMIInstance*)instance, vr, nvr, value));
+}
+
 void FMU_FMI2SetupExperiment(void* instance,
     int toleranceDefined,
     double tolerance,
diff --git a/src/modelica/ModelicaFMI2.h b/src/modelica/ModelicaFMI2.h
index 0b26af4c..76799fcb 100644
--- a/src/modelica/ModelicaFMI2.h
+++ b/src/modelica/ModelicaFMI2.h
@@ -22,6 +22,8 @@ EXPORT void FMU_FMI2SetInteger(void* instance, const int vr[], int nvr, const in
 
 EXPORT void FMU_FMI2SetBoolean(void* instance, const int vr[], int nvr, const int value[]);
 
+EXPORT void FMU_FMI2SetString(void* instance, const int vr[], int nvr, const char* value[]);
+
 EXPORT void FMU_FMI2SetupExperiment(void* instance,
     int toleranceDefined,
     double tolerance,
diff --git a/src/modelica/ModelicaFMI3.c b/src/modelica/ModelicaFMI3.c
index a37157a9..00be448f 100644
--- a/src/modelica/ModelicaFMI3.c
+++ b/src/modelica/ModelicaFMI3.c
@@ -61,6 +61,10 @@ void FMU_FMI3SetBoolean(void* instance, const int valueReferences[], int nValueR
     if (nValueReferences > 0) CALL(FMI3SetBoolean((FMIInstance*)instance, valueReferences, nValueReferences, (fmi3Boolean*)values, nValueReferences));
 }
 
+void FMU_FMI3SetString(void* instance, const int valueReferences[], int nValueReferences, const char* values[]) {
+    if (nValueReferences > 0) CALL(FMI3SetString((FMIInstance*)instance, valueReferences, nValueReferences, (fmi3String*)values, nValueReferences));
+}
+
 void FMU_FMI3UpdateDiscreteStates(void* instance, int* valuesOfContinuousStatesChanged, double* nextEventTime) {
 
     fmi3Boolean _discreteStatesNeedUpdate;
diff --git a/src/modelica/ModelicaFMI3.h b/src/modelica/ModelicaFMI3.h
index 85b54f78..883e8769 100644
--- a/src/modelica/ModelicaFMI3.h
+++ b/src/modelica/ModelicaFMI3.h
@@ -34,6 +34,8 @@ EXPORT void FMU_FMI3SetInt32(void* instance, const int valueReferences[], int nV
 
 EXPORT void FMU_FMI3SetBoolean(void* instance, const int valueReferences[], int nValueReferences, const int values[]);
 
+EXPORT void FMU_FMI3SetString(void* instance, const int valueReferences[], int nValueReferences, const char* values[]);
+
 EXPORT void FMU_FMI3UpdateDiscreteStates(void* instance, int* valuesOfContinuousStatesChanged, double* nextEventTime);
 
 /***************************************************


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 8233dfa4..7e15f158 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -80,4 +80,7 @@ def dymola(executable, work_dir):
         # ensure, that MSL is loaded, as functions like exportSSP do not trigger demand loading
         dymola.openModelFile('Modelica')
 
+        # build 64-bit binaries
+        dymola.setVariable('Advanced.CompileWith64', 2)
+
         yield dymola
diff --git a/tests/test_import_to_modelica.py b/tests/test_import_to_modelica.py
index 02f26428..83936646 100644
--- a/tests/test_import_to_modelica.py
+++ b/tests/test_import_to_modelica.py
@@ -1,4 +1,5 @@
 import os
+from itertools import product
 
 import numpy as np
 import pytest
@@ -8,6 +9,11 @@
 from fmpy.util import validate_signal
 
 
+models = ['BouncingBall', 'Dahlquist', 'Stair', 'Resource', 'VanDerPol']
+fmi_versions = [2, 3]
+interface_types = ['CoSimulation', 'ModelExchange']
+
+
 def pymola_available():
     try:
         import pymola
@@ -32,85 +38,85 @@ def test_import_fmu_to_modelica(root_dir, reference_fmus_dist_dir):
 
 
 @pytest.mark.skipif(not om_available() or not pymola_available(), reason="OpenModelica and/or Pymola was not found")
-def test_run_examples_in_om(reference_fmus_dist_dir):
+@pytest.mark.parametrize('fmi_version, interface_type, model', product(fmi_versions, interface_types, models))
+def test_run_examples_in_om(root_dir, reference_fmus_dist_dir, fmi_version, interface_type, model):
 
-    import os
     from OMPython import OMCSessionZMQ
     from pymola import Dsres
 
-    os.environ['OPENMODELICAHOME'] = r'E:\OpenModelica'
-
     omc = OMCSessionZMQ()
 
-    assert omc.sendExpression('loadFile("E:/Development/FMPy/fmpy/modelica/FMI/package.mo")')
+    package = root_dir / 'fmpy' / 'modelica' / 'FMI' / 'package.mo'
 
-    for fmi_version in [2, 3]:
-        for interface_type in ['CoSimulation', 'ModelExchange']:
-            for model, start_values, stop_time in [
-                ('BouncingBall', {'e': 0.8}, 3),
-                ('Dahlquist', {}, 10),
-                ('Stair', {}, 8),
-                ('Resource', {}, 1),
-                ('VanDerPol', {'mu': 2.5}, 20),
-                # ('Feedthrough', {}, None),
-            ]:
+    assert omc.sendExpression(f'loadFile("{package.as_posix()}")')
 
-                info = omc.sendExpression(f'simulate(FMI.Examples.FMI{fmi_version}.{interface_type}.{model}, stopTime={stop_time})')
+    if model == 'BouncingBall':
+        stop_time = 3
+    elif model == 'Dahlquist':
+        stop_time = 10
+    elif model == 'Stair':
+        stop_time = 8
+    elif model == 'Resource':
+        stop_time = 1
+    elif model == 'VanDerPol':
+        stop_time = 20
 
-                result = Dsres(info['resultFile'])
+    info = omc.sendExpression(f'simulate(FMI.Examples.FMI{fmi_version}.{interface_type}.{model}, stopTime={stop_time})')
 
-                filename = str(reference_fmus_dist_dir / f'{fmi_version}.0' / f'{model}.fmu')
+    result = Dsres(info['resultFile'])
 
-                model_description = read_model_description(filename)
+    filename = str(reference_fmus_dist_dir / f'{fmi_version}.0' / f'{model}.fmu')
 
-                outputs = [v.name for v in model_description.modelVariables if v.causality == 'output']
+    model_description = read_model_description(filename)
 
-                reference = simulate_fmu(filename=filename,
-                                         # start_values=start_values,
-                                         output=outputs, stop_time=stop_time,
-                                         fmi_type=interface_type)
+    outputs = [v.name for v in model_description.modelVariables if v.causality == 'output']
 
-                for name in outputs:
-                    _, _, _, i_out = validate_signal(result['time'], result[f"'{name}'"], reference['time'],  reference[name])
-                    assert not i_out.any()
+    reference = simulate_fmu(filename=filename,
+                             # start_values=start_values,
+                             output=outputs, stop_time=stop_time,
+                             fmi_type=interface_type)
+
+    for name in outputs:
+        _, _, _, i_out = validate_signal(result['Time'], result[f"'{name}'"], reference['time'],  reference[name])
+        assert not i_out.any()
 
 
 @pytest.mark.skipif(not pymola_available(), reason="Pymola was not found")
-def test_run_examples_in_dymola(dymola, root_dir, reference_fmus_dist_dir):
+@pytest.mark.parametrize('fmi_version, interface_type, model', product(fmi_versions, interface_types, models))
+def test_run_examples_in_dymola(dymola, root_dir, reference_fmus_dist_dir, fmi_version, interface_type, model):
 
     dymola.loadClass(root_dir / 'fmpy' / 'modelica' / 'FMI' / 'package.mo')
 
-    for fmi_version in [2, 3]:
-        for interface_type in ['CoSimulation', 'ModelExchange']:
-            for model, start_values, stop_time in [
-                ('BouncingBall', {'e': 0.8}, None),
-                ('Dahlquist', {}, None),
-                ('Stair', {}, 8),
-                ('Resource', {}, None),
-                ('VanDerPol', {'mu': 2.5}, None),
-                # ('Feedthrough', {}, None),
-            ]:
+    start_values = {}
+    stop_time = None
+
+    if model == 'BouncingBall':
+        start_values = {'e': 0.8}
+    elif model == 'Stair':
+        stop_time = 8
+    elif model == 'VanDerPol':
+        start_values = {'mu': 2.5}
 
-                filename = str(reference_fmus_dist_dir / f'{fmi_version}.0' / f'{model}.fmu')
+    filename = str(reference_fmus_dist_dir / f'{fmi_version}.0' / f'{model}.fmu')
 
-                model_description = read_model_description(filename)
+    model_description = read_model_description(filename)
 
-                outputs = [v.name for v in model_description.modelVariables if v.causality == 'output']
+    outputs = [v.name for v in model_description.modelVariables if v.causality == 'output']
 
-                reference = simulate_fmu(filename=filename, start_values=start_values, output=outputs, stop_time=stop_time, fmi_type=interface_type)
+    reference = simulate_fmu(filename=filename, start_values=start_values, output=outputs, stop_time=stop_time, fmi_type=interface_type)
 
-                intial_values = dict(map(lambda i: (f"'{i[0]}'", i[1]), start_values.items()))
+    intial_values = dict(map(lambda i: (f"'{i[0]}'", i[1]), start_values.items()))
 
-                result = dymola.simulate(
-                    model=f'FMI.Examples.FMI{fmi_version}.{interface_type}.{model}',
-                    initialValues=intial_values,
-                    stopTime=stop_time
-                )
+    result = dymola.simulate(
+        model=f'FMI.Examples.FMI{fmi_version}.{interface_type}.{model}',
+        initialValues=intial_values,
+        stopTime=stop_time
+    )
 
-                for name in outputs:
-                    _, _, _, i_out = validate_signal(result['Time'], result[f"'{name}'"], reference['time'],  reference[name])
-                    assert not i_out.any()
+    for name in outputs:
+        _, _, _, i_out = validate_signal(result['Time'], result[f"'{name}'"], reference['time'],  reference[name])
+        assert not i_out.any()
 
-                for name, expected in intial_values.items():
-                    actual = result[name]
-                    assert np.allclose(actual, expected)
+    for name, expected in intial_values.items():
+        actual = result[name]
+        assert np.allclose(actual, expected)