fmu-manipulation-toolbox 1.9rc0__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 +7 -5
- fmu_manipulation_toolbox/container.py +355 -154
- 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 +8 -0
- {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/METADATA +1 -1
- {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/RECORD +18 -17
- {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/WHEEL +0 -0
- {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/entry_points.txt +0 -0
- {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc1.dist-info}/licenses/LICENSE.txt +0 -0
- {fmu_manipulation_toolbox-1.9rc0.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
|
+
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):
|
|
19
99
|
self.causality = attrs.get("causality", "local")
|
|
20
100
|
self.variability = attrs.get("variability", "continuous")
|
|
21
|
-
self.name = attrs
|
|
22
|
-
self.vr = int(attrs
|
|
101
|
+
self.name = attrs["name"]
|
|
102
|
+
self.vr = int(attrs["valueReference"])
|
|
23
103
|
self.description = attrs.get("description", None)
|
|
24
104
|
|
|
25
|
-
|
|
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
|
+
|
|
26
110
|
self.start_value = attrs.get("start", None)
|
|
27
111
|
self.initial = attrs.get("initial", None)
|
|
28
112
|
|
|
29
|
-
|
|
30
|
-
self.type_name = type_name
|
|
31
|
-
self.start_value = attrs.pop("start", None)
|
|
32
|
-
self.initial = attrs.pop("initial", None)
|
|
113
|
+
|
|
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
|
-
|
|
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
|
|
295
|
+
|
|
222
296
|
|
|
223
|
-
def
|
|
224
|
-
|
|
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
|
|
225
302
|
|
|
226
|
-
def add_vr(self, type_name: str) -> int:
|
|
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
|
|
@@ -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,94 +666,91 @@ 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>")
|
|
747
|
+
|
|
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)
|
|
752
|
+
|
|
753
|
+
xml_file.write(" </ModelStructure>\n\n</fmiModelDescription>\n""")
|
|
596
754
|
|
|
597
755
|
def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
|
|
598
756
|
print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
|
|
@@ -604,15 +762,14 @@ class FMUContainer:
|
|
|
604
762
|
print(f"# NB of embedded FMU's", file=txt_file)
|
|
605
763
|
print(f"{len(self.involved_fmu)}", file=txt_file)
|
|
606
764
|
fmu_rank: Dict[str, int] = {}
|
|
607
|
-
for i, fmu in enumerate(self.
|
|
608
|
-
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)
|
|
609
767
|
print(f"{fmu.model_identifier}", file=txt_file)
|
|
610
768
|
print(f"{fmu.guid}", file=txt_file)
|
|
611
769
|
fmu_rank[fmu.name] = i
|
|
612
770
|
|
|
613
771
|
# Prepare data structure
|
|
614
|
-
|
|
615
|
-
inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
|
|
772
|
+
inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
|
|
616
773
|
outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
|
|
617
774
|
|
|
618
775
|
inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
|
|
@@ -620,7 +777,7 @@ class FMUContainer:
|
|
|
620
777
|
outputs_fmu_per_type = {}
|
|
621
778
|
locals_per_type: Dict[str, List[Local]] = {}
|
|
622
779
|
|
|
623
|
-
for type_name in
|
|
780
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
624
781
|
inputs_per_type[type_name] = []
|
|
625
782
|
outputs_per_type[type_name] = []
|
|
626
783
|
locals_per_type[type_name] = []
|
|
@@ -629,7 +786,7 @@ class FMUContainer:
|
|
|
629
786
|
start_values_fmu_per_type[type_name] = {}
|
|
630
787
|
outputs_fmu_per_type[type_name] = {}
|
|
631
788
|
|
|
632
|
-
for fmu in self.
|
|
789
|
+
for fmu in self.involved_fmu.values():
|
|
633
790
|
inputs_fmu_per_type[type_name][fmu.name] = {}
|
|
634
791
|
start_values_fmu_per_type[type_name][fmu.name] = {}
|
|
635
792
|
outputs_fmu_per_type[type_name][fmu.name] = {}
|
|
@@ -651,27 +808,35 @@ class FMUContainer:
|
|
|
651
808
|
for cport_to in local.cport_to_list:
|
|
652
809
|
inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
|
|
653
810
|
|
|
654
|
-
print(f"# NB local variables
|
|
655
|
-
|
|
811
|
+
print(f"# NB local variables:", ", ".join(EmbeddedFMUPort.ALL_TYPES), file=txt_file)
|
|
812
|
+
nb_local = []
|
|
813
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
656
814
|
nb = len(locals_per_type[type_name])
|
|
657
|
-
if
|
|
658
|
-
nb +=
|
|
659
|
-
|
|
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='')
|
|
660
821
|
print("", file=txt_file)
|
|
661
822
|
|
|
662
823
|
print("# CONTAINER I/O: <VR> <NB> <FMU_INDEX> <FMU_VR> [<FMU_INDEX> <FMU_VR>]", file=txt_file)
|
|
663
|
-
for type_name in
|
|
824
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
664
825
|
print(f"# {type_name}", file=txt_file)
|
|
665
826
|
nb = len(inputs_per_type[type_name]) + len(outputs_per_type[type_name]) + len(locals_per_type[type_name])
|
|
666
827
|
nb_input_link = 0
|
|
667
828
|
for input_port in inputs_per_type[type_name]:
|
|
668
829
|
nb_input_link += len(input_port.cport_list) - 1
|
|
669
830
|
|
|
670
|
-
if
|
|
671
|
-
nb +=
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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)
|
|
675
840
|
else:
|
|
676
841
|
print(f"{nb} {nb+nb_input_link}", file=txt_file)
|
|
677
842
|
for input_port in inputs_per_type[type_name]:
|
|
@@ -680,24 +845,24 @@ class FMUContainer:
|
|
|
680
845
|
for cport in outputs_per_type[type_name]:
|
|
681
846
|
print(f"{cport.vr} 1 {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
|
|
682
847
|
for local in locals_per_type[type_name]:
|
|
683
|
-
print(f"{local.vr} 1 -1 {local.vr}", file=txt_file)
|
|
848
|
+
print(f"{local.vr} 1 -1 {local.vr & 0xFFFFFF}", file=txt_file)
|
|
684
849
|
|
|
685
850
|
# LINKS
|
|
686
|
-
for fmu in self.
|
|
687
|
-
for type_name in
|
|
851
|
+
for fmu in self.involved_fmu.values():
|
|
852
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
688
853
|
print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
689
854
|
print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
690
855
|
for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
|
|
691
856
|
print(f"{vr} {cport.port.vr}", file=txt_file)
|
|
692
857
|
|
|
693
|
-
for type_name in
|
|
858
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
694
859
|
print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
|
|
695
860
|
print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
696
861
|
for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
|
|
697
862
|
reset = 1 if cport.port.causality == "input" else 0
|
|
698
863
|
print(f"{cport.port.vr} {reset} {value}", file=txt_file)
|
|
699
864
|
|
|
700
|
-
for type_name in
|
|
865
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
701
866
|
print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
702
867
|
print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
703
868
|
for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
|
|
@@ -711,6 +876,39 @@ class FMUContainer:
|
|
|
711
876
|
else:
|
|
712
877
|
return path
|
|
713
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
|
+
|
|
714
912
|
def make_fmu_skeleton(self, base_directory: Path) -> Path:
|
|
715
913
|
logger.debug(f"Initialize directory '{base_directory}'")
|
|
716
914
|
|
|
@@ -725,20 +923,23 @@ class FMUContainer:
|
|
|
725
923
|
documentation_directory.mkdir(exist_ok=True)
|
|
726
924
|
|
|
727
925
|
if self.description_pathname:
|
|
728
|
-
|
|
729
|
-
|
|
926
|
+
self.copyfile(self.description_pathname, documentation_directory)
|
|
927
|
+
|
|
928
|
+
self.copyfile(origin / "model.png", base_directory)
|
|
730
929
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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}")
|
|
738
938
|
|
|
739
939
|
for i, fmu in enumerate(self.involved_fmu.values()):
|
|
740
940
|
shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
|
|
741
941
|
self.long_path(resources_directory / f"{i:02x}"), dirs_exist_ok=True)
|
|
942
|
+
|
|
742
943
|
return resources_directory
|
|
743
944
|
|
|
744
945
|
def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
|