pythonfmu3 0.1.10__py3-none-any.whl → 0.1.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. pythonfmu3/__init__.py +2 -1
  2. pythonfmu3/_version.py +1 -1
  3. pythonfmu3/fmi3slave.py +23 -5
  4. pythonfmu3/osutil.py +2 -2
  5. pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +0 -3
  6. pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +4 -3
  7. pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp +18 -0
  8. pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +67 -8
  9. pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +3 -1
  10. pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp +67 -23
  11. pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  12. pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  13. pythonfmu3/tests/slaves/PythonSlaveWithException.py +4 -1
  14. pythonfmu3/tests/slaves/pythonslave.py +3 -0
  15. pythonfmu3/tests/slaves/pythonslave_read_file.py +4 -1
  16. pythonfmu3/tests/slaves/slavewithdep.py +4 -1
  17. pythonfmu3/tests/test_integration.py +41 -47
  18. pythonfmu3/tests/test_multiple_fmus.py +12 -10
  19. pythonfmu3/tests/test_variables.py +3 -1
  20. pythonfmu3/unit.py +62 -0
  21. pythonfmu3/{type_definitions.py → variable_types.py} +41 -25
  22. pythonfmu3/variables.py +46 -5
  23. {pythonfmu3-0.1.10.dist-info → pythonfmu3-0.1.16.dist-info}/METADATA +10 -15
  24. {pythonfmu3-0.1.10.dist-info → pythonfmu3-0.1.16.dist-info}/RECORD +28 -26
  25. {pythonfmu3-0.1.10.dist-info → pythonfmu3-0.1.16.dist-info}/WHEEL +1 -1
  26. {pythonfmu3-0.1.10.dist-info → pythonfmu3-0.1.16.dist-info}/LICENSE +0 -0
  27. {pythonfmu3-0.1.10.dist-info → pythonfmu3-0.1.16.dist-info}/entry_points.txt +0 -0
  28. {pythonfmu3-0.1.10.dist-info → pythonfmu3-0.1.16.dist-info}/top_level.txt +0 -0
pythonfmu3/__init__.py CHANGED
@@ -4,4 +4,5 @@ from .enums import Fmi3Causality, Fmi3Initial, Fmi3Variability
4
4
  from .fmi3slave import Fmi3Slave
5
5
  from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, String, Dimension
6
6
  from .default_experiment import DefaultExperiment
7
- from .type_definitions import TypeDefinitions, Item, EnumerationType
7
+ from .variable_types import Float64Type, EnumerationType
8
+ from .unit import BaseUnit, Unit
pythonfmu3/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.10"
1
+ __version__ = "0.1.16"
pythonfmu3/fmi3slave.py CHANGED
@@ -11,10 +11,11 @@ from xml.etree.ElementTree import Element, SubElement
11
11
 
12
12
  from .logmsg import LogMsg
13
13
  from .default_experiment import DefaultExperiment
14
- from .type_definitions import TypeDefinitions
15
14
  from ._version import __version__ as VERSION
16
15
  from .enums import Fmi3Type, Fmi3Status, Fmi3Causality, Fmi3Initial, Fmi3Variability
17
16
  from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, ModelVariable, String
17
+ from .variable_types import VariableType
18
+ from .unit import Unit
18
19
 
19
20
  ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"])
20
21
 
@@ -54,7 +55,9 @@ class Fmi3Slave(ABC):
54
55
  self.modelName: Optional[str] = self.__class__.__name__
55
56
  self.description: Optional[str] = None
56
57
  self.default_experiment: Optional[DefaultExperiment] = None
57
- self.type_definitions: Optional[TypeDefinitions] = None
58
+
59
+ self.type_definitions: Dict[str, VariableType] = {}
60
+ self.units: Dict[str, Unit] = {}
58
61
 
59
62
  def to_xml(self, model_options: Dict[str, str] = dict()) -> Element:
60
63
  """Build the XML representation of the model.
@@ -99,8 +102,15 @@ class Fmi3Slave(ABC):
99
102
 
100
103
  SubElement(root, "CoSimulation", attrib=options)
101
104
 
102
- if self.type_definitions is not None:
103
- root.append(self.type_definitions.to_xml())
105
+ if self.units:
106
+ unit_defs = SubElement(root, "UnitDefinitions")
107
+ for _, unit in self.units.items():
108
+ unit_defs.append(unit.to_xml())
109
+
110
+ if self.type_definitions:
111
+ type_defs = SubElement(root, "TypeDefinitions")
112
+ for _, val in self.type_definitions.items():
113
+ type_defs.append(val.to_xml())
104
114
 
105
115
  if len(self.log_categories) > 0:
106
116
  categories = SubElement(root, "LogCategories")
@@ -178,7 +188,7 @@ class Fmi3Slave(ABC):
178
188
  raise Exception(f"Unsupported type {type(var)}!")
179
189
  var.start = refs if len(getattr(var, "dimensions", [])) > 0 else refs[0]
180
190
 
181
- def register_variable(self, var: ModelVariable, nested: bool = True):
191
+ def register_variable(self, var: ModelVariable, nested: bool = True, var_type: Any = None):
182
192
  """Register a variable as FMU interface.
183
193
 
184
194
  Args:
@@ -206,10 +216,18 @@ class Fmi3Slave(ABC):
206
216
  var.setter = lambda v: setattr(owner, var.local_name, np.reshape(v, newshape=getattr(owner, var.local_name).shape))
207
217
  else:
208
218
  var.setter = lambda v: setattr(owner, var.local_name, v)
219
+
220
+ if var_type:
221
+ self.type_definitions[var_type.name] = var_type
222
+ var.declared_type = var_type.name
209
223
 
210
224
  def setup_experiment(self, start_time: float):
211
225
  pass
212
226
 
227
+ def register_units(self, units: List[Unit]):
228
+ for unit in units:
229
+ self.units[unit.name] = unit
230
+
213
231
  def enter_initialization_mode(self):
214
232
  pass
215
233
 
pythonfmu3/osutil.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import sys
2
- import platform
3
2
 
4
3
  import platform
5
4
 
@@ -8,7 +7,8 @@ def get_fmu_arch():
8
7
  system = platform.system()
9
8
  is_x86 = platform.machine() in ["i386", "AMD64", "x86_64"]
10
9
  platforms = {"Windows": "windows", "Linux": "linux", "Darwin": "darwin"}
11
- return "x86_64" if is_x86 else "x86" + "-" + platforms.get(system, "unknown")
10
+ arch = "x86_64" if is_x86 else "x86"
11
+ return arch + "-" + platforms.get(system, "unknown")
12
12
 
13
13
  def get_platform() -> str:
14
14
  """Get FMU binary platform folder name."""
@@ -105,11 +105,9 @@ public:
105
105
 
106
106
  Logger(
107
107
  FMIComponentEnvironment component,
108
- String instanceName,
109
108
  fmi3LogMessageCallback logCallback,
110
109
  std::shared_ptr<Settings> settings)
111
110
  : m_component{component}
112
- , m_instanceName(std::move(instanceName))
113
111
  , m_fmiLogger{logCallback}
114
112
  , m_settings{settings}
115
113
  {
@@ -154,7 +152,6 @@ public:
154
152
 
155
153
  private:
156
154
  const FMIComponentEnvironment m_component;
157
- const String m_instanceName;
158
155
  const FMICallbackLogger m_fmiLogger;
159
156
  std::shared_ptr<Settings> m_settings;
160
157
  };
@@ -47,10 +47,10 @@ struct Component
47
47
  modelError = 1 << 12,
48
48
  modelFatal = 1 << 13,
49
49
  };
