pythonfmu3 0.3.4__tar.gz → 1.0.0__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 (86) hide show
  1. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/MANIFEST.in +3 -0
  2. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/PKG-INFO +61 -25
  3. pythonfmu3-0.3.4/pythonfmu3.egg-info/PKG-INFO → pythonfmu3-1.0.0/README.md +43 -42
  4. pythonfmu3-1.0.0/pyproject.toml +70 -0
  5. pythonfmu3-1.0.0/pythonfmu3/_version.py +1 -0
  6. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/builder.py +96 -24
  7. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/fmi3slave.py +23 -19
  8. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/modelexchange.py +17 -2
  9. pythonfmu3-1.0.0/pythonfmu3/osutil.py +47 -0
  10. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/CMakeLists.txt +36 -4
  11. pythonfmu3-1.0.0/pythonfmu3/pythonfmu-export/generate_shim.py +88 -0
  12. pythonfmu3-1.0.0/pythonfmu3/pythonfmu-export/src/CMakeLists.txt +73 -0
  13. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +3 -0
  14. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +6 -4
  15. pythonfmu3-1.0.0/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +922 -0
  16. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +20 -7
  17. pythonfmu3-1.0.0/pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp +612 -0
  18. pythonfmu3-1.0.0/pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp +157 -0
  19. pythonfmu3-1.0.0/pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp +29 -0
  20. pythonfmu3-1.0.0/pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-backend.so +0 -0
  21. pythonfmu3-1.0.0/pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  22. pythonfmu3-1.0.0/pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  23. pythonfmu3-1.0.0/pythonfmu3/tests/data/fmi3_host.c +31 -0
  24. pythonfmu3-1.0.0/pythonfmu3/tests/fmi3_ctypes_driver.py +137 -0
  25. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_builder.py +48 -2
  26. pythonfmu3-1.0.0/pythonfmu3/tests/test_docs.py +107 -0
  27. pythonfmu3-1.0.0/pythonfmu3/tests/test_non_python_host.py +75 -0
  28. pythonfmu3-1.0.0/pythonfmu3/tests/test_osutil.py +23 -0
  29. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_variables.py +1 -1
  30. pythonfmu3-1.0.0/pythonfmu3/tests/test_wrapper_ctypes.py +61 -0
  31. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/variables.py +4 -3
  32. pythonfmu3-0.3.4/README.md → pythonfmu3-1.0.0/pythonfmu3.egg-info/PKG-INFO +78 -15
  33. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3.egg-info/SOURCES.txt +11 -3
  34. pythonfmu3-1.0.0/setup.cfg +4 -0
  35. pythonfmu3-0.3.4/pythonfmu3/_version.py +0 -1
  36. pythonfmu3-0.3.4/pythonfmu3/osutil.py +0 -22
  37. pythonfmu3-0.3.4/pythonfmu3/pythonfmu-export/src/CMakeLists.txt +0 -45
  38. pythonfmu3-0.3.4/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +0 -933
  39. pythonfmu3-0.3.4/pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  40. pythonfmu3-0.3.4/pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  41. pythonfmu3-0.3.4/pythonfmu3/tests/test_docs.py +0 -98
  42. pythonfmu3-0.3.4/pythonfmu3.egg-info/not-zip-safe +0 -1
  43. pythonfmu3-0.3.4/setup.cfg +0 -57
  44. pythonfmu3-0.3.4/setup.py +0 -4
  45. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/LICENSE +0 -0
  46. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/__init__.py +0 -0
  47. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/__main__.py +0 -0
  48. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/cosimulation.py +0 -0
  49. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/default_experiment.py +0 -0
  50. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/deploy.py +0 -0
  51. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/enums.py +0 -0
  52. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/logmsg.py +0 -0
  53. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/.clang-format +0 -0
  54. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/build_unix.sh +0 -0
  55. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/build_win.sh +0 -0
  56. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.cpp +0 -0
  57. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +0 -0
  58. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/fmi/fmi3FunctionTypes.h +0 -0
  59. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/fmi/fmi3Functions.h +0 -0
  60. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/fmi/fmi3PlatformTypes.h +0 -0
  61. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp +0 -0
  62. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp +0 -0
  63. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/__init__.py +0 -0
  64. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/conftest.py +0 -0
  65. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/data/csvdemo.csv +0 -0
  66. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/data/hello.txt +0 -0
  67. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/PythonSlaveWithException.py +0 -0
  68. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/localmodule.py +0 -0
  69. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/pythonslave.py +0 -0
  70. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/pythonslaveMX.py +0 -0
  71. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/pythonslaveMXCS.py +0 -0
  72. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/pythonslave_arraytypes.py +0 -0
  73. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/pythonslave_read_file.py +0 -0
  74. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/slaves/slavewithdep.py +0 -0
  75. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_deploy.py +0 -0
  76. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_fmi3slave.py +0 -0
  77. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_integration.py +0 -0
  78. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_logger.py +0 -0
  79. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/test_multiple_fmus.py +0 -0
  80. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/tests/utils.py +0 -0
  81. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/unit.py +0 -0
  82. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3/variable_types.py +0 -0
  83. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3.egg-info/dependency_links.txt +0 -0
  84. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3.egg-info/entry_points.txt +0 -0
  85. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3.egg-info/requires.txt +0 -0
  86. {pythonfmu3-0.3.4 → pythonfmu3-1.0.0}/pythonfmu3.egg-info/top_level.txt +0 -0
