Skip to content

Commit c8a95f5

Browse files
authored
parse fmi3 modeldescription.xml (#1512)
* check and set variable types when setting value * handle special FMI3Types in class * import FMI3 specical data types * set value for fmi3 through ssv files * update setValue.rst with FMI-3.0 data types
1 parent ac4f8bb commit c8a95f5

13 files changed

Lines changed: 639 additions & 61 deletions

File tree

doc/UsersGuide/source/api/oms3/setValue.rst

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ when the model is simulated or exported.
1111
* The parameter must exist in the referenced FMU/component or a top level system connectors; otherwise, an error will be raised.
1212
* Values are stored at the SSP level and exported together with the model structure.
1313
* Supported data types are numerical (``float``, ``int``, ``str``, ``bool``).
14+
* Special FMI-3.0 data types like ``Float64``, ``Float32``, ``Int8``, ``Int16``, ``Int32``, ``Int64``, ``UInt8``, ``UInt16``, ``UInt32``, ``UInt64`` are handled via special
15+
classes in OMSimulator like ``Float64``, ``Int32``, etc. These classes can be imported from OMSimulator and used to set parameter values with specific fmi3 data types.
1416
* Parameters can be set before or after instantation.
1517

1618
**Syntax:**
@@ -21,7 +23,7 @@ when the model is simulated or exported.
2123
2224
**Parameters:**
2325
- ``cref`` (*CRef*): Reference to the parameter variable within a component. The :class:`CRef` must specify the system, component, and parameter name (e.g., ``CRef("default", "Add1", "k1")``).
24-
- ``value`` (*float* | *int* | *str* | *bool* ): The value to assign to the parameter.
26+
- ``value`` (*float* | *int* | *str* | *bool* | *Float64* | *Float32* | *Int8* | *Int16* | *Int32* | *Int64* | *UInt8* | *UInt16* | *UInt32* | *UInt64*): The value to assign to the parameter.
2527

2628
**Example usage**:
2729

@@ -54,3 +56,28 @@ when the model is simulated or exported.
5456
5557
# Export the SSP with parameter values applied
5658
model.export("setValue2.ssp")
59+
60+
61+
**Example of setting FMI-3.0 specific data types:**
62+
63+
.. code-block:: python
64+
65+
from OMSimulator import SSP, CRef, Settings, Float64, Int32, UInt16, Float32, Int8, Int16, Int64, UInt8, UInt32, UInt64
66+
67+
model = SSP()
68+
model.addResource("../resources/Feedthrough3.fmu", new_name="resources/Feedthrough3.fmu")
69+
model.addComponent(CRef("default", "Feedthrough1"), "resources/Feedthrough3.fmu")
70+
71+
model.setValue(CRef("default", "Feedthrough1", "Float64_continuous_input"), Float64(3.5))
72+
model.setValue(CRef("default", "Feedthrough1", "Int64_input"), Int64(7))
73+
model.setValue(CRef("default", "Feedthrough1", "Float32_continuous_input"), Float32(1.5))
74+
model.setValue(CRef("default", "Feedthrough1", "Int8_input"), Int8(3))
75+
model.setValue(CRef("default", "Feedthrough1", "Boolean_input"), True)
76+
77+
model.setValue(CRef("default", "Feedthrough1", "Float32_discrete_input"), Float32(2.5))
78+
model.setValue(CRef("default", "Feedthrough1", "Enumeration_input"), 2)
79+
80+
model.setValue(CRef("default", "Feedthrough1", "Int16_input"), Int16(1000))
81+
model.setValue(CRef("default", "Feedthrough1", "UInt16_input"), UInt16(2000))
82+
model.setValue(CRef("default", "Feedthrough1", "UInt32_input"), UInt32(3000))
83+
model.setValue(CRef("default", "Feedthrough1", "UInt64_input"), UInt64(4000))

src/OMSimulatorPython/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from OMSimulator.ssv import SSV
1616
from OMSimulator.ssm import SSM
1717
from OMSimulator.system import System
18-
from OMSimulator.variable import Causality, SignalType, Variable
18+
from OMSimulator.variable import Causality, SignalType, Variable, Float32, Float64, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64
1919
from OMSimulator.instantiated_model import InstantiatedModel
2020

2121
# Define public API
@@ -26,6 +26,16 @@
2626
'Connector',
2727
'CRef',
2828
'FMU',
29+
'Float32',
30+
'Float64',
31+
'Int8',
32+
'Int16',
33+
'Int32',
34+
'Int64',
35+
'UInt8',
36+
'UInt16',
37+
'UInt32',
38+
'UInt64',
2939
'InstantiatedModel',
3040
'Settings',
3141
'SignalType',

src/OMSimulatorPython/connector.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ def importFromNode(node):
9898

9999
# Find the connector type (Real, Integer, Boolean)
100100
con = None
101-
for connectortype in ["ssc:Real", "ssc:Integer", "ssc:Boolean", "ssc:Enumeration"]: # Expected connector types
101+
## Expected connector type including fmi3 types
102+
types = ["ssc:Real", "ssc:Integer", "ssc:Boolean", "ssc:String", "ssc:Enumeration", "ssc:Float32", "ssc:Float64", "ssc:Int8", "ssc:UInt8", "ssc:Int16", "ssc:UInt16", "ssc:Int32", "ssc:UInt32", "ssc:Int64", "ssc:UInt64", "ssc:Binary"]
103+
for connectortype in types: # Expected connector types
102104
type_element = connector.find(connectortype, namespaces=namespace.ns)
103105
if type_element is not None:
104106
signal_type = connectortype.split(":")[-1] # Extracts 'Real', 'Integer', or 'Boolean'

src/OMSimulatorPython/fmu.py

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,20 @@ def _load_model_description(self):
117117
# Parse default experiment settings
118118
self._parse_default_experiment(model_description)
119119

120-
# Parse typeDefinitions
121-
self._parse_typeDefinitions(model_description)
122-
123120
# Parse UnitDefinitions
124121
self._parse_units(model_description)
125122

126123
# Dispatch version-specific handling
127124
if self._fmiVersion == "2.0":
125+
# Parse typeDefinitions
126+
self._parse_typeDefinitions_fmi2(model_description)
127+
# Parse ModelVariables
128128
self._parse_variables_fmi2(model_description)
129129
elif self._fmiVersion == "3.0":
130-
## TODO: implement FMI 3.0 variable parsing
131-
pass
130+
# Parse typeDefinitions
131+
self._parse_typeDefinitions_fmi3(model_description)
132+
# Parse ModelVariables
133+
self._parse_variables_fmi3(model_description)
132134
except ET.XMLSyntaxError as e:
133135
raise ValueError(f'Error parsing {model_desc_name}: {e}')
134136

@@ -197,6 +199,59 @@ def _parse_variables_fmi2(self, model_description):
197199
if not scalar_variables:
198200
raise Exception('ModelVariables section not found in modelDescription.xml')
199201

202+
def _parse_variables_fmi3(self, model_description):
203+
'''Parses variables from the ModelVariables section of modelDescription.xml'''
204+
model_variables = model_description.xpath('//ModelVariables')
205+
## iterate over all ModelVariables sections (fmi3 may have multiple)
206+
for model_var_section in model_variables:
207+
for element in model_var_section:
208+
# Element tag (e.g., Float64, Int32, Boolean, String, etc.)
209+
var_type = element.tag
210+
# Extract standard FMI 3.0 attributes
211+
name = element.get("name")
212+
description = element.get("description")
213+
value_reference = element.get("valueReference")
214+
causality = element.get("causality", "local")
215+
variability = element.get("variability", "continuous")
216+
initial = element.get("initial")
217+
start = element.get("start")
218+
derivative_index = int(element.get('derivative', '-1'))
219+
declaredType = element.get('declaredType')
220+
unit = element.get("unit")
221+
222+
# Handle state variables if it's a derivative
223+
if derivative_index > 0 and variability == 'continuous':
224+
self._states.append(self._variables[derivative_index - 1])
225+
226+
# Handle nested <Start> element for String/Binary/Enumeration types
227+
if start is None:
228+
start_elem = element.find("Start")
229+
if start_elem is not None:
230+
start = start_elem.get("value")
231+
232+
# print(f"var_name: {name}, var_type: {var_type}, causality: {causality}, variability: {variability}, initial: {initial}, start: {start}")
233+
234+
# Create and store the variable
235+
variable = Variable(name, description, value_reference, causality, variability, var_type, unit, start, declaredType)
236+
237+
# Assign unit definitions if applicable
238+
if unit:
239+
for defined_unit in self._unitDefinitions:
240+
if defined_unit.name == variable.unit:
241+
variable.unitDefinition.append(defined_unit)
242+
243+
# assign enumeration definitions if applicable
244+
if declaredType:
245+
for enumeration in self._enumerationDefinitions:
246+
if declaredType == enumeration.name:
247+
variable.enumerationDefinition.append(enumeration)
248+
249+
self._variables.append(variable)
250+
251+
# Raise an error if ModelVariables section is missing
252+
if not model_variables:
253+
raise Exception('ModelVariables section not found in modelDescription.xml')
254+
200255
def _parse_units(self, model_description):
201256
'''Extracts unit definitions from modelDescription.xml.'''
202257

@@ -212,7 +267,7 @@ def _parse_units(self, model_description):
212267

213268
self._unitDefinitions.append(Unit(name, base_units))
214269

215-
def _parse_typeDefinitions(self, model_description):
270+
def _parse_typeDefinitions_fmi2(self, model_description):
216271
'''Extracts only enumeration type definitions from modelDescription.xml.'''
217272
for simple_type in model_description.xpath('//TypeDefinitions/SimpleType'):
218273
type_name = simple_type.get('name')
@@ -225,6 +280,16 @@ def _parse_typeDefinitions(self, model_description):
225280
items[item.get("name")] = int(item.get("value"))
226281
self._enumerationDefinitions.append(Enumeration(type_name, items))
227282

283+
def _parse_typeDefinitions_fmi3(self, model_description):
284+
'''Extracts only enumeration type definitions from modelDescription.xml.'''
285+
for simple_type in model_description.xpath('//TypeDefinitions/EnumerationType'):
286+
type_name = simple_type.get('name')
287+
# Collect enumeration items
288+
items = {}
289+
for item in simple_type.xpath('./Item'):
290+
items[item.get("name")] = int(item.get("value"))
291+
self._enumerationDefinitions.append(Enumeration(type_name, items))
292+
228293
def makeConnectors(self):
229294
connectors = []
230295
for var in self.variables:

src/OMSimulatorPython/instantiated_model.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -243,18 +243,20 @@ def apply_start_value(self, value_path:str, value):
243243
type, status = Capi.getVariableType(value_path)
244244
if status != Status.ok:
245245
raise RuntimeError(f"Failed to get variable type for {value_path}: {status}")
246+
## TODO: handle FMi3 data types directly, like Float64, Int32,
247+
value_ = value.value if hasattr(value, 'value') else value
246248

247249
match SignalType(type):
248250
case SignalType.Real: # oms_signal_type_real
249-
self._setReal(value_path, value)
251+
self._setReal(value_path, float(value_))
250252
case SignalType.Integer: # oms_signal_type_integer
251-
self._setInteger(value_path, value)
253+
self._setInteger(value_path, int(value_))
252254
case SignalType.Boolean: # oms_signal_type_boolean
253-
self._setBoolean(value_path, value)
255+
self._setBoolean(value_path, bool(value_))
254256
case SignalType.String: # oms_signal_type_string
255-
self._setString(value_path, value)
257+
self._setString(value_path, str(value_))
256258
case SignalType.Enumeration: # oms_signal_type_enumeration
257-
self._setInteger(value_path, value) # Treat enumeration as integer
259+
self._setInteger(value_path, int(value_)) # Treat enumeration as integer
258260
case _:
259261
raise TypeError(f"Unsupported type: {type}")
260262

@@ -301,17 +303,19 @@ def setValue(self, cref: CRef, value):
301303
if status != Status.ok:
302304
raise RuntimeError(f"Failed to get variable type for {cref}: {status}")
303305

306+
value_ = value.value if hasattr(value, 'value') else value
307+
## TODO handle FMi3 data types directly, like Float64, Int32,etc..
304308
match SignalType(type):
305309
case SignalType.Real: # oms_signal_type_real
306-
return self._setReal(value_path, value)
310+
return self._setReal(value_path, float(value_))
307311
case SignalType.Integer: # oms_signal_type_integer
308-
return self._setInteger(value_path, value)
312+
return self._setInteger(value_path, int(value_))
309313
case SignalType.Boolean: # oms_signal_type_boolean
310-
return self._setBoolean(value_path, value)
314+
return self._setBoolean(value_path, bool(value_))
311315
case SignalType.String: # oms_signal_type_string
312-
return self._setString(value_path, value)
316+
return self._setString(value_path, str(value_))
313317
case SignalType.Enumeration: # oms_signal_type_enumeration
314-
return self._setInteger(value_path, value) # Treat enumeration as integer
318+
return self._setInteger(value_path, int(value_)) # Treat enumeration as integer
315319
case _:
316320
raise TypeError(f"Unsupported type: {type}")
317321

src/OMSimulatorPython/utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from OMSimulator.connection import Connection, ConnectionGeometry
77
from OMSimulator.connector import Connector, ConnectorGeometry
88
from OMSimulator.unit import Unit
9-
from OMSimulator.variable import Causality, SignalType
9+
from OMSimulator.variable import Causality, SignalType, Float64, Float32, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64
1010
from OMSimulator.elementgeometry import ElementGeometry
1111
from OMSimulator import namespace, CRef
1212
from collections import defaultdict
@@ -154,7 +154,18 @@ def parseParameterBindingHelper(parameters):
154154
"ssv:Real": float,
155155
"ssv:Integer": int,
156156
"ssv:Boolean": lambda v: v.lower() == "true", # Convert "true"/"false" to bool
157-
"ssv:String": str
157+
"ssv:String": str,
158+
## FMI-3.0 types
159+
"ssv:Float64": Float64,
160+
"ssv:Float32": Float32,
161+
"ssv:Int8": Int8,
162+
"ssv:Int16": Int16,
163+
"ssv:Int32": Int32,
164+
"ssv:Int64": Int64,
165+
"ssv:UInt8": UInt8,
166+
"ssv:UInt16": UInt16,
167+
"ssv:UInt32": UInt32,
168+
"ssv:UInt64": UInt64
158169
}
159170
for value_type, cast_func in value_types.items():
160171
value_element = param.find(value_type, namespaces=namespace.ns)