50
- Component(cppfmu::FMIString instanceName,
50
+ Component(cppfmu::FMIComponentEnvironment instanceEnvironment,
51
51
  cppfmu::FMICallbackLogger logCallback,
52
52
  cppfmu::FMIBoolean loggingOn) : loggerSettings{std::make_shared<cppfmu::Logger::Settings>()},
53
- logger{this, instanceName, logCallback, loggerSettings}, lastSuccessfulTime{std::numeric_limits<cppfmu::FMIFloat64>::quiet_NaN()}
53
+ logger{instanceEnvironment, logCallback, loggerSettings}, lastSuccessfulTime{std::numeric_limits<cppfmu::FMIFloat64>::quiet_NaN()}
54
54
  {
55
55
  loggerSettings->debugLoggingEnabled = (loggingOn == cppfmu::FMITrue);
56
56
  }
@@ -96,7 +96,8 @@ fmi3Instance fmi3InstantiateCoSimulation(
96
96
  fmi3IntermediateUpdateCallback intermediateUpdate )
97
97
  {
98
98
  try {
99
- auto component = new Component(instanceName,
99
+ auto component = new Component(
100
+ environment,
100
101
  logMessage,
101
102
  loggingOn);
102
103
  component->slave = CppfmuInstantiateSlave(
@@ -0,0 +1,18 @@
1
+ #ifndef PYTHONFMU_IPYTHONSTATE_HPP
2
+ #define PYTHONFMU_IPYTHONSTATE_HPP
3
+
4
+ namespace pythonfmu
5
+ {
6
+ class IPyState
7
+ {
8
+ public:
9
+ IPyState() = default;
10
+ IPyState(IPyState const& other) = delete;
11
+ IPyState(IPyState&& other) = delete;
12
+ IPyState& operator=(IPyState const& other) = delete;
13
+ IPyState& operator=(IPyState&& other) = delete;
14
+ virtual ~IPyState() = default;
15
+ };
16
+ }
17
+
18
+ #endif //PYTHONFMU_IPYTHONSTATE_HPP
@@ -1,4 +1,5 @@
1
1
 
2
+ #include "pythonfmu/IPyState.hpp"
2
3
  #include "pythonfmu/PySlaveInstance.hpp"
3
4
 
4
5
  #include "pythonfmu/PyState.hpp"
@@ -7,6 +8,7 @@
7
8
 
8
9
  #include <fstream>
9
10
  #include <functional>
11
+ #include <mutex>
10
12
  #include <regex>
11
13
  #include <sstream>
12
14
  #include <utility>
@@ -44,8 +46,9 @@ inline void py_safe_run(const std::function<void(PyGILState_STATE gilState)>& f)
44
46
  PyGILState_Release(gil_state);
45
47
  }
46
48
 
47
- PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible)
48
- : instanceName_(std::move(instanceName))
49
+ PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible, std::shared_ptr<IPyState> pyState)
50
+ : pyState_{ std::move(pyState) }
51
+ , instanceName_(std::move(instanceName))
49
52
  , resources_(std::move(resources))
50
53
  , logger_(logger)
51
54
  , visible_(visible)
@@ -441,9 +444,14 @@ void PySlaveInstance::GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t
441
444
  }
442
445
 
443
446
  for (int i = 0; i < nvr; i++) {
444
- PyObject* value = PyList_GetItem(refs, i);
445
- values[i] = static_cast<cppfmu::FMIUInt64>(PyLong_AsUnsignedLongLong(value));
447
+ PyObject* item = PyList_GetItem(refs, i);
448
+ PyObject* value = PyObject_GetAttrString(item, "value");
449
+ if (value && PyLong_Check(value))
450
+ {
451
+ values[i] = static_cast<cppfmu::FMIUInt64>(PyLong_AsUnsignedLongLong(value));
452
+ }
446
453
  }
454
+
447
455
  Py_DECREF(refs);
448
456
  clearLogBuffer();
449
457
  });
@@ -625,7 +633,10 @@ PySlaveInstance::~PySlaveInstance()
625
633
 
626
634
  } // namespace pythonfmu
627
635
 
628
- std::unique_ptr<pythonfmu::PyState> pyState = nullptr;
636
+ namespace {
637
+ std::mutex pyStateMutex{};
638
+ std::shared_ptr<pythonfmu::PyState> pyState{};
639
+ }
629
640
 
630
641
  std::unique_ptr<cppfmu::SlaveInstance> CppfmuInstantiateSlave(
631
642
  cppfmu::FMIString instanceName,
@@ -650,9 +661,57 @@ std::unique_ptr<cppfmu::SlaveInstance> CppfmuInstantiateSlave(
650
661
  #endif
651
662
  }
652
663
 
653
- if (pyState == nullptr) {
654
- pyState = std::make_unique<pythonfmu::PyState>();
664
+ {
665
+ auto const ensurePyStateAlive = [&]() {
666
+ auto const lock = std::lock_guard{pyStateMutex};
667
+ if (nullptr == pyState) pyState = std::make_shared<pythonfmu::PyState>();
668
+ };
669
+
670
+ ensurePyStateAlive();
671
+ return std::make_unique<pythonfmu::PySlaveInstance>(
672
+ instanceName, resources, logger, visible, pyState);
655
673
  }
674
+ }
675
+
676
+ extern "C" {
656
677
 
657
- return std::make_unique<pythonfmu::PySlaveInstance>(instanceName, resources, logger, visible);
678
+ // The PyState instance owns it's own thread for constructing and destroying the Py* from the same thread.
679
+ // Creation of an std::thread increments ref counter of a shared library. So, when the client unloads the library
680
+ // the library won't be freed, as std::thread is alive, and the std::thread itself waits for de-initialization request.
681
+ // Thus, use DllMain on Windows and __attribute__((destructor)) on Linux for signaling to the PyState about de-initialization.
682
+ void finalizePythonInterpreter()
683
+ {
684
+ pyState = nullptr;
685
+ }
686
+ }
687
+
688
+ namespace
689
+ {
690
+ #ifdef _WIN32
691
+ #include <windows.h>
692
+
693
+ BOOL APIENTRY DllMain(HMODULE hModule,
694
+ DWORD ul_reason_for_call,
695
+ LPVOID lpReserved)
696
+ {
697
+ switch (ul_reason_for_call) {
698
+ case DLL_PROCESS_ATTACH:
699
+ break;
700
+ case DLL_THREAD_ATTACH:
701
+ case DLL_THREAD_DETACH:
702
+ break;
703
+ case DLL_PROCESS_DETACH:
704
+ finalizePythonInterpreter();
705
+ break;
706
+ }
707
+ return TRUE;
708
+ }
709
+ #elif defined(__linux__)
710
+ __attribute__((destructor)) void onLibraryUnload()
711
+ {
712
+ finalizePythonInterpreter();
713
+ }
714
+ #else
715
+ #error port the code
716
+ #endif
658
717
  }
@@ -3,6 +3,7 @@
3
3
  #define PYTHONFMU_SLAVEINSTANCE_HPP
4
4
 
5
5
  #include "cppfmu/cppfmu_cs.hpp"
6
+ #include "pythonfmu/IPyState.hpp"
6
7
 
7
8
  #include <Python.h>
8
9
  #include <string>
@@ -15,7 +16,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance
15
16
  {
16
17
 
17
18
  public:
18
- PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, bool visible);
19
+ PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, bool visible, std::shared_ptr<IPyState> pyState);
19
20
 
20
21
  void initialize(PyGILState_STATE gilState);
21
22
 
@@ -53,6 +54,7 @@ public:
53
54
  ~PySlaveInstance() override;
54
55
 
55
56
  private:
57
+ std::shared_ptr<IPyState> pyState_;
56
58
  PyObject* pClass_;
57
59
  PyObject* pInstance_{};
58
60
  PyObject* pMessages_{};
@@ -2,40 +2,84 @@
2
2
  #ifndef PYTHONFMU_PYTHONSTATE_HPP
3
3
  #define PYTHONFMU_PYTHONSTATE_HPP
4
4
 
5
+ #include "IPyState.hpp"
5
6
  #include <Python.h>
7
+ #include <condition_variable>
6
8
  #include <iostream>
9
+ #include <mutex>
10
+ #include <thread>
7
11
 
8
12
  namespace pythonfmu
