pythonfmu3 0.3.4__py3-none-any.whl → 1.1.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/__main__.py +8 -0
- pythonfmu3/_version.py +1 -1
- pythonfmu3/builder.py +96 -24
- pythonfmu3/fmi3slave.py +23 -19
- 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/fmi_functions.cpp +6 -4
- pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +560 -571
- pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +20 -7
- 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/studio/__init__.py +1 -0
- pythonfmu3/studio/server.py +358 -0
- pythonfmu3/studio/static/app.css +179 -0
- pythonfmu3/studio/static/app.js +613 -0
- pythonfmu3/studio/static/index.html +141 -0
- pythonfmu3/studio/static/logo.svg +8 -0
- pythonfmu3/studio/templates/bouncing_ball.py +75 -0
- pythonfmu3/studio/templates/dahlquist.py +31 -0
- pythonfmu3/studio/templates/pid_controller.py +42 -0
- pythonfmu3/studio/templates/signal_generator.py +40 -0
- pythonfmu3/studio/templates/surrogate.py +37 -0
- pythonfmu3/studio/templates/thermostat.py +45 -0
- pythonfmu3/studio/templates/van_der_pol.py +34 -0
- pythonfmu3/studio/worker.py +519 -0
- pythonfmu3/tests/data/fmi3_host.c +31 -0
- pythonfmu3/tests/fmi3_ctypes_driver.py +137 -0
- pythonfmu3/tests/test_builder.py +48 -2
- pythonfmu3/tests/test_docs.py +70 -61
- pythonfmu3/tests/test_example_pipe_cht.py +200 -0
- pythonfmu3/tests/test_migration_doc.py +34 -0
- pythonfmu3/tests/test_non_python_host.py +75 -0
- pythonfmu3/tests/test_osutil.py +23 -0
- pythonfmu3/tests/test_studio.py +149 -0
- pythonfmu3/tests/test_variables.py +1 -1
- pythonfmu3/tests/test_wrapper_ctypes.py +61 -0
- pythonfmu3/variables.py +4 -3
- {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.1.0.dist-info}/METADATA +69 -25
- pythonfmu3-1.1.0.dist-info/RECORD +87 -0
- {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.1.0.dist-info}/WHEEL +1 -1
- pythonfmu3-0.3.4.dist-info/RECORD +0 -60
- {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.1.0.dist-info}/entry_points.txt +0 -0
- {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.1.0.dist-info}/top_level.txt +0 -0
pythonfmu3/__main__.py
CHANGED
|
@@ -43,6 +43,14 @@ def cli_main():
|
|
|
43
43
|
)
|
|
44
44
|
deploy.create_command_parser(deploy_parser)
|
|
45
45
|
|
|
46
|
+
studio_parser = subparsers.add_parser(
|
|
47
|
+
"studio",
|
|
48
|
+
description="Open PythonFMU3 Studio, a local web app to edit, check, simulate, build and inspect FMUs.",
|
|
49
|
+
help="Open the PythonFMU3 Studio web app."
|
|
50
|
+
)
|
|
51
|
+
from .studio.server import create_command_parser as create_studio_parser
|
|
52
|
+
create_studio_parser(studio_parser)
|
|
53
|
+
|
|
46
54
|
options = vars(parser.parse_args())
|
|
47
55
|
execute = options.pop("execute")
|
|
48
56
|
execute(**options)
|
pythonfmu3/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "1.1.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,9 +179,7 @@ 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(
|
|
@@ -200,6 +202,15 @@ class Fmi3SlaveBase(object):
|
|
|
200
202
|
|
|
201
203
|
return root
|
|
202
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
|
+
|
|
203
214
|
def __apply_start_value(self, var: ModelVariable):
|
|
204
215
|
vrs = [var.value_reference]
|
|
205
216
|
if isinstance(var, Int32):
|
|
@@ -243,7 +254,7 @@ class Fmi3SlaveBase(object):
|
|
|
243
254
|
if var.setter is None and hasattr(owner, var.local_name) and var.variability != Fmi3Variability.constant:
|
|
244
255
|
if hasattr(var, "dimensions") and len(var.dimensions) > 0:
|
|
245
256
|
import numpy as np
|
|
246
|
-
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))
|
|
247
258
|
else:
|
|
248
259
|
var.setter = lambda v: setattr(owner, var.local_name, v)
|
|
249
260
|
|
|
@@ -476,9 +487,7 @@ class Fmi3SlaveBase(object):
|
|
|
476
487
|
|
|
477
488
|
def set_continuous_states(self, values: List[float]):
|
|
478
489
|
offset = 0
|
|
479
|
-
continuous_state_derivatives =
|
|
480
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
481
|
-
)
|
|
490
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
482
491
|
|
|
483
492
|
vrs = [v.derivative for v in continuous_state_derivatives]
|
|
484
493
|
|
|
@@ -492,10 +501,7 @@ class Fmi3SlaveBase(object):
|
|
|
492
501
|
offset += size
|
|
493
502
|
|
|
494
503
|
def get_continuous_states(self) -> List[float]:
|
|
495
|
-
|
|
496
|
-
continuous_state_derivatives = list(
|
|
497
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
498
|
-
)
|
|
504
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
499
505
|
|
|
500
506
|
vrs = [v.derivative for v in continuous_state_derivatives]
|
|
501
507
|
|
|
@@ -510,9 +516,7 @@ class Fmi3SlaveBase(object):
|
|
|
510
516
|
return refs
|
|
511
517
|
|
|
512
518
|
def get_number_of_continuous_states(self) -> int:
|
|
513
|
-
continuous_state_derivatives =
|
|
514
|
-
filter(lambda v: v.variability == Fmi3Variability.continuous and (isinstance(v, Float64) and v.derivative is not None), self.vars.values())
|
|
515
|
-
)
|
|
519
|
+
continuous_state_derivatives = self._continuous_state_derivatives()
|
|
516
520
|
return len(continuous_state_derivatives)
|
|
517
521
|
|
|
518
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)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Generate src/shim/fmi3_forwarders.cpp from src/fmi/fmi3FunctionTypes.h.
|
|
2
|
+
|
|
3
|
+
The shim is the library the importer loads on Linux/macOS. It has no link-time
|
|
4
|
+
dependency on libpython: on instantiation it loads libpython (unless the host process
|
|
5
|
+
already provides it, e.g. when the importer is itself written in Python) and then the
|
|
6
|
+
real implementation (the "backend" library), and forwards every FMI call to it.
|
|
7
|
+
|
|
8
|
+
Re-run this script after updating the FMI headers:
|
|
9
|
+
python generate_shim.py
|
|
10
|
+
"""
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
HERE = Path(__file__).parent
|
|
15
|
+
TYPES_HEADER = HERE / "src" / "fmi" / "fmi3FunctionTypes.h"
|
|
16
|
+
OUTPUT = HERE / "src" / "shim" / "fmi3_forwarders.cpp"
|
|
17
|
+
|
|
18
|
+
TYPEDEF_RE = re.compile(r"typedef\s+([\w\s\*]+?)\s*\b(fmi3\w+)TYPE\s*\((.*?)\)\s*;", re.S)
|
|
19
|
+
INSTANTIATE = {
|
|
20
|
+
"fmi3InstantiateModelExchange",
|
|
21
|
+
"fmi3InstantiateCoSimulation",
|
|
22
|
+
"fmi3InstantiateScheduledExecution",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def param_names(params: str):
|
|
27
|
+
params = " ".join(params.split())
|
|
28
|
+
if params in ("", "void"):
|
|
29
|
+
return []
|
|
30
|
+
return [re.findall(r"\w+", p.replace("[]", ""))[-1] for p in params.split(",")]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
functions = [
|
|
35
|
+
(" ".join(ret.split()), name, " ".join(params.split()))
|
|
36
|
+
for ret, name, params in TYPEDEF_RE.findall(TYPES_HEADER.read_text())
|
|
37
|
+
]
|
|
38
|
+
forwarded = [f for f in functions if f[1] != "fmi3GetVersion"]
|
|
39
|
+
|
|
40
|
+
out = [
|
|
41
|
+
"// GENERATED by generate_shim.py from fmi/fmi3FunctionTypes.h - do not edit by hand.",
|
|
42
|
+
'#include "shim/shim_loader.hpp"',
|
|
43
|
+
"",
|
|
44
|
+
"namespace pythonfmu_shim",
|
|
45
|
+
"{",
|
|
46
|
+
"const char* const kFunctionNames[] = {",
|
|
47
|
+
*[f' "{name}",' for _, name, _ in forwarded],
|
|
48
|
+
"};",
|
|
49
|
+
f"const std::size_t kFunctionCount = {len(forwarded)};",
|
|
50
|
+
"} // namespace pythonfmu_shim",
|
|
51
|
+
"",
|
|
52
|
+
'extern "C" {',
|
|
53
|
+
"",
|
|
54
|
+
"FMI3_Export const char* fmi3GetVersion(void)",
|
|
55
|
+
"{",
|
|
56
|
+
" return fmi3Version;",
|
|
57
|
+
"}",
|
|
58
|
+
"",
|
|
59
|
+
]
|
|
60
|
+
for index, (ret, name, params) in enumerate(forwarded):
|
|
61
|
+
names = param_names(params)
|
|
62
|
+
args = ", ".join(names)
|
|
63
|
+
out.append(f"FMI3_Export {ret} {name}({params})")
|
|
64
|
+
out.append("{")
|
|
65
|
+
if name in INSTANTIATE:
|
|
66
|
+
out.append(
|
|
67
|
+
" if (!pythonfmu_shim::ensureBackendLoaded(resourcePath, instanceEnvironment, logMessage)) return nullptr;"
|
|
68
|
+
)
|
|
69
|
+
out.append(f" const auto fn = reinterpret_cast<{name}TYPE*>(pythonfmu_shim::function({index}));")
|
|
70
|
+
if ret == "void":
|
|
71
|
+
out.append(f" if (fn) fn({args});")
|
|
72
|
+
elif ret == "fmi3Status":
|
|
73
|
+
out.append(" if (!fn) return fmi3Error;")
|
|
74
|
+
out.append(f" return fn({args});")
|
|
75
|
+
else:
|
|
76
|
+
out.append(" if (!fn) return nullptr;")
|
|
77
|
+
out.append(f" return fn({args});")
|
|
78
|
+
out.append("}")
|
|
79
|
+
out.append("")
|
|
80
|
+
out.append('} // extern "C"')
|
|
81
|
+
out.append("")
|
|
82
|
+
OUTPUT.parent.mkdir(exist_ok=True)
|
|
83
|
+
OUTPUT.write_text("\n".join(out))
|
|
84
|
+
print(f"wrote {OUTPUT.relative_to(HERE)} ({len(functions)} functions)")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
set(headers
|
|
3
2
|
cppfmu/cppfmu_cs.hpp
|
|
4
3
|
cppfmu/cppfmu_common.hpp
|
|
@@ -17,29 +16,58 @@ set(sources
|
|
|
17
16
|
pythonfmu/PySlaveInstance.cpp
|
|
18
17
|
)
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
target_compile_features(pythonfmu-export PUBLIC "cxx_std_17")
|
|
19
|
+
set(BINARY_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/../resources/binaries/${TARGET_PLATFORM}")
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
if (WIN32)
|
|
22
|
+
# Windows: a single library, linked against the version-independent python3.dll.
|
|
23
|
+
set(PYTHONFMU_IMPL_TARGET pythonfmu-export)
|
|
24
|
+
else ()
|
|
25
|
+
# Linux/macOS: the Python-dependent implementation lives in a "backend" library
|
|
26
|
+
# (FMI functions renamed with a prefix). The FMU library itself (pythonfmu-export,
|
|
27
|
+
# renamed to <modelIdentifier> by the builder) is a small shim with no link-time
|
|
28
|
+
# dependency on libpython, so non-Python importers can load it. See shim/shim_loader.cpp.
|
|
29
|
+
set(PYTHONFMU_IMPL_TARGET pythonfmu-backend)
|
|
30
|
+
endif ()
|
|
31
|
+
|
|
32
|
+
add_library(${PYTHONFMU_IMPL_TARGET} ${sources} ${headers})
|
|
33
|
+
target_compile_features(${PYTHONFMU_IMPL_TARGET} PUBLIC "cxx_std_17")
|
|
34
|
+
target_include_directories(${PYTHONFMU_IMPL_TARGET}
|
|
24
35
|
PRIVATE
|
|
25
36
|
"${Python3_INCLUDE_DIRS}"
|
|
26
37
|
"${CMAKE_CURRENT_SOURCE_DIR}"
|
|
27
38
|
)
|
|
28
39
|
|
|
29
40
|
if (WIN32)
|
|
30
|
-
target_link_libraries(
|
|
31
|
-
|
|
32
|
-
set_target_properties(pythonfmu-export PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
|
|
33
|
-
endif ()
|
|
34
|
-
|
|
35
|
-
if (WIN32)
|
|
36
|
-
set_target_properties(pythonfmu-export
|
|
41
|
+
target_link_libraries(${PYTHONFMU_IMPL_TARGET} PRIVATE ${Python3_LIBRARIES})
|
|
42
|
+
set_target_properties(${PYTHONFMU_IMPL_TARGET}
|
|
37
43
|
PROPERTIES
|
|
38
|
-
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${
|
|
44
|
+
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${BINARY_OUTPUT_DIR}"
|
|
39
45
|
)
|
|
40
46
|
else ()
|
|
47
|
+
# Python symbols are resolved at load time against the process (the host Python, or
|
|
48
|
+
# the libpython the shim opened with RTLD_GLOBAL).
|
|
49
|
+
target_compile_definitions(${PYTHONFMU_IMPL_TARGET} PRIVATE FMI3_FUNCTION_PREFIX=pythonfmu_backend_)
|
|
50
|
+
if (APPLE)
|
|
51
|
+
set_target_properties(${PYTHONFMU_IMPL_TARGET} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
|
|
52
|
+
endif ()
|
|
53
|
+
set_target_properties(${PYTHONFMU_IMPL_TARGET}
|
|
54
|
+
PROPERTIES
|
|
55
|
+
LIBRARY_OUTPUT_DIRECTORY "${BINARY_OUTPUT_DIR}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
add_library(pythonfmu-export
|
|
59
|
+
shim/shim_loader.cpp
|
|
60
|
+
shim/shim_loader.hpp
|
|
61
|
+
shim/fmi3_forwarders.cpp
|
|
62
|
+
)
|
|
63
|
+
target_compile_features(pythonfmu-export PUBLIC "cxx_std_17")
|
|
64
|
+
target_include_directories(pythonfmu-export PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
|
65
|
+
target_link_libraries(pythonfmu-export PRIVATE ${CMAKE_DL_LIBS})
|
|
41
66
|
set_target_properties(pythonfmu-export
|
|
42
67
|
PROPERTIES
|
|
43
|
-
LIBRARY_OUTPUT_DIRECTORY "${
|
|
68
|
+
LIBRARY_OUTPUT_DIRECTORY "${BINARY_OUTPUT_DIR}"
|
|
69
|
+
# Only the FMI functions (marked FMI3_Export) are exported
|
|
70
|
+
CXX_VISIBILITY_PRESET hidden
|
|
71
|
+
VISIBILITY_INLINES_HIDDEN ON
|
|
44
72
|
)
|
|
45
73
|
endif ()
|
|
@@ -121,6 +121,9 @@ public:
|
|
|
121
121
|
FMIString message,
|
|
122
122
|
Args&&... args) CPPFMU_NOEXCEPT
|
|
123
123
|
{
|
|
124
|
+
if (m_fmiLogger == nullptr) {
|
|
125
|
+
return; // FMI 3.0 allows instantiating without a logger callback
|
|
126
|
+
}
|
|
124
127
|
if (m_settings->loggedCategories.empty() ||
|
|
125
128
|
detail::CanFind(m_settings->loggedCategories, category)) {
|
|
126
129
|
m_fmiLogger(
|