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
3 changes: 2 additions & 1 deletion pythonfmu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 19 additions & 2 deletions pythonfmu/fmi2slave.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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!")

Expand Down Expand Up @@ -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

Expand All @@ -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(
Expand Down
17 changes: 16 additions & 1 deletion pythonfmu/tests/slaves/pythonslave.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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))
Expand All @@ -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
179 changes: 179 additions & 0 deletions pythonfmu/type.py
Original file line number Diff line number Diff line change
@@ -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
Loading