9
13
  {
10
-
11
- class PyState
12
- {
13
- public:
14
- PyState()
14
+ class PyState : public IPyState
15
15
  {
16
- was_initialized_ = Py_IsInitialized();
16
+ public:
17
+ PyState()
18
+ : _initDeinitPyThread_{ &PyState::Worker, this }
19
+ {
20
+ auto lock = std::unique_lock{ mutex_ };
21
+ conditionalVariable_.wait(lock, [&] { return consructed_; });
22
+ }
17
23
 
18
- if (!was_initialized_) {
19
- Py_SetProgramName(L"./PythonFMU");
20
- Py_Initialize();
21
- PyEval_InitThreads();
22
- _mainPyThread = PyEval_SaveThread();
24
+ ~PyState() override
25
+ {
26
+ {
27
+ auto const lock = std::lock_guard{ mutex_ };
28
+ destroyRequested_ = true;
29
+ }
30
+ conditionalVariable_.notify_one();
31
+ if (_initDeinitPyThread_.joinable()) _initDeinitPyThread_.join();
23
32
  }
24
- }
25
33
 
26
- ~PyState()
27
- {
28
- if (!was_initialized_) {
29
- PyEval_RestoreThread(_mainPyThread);
30
- Py_Finalize();
34
+ private:
35
+
36
+ // In accordance to the documentation https://docs.python.org/3/c-api/init.html#c.Py_FinalizeEx
37
+ // the Py_Initialize/Py_Finalize should be called from the same
38
+ // thread. The FMI standard allows to call fmi functions from different threads, also different threads
39
+ // could be used for loading unloading the FMU libraries. There is a deadlock, when the simulation tool
40
+ // unloads the FMU library from the thread which is differs from the thread where the Py_Initialize was called.
41
+ // Create a new thread which is used for calling Py_Initialize and Py_Deinitialize. The thread waits for
42
+ // notification from destructor and then calls Py_Deinitialize.
43
+
44
+ void Worker()
45
+ {
46
+ // It will be nullptr in case when some other tool already called Py_IsInitialized.
47
+ // There is no need to call Py_Finalize thus, this thread can exit ASAP.
48
+ auto const mainPyThread = []() -> PyThreadState* {
49
+ auto const justInitialized = !Py_IsInitialized();
50
+ if (justInitialized) {
51
+ Py_SetProgramName(L"./PythonFMU");
52
+ Py_Initialize();
53
+ #if PY_VERSION_HEX < 0x03070000
54
+ PyEval_InitThreads();
55
+ #endif
56
+ return PyEval_SaveThread();
57
+ }
58
+ return nullptr;
59
+ }();
60
+
61
+ {
62
+ auto const lock = std::lock_guard{ mutex_ };
63
+ consructed_ = true;
64
+ }
65
+ conditionalVariable_.notify_one();
66
+
67
+ if (nullptr != mainPyThread) {
68
+ auto lock = std::unique_lock{ mutex_ };
69
+ conditionalVariable_.wait(lock, [&] { return destroyRequested_; });
70
+
71
+ PyEval_RestoreThread(mainPyThread);
72
+ Py_Finalize();
73
+ }
31
74
  }
32
- }
33
75
 
34
- private:
35
- bool was_initialized_;
36
- PyThreadState* _mainPyThread;
37
- };
76
+ bool consructed_ = false;
77
+ bool destroyRequested_ = false;
78
+ std::condition_variable conditionalVariable_;
79
+ std::mutex mutex_;
80
+ std::thread _initDeinitPyThread_;
81
+ };
38
82
 
39
83
  } // namespace pythonfmu
40
84
 
41
- #endif //PYTHONFMU_PYTHONSTATE_HPP
85
+ #endif //PYTHONFMU_PYTHONSTATE_HPP
@@ -1,4 +1,4 @@
1
- from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Float64
1
+ from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Float64, Fmi3Variability
2
2
 
3
3
 
4
4
  class PythonSlaveWithException(Fmi3Slave):
@@ -8,6 +8,9 @@ class PythonSlaveWithException(Fmi3Slave):
8
8
 
9
9
  self.realIn = 22.0
10
10
  self.realOut = 0.0
11
+ self.time = 0
12
+
13
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
11
14
  self.register_variable(Float64("realIn", causality=Fmi3Causality.input))
12
15
  self.register_variable(Float64("realOut", causality=Fmi3Causality.output))
13
16
 
@@ -21,6 +21,9 @@ class PythonSlave(Fmi3Slave):
21
21
  self.realIn = 2. / 3.
22
22
  self.booleanParameter = False
23
23
  self.stringParameter = "dog"
24
+ self.time = 0
25
+
26
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
24
27
  self.register_variable(
25
28
  Int32("intParam", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable))
26
29
  self.register_variable(Float64("realIn", causality=Fmi3Causality.input))
@@ -1,4 +1,4 @@
1
- from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Fmi3Variability, String, DefaultExperiment
1
+ from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Fmi3Variability, String, DefaultExperiment, Float64
2
2
 
3
3
 
4
4
  class PythonSlaveReadFile(Fmi3Slave):
@@ -11,6 +11,9 @@ class PythonSlaveReadFile(Fmi3Slave):
11
11
  with (open(f'{self.resources}/hello.txt', 'r')) as file:
12
12
  data = file.read()
13
13
 
14
+ self.time = 0
15
+
16
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
14
17
  self.register_variable(
15
18
  String("file_content", getter=lambda: data,
16
19
  causality=Fmi3Causality.output,
@@ -1,5 +1,5 @@
1
1
  import math
2
- from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Float64
2
+ from pythonfmu3.fmi3slave import Fmi3Slave, Fmi3Causality, Float64, Fmi3Variability
3
3
  from localmodule import get_amplitude, get_time_constant
4
4
 
5
5
 
@@ -10,6 +10,9 @@ class PythonSlaveWithDep(Fmi3Slave):
10
10
 
11
11
  self.realIn = 22.0
12
12
  self.realOut = 0.0
13
+ self.time = 0
14
+
15
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
13
16
  self.register_variable(Float64("realIn", causality=Fmi3Causality.input))
14
17
  self.register_variable(Float64("realOut", causality=Fmi3Causality.output))
15
18
 
@@ -38,13 +38,12 @@ def test_integration_reset(tmp_path):
38
38
 
39
39
  md = fmpy.read_model_description(str(fmu))
40
40
  unzipdir = fmpy.extract(str(fmu))
41
- model = fmpy.fmi3.FMI3Slave(guid=md.guid,
41
+ model = fmpy.fmi3.FMU3Slave(guid=md.guid,
42
42
  unzipDirectory=unzipdir,
43
43
  modelIdentifier=md.coSimulation.modelIdentifier,
44
44
  instanceName="instance"
45
45
  )
46
46
  model.instantiate()
47
- model.setupExperiment()
48
47
  model.enterInitializationMode()
49
48
  model.exitInitializationMode()
50
49
 
@@ -52,13 +51,13 @@ def test_integration_reset(tmp_path):
52
51
  vr = vars["realOut"].valueReference
53
52
  dt = 0.1
54
53
 
55
- initial_value = model.getReal([vr])[0]
54
+ initial_value = model.getFloat64([vr])[0]
56
55
  assert initial_value == pytest.approx(3.0, rel=1e-7)
57
56
  model.doStep(0.0, dt, True)
58
- read = model.getReal([vr])[0]
57
+ read = model.getFloat64([vr])[0]
59
58
  assert read == pytest.approx(dt, rel=1e-7)
60
59
  model.reset()
61
- read = model.getReal([vr])[0]
60
+ read = model.getFloat64([vr])[0]
62
61
  assert read == pytest.approx(initial_value, rel=1e-7)
63
62
 
64
63
  model.terminate()
@@ -77,13 +76,12 @@ def test_integration_get_state(tmp_path):
77
76
 
78
77
  md = fmpy.read_model_description(str(fmu))
79
78
  unzipdir = fmpy.extract(str(fmu))
80
- model = fmpy.fmi3.FMI3Slave(guid=md.guid,
79
+ model = fmpy.fmi3.FMU3Slave(guid=md.guid,
81
80
  unzipDirectory=unzipdir,
82
81
  modelIdentifier=md.coSimulation.modelIdentifier,
83
82
  instanceName="instance"
84
83
  )
85
84
  model.instantiate()
86
- model.setupExperiment()
87
85
  model.enterInitializationMode()
88
86
  model.exitInitializationMode()
89
87
 
@@ -98,15 +96,15 @@ def test_integration_get_state(tmp_path):
98
96
  t += dt
99
97
 
100
98
  step_model()
101
- state = model.getFMUstate()
102
- assert model.getReal([vr])[0] == pytest.approx(dt, rel=1e-7)
99
+ state = model.getFMUState()
100
+ assert model.getFloat64([vr])[0] == pytest.approx(dt, rel=1e-7)
103
101
  step_model()
104
- assert model.getReal([vr])[0] == pytest.approx(dt * 2, rel=1e-7)
105
- model.setFMUstate(state)
106
- assert model.getReal([vr])[0] == pytest.approx(dt, rel=1e-7)
102
+ assert model.getFloat64([vr])[0] == pytest.approx(dt * 2, rel=1e-7)
103
+ model.setFMUState(state)
104
+ assert model.getFloat64([vr])[0] == pytest.approx(dt, rel=1e-7)
107
105
  step_model()
108
- assert model.getReal([vr])[0] == pytest.approx(dt * 3, rel=1e-7)
109
- model.freeFMUstate(state)
106
+ assert model.getFloat64([vr])[0] == pytest.approx(dt * 3, rel=1e-7)
107
+ model.freeFMUState(state)
110
108
 
111
109
  model.terminate()
112
110
  model.freeInstance()
@@ -126,14 +124,13 @@ def test_integration_get_serialize_state(tmp_path):
126
124
  md = fmpy.read_model_description(fmu)
127
125
  unzip_dir = fmpy.extract(fmu)
128
126
 
129
- model = fmpy.fmi3.FMI3Slave(
127
+ model = fmpy.fmi3.FMU3Slave(
130
128
  guid=md.guid,
131
129
  unzipDirectory=unzip_dir,
132
130
  modelIdentifier=md.coSimulation.modelIdentifier,
133
131
  instanceName='instance1')
134
132
 
135
133
  model.instantiate()
136
- model.setupExperiment()
137
134
  model.enterInitializationMode()
138
135
  model.exitInitializationMode()
139
136
 
@@ -148,22 +145,22 @@ def test_integration_get_serialize_state(tmp_path):
148
145
  t += dt
149
146
 
150
147
  step_model()
151
- state = model.getFMUstate()
152
- assert model.getReal(vrs)[0] == pytest.approx(dt, rel=1e-7)
148
+ state = model.getFMUState()
149
+ assert model.getFloat64(vrs)[0] == pytest.approx(dt, rel=1e-7)
153
150
  step_model()
154
- assert model.getReal(vrs)[0] == pytest.approx(dt * 2, rel=1e-7)
155
- model.setFMUstate(state)
156
- assert model.getReal(vrs)[0] == pytest.approx(dt, rel=1e-7)
151
+ assert model.getFloat64(vrs)[0] == pytest.approx(dt * 2, rel=1e-7)
152
+ model.setFMUState(state)
153
+ assert model.getFloat64(vrs)[0] == pytest.approx(dt, rel=1e-7)
157
154
  step_model()
158
- assert model.getReal(vrs)[0] == pytest.approx(dt * 3, rel=1e-7)
155
+ assert model.getFloat64(vrs)[0] == pytest.approx(dt * 3, rel=1e-7)
159
156
 
160
- serialize_fmu_state = model.serializeFMUstate(state)
161
- model.freeFMUstate(state)
162
- de_serialize_fmu_state = model.deSerializeFMUstate(serialize_fmu_state)
163
- model.setFMUstate(de_serialize_fmu_state)
164
- assert model.getReal(vrs)[0] == pytest.approx(dt, rel=1e-7)
157
+ serialize_fmu_state = model.serializeFMUState(state)
158
+ model.freeFMUState(state)
159
+ de_serialize_fmu_state = model.deserializeFMUState(serialize_fmu_state)
160
+ model.setFMUState(de_serialize_fmu_state)
161
+ assert model.getFloat64(vrs)[0] == pytest.approx(dt, rel=1e-7)
165
162
 
166
- model.freeFMUstate(de_serialize_fmu_state)
163
+ model.freeFMUState(de_serialize_fmu_state)
167
164
 
168
165
  model.terminate()
169
166
  model.freeInstance()
@@ -178,14 +175,13 @@ def test_integration_get(tmp_path):
178
175
  md = fmpy.read_model_description(fmu)
179
176
  unzip_dir = fmpy.extract(fmu)
180
177
 
181
- model = fmpy.fmi3.FMI3Slave(
178
+ model = fmpy.fmi3.FMU3Slave(
182
179
  guid=md.guid,
183
180
  unzipDirectory=unzip_dir,
184
181
  modelIdentifier=md.coSimulation.modelIdentifier,
185
182
  instanceName='instance1')
186
183
 
187
184
  model.instantiate()
188
- model.setupExperiment()
189
185
  model.enterInitializationMode()
190
186
  model.exitInitializationMode()
191
187
 
@@ -207,14 +203,14 @@ def test_integration_get(tmp_path):
207
203
  for key, value in to_test.items():
208
204
  var = variables[key]
209
205
  vrs = [var.valueReference]
210
- if var.type == "Integer":
211
- model_value = model.getInteger(vrs)[0]
212
- elif var.type == "Real":
213
- model_value = model.getReal(vrs)[0]
206
+ if var.type == "Int32":
207
+ model_value = model.getInt32(vrs)[0]
208
+ elif var.type == "Float64":
209
+ model_value = model.getFloat64(vrs)[0]
214
210
  elif var.type == "Boolean":
215
211
  model_value = model.getBoolean(vrs)[0]
216
212
  elif var.type == "String":
217
- model_value = model.getString(vrs)[0].decode("UTF-8")
213
+ model_value = model.getString(vrs)[0]
218
214
  else:
219
215
  pytest.xfail("Unsupported type")
220
216
 
@@ -234,20 +230,19 @@ def test_integration_read_from_file(tmp_path):
234
230
  md = fmpy.read_model_description(fmu)
235
231
  unzip_dir = fmpy.extract(fmu)
236
232
 
237
- model = fmpy.fmi3.FMI3Slave(
233
+ model = fmpy.fmi3.FMU3Slave(
238
234
  guid=md.guid,
239
235
  unzipDirectory=unzip_dir,
240
236
  modelIdentifier=md.coSimulation.modelIdentifier,
241
237
  instanceName='instance1')
242
238
 
243
239
  model.instantiate()
244
- model.setupExperiment()
245
240
  model.enterInitializationMode()
246
241
  model.exitInitializationMode()
247
242
 
248
243
  variables = mapped(md)
249
244
  var = variables["file_content"]
250
- model_value = model.getString([var.valueReference])[0].decode("UTF-8")
245
+ model_value = model.getString([var.valueReference])[0]
251
246
 
252
247
  with (open(project_file, 'r')) as file:
253
248
  data = file.read()
@@ -267,14 +262,13 @@ def test_integration_set(tmp_path):
267
262
  md = fmpy.read_model_description(fmu)
268
263
  unzip_dir = fmpy.extract(fmu)
269
264
 
270
- model = fmpy.fmi3.FMI3Slave(
265
+ model = fmpy.fmi3.FMU3Slave(
271
266
  guid=md.guid,
272
267
  unzipDirectory=unzip_dir,
273
268
  modelIdentifier=md.coSimulation.modelIdentifier,
274
269
  instanceName='instance1')
275
270
 
276
271
  model.instantiate()
277
- model.setupExperiment()
278
272
  model.enterInitializationMode()
279
273
  model.exitInitializationMode()
280
274
 
@@ -292,18 +286,18 @@ def test_integration_set(tmp_path):
292
286
  for key, value in to_test.items():
293
287
  var = variables[key]
294
288
  vrs = [var.valueReference]
295
- if var.type == "Integer":
296
- model.setInteger(vrs, [value])
297
- model_value = model.getInteger(vrs)[0]
298
- elif var.type == "Real":
299
- model.setReal(vrs, [value])
300
- model_value = model.getReal(vrs)[0]
289
+ if var.type == "Int32":
290
+ model.setInt32(vrs, [value])
291
+ model_value = model.getInt32(vrs)[0]
292
+ elif var.type == "Float64":
293
+ model.setFloat64(vrs, [value])
294
+ model_value = model.getFloat64(vrs)[0]
301
295
  elif var.type == "Boolean":
302
296
  model.setBoolean(vrs, [value])
303
297
  model_value = model.getBoolean(vrs)[0]
304
298
  elif var.type == "String":
305
299
  model.setString(vrs, [value])
306
- model_value = model.getString(vrs)[0].decode("UTF-8")
300
+ model_value = model.getString(vrs)[0]
307
301
  else:
308
302
  pytest.xfail("Unsupported type")
309
303
 
@@ -22,7 +22,7 @@ def mapped(md):
22
22
  @pytest.mark.integration
23
23
  def test_integration_multiple_fmus(tmp_path):
24
24
  slave1_code = """import math
25
- from pythonfmu3.Fmi3Slave import Fmi3Slave, Fmi3Causality, Int32, Float64, Boolean, String
25
+ from pythonfmu3 import Fmi3Slave, Fmi3Causality, Fmi3Variability, Int32, Float64, Boolean, String
26
26
 
27
27
 
28
28
  class Slave1(Fmi3Slave):
@@ -32,6 +32,8 @@ class Slave1(Fmi3Slave):
32
32
 
33
33
  self.realIn = 22.0
34
34
  self.realOut = 0.0
35
+ self.time = 0
36
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
35
37
  self.register_variable(Float64("realIn", causality=Fmi3Causality.input))
36
38
  self.register_variable(Float64("realOut", causality=Fmi3Causality.output))
37
39
 
@@ -41,7 +43,7 @@ class Slave1(Fmi3Slave):
41
43
  return True
42
44
  """
43
45
 
44
- slave2_code = """from pythonfmu3.Fmi3Slave import Fmi3Slave, Fmi3Causality, Int32, Float64, Boolean, String
46
+ slave2_code = """from pythonfmu3 import Fmi3Slave, Fmi3Causality, Fmi3Variability, Int32, Float64, Boolean, String
45
47
 
46
48
 
47
49
  class Slave2(Fmi3Slave):
@@ -51,6 +53,8 @@ class Slave2(Fmi3Slave):
51
53
 
52
54
  self.realIn = 22.0
53
55
  self.realOut = 0.0
56
+ self.time = 0
57
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous))
54
58
  self.register_variable(Float64("realIn", causality=Fmi3Causality.input))
55
59
  self.register_variable(Float64("realOut", causality=Fmi3Causality.output))
56
60
 
@@ -84,21 +88,20 @@ class Slave2(Fmi3Slave):
84
88
  md1 = fmpy.read_model_description(fmu1)
85
89
  unzip_dir = fmpy.extract(fmu1)
86
90
 
87
- model1 = fmpy.fmi3.FMI3Slave(
91
+ model1 = fmpy.fmi3.FMU3Slave(
88
92
  guid=md1.guid,
89
93
  unzipDirectory=unzip_dir,
90
94
  modelIdentifier=md1.coSimulation.modelIdentifier,
91
95
  instanceName='instance1')
92
96
 
93
97
  model1.instantiate()
94
- model1.setupExperiment()
95
98
  model1.enterInitializationMode()
96
99
  model1.exitInitializationMode()
97
100
 
98
101
  md2 = fmpy.read_model_description(fmu2)
99
102
  unzip_dir = fmpy.extract(fmu2)
100
103
 
101
- model2 = fmpy.fmi3.FMI3Slave(
104
+ model2 = fmpy.fmi3.FMU3Slave(
102
105
  guid=md2.guid,
103
106
  unzipDirectory=unzip_dir,
104
107
  modelIdentifier=md2.coSimulation.modelIdentifier,
@@ -111,11 +114,10 @@ class Slave2(Fmi3Slave):
111
114
  realIn = variables2["realIn"]
112
115
 
113
116
  model2.instantiate()
114
- model2.setupExperiment()
115
117
  model2.enterInitializationMode()
116
118
 
117
- value = model1.getReal([realOut.valueReference])[0]
118
- model2.setReal([realIn.valueReference], [value])
119
+ value = model1.getFloat64([realOut.valueReference])[0]
120
+ model2.setFloat64([realIn.valueReference], [value])
119
121
 
120
122
  model2.exitInitializationMode()
121
123
 
@@ -125,12 +127,12 @@ class Slave2(Fmi3Slave):
125
127
 
126
128
  while time < stop_time:
127
129
 
128
- model2.setReal([realIn.valueReference], [value])
130
+ model2.setFloat64([realIn.valueReference], [value])
129
131
 
130
132
  model1.doStep(time, step_size)
131
133
  model2.doStep(time, step_size)
132
134
 
133
- value = model1.getReal([realOut.valueReference])[0]
135
+ value = model1.getFloat64([realOut.valueReference])[0]
134
136
 
135
137
  time += step_size
136
138
 
@@ -295,7 +295,9 @@ def test_String_to_xml(name, start):
295
295
  r = String(name, start)
296
296
  xml = r.to_xml()
297
297
  if start is not None:
298
- assert xml.attrib['start'] == str(start)
298
+ start_elements = xml.findall('.//Start')
299
+ assert len(start_elements) == 1
300
+ assert start_elements[0].attrib['value'] == str(start)
299
301
 
300
302
 
301
303
  @pytest.mark.requirements("numpy")
pythonfmu3/unit.py ADDED
@@ -0,0 +1,62 @@
1
+ from xml.etree.ElementTree import Element
2
+
3
+ class BaseUnit(object):
4
+ tag = "BaseUnit"
5
+ base_units = {
6
+ "kg": 0,
7
+ "m": 0,
8
+ "s": 0,
9
+ "A": 0,
10
+ "K": 0,
11
+ "mol": 0,
12
+ "cd": 0,
13
+ "rad": 0,
14
+ }
15
+
16
+ def __init__(self, factor=1.0, offset=0.0, **kwargs) -> None:
17
+ self._factor = factor
18
+ self._offset = offset
19
+ self._units = {**BaseUnit.base_units, **kwargs}
20
+
21
+ @property
22
+ def units(self):
23
+ return {key: str(value) for key, value in self._units.items() if value != 0}
24
+
25
+ def to_xml(self):
26
+ """Convert the variable to XML node.
27
+
28
+ Returns
29
+ xml.etree.ElementTree.Element: XML node
30
+ """
31
+ attrib = dict()
32
+ if self._factor != 1.0:
33
+ attrib["factor"] = str(self._factor)
34
+ if self._offset != 0.0:
35
+ attrib["offset"] = str(self._offset)
36
+ attrib.update(self.units)
37
+ ele = Element(self.tag, attrib)
38
+ return ele
39
+
40
+
41
+ class Unit(object):
42
+ tag = "Unit"
43
+
44
+ def __init__(self, name, **kwargs):
45
+ self._name = name
46
+ self._base_unit = BaseUnit(**kwargs)
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return self._name
51
+
52
+ def to_xml(self) -> Element:
53
+ """Convert the variable to XML node.
54
+
55
+ Returns
56
+ xml.etree.ElementTree.Element: XML node
57
+ """
58
+ attrib = dict()
59
+ attrib["name"] = self._name
60
+ ele = Element(self.tag, attrib)
61
+ ele.append(self._base_unit.to_xml())
62
+ return ele
@@ -1,13 +1,20 @@
1
1
  from enum import Enum
2
- from typing import Any, Optional, List
2
+ from typing import Optional
3
3
  from xml.etree.ElementTree import Element
4
4
 
5
+ class VariableType(object):
5
6
 
6
- class TypeDefinitions(object):
7
- tag = "TypeDefinitions"
8
-
9
- def __init__(self, types: List[Any]):
10
- self.types = types
7
+ def __init__(self, tag, name, **kwargs):
8
+ self._tag = tag
9
+ self._attrs = {
10
+ "name": name,
11
+ }
12
+ self._attrs.update(**kwargs)
13
+
14
+ @property
15
+ def name(self) -> str:
16
+ """str: Variable name"""
17
+ return self._attrs["name"]
11
18
 
12
19
  def to_xml(self) -> Element:
13
20
  """Convert the variable to XML node.
@@ -16,19 +23,25 @@ class TypeDefinitions(object):
16
23
  xml.etree.ElementTree.Element: XML node
17
24
  """
18
25
  attrib = dict()
19
- ele = Element(self.tag, attrib)
20
- for definition in self.types:
21
- ele.append(definition.to_xml())
22
- return ele
26
+ for key, value in self._attrs.items():
27
+ if value is not None:
28
+ attrib[key] = str(value.name if isinstance(value, Enum) else value)
29
+ return Element(self._tag, attrib)
30
+
31
+ class Float64Type(VariableType):
32
+ tag = "Float64Type"
33
+
34
+ def __init__(self, name, **kwargs):
35
+ super().__init__(self.tag, name, **kwargs)
23
36
 
24
37
 
25
38
  class Item(object):
26
39
  tag = "Item"
27
40
 
28
- def __init__(self, name: str, value: str, description: str = ""):
41
+ def __init__(self, name: str, value: int, description: str = "", **kwargs):
29
42
  self.__attrs = {
30
43
  "name": name,
31
- "value": value,
44
+ "value": str(value),
32
45
  "description": description
33
46
  }
34
47
 
@@ -61,18 +74,21 @@ class Item(object):
61
74
  f"value={self.value}, " \
62
75
  f"description={self.description})"
63
76
 
64
-
65
- class EnumerationType(object):
77
+ class EnumerationType(VariableType):
66
78
  tag = "EnumerationType"
67
79
 
68
- def __init__(self, name: str, items = List[Item]):
69
- self.__attrs = {
70
- "name": name
71
- }
72
- self._items = items
80
+ def __init__(self, name: str, values: Optional[Enum] = None, **kwargs):
81
+ super().__init__(self.tag, name, **kwargs)
82
+ self._items = {}
83
+
84
+ for val in values:
85
+ self.add_value(name = val.name, value = val.value)
73
86
 
74
- def name(self) -> str:
75
- return self.__attrs["name"]
87
+ def add_value(self, name: str, value: int, description: str = "", **kwargs):
88
+ self._items[name] = Item(name, value, description, **kwargs)
89
+
90
+ def get_value(self, name):
91
+ return self._items[name]
76
92
 
77
93
  def to_xml(self) -> Element:
78
94
  """Convert the variable to XML node.
@@ -80,7 +96,7 @@ class EnumerationType(object):
80
96
  Returns
81
97
  xml.etree.ElementTree.Element: XML node
82
98
  """
83
- element = Element(self.tag, self.__attrs)
84
- for item in self._items:
85
- element.append(item.to_xml())
86
- return element
99
+ parent = super().to_xml()
100
+ for _, item in self._items.items():
101
+ parent.append(item.to_xml())
102
+ return parent
pythonfmu3/variables.py CHANGED
@@ -32,6 +32,7 @@ class ModelVariable(ABC):
32
32
  description: Optional[str] = None,
33
33
  initial: Optional[Fmi3Initial] = None,
34
34
  variability: Optional[Fmi3Variability] = None,
35
+ declared_type: Optional[str] = None,
35
36
  getter: Any = None,
36
37
  setter: Any = None
37
38
  ):
@@ -46,6 +47,7 @@ class ModelVariable(ABC):
46
47
  "causality": causality,
47
48
  "variability": variability,
48
49
  "initial": initial,
50
+ "declaredType": declared_type,
49
51
  # 'canHandleMultipleSetPerTimeInstant': # Only for ME
50
52
  }
51
53
  self._extras = {}
@@ -81,6 +83,17 @@ class ModelVariable(ABC):
81
83
  raise RuntimeError("Value reference already set.")
82
84
  self.__attrs["valueReference"] = value
83
85
 
86
+ @property
87
+ def declared_type(self) -> str:
88
+ """str: declared type"""
89
+ return self.__attrs["declaredType"]
90
+
91
+ @declared_type.setter
92
+ def declared_type(self, value: str):
93
+ if self.__attrs["declaredType"] is not None:
94
+ raise RuntimeError("Declared type already set.")
95
+ self.__attrs["declaredType"] = value
96
+
84
97
  @property
85
98
  def variability(self) -> Optional[Fmi3Variability]:
86
99
  """:obj:`Fmi3Variability` or None: Variable variability - None if not set"""
@@ -119,6 +132,16 @@ class ModelVariable(ABC):
119
132
  f"causality={self.causality}, " \
120
133
  f"variability={self.variability})"
121
134
 
135
+ class Start(object):
136
+ def __init__(self, startValue):
137
+ self.value = startValue
138
+
139
+ def to_xml(self) -> Element:
140
+ attrib = dict()
141
+ attrib["value"] = self.value
142
+ return Element("Start", attrib)
143
+
144
+
122
145
  class Dimension(object):
123
146
  def __init__(self, start: str = "", valueReference: str = ""):
124
147
  if start and valueReference and any((start, valueReference)):
@@ -131,7 +154,7 @@ class Dimension(object):
131
154
  if self.start:
132
155
  return self.start
133
156
  else:
134
- result = next((var for var in vars if var.value_reference == self.value_reference), None)
157
+ result = next((value for vr, value in vars.items() if vr == int(self.value_reference)), None)
135
158
  return result.getter()
136
159
 
137
160
  def to_xml(self) -> Element:
@@ -145,10 +168,11 @@ class Dimension(object):
145
168
  return ele
146
169
 
147
170
  class Float64(ModelVariable):
148
- def __init__(self, name: str, start: Optional[Any] = None, derivative: Optional[Any] = None, dimensions: List[Dimension] = [], **kwargs):
171
+ def __init__(self, name: str, start: Optional[Any] = None, derivative: Optional[Any] = None, dimensions: List[Dimension] = [], unit: Optional[str] = None, **kwargs):
149
172
  super().__init__(name, **kwargs)
150
173
  self.__attrs = {"start": start, "derivative": derivative}
151
174
  self._type = "Float64"
175
+ self._unit = unit
152
176
  if dimensions:
153
177
  check_numpy()
154
178
  self.__dimensions = dimensions
@@ -160,6 +184,14 @@ class Float64(ModelVariable):
160
184
  @start.setter
161
185
  def start(self, value: float):
162
186
  self.__attrs["start"] = value
187
+
188
+ @property
189
+ def unit(self) -> Optional[Any]:
190
+ return self._unit
191
+
192
+ @unit.setter
193
+ def unit(self, value: str):
194
+ self._unit = value
163
195
 
164
196
  @property
165
197
  def derivative(self):
@@ -169,6 +201,7 @@ class Float64(ModelVariable):
169
201
  def dimensions(self) -> List[Dimension]:
170
202
  return self.__dimensions
171
203
 
204
+
172
205
  def to_xml(self) -> Element:
173
206
  attrib = dict()
174
207
  for key, value in self.__attrs.items():
@@ -181,6 +214,10 @@ class Float64(ModelVariable):
181
214
  else:
182
215
  output = f"{value:.16g}"
183
216
  attrib[key] = output
217
+
218
+ if self.unit:
219
+ attrib["unit"] = self.unit
220
+
184
221
  self._extras = attrib
185
222
  parent = super().to_xml()
186
223
 
@@ -293,16 +330,17 @@ class Boolean(ModelVariable):
293
330
  class String(ModelVariable):
294
331
  def __init__(self, name: str, start: Optional[Any] = None, **kwargs):
295
332
  super().__init__(name, **kwargs)
296
- self.__attrs = {"start": start}
333
+ self.__attrs = dict()
297
334
  self._type = "String"
335
+ self._start = Start(start)
298
336
 
299
337
  @property
300
338
  def start(self) -> Optional[Any]:
301
- return self.__attrs["start"]
339
+ return self._start.value
302
340
 
303
341
  @start.setter
304
342
  def start(self, value: float):
305
- self.__attrs["start"] = value
343
+ self._start.value = value
306
344
 
307
345
  def to_xml(self) -> Element:
308
346
  attrib = dict()
@@ -311,6 +349,9 @@ class String(ModelVariable):
311
349
  attrib[key] = str(value)
312
350
  self._extras = attrib
313
351
  parent = super().to_xml()
352
+
353
+ if self.start is not None:
354
+ parent.append(self._start.to_xml())
314
355
 
315
356
  return parent
316
357
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: pythonfmu3
3
- Version: 0.1.10
3
+ Version: 0.1.16
4
4
  Summary: A lightweight framework that enables the packaging of Python3.x code as co-simulation FMUs.
5
5
  Home-page: https://github.com/stephensmith25/PythonFMU3
6
6
  Author: Stephen Smith
@@ -21,8 +21,8 @@ Classifier: Topic :: Scientific/Engineering
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Provides-Extra: tests
24
- Requires-Dist: pytest ; extra == 'tests'
25
- Requires-Dist: fmpy ; extra == 'tests'
24
+ Requires-Dist: pytest; extra == "tests"
25
+ Requires-Dist: fmpy; extra == "tests"
26
26
 
27
27
  # PythonFMU3
28
28
 
@@ -40,13 +40,7 @@ This project is a fork of the original PythonFMU repository available at https:/
40
40
 
41
41
  ### Support:
42
42
 
43
- We currently support a basic FMU which follows the FMI3 standard. This includes support for:
44
-
45
- - Float64, Int32, UInt64, String, Boolean variables
46
- - Step Mode
47
- - Arrays, which requires the numpy package
48
-
49
- Please take a look at the examples bouncingBall.py and linearTransform.py for a demonstration of these features.
43
+ Please take a look at the examples to see the supported features.
50
44
 
51
45
  ### Future
52
46
 
@@ -55,7 +49,6 @@ In no particular order, we plan to add support for:
55
49
  - Support more variable types from FMI3
56
50
  - Improve array support
57
51
  - Add event mode
58
- - Include FMI3 related testing
59
52
 
60
53
  ### How do I build an FMU from python code?
61
54
 
@@ -138,16 +131,18 @@ from pythonfmu3 import Fmi3Causality, Fmi3Slave, Boolean, Int32, Float64, String
138
131
 
139
132
  class PythonSlave(Fmi3Slave):
140
133
 
141
- author = "John Doe"
142
- description = "A simple description"
143
-
144
134
  def __init__(self, **kwargs):
145
135
  super().__init__(**kwargs)
146
136
 
137
+ self.author = "John Doe"
138
+ self.description = "A simple description"
139
+
140
+ self.time = 0.0
147
141
  self.intOut = 1
148
142
  self.realOut = 3.0
149
143
  self.booleanVariable = True
150
144
  self.stringVariable = "Hello World!"
145
+ self.register_variable(Float64("time", causality=Fmi3Causality.independent))
151
146
  self.register_variable(Int32("intOut", causality=Fmi3Causality.output))
152
147
  self.register_variable(Float64("realOut", causality=Fmi3Causality.output))
153
148
  self.register_variable(Boolean("booleanVariable", causality=Fmi3Causality.local))
@@ -1,52 +1,54 @@
1
- pythonfmu3/__init__.py,sha256=aTB9p2E_p6D6-KeUa1HGfzH88i_8Oin3JNApVD2D5HQ,375
1
+ pythonfmu3/__init__.py,sha256=PT1R0LpSInyVdXGJbkWMTQym6_SEU7CqUkG7OH5j1f0,396
2
2
  pythonfmu3/__main__.py,sha256=gC1T8x2d02hhqshyNQGSS70SyCk-3I3t8zPqoRo2Xd8,1543
3
- pythonfmu3/_version.py,sha256=z0zCHFTcKSR0tJ6h5qrpNmRVP21QIPP8N0p7quCnnm0,23
3
+ pythonfmu3/_version.py,sha256=yF88-8vL8keLe6gCTumymw0UoMkWkSrJnzLru4zBCLQ,23
4
4
  pythonfmu3/builder.py,sha256=YLTeLLpiFOkoDYwnJ55GEhb3ham0xxROMjMN6bvaw5Y,8784
5
5
  pythonfmu3/default_experiment.py,sha256=FJ0l0FpU7pT4Pox2RiA2y-Cb_e7DE2XlbWKySRYQaVk,293
6
6
  pythonfmu3/deploy.py,sha256=eqFWVxZUkJCLvmq1-GuMNjn2j0VCGoaXPhRWrCk2Vxg,3741
7
7
  pythonfmu3/enums.py,sha256=ZmC6vGeBTGt7HarPQ4uYV10ezPsvIxAHH1ypjNq7G3E,729
8
- pythonfmu3/fmi3slave.py,sha256=sJvODS7_adGYR4Ki4cBlmX-1LMxiK2iHrBKL0WqzDmI,15962
8
+ pythonfmu3/fmi3slave.py,sha256=7rDp_5TCQnkFLV11UF55mKEvUDXbpZPfn8sOjYCNCSw,16569
9
9
  pythonfmu3/logmsg.py,sha256=0wn11GLwq0LLWqyC2ktLRjQFWlpj07wWliZ8rUHx1gg,359
10
- pythonfmu3/osutil.py,sha256=KMAoMf_BpqFQQUy3WzHnt9jiklkS1xy0syIbj7kNH6A,656
11
- pythonfmu3/type_definitions.py,sha256=gcwe5HueMBfOuHk6pkf6TNJHKfUtqga_DNbj0o13XKk,2219
12
- pythonfmu3/variables.py,sha256=sKVOnITuirzHIMAC2f9CrKlGX8-R-dGoteKS-QCv2nk,10838
10
+ pythonfmu3/osutil.py,sha256=1qN6qudN4zCupZwc15pzp52WObkqi6jGy9a-MehNt9Q,656
11
+ pythonfmu3/unit.py,sha256=SjOaywb5lVQbAO_iLjc_7Ly3nYrmAvIW2_hAGt12UFU,1475
12
+ pythonfmu3/variable_types.py,sha256=Q0D4wlGi3i3zE5UsmGJEpNh7SV_W8mn_8p_WIkwrLQA,2881
13
+ pythonfmu3/variables.py,sha256=hwYT6AfaSQIa1egVJrOxnDi5NEj1bk_VkQd9-hsoDII,11905
13
14
  pythonfmu3/pythonfmu-export/.clang-format,sha256=ApbsHWYwilCXJ1iBux4nqOjLkaqgbGLsVrc_U3hX7WI,944
14
15
  pythonfmu3/pythonfmu-export/CMakeLists.txt,sha256=p0-4Xh_sAaOI9AVb8lsUsMxJPc6bg9b_6CF6NJt5FHE,1847
15
16
  pythonfmu3/pythonfmu-export/build_unix.sh,sha256=I5RrF2O6y0hRG_SN4ExKN_CCFYaa2l-ngKzqmBXnHmA,73
16
17
  pythonfmu3/pythonfmu-export/build_win.sh,sha256=UzmAcWqFmwBu6mvYne2Dx3UK8iPwTtP4HpjICsjUaJA,97
17
18
  pythonfmu3/pythonfmu-export/src/CMakeLists.txt,sha256=RyjGhUUDhG_HEieCdt5WMfTR1snofOqigEKc83_zvfE,1104
18
- pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp,sha256=k6TEU6J6fe6RJWxsvA4uA2W4BRQriPnkQX4GKdM-MgA,4465
19
+ pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp,sha256=cgrlZa5L-bbjZeVi0pmy4JKE5UJ2GszzuyOqTdRcBRQ,4353
19
20
  pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp,sha256=EsUIevwmCzX9hsui3i8x1crSnSOG8dEK15Zm1ZIavYQ,3843
20
21
  pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp,sha256=v4Fb8gOWk2tsUgnuQ2NBeqwc3EFQhWRxvDYHtL1C0zk,6144
21
- pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp,sha256=sTHQVUCH_dwIxRl6u1VRNkTb1MRUoVlQLGpTPh5WQdk,25505
22
+ pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp,sha256=olF5Umz35uqXVMT_v9UMvwmvOJzbRbjuq1uOfnlvtUA,25539
22
23
  pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h,sha256=Tk1WY0nAYBGiOAQ8zVbmqRAfdQttITWsLba7udhBwG4,30050
23
24
  pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h,sha256=NsHuclp20wcuUInYf-91UXZbljTeu2cPXENqNEORdgU,16262
24
25
  pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h,sha256=ZCQLJgSbRGrMsBiQ4auaYCY4J3Hck6g3t1iZp5NEguw,4251
25
- pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp,sha256=uVGq0QcTySLjgTd3_fe-csPenT89DukCPL4u_j4rDew,22610
26
- pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp,sha256=XvRaA0OvB9MRSxFUP64TvHsLEP4CSOn0WcfImegS2_8,3954
27
- pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp,sha256=PApY1VOpEJbzrnQjKB1-UoTKKCzaDbIGF-dZt6f3Uls,717
28
- pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so,sha256=-xeuSKeqjB5_96yRhInnWCPh6kBXmlOHNeRzZZfOG9M,353232
29
- pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll,sha256=itKDopGQbFf_7BJDOZSctnRsamjyx5sVslfrAWWnyFk,417280
26
+ pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp,sha256=GLiOjhKLDVEICersnqVwezwEshbxfdh_cyMxBJGFbu8,456
27
+ pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp,sha256=pzHSx3Cfq7Lt7dcvMR5na7tyOrTs0RsNglnrAcRc0cs,24333
28
+ pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp,sha256=Q8eulMAkv7YkfzCi2KSwxvYngv0I_nRbmfTJR9vT92M,4063
29
+ pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp,sha256=CjFEIrZu8b1jxQAIgfI_o36hyItW_SdQ5VSkImf7JNs,2977
30
+ pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so,sha256=w89mM0c5nt_5RT1uYWtFFrM-qd40y7vdW-cmLbuo9Cc,365120
31
+ pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll,sha256=jjd6J1oOhFfExxYkoUClk4uakAmUc00HbZParm7iFXY,430592
30
32
  pythonfmu3/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
33
  pythonfmu3/tests/conftest.py,sha256=B6XxTnBdeF_tEJ3w2DlfPKUfvlk-oULgIhQtXkgOJIc,171
32
34
  pythonfmu3/tests/test_builder.py,sha256=StR9oNmF6dtuxxKtPFlncLQN-O7n97nvHm6EFH4Qn14,6252
33
35
  pythonfmu3/tests/test_deploy.py,sha256=JCDo-O93-izxrP9Q4K0njBcfZ-gAXYx9aZ4plgctz7k,1783
34
36
  pythonfmu3/tests/test_fmi3slave.py,sha256=1uwTdfiFZNmU0yzIdC30kAuu95xsZSTMA9CGk_Kdmm4,4304
35
- pythonfmu3/tests/test_integration.py,sha256=2ewA-Mai5qbC5ujyYDLo_Em1NzGsfrgaYiugDZH53ow,10562
37
+ pythonfmu3/tests/test_integration.py,sha256=zA3iJ4rI2cKEE8mDsAmcVuwNWlJzhonV8Wxz92hzEac,10387
36
38
  pythonfmu3/tests/test_logger.py,sha256=OcHQhxUtNBPH-0zdmheC4I9OWz1yu48W8MYDshNVkf8,5765
37
- pythonfmu3/tests/test_multiple_fmus.py,sha256=NJC3aHu1PA4LlZjMmCfC3IKh-_IBu1h3QMkrKiDYFrY,3680
38
- pythonfmu3/tests/test_variables.py,sha256=ULSOUASU8PpRwDWO9MMNTv-u-fqhSqNGVJ28S2Q6M2k,9495
39
+ pythonfmu3/tests/test_multiple_fmus.py,sha256=-sthCm2YUZiRkdfvpLQNgquPBForUKQ7HGqfFBdrRas,3942
40
+ pythonfmu3/tests/test_variables.py,sha256=t4r2TBruf58uYwjkp3r-RcZS5TrsOaVT_mDuoiUTQtg,9598
39
41
  pythonfmu3/tests/utils.py,sha256=WusWSUGoBPenZL-FAKQlzaHN2aYXiznZM2fwCjoldwY,301
40
42
  pythonfmu3/tests/data/csvdemo.csv,sha256=TiTUyzHyjQNXM4eXI33oJipc7w9_PJ8aMejRmwc52Xw,256
41
43
  pythonfmu3/tests/data/hello.txt,sha256=wFNeS-K3n_2TKRMFQ2v4iTFOSj-uwF7P_Lt98xrZ5Ro,12
42
- pythonfmu3/tests/slaves/PythonSlaveWithException.py,sha256=SUSjrba1mZBT_2pOYNQi2fEv8Ns5QWDM5zYps__4NJM,479
44
+ pythonfmu3/tests/slaves/PythonSlaveWithException.py,sha256=lTTMMnD5Cu09HzA8G2masKyf-ntEMfJ0a3M8btdYVLo,644
43
45
  pythonfmu3/tests/slaves/localmodule.py,sha256=-q5Lmh2I88MG7nLTscms0n5xhHLkUDed1IuQ5wj49lo,79
44
- pythonfmu3/tests/slaves/pythonslave.py,sha256=H75vLhqshSWWvMj2NQt6a_aQhA6BGSkBVJstjjVE0AU,2024
45
- pythonfmu3/tests/slaves/pythonslave_read_file.py,sha256=NIAmyZZGNAcnqf8R5jb1yFcQxj6iSMOAc13pY0GFtco,634
46
- pythonfmu3/tests/slaves/slavewithdep.py,sha256=4yqxa0vsOTLLvtaUQFkjfU9OcwjDUtuUe-qIjnD-jrw,647
47
- pythonfmu3-0.1.10.dist-info/LICENSE,sha256=24Cw_MPouLK-G_t8Hgjm59IS9BnPoL6SKkyuZ0xesXY,1155
48
- pythonfmu3-0.1.10.dist-info/METADATA,sha256=P0EXnuAfYYGxZMM0YIYUIX_SKWp5bNYXKrCPv0gkAz0,7597
49
- pythonfmu3-0.1.10.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
50
- pythonfmu3-0.1.10.dist-info/entry_points.txt,sha256=GgkWYs0DKGHLcijMEYZGkhkHOK2RVUpjGwvl1fwyDwQ,60
51
- pythonfmu3-0.1.10.dist-info/top_level.txt,sha256=6ne7bh_cME1hepxCx1BG8N5QVhVAufrp7LLok6vq7nE,11
52
- pythonfmu3-0.1.10.dist-info/RECORD,,
46
+ pythonfmu3/tests/slaves/pythonslave.py,sha256=uzrTQmfk0QBalxNTviGMZNph_YgVmcbxPqKgX5rSvXg,2172
47
+ pythonfmu3/tests/slaves/pythonslave_read_file.py,sha256=t2sDaZSr2veD91gdqCO9W9CN-AN3TyXABXXfV9Rt0Xs,791
48
+ pythonfmu3/tests/slaves/slavewithdep.py,sha256=hHDFB0dw8wkHiGzxeAVbz9O5LR6ghsFeONgRFdiK_t0,812
49
+ pythonfmu3-0.1.16.dist-info/LICENSE,sha256=24Cw_MPouLK-G_t8Hgjm59IS9BnPoL6SKkyuZ0xesXY,1155
50
+ pythonfmu3-0.1.16.dist-info/METADATA,sha256=fNhz4isDTQ1wlef8U-x5AYctr3bPGKRSqtSC8IunMhU,7442
51
+ pythonfmu3-0.1.16.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
52
+ pythonfmu3-0.1.16.dist-info/entry_points.txt,sha256=GgkWYs0DKGHLcijMEYZGkhkHOK2RVUpjGwvl1fwyDwQ,60
53
+ pythonfmu3-0.1.16.dist-info/top_level.txt,sha256=6ne7bh_cME1hepxCx1BG8N5QVhVAufrp7LLok6vq7nE,11
54
+ pythonfmu3-0.1.16.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.1)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5