diff --git a/pythonfmu/__init__.py b/pythonfmu/__init__.py index 010886d..a5a976f 100644 --- a/pythonfmu/__init__.py +++ b/pythonfmu/__init__.py @@ -2,5 +2,6 @@ from .builder import FmuBuilder from .enums import Fmi2Causality, Fmi2Initial, Fmi2Variability from .fmi2slave import Fmi2Slave -from .variables import Boolean, Integer, Real, String +from .variables import Boolean, Integer, Real, String, Enumeration +from .type import Real as TypeReal, Integer as TypeInteger, Item, Enumeration as TypeEnum from .default_experiment import DefaultExperiment diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index 1599142..7ba9831 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -12,7 +12,8 @@ from .default_experiment import DefaultExperiment from ._version import __version__ as VERSION from .enums import Fmi2Type, Fmi2Status, Fmi2Causality, Fmi2Initial, Fmi2Variability -from .variables import Boolean, Integer, Real, ScalarVariable, String +from .variables import Boolean, Integer, Real, ScalarVariable, String, Enumeration +from .type import SimpleType, Real as TypeReal, Integer as TypeInteger, Item, Enumeration as TypeEnum ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"]) @@ -40,6 +41,7 @@ class Fmi2Slave(ABC): def __init__(self, **kwargs): self.vars = OrderedDict() + self.types = [] self.instance_name = kwargs["instance_name"] self.resources = kwargs.get("resources", None) self.visible = kwargs.get("visible", False) @@ -92,6 +94,11 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: SubElement(root, "CoSimulation", attrib=options) + if len(self.types) > 0: + types = SubElement(root, "TypeDefinitions") + for type_ in self.types: + types.append(type_.to_xml()) + if len(self.log_categories) > 0: categories = SubElement(root, "LogCategories") for category, description in self.log_categories.items(): @@ -145,6 +152,8 @@ def __apply_start_value(self, var: ScalarVariable): refs = self.get_boolean(vrs) elif isinstance(var, String): refs = self.get_string(vrs) + elif isinstance(var, Enumeration): + refs = self.get_integer(vrs) else: raise Exception(f"Unsupported type!") @@ -172,6 +181,14 @@ def register_variable(self, var: ScalarVariable, nested: bool = True): if var.setter is None and hasattr(owner, var.local_name) and var.variability != Fmi2Variability.constant: var.setter = lambda v: setattr(owner, var.local_name, v) + def register_type(self, type: SimpleType): + """Register a type as FMU interface. + + Args: + type (SimpleType): The type to be registered + """ + self.types.append(type) + def setup_experiment(self, start_time: float, stop_time: Optional[float], tolerance: Optional[float]): pass @@ -192,7 +209,7 @@ def get_integer(self, vrs: List[int]) -> List[int]: refs = list() for vr in vrs: var = self.vars[vr] - if isinstance(var, Integer): + if isinstance(var, Integer) or isinstance(var, Enumeration): refs.append(int(var.getter())) else: raise TypeError( diff --git a/pythonfmu/tests/slaves/pythonslave.py b/pythonfmu/tests/slaves/pythonslave.py index 8014d0a..8b4379c 100644 --- a/pythonfmu/tests/slaves/pythonslave.py +++ b/pythonfmu/tests/slaves/pythonslave.py @@ -1,4 +1,4 @@ -from pythonfmu.fmi2slave import Fmi2Slave, Fmi2Causality, Fmi2Variability, Integer, Real, Boolean, String +from pythonfmu.fmi2slave import Fmi2Slave, Fmi2Causality, Fmi2Variability, Integer, Real, Boolean, String, Enumeration, TypeEnum, Item class Container: @@ -19,15 +19,26 @@ def __init__(self, **kwargs): self.booleanVariable = True self.stringVariable = "Hello World!" self.realIn = 2. / 3. + self.realIn2 = 2. / 3. self.booleanParameter = False self.stringParameter = "dog" + self.enumParameter = 0 + + self.register_type(TypeEnum("enumType", item=[Item("red", 0), Item("green", 1), Item("blue", 2)])) + self.register_variable( Integer("intParam", causality=Fmi2Causality.parameter, variability=Fmi2Variability.tunable)) self.register_variable(Real("realIn", causality=Fmi2Causality.input)) + self.register_variable(Real("realIn2", causality=Fmi2Causality.input, start=30.0, min_=-100.0, max_=100.0, + unit="m", display_unit="ft", relative_quantity=True, nominal=10.0, unbounded=False, + derivative=1, reinit=True)) self.register_variable( Boolean("booleanParameter", causality=Fmi2Causality.parameter, variability=Fmi2Variability.tunable)) self.register_variable( String("stringParameter", causality=Fmi2Causality.parameter, variability=Fmi2Variability.tunable)) + self.register_variable( + Enumeration("enumParameter", causality=Fmi2Causality.parameter, variability=Fmi2Variability.tunable, + declared_type="enumType")) self.register_variable(Integer("intOut", causality=Fmi2Causality.output)) self.register_variable(Real("realOut", causality=Fmi2Causality.output)) @@ -47,3 +58,7 @@ def __init__(self, **kwargs): def do_step(self, current_time, step_size): self.realOut = current_time + step_size return True + + def enter_initialization_mode(self): + self.intOut = self.intParam + self.container.someReal = self.intParam diff --git a/pythonfmu/type.py b/pythonfmu/type.py new file mode 100644 index 0000000..4a38a84 --- /dev/null +++ b/pythonfmu/type.py @@ -0,0 +1,179 @@ +"""Classes describing interface type.""" +from abc import ABC +from typing import Any, Optional +from xml.etree.ElementTree import Element, SubElement + + +class SimpleType(ABC): + """Abstract FMI simple type definition. + + Args: + name (str): Type name + description (str, optional): Type description + """ + + def __init__( + self, + name: str, + description: Optional[str] = None, + getter: Any = None, + setter: Any = None + ): + self.getter = getter + self.setter = setter + self.local_name = name.split(".")[-1] + self.__attrs = { + "name": name, + "valueReference": None, + "description": description, # Only for ME + } + + @property + def name(self) -> str: + """str: Type name""" + return self.__attrs["name"] + + @property + def description(self) -> Optional[str]: + """str or None: Type description - None if not set""" + return self.__attrs["description"] + + def to_xml(self) -> Element: + """Convert the type to XML node. + + Returns + xml.etree.ElementTree.Element: XML node + """ + attrib = dict() + for key, value in self.__attrs.items(): + if value is not None: + attrib[key] = str(value) + return Element("SimpleType", attrib) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(name={self.name})" + + +class Real(SimpleType): + def __init__(self, name: str, quantity: Optional[str] = None, unit: Optional[str] = None, + display_unit: Optional[str] = None, relative_quantity: Optional[bool] = None, + min_: Optional[float] = None, max_: Optional[float] = None, nominal: Optional[float] = None, + unbounded: Optional[bool] = None, **kwargs): + super().__init__(name, **kwargs) + self.__attrs = {"quantity": quantity, "unit": unit, "displayUnit": display_unit, + "relativeQuantity": relative_quantity, "min": min_, "max": max_, "nominal": nominal, + "unbounded": unbounded} + + @property + def quantity(self) -> Optional[str]: + return self.__attrs["quantity"] + + @property + def unit(self) -> Optional[str]: + return self.__attrs["unit"] + + @property + def display_unit(self) -> Optional[str]: + return self.__attrs["displayUnit"] + + @property + def relative_quantity(self) -> Optional[bool]: + return self.__attrs["relativeQuantity"] + + @property + def min(self) -> Optional[float]: + return self.__attrs["min"] + + @property + def max(self) -> Optional[float]: + return self.__attrs["max"] + + @property + def nominal(self) -> Optional[float]: + return self.__attrs["nominal"] + + @property + def unbounded(self) -> Optional[bool]: + return self.__attrs["unbounded"] + + def to_xml(self) -> Element: + attrib = dict() + for key, value in self.__attrs.items(): + if value is not None: + # In order to not loose precision, a number of this type should be + # stored on an XML file with at least 16 significant digits + if key in ["min", "max", "nominal"]: + attrib[key] = f"{value:.16g}" + elif key in ["relativeQuantity", "unbounded"]: + attrib[key] = str(value).lower() + else: + attrib[key] = str(value) + parent = super().to_xml() + SubElement(parent, "Real", attrib) + + return parent + + +class Integer(SimpleType): + def __init__(self, name: str, quantity: Optional[str] = None, min_: Optional[int] = None, + max_: Optional[int] = None, **kwargs): + super().__init__(name, **kwargs) + self.__attrs = {"quantity": quantity, "min": min_, "max": max_} + + @property + def quantity(self) -> Optional[str]: + return self.__attrs["quantity"] + + @property + def min(self) -> Optional[int]: + return self.__attrs["min"] + + @property + def max(self) -> Optional[int]: + return self.__attrs["max"] + + def to_xml(self) -> Element: + attrib = dict() + for key, value in self.__attrs.items(): + if value is not None: + attrib[key] = str(value) + parent = super().to_xml() + SubElement(parent, "Integer", attrib) + + return parent + + +class Item: + def __init__(self, name: str, value: int, description: Optional[str] = None): + self.name = name + self.value = value + self.description = description + + +class Enumeration(SimpleType): + def __init__(self, name: str, item: list[Item], quantity: Optional[str] = None, **kwargs): + super().__init__(name, **kwargs) + self.__attrs = {"quantity": quantity, "Item": item} + + @property + def quantity(self) -> Optional[str]: + return self.__attrs["quantity"] + + @property + def item(self) -> list[Item]: + return self.__attrs["Item"] + + def to_xml(self) -> Element: + attrib = dict() + if self.quantity is not None: + attrib["quantity"] = self.quantity + parent = super().to_xml() + sub = SubElement(parent, "Enumeration", attrib) + + for item in self.item: + item_attrib = {"name": item.name, "value": str(item.value)} + if item.description is not None: + item_attrib["description"] = item.description + SubElement(sub, "Item", item_attrib) + + return parent diff --git a/pythonfmu/variables.py b/pythonfmu/variables.py index 086a2d9..97e9938 100644 --- a/pythonfmu/variables.py +++ b/pythonfmu/variables.py @@ -111,14 +111,64 @@ def __repr__(self) -> str: class Real(ScalarVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): + def __init__(self, name: str, start: Optional[float] = None, declared_type: Optional[str] = None, + quantity: Optional[str] = None, unit: Optional[str] = None, display_unit: Optional[str] = None, + relative_quantity: Optional[bool] = None, min_: Optional[float] = None, max_: Optional[float] = None, + nominal: Optional[float] = None, unbounded: Optional[bool] = None, derivative: Optional[int] = None, + reinit: Optional[bool] = None, **kwargs): super().__init__(name, **kwargs) - self.__attrs = {"start": start} + self.__attrs = {"start": start, "declaredType": declared_type, "quantity": quantity, "unit": unit, + "displayUnit": display_unit, "relativeQuantity": relative_quantity, "min": min_, "max": max_, + "nominal": nominal, "unbounded": unbounded, "derivative": derivative, "reinit": reinit} @property - def start(self) -> Optional[Any]: + def start(self) -> Optional[float]: return self.__attrs["start"] + @property + def declared_type(self) -> Optional[str]: + return self.__attrs["declaredType"] + + @property + def quantity(self) -> Optional[str]: + return self.__attrs["quantity"] + + @property + def unit(self) -> Optional[str]: + return self.__attrs["unit"] + + @property + def display_unit(self) -> Optional[str]: + return self.__attrs["displayUnit"] + + @property + def relative_quantity(self) -> Optional[bool]: + return self.__attrs["relativeQuantity"] + + @property + def min(self) -> Optional[float]: + return self.__attrs["min"] + + @property + def max(self) -> Optional[float]: + return self.__attrs["max"] + + @property + def nominal(self) -> Optional[float]: + return self.__attrs["nominal"] + + @property + def unbounded(self) -> Optional[bool]: + return self.__attrs["unbounded"] + + @property + def derivative(self) -> Optional[int]: + return self.__attrs["derivative"] + + @property + def reinit(self) -> Optional[bool]: + return self.__attrs["reinit"] + @start.setter def start(self, value: float): self.__attrs["start"] = value @@ -129,7 +179,12 @@ def to_xml(self) -> Element: if value is not None: # In order to not loose precision, a number of this type should be # stored on an XML file with at least 16 significant digits - attrib[key] = f"{value:.16g}" + if key in ["start", "min", "max", "nominal"]: + attrib[key] = f"{value:.16g}" + elif key in ["relativeQuantity", "unbounded", "reinit"]: + attrib[key] = str(value).lower() + else: + attrib[key] = str(value) parent = super().to_xml() SubElement(parent, "Real", attrib) @@ -137,16 +192,33 @@ def to_xml(self) -> Element: class Integer(ScalarVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): + def __init__(self, name: str, start: Optional[int] = None, declared_type: Optional[str] = None, + quantity: Optional[str] = None, min_: Optional[int] = None, max_: Optional[int] = None, **kwargs): super().__init__(name, **kwargs) - self.__attrs = {"start": start} + self.__attrs = {"start": start, "declaredType": declared_type, "quantity": quantity, "min": min_, "max": max_} @property - def start(self) -> Optional[Any]: + def start(self) -> Optional[int]: return self.__attrs["start"] + @property + def declared_type(self) -> Optional[str]: + return self.__attrs["declaredType"] + + @property + def quantity(self) -> Optional[str]: + return self.__attrs["quantity"] + + @property + def min(self) -> Optional[Any]: + return self.__attrs["min"] + + @property + def max(self) -> Optional[Any]: + return self.__attrs["max"] + @start.setter - def start(self, value: float): + def start(self, value: int): self.__attrs["start"] = value def to_xml(self) -> Element: @@ -161,23 +233,30 @@ def to_xml(self) -> Element: class Boolean(ScalarVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): + def __init__(self, name: str, start: Optional[bool] = None, declared_type: Optional[str] = None, **kwargs): super().__init__(name, **kwargs) - self.__attrs = {"start": start} + self.__attrs = {"start": start, "declaredType": declared_type} @property - def start(self) -> Optional[Any]: + def start(self) -> Optional[bool]: return self.__attrs["start"] + @property + def declared_type(self) -> Optional[str]: + return self.__attrs["declaredType"] + @start.setter - def start(self, value: float): + def start(self, value: bool): self.__attrs["start"] = value def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value).lower() + if key == "start": + attrib[key] = str(value).lower() + else: + attrib[key] = str(value) parent = super().to_xml() SubElement(parent, "Boolean", attrib) @@ -185,16 +264,20 @@ def to_xml(self) -> Element: class String(ScalarVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): + def __init__(self, name: str, start: Optional[str] = None, declared_type: Optional[str] = None, **kwargs): super().__init__(name, **kwargs) - self.__attrs = {"start": start} + self.__attrs = {"start": start, "declaredType": declared_type} @property - def start(self) -> Optional[Any]: + def start(self) -> Optional[str]: return self.__attrs["start"] + @property + def declared_type(self) -> Optional[str]: + return self.__attrs["declaredType"] + @start.setter - def start(self, value: float): + def start(self, value: str): self.__attrs["start"] = value def to_xml(self) -> Element: @@ -206,3 +289,44 @@ def to_xml(self) -> Element: SubElement(parent, "String", attrib) return parent + + +class Enumeration(ScalarVariable): + def __init__(self, name: str, start: Optional[int] = None, declared_type: Optional[str] = None, + quantity: Optional[str] = None, min_: Optional[int] = None, max_: Optional[int] = None, **kwargs): + super().__init__(name, **kwargs) + self.__attrs = {"start": start, "declaredType": declared_type, "quantity": quantity, "min": min_, "max": max_} + + @property + def start(self) -> Optional[int]: + return self.__attrs["start"] + + @property + def declared_type(self) -> Optional[str]: + return self.__attrs["declaredType"] + + @property + def quantity(self) -> Optional[str]: + return self.__attrs["quantity"] + + @property + def min(self) -> Optional[int]: + return self.__attrs["min"] + + @property + def max(self) -> Optional[int]: + return self.__attrs["max"] + + @start.setter + def start(self, value: int): + self.__attrs["start"] = value + + def to_xml(self) -> Element: + attrib = dict() + for key, value in self.__attrs.items(): + if value is not None: + attrib[key] = str(value) + parent = super().to_xml() + SubElement(parent, "Enumeration", attrib) + + return parent