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.
Files changed (37) hide show
  1. pythonfmu3/_version.py +1 -1
  2. pythonfmu3/builder.py +96 -24
  3. pythonfmu3/fmi3slave.py +80 -40
  4. pythonfmu3/modelexchange.py +17 -2
  5. pythonfmu3/osutil.py +34 -9
  6. pythonfmu3/pythonfmu-export/CMakeLists.txt +36 -4
  7. pythonfmu3/pythonfmu-export/generate_shim.py +88 -0
  8. pythonfmu3/pythonfmu-export/src/CMakeLists.txt +41 -13
  9. pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +3 -0
  10. pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +20 -10
  11. pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +20 -10
  12. pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +16 -14
  13. pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +570 -570
  14. pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +30 -17
  15. pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp +612 -0
  16. pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp +157 -0
  17. pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp +29 -0
  18. pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-backend.so +0 -0
  19. pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  20. pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  21. pythonfmu3/tests/data/fmi3_host.c +31 -0
  22. pythonfmu3/tests/fmi3_ctypes_driver.py +137 -0
  23. pythonfmu3/tests/slaves/pythonslave_arraytypes.py +80 -0
  24. pythonfmu3/tests/test_builder.py +48 -2
  25. pythonfmu3/tests/test_docs.py +70 -61
  26. pythonfmu3/tests/test_integration.py +106 -0
  27. pythonfmu3/tests/test_non_python_host.py +75 -0
  28. pythonfmu3/tests/test_osutil.py +23 -0
  29. pythonfmu3/tests/test_variables.py +1 -1
  30. pythonfmu3/tests/test_wrapper_ctypes.py +61 -0
  31. pythonfmu3/variables.py +72 -47
  32. {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/METADATA +61 -25
  33. {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/RECORD +37 -26
  34. {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/WHEEL +1 -1
  35. {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/entry_points.txt +0 -0
  36. {pythonfmu3-0.3.3.dist-info → pythonfmu3-1.0.0.dist-info}/licenses/LICENSE +0 -0
  37. {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.3.3"
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
- with open(str(file_name), 'r') as file:
26
- data = file.read()
27
- return re.search(r'class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:', data).group(1)
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
- def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Element]:
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
- class_name = get_class_name(filepath)
49
- instance = getattr(fmu_interface, class_name)(instance_name="dummyInstance", resources=str(filepath.parent))
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] = set(),
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
- # Add information for the Python loader
167
- zip_fmu.writestr(str(resource.joinpath("slavemodule.txt")), module_name)
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
- arcname = (
190
- binaries
191
- / relative_f.parent
192
- / f"{model_identifier}{relative_f.suffix}"
193
- )
194
- zip_fmu.write(f, arcname=arcname)
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
- relative_f = terminals.relative_to(terminals.parent)
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, str] = dict()) -> Element:
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
- value = model_options.get(option.name, option.value)
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] = str(value).lower()
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] = str(value).lower()
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 = list(
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, newshape=getattr(owner, var.local_name).shape))
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
- refs.append(int(var.getter()))
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 Integer!"
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
- refs.append(int(var.getter()))
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
- val = var.getter()
310
- refs.append(val if isinstance(val, ctypes.c_uint64) else ctypes.c_uint64(val))
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 UInt64!"
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 Real!"
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
- refs.append(bool(var.getter()))
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
- for vr, value in zip(vrs, values):
380
+ offset = 0
381
+ for vr in vrs:
358
382
  var = self.vars[vr]
359
383
  if isinstance(var, Int32):
360
- var.setter(value)
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 Integer!"
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
- for vr, value in zip(vrs, values):
396
+ offset = 0
397
+ for vr in vrs:
368
398
  var = self.vars[vr]
369
399
  if isinstance(var, (Enumeration, Int64)):
370
- var.setter(value)
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 Integer!"
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
- for vr, value in zip(vrs, values):
412
+ offset = 0
413
+ for vr in vrs:
378
414
  var = self.vars[vr]
379
415
  if isinstance(var, UInt64):
380
- var.setter(value)
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 Real!"
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
- for vr, value in zip(vrs, values):
444
+ offset = 0
445
+ for vr in vrs:
404
446
  var = self.vars[vr]
405
447
  if isinstance(var, Boolean):
406
- var.setter(value)
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 = list(
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
- offset = 0
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 = list(
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):
@@ -3,13 +3,28 @@ from dataclasses import dataclass
3
3
 
4
4
  @dataclass
5
5
  class Fmi3UpdateDiscreteStatesResult:
6
- discreteStateNeedsUpdate: bool = False
6
+ """Return value of `update_discrete_states`, mirroring the outputs of fmi3UpdateDiscreteStates."""
7
+ discreteStatesNeedUpdate: bool = False
7
8
  terminateSimulation: bool = False
8
- nominalsOfContinuousStatesChanged: bool = False
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
- import platform
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.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
- if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
52
- set(TARGET_PLATFORM x86_64-${TARGET_PLATFORM})
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(TARGET_PLATFORM x86-${TARGET_PLATFORM})
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)