pythonfmu3 0.3.1__tar.gz → 0.3.3__tar.gz

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 (70) hide show
  1. {pythonfmu3-0.3.1/pythonfmu3.egg-info → pythonfmu3-0.3.3}/PKG-INFO +2 -10
  2. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/README.md +1 -9
  3. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/__init__.py +1 -1
  4. pythonfmu3-0.3.3/pythonfmu3/_version.py +1 -0
  5. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/builder.py +29 -7
  6. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/fmi3slave.py +33 -11
  7. pythonfmu3-0.3.3/pythonfmu3/modelexchange.py +52 -0
  8. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +1 -0
  9. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +11 -1
  10. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +85 -10
  11. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +1 -0
  12. pythonfmu3-0.3.3/pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  13. pythonfmu3-0.3.3/pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  14. pythonfmu3-0.3.3/pythonfmu3/tests/test_docs.py +98 -0
  15. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3/pythonfmu3.egg-info}/PKG-INFO +2 -10
  16. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3.egg-info/SOURCES.txt +1 -0
  17. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/setup.cfg +1 -0
  18. pythonfmu3-0.3.1/pythonfmu3/_version.py +0 -1
  19. pythonfmu3-0.3.1/pythonfmu3/modelexchange.py +0 -23
  20. pythonfmu3-0.3.1/pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  21. pythonfmu3-0.3.1/pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  22. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/LICENSE +0 -0
  23. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/MANIFEST.in +0 -0
  24. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/__main__.py +0 -0
  25. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/cosimulation.py +0 -0
  26. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/default_experiment.py +0 -0
  27. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/deploy.py +0 -0
  28. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/enums.py +0 -0
  29. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/logmsg.py +0 -0
  30. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/osutil.py +0 -0
  31. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/.clang-format +0 -0
  32. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/CMakeLists.txt +0 -0
  33. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/build_unix.sh +0 -0
  34. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/build_win.sh +0 -0
  35. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/CMakeLists.txt +0 -0
  36. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +0 -0
  37. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +0 -0
  38. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h +0 -0
  39. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h +0 -0
  40. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h +0 -0
  41. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp +0 -0
  42. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp +0 -0
  43. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/__init__.py +0 -0
  44. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/conftest.py +0 -0
  45. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/data/csvdemo.csv +0 -0
  46. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/data/hello.txt +0 -0
  47. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/PythonSlaveWithException.py +0 -0
  48. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/localmodule.py +0 -0
  49. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/pythonslave.py +0 -0
  50. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/pythonslaveMX.py +0 -0
  51. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/pythonslaveMXCS.py +0 -0
  52. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/pythonslave_read_file.py +0 -0
  53. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/slaves/slavewithdep.py +0 -0
  54. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_builder.py +0 -0
  55. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_deploy.py +0 -0
  56. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_fmi3slave.py +0 -0
  57. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_integration.py +0 -0
  58. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_logger.py +0 -0
  59. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_multiple_fmus.py +0 -0
  60. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/test_variables.py +0 -0
  61. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/tests/utils.py +0 -0
  62. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/unit.py +0 -0
  63. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/variable_types.py +0 -0
  64. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3/variables.py +0 -0
  65. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3.egg-info/dependency_links.txt +0 -0
  66. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3.egg-info/entry_points.txt +0 -0
  67. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3.egg-info/not-zip-safe +0 -0
  68. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3.egg-info/requires.txt +0 -0
  69. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/pythonfmu3.egg-info/top_level.txt +0 -0
  70. {pythonfmu3-0.3.1 → pythonfmu3-0.3.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonfmu3
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -44,15 +44,6 @@ This project is a fork of the original PythonFMU repository available at https:/
44
44
  Please take a look at the examples to see the supported features.
45
45
  We currently support both the Model exchange and Cosimulation Interfaces.
46
46
 
47
- For both interfaces event mode is not supported.
48
-
49
- We currently do not support the following methods (beyond a default, trivial implementation)
50
-
51
- - fmi3UpdateDiscreteStates
52
- - fmi3GetNumberOfEventindicators
53
- - fmi3GetNominalsOfContinuousStates
54
- - fmi3EvaluateDiscreteStates
55
-
56
47
  ### How do I build an FMU from python code?
57
48
 
58
49
  1. Install `pythonfmu3` package:
@@ -182,6 +173,7 @@ To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of
182
173
  ```python
183
174
 
184
175
  from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange
176
+ from typing import List
185
177
 
186
178
 
187
179
  class PythonSlaveMX(Fmi3SlaveBase, ModelExchange):
@@ -17,15 +17,6 @@ This project is a fork of the original PythonFMU repository available at https:/
17
17
  Please take a look at the examples to see the supported features.
18
18
  We currently support both the Model exchange and Cosimulation Interfaces.
19
19
 
20
- For both interfaces event mode is not supported.
21
-
22
- We currently do not support the following methods (beyond a default, trivial implementation)
23
-
24
- - fmi3UpdateDiscreteStates
25
- - fmi3GetNumberOfEventindicators
26
- - fmi3GetNominalsOfContinuousStates
27
- - fmi3EvaluateDiscreteStates
28
-
29
20
  ### How do I build an FMU from python code?
30
21
 
31
22
  1. Install `pythonfmu3` package:
@@ -155,6 +146,7 @@ To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of
155
146
  ```python
156
147
 
157
148
  from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange
149
+ from typing import List
158
150
 
159
151
 
160
152
  class PythonSlaveMX(Fmi3SlaveBase, ModelExchange):
@@ -1,7 +1,7 @@
1
1
  from ._version import __version__
2
2
  from .builder import FmuBuilder
3
3
  from .cosimulation import CoSimulation
4
- from .modelexchange import ModelExchange
4
+ from .modelexchange import ModelExchange, Fmi3UpdateDiscreteStatesResult
5
5
  from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability
6
6
  from .fmi3slave import Fmi3Slave, Fmi3SlaveBase, Fmi3StepResult
7
7
  from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, String, Dimension
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3"
@@ -13,7 +13,7 @@ from typing import Iterable, Optional, Tuple, Union
13
13
  from xml.dom.minidom import parseString
14
14
  from xml.etree.ElementTree import Element, SubElement, tostring
15
15
  from .osutil import get_lib_extension, get_platform
16
- from .fmi3slave import FMI3_MODEL_OPTIONS, Fmi3Slave, Fmi3SlaveBase
16
+ from .fmi3slave import FMI3_MODEL_OPTIONS_COMMON, FMI3_MODEL_OPTIONS_COSIM, FMI3_MODEL_OPTIONS_MX, Fmi3Slave, Fmi3SlaveBase
17
17
 
18
18
  FilePath = Union[str, Path]
19
19
  HERE = Path(__file__).parent
@@ -137,16 +137,20 @@ class FmuBuilder:
137
137
  dest_file = dest / f"{model_identifier}.fmu"
138
138
 
139
139
  type_node = xml.find("CoSimulation")
140
- option_names = [opt.name for opt in FMI3_MODEL_OPTIONS]
141
- if type_node:
140
+ option_names = [opt.name for opt in FMI3_MODEL_OPTIONS_COMMON]
141
+ cosim_option_names = option_names.copy()
142
+ cosim_option_names.extend(opt.name for opt in FMI3_MODEL_OPTIONS_COSIM)
143
+ if type_node is not None:
142
144
  for option, value in options.items():
143
- if option in option_names:
145
+ if option in cosim_option_names:
144
146
  type_node.set(option, str(value).lower())
145
147
 
146
148
  type_node = xml.find("ModelExchange")
147
- if type_node:
149
+ mx_option_names = option_names.copy()
150
+ mx_option_names.extend(opt.name for opt in FMI3_MODEL_OPTIONS_MX)
151
+ if type_node is not None:
148
152
  for option, value in options.items():
149
- if option in option_names:
153
+ if option in mx_option_names:
150
154
  type_node.set(option, str(value).lower())
151
155
 
152
156
  with zipfile.ZipFile(dest_file, "w") as zip_fmu:
@@ -247,7 +251,25 @@ def create_command_parser(parser: argparse.ArgumentParser):
247
251
  default=None
248
252
  )