src/OMSimulatorPython/values.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from OMSimulator.unit import Unit
33
from OMSimulator.ssm import SSM
44
from OMSimulator import namespace
5-
5+
from OMSimulator.variable import Float32, Float64, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64
66

77
class Values:
88
def __init__(self):
@@ -27,18 +27,8 @@ def list(self, prefix = ""):
2727
return
2828

2929
for key, (value, unit, description) in self.start_values.items():
30-
match value:
31-
case float():
32-
type_tag = "Real"
33-
case bool(): # Check for boolean first, because it is a subclass of int
34-
type_tag = "Boolean"
35-
case int():
36-
type_tag = "Integer"
37-
case str():
38-
type_tag = "String"
39-
case _:
40-
raise TypeError(f"Unsupported type: {type(value)}")
41-
print(f"{prefix} ({type_tag} {key}, {value}, {unit}, '{description}')")
30+
type_tag = self._getVariableType(value)
31+
print(f"{prefix} ({type_tag} {key}, {str(value.value if hasattr(value, 'value') else value)}, {unit}, '{description}')")
4232

4333
def exportToSSD(self, node, parameterMapping : SSM | None = None, unitDefinitions = None):
4434
if self.empty():
@@ -82,18 +72,47 @@ def add_parameters(self, parameters_node, prefix = None):
8272
parameter_node.set("name", str(key))
8373
if description:
8474
parameter_node.set("description", description)
85-
match value:
86-
case float():
87-
type_tag = "Real"
88-
case bool(): # Check for boolean first, because it is a subclass of int
89-
type_tag = "Boolean"
90-
case int():
91-
type_tag = "Integer"
92-
case str():
93-
type_tag = "String"
94-
case _:
95-
raise TypeError(f"Unsupported type: {type(value)}")
75+
76+
type_tag = self._getVariableType(value)
77+
9678
parameter_type = ET.SubElement(parameter_node, namespace.tag("ssv", type_tag))
97-
parameter_type.set("value", str(value))
79+
value_ = str(value.value if hasattr(value, 'value') else value)
80+
if type_tag == "Boolean":
81+
value_ = "true" if value else "false"
82+
parameter_type.set("value", value_)
9883
if unit is not None:
9984
parameter_type.set("unit", unit)
85+
86+
def _getVariableType(self, value):
87+
match value:
88+
case float():
89+
return "Real"
90+
case bool(): # Check for boolean first, because it is a subclass of int
91+
return "Boolean"
92+
case int():
93+
return "Integer"
94+
case str():
95+
return "String"
96+
## handle fmi3 types
97+
case Float32():
98+
return "Float32"
99+
case Float64():
100+
return "Float64"
101+
case Int8():
102+
return "Int8"
103+
case Int16():
104+
return "Int16"
105+
case Int32():
106+
return "Int32"
107+
case Int64():
108+
return "Int64"
109+
case UInt8():
110+
return "UInt8"
111+
case UInt16():
112+
return "UInt16"
113+
case UInt32():
114+
return "UInt32"
115+
case UInt64():
116+
return "UInt64"
117+
case _:
118+
raise TypeError(f"Unsupported type: {type(value)}")

0 commit comments

Comments
 (0)