diff --git a/examples/alltypes.py b/examples/alltypes.py new file mode 100644 index 0000000..5bad6b9 --- /dev/null +++ b/examples/alltypes.py @@ -0,0 +1,52 @@ +from pythonfmu3 import Fmi3Causality, Fmi3Variability, Dimension, Fmi3Slave, Fmi3Status, Float64, Int32, Int64, UInt64, String, Boolean + + +TYPES = [UInt64, Float64, Boolean, Int32, Int64] +CAUSALITY = [Fmi3Causality.output, Fmi3Causality.input] +type_map = { + UInt64: int, + Float64: float, + Int32: int, + Int64: int, + Boolean: bool +} + +def var_names(var_type, causality): + return f"{var_type.__name__.lower()}_{causality.name.lower()}" + +def init_var(var_type, causality): + return type_map[var_type]() + +def create_vars(self): + for var_type in TYPES: + for causality in CAUSALITY: + name = var_names(var_type, causality) + if var_type == Float64: + var = var_type(name, causality=causality, variability=Fmi3Variability.continuous) + else: + var = var_type(name, causality=causality, variability=Fmi3Variability.discrete) + setattr(self, name, init_var(var_type, causality)) + self.register_variable(var) + +class AllTypes(Fmi3Slave): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Stephen Smith" + self.description = "All types example" + + self.time = 0.0 + + self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + + create_vars(self) + + + def do_step(self, current_time: float, step_size: float) -> Fmi3Status: + # feedthrough + for var_type in TYPES: + input_var = getattr(self, var_names(var_type, Fmi3Causality.input)) + setattr(self, var_names(var_type, Fmi3Causality.output), input_var) + + return True diff --git a/examples/arraytypes.py b/examples/arraytypes.py new file mode 100644 index 0000000..b04b4ff --- /dev/null +++ b/examples/arraytypes.py @@ -0,0 +1,74 @@ + +from pythonfmu3 import Fmi3Causality, Fmi3Variability, Dimension, Fmi3Slave, Fmi3Status, Float64, Int32, Int64, UInt64, String, Boolean + +import numpy as np + +SIZE = 10 + +TYPES = [UInt64, Float64, Boolean, Int32, Int64] +CAUSALITY = [Fmi3Causality.output, Fmi3Causality.input] + +TYPE_MAP = { + UInt64: np.uint64, + Float64: np.float64, + Int32: np.int32, + Int64: np.int64, + Boolean: bool +} + +def var_names(var_type, causality): + return f"{var_type.__name__.lower()}_{causality.name.lower()}" + + +def init_var(var_type, causality, array=True): + if array: + return np.zeros(SIZE, dtype=TYPE_MAP[var_type]) + else: + return TYPE_MAP[var_type]() + + +def create_vars(self): + dimensions = [Dimension(start=str(SIZE))] + for var_type in TYPES: + for causality in CAUSALITY: + name = var_names(var_type, causality) + if var_type == Float64: + var = var_type(name, causality=causality, variability=Fmi3Variability.continuous, dimensions=dimensions) + else: + var = var_type(name, causality=causality, variability=Fmi3Variability.discrete, dimensions=dimensions) + setattr(self, name, init_var(var_type, causality)) + self.register_variable(var) + + +def generate_random_data(self): + for var_type in TYPES: + causality = Fmi3Causality.output + name = var_names(var_type, causality) + var = getattr(self, name) + if var_type == Boolean: + setattr(self, name, np.random.choice(a=[False, True], size=SIZE).astype(bool)) + elif var_type == Int32 or var_type == Int64 or var_type == UInt64: + setattr(self, name, np.random.randint(0, 100, size=SIZE).astype(TYPE_MAP[var_type])) + else: + setattr(self, name, np.random.rand(SIZE).astype(TYPE_MAP[var_type])) + +class ArrayTypes(Fmi3Slave): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Stephen Smith" + self.description = "All types example" + + self.time = 0.0 + + self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + + create_vars(self) + + + def do_step(self, current_time: float, step_size: float) -> Fmi3Status: + generate_random_data(self) + return True + + diff --git a/pythonfmu3/fmi3slave.py b/pythonfmu3/fmi3slave.py index ebdbaa9..2a95dc1 100644 --- a/pythonfmu3/fmi3slave.py +++ b/pythonfmu3/fmi3slave.py @@ -183,8 +183,7 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: initial_unknown = list( filter(lambda v: (v.causality == Fmi3Causality.output and (v.initial in allowed_variability)) or v.causality == Fmi3Causality.calculatedParameter - or v in continuous_state_derivatives and v.initial in allowed_variability - or v.variability == Fmi3Variability.continuous and v.initial in allowed_variability and v.causality != Fmi3Causality.independent, self.vars.values()) + or v in continuous_state_derivatives and v.initial in allowed_variability, self.vars.values()) ) for v in outputs: @@ -282,10 +281,13 @@ def get_int32(self, vrs: List[int]) -> List[int]: for vr in vrs: var = self.vars[vr] if isinstance(var, Int32): - refs.append(int(var.getter())) + if len(var.dimensions) == 0: + refs.append(int(var.getter())) + else: + refs.extend(map(int, var.getter())) else: raise TypeError( - f"Variable with valueReference={vr} is not of type Integer!" + f"Variable with valueReference={vr} is not of type Int32!" ) return refs @@ -294,7 +296,10 @@ def get_int64(self, vrs: List[int]) -> List[int]: for vr in vrs: var = self.vars[vr] if isinstance(var, (Enumeration, Int64)): - refs.append(int(var.getter())) + if len(var.dimensions) == 0: + refs.append(int(var.getter())) + else: + refs.extend(map(int, var.getter())) else: raise TypeError( f"Variable with valueReference={vr} is not of type Int64!" @@ -306,11 +311,14 @@ def get_uint64(self, vrs: List[int]) -> List[ctypes.c_uint64]: for vr in vrs: var = self.vars[vr] if isinstance(var, UInt64): - val = var.getter() - refs.append(val if isinstance(val, ctypes.c_uint64) else ctypes.c_uint64(val)) + if len(var.dimensions) == 0: + val = var.getter() + refs.append(ctypes.c_uint64(val) if not isinstance(val, ctypes.c_uint64) else val) + else: + refs.extend(map(ctypes.c_uint64, var.getter())) else: raise TypeError( - f"Variable with valueReference={vr} is not of type UInt64!" + f"Variable with valueReference={vr} is not of type Uint64!" ) return refs @@ -325,7 +333,7 @@ def get_float64(self, vrs: List[int]) -> List[float]: refs.extend(var.getter()) else: raise TypeError( - f"Variable with valueReference={vr} is not of type Real!" + f"Variable with valueReference={vr} is not of type Float64!" ) return refs @@ -334,7 +342,11 @@ def get_boolean(self, vrs: List[int]) -> List[bool]: for vr in vrs: var = self.vars[vr] if isinstance(var, Boolean): - refs.append(bool(var.getter())) + if len(var.dimensions) == 0: + refs.append(bool(var.getter())) + else: + refs.extend(var.getter()) + else: raise TypeError( f"Variable with valueReference={vr} is not of type Boolean!" @@ -354,30 +366,48 @@ def get_string(self, vrs: List[int]) -> List[str]: return refs def set_int32(self, vrs: List[int], values: List[int]): - for vr, value in zip(vrs, values): + offset = 0 + for vr in vrs: var = self.vars[vr] if isinstance(var, Int32): - var.setter(value) + size = var.size(self.vars) + if size > 1: + var.setter(values[offset:offset+size]) + else: + var.setter(values[offset]) + offset += size else: raise TypeError( - f"Variable with valueReference={vr} is not of type Integer!" + f"Variable with valueReference={vr} is not of type Int32!" ) def set_int64(self, vrs: List[int], values: List[int]): - for vr, value in zip(vrs, values): + offset = 0 + for vr in vrs: var = self.vars[vr] if isinstance(var, (Enumeration, Int64)): - var.setter(value) + size = var.size(self.vars) + if size > 1: + var.setter(values[offset:offset+size]) + else: + var.setter(values[offset]) + offset += size else: raise TypeError( - f"Variable with valueReference={vr} is not of type Integer!" + f"Variable with valueReference={vr} is not of type Int64!" ) def set_uint64(self, vrs: List[int], values: List[int]): - for vr, value in zip(vrs, values): + offset = 0 + for vr in vrs: var = self.vars[vr] if isinstance(var, UInt64): - var.setter(value) + size = var.size(self.vars) + if size > 1: + var.setter(values[offset:offset+size]) + else: + var.setter(values[offset]) + offset += size else: raise TypeError( f"Variable with valueReference={vr} is not of type UInt64!" @@ -396,14 +426,20 @@ def set_float64(self, vrs: List[int], values: List[float]): offset += size else: raise TypeError( - f"Variable with valueReference={vr} is not of type Real!" + f"Variable with valueReference={vr} is not of type Float64!" ) def set_boolean(self, vrs: List[int], values: List[bool]): - for vr, value in zip(vrs, values): + offset = 0 + for vr in vrs: var = self.vars[vr] if isinstance(var, Boolean): - var.setter(value) + size = var.size(self.vars) + if size > 1: + var.setter(values[offset:offset+size]) + else: + var.setter(values[offset]) + offset += size else: raise TypeError( f"Variable with valueReference={vr} is not of type Boolean!" diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp b/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp index afe899e..ecc0855 100644 --- a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +++ b/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp @@ -66,7 +66,8 @@ void SlaveInstance::SetFloat64( void SlaveInstance::SetInt32( const FMIValueReference /*vr*/[], std::size_t nvr, - const FMIInt32 /*value*/[]) + const FMIInt32 /*value*/[], + std::size_t nValues) { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); @@ -76,7 +77,8 @@ void SlaveInstance::SetInt32( void SlaveInstance::SetInt64( const FMIValueReference /*vr*/[], std::size_t nvr, - const FMIInt64 /*value*/[]) + const FMIInt64 /*value*/[], + std::size_t nValues) { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); @@ -86,7 +88,8 @@ void SlaveInstance::SetInt64( void SlaveInstance::SetUInt64( const FMIValueReference /*vr*/[], std::size_t nvr, - const FMIUInt64 /*value*/[]) + const FMIUInt64 /*value*/[], + std::size_t nValues) { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); @@ -97,7 +100,8 @@ void SlaveInstance::SetUInt64( void SlaveInstance::SetBoolean( const FMIValueReference /*vr*/[], std::size_t nvr, - const FMIBoolean /*value*/[]) + const FMIBoolean /*value*/[], + std::size_t nValues) { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); @@ -108,7 +112,8 @@ void SlaveInstance::SetBoolean( void SlaveInstance::SetString( const FMIValueReference /*vr*/[], std::size_t nvr, - const FMIString /*value*/[]) + const FMIString /*value*/[], + std::size_t nValues) { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); @@ -131,7 +136,8 @@ void SlaveInstance::GetFloat64( void SlaveInstance::GetInt32( const FMIValueReference /*vr*/[], std::size_t nvr, - FMIInt32 /*value*/[]) const + FMIInt32 /*value*/[], + std::size_t nValues) const { if (nvr != 0) { throw std::logic_error("Attempted to get nonexistent variable"); @@ -142,7 +148,8 @@ void SlaveInstance::GetInt32( void SlaveInstance::GetInt64( const FMIValueReference /*vr*/[], std::size_t nvr, - FMIInt64 /*value*/[]) const + FMIInt64 /*value*/[], + std::size_t nValues) const { if (nvr != 0) { throw std::logic_error("Attempted to get nonexistent variable"); @@ -153,7 +160,8 @@ void SlaveInstance::GetInt64( void SlaveInstance::GetUInt64( const FMIValueReference /*vr*/[], std::size_t nvr, - FMIUInt64 /*value*/[]) const + FMIUInt64 /*value*/[], + std::size_t nValues) const { if (nvr != 0) { throw std::logic_error("Attempted to get nonexistent variable"); @@ -164,7 +172,8 @@ void SlaveInstance::GetUInt64( void SlaveInstance::GetBoolean( const FMIValueReference /*vr*/[], std::size_t nvr, - FMIBoolean /*value*/[]) const + FMIBoolean /*value*/[], + std::size_t nValues) const { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); @@ -175,7 +184,8 @@ void SlaveInstance::GetBoolean( void SlaveInstance::GetString( const FMIValueReference /*vr*/[], std::size_t nvr, - FMIString /*value*/[]) const + FMIString /*value*/[], + std::size_t nValues) const { if (nvr != 0) { throw std::logic_error("Attempted to set nonexistent variable"); diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 6b83fb4..61bc2cb 100644 --- a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -76,23 +76,28 @@ class SlaveInstance virtual void SetInt32( const FMIValueReference vr[], std::size_t nvr, - const FMIInt32 value[]); + const FMIInt32 value[], + std::size_t nValues); virtual void SetInt64( const FMIValueReference vr[], std::size_t nvr, - const FMIInt64 value[]); + const FMIInt64 value[], + std::size_t nValues); virtual void SetUInt64( const FMIValueReference vr[], std::size_t nvr, - const FMIUInt64 value[]); + const FMIUInt64 value[], + std::size_t nValues); virtual void SetBoolean( const FMIValueReference vr[], std::size_t nvr, - const FMIBoolean value[]); + const FMIBoolean value[], + std::size_t nValues); virtual void SetString( const FMIValueReference vr[], std::size_t nvr, - const FMIString value[]); + const FMIString value[], + std::size_t nValues); /* Called from fmi3GetXxx()/fmiGetXxx(). * Throws std::logic_error by default. @@ -105,23 +110,28 @@ class SlaveInstance virtual void GetInt32( const FMIValueReference vr[], std::size_t nvr, - FMIInt32 value[]) const; + FMIInt32 value[], + std::size_t nValues) const; virtual void GetInt64( const FMIValueReference vr[], std::size_t nvr, - FMIInt64 value[]) const; + FMIInt64 value[], + std::size_t nValues) const; virtual void GetUInt64( const FMIValueReference vr[], std::size_t nvr, - FMIUInt64 value[]) const; + FMIUInt64 value[], + std::size_t nValues) const; virtual void GetBoolean( const FMIValueReference vr[], std::size_t nvr, - FMIBoolean value[]) const; + FMIBoolean value[], + std::size_t nValues) const; virtual void GetString( const FMIValueReference vr[], std::size_t nvr, - FMIString value[]) const; + FMIString value[], + std::size_t nValues) const; // Called from fmi3DoStep()/fmiDoStep(). Must be implemented in model code. virtual FMIStatus DoStep( diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp index 9501db3..6cd61d4 100644 --- a/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +++ b/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp @@ -334,7 +334,7 @@ fmi3Status fmi3GetInt32( { const auto component = reinterpret_cast(c); try { - component->slave->GetInt32(vr, nvr, values); + component->slave->GetInt32(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -354,7 +354,7 @@ fmi3Status fmi3GetInt64( { const auto component = reinterpret_cast(c); try { - component->slave->GetInt64(vr, nvr, values); + component->slave->GetInt64(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -374,7 +374,7 @@ fmi3Status fmi3GetUInt64( { const auto component = reinterpret_cast(c); try { - component->slave->GetUInt64(vr, nvr, values); + component->slave->GetUInt64(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -394,7 +394,7 @@ fmi3Status fmi3GetBoolean( { const auto component = reinterpret_cast(c); try { - component->slave->GetBoolean(vr, nvr, values); + component->slave->GetBoolean(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -414,7 +414,7 @@ fmi3Status fmi3GetString( { const auto component = reinterpret_cast(c); try { - component->slave->GetString(vr, nvr, values); + component->slave->GetString(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -455,7 +455,7 @@ fmi3Status fmi3SetInt32( { const auto component = reinterpret_cast(c); try { - component->slave->SetInt32(vr, nvr, values); + component->slave->SetInt32(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -475,7 +475,7 @@ fmi3Status fmi3SetInt64( { const auto component = reinterpret_cast(c); try { - component->slave->SetInt64(vr, nvr, values); + component->slave->SetInt64(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -495,7 +495,7 @@ fmi3Status fmi3SetUInt64( { const auto component = reinterpret_cast(c); try { - component->slave->SetUInt64(vr, nvr, values); + component->slave->SetUInt64(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -515,7 +515,7 @@ fmi3Status fmi3SetBoolean( { const auto component = reinterpret_cast(c); try { - component->slave->SetBoolean(vr, nvr, values); + component->slave->SetBoolean(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); @@ -535,7 +535,7 @@ fmi3Status fmi3SetString( { const auto component = reinterpret_cast(c); try { - component->slave->SetString(vr, nvr, values); + component->slave->SetString(vr, nvr, values, nValues); return fmi3OK; } catch (const cppfmu::FatalError& e) { component->logger.Log(fmi3Fatal, "", e.what()); diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index b8c3f4c..59508be 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -285,13 +285,16 @@ void PySlaveInstance::SetFloat64(const cppfmu::FMIValueReference* vr, std::size_ }); } -void PySlaveInstance::SetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt32* values) +void PySlaveInstance::SetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt32* values, std::size_t nValues) { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); + PyObject* refs = PyList_New(nValues); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + + for (int i = 0; i < nValues; i++) { PyList_SetItem(refs, i, Py_BuildValue("i", values[i])); } @@ -306,13 +309,15 @@ void PySlaveInstance::SetInt32(const cppfmu::FMIValueReference* vr, std::size_t }); } -void PySlaveInstance::SetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt64* values) +void PySlaveInstance::SetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt64* values, std::size_t nValues) { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); + PyObject* refs = PyList_New(nValues); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nValues; i++) { PyList_SetItem(refs, i, Py_BuildValue("L", values[i])); } @@ -327,13 +332,15 @@ void PySlaveInstance::SetInt64(const cppfmu::FMIValueReference* vr, std::size_t }); } -void PySlaveInstance::SetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIUInt64* values) +void PySlaveInstance::SetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIUInt64* values, std::size_t nValues) { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); + PyObject* refs = PyList_New(nValues); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nValues; i++) { PyList_SetItem(refs, i, Py_BuildValue("K", values[i])); } @@ -348,13 +355,15 @@ void PySlaveInstance::SetUInt64(const cppfmu::FMIValueReference* vr, std::size_t }); } -void PySlaveInstance::SetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIBoolean* values) +void PySlaveInstance::SetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIBoolean* values, std::size_t nValues) { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); + PyObject* refs = PyList_New(nValues); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nValues; i++) { PyList_SetItem(refs, i, PyBool_FromLong(values[i])); } @@ -369,13 +378,15 @@ void PySlaveInstance::SetBoolean(const cppfmu::FMIValueReference* vr, std::size_ }); } -void PySlaveInstance::SetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString const* values) +void PySlaveInstance::SetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString const* values, std::size_t nValues) { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); PyObject* refs = PyList_New(nvr); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); + } + for (int i = 0; i < nValues; i++) { PyList_SetItem(refs, i, Py_BuildValue("s", values[i])); } @@ -413,9 +424,9 @@ void PySlaveInstance::GetFloat64(const cppfmu::FMIValueReference* vr, std::size_ }); } -void PySlaveInstance::GetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt32* values) const +void PySlaveInstance::GetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt32* values, std::size_t nValues) const { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); @@ -426,7 +437,7 @@ void PySlaveInstance::GetInt32(const cppfmu::FMIValueReference* vr, std::size_t handle_py_exception("[getInt32] PyObject_CallMethod", gilState); } - for (int i = 0; i < nvr; i++) { + for (int i = 0; i < nValues; i++) { PyObject* value = PyList_GetItem(refs, i); values[i] = static_cast(PyLong_AsLong(value)); } @@ -435,9 +446,9 @@ void PySlaveInstance::GetInt32(const cppfmu::FMIValueReference* vr, std::size_t }); } -void PySlaveInstance::GetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt64* values) const +void PySlaveInstance::GetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt64* values, std::size_t nValues) const { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); @@ -448,7 +459,7 @@ void PySlaveInstance::GetInt64(const cppfmu::FMIValueReference* vr, std::size_t handle_py_exception("[getInt64] PyObject_CallMethod", gilState); } - for (int i = 0; i < nvr; i++) { + for (int i = 0; i < nValues; i++) { PyObject* value = PyList_GetItem(refs, i); values[i] = static_cast(PyLong_AsLongLong(value)); } @@ -457,9 +468,9 @@ void PySlaveInstance::GetInt64(const cppfmu::FMIValueReference* vr, std::size_t }); } -void PySlaveInstance::GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIUInt64* values) const +void PySlaveInstance::GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIUInt64* values, std::size_t nValues) const { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); @@ -470,7 +481,7 @@ void PySlaveInstance::GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t handle_py_exception("[getUInt64] PyObject_CallMethod", gilState); } - for (int i = 0; i < nvr; i++) { + for (int i = 0; i < nValues; i++) { PyObject* item = PyList_GetItem(refs, i); PyObject* value = PyObject_GetAttrString(item, "value"); if (value && PyLong_Check(value)) @@ -484,9 +495,9 @@ void PySlaveInstance::GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t }); } -void PySlaveInstance::GetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIBoolean* values) const +void PySlaveInstance::GetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIBoolean* values, std::size_t nValues) const { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { PyObject* vrs = PyList_New(nvr); for (int i = 0; i < nvr; i++) { PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); @@ -497,7 +508,7 @@ void PySlaveInstance::GetBoolean(const cppfmu::FMIValueReference* vr, std::size_ handle_py_exception("[getBoolean] PyObject_CallMethod", gilState); } - for (int i = 0; i < nvr; i++) { + for (int i = 0; i < nValues; i++) { PyObject* value = PyList_GetItem(refs, i); values[i] = PyObject_IsTrue(value); } @@ -506,9 +517,9 @@ void PySlaveInstance::GetBoolean(const cppfmu::FMIValueReference* vr, std::size_ }); } -void PySlaveInstance::GetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString* values) const +void PySlaveInstance::GetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString* values, std::size_t nValues) const { - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { + py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) { clearStrBuffer(); PyObject* vrs = PyList_New(nvr); for (int i = 0; i < nvr; i++) { @@ -520,7 +531,7 @@ void PySlaveInstance::GetString(const cppfmu::FMIValueReference* vr, std::size_t handle_py_exception("[getString] PyObject_CallMethod", gilState); } - for (int i = 0; i < nvr; i++) { + for (int i = 0; i < nValues; i++) { PyObject* value = PyUnicode_AsEncodedString(PyList_GetItem(refs, i), "utf-8", nullptr); values[i] = PyBytes_AsString(value); strBuffer.emplace_back(value); diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index 8f7c204..609ca61 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -34,18 +34,18 @@ class PySlaveInstance : public cppfmu::SlaveInstance cppfmu::FMIFloat64& endOfStep) override; void SetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* value, std::size_t nValues) override; - void SetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt32* value) override; - void SetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt64* value) override; - void SetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIUInt64* value) override; - void SetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIBoolean* value) override; - void SetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString const* value) override; + void SetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt32* value, std::size_t nValues) override; + void SetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt64* value, std::size_t nValues) override; + void SetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIUInt64* value, std::size_t nValues) override; + void SetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIBoolean* value, std::size_t nValues) override; + void SetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString const* value, std::size_t nValues) override; void GetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIFloat64* value, std::size_t nValues) const override; - void GetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt32* value) const override; - void GetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt64* value) const override; - void GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIUInt64* value) const override; - void GetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIBoolean* value) const override; - void GetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString* value) const override; + void GetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt32* value, std::size_t nValues) const override; + void GetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt64* value, std::size_t nValues) const override; + void GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIUInt64* value, std::size_t nValues) const override; + void GetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIBoolean* value, std::size_t nValues) const override; + void GetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString* value, std::size_t nValues) const override; void GetFMUstate(fmi3FMUState& State) override; void SetFMUstate(const fmi3FMUState& State) override; diff --git a/pythonfmu3/tests/slaves/pythonslave_arraytypes.py b/pythonfmu3/tests/slaves/pythonslave_arraytypes.py new file mode 100644 index 0000000..c1a67d5 --- /dev/null +++ b/pythonfmu3/tests/slaves/pythonslave_arraytypes.py @@ -0,0 +1,80 @@ + +from pythonfmu3 import Fmi3Causality, Fmi3Variability, Dimension, Fmi3Slave, Fmi3Status, Float64, Int32, Int64, UInt64, String, Boolean + +import numpy as np + +SIZE = 10 + +TYPES = [UInt64, Float64, Boolean, Int32, Int64] +CAUSALITY = [Fmi3Causality.output, Fmi3Causality.input] + +TYPE_MAP = { + UInt64: np.uint64, + Float64: np.float64, + Int32: np.int32, + Int64: np.int64, + Boolean: bool +} + +def var_names(var_type, causality): + return f"{var_type.__name__.lower()}_{causality.name.lower()}" + + +def init_var(var_type, causality, array=True): + if array: + return np.zeros(SIZE, dtype=TYPE_MAP[var_type]) + else: + return TYPE_MAP[var_type]() + + +def create_vars(self): + dimensions = [Dimension(start=str(SIZE))] + for var_type in TYPES: + for causality in CAUSALITY: + name = var_names(var_type, causality) + if var_type == Float64: + var = var_type(name, causality=causality, variability=Fmi3Variability.continuous, dimensions=dimensions) + else: + var = var_type(name, causality=causality, variability=Fmi3Variability.discrete, dimensions=dimensions) + setattr(self, name, init_var(var_type, causality)) + self.register_variable(var) + + +def generate_data(self): + for var_type in TYPES: + causality = Fmi3Causality.output + name = var_names(var_type, causality) + var = getattr(self, name) + if var_type == Boolean: + data = [False, True, False, True, True, False, False, True, False, True] + setattr(self, name, np.array(data).astype(bool)) + elif var_type == Int32 or var_type == Int64 or var_type == UInt64: + data = [1,2,3,4,5,6,7,8,9,10] + setattr(self, name, np.array(data).astype(TYPE_MAP[var_type])) + else: + data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + setattr(self, name, np.array(data).astype(TYPE_MAP[var_type])) + +class ArrayTypes(Fmi3Slave): + + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "Tester" + self.description = "FMU with all basic types as arrays" + + self.time = 0.0 + + self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + + create_vars(self) + + generate_data(self) + + + def do_step(self, current_time: float, step_size: float) -> Fmi3Status: + generate_data(self) + return True + + diff --git a/pythonfmu3/tests/test_integration.py b/pythonfmu3/tests/test_integration.py index 10640a3..cc8a1b7 100644 --- a/pythonfmu3/tests/test_integration.py +++ b/pythonfmu3/tests/test_integration.py @@ -280,6 +280,57 @@ def test_integration_get(tmp_path): model.terminate() model.freeInstance() +@pytest.mark.integration +def test_integration_get_array(tmp_path): + script_file = Path(__file__).parent / "slaves/pythonslave_arraytypes.py" + fmu = FmuBuilder.build_FMU(script_file, dest=tmp_path, needsExecutionTool="false") + assert fmu.exists() + + md = fmpy.read_model_description(fmu) + unzip_dir = fmpy.extract(fmu) + + model = fmpy.fmi3.FMU3Slave( + guid=md.guid, + unzipDirectory=unzip_dir, + modelIdentifier=md.coSimulation.modelIdentifier, + instanceName='instance1') + + model.instantiate() + model.enterInitializationMode() + model.exitInitializationMode() + + to_test = { + "int32_output": [1,2,3,4,5,6,7,8,9,10], + "int64_output": [1,2,3,4,5,6,7,8,9,10], + "uint64_output": [1,2,3,4,5,6,7,8,9,10], + "float64_output": [1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0], + "boolean_output": [False, True, False, True, True, False, False, True, False, True] + } + + model_value = None + variables = mapped(md) + for key, value in to_test.items(): + var = variables[key] + vrs = [var.valueReference] + if var.type == "Int32": + model_values = model.getInt32(vrs, nValues=10) + elif var.type == "Int64": + model_values = model.getInt64(vrs, nValues=10) + elif var.type == "UInt64": + model_values = model.getUInt64(vrs, nValues=10) + elif var.type == "Float64": + model_values = model.getFloat64(vrs, nValues=10) + elif var.type == "Boolean": + model_values = model.getBoolean(vrs, nValues=10) + else: + pytest.xfail("Unsupported type") + + assert len(model_values) == len(value) + assert model_values == value + + model.terminate() + model.freeInstance() + @pytest.mark.integration def test_integration_read_from_file(tmp_path): @@ -367,6 +418,61 @@ def test_integration_set(tmp_path): model.terminate() model.freeInstance() +@pytest.mark.integration +def test_integration_set_array(tmp_path): + script_file = Path(__file__).parent / "slaves/pythonslave_arraytypes.py" + fmu = FmuBuilder.build_FMU(script_file, dest=tmp_path, needsExecutionTool="false") + assert fmu.exists() + + md = fmpy.read_model_description(fmu) + unzip_dir = fmpy.extract(fmu) + + model = fmpy.fmi3.FMU3Slave( + guid=md.guid, + unzipDirectory=unzip_dir, + modelIdentifier=md.coSimulation.modelIdentifier, + instanceName='instance1') + + model.instantiate() + model.enterInitializationMode() + model.exitInitializationMode() + + to_test = { + "int32_input": [1,2,3,4,5,6,7,8,9,10], + "int64_input": [1,2,3,4,5,6,7,8,9,10], + "uint64_input": [1,2,3,4,5,6,7,8,9,10], + "float64_input": [1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0], + "boolean_input": [False, True, False, True, True, False, False, True, False, True] + } + + model_value = None + variables = mapped(md) + for key, value in to_test.items(): + var = variables[key] + vrs = [var.valueReference] + if var.type == "Int32": + model.setInt32(vrs, value) + model_value = model.getInt32(vrs, nValues=10) + elif var.type == "Int64": + model.setInt64(vrs, value) + model_value = model.getInt64(vrs, nValues=10) + elif var.type == "UInt64": + model.setUInt64(vrs, value) + model_value = model.getUInt64(vrs, nValues=10) + elif var.type == "Float64": + model.setFloat64(vrs, value) + model_value = model.getFloat64(vrs,nValues=10) + elif var.type == "Boolean": + model.setBoolean(vrs, value) + model_value = model.getBoolean(vrs, nValues=10) + else: + pytest.xfail("Unsupported type") + + assert list(model_value) == value + + model.terminate() + model.freeInstance() + @pytest.mark.integration def test_simple_integration_fmpy(tmp_path): diff --git a/pythonfmu3/variables.py b/pythonfmu3/variables.py index c476f7f..c3aba08 100644 --- a/pythonfmu3/variables.py +++ b/pythonfmu3/variables.py @@ -50,6 +50,11 @@ def __init__( self.setter = setter self._type = None self.local_name = name.split(".")[-1] + + # demanagle names + self.local_name = self.local_name.lstrip('_') + + self.__attrs = { "name": name, "valueReference": None, @@ -151,7 +156,6 @@ def to_xml(self) -> Element: attrib["value"] = self.value return Element("Start", attrib) - class Dimension(object): def __init__(self, start: str = "", valueReference: str = ""): if start and valueReference and any((start, valueReference)): @@ -177,15 +181,38 @@ def to_xml(self) -> Element: ele = Element("Dimension", attrib) return ele -class Float64(ModelVariable): +class Arrayable(object): + def __init__(self, dimensions : List[Dimension] = [], **kwargs): + if dimensions: + check_numpy() + self._dimensions = dimensions + + @property + def dimensions(self) -> List[Dimension]: + return self._dimensions + + def get_start_str(self, value, formatter): + if len(self.dimensions) > 0: + output = " ".join([formatter(val) for val in flatten(value)]) + if len(output) > MAX_LENGTH: + output = output[0] + return output + else: + return formatter(value) + + def dimensions_xml(self) -> List[Element]: + return [dim.to_xml() for dim in self._dimensions] + + def size(self, vars): + return reduce(lambda x, dim: x * int(dim.size(vars)), self._dimensions, 1) + +class Float64(ModelVariable, Arrayable): def __init__(self, name: str, start: Optional[Any] = None, derivative: Optional[Any] = None, dimensions: List[Dimension] = [], unit: Optional[str] = None, **kwargs): - super().__init__(name, **kwargs) + ModelVariable.__init__(self, name, **kwargs) + Arrayable.__init__(self, dimensions) self.__attrs = {"start": start, "derivative": derivative} self._type = "Float64" self._unit = unit - if dimensions: - check_numpy() - self.__dimensions = dimensions @property def start(self) -> Optional[Any]: @@ -206,11 +233,6 @@ def unit(self, value: str): @property def derivative(self): return self.__attrs["derivative"] - - @property - def dimensions(self) -> List[Dimension]: - return self.__dimensions - def to_xml(self) -> Element: attrib = dict() @@ -218,13 +240,7 @@ 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 - output = "" - if len(self.dimensions) > 0: - output = " ".join([f"{val:.16g}" for val in flatten(value)]) - if len(output) > MAX_LENGTH: - output = output[0] - else: - output = f"{value:.16g}" + output = self.get_start_str(value, lambda v: f"{v:.16g}") attrib[key] = output if self.unit: @@ -233,18 +249,15 @@ def to_xml(self) -> Element: self._extras = attrib parent = super().to_xml() - for dimension in self.__dimensions: - parent.append(dimension.to_xml()) + parent.extend(self.dimensions_xml()) return parent - def size(self, vars): - return reduce(lambda x, dim: x * int(dim.size(vars)), self.__dimensions, 1) - -class Int32(ModelVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): - super().__init__(name, **kwargs) +class Int32(ModelVariable, Arrayable): + def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + ModelVariable.__init__(self, name, **kwargs) + Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} self._type = "Int32"; @@ -260,15 +273,18 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value) + attrib[key] = self.get_start_str(value, lambda v: str(v)) self._extras = attrib parent = super().to_xml() + + parent.extend(self.dimensions_xml()) return parent -class Int64(ModelVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): - super().__init__(name, **kwargs) +class Int64(ModelVariable, Arrayable): + def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + ModelVariable.__init__(self, name, **kwargs) + Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} self._type = "Int64"; @@ -284,15 +300,18 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value) + attrib[key] = self.get_start_str(value, lambda v: str(v)) self._extras = attrib parent = super().to_xml() + parent.extend(self.dimensions_xml()) + return parent -class UInt64(ModelVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): - super().__init__(name, **kwargs) +class UInt64(ModelVariable, Arrayable): + def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + ModelVariable.__init__(self, name, **kwargs) + Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} self._type = "UInt64"; @@ -308,15 +327,18 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value) + attrib[key] = self.get_start_str(value, lambda v: str(v)) self._extras = attrib parent = super().to_xml() - return parent + parent.extend(self.dimensions_xml()) -class Boolean(ModelVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): - super().__init__(name, **kwargs) + return parent + +class Boolean(ModelVariable, Arrayable): + def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + ModelVariable.__init__(self, name, **kwargs) + Arrayable.__init__(self, dimensions, **kwargs) self.__attrs = {"start": start} self._type = "Boolean" @@ -332,16 +354,18 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value).lower() + attrib[key] = self.get_start_str(value, lambda v: str(v).lower()) self._extras = attrib parent = super().to_xml() + + parent.extend(self.dimensions_xml()) return parent class String(ModelVariable): - def __init__(self, name: str, start: Optional[Any] = None, **kwargs): - super().__init__(name, **kwargs) + def __init__(self, name: str, start: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs): + ModelVariable.__init__(self, name, **kwargs) self.__attrs = dict() self._type = "String" self._start = Start(start) @@ -358,13 +382,13 @@ def to_xml(self) -> Element: attrib = dict() for key, value in self.__attrs.items(): if value is not None: - attrib[key] = str(value) + attrib[key] = str(value, lambda v: str(v)) self._extras = attrib parent = super().to_xml() if self.start is not None: parent.append(self._start.to_xml()) - + return parent class Enumeration(ModelVariable):