@@ -6,3 +6,6 @@ recursive-include pythonfmu3/tests/slaves *
6
6
 
7
7
  recursive-include pythonfmu3/resources *
8
8
  recursive-include pythonfmu3/pythonfmu-export *
9
+
10
+ prune pythonfmu3/pythonfmu-export/build
11
+ global-exclude __pycache__ *.py[cod]
@@ -1,23 +1,31 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonfmu3
3
- Version: 0.3.4
4
- Summary: A lightweight framework that enables the packaging of Python3.x code as co-simulation FMUs.
5
- Home-page: https://github.com/stephensmith25/PythonFMU3
3
+ Version: 1.0.0
4
+ Summary: A lightweight framework that enables the packaging of Python 3 code as FMI 3.0 co-simulation and model exchange FMUs.
6
5
  Author: Stephen Smith
7
- License: MIT
8
- Keywords: FMI
9
- Classifier: Development Status :: 3 - Alpha
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/StephenSmith25/PythonFMU3
8
+ Project-URL: Documentation, https://pythonfmu3.readthedocs.io/
9
+ Project-URL: Issues, https://github.com/StephenSmith25/PythonFMU3/issues
10
+ Keywords: FMI,FMI3,FMU,co-simulation,model exchange,simulation
11
+ Classifier: Development Status :: 5 - Production/Stable
10
12
  Classifier: Intended Audience :: End Users/Desktop
11
13
  Classifier: Intended Audience :: Developers
12
14
  Classifier: Intended Audience :: Science/Research
13
- Classifier: License :: OSI Approved :: MIT License
14
15
  Classifier: Natural Language :: English
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Operating System :: MacOS
15
19
  Classifier: Programming Language :: C++
16
20
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.6
18
- Classifier: Programming Language :: Python :: 3.7
19
- Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
20
27
  Classifier: Topic :: Scientific/Engineering
28
+ Requires-Python: >=3.9
21
29
  Description-Content-Type: text/markdown
22
30
  License-File: LICENSE
23
31
  Provides-Extra: tests
@@ -41,8 +49,30 @@ This project is a fork of the original PythonFMU repository available at https:/
41
49
 
42
50
  ### Support:
43
51
 
44
- Please take a look at the examples to see the supported features.
45
- We currently support both the Model exchange and Cosimulation Interfaces.
52
+ Both the Model Exchange and Co-simulation interfaces are supported, with Float64, Int32,
53
+ Int64, UInt64, Boolean, String and Enumeration variables (plus NumPy arrays of the numeric
54
+ types). See the [documentation](https://pythonfmu3.readthedocs.io/) for the full list of
55
+ supported features and current limitations, and the `examples` folder for more models.
56
+
57
+ See the [changelog](docs/changelog.md) for what changed in each release.
58
+
59
+ ### Where can I run the FMU?
60
+
61
+ In any FMI 3.0 importer: Python-based tools such as FMPy, and native tools such as
62
+ OMSimulator, `fmusim` or Simulink.
63
+
64
+ The machine running the simulation needs Python 3 with a **shared** Python library, plus
65
+ the packages your model imports (see `pythonfmu3 deploy` below). Inside a Python-based
66
+ importer the FMU uses that interpreter. Otherwise, on Linux and macOS the FMU finds and
67
+ loads `libpython` itself; if it picks the wrong one, or none, point it at the right library:
68
+
69
+ ```bash
70
+ export PYTHONFMU_PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.12.so.1.0
71
+ ```
72
+
73
+ On Windows, `python3.dll` must be on the `PATH`. See
74
+ [Running FMUs outside Python](docs/install.md#running-fmus-outside-python-linux-and-macos)
75
+ for the full lookup order.
46
76
 
47
77
  ### How do I build an FMU from python code?
48
78
 
@@ -53,35 +83,37 @@ pip install pythonfmu3
53
83
  ```
54
84
 
55
85
  2. Create a new class extending the `Fmi3Slave` class declared in the `pythonfmu3.fmi3slave` module (see below for an example).
86
+ Keep a single FMU class per script.
56
87
  3. Run `pythonfmu3 build` to create the fmu.
57
88
 
58
89
  ```
59
- usage: pythonfmu3 build [-h] -f SCRIPT_FILE [-d DEST] [--doc DOCUMENTATION_FOLDER] [--terminals TERMINALS_FILE] [--no-external-tool]
60
- [--no-variable-step] [--interpolate-inputs] [--only-one-per-process] [--handle-state]
61
- [--serialize-state] [--use-memory-management]
62
- [Project files [Project files ...]]
90
+ usage: pythonfmu3 build [-h] -f SCRIPT_FILE [-d DEST] [--doc DOCUMENTATION_FOLDER] [--terminals TERMINALS]
91
+ [--no-external-tool] [--only-one-per-process] [--handle-state] [--serialize-state]
92
+ [--no-variable-step] [--needs-completed-step]
93
+ [Project files ...]
63
94
 
64
95
  Build an FMU from a Python script.
65
96
 
66
97
  positional arguments:
67
98
  Project files Additional project files required by the Python script.
68
99
 
69
- optional arguments:
100
+ options:
70
101
  -h, --help show this help message and exit
71
102
  -f SCRIPT_FILE, --file SCRIPT_FILE
72
103
  Path to the Python script.
73
104
  -d DEST, --dest DEST Where to save the FMU.
74
105
  --doc DOCUMENTATION_FOLDER
75
106
  Documentation folder to include in the FMU.
76
- --terminals TERMINALS_FILE
107
+ --terminals TERMINALS
77
108
  Terminals file (terminalsAndIcons.xml) to include in the FMU.
78
109
  --no-external-tool If given, needsExecutionTool=false
79
- --no-variable-step If given, canHandleVariableCommunicationStepSize=false
80
- --interpolate-inputs If given, canInterpolateInputs=true
81
110
  --only-one-per-process
82
111
  If given, canBeInstantiatedOnlyOncePerProcess=true
83
- --handle-state If given, canGetAndSetFMUstate=true
84
- --serialize-state If given, canSerializeFMUstate=true
112
+ --handle-state If given, canGetAndSetFMUState=true
113
+ --serialize-state If given, canSerializeFMUState=true
114
+ --no-variable-step If given, canHandleVariableCommunicationStepSize=false
115
+ --needs-completed-step
116
+ If given, needsCompletedIntegratorStep=true
85
117
  ```
86
118
 
87
119
  ### How do I build an FMU from python code with third-party dependencies?
@@ -91,7 +123,7 @@ _PythonFMU_ does not package a full environment within the FMU.
91
123
  However, you can package a `requirements.txt` or `environment.yml` file within your FMU following these steps:
92
124
 
93
125
  1. Install _pythonfmu_ package: `pip install pythonfmu3`
94
- 2. Create a new class extending the `Fmi3Slave` class declared in the `pythonfmu3.fmi3slave` module (see below for an example).
126
+ 2. Create a new class extending the `Fmi3Slave` class declared in the `pythonfmu3.fmi3slave` module (see the examples below).
95
127
  3. Create a `requirements.txt` file (to use _pip_ manager) and/or a `environment.yml` file (to use _conda_ manager) that defines your dependencies.
96
128
  4. Run `pythonfmu3 build -f myscript.py requirements.txt` to create the fmu including the dependencies file.
97
129
 
@@ -133,6 +165,8 @@ class PythonSlave(Fmi3Slave):
133
165
  self.author = "John Doe"
134
166
  self.description = "A simple description"
135
167
 
168
+ # Create attributes before registering them: register_variable only adds a setter
169
+ # (needed for inputs and parameters) for attributes that already exist.
136
170
  self.time = 0.0
137
171
  self.intOut = 1
138
172
  self.realOut = 3.0
@@ -172,7 +206,7 @@ To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of
172
206
 
173
207
  ```python
174
208
 
175
- from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange
209
+ from pythonfmu3 import Fmi3Causality, Fmi3Initial, Fmi3SlaveBase, Fmi3Variability, Float64, ModelExchange
176
210
  from typing import List
177
211
 
178
212
 
@@ -207,7 +241,9 @@ class PythonSlaveMX(Fmi3SlaveBase, ModelExchange):
207
241
 
208
242
  ```
209
243
 
210
- Note: There is a hard requirement that we have a `self.time` member in the derived class.
244
+ Note: Model Exchange classes must define a `self.time` member. Derivative variables give the
245
+ value reference of their state with `derivative=`; value references are assigned in
246
+ registration order starting at 0 (here `time` = 0, `x0` = 1, `x1` = 2).
211
247
 
212
248
  #### Create the FMU
213
249
 
@@ -1,30 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: pythonfmu3
3
- Version: 0.3.4
4
- Summary: A lightweight framework that enables the packaging of Python3.x code as co-simulation FMUs.
5
- Home-page: https://github.com/stephensmith25/PythonFMU3
6
- Author: Stephen Smith
7
- License: MIT
8
- Keywords: FMI
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Intended Audience :: End Users/Desktop
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Intended Audience :: Science/Research
13
- Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Natural Language :: English
15
- Classifier: Programming Language :: C++
16
- Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.6
18
- Classifier: Programming Language :: Python :: 3.7
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Topic :: Scientific/Engineering
21
- Description-Content-Type: text/markdown
22
- License-File: LICENSE
23
- Provides-Extra: tests
24
- Requires-Dist: pytest; extra == "tests"
25
- Requires-Dist: fmpy; extra == "tests"
26
- Dynamic: license-file
27
-
28
1
  # PythonFMU3
29
2
 
30
3
  > A lightweight framework that enables the packaging of Python 3 code as FMUs (following FMI version 3.0).
@@ -41,8 +14,30 @@ This project is a fork of the original PythonFMU repository available at https:/
41
14
 
42
15
  ### Support:
43
16
 
44
- Please take a look at the examples to see the supported features.
45
- We currently support both the Model exchange and Cosimulation Interfaces.
17
+ Both the Model Exchange and Co-simulation interfaces are supported, with Float64, Int32,
18
+ Int64, UInt64, Boolean, String and Enumeration variables (plus NumPy arrays of the numeric
19
+ types). See the [documentation](https://pythonfmu3.readthedocs.io/) for the full list of
20
+ supported features and current limitations, and the `examples` folder for more models.
21
+
22
+ See the [changelog](docs/changelog.md) for what changed in each release.
23
+
24
+ ### Where can I run the FMU?
25
+
26
+ In any FMI 3.0 importer: Python-based tools such as FMPy, and native tools such as
27
+ OMSimulator, `fmusim` or Simulink.
28
+
29
+ The machine running the simulation needs Python 3 with a **shared** Python library, plus
30
+ the packages your model imports (see `pythonfmu3 deploy` below). Inside a Python-based
31
+ importer the FMU uses that interpreter. Otherwise, on Linux and macOS the FMU finds and
32
+ loads `libpython` itself; if it picks the wrong one, or none, point it at the right library:
33
+
34
+ ```bash
35
+ export PYTHONFMU_PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.12.so.1.0
36
+ ```
37
+
38
+ On Windows, `python3.dll` must be on the `PATH`. See
39
+ [Running FMUs outside Python](docs/install.md#running-fmus-outside-python-linux-and-macos)
40
+ for the full lookup order.
46
41
 
47
42
  ### How do I build an FMU from python code?
48
43
 
@@ -53,35 +48,37 @@ pip install pythonfmu3
53
48
  ```
54
49
 
55
50
  2. Create a new class extending the `Fmi3Slave` class declared in the `pythonfmu3.fmi3slave` module (see below for an example).
51
+ Keep a single FMU class per script.
56
52
  3. Run `pythonfmu3 build` to create the fmu.
57
53
 
58
54
  ```
59
- usage: pythonfmu3 build [-h] -f SCRIPT_FILE [-d DEST] [--doc DOCUMENTATION_FOLDER] [--terminals TERMINALS_FILE] [--no-external-tool]
60
- [--no-variable-step] [--interpolate-inputs] [--only-one-per-process] [--handle-state]
61
- [--serialize-state] [--use-memory-management]
62
- [Project files [Project files ...]]
55
+ usage: pythonfmu3 build [-h] -f SCRIPT_FILE [-d DEST] [--doc DOCUMENTATION_FOLDER] [--terminals TERMINALS]
56
+ [--no-external-tool] [--only-one-per-process] [--handle-state] [--serialize-state]
57
+ [--no-variable-step] [--needs-completed-step]
58
+ [Project files ...]
63
59
 
64
60
  Build an FMU from a Python script.
65
61
 
66
62
  positional arguments:
67
63
  Project files Additional project files required by the Python script.
68
64
 
69
- optional arguments:
65
+ options:
70
66
  -h, --help show this help message and exit
71
67
  -f SCRIPT_FILE, --file SCRIPT_FILE
72
68
  Path to the Python script.
73
69
  -d DEST, --dest DEST Where to save the FMU.
74
70
  --doc DOCUMENTATION_FOLDER
75
71
  Documentation folder to include in the FMU.
76
- --terminals TERMINALS_FILE
72
+ --terminals TERMINALS
77
73
  Terminals file (terminalsAndIcons.xml) to include in the FMU.
78
74
  --no-external-tool If given, needsExecutionTool=false
79
- --no-variable-step If given, canHandleVariableCommunicationStepSize=false
80
- --interpolate-inputs If given, canInterpolateInputs=true
81
75
  --only-one-per-process
82
76
  If given, canBeInstantiatedOnlyOncePerProcess=true
83
- --handle-state If given, canGetAndSetFMUstate=true
84
- --serialize-state If given, canSerializeFMUstate=true
77
+ --handle-state If given, canGetAndSetFMUState=true
78
+ --serialize-state If given, canSerializeFMUState=true
79
+ --no-variable-step If given, canHandleVariableCommunicationStepSize=false
80
+ --needs-completed-step
81
+ If given, needsCompletedIntegratorStep=true
85
82
  ```
86
83
 
87
84
  ### How do I build an FMU from python code with third-party dependencies?
@@ -91,7 +88,7 @@ _PythonFMU_ does not package a full environment within the FMU.
91
88
  However, you can package a `requirements.txt` or `environment.yml` file within your FMU following these steps:
92
89
 
93
90
  1. Install _pythonfmu_ package: `pip install pythonfmu3`
94
- 2. Create a new class extending the `Fmi3Slave` class declared in the `pythonfmu3.fmi3slave` module (see below for an example).
91
+ 2. Create a new class extending the `Fmi3Slave` class declared in the `pythonfmu3.fmi3slave` module (see the examples below).
95
92
  3. Create a `requirements.txt` file (to use _pip_ manager) and/or a `environment.yml` file (to use _conda_ manager) that defines your dependencies.
96
93
  4. Run `pythonfmu3 build -f myscript.py requirements.txt` to create the fmu including the dependencies file.
97
94
 
@@ -133,6 +130,8 @@ class PythonSlave(Fmi3Slave):
133
130
  self.author = "John Doe"
134
131
  self.description = "A simple description"
135
132
 
133
+ # Create attributes before registering them: register_variable only adds a setter
134
+ # (needed for inputs and parameters) for attributes that already exist.
136
135
  self.time = 0.0
137
136
  self.intOut = 1
138
137
  self.realOut = 3.0
@@ -172,7 +171,7 @@ To create a model exchange FMU you must inherit from `Fmi3SlaveBase` and one of
172
171
 
173
172
  ```python
174
173
 
175
- from pythonfmu3 import Fmi3Causality, Fmi3SlaveBase, Float64, ModelExchange
174
+ from pythonfmu3 import Fmi3Causality, Fmi3Initial, Fmi3SlaveBase, Fmi3Variability, Float64, ModelExchange
176
175
  from typing import List
177
176
 
178
177
 
@@ -207,7 +206,9 @@ class PythonSlaveMX(Fmi3SlaveBase, ModelExchange):
207
206
 
208
207
  ```
209
208
 
210
- Note: There is a hard requirement that we have a `self.time` member in the derived class.
209
+ Note: Model Exchange classes must define a `self.time` member. Derivative variables give the
210
+ value reference of their state with `derivative=`; value references are assigned in
211
+ registration order starting at 0 (here `time` = 0, `x0` = 1, `x1` = 2).
211
212
 
212
213
  #### Create the FMU
213
214
 
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ # >= 77 for the SPDX `license` expression (PEP 639)
3
+ requires = ["setuptools>=77"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "pythonfmu3"
8
+ dynamic = ["version"]
9
+ description = "A lightweight framework that enables the packaging of Python 3 code as FMI 3.0 co-simulation and model exchange FMUs."
10
+ readme = "README.md"
11
+ requires-python = ">=3.9"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [{ name = "Stephen Smith" }]
15
+ keywords = ["FMI", "FMI3", "FMU", "co-simulation", "model exchange", "simulation"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Science/Research",
21
+ "Natural Language :: English",
22
+ "Operating System :: Microsoft :: Windows",
23
+ "Operating System :: POSIX :: Linux",
24
+ "Operating System :: MacOS",
25
+ "Programming Language :: C++",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Topic :: Scientific/Engineering",
34
+ ]
35
+ dependencies = []
36
+
37
+ [project.optional-dependencies]
38
+ tests = ["pytest", "fmpy"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/StephenSmith25/PythonFMU3"
42
+ Documentation = "https://pythonfmu3.readthedocs.io/"
43
+ Issues = "https://github.com/StephenSmith25/PythonFMU3/issues"
44
+
45
+ [project.scripts]
46
+ pythonfmu3 = "pythonfmu3.__main__:cli_main"
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = { attr = "pythonfmu3._version.__version__" }
50
+
51
+ [tool.setuptools.packages.find]
52
+ include = ["pythonfmu3*"]
53
+
54
+ [tool.setuptools.package-data]
55
+ # Prebuilt wrapper libraries, the wrapper sources (embedded in every FMU) and test data
56
+ pythonfmu3 = [
57
+ "resources/binaries/*/*",
58
+ "pythonfmu-export/**/*",
59
+ "tests/data/*",
60
+ "tests/slaves/*",
61
+ ]
62
+
63
+ [tool.setuptools.exclude-package-data]
64
+ pythonfmu3 = ["pythonfmu-export/build/**", "**/__pycache__/*"]
65
+
66
+ [tool.pytest.ini_options]
67
+ markers = [
68
+ "integration: marks integration tests",
69
+ "doc: marks documentation tests",
70
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -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"))