249
253
 
250
- for option in FMI3_MODEL_OPTIONS:
254
+ for option in FMI3_MODEL_OPTIONS_COMMON:
255
+ action = "store_false" if option.value else "store_true"
256
+ parser.add_argument(
257
+ f"--{option.cli}",
258
+ dest=option.name,
259
+ help=f"If given, {option.name}={action[6:]}",
260
+ action=action
261
+ )
262
+
263
+ for option in FMI3_MODEL_OPTIONS_COSIM:
264
+ action = "store_false" if option.value else "store_true"
265
+ parser.add_argument(
266
+ f"--{option.cli}",
267
+ dest=option.name,
268
+ help=f"If given, {option.name}={action[6:]}",
269
+ action=action
270
+ )
271
+
272
+ for option in FMI3_MODEL_OPTIONS_MX:
251
273
  action = "store_false" if option.value else "store_true"
252
274
  parser.add_argument(
253
275
  f"--{option.cli}",
@@ -21,14 +21,22 @@ from .unit import Unit
21
21
 
22
22
  ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"])
23
23
 
24
- FMI3_MODEL_OPTIONS: List[ModelOptions] = [
24
+ FMI3_MODEL_OPTIONS_COMMON: List[ModelOptions] = [
25
25
  ModelOptions("needsExecutionTool", True, "no-external-tool"),
26
- ModelOptions("canHandleVariableCommunicationStepSize", True, "no-variable-step"),
27
26
  ModelOptions("canBeInstantiatedOnlyOncePerProcess", False, "only-one-per-process"),
28
27
  ModelOptions("canGetAndSetFMUState", False, "handle-state"),
29
28
  ModelOptions("canSerializeFMUState", False, "serialize-state")
30
29
  ]
31
30
 
31
+ FMI3_MODEL_OPTIONS_COSIM: List[ModelOptions] = [
32
+ ModelOptions("canHandleVariableCommunicationStepSize", True, "no-variable-step"),
33
+ ]
34
+
35
+ FMI3_MODEL_OPTIONS_MX: List[ModelOptions] = [
36
+ ModelOptions("needsCompletedIntegratorStep", False, "needs-completed-step"),
37
+ ]
38
+
39
+
32
40
  class Fmi3StepResult(NamedTuple):
33
41
  status: Fmi3Status = Fmi3Status.ok
34
42
  eventHandlingNeeded: bool = False
@@ -50,6 +58,7 @@ class Fmi3SlaveBase(object):
50
58
 
51
59
  def __init__(self, **kwargs):
52
60
  self.vars = OrderedDict()
61
+ self.event_indicators: List[int] = []
53
62
  self.instance_name = kwargs["instance_name"]
54
63
  self.resources = kwargs.get("resources", None)
55
64
  self.visible = kwargs.get("visible", False)
@@ -102,23 +111,26 @@ class Fmi3SlaveBase(object):
102
111
  root = Element("fmiModelDescription", attrib)
103
112
 
104
113
  options = dict()
105
- for option in FMI3_MODEL_OPTIONS:
114
+ for option in FMI3_MODEL_OPTIONS_COMMON:
106
115
  value = model_options.get(option.name, option.value)
107
116
  options[option.name] = str(value).lower()
117
+
108
118
  options["modelIdentifier"] = self.modelName
109
- options["canNotUseMemoryManagementFunctions"] = "true"
119
+
120
+ options_cs = options.copy()
121
+ for option in FMI3_MODEL_OPTIONS_COSIM:
122
+ options_cs[option.name] = str(value).lower()
110
123
 
111
- options_me = dict()
112
- options_me["canGetAndSetFMUState"] = "true"
113
- options_me["modelIdentifier"] = self.modelName
114
- options_me["needsCompletedIntegratorStep"] = "false"
124
+ options_me = options.copy()
125
+ for option in FMI3_MODEL_OPTIONS_MX:
126
+ options_me[option.name] = str(value).lower()
115
127
 
116
128
  # check if we have cosim mixin or model exchange mixin
117
129
  if isinstance(self, ModelExchange):
118
130
  SubElement(root, "ModelExchange", attrib=options_me)
119
131
 
120
132
  if isinstance(self, CoSimulation):
121
- SubElement(root, "CoSimulation", attrib=options)
133
+ SubElement(root, "CoSimulation", attrib=options_cs)
122
134
 
123
135
  if self.units:
124
136
  unit_defs = SubElement(root, "UnitDefinitions")
@@ -184,7 +196,8 @@ class Fmi3SlaveBase(object):
184
196
  for v in initial_unknown:
185
197
  SubElement(structure, "InitialUnknown", attrib=dict(valueReference=str(v.value_reference)))
186
198
 
187
-
199
+ for v in self.event_indicators:
200
+ SubElement(structure, "EventIndicator", attrib=dict(valueReference=str(v)))
188
201
 
189
202
  return root
190
203
 
@@ -206,7 +219,7 @@ class Fmi3SlaveBase(object):
206
219
  raise Exception(f"Unsupported type {type(var)}!")
207
220
  var.start = refs if len(getattr(var, "dimensions", [])) > 0 else refs[0]
208
221
 
209
- def register_variable(self, var: ModelVariable, nested: bool = True, var_type: Any = None):
222
+ def register_variable(self, var: ModelVariable, nested: bool = True, var_type: Any = None, has_event_indicator: bool = False):
210
223
  """Register a variable as FMU interface.
211
224
 
212
225
  Args:
@@ -238,6 +251,12 @@ class Fmi3SlaveBase(object):
238
251
  if var_type:
239
252
  self.type_definitions[var_type.name] = var_type
240
253
  var.declared_type = var_type.name
254
+
255
+ if has_event_indicator:
256
+ self.register_event_indicator(var.value_reference)
257
+
258
+ def register_event_indicator(self, vr):
259
+ self.event_indicators.append(vr)
241
260
 
242
261
  def setup_experiment(self, start_time: float):
243
262
  pass
@@ -416,6 +435,9 @@ class Fmi3SlaveBase(object):
416
435
  if v.setter is not None:
417
436
  v.setter(value)
418
437
 
438
+ def get_number_of_event_indicators(self) -> int:
439
+ return len(self.event_indicators)
440
+
419
441
  def set_continuous_states(self, values: List[float]):
420
442
  offset = 0
421
443
  continuous_state_derivatives = list(
@@ -0,0 +1,52 @@
1
+ from abc import ABC, ABCMeta, abstractmethod
2
+ from dataclasses import dataclass
3
+
4
+ @dataclass
5
+ class Fmi3UpdateDiscreteStatesResult:
6
+ discreteStateNeedsUpdate: bool = False
7
+ terminateSimulation: bool = False
8
+ nominalsOfContinuousStatesChanged: bool = False
9
+ valuesOfContinuousStatesChanged: bool = False
10
+ nextEventTimeDefined: bool = False
11
+ nextEventTime: float = 0.0
12
+
13
+ class RequireTimeMeta(ABCMeta):
14
+ def __init__(cls, name, bases, namespace):
15
+ super().__init__(name, bases, namespace)
16
+ orig_init = cls.__init__
17
+ def new_init(self, *args, **kwargs):
18
+ orig_init(self, *args, **kwargs)
19
+ if not hasattr(self, 'time'):
20
+ raise AttributeError(f"{cls.__name__} must define 'self.time' in __init__")
21
+ cls.__init__ = new_init
22
+
23
+
24
+ # model exchange mixin
25
+ class ModelExchange(ABC, metaclass=RequireTimeMeta):
26
+ """
27
+ Classes derived from ModelExchange must define a 'self.time' member variable.
28
+
29
+ Required methods to override:
30
+ - `get_continuous_state_derivatives`: Must return a list of continuous state derivatives.
31
+
32
+ Optional methods:
33
+ - `get_event_indicators`: Should return a list of event indicators.
34
+ - `update_discrete_states`: Signify converged solution at current super-dense time instant.
35
+ """
36
+
37
+ @abstractmethod
38
+ def get_continuous_state_derivatives(self):
39
+ """Return the continuous state derivatives of the model."""
40
+ pass
41
+
42
+ def get_event_indicators(self):
43
+ """Return the event indicators of the model."""
44
+ return []
45
+
46
+ def update_discrete_states(self):
47
+ """Update the discrete states of the model."""
48
+ return Fmi3UpdateDiscreteStatesResult()
49
+
50
+ def get_nominals_of_continuous_states(self, size: int) -> list[float]:
51
+ """Return the nominal values of the continuous states."""
52
+ return [1.0] * size
@@ -141,6 +141,7 @@ public:
141
141
  virtual void SetTime(cppfmu::FMIFloat64 time) = 0;
142
142
  virtual void GetNumberOfContinuousStates(std::size_t& nStates) const = 0;
143
143
  virtual void GetNumberOfEventIndicators(std::size_t& nEventIndicators) const = 0;
144
+ virtual void GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nEventIndicators) const = 0;
144
145
  virtual void UpdateDiscreteStates(
145
146
  cppfmu::FMIBoolean* discreteStatesNeedUpdate,
146
147
  cppfmu::FMIBoolean* terminateSimulation,
@@ -622,7 +622,17 @@ fmi3Status fmi3GetEventIndicators(
622
622
  fmi3Float64 eventIndicators[],
623
623
  size_t nEventIndicators)
624
624
  {
625
- return fmi3OK;
625
+ const auto component = reinterpret_cast<Component*>(c);
626
+ try {
627
+ component->slave->GetEventIndicators(eventIndicators, nEventIndicators);
628
+ return fmi3OK;
629
+ } catch (const cppfmu::FatalError& e) {
630
+ component->logger.Log(fmi3Fatal, "", e.what());
631
+ return fmi3Fatal;
632
+ } catch (const std::exception& e) {
633
+ component->logger.Log(fmi3Error, "", e.what());
634
+ return fmi3Error;
635
+ }
626
636
  }
627
637
 
628
638
  fmi3Status fmi3GetNominalsOfContinuousStates(
@@ -542,6 +542,7 @@ void PySlaveInstance::GetContinuousStates(cppfmu::FMIFloat64* continuousStates,
542
542
  PyObject* item = PyList_GetItem(f, i);
543
543
  continuousStates[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
544
544
  }
545
+ Py_DECREF(f);
545
546
  clearLogBuffer();
546
547
  });
547
548
  }
@@ -551,6 +552,7 @@ void PySlaveInstance::GetContinuousStateDerivatives(cppfmu::FMIFloat64* continuo
551
552
  py_safe_run([this, &continuousStateDerivatives, nStates](PyGILState_STATE gilState) {
552
553
  auto f = PyObject_CallMethod(pInstance_, "get_continuous_state_derivatives", nullptr);
553
554
  if (f == nullptr) {
555
+ PyErr_Print();
554
556
  handle_py_exception("[get_continuous_state_derivatives] PyObject_CallMethod", gilState);
555
557
  }
556
558
  // Assuming f is a list of floats
@@ -558,15 +560,27 @@ void PySlaveInstance::GetContinuousStateDerivatives(cppfmu::FMIFloat64* continuo
558
560
  PyObject* item = PyList_GetItem(f, i);
559
561
  continuousStateDerivatives[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
560
562
  }
563
+ Py_DECREF(f);
561
564
  clearLogBuffer();
562
565
  });
563
566
  }
564
567
 
565
568
  void PySlaveInstance::GetNominalsOfContinuousStates(cppfmu::FMIFloat64* nominalsOfContinuousStates, std::size_t nStates) const
566
569
  {
567
- for (size_t i = 0u; i < nStates; i++) {
568
- nominalsOfContinuousStates[i] = 1.0;
569
- }
570
+ py_safe_run([this, &nominalsOfContinuousStates, nStates](PyGILState_STATE gilState) {
571
+ auto f = PyObject_CallMethod(pInstance_, "get_nominals_of_continuous_states", "(i)", static_cast<int>(nStates));
572
+ // Check if f is a valid object
573
+ if (f == nullptr) {
574
+ handle_py_exception("[getNominalContinuousStates] PyObject_CallMethod", gilState);
575
+ }
576
+ // Assuming f is a list of floats
577
+ for (std::size_t i = 0; i < nStates; i++) {
578
+ PyObject* item = PyList_GetItem(f, i);
579
+ nominalsOfContinuousStates[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
580
+ }
581
+ Py_DECREF(f);
582
+ clearLogBuffer();
583
+ });
570
584
  }
571
585
 
572
586
  void PySlaveInstance::SetContinuousStates(const cppfmu::FMIFloat64* continuousStates, std::size_t nStates)
@@ -602,9 +616,36 @@ void PySlaveInstance::GetNumberOfContinuousStates(std::size_t& nStates) const
602
616
 
603
617
  void PySlaveInstance::GetNumberOfEventIndicators(std::size_t& nIndicators) const
604
618
  {
605
- nIndicators= 0u;
619
+ py_safe_run([this, &nIndicators](PyGILState_STATE gilState) {
620
+ auto f = PyObject_CallMethod(pInstance_, "get_number_of_event_indicators", nullptr);
621
+ if (f == nullptr) {
622
+ handle_py_exception("[getNumberOfEventIndicators] PyObject_CallMethod", gilState);
623
+ }
624
+ nIndicators = static_cast<std::size_t>(PyLong_AsLong(f));
625
+ Py_DECREF(f);
626
+ clearLogBuffer();
627
+ });
606
628
  }
607
629
 
630
+ void PySlaveInstance::GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nIndicators) const
631
+ {
632
+ py_safe_run([this, &eventIndicators, nIndicators](PyGILState_STATE gilState) {
633
+ auto f = PyObject_CallMethod(pInstance_, "get_event_indicators", nullptr);
634
+ if (f == nullptr) {
635
+ handle_py_exception("[getEventIndicators] PyObject_CallMethod", gilState);
636
+ }
637
+ // Assuming f is a list of floats
638
+ for (std::size_t i = 0; i < nIndicators; i++) {
639
+ PyObject* item = PyList_GetItem(f, i);
640
+ eventIndicators[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
641
+ }
642
+ Py_DECREF(f);
643
+ clearLogBuffer();
644
+ });
645
+
646
+ }
647
+
648
+
608
649
  void PySlaveInstance::SetTime(cppfmu::FMIFloat64 time)
609
650
  {
610
651
  py_safe_run([this, time](PyGILState_STATE gilState) {
@@ -625,12 +666,46 @@ void PySlaveInstance::UpdateDiscreteStates(
625
666
  cppfmu::FMIBoolean* nextEventTimeDefined,
626
667
  cppfmu::FMIFloat64* nextEventTime)
627
668
  {
628
- *discreteStatesNeedUpdate = false;
629
- *terminateSimulation = false;
630
- *nominalContinuousStatesChanged = false;
631
- *valuesOfContinuousStatesChanged = false;
632
- *nextEventTimeDefined = false;
633
- *nextEventTime = 0.0;
669
+ py_safe_run([this, discreteStatesNeedUpdate, terminateSimulation, nominalContinuousStatesChanged, valuesOfContinuousStatesChanged, nextEventTimeDefined, nextEventTime](PyGILState_STATE gilState) {
670
+ auto f = PyObject_CallMethod(pInstance_, "update_discrete_states", nullptr);
671
+ if (f == nullptr) {
672
+ handle_py_exception("[updateDiscreteStates] PyObject_CallMethod", gilState);
673
+ }
674
+
675
+ if (PyObject_HasAttrString(f, "discreteStatesNeedUpdate")) {
676
+ PyObject * pyDiscreteStatesNeedUpdate = PyObject_GetAttrString(f, "discreteStatesNeedUpdate");
677
+ *discreteStatesNeedUpdate = static_cast<bool>(PyObject_IsTrue(pyDiscreteStatesNeedUpdate));
678
+ Py_DECREF(pyDiscreteStatesNeedUpdate);
679
+ }
680
+ if (PyObject_HasAttrString(f, "terminateSimulation")) {
681
+ PyObject* pyTerminateSimulation = PyObject_GetAttrString(f, "terminateSimulation");
682
+ *terminateSimulation = static_cast<bool>(PyObject_IsTrue(pyTerminateSimulation));
683
+ Py_DECREF(pyTerminateSimulation);
684
+ }
685
+ if (PyObject_HasAttrString(f, "nominalContinuousStatesChanged")) {
686
+ PyObject* pyNominalContinuousStatesChanged = PyObject_GetAttrString(f, "nominalContinuousStatesChanged");
687
+ *nominalContinuousStatesChanged = static_cast<bool>(PyObject_IsTrue(pyNominalContinuousStatesChanged));
688
+ Py_DECREF(pyNominalContinuousStatesChanged);
689
+ }
690
+ if (PyObject_HasAttrString(f, "valuesOfContinuousStatesChanged")) {
691
+ PyObject* pyValuesOfContinuousStatesChanged = PyObject_GetAttrString(f, "valuesOfContinuousStatesChanged");
692
+ *valuesOfContinuousStatesChanged = static_cast<bool>(PyObject_IsTrue(pyValuesOfContinuousStatesChanged));
693
+ Py_DECREF(pyValuesOfContinuousStatesChanged);
694
+ }
695
+ if (PyObject_HasAttrString(f, "nextEventTimeDefined")) {
696
+ PyObject* pyNextEventTimeDefined = PyObject_GetAttrString(f, "nextEventTimeDefined");
697
+ *nextEventTimeDefined = static_cast<bool>(PyObject_IsTrue(pyNextEventTimeDefined));
698
+ Py_DECREF(pyNextEventTimeDefined);
699
+ }
700
+ if (nextEventTimeDefined && *nextEventTimeDefined) {
701
+ PyObject* pyNextEventTime = PyObject_GetAttrString(f, "nextEventTime");
702
+ *nextEventTime = PyFloat_AsDouble(pyNextEventTime);
703
+ Py_DECREF(pyNextEventTime);
704
+ }
705
+
706
+ Py_DECREF(f);
707
+ clearLogBuffer();
708
+ });
634
709
  }
635
710
 
636
711
  void PySlaveInstance::GetFMUstate(fmi3FMUState& state)
@@ -57,6 +57,7 @@ public:
57
57
  void GetNumberOfEventIndicators(std::size_t& nEventIndicators) const override;
58
58
  void GetContinuousStateDerivatives(cppfmu::FMIFloat64* derivatives, std::size_t nStates) const override;
59
59
  void GetNominalsOfContinuousStates(cppfmu::FMIFloat64* nominalContinuousStates, std::size_t nStates) const override;
60
+ void GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nEventIndicators) const override;
60
61
  void SetContinuousStates(const cppfmu::FMIFloat64* continuousStates, std::size_t nStates) override;
61
62
  void SetTime(cppfmu::FMIFloat64 time) override;
62
63
  void UpdateDiscreteStates(
@@ -0,0 +1,98 @@
1
+ import pytest
2
+ import pathlib
3
+ import re
4
+ import tempfile
5
+
6
+ def extract_python_code_from_md(md_file):
7
+ """Extract Python code blocks from markdown, excluding skip blocks"""
8
+ content = pathlib.Path(md_file).read_text()
9
+
10
+ # Find all python code blocks that aren't marked as skip
11
+ python_blocks = []
12
+
13
+ # Pattern to match python blocks but not python skip
14
+ pattern = r'```python(?!\s+skip)(?:[^\n]*)\n(.*?)\n```'
15
+ matches = re.findall(pattern, content, re.DOTALL)
16
+
17
+ for match in matches:
18
+ # Skip blocks that are wrapped in skip-test comments
19
+ if '<!-- skip-test -->' not in content.split(match)[0][-100:]:
20
+ python_blocks.append(match)
21
+
22
+ return python_blocks
23
+
24
+ @pytest.mark.doc
25
+ def test_readme():
26
+ test_dir = pathlib.Path(__file__).parent
27
+ fpath = test_dir.parent.parent / "README.md"
28
+
29
+ # extract Python code
30
+ code_blocks = extract_python_code_from_md(fpath)
31
+
32
+ namespace = {}
33
+ exec("from pythonfmu3 import *", namespace)
34
+
35
+ for i, code in enumerate(code_blocks):
36
+ try:
37
+ exec(code, namespace)
38
+ print(f"✅ Code block {i+1} executed successfully")
39
+ except Exception as e:
40
+ pytest.fail(f"Code block {i+1} failed: {e}\nCode:\n{code}")
41
+
42
+
43
+
44
+
45
+ @pytest.mark.doc
46
+ def test_cosimulation_examples():
47
+ """Test that all Python examples in usage.md actually work"""
48
+ test_dir = pathlib.Path(__file__).parent
49
+ usage_file = test_dir.parent.parent / "docs" / "usage.md"
50
+
51
+ # Extract Python code
52
+ code_blocks = extract_python_code_from_md(usage_file)
53
+
54
+ # Create a test namespace
55
+ namespace = {}
56
+ exec("from pythonfmu3 import *", namespace)
57
+
58
+ # Execute each code block
59
+ for i, code in enumerate(code_blocks):
60
+ try:
61
+ exec(code, namespace)
62
+ print(f"✅ Code block {i+1} executed successfully")
63
+ except Exception as e:
64
+ pytest.fail(f"Code block {i+1} failed: {e}\nCode:\n{code}")
65
+
66
+ assert 'LinearTransform' in namespace, "LinearTransform class not found"
67
+ assert 'PIDController' in namespace, "PIDController class not found"
68
+
69
+ # Test instantiation
70
+ lt = namespace['LinearTransform'](instance_name="test")
71
+ pc = namespace['PIDController'](instance_name="test")
72
+
73
+
74
+ @pytest.mark.doc
75
+ def test_model_exchange_examples():
76
+ """Test Model Exchange examples"""
77
+ test_dir = pathlib.Path(__file__).parent
78
+ mx_file = test_dir.parent.parent / "docs" / "usageMX.md"
79
+
80
+ code_blocks = extract_python_code_from_md(mx_file)
81
+
82
+ namespace = {}
83
+ exec("from pythonfmu3 import *", namespace)
84
+
85
+ for i, code in enumerate(code_blocks):
86
+ try:
87
+ exec(code, namespace)
88
+ except Exception as e:
89
+ pytest.fail(f"MX Code block {i+1} failed: {e}")
90
+
91
+ # Test that classes were actually created
92
+ assert 'VanDerPol' in namespace, "VanDerPol class not found"
93
+ assert 'BouncingBall' in namespace, "BouncingBall class not found"
94
+
95
+ # Test instantiation
96
+ vdp = namespace['VanDerPol'](instance_name="test")
97
+
98
+ bb = namespace['BouncingBall'](instance_name="test")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonfmu3
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -44,15 +44,6 @@ This project is a fork of the original PythonFMU repository available at https:/
44
44
  Please take a look at the examples to see the supported features.
45
45
  We currently support both the Model exchange and Cosimulation Interfaces.
46
46
 
47
- For both interfaces event mode is not supported.
48
-
49
- We currently do not support the following methods (beyond a default, trivial implementation)
50
-
51
- - fmi3UpdateDiscreteStates
52
- - fmi3GetNumberOfEventindicators
53
- - fmi3GetNominalsOfContinuousStates
54
- - fmi3EvaluateDiscreteStates
55
-
56
47
  ### How do I build an FMU from python code?
57
48
 
58
49
  1. Install `pythonfmu3` package:
@@ -182,6 +173,7 @@ To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of
182
173
  ```python
183
174
 
184
175
  from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange
176
+ from typing import List
185
177
 
186
178
 
187
179
  class PythonSlaveMX(Fmi3SlaveBase, ModelExchange):
@@ -47,6 +47,7 @@ pythonfmu3/tests/__init__.py
47
47
  pythonfmu3/tests/conftest.py
48
48
  pythonfmu3/tests/test_builder.py
49
49
  pythonfmu3/tests/test_deploy.py
50
+ pythonfmu3/tests/test_docs.py
50
51
  pythonfmu3/tests/test_fmi3slave.py
51
52
  pythonfmu3/tests/test_integration.py
52
53
  pythonfmu3/tests/test_logger.py
@@ -49,6 +49,7 @@ tests =
49
49
  [tool:pytest]
50
50
  markers =
51
51
  integration: marks integration tests
52
+ doc: marks documentation tests
52
53
 
53
54
  [egg_info]
54
55
  tag_build =
@@ -1 +0,0 @@
1
- __version__ = "0.3.1"
@@ -1,23 +0,0 @@
1
- from abc import ABC, ABCMeta, abstractmethod
2
-
3
- class RequireTimeMeta(ABCMeta):
4
- def __init__(cls, name, bases, namespace):
5
- super().__init__(name, bases, namespace)
6
- orig_init = cls.__init__
7
- def new_init(self, *args, **kwargs):
8
- orig_init(self, *args, **kwargs)
9
- if not hasattr(self, 'time'):
10
- raise AttributeError(f"{cls.__name__} must define 'self.time' in __init__")
11
- cls.__init__ = new_init
12
-
13
-
14
- # model exchange mixin
15
- class ModelExchange(ABC, metaclass=RequireTimeMeta):
16
- """
17
- Classes derived from ModelExchange must define a 'self.time' member variable.
18
- """
19
-
20
- @abstractmethod
21
- def get_continuous_state_derivatives(self):
22
- """Return the continuous state derivatives of the model."""
23
- pass
File without changes
File without changes
File without changes