pythonfmu3 0.3.3__py3-none-any.whl → 1.0.0__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.
- pythonfmu3/_version.py +1 -1
- pythonfmu3/builder.py +96 -24
- pythonfmu3/fmi3slave.py +80 -40
- pythonfmu3/modelexchange.py +17 -2
- pythonfmu3/osutil.py +34 -9
- pythonfmu3/pythonfmu-export/CMakeLists.txt +36 -4
- pythonfmu3/pythonfmu-export/generate_shim.py +88 -0
- pythonfmu3/pythonfmu-export/src/CMakeLists.txt +41 -13
- pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +3 -0
- pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +20 -10
- pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +20 -10
- pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +16 -14
- pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +570 -570
- pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +30 -17
- pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp +612 -0
- pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp +157 -0
- pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp +29 -0
- pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-backend.so +0 -0
- pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
- pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
- pythonfmu3/tests/data/fmi3_host.c +31 -0
- pythonfmu3/tests/fmi3_ctypes_driver.py +137 -0
- pythonfmu3/tests/slaves/pythonslave_arraytypes.py +80 -0
- pythonfmu3/tests/test_builder.py +48 -2
- pythonfmu3/tests/test_docs.py +70 -61
- pythonfmu3/tests/test_integration.py +106 -0
- pythonfmu3/tests/test_non_python_host.py +75 -0
- pythonfmu3/tests/test_osutil.py +23 -0
- pythonfmu3/tests/test_variables.py +1 -1
- pythonfmu3/tests/test_wrapper_ctypes.py +61 -0
- pythonfmu3/variables.py +72 -47
- {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/METADATA +61 -25
- {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/RECORD +37 -26
- {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/WHEEL +1 -1
- {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/entry_points.txt +0 -0
- {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/top_level.txt +0 -0
pythonfmu3/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "1.0.0"
|
pythonfmu3/builder.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""Python FMU builder"""
|
|
2
2
|
import argparse
|
|
3
|
-
import importlib
|
|
3
|
+
import importlib.util
|
|
4
4
|
import itertools
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
7
|
import shutil
|
|
8
8
|
import sys
|
|
9
|
+
import sysconfig
|
|
9
10
|
import tempfile
|
|
10
11
|
import zipfile
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Iterable, Optional, Tuple, Union
|
|
13
|
+
from typing import Iterable, List, Optional, Tuple, Union
|
|
13
14
|
from xml.dom.minidom import parseString
|
|
14
15
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
15
16
|
from .osutil import get_lib_extension, get_platform
|
|
@@ -21,13 +22,49 @@ HERE = Path(__file__).parent
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
_SLAVE_CLASS_RE = re.compile(r'^class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:', re.MULTILINE)
|
|
26
|
+
|
|
27
|
+
|
|
24
28
|
def get_class_name(file_name: Path) -> str:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
"""Find the name of the first class in *file_name* that textually extends Fmi3Slave/Fmi3SlaveBase."""
|
|
30
|
+
data = Path(file_name).read_text(encoding="utf-8")
|
|
31
|
+
match = _SLAVE_CLASS_RE.search(data)
|
|
32
|
+
if match is None:
|
|
33
|
+
raise ValueError(f"Unable to find a class extending Fmi3Slave or Fmi3SlaveBase in {file_name!s}")
|
|
34
|
+
return match.group(1)
|
|
35
|
+
|
|
28
36
|
|
|
37
|
+
def _find_slave_class(module, filepath: Path) -> type:
|
|
38
|
+
"""Locate the FMU class in an imported module.
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
Classes defined in the module that inherit from Fmi3SlaveBase are candidates. If the
|
|
41
|
+
regex-based lookup (also used by the native wrapper as a fallback) matches one of them
|
|
42
|
+
it wins, so behaviour stays identical for existing scripts; otherwise a single
|
|
43
|
+
candidate is accepted (this handles multi-line base lists and indirect inheritance).
|
|
44
|
+
"""
|
|
45
|
+
candidates = {
|
|
46
|
+
name: obj for name, obj in vars(module).items()
|
|
47
|
+
if isinstance(obj, type)
|
|
48
|
+
and obj.__module__ == module.__name__
|
|
49
|
+
and any(base.__name__ == Fmi3SlaveBase.__name__ for base in obj.__mro__)
|
|
50
|
+
}
|
|
51
|
+
try:
|
|
52
|
+
preferred = get_class_name(filepath)
|
|
53
|
+
except ValueError:
|
|
54
|
+
preferred = None
|
|
55
|
+
if preferred in candidates:
|
|
56
|
+
return candidates[preferred]
|
|
57
|
+
if len(candidates) == 1:
|
|
58
|
+
return next(iter(candidates.values()))
|
|
59
|
+
if not candidates:
|
|
60
|
+
raise ValueError(f"No class inheriting from {Fmi3SlaveBase.__qualname__} found in {filepath!s}")
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Several FMU classes found in {filepath!s} ({', '.join(sorted(candidates))}); "
|
|
63
|
+
"keep a single Fmi3Slave subclass per script."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Element, str]:
|
|
31
68
|
"""Extract the FMU model description as XML.
|
|
32
69
|
|
|
33
70
|
Args:
|
|
@@ -35,7 +72,7 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen
|
|
|
35
72
|
module_name (str) : python module to load
|
|
36
73
|
|
|
37
74
|
Returns:
|
|
38
|
-
Tuple[str, xml.etree.TreeElement.Element] : FMU model name, model description
|
|
75
|
+
Tuple[str, xml.etree.TreeElement.Element, str] : FMU model name, model description, class name
|
|
39
76
|
"""
|
|
40
77
|
# Add current folder to handle local dependencies
|
|
41
78
|
sys.path.insert(0, str(filepath.parent))
|
|
@@ -45,8 +82,9 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen
|
|
|
45
82
|
fmu_interface = importlib.util.module_from_spec(spec)
|
|
46
83
|
spec.loader.exec_module(fmu_interface)
|
|
47
84
|
# Instantiate the interface
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
slave_class = _find_slave_class(fmu_interface, filepath)
|
|
86
|
+
class_name = slave_class.__name__
|
|
87
|
+
instance = slave_class(instance_name="dummyInstance", resources=str(filepath.parent))
|
|
50
88
|
finally:
|
|
51
89
|
sys.path.remove(str(filepath.parent)) # remove inserted temporary path
|
|
52
90
|
|
|
@@ -55,7 +93,30 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen
|
|
|
55
93
|
f"The provided class '{class_name}' does not inherit from {Fmi3SlaveBase.__qualname__}"
|
|
56
94
|
)
|
|
57
95
|
# Produce the xml
|
|
58
|
-
return instance.modelName, instance.to_xml()
|
|
96
|
+
return instance.modelName, instance.to_xml(), class_name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_fmu_entry_library(path: Path) -> bool:
|
|
100
|
+
"""True for the library exporting the FMI functions (renamed to the model identifier)."""
|
|
101
|
+
return path.stem in ("pythonfmu-export", "libpythonfmu-export")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _libpython_candidates() -> List[str]:
|
|
105
|
+
"""Paths of the shared libpython of the running interpreter, if it has one."""
|
|
106
|
+
if sys.platform == "win32":
|
|
107
|
+
return [] # the Windows library links python3.dll directly
|
|
108
|
+
libdir = sysconfig.get_config_var("LIBDIR") or ""
|
|
109
|
+
framework_prefix = sysconfig.get_config_var("PYTHONFRAMEWORKPREFIX") or ""
|
|
110
|
+
candidates = []
|
|
111
|
+
for name in (sysconfig.get_config_var("INSTSONAME"), sysconfig.get_config_var("LDLIBRARY")):
|
|
112
|
+
if not name or name.endswith(".a"):
|
|
113
|
+
continue # statically linked interpreter: nothing to point at
|
|
114
|
+
for base in (libdir, framework_prefix):
|
|
115
|
+
if base:
|
|
116
|
+
candidate = Path(base) / name
|
|
117
|
+
if candidate.is_file() and str(candidate) not in candidates:
|
|
118
|
+
candidates.append(str(candidate))
|
|
119
|
+
return candidates
|
|
59
120
|
|
|
60
121
|
|
|
61
122
|
class FmuBuilder:
|
|
@@ -64,7 +125,7 @@ class FmuBuilder:
|
|
|
64
125
|
def build_FMU(
|
|
65
126
|
script_file: FilePath,
|
|
66
127
|
dest: FilePath = ".",
|
|
67
|
-
project_files: Iterable[FilePath] =
|
|
128
|
+
project_files: Iterable[FilePath] = (),
|
|
68
129
|
documentation_folder: Optional[FilePath] = None,
|
|
69
130
|
terminals : Optional[FilePath] = None,
|
|
70
131
|
**options,
|
|
@@ -130,7 +191,7 @@ class FmuBuilder:
|
|
|
130
191
|
else:
|
|
131
192
|
shutil.copy2(file_, temp_dir)
|
|
132
193
|
|
|
133
|
-
model_identifier, xml = get_model_description(
|
|
194
|
+
model_identifier, xml, class_name = get_model_description(
|
|
134
195
|
temp_dir.absolute() / script_file.name, module_name
|
|
135
196
|
)
|
|
136
197
|
|
|
@@ -163,8 +224,20 @@ class FmuBuilder:
|
|
|
163
224
|
relative_f = f.relative_to(temp_dir)
|
|
164
225
|
zip_fmu.write(f, arcname=(resource / relative_f))
|
|
165
226
|
|
|
166
|
-
#
|
|
167
|
-
|
|
227
|
+
# Hint for the Linux/macOS shim: where the Python that built this FMU
|
|
228
|
+
# keeps its shared library (used when the importer is not Python itself).
|
|
229
|
+
libpython_hints = _libpython_candidates()
|
|
230
|
+
if libpython_hints:
|
|
231
|
+
zip_fmu.writestr(
|
|
232
|
+
str(resource.joinpath("pythonfmu_libpython.txt")),
|
|
233
|
+
"\n".join(libpython_hints) + "\n",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Add information for the Python loader: line 1 is the module, line 2 the class.
|
|
237
|
+
# Older wrappers only read the first line, so this stays backward compatible.
|
|
238
|
+
zip_fmu.writestr(
|
|
239
|
+
str(resource.joinpath("slavemodule.txt")), f"{module_name}\n{class_name}\n"
|
|
240
|
+
)
|
|
168
241
|
|
|
169
242
|
# Add FMI API wrapping Python class source
|
|
170
243
|
sources = Path("sources")
|
|
@@ -174,7 +247,7 @@ class FmuBuilder:
|
|
|
174
247
|
):
|
|
175
248
|
relative_f = f.relative_to(src)
|
|
176
249
|
if "build" in relative_f.parts:
|
|
177
|
-
continue
|
|
250
|
+
continue
|
|
178
251
|
zip_fmu.write(f, arcname=(sources / relative_f))
|
|
179
252
|
|
|
180
253
|
# Add FMI API wrapping Python class library
|
|
@@ -186,12 +259,13 @@ class FmuBuilder:
|
|
|
186
259
|
src_binaries.rglob("*.dylib"),
|
|
187
260
|
):
|
|
188
261
|
relative_f = f.relative_to(src_binaries)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
262
|
+
# Only the FMI entry-point library takes the model identifier's name;
|
|
263
|
+
# helper libraries (the Linux/macOS backend) keep their own name.
|
|
264
|
+
if _is_fmu_entry_library(f):
|
|
265
|
+
name = f"{model_identifier}{relative_f.suffix}"
|
|
266
|
+
else:
|
|
267
|
+
name = relative_f.name
|
|
268
|
+
zip_fmu.write(f, arcname=(binaries / relative_f.parent / name))
|
|
195
269
|
|
|
196
270
|
# Add the documentation folder
|
|
197
271
|
if documentation_folder is not None:
|
|
@@ -203,9 +277,7 @@ class FmuBuilder:
|
|
|
203
277
|
|
|
204
278
|
if terminals is not None:
|
|
205
279
|
terminalsFolder = Path("terminalsAndIcons")
|
|
206
|
-
|
|
207
|
-
zip_fmu.write(f, arcname=(terminalsFolder / relative_f))
|
|
208
|
-
|
|
280
|
+
zip_fmu.write(terminals, arcname=(terminalsFolder / terminals.name))
|
|
209
281
|
|
|
210
282
|
# Add the model description
|
|
211
283
|
xml_str = parseString(tostring(xml, "UTF-8"))
|
pythonfmu3/fmi3slave.py
CHANGED
|
@@ -76,7 +76,7 @@ class Fmi3SlaveBase(object):
|
|
|
76
76
|
self.type_definitions: Dict[str, VariableType] = {}
|
|
77
77
|
self.units: Dict[str, Unit] = {}
|
|
78
78
|
|
|
79
|
-
def to_xml(self, model_options: Dict[str,
|
|
79
|
+
def to_xml(self, model_options: Optional[Dict[str, Any]] = None) -> Element:
|
|
80
80
|
"""Build the XML representation of the model.
|
|
81
81
|
|
|
82
82
|
Args:
|
|
@@ -110,20 +110,24 @@ class Fmi3SlaveBase(object):
|
|
|
110
110
|
|
|
111
111
|
root = Element("fmiModelDescription", attrib)
|
|
112
112
|
|
|
113
|
+
model_options = model_options or {}
|
|
114
|
+
|
|
115
|
+
def _resolve(option: ModelOptions) -> str:
|
|
116
|
+
return str(model_options.get(option.name, option.value)).lower()
|
|
117
|
+
|
|
113
118
|
options = dict()
|
|
114
119
|
for option in FMI3_MODEL_OPTIONS_COMMON:
|
|
115
|
-
|
|
116
|
-
options[option.name] = str(value).lower()
|
|
120
|
+
options[option.name] = _resolve(option)
|
|
117
121
|
|
|
118
122
|
options["modelIdentifier"] = self.modelName
|
|
119
123
|
|
|
120
124
|
options_cs = options.copy()
|
|
121
125
|
for option in FMI3_MODEL_OPTIONS_COSIM:
|
|
122
|
-
options_cs[option.name] =
|
|
126
|
+
options_cs[option.name] = _resolve(option)
|
|
123
127
|
|
|
124
128
|
options_me = options.copy()
|
|
125
129
|
for option in FMI3_MODEL_OPTIONS_MX:
|
|
126
|
-
options_me[option.name] =
|
|
130
|
+
options_me[option.name] = _resolve(option)
|
|
127
131
|
|
|
128
132
|
# check if we have cosim mixin or model exchange mixin
|
|
129
133
|
if isinstance(self, ModelExchange):
|
|
@@ -175,16 +179,13 @@ class Fmi3SlaveBase(object):
|
|
|
175
179
|
filter(lambda v: v.causality == Fmi3Causality.output, self.vars.values())
|
|
176
180
|
)
|
|
177
181
|
|
|
178
|
-
continuous_state_derivatives =
|
|
179
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
180
|
-
)
|
|
182
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
181
183
|
|
|
182
184
|
allowed_variability = [None, Fmi3Initial.approx, Fmi3Initial.calculated]
|
|
183
185
|
initial_unknown = list(
|
|
184
186
|
filter(lambda v: (v.causality == Fmi3Causality.output and (v.initial in allowed_variability))
|
|
185
187
|
or v.causality == Fmi3Causality.calculatedParameter
|
|
186
|
-
or v in continuous_state_derivatives and v.initial in allowed_variability
|
|
187
|
-
or v.variability == Fmi3Variability.continuous and v.initial in allowed_variability and v.causality != Fmi3Causality.independent, self.vars.values())
|
|
188
|
+
or v in continuous_state_derivatives and v.initial in allowed_variability, self.vars.values())
|
|
188
189
|
)
|
|
189
190
|
|
|
190
191
|
for v in outputs:
|
|
@@ -201,6 +202,15 @@ class Fmi3SlaveBase(object):
|
|
|
201
202
|
|
|
202
203
|
return root
|
|
203
204
|
|
|
205
|
+
def _continuous_state_derivatives(self) -> List[Float64]:
|
|
206
|
+
"""Return the registered derivative variables (one per continuous state), in registration order."""
|
|
207
|
+
return [
|
|
208
|
+
v for v in self.vars.values()
|
|
209
|
+
if isinstance(v, Float64)
|
|
210
|
+
and v.variability == Fmi3Variability.continuous
|
|
211
|
+
and v.derivative is not None
|
|
212
|
+
]
|
|
213
|
+
|
|
204
214
|
def __apply_start_value(self, var: ModelVariable):
|
|
205
215
|
vrs = [var.value_reference]
|
|
206
216
|
if isinstance(var, Int32):
|
|
@@ -244,7 +254,7 @@ class Fmi3SlaveBase(object):
|
|
|
244
254
|
if var.setter is None and hasattr(owner, var.local_name) and var.variability != Fmi3Variability.constant:
|
|
245
255
|
if hasattr(var, "dimensions") and len(var.dimensions) > 0:
|
|
246
256
|
import numpy as np
|
|
247
|
-
var.setter = lambda v: setattr(owner, var.local_name, np.reshape(v,
|
|
257
|
+
var.setter = lambda v: setattr(owner, var.local_name, np.reshape(v, getattr(owner, var.local_name).shape))
|
|
248
258
|
else:
|
|
249
259
|
var.setter = lambda v: setattr(owner, var.local_name, v)
|
|
250
260
|
|
|
@@ -282,10 +292,13 @@ class Fmi3SlaveBase(object):
|
|
|
282
292
|
for vr in vrs:
|
|
283
293
|
var = self.vars[vr]
|
|
284
294
|
if isinstance(var, Int32):
|
|
285
|
-
|
|
295
|
+
if len(var.dimensions) == 0:
|
|
296
|
+
refs.append(int(var.getter()))
|
|
297
|
+
else:
|
|
298
|
+
refs.extend(map(int, var.getter()))
|
|
286
299
|
else:
|
|
287
300
|
raise TypeError(
|
|
288
|
-
f"Variable with valueReference={vr} is not of type
|
|
301
|
+
f"Variable with valueReference={vr} is not of type Int32!"
|
|
289
302
|
)
|
|
290
303
|
return refs
|
|
291
304
|
|
|
@@ -294,7 +307,10 @@ class Fmi3SlaveBase(object):
|
|
|
294
307
|
for vr in vrs:
|
|
295
308
|
var = self.vars[vr]
|
|
296
309
|
if isinstance(var, (Enumeration, Int64)):
|
|
297
|
-
|
|
310
|
+
if len(var.dimensions) == 0:
|
|
311
|
+
refs.append(int(var.getter()))
|
|
312
|
+
else:
|
|
313
|
+
refs.extend(map(int, var.getter()))
|
|
298
314
|
else:
|
|
299
315
|
raise TypeError(
|
|
300
316
|
f"Variable with valueReference={vr} is not of type Int64!"
|
|
@@ -306,11 +322,14 @@ class Fmi3SlaveBase(object):
|
|
|
306
322
|
for vr in vrs:
|
|
307
323
|
var = self.vars[vr]
|
|
308
324
|
if isinstance(var, UInt64):
|
|
309
|
-
|
|
310
|
-
|
|
325
|
+
if len(var.dimensions) == 0:
|
|
326
|
+
val = var.getter()
|
|
327
|
+
refs.append(ctypes.c_uint64(val) if not isinstance(val, ctypes.c_uint64) else val)
|
|
328
|
+
else:
|
|
329
|
+
refs.extend(map(ctypes.c_uint64, var.getter()))
|
|
311
330
|
else:
|
|
312
331
|
raise TypeError(
|
|
313
|
-
f"Variable with valueReference={vr} is not of type
|
|
332
|
+
f"Variable with valueReference={vr} is not of type Uint64!"
|
|
314
333
|
)
|
|
315
334
|
return refs
|
|
316
335
|
|
|
@@ -325,7 +344,7 @@ class Fmi3SlaveBase(object):
|
|
|
325
344
|
refs.extend(var.getter())
|
|
326
345
|
else:
|
|
327
346
|
raise TypeError(
|
|
328
|
-
f"Variable with valueReference={vr} is not of type
|
|
347
|
+
f"Variable with valueReference={vr} is not of type Float64!"
|
|
329
348
|
)
|
|
330
349
|
return refs
|
|
331
350
|
|
|
@@ -334,7 +353,11 @@ class Fmi3SlaveBase(object):
|
|
|
334
353
|
for vr in vrs:
|
|
335
354
|
var = self.vars[vr]
|
|
336
355
|
if isinstance(var, Boolean):
|
|
337
|
-
|
|
356
|
+
if len(var.dimensions) == 0:
|
|
357
|
+
refs.append(bool(var.getter()))
|
|
358
|
+
else:
|
|
359
|
+
refs.extend(var.getter())
|
|
360
|
+
|
|
338
361
|
else:
|
|
339
362
|
raise TypeError(
|
|
340
363
|
f"Variable with valueReference={vr} is not of type Boolean!"
|
|
@@ -354,30 +377,48 @@ class Fmi3SlaveBase(object):
|
|
|
354
377
|
return refs
|
|
355
378
|
|
|
356
379
|
def set_int32(self, vrs: List[int], values: List[int]):
|
|
357
|
-
|
|
380
|
+
offset = 0
|
|
381
|
+
for vr in vrs:
|
|
358
382
|
var = self.vars[vr]
|
|
359
383
|
if isinstance(var, Int32):
|
|
360
|
-
var.
|
|
384
|
+
size = var.size(self.vars)
|
|
385
|
+
if size > 1:
|
|
386
|
+
var.setter(values[offset:offset+size])
|
|
387
|
+
else:
|
|
388
|
+
var.setter(values[offset])
|
|
389
|
+
offset += size
|
|
361
390
|
else:
|
|
362
391
|
raise TypeError(
|
|
363
|
-
f"Variable with valueReference={vr} is not of type
|
|
392
|
+
f"Variable with valueReference={vr} is not of type Int32!"
|
|
364
393
|
)
|
|
365
394
|
|
|
366
395
|
def set_int64(self, vrs: List[int], values: List[int]):
|
|
367
|
-
|
|
396
|
+
offset = 0
|
|
397
|
+
for vr in vrs:
|
|
368
398
|
var = self.vars[vr]
|
|
369
399
|
if isinstance(var, (Enumeration, Int64)):
|
|
370
|
-
var.
|
|
400
|
+
size = var.size(self.vars)
|
|
401
|
+
if size > 1:
|
|
402
|
+
var.setter(values[offset:offset+size])
|
|
403
|
+
else:
|
|
404
|
+
var.setter(values[offset])
|
|
405
|
+
offset += size
|
|
371
406
|
else:
|
|
372
407
|
raise TypeError(
|
|
373
|
-
f"Variable with valueReference={vr} is not of type
|
|
408
|
+
f"Variable with valueReference={vr} is not of type Int64!"
|
|
374
409
|
)
|
|
375
410
|
|
|
376
411
|
def set_uint64(self, vrs: List[int], values: List[int]):
|
|
377
|
-
|
|
412
|
+
offset = 0
|
|
413
|
+
for vr in vrs:
|
|
378
414
|
var = self.vars[vr]
|
|
379
415
|
if isinstance(var, UInt64):
|
|
380
|
-
var.
|
|
416
|
+
size = var.size(self.vars)
|
|
417
|
+
if size > 1:
|
|
418
|
+
var.setter(values[offset:offset+size])
|
|
419
|
+
else:
|
|
420
|
+
var.setter(values[offset])
|
|
421
|
+
offset += size
|
|
381
422
|
else:
|
|
382
423
|
raise TypeError(
|
|
383
424
|
f"Variable with valueReference={vr} is not of type UInt64!"
|
|
@@ -396,14 +437,20 @@ class Fmi3SlaveBase(object):
|
|
|
396
437
|
offset += size
|
|
397
438
|
else:
|
|
398
439
|
raise TypeError(
|
|
399
|
-
f"Variable with valueReference={vr} is not of type
|
|
440
|
+
f"Variable with valueReference={vr} is not of type Float64!"
|
|
400
441
|
)
|
|
401
442
|
|
|
402
443
|
def set_boolean(self, vrs: List[int], values: List[bool]):
|
|
403
|
-
|
|
444
|
+
offset = 0
|
|
445
|
+
for vr in vrs:
|
|
404
446
|
var = self.vars[vr]
|
|
405
447
|
if isinstance(var, Boolean):
|
|
406
|
-
var.
|
|
448
|
+
size = var.size(self.vars)
|
|
449
|
+
if size > 1:
|
|
450
|
+
var.setter(values[offset:offset+size])
|
|
451
|
+
else:
|
|
452
|
+
var.setter(values[offset])
|
|
453
|
+
offset += size
|
|
407
454
|
else:
|
|
408
455
|
raise TypeError(
|
|
409
456
|
f"Variable with valueReference={vr} is not of type Boolean!"
|
|
@@ -440,9 +487,7 @@ class Fmi3SlaveBase(object):
|
|
|
440
487
|
|
|
441
488
|
def set_continuous_states(self, values: List[float]):
|
|
442
489
|
offset = 0
|
|
443
|
-
continuous_state_derivatives =
|
|
444
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
445
|
-
)
|
|
490
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
446
491
|
|
|
447
492
|
vrs = [v.derivative for v in continuous_state_derivatives]
|
|
448
493
|
|
|
@@ -456,10 +501,7 @@ class Fmi3SlaveBase(object):
|
|
|
456
501
|
offset += size
|
|
457
502
|
|
|
458
503
|
def get_continuous_states(self) -> List[float]:
|
|
459
|
-
|
|
460
|
-
continuous_state_derivatives = list(
|
|
461
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
462
|
-
)
|
|
504
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
463
505
|
|
|
464
506
|
vrs = [v.derivative for v in continuous_state_derivatives]
|
|
465
507
|
|
|
@@ -474,9 +516,7 @@ class Fmi3SlaveBase(object):
|
|
|
474
516
|
return refs
|
|
475
517
|
|
|
476
518
|
def get_number_of_continuous_states(self) -> int:
|
|
477
|
-
continuous_state_derivatives =
|
|
478
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
479
|
-
)
|
|
519
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
480
520
|
return len(continuous_state_derivatives)
|
|
481
521
|
|
|
482
522
|
def set_time(self, time: float):
|
pythonfmu3/modelexchange.py
CHANGED
|
@@ -3,13 +3,28 @@ from dataclasses import dataclass
|
|
|
3
3
|
|
|
4
4
|
@dataclass
|
|
5
5
|
class Fmi3UpdateDiscreteStatesResult:
|
|
6
|
-
|
|
6
|
+
"""Return value of `update_discrete_states`, mirroring the outputs of fmi3UpdateDiscreteStates."""
|
|
7
|
+
discreteStatesNeedUpdate: bool = False
|
|
7
8
|
terminateSimulation: bool = False
|
|
8
|
-
|
|
9
|
+
nominalContinuousStatesChanged: bool = False
|
|
9
10
|
valuesOfContinuousStatesChanged: bool = False
|
|
10
11
|
nextEventTimeDefined: bool = False
|
|
11
12
|
nextEventTime: float = 0.0
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
def _alias(field_name: str) -> property:
|
|
16
|
+
return property(
|
|
17
|
+
lambda self: getattr(self, field_name),
|
|
18
|
+
lambda self, value: setattr(self, field_name, value),
|
|
19
|
+
doc=f"Deprecated alias of `{field_name}`.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Earlier releases used these names, which the FMU wrapper never read (so the flags were
|
|
24
|
+
# silently ignored). Kept as aliases so existing models keep working - and now take effect.
|
|
25
|
+
Fmi3UpdateDiscreteStatesResult.discreteStateNeedsUpdate = _alias("discreteStatesNeedUpdate")
|
|
26
|
+
Fmi3UpdateDiscreteStatesResult.nominalsOfContinuousStatesChanged = _alias("nominalContinuousStatesChanged")
|
|
27
|
+
|
|
13
28
|
class RequireTimeMeta(ABCMeta):
|
|
14
29
|
def __init__(cls, name, bases, namespace):
|
|
15
30
|
super().__init__(name, bases, namespace)
|
pythonfmu3/osutil.py
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
|
+
import platform
|
|
1
2
|
import sys
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
# FMI 3.0 platform tuples are "<arch>-<sys>" (FMI 3.0 standard, section 2.5.1.4),
|
|
5
|
+
# e.g. x86_64-linux, aarch64-darwin, x86-windows.
|
|
6
|
+
_SYSTEMS = {"Windows": "windows", "Linux": "linux", "Darwin": "darwin"}
|
|
7
|
+
|
|
8
|
+
_ARCHITECTURES_64 = {
|
|
9
|
+
"x86_64": "x86_64", "amd64": "x86_64", "x64": "x86_64",
|
|
10
|
+
"i386": "x86_64", "i686": "x86_64", "x86": "x86_64", # 64-bit Python on x86 hardware
|
|
11
|
+
"aarch64": "aarch64", "arm64": "aarch64", "armv8l": "aarch64",
|
|
12
|
+
"ppc64": "ppc64", "ppc64le": "ppc64",
|
|
13
|
+
"riscv64": "riscv64",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_ARCHITECTURES_32 = {
|
|
17
|
+
"x86_64": "x86", "amd64": "x86", "x64": "x86", "i386": "x86", "i686": "x86", "x86": "x86",
|
|
18
|
+
"aarch64": "aarch32", "arm64": "aarch32", "armv7l": "aarch32", "armv8l": "aarch32", "arm": "aarch32",
|
|
19
|
+
"ppc": "ppc32",
|
|
20
|
+
"riscv32": "riscv32",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_64bit() -> bool:
|
|
25
|
+
# Bitness of the running interpreter, which is what the FMU binary must match
|
|
26
|
+
# (e.g. a 32-bit Python on 64-bit Windows needs x86-windows).
|
|
27
|
+
return sys.maxsize > 2 ** 32
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_fmu_arch() -> str:
|
|
31
|
+
"""FMI 3.0 platform tuple of the running interpreter, e.g. 'aarch64-darwin'."""
|
|
32
|
+
machine = platform.machine().lower()
|
|
33
|
+
table = _ARCHITECTURES_64 if _is_64bit() else _ARCHITECTURES_32
|
|
34
|
+
arch = table.get(machine, machine or "unknown")
|
|
35
|
+
system = _SYSTEMS.get(platform.system(), platform.system().lower() or "unknown")
|
|
36
|
+
return f"{arch}-{system}"
|
|
4
37
|
|
|
5
|
-
def get_fmu_arch():
|
|
6
|
-
#is_64bits = sys.maxsize > 2 ** 32
|
|
7
|
-
system = platform.system()
|
|
8
|
-
is_x86 = platform.machine() in ["i386", "AMD64", "x86_64"]
|
|
9
|
-
platforms = {"Windows": "windows", "Linux": "linux", "Darwin": "darwin"}
|
|
10
|
-
arch = "x86_64" if is_x86 else "x86"
|
|
11
|
-
return arch + "-" + platforms.get(system, "unknown")
|
|
12
38
|
|
|
13
39
|
def get_platform() -> str:
|
|
14
40
|
"""Get FMU binary platform folder name."""
|
|
@@ -19,4 +45,3 @@ def get_lib_extension() -> str:
|
|
|
19
45
|
"""Get FMU library platform extension."""
|
|
20
46
|
platforms = {"Darwin": "dylib", "Linux": "so", "Windows": "dll"}
|
|
21
47
|
return platforms.get(platform.system(), "")
|
|
22
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
cmake_minimum_required(VERSION 3.10)
|
|
2
|
-
project(pythonfmu-export VERSION 0.
|
|
2
|
+
project(pythonfmu-export VERSION 1.0.0)
|
|
3
3
|
|
|
4
4
|
# ==============================================================================
|
|
5
5
|
# Build settings
|
|
@@ -48,12 +48,44 @@ else ()
|
|
|
48
48
|
set(TARGET_PLATFORM linux)
|
|
49
49
|
endif ()
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
# FMI 3.0 platform tuple <arch>-<sys>; must match pythonfmu3/osutil.py:get_fmu_arch().
|
|
52
|
+
# The architecture is the one being *built for*: the Visual Studio -A option, or a single
|
|
53
|
+
# CMAKE_OSX_ARCHITECTURES entry on macOS, take precedence over the host processor.
|
|
54
|
+
if (CMAKE_GENERATOR_PLATFORM)
|
|
55
|
+
set(_arch "${CMAKE_GENERATOR_PLATFORM}")
|
|
56
|
+
elseif (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
|
57
|
+
list(LENGTH CMAKE_OSX_ARCHITECTURES _n_arch)
|
|
58
|
+
if (NOT _n_arch EQUAL 1)
|
|
59
|
+
message(FATAL_ERROR "Universal builds are not supported: set CMAKE_OSX_ARCHITECTURES to a single architecture")
|
|
60
|
+
endif ()
|
|
61
|
+
set(_arch "${CMAKE_OSX_ARCHITECTURES}")
|
|
62
|
+
else ()
|
|
63
|
+
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
|
|
64
|
+
endif ()
|
|
65
|
+
string(TOLOWER "${_arch}" _arch)
|
|
66
|
+
|
|
67
|
+
if (_arch MATCHES "^(aarch64|arm64|armv8)")
|
|
68
|
+
set(_fmi_arch aarch64)
|
|
69
|
+
elseif (_arch MATCHES "^arm")
|
|
70
|
+
set(_fmi_arch aarch32)
|
|
71
|
+
elseif (_arch MATCHES "^(x86_64|amd64|x64|i[3-6]86|x86|win32)$")
|
|
72
|
+
set(_fmi_arch x86)
|
|
53
73
|
else ()
|
|
54
|
-
set(
|
|
74
|
+
set(_fmi_arch "${_arch}")
|
|
55
75
|
endif ()
|
|
56
76
|
|
|
77
|
+
if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
|
|
78
|
+
if (_fmi_arch STREQUAL "x86")
|
|
79
|
+
set(_fmi_arch x86_64)
|
|
80
|
+
elseif (_fmi_arch STREQUAL "aarch32")
|
|
81
|
+
set(_fmi_arch aarch64)
|
|
82
|
+
endif ()
|
|
83
|
+
elseif (_fmi_arch STREQUAL "aarch64")
|
|
84
|
+
set(_fmi_arch aarch32)
|
|
85
|
+
endif ()
|
|
86
|
+
|
|
87
|
+
set(TARGET_PLATFORM ${_fmi_arch}-${TARGET_PLATFORM})
|
|
88
|
+
|
|
57
89
|
message("Building pythonfmu-export for platform ${TARGET_PLATFORM}")
|
|
58
90
|
|
|
59
91
|
add_subdirectory(src)
|