fmu-manipulation-toolbox 1.8.4.3b0__py3-none-any.whl → 1.9rc1__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.
- fmu_manipulation_toolbox/__version__.py +1 -1
- fmu_manipulation_toolbox/assembly.py +20 -11
- fmu_manipulation_toolbox/container.py +365 -170
- fmu_manipulation_toolbox/operations.py +129 -92
- fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
- fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
- fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
- fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
- fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
- fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
- fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
- fmu_manipulation_toolbox/split.py +33 -9
- {fmu_manipulation_toolbox-1.8.4.3b0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/METADATA +1 -1
- {fmu_manipulation_toolbox-1.8.4.3b0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/RECORD +18 -17
- {fmu_manipulation_toolbox-1.8.4.3b0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/WHEEL +0 -0
- {fmu_manipulation_toolbox-1.8.4.3b0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/entry_points.txt +0 -0
- {fmu_manipulation_toolbox-1.8.4.3b0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/licenses/LICENSE.txt +0 -0
- {fmu_manipulation_toolbox-1.8.4.3b0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import getpass
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import uuid
|
|
6
|
+
import platform
|
|
5
7
|
import zipfile
|
|
6
8
|
from datetime import datetime
|
|
7
9
|
from pathlib import Path
|
|
@@ -14,22 +16,101 @@ from .version import __version__ as tool_version
|
|
|
14
16
|
logger = logging.getLogger("fmu_manipulation_toolbox")
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
class EmbeddedFMUPort:
|
|
20
|
+
FMI_TO_CONTAINER = {
|
|
21
|
+
2: {
|
|
22
|
+
'Real': 'real64',
|
|
23
|
+
'Integer': 'integer32',
|
|
24
|
+
'String': 'string',
|
|
25
|
+
'Boolean': 'boolean'
|
|
26
|
+
},
|
|
27
|
+
3: {
|
|
28
|
+
'Float64': 'real64',
|
|
29
|
+
'Float32': 'real32',
|
|
30
|
+
'Int8': 'integer8',
|
|
31
|
+
'UInt8': 'uinteger8',
|
|
32
|
+
'Int16': 'integer16',
|
|
33
|
+
'UInt16': 'uinteger16',
|
|
34
|
+
'Int32': 'integer32',
|
|
35
|
+
'UInt32': 'uinteger32',
|
|
36
|
+
'Int64': 'integer64',
|
|
37
|
+
'UInt64': 'uinteger64',
|
|
38
|
+
'String': 'string',
|
|
39
|
+
'Boolean': 'boolean1'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
FMI_TO_CONTAINER = {
|
|
44
|
+
2: {
|
|
45
|
+
'Real': 'real64',
|
|
46
|
+
'Integer': 'integer32',
|
|
47
|
+
'String': 'string',
|
|
48
|
+
'Boolean': 'boolean'
|
|
49
|
+
},
|
|
50
|
+
3: {
|
|
51
|
+
'Float64': 'real64',
|
|
52
|
+
'Float32': 'real32',
|
|
53
|
+
'Int8': 'integer8',
|
|
54
|
+
'UInt8': 'uinteger8',
|
|
55
|
+
'Int16': 'integer16',
|
|
56
|
+
'UInt16': 'uinteger16',
|
|
57
|
+
'Int32': 'integer32',
|
|
58
|
+
'UInt32': 'uinteger32',
|
|
59
|
+
'Int64': 'integer64',
|
|
60
|
+
'UInt64': 'uinteger64',
|
|
61
|
+
'String': 'string',
|
|
62
|
+
'Boolean': 'boolean1'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
CONTAINER_TO_FMI = {
|
|
67
|
+
2: {
|
|
68
|
+
'real64': 'Real',
|
|
69
|
+
'integer32': 'Integer',
|
|
70
|
+
'string': 'String',
|
|
71
|
+
'boolean': 'Boolean'
|
|
72
|
+
},
|
|
73
|
+
3: {
|
|
74
|
+
'real64': 'Float64' ,
|
|
75
|
+
'real32': 'Float32' ,
|
|
76
|
+
'integer8': 'Int8' ,
|
|
77
|
+
'uinteger8': 'UInt8' ,
|
|
78
|
+
'integer16': 'Int16' ,
|
|
79
|
+
'uinteger16': 'UInt16' ,
|
|
80
|
+
'integer32': 'Int32' ,
|
|
81
|
+
'uinteger32': 'UInt32' ,
|
|
82
|
+
'integer64': 'Int64' ,
|
|
83
|
+
'uinteger64': 'UInt64' ,
|
|
84
|
+
'string': 'String' ,
|
|
85
|
+
'boolean1': 'Boolean'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ALL_TYPES = (
|
|
90
|
+
"real64", "real32",
|
|
91
|
+
"integer8", "uinteger8", "integer16", "uinteger16", "integer32", "uinteger32", "integer64", "uinteger64",
|
|
92
|
+
"boolean", "boolean1",
|
|
93
|
+
"strings"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def __init__(self, fmi_type, attrs: Dict[str, str], fmi_version=0):
|
|
99
|
+
self.causality = attrs.get("causality", "local")
|
|
100
|
+
self.variability = attrs.get("variability", "continuous")
|
|
101
|
+
self.name = attrs["name"]
|
|
102
|
+
self.vr = int(attrs["valueReference"])
|
|
103
|
+
self.description = attrs.get("description", None)
|
|
104
|
+
|
|
105
|
+
if fmi_version > 0:
|
|
106
|
+
self.type_name = self.FMI_TO_CONTAINER[fmi_version][fmi_type]
|
|
107
|
+
else:
|
|
108
|
+
self.type_name = fmi_type
|
|
109
|
+
|
|
110
|
+
self.start_value = attrs.get("start", None)
|
|
111
|
+
self.initial = attrs.get("initial", None)
|
|
24
112
|
|
|
25
|
-
self.type_name = attrs.pop("type_name", None)
|
|
26
|
-
self.start_value = attrs.pop("start", None)
|
|
27
|
-
self.initial = attrs.pop("initial", None)
|
|
28
113
|
|
|
29
|
-
def set_port_type(self, type_name: str, attrs: Dict[str, str]):
|
|
30
|
-
self.type_name = type_name
|
|
31
|
-
self.start_value = attrs.pop("start", None)
|
|
32
|
-
self.initial = attrs.pop("initial", None)
|
|
33
114
|
|
|
34
115
|
def xml(self, vr: int, name=None, causality=None, start=None, fmi_version=2):
|
|
35
116
|
if name is None:
|
|
@@ -39,23 +120,18 @@ class FMUPort:
|
|
|
39
120
|
if start is None:
|
|
40
121
|
start = self.start_value
|
|
41
122
|
if self.variability is None:
|
|
42
|
-
self.variability = "continuous" if self.type_name
|
|
123
|
+
self.variability = "continuous" if "real" in self.type_name else "discrete"
|
|
43
124
|
|
|
125
|
+
fmi_type = self.CONTAINER_TO_FMI[fmi_version][self.type_name]
|
|
44
126
|
|
|
45
127
|
if fmi_version == 2:
|
|
46
|
-
|
|
128
|
+
child_attrs = {
|
|
47
129
|
"start": start,
|
|
48
130
|
}
|
|
49
|
-
if "Float" in self.type_name:
|
|
50
|
-
type_name = "Real"
|
|
51
|
-
elif "Int" in self.type_name:
|
|
52
|
-
type_name = "Integer"
|
|
53
|
-
else:
|
|
54
|
-
type_name = self.type_name
|
|
55
131
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
132
|
+
filtered_child_attrs = {key: value for key, value in child_attrs.items() if value is not None}
|
|
133
|
+
child_str = (f"<{fmi_type} " +
|
|
134
|
+
" ".join([f'{key}="{value}"' for (key, value) in filtered_child_attrs.items()]) +
|
|
59
135
|
"/>")
|
|
60
136
|
|
|
61
137
|
scalar_attrs = {
|
|
@@ -66,11 +142,23 @@ class FMUPort:
|
|
|
66
142
|
"initial": self.initial,
|
|
67
143
|
"description": self.description,
|
|
68
144
|
}
|
|
69
|
-
|
|
70
|
-
|
|
145
|
+
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
146
|
+
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
71
147
|
return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
|
|
72
148
|
else:
|
|
73
|
-
|
|
149
|
+
scalar_attrs = {
|
|
150
|
+
"name": name,
|
|
151
|
+
"valueReference": vr,
|
|
152
|
+
"causality": causality,
|
|
153
|
+
"variability": self.variability,
|
|
154
|
+
"initial": self.initial,
|
|
155
|
+
"description": self.description,
|
|
156
|
+
"start": start
|
|
157
|
+
}
|
|
158
|
+
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
159
|
+
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
160
|
+
|
|
161
|
+
return f'<{fmi_type} {scalar_attrs_str}/>'
|
|
74
162
|
|
|
75
163
|
|
|
76
164
|
class EmbeddedFMU(OperationAbstract):
|
|
@@ -80,14 +168,15 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
80
168
|
def __init__(self, filename):
|
|
81
169
|
self.fmu = FMU(filename)
|
|
82
170
|
self.name = Path(filename).name
|
|
83
|
-
self.id = Path(filename).stem
|
|
171
|
+
self.id = Path(filename).stem.lower()
|
|
84
172
|
|
|
85
173
|
self.step_size = None
|
|
86
174
|
self.start_time = None
|
|
87
175
|
self.stop_time = None
|
|
88
176
|
self.model_identifier = None
|
|
89
177
|
self.guid = None
|
|
90
|
-
self.
|
|
178
|
+
self.fmi_version = None
|
|
179
|
+
self.ports: Dict[str, EmbeddedFMUPort] = {}
|
|
91
180
|
|
|
92
181
|
self.capabilities: Dict[str, str] = {}
|
|
93
182
|
self.current_port = None # used during apply_operation()
|
|
@@ -100,21 +189,10 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
100
189
|
fmi_version = attrs['fmiVersion']
|
|
101
190
|
if fmi_version == "2.0":
|
|
102
191
|
self.guid = attrs['guid']
|
|
192
|
+
self.fmi_version = 2
|
|
103
193
|
if fmi_version == "3.0":
|
|
104
194
|
self.guid = attrs['instantiationToken']
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def scalar_attrs(self, attrs) -> int:
|
|
108
|
-
if 'type_name' in attrs: # FMI 3.0
|
|
109
|
-
type_name = attrs.pop('type_name')
|
|
110
|
-
port = FMUPort(attrs)
|
|
111
|
-
port.type_name = type_name
|
|
112
|
-
self.ports[port.name] = port
|
|
113
|
-
else: # FMI 2.0
|
|
114
|
-
self.current_port = FMUPort(attrs)
|
|
115
|
-
self.ports[self.current_port.name] = self.current_port
|
|
116
|
-
|
|
117
|
-
return 0
|
|
195
|
+
self.fmi_version = 3
|
|
118
196
|
|
|
119
197
|
def cosimulation_attrs(self, attrs: Dict[str, str]):
|
|
120
198
|
self.model_identifier = attrs['modelIdentifier']
|
|
@@ -129,12 +207,9 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
129
207
|
self.start_time = float(attrs.get("startTime", 0.0))
|
|
130
208
|
self.stop_time = float(attrs.get("stopTime", self.start_time + 1.0))
|
|
131
209
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
type_name = "Integer"
|
|
136
|
-
self.current_port.set_port_type(type_name, attrs)
|
|
137
|
-
self.current_port = None
|
|
210
|
+
def port_attrs(self, fmu_port):
|
|
211
|
+
port = EmbeddedFMUPort(fmu_port.fmi_type, fmu_port, fmi_version=self.fmi_version)
|
|
212
|
+
self.ports[port.name] = port
|
|
138
213
|
|
|
139
214
|
def __repr__(self):
|
|
140
215
|
return f"FMU '{self.name}' ({len(self.ports)} variables)"
|
|
@@ -212,21 +287,23 @@ class Local:
|
|
|
212
287
|
|
|
213
288
|
class ValueReferenceTable:
|
|
214
289
|
def __init__(self):
|
|
215
|
-
self.vr_table
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"String": 0,
|
|
221
|
-
}
|
|
290
|
+
self.vr_table = {}
|
|
291
|
+
self.masks = {}
|
|
292
|
+
for i, type_name in enumerate(EmbeddedFMUPort.ALL_TYPES):
|
|
293
|
+
self.vr_table[type_name] = 0
|
|
294
|
+
self.masks[type_name] = i << 24
|
|
222
295
|
|
|
223
|
-
def get_vr(self, cport: ContainerPort) -> int:
|
|
224
|
-
return self.add_vr(cport.port.type_name)
|
|
225
296
|
|
|
226
|
-
def add_vr(self,
|
|
297
|
+
def add_vr(self, port_or_type_name: Union[ContainerPort, str]) -> int:
|
|
298
|
+
if isinstance(port_or_type_name, ContainerPort):
|
|
299
|
+
type_name = port_or_type_name.port.type_name
|
|
300
|
+
else:
|
|
301
|
+
type_name = port_or_type_name
|
|
302
|
+
|
|
227
303
|
vr = self.vr_table[type_name]
|
|
228
304
|
self.vr_table[type_name] += 1
|
|
229
|
-
|
|
305
|
+
|
|
306
|
+
return vr | self.masks[type_name]
|
|
230
307
|
|
|
231
308
|
|
|
232
309
|
class AutoWired:
|
|
@@ -255,15 +332,83 @@ class AutoWired:
|
|
|
255
332
|
|
|
256
333
|
|
|
257
334
|
class FMUContainer:
|
|
258
|
-
|
|
335
|
+
HEADER_XML_2 = """<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
336
|
+
<fmiModelDescription
|
|
337
|
+
fmiVersion="2.0"
|
|
338
|
+
modelName="{identifier}"
|
|
339
|
+
generationTool="FMUContainer-{tool_version}"
|
|
340
|
+
generationDateAndTime="{timestamp}"
|
|
341
|
+
guid="{guid}"
|
|
342
|
+
description="FMUContainer with {embedded_fmu}"
|
|
343
|
+
author="{author}"
|
|
344
|
+
license="Proprietary"
|
|
345
|
+
copyright="See Embedded FMU's copyrights."
|
|
346
|
+
variableNamingConvention="structured">
|
|
347
|
+
|
|
348
|
+
<CoSimulation
|
|
349
|
+
modelIdentifier="{identifier}"
|
|
350
|
+
canHandleVariableCommunicationStepSize="true"
|
|
351
|
+
canBeInstantiatedOnlyOncePerProcess="{only_once}"
|
|
352
|
+
canNotUseMemoryManagementFunctions="true"
|
|
353
|
+
canGetAndSetFMUstate="false"
|
|
354
|
+
canSerializeFMUstate="false"
|
|
355
|
+
providesDirectionalDerivative="false"
|
|
356
|
+
needsExecutionTool="{execution_tool}">
|
|
357
|
+
</CoSimulation>
|
|
358
|
+
|
|
359
|
+
<LogCategories>
|
|
360
|
+
<Category name="fmucontainer"/>
|
|
361
|
+
</LogCategories>
|
|
362
|
+
|
|
363
|
+
<DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
|
|
364
|
+
|
|
365
|
+
<ModelVariables>
|
|
366
|
+
<ScalarVariable valueReference="0" name="time" causality="independent"><Real /></ScalarVariable>
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
HEADER_XML_3 = """<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
370
|
+
<fmiModelDescription
|
|
371
|
+
fmiVersion="3.0"
|
|
372
|
+
modelName="{identifier}"
|
|
373
|
+
generationTool="FMUContainer-{tool_version}"
|
|
374
|
+
generationDateAndTime="{timestamp}"
|
|
375
|
+
instantiationToken="{guid}"
|
|
376
|
+
description="FMUContainer with {embedded_fmu}"
|
|
377
|
+
author="{author}"
|
|
378
|
+
license="Proprietary"
|
|
379
|
+
copyright="See Embedded FMU's copyrights."
|
|
380
|
+
variableNamingConvention="structured">
|
|
381
|
+
|
|
382
|
+
<CoSimulation
|
|
383
|
+
modelIdentifier="{identifier}"
|
|
384
|
+
canHandleVariableCommunicationStepSize="true"
|
|
385
|
+
canBeInstantiatedOnlyOncePerProcess="{only_once}"
|
|
386
|
+
canNotUseMemoryManagementFunctions="true"
|
|
387
|
+
canGetAndSetFMUstate="false"
|
|
388
|
+
canSerializeFMUstate="false"
|
|
389
|
+
providesDirectionalDerivative="false"
|
|
390
|
+
needsExecutionTool="{execution_tool}">
|
|
391
|
+
</CoSimulation>
|
|
392
|
+
|
|
393
|
+
<LogCategories>
|
|
394
|
+
<Category name="fmucontainer"/>
|
|
395
|
+
</LogCategories>
|
|
396
|
+
|
|
397
|
+
<DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
|
|
398
|
+
|
|
399
|
+
<ModelVariables>
|
|
400
|
+
<Float64 valueReference="0" name="time" causality="independent"/>
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None, fmi_version=2):
|
|
259
404
|
self.fmu_directory = Path(fmu_directory)
|
|
260
405
|
self.identifier = identifier
|
|
261
406
|
if not self.fmu_directory.is_dir():
|
|
262
407
|
raise FMUContainerError(f"{self.fmu_directory} is not a valid directory")
|
|
263
|
-
self.involved_fmu:
|
|
264
|
-
self.execution_order: List[EmbeddedFMU] = []
|
|
408
|
+
self.involved_fmu: OrderedDict[str, EmbeddedFMU] = OrderedDict()
|
|
265
409
|
|
|
266
410
|
self.description_pathname = description_pathname
|
|
411
|
+
self.fmi_version = fmi_version
|
|
267
412
|
|
|
268
413
|
self.start_time = None
|
|
269
414
|
self.stop_time = None
|
|
@@ -276,15 +421,31 @@ class FMUContainer:
|
|
|
276
421
|
self.rules: Dict[ContainerPort, str] = {}
|
|
277
422
|
self.start_values: Dict[ContainerPort, str] = {}
|
|
278
423
|
|
|
424
|
+
|
|
425
|
+
def convert_type_name(self, type_name: str) -> str:
|
|
426
|
+
if self.fmi_version == 2:
|
|
427
|
+
table = {}
|
|
428
|
+
elif self.fmi_version == 3:
|
|
429
|
+
table = {}
|
|
430
|
+
else:
|
|
431
|
+
table = {}
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
return table[type_name]
|
|
435
|
+
except KeyError:
|
|
436
|
+
return type_name
|
|
437
|
+
|
|
279
438
|
def get_fmu(self, fmu_filename: str) -> EmbeddedFMU:
|
|
280
439
|
if fmu_filename in self.involved_fmu:
|
|
281
440
|
return self.involved_fmu[fmu_filename]
|
|
282
441
|
|
|
283
442
|
try:
|
|
284
443
|
fmu = EmbeddedFMU(self.fmu_directory / fmu_filename)
|
|
444
|
+
if fmu.fmi_version > self.fmi_version:
|
|
445
|
+
logger.fatal(f"Try to embed FMU-{fmu.fmi_version} into container FMI-{self.fmi_version}")
|
|
285
446
|
self.involved_fmu[fmu.name] = fmu
|
|
286
|
-
|
|
287
|
-
logger.debug(f"Adding FMU #{len(self.
|
|
447
|
+
|
|
448
|
+
logger.debug(f"Adding FMU #{len(self.involved_fmu)}: {fmu}")
|
|
288
449
|
except (FMUContainerError, FMUError) as e:
|
|
289
450
|
raise FMUContainerError(f"Cannot load '{fmu_filename}': {e}")
|
|
290
451
|
|
|
@@ -299,7 +460,7 @@ class FMUContainer:
|
|
|
299
460
|
self.rules[cport] = rule
|
|
300
461
|
|
|
301
462
|
def get_all_cports(self):
|
|
302
|
-
return [ContainerPort(fmu, port_name) for fmu in self.
|
|
463
|
+
return [ContainerPort(fmu, port_name) for fmu in self.involved_fmu.values() for port_name in fmu.ports]
|
|
303
464
|
|
|
304
465
|
def add_input(self, container_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
305
466
|
if not container_port_name:
|
|
@@ -373,7 +534,7 @@ class FMUContainer:
|
|
|
373
534
|
|
|
374
535
|
self.start_values[cport] = value
|
|
375
536
|
|
|
376
|
-
def find_inputs(self, port_to_connect:
|
|
537
|
+
def find_inputs(self, port_to_connect: EmbeddedFMUPort) -> List[ContainerPort]:
|
|
377
538
|
candidates = []
|
|
378
539
|
for cport in self.get_all_cports():
|
|
379
540
|
if (cport.port.causality == 'input' and cport not in self.rules and cport.port.name == port_to_connect.name
|
|
@@ -432,7 +593,7 @@ class FMUContainer:
|
|
|
432
593
|
|
|
433
594
|
def minimum_step_size(self) -> float:
|
|
434
595
|
step_size = None
|
|
435
|
-
for fmu in self.
|
|
596
|
+
for fmu in self.involved_fmu.values():
|
|
436
597
|
if step_size:
|
|
437
598
|
if fmu.step_size and fmu.step_size < step_size:
|
|
438
599
|
step_size = fmu.step_size
|
|
@@ -446,7 +607,7 @@ class FMUContainer:
|
|
|
446
607
|
return step_size
|
|
447
608
|
|
|
448
609
|
def sanity_check(self, step_size: Optional[float]):
|
|
449
|
-
for fmu in self.
|
|
610
|
+
for fmu in self.involved_fmu.values():
|
|
450
611
|
if not fmu.step_size:
|
|
451
612
|
continue
|
|
452
613
|
ts_ratio = step_size / fmu.step_size
|
|
@@ -465,7 +626,7 @@ class FMUContainer:
|
|
|
465
626
|
logger.warning(f"{cport} is not connected")
|
|
466
627
|
|
|
467
628
|
def make_fmu(self, fmu_filename: Union[str, Path], step_size: Optional[float] = None, debug=False, mt=False,
|
|
468
|
-
profiling=False):
|
|
629
|
+
profiling=False, sequential=False):
|
|
469
630
|
if isinstance(fmu_filename, str):
|
|
470
631
|
fmu_filename = Path(fmu_filename)
|
|
471
632
|
|
|
@@ -481,7 +642,7 @@ class FMUContainer:
|
|
|
481
642
|
with open(base_directory / "modelDescription.xml", "wt") as xml_file:
|
|
482
643
|
self.make_fmu_xml(xml_file, step_size, profiling)
|
|
483
644
|
with open(resources_directory / "container.txt", "wt") as txt_file:
|
|
484
|
-
self.make_fmu_txt(txt_file, step_size, mt, profiling)
|
|
645
|
+
self.make_fmu_txt(txt_file, step_size, mt, profiling, sequential)
|
|
485
646
|
|
|
486
647
|
self.make_fmu_package(base_directory, fmu_filename)
|
|
487
648
|
if not debug:
|
|
@@ -494,7 +655,7 @@ class FMUContainer:
|
|
|
494
655
|
guid = str(uuid.uuid4())
|
|
495
656
|
embedded_fmu = ", ".join([fmu_name for fmu_name in self.involved_fmu])
|
|
496
657
|
try:
|
|
497
|
-
author =
|
|
658
|
+
author = getpass.getuser()
|
|
498
659
|
except OSError:
|
|
499
660
|
author = "Unspecified"
|
|
500
661
|
|
|
@@ -505,120 +666,110 @@ class FMUContainer:
|
|
|
505
666
|
if fmu.capabilities[capability] == "true":
|
|
506
667
|
capabilities[capability] = "true"
|
|
507
668
|
|
|
669
|
+
first_fmu = next(iter(self.involved_fmu.values()))
|
|
508
670
|
if self.start_time is None:
|
|
509
|
-
self.start_time =
|
|
510
|
-
logger.info(f"start_time={self.start_time} (deduced from '{
|
|
671
|
+
self.start_time = first_fmu.start_time
|
|
672
|
+
logger.info(f"start_time={self.start_time} (deduced from '{first_fmu.name}')")
|
|
511
673
|
else:
|
|
512
674
|
logger.info(f"start_time={self.start_time}")
|
|
513
675
|
|
|
514
676
|
if self.stop_time is None:
|
|
515
|
-
self.stop_time =
|
|
516
|
-
logger.info(f"stop_time={self.stop_time} (deduced from '{
|
|
677
|
+
self.stop_time = first_fmu.stop_time
|
|
678
|
+
logger.info(f"stop_time={self.stop_time} (deduced from '{first_fmu.name}')")
|
|
517
679
|
else:
|
|
518
680
|
logger.info(f"stop_time={self.stop_time}")
|
|
519
681
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
canSerializeFMUstate="false"
|
|
540
|
-
providesDirectionalDerivative="false"
|
|
541
|
-
needsExecutionTool="{capabilities['needsExecutionTool']}">
|
|
542
|
-
</CoSimulation>
|
|
543
|
-
|
|
544
|
-
<LogCategories>
|
|
545
|
-
<Category name="fmucontainer"/>
|
|
546
|
-
</LogCategories>
|
|
547
|
-
|
|
548
|
-
<DefaultExperiment stepSize="{step_size}" startTime="{self.start_time}" stopTime="{self.stop_time}"/>
|
|
549
|
-
|
|
550
|
-
<ModelVariables>
|
|
551
|
-
""")
|
|
682
|
+
if self.fmi_version == 2:
|
|
683
|
+
xml_file.write(self.HEADER_XML_2.format(identifier=self.identifier, tool_version=tool_version,
|
|
684
|
+
timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
|
|
685
|
+
author=author,
|
|
686
|
+
only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
|
|
687
|
+
execution_tool=capabilities['needsExecutionTool'],
|
|
688
|
+
start_time=self.start_time, stop_time=self.stop_time,
|
|
689
|
+
step_size=step_size))
|
|
690
|
+
elif self.fmi_version == 3:
|
|
691
|
+
xml_file.write(self.HEADER_XML_3.format(identifier=self.identifier, tool_version=tool_version,
|
|
692
|
+
timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
|
|
693
|
+
author=author,
|
|
694
|
+
only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
|
|
695
|
+
execution_tool=capabilities['needsExecutionTool'],
|
|
696
|
+
start_time=self.start_time, stop_time=self.stop_time,
|
|
697
|
+
step_size=step_size))
|
|
698
|
+
|
|
699
|
+
vr_time = vr_table.add_vr("real64")
|
|
700
|
+
logger.debug(f"Time vr = {vr_time}")
|
|
552
701
|
if profiling:
|
|
553
|
-
for fmu in self.
|
|
554
|
-
vr = vr_table.add_vr("
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
702
|
+
for fmu in self.involved_fmu.values():
|
|
703
|
+
vr = vr_table.add_vr("real64")
|
|
704
|
+
port = EmbeddedFMUPort("real64", {"valueReference": vr,
|
|
705
|
+
"name": f"container.{fmu.id}.rt_ratio",
|
|
706
|
+
"description": f"RT ratio for embedded FMU '{fmu.name}'"})
|
|
707
|
+
print(f" {port.xml(vr, fmi_version=self.fmi_version)}", file=xml_file)
|
|
558
708
|
|
|
559
709
|
# Local variable should be first to ensure to attribute them the lowest VR.
|
|
560
710
|
for local in self.locals.values():
|
|
561
|
-
vr = vr_table.
|
|
562
|
-
|
|
711
|
+
vr = vr_table.add_vr(local.cport_from)
|
|
712
|
+
try:
|
|
713
|
+
print(f" {local.cport_from.port.xml(vr, name=local.name, causality='local', fmi_version=self.fmi_version)}", file=xml_file)
|
|
714
|
+
except KeyError:
|
|
715
|
+
logger.error(f"Cannot expose '{local.name}' because type '{local.cport_from.port.type_name}' is not compatible with FMI-{self.fmi_version}.0")
|
|
563
716
|
local.vr = vr
|
|
564
717
|
|
|
565
718
|
for input_port_name, input_port in self.inputs.items():
|
|
566
719
|
vr = vr_table.add_vr(input_port.type_name)
|
|
567
720
|
# Get Start and XML from first connected input
|
|
568
721
|
start = self.start_values.get(input_port.cport_list[0], None)
|
|
569
|
-
print(f" {input_port.cport_list[0].port.xml(vr, name=input_port_name, start=start)}", file=xml_file)
|
|
722
|
+
print(f" {input_port.cport_list[0].port.xml(vr, name=input_port_name, start=start, fmi_version=self.fmi_version)}", file=xml_file)
|
|
570
723
|
input_port.vr = vr
|
|
571
724
|
|
|
572
725
|
for output_port_name, cport in self.outputs.items():
|
|
573
|
-
vr = vr_table.
|
|
574
|
-
print(f" {cport.port.xml(vr, name=output_port_name)}", file=xml_file)
|
|
726
|
+
vr = vr_table.add_vr(cport)
|
|
727
|
+
print(f" {cport.port.xml(vr, name=output_port_name, fmi_version=self.fmi_version)}", file=xml_file)
|
|
575
728
|
cport.vr = vr
|
|
576
729
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
""")
|
|
730
|
+
if self.fmi_version == 2:
|
|
731
|
+
self.make_fmu_xml_epilog_2(xml_file)
|
|
732
|
+
elif self.fmi_version == 3:
|
|
733
|
+
self.make_fmu_xml_epilog_3(xml_file)
|
|
582
734
|
|
|
735
|
+
def make_fmu_xml_epilog_2(self, xml_file):
|
|
736
|
+
xml_file.write(f" </ModelVariables>\n\n <ModelStructure>\n <Outputs>\n")
|
|
583
737
|
index_offset = len(self.locals) + len(self.inputs) + 1
|
|
584
738
|
for i, _ in enumerate(self.outputs.keys()):
|
|
585
739
|
print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
|
|
586
|
-
|
|
587
|
-
<InitialUnknowns
|
|
588
|
-
|
|
740
|
+
|
|
741
|
+
xml_file.write(" </Outputs>\n <InitialUnknowns>\n")
|
|
742
|
+
|
|
589
743
|
for i, _ in enumerate(self.outputs.keys()):
|
|
590
744
|
print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
|
|
591
|
-
xml_file.write(""" </InitialUnknowns>
|
|
592
|
-
</ModelStructure>
|
|
593
745
|
|
|
594
|
-
</fmiModelDescription>
|
|
595
|
-
""")
|
|
746
|
+
xml_file.write(" </InitialUnknowns>\n </ModelStructure>\n\n</fmiModelDescription>")
|
|
596
747
|
|
|
597
|
-
def
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
print("# Don't use MT\n0", file=txt_file)
|
|
748
|
+
def make_fmu_xml_epilog_3(self, xml_file):
|
|
749
|
+
xml_file.write(f" </ModelVariables>\n\n <ModelStructure>\n")
|
|
750
|
+
for output in self.outputs.values():
|
|
751
|
+
print(f' <Output valueReference="{output.vr}"/>', file=xml_file)
|
|
602
752
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
753
|
+
xml_file.write(" </ModelStructure>\n\n</fmiModelDescription>\n""")
|
|
754
|
+
|
|
755
|
+
def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
|
|
756
|
+
print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
|
|
757
|
+
flags = [ str(int(flag == True)) for flag in (mt, profiling, sequential)]
|
|
758
|
+
print(" ".join(flags), file=txt_file)
|
|
607
759
|
|
|
608
760
|
print(f"# Internal time step in seconds", file=txt_file)
|
|
609
761
|
print(f"{step_size}", file=txt_file)
|
|
610
762
|
print(f"# NB of embedded FMU's", file=txt_file)
|
|
611
763
|
print(f"{len(self.involved_fmu)}", file=txt_file)
|
|
612
764
|
fmu_rank: Dict[str, int] = {}
|
|
613
|
-
for i, fmu in enumerate(self.
|
|
614
|
-
print(f"{fmu.name}", file=txt_file)
|
|
765
|
+
for i, fmu in enumerate(self.involved_fmu.values()):
|
|
766
|
+
print(f"{fmu.name} {fmu.fmi_version}", file=txt_file)
|
|
615
767
|
print(f"{fmu.model_identifier}", file=txt_file)
|
|
616
768
|
print(f"{fmu.guid}", file=txt_file)
|
|
617
769
|
fmu_rank[fmu.name] = i
|
|
618
770
|
|
|
619
771
|
# Prepare data structure
|
|
620
|
-
|
|
621
|
-
inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
|
|
772
|
+
inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
|
|
622
773
|
outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
|
|
623
774
|
|
|
624
775
|
inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
|
|
@@ -626,7 +777,7 @@ class FMUContainer:
|
|
|
626
777
|
outputs_fmu_per_type = {}
|
|
627
778
|
locals_per_type: Dict[str, List[Local]] = {}
|
|
628
779
|
|
|
629
|
-
for type_name in
|
|
780
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
630
781
|
inputs_per_type[type_name] = []
|
|
631
782
|
outputs_per_type[type_name] = []
|
|
632
783
|
locals_per_type[type_name] = []
|
|
@@ -635,7 +786,7 @@ class FMUContainer:
|
|
|
635
786
|
start_values_fmu_per_type[type_name] = {}
|
|
636
787
|
outputs_fmu_per_type[type_name] = {}
|
|
637
788
|
|
|
638
|
-
for fmu in self.
|
|
789
|
+
for fmu in self.involved_fmu.values():
|
|
639
790
|
inputs_fmu_per_type[type_name][fmu.name] = {}
|
|
640
791
|
start_values_fmu_per_type[type_name][fmu.name] = {}
|
|
641
792
|
outputs_fmu_per_type[type_name][fmu.name] = {}
|
|
@@ -657,27 +808,35 @@ class FMUContainer:
|
|
|
657
808
|
for cport_to in local.cport_to_list:
|
|
658
809
|
inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
|
|
659
810
|
|
|
660
|
-
print(f"# NB local variables
|
|
661
|
-
|
|
811
|
+
print(f"# NB local variables:", ", ".join(EmbeddedFMUPort.ALL_TYPES), file=txt_file)
|
|
812
|
+
nb_local = []
|
|
813
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
662
814
|
nb = len(locals_per_type[type_name])
|
|
663
|
-
if
|
|
664
|
-
nb +=
|
|
665
|
-
|
|
815
|
+
if type_name == "real64":
|
|
816
|
+
nb += 1 # reserver a slot for "time"
|
|
817
|
+
if profiling:
|
|
818
|
+
nb += len(self.involved_fmu)
|
|
819
|
+
nb_local.append(str(nb))
|
|
820
|
+
print(" ".join(nb_local), file=txt_file, end='')
|
|
666
821
|
print("", file=txt_file)
|
|
667
822
|
|
|
668
823
|
print("# CONTAINER I/O: <VR> <NB> <FMU_INDEX> <FMU_VR> [<FMU_INDEX> <FMU_VR>]", file=txt_file)
|
|
669
|
-
for type_name in
|
|
824
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
670
825
|
print(f"# {type_name}", file=txt_file)
|
|
671
826
|
nb = len(inputs_per_type[type_name]) + len(outputs_per_type[type_name]) + len(locals_per_type[type_name])
|
|
672
827
|
nb_input_link = 0
|
|
673
828
|
for input_port in inputs_per_type[type_name]:
|
|
674
829
|
nb_input_link += len(input_port.cport_list) - 1
|
|
675
830
|
|
|
676
|
-
if
|
|
677
|
-
nb +=
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
831
|
+
if type_name == "real64":
|
|
832
|
+
nb += 1 # reserver a slot for "time"
|
|
833
|
+
if profiling:
|
|
834
|
+
nb += len(self.involved_fmu)
|
|
835
|
+
print(f"{nb} {nb + nb_input_link}", file=txt_file)
|
|
836
|
+
print(f"0 1 -1 0", file=txt_file) # Time slot
|
|
837
|
+
if profiling:
|
|
838
|
+
for profiling_port, _ in enumerate(self.involved_fmu.values()):
|
|
839
|
+
print(f"{profiling_port+1} 1 -2 {profiling_port+1}", file=txt_file)
|
|
681
840
|
else:
|
|
682
841
|
print(f"{nb} {nb+nb_input_link}", file=txt_file)
|
|
683
842
|
for input_port in inputs_per_type[type_name]:
|
|
@@ -686,24 +845,24 @@ class FMUContainer:
|
|
|
686
845
|
for cport in outputs_per_type[type_name]:
|
|
687
846
|
print(f"{cport.vr} 1 {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
|
|
688
847
|
for local in locals_per_type[type_name]:
|
|
689
|
-
print(f"{local.vr} 1 -1 {local.vr}", file=txt_file)
|
|
848
|
+
print(f"{local.vr} 1 -1 {local.vr & 0xFFFFFF}", file=txt_file)
|
|
690
849
|
|
|
691
850
|
# LINKS
|
|
692
|
-
for fmu in self.
|
|
693
|
-
for type_name in
|
|
851
|
+
for fmu in self.involved_fmu.values():
|
|
852
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
694
853
|
print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
695
854
|
print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
696
855
|
for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
|
|
697
856
|
print(f"{vr} {cport.port.vr}", file=txt_file)
|
|
698
857
|
|
|
699
|
-
for type_name in
|
|
858
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
700
859
|
print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
|
|
701
860
|
print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
702
861
|
for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
|
|
703
862
|
reset = 1 if cport.port.causality == "input" else 0
|
|
704
863
|
print(f"{cport.port.vr} {reset} {value}", file=txt_file)
|
|
705
864
|
|
|
706
|
-
for type_name in
|
|
865
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
707
866
|
print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
708
867
|
print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
709
868
|
for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
|
|
@@ -717,6 +876,39 @@ class FMUContainer:
|
|
|
717
876
|
else:
|
|
718
877
|
return path
|
|
719
878
|
|
|
879
|
+
@staticmethod
|
|
880
|
+
def copyfile(origin, destination):
|
|
881
|
+
logger.debug(f"Copying {origin} in {destination}")
|
|
882
|
+
shutil.copy(origin, destination)
|
|
883
|
+
|
|
884
|
+
def get_bindir_and_suffixe(self) -> (str, str, str):
|
|
885
|
+
suffixes = {
|
|
886
|
+
"Windows": "dll",
|
|
887
|
+
"Linux": "so",
|
|
888
|
+
"Darwin": "dylib"
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
origin_bindirs = {
|
|
892
|
+
"Windows": "win64",
|
|
893
|
+
"Linux": "linux64",
|
|
894
|
+
"Darwin": "darwin64"
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if self.fmi_version == 3:
|
|
898
|
+
target_bindirs = {
|
|
899
|
+
"Windows": "x86_64-windows",
|
|
900
|
+
"Linux": "x86_64-linux",
|
|
901
|
+
"Darwin": "aarch64-darwin"
|
|
902
|
+
}
|
|
903
|
+
else:
|
|
904
|
+
target_bindirs = origin_bindirs
|
|
905
|
+
|
|
906
|
+
os_name = platform.system()
|
|
907
|
+
try:
|
|
908
|
+
return origin_bindirs[os_name], suffixes[os_name], target_bindirs[os_name]
|
|
909
|
+
except KeyError:
|
|
910
|
+
raise FMUContainerError(f"OS '{os_name}' is not supported.")
|
|
911
|
+
|
|
720
912
|
def make_fmu_skeleton(self, base_directory: Path) -> Path:
|
|
721
913
|
logger.debug(f"Initialize directory '{base_directory}'")
|
|
722
914
|
|
|
@@ -731,20 +923,23 @@ class FMUContainer:
|
|
|
731
923
|
documentation_directory.mkdir(exist_ok=True)
|
|
732
924
|
|
|
733
925
|
if self.description_pathname:
|
|
734
|
-
|
|
735
|
-
|
|
926
|
+
self.copyfile(self.description_pathname, documentation_directory)
|
|
927
|
+
|
|
928
|
+
self.copyfile(origin / "model.png", base_directory)
|
|
736
929
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
930
|
+
origin_bindir, suffixe, target_bindir = self.get_bindir_and_suffixe()
|
|
931
|
+
|
|
932
|
+
library_filename = origin / origin_bindir / f"container.{suffixe}"
|
|
933
|
+
if not library_filename.is_file():
|
|
934
|
+
raise FMUContainerError(f"File {library_filename} not found")
|
|
935
|
+
binary_directory = binaries_directory / target_bindir
|
|
936
|
+
binary_directory.mkdir(exist_ok=True)
|
|
937
|
+
self.copyfile(library_filename, binary_directory / f"{self.identifier}.{suffixe}")
|
|
744
938
|
|
|
745
939
|
for i, fmu in enumerate(self.involved_fmu.values()):
|
|
746
940
|
shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
|
|
747
941
|
self.long_path(resources_directory / f"{i:02x}"), dirs_exist_ok=True)
|
|
942
|
+
|
|
748
943
|
return resources_directory
|
|
749
944
|
|
|
750
945
|
def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
|