fmu-manipulation-toolbox 1.8.4.2rc1__py3-none-any.whl → 1.9__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/__init__.py +0 -1
- fmu_manipulation_toolbox/__main__.py +1 -1
- fmu_manipulation_toolbox/__version__.py +1 -1
- fmu_manipulation_toolbox/assembly.py +22 -13
- fmu_manipulation_toolbox/checker.py +16 -11
- fmu_manipulation_toolbox/cli/__init__.py +0 -0
- fmu_manipulation_toolbox/cli/fmucontainer.py +105 -0
- fmu_manipulation_toolbox/cli/fmusplit.py +48 -0
- fmu_manipulation_toolbox/cli/fmutool.py +127 -0
- fmu_manipulation_toolbox/cli/utils.py +36 -0
- fmu_manipulation_toolbox/container.py +1054 -0
- fmu_manipulation_toolbox/gui.py +48 -56
- fmu_manipulation_toolbox/gui_style.py +8 -0
- fmu_manipulation_toolbox/help.py +3 -0
- fmu_manipulation_toolbox/operations.py +577 -0
- fmu_manipulation_toolbox/remoting.py +107 -0
- fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Annotation.xsd +51 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3AttributeGroups.xsd +119 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3BuildDescription.xsd +117 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3InterfaceType.xsd +80 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3LayeredStandardManifest.xsd +93 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3ModelDescription.xsd +131 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Terminal.xsd +87 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3TerminalsAndIcons.xsd +84 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Type.xsd +207 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Unit.xsd +69 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Variable.xsd +413 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3VariableDependency.xsd +64 -0
- fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
- fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
- fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
- fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
- fmu_manipulation_toolbox/resources/linux64/server_sm +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 +331 -0
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/METADATA +1 -1
- fmu_manipulation_toolbox-1.9.dist-info/RECORD +71 -0
- fmu_manipulation_toolbox-1.9.dist-info/entry_points.txt +7 -0
- fmu_manipulation_toolbox/cli.py +0 -235
- fmu_manipulation_toolbox/fmu_container.py +0 -753
- fmu_manipulation_toolbox/fmu_operations.py +0 -489
- fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/RECORD +0 -52
- fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/entry_points.txt +0 -3
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/WHEEL +0 -0
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/licenses/LICENSE.txt +0 -0
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import getpass
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import uuid
|
|
7
|
+
import platform
|
|
8
|
+
import zipfile
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import *
|
|
12
|
+
|
|
13
|
+
from .operations import FMU, OperationAbstract, FMUError, FMUPort
|
|
14
|
+
from .version import __version__ as tool_version
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("fmu_manipulation_toolbox")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EmbeddedFMUPort:
|
|
21
|
+
FMI_TO_CONTAINER = {
|
|
22
|
+
2: {
|
|
23
|
+
'Real': 'real64',
|
|
24
|
+
'Integer': 'integer32',
|
|
25
|
+
'String': 'string',
|
|
26
|
+
'Boolean': 'boolean'
|
|
27
|
+
},
|
|
28
|
+
3: {
|
|
29
|
+
'Float64': 'real64',
|
|
30
|
+
'Float32': 'real32',
|
|
31
|
+
'Int8': 'integer8',
|
|
32
|
+
'UInt8': 'uinteger8',
|
|
33
|
+
'Int16': 'integer16',
|
|
34
|
+
'UInt16': 'uinteger16',
|
|
35
|
+
'Int32': 'integer32',
|
|
36
|
+
'UInt32': 'uinteger32',
|
|
37
|
+
'Int64': 'integer64',
|
|
38
|
+
'UInt64': 'uinteger64',
|
|
39
|
+
'String': 'string',
|
|
40
|
+
'Boolean': 'boolean1'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
CONTAINER_TO_FMI = {
|
|
45
|
+
2: {
|
|
46
|
+
'real64': 'Real',
|
|
47
|
+
'integer32': 'Integer',
|
|
48
|
+
'string': 'String',
|
|
49
|
+
'boolean': 'Boolean'
|
|
50
|
+
},
|
|
51
|
+
3: {
|
|
52
|
+
'real64': 'Float64' ,
|
|
53
|
+
'real32': 'Float32' ,
|
|
54
|
+
'integer8': 'Int8' ,
|
|
55
|
+
'uinteger8': 'UInt8' ,
|
|
56
|
+
'integer16': 'Int16' ,
|
|
57
|
+
'uinteger16': 'UInt16' ,
|
|
58
|
+
'integer32': 'Int32' ,
|
|
59
|
+
'uinteger32': 'UInt32' ,
|
|
60
|
+
'integer64': 'Int64' ,
|
|
61
|
+
'uinteger64': 'UInt64' ,
|
|
62
|
+
'string': 'String' ,
|
|
63
|
+
'boolean1': 'Boolean'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ALL_TYPES = (
|
|
68
|
+
"real64", "real32",
|
|
69
|
+
"integer8", "uinteger8", "integer16", "uinteger16", "integer32", "uinteger32", "integer64", "uinteger64",
|
|
70
|
+
"boolean", "boolean1",
|
|
71
|
+
"string"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __init__(self, fmi_type, attrs: Union[FMUPort, Dict[str, str]], fmi_version=0):
|
|
75
|
+
self.causality = attrs.get("causality", "local")
|
|
76
|
+
self.variability = attrs.get("variability", "continuous")
|
|
77
|
+
self.name = attrs["name"]
|
|
78
|
+
self.vr = int(attrs["valueReference"])
|
|
79
|
+
self.description = attrs.get("description", None)
|
|
80
|
+
|
|
81
|
+
if fmi_version > 0:
|
|
82
|
+
self.type_name = self.FMI_TO_CONTAINER[fmi_version][fmi_type]
|
|
83
|
+
else:
|
|
84
|
+
self.type_name = fmi_type
|
|
85
|
+
|
|
86
|
+
self.start_value = attrs.get("start", None)
|
|
87
|
+
self.initial = attrs.get("initial", None)
|
|
88
|
+
|
|
89
|
+
def xml(self, vr: int, name=None, causality=None, start=None, fmi_version=2) -> str:
|
|
90
|
+
if name is None:
|
|
91
|
+
name = self.name
|
|
92
|
+
if causality is None:
|
|
93
|
+
causality = self.causality
|
|
94
|
+
if start is None:
|
|
95
|
+
start = self.start_value
|
|
96
|
+
if self.variability is None:
|
|
97
|
+
self.variability = "continuous" if "real" in self.type_name else "discrete"
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
fmi_type = self.CONTAINER_TO_FMI[fmi_version][self.type_name]
|
|
101
|
+
except KeyError:
|
|
102
|
+
logger.error(f"Cannot expose '{name}' because type '{self.type_name}' is not compatible "
|
|
103
|
+
f"with FMI-{fmi_version}.0")
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
if fmi_version == 2:
|
|
107
|
+
child_attrs = {
|
|
108
|
+
"start": start,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
filtered_child_attrs = {key: value for key, value in child_attrs.items() if value is not None}
|
|
112
|
+
child_str = (f"<{fmi_type} " +
|
|
113
|
+
" ".join([f'{key}="{value}"' for (key, value) in filtered_child_attrs.items()]) +
|
|
114
|
+
"/>")
|
|
115
|
+
|
|
116
|
+
scalar_attrs = {
|
|
117
|
+
"name": name,
|
|
118
|
+
"valueReference": vr,
|
|
119
|
+
"causality": causality,
|
|
120
|
+
"variability": self.variability,
|
|
121
|
+
"initial": self.initial,
|
|
122
|
+
"description": self.description,
|
|
123
|
+
}
|
|
124
|
+
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
125
|
+
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
126
|
+
return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
|
|
127
|
+
else:
|
|
128
|
+
if fmi_type in ('String', 'Binary'):
|
|
129
|
+
if start:
|
|
130
|
+
child_str = f'<Start value="{start}"/>'
|
|
131
|
+
else:
|
|
132
|
+
child_str = ''
|
|
133
|
+
scalar_attrs = {
|
|
134
|
+
"name": name,
|
|
135
|
+
"valueReference": vr,
|
|
136
|
+
"causality": causality,
|
|
137
|
+
"variability": self.variability,
|
|
138
|
+
"initial": self.initial,
|
|
139
|
+
"description": self.description,
|
|
140
|
+
}
|
|
141
|
+
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
142
|
+
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
143
|
+
return f'<{fmi_type} {scalar_attrs_str}>{child_str}</{fmi_type}>'
|
|
144
|
+
else:
|
|
145
|
+
scalar_attrs = {
|
|
146
|
+
"name": name,
|
|
147
|
+
"valueReference": vr,
|
|
148
|
+
"causality": causality,
|
|
149
|
+
"variability": self.variability,
|
|
150
|
+
"initial": self.initial,
|
|
151
|
+
"description": self.description,
|
|
152
|
+
"start": start
|
|
153
|
+
}
|
|
154
|
+
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
155
|
+
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
156
|
+
|
|
157
|
+
return f'<{fmi_type} {scalar_attrs_str}/>'
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class EmbeddedFMU(OperationAbstract):
|
|
161
|
+
capability_list = ("needsExecutionTool",
|
|
162
|
+
"canBeInstantiatedOnlyOncePerProcess",
|
|
163
|
+
"canHandleVariableCommunicationStepSize")
|
|
164
|
+
|
|
165
|
+
def __init__(self, filename):
|
|
166
|
+
self.fmu = FMU(filename)
|
|
167
|
+
self.name = Path(filename).name
|
|
168
|
+
self.id = Path(filename).stem.lower()
|
|
169
|
+
|
|
170
|
+
self.step_size = None
|
|
171
|
+
self.start_time = None
|
|
172
|
+
self.stop_time = None
|
|
173
|
+
self.model_identifier = None
|
|
174
|
+
self.guid = None
|
|
175
|
+
self.fmi_version = None
|
|
176
|
+
self.ports: Dict[str, EmbeddedFMUPort] = {}
|
|
177
|
+
|
|
178
|
+
self.capabilities: Dict[str, str] = {}
|
|
179
|
+
self.current_port = None # used during apply_operation()
|
|
180
|
+
|
|
181
|
+
self.fmu.apply_operation(self) # Should be the last command in constructor!
|
|
182
|
+
if self.model_identifier is None:
|
|
183
|
+
raise FMUContainerError(f"FMU '{self.name}' does not implement Co-Simulation mode.")
|
|
184
|
+
|
|
185
|
+
def fmi_attrs(self, attrs):
|
|
186
|
+
fmi_version = attrs['fmiVersion']
|
|
187
|
+
if fmi_version == "2.0":
|
|
188
|
+
self.guid = attrs['guid']
|
|
189
|
+
self.fmi_version = 2
|
|
190
|
+
if fmi_version == "3.0": # TODO: handle 3.x cases
|
|
191
|
+
self.guid = attrs['instantiationToken']
|
|
192
|
+
self.fmi_version = 3
|
|
193
|
+
|
|
194
|
+
def cosimulation_attrs(self, attrs: Dict[str, str]):
|
|
195
|
+
self.model_identifier = attrs['modelIdentifier']
|
|
196
|
+
for capability in self.capability_list:
|
|
197
|
+
self.capabilities[capability] = attrs.get(capability, "false")
|
|
198
|
+
|
|
199
|
+
def experiment_attrs(self, attrs: Dict[str, str]):
|
|
200
|
+
try:
|
|
201
|
+
self.step_size = float(attrs['stepSize'])
|
|
202
|
+
except KeyError:
|
|
203
|
+
logger.warning(f"FMU '{self.name}' does not specify preferred step size")
|
|
204
|
+
self.start_time = float(attrs.get("startTime", 0.0))
|
|
205
|
+
self.stop_time = float(attrs.get("stopTime", self.start_time + 1.0))
|
|
206
|
+
|
|
207
|
+
def port_attrs(self, fmu_port: FMUPort):
|
|
208
|
+
# Container will manage Enumeration as Integer
|
|
209
|
+
if fmu_port.fmi_type == "Enumeration":
|
|
210
|
+
if self.fmi_version == 2:
|
|
211
|
+
fmu_port.fmi_type = "Integer"
|
|
212
|
+
else:
|
|
213
|
+
fmu_port.fmi_type = "Int32"
|
|
214
|
+
port = EmbeddedFMUPort(fmu_port.fmi_type, fmu_port, fmi_version=self.fmi_version)
|
|
215
|
+
self.ports[port.name] = port
|
|
216
|
+
|
|
217
|
+
def __repr__(self):
|
|
218
|
+
return f"FMU '{self.name}' ({len(self.ports)} variables, ts={self.step_size}s)"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class FMUContainerError(Exception):
|
|
222
|
+
def __init__(self, reason: str):
|
|
223
|
+
self.reason = reason
|
|
224
|
+
|
|
225
|
+
def __repr__(self):
|
|
226
|
+
return f"{self.reason}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ContainerPort:
|
|
230
|
+
def __init__(self, fmu: EmbeddedFMU, port_name: str):
|
|
231
|
+
self.fmu = fmu
|
|
232
|
+
try:
|
|
233
|
+
self.port = fmu.ports[port_name]
|
|
234
|
+
except KeyError:
|
|
235
|
+
raise FMUContainerError(f"Port '{fmu.name}/{port_name}' does not exist")
|
|
236
|
+
self.vr = None
|
|
237
|
+
|
|
238
|
+
def __repr__(self):
|
|
239
|
+
return f"Port {self.fmu.name}/{self.port.name}"
|
|
240
|
+
|
|
241
|
+
def __hash__(self):
|
|
242
|
+
return hash(str(self))
|
|
243
|
+
|
|
244
|
+
def __eq__(self, other):
|
|
245
|
+
return str(self) == str(other)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class ContainerInput:
|
|
249
|
+
def __init__(self, name: str, cport_to: ContainerPort):
|
|
250
|
+
self.name = name
|
|
251
|
+
self.type_name = cport_to.port.type_name
|
|
252
|
+
self.causality = cport_to.port.causality
|
|
253
|
+
self.cport_list = [cport_to]
|
|
254
|
+
self.vr = None
|
|
255
|
+
|
|
256
|
+
def add_cport(self, cport_to: ContainerPort):
|
|
257
|
+
if cport_to in self.cport_list: # Cannot be reached ! (Assembly prevent this to happen)
|
|
258
|
+
raise FMUContainerError(f"Duplicate INPUT {cport_to} already connected to {self.name}")
|
|
259
|
+
|
|
260
|
+
if cport_to.port.type_name != self.type_name:
|
|
261
|
+
raise FMUContainerError(f"Cannot connect {self.name} of type {self.type_name} to "
|
|
262
|
+
f"{cport_to} of type {cport_to.port.type_name}")
|
|
263
|
+
|
|
264
|
+
if cport_to.port.causality != self.causality:
|
|
265
|
+
raise FMUContainerError(f"Cannot connect {self.causality.upper()} {self.name} to "
|
|
266
|
+
f"{cport_to.port.causality.upper()} {cport_to}")
|
|
267
|
+
|
|
268
|
+
self.cport_list.append(cport_to)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class Link:
|
|
272
|
+
CONVERSION_FUNCTION = {
|
|
273
|
+
"real32/real64": "F32_F64",
|
|
274
|
+
|
|
275
|
+
"Int8/Int16": "D8_D16",
|
|
276
|
+
"Int8/UInt16": "D8_U16",
|
|
277
|
+
"Int8/Int32": "D8_D32",
|
|
278
|
+
"Int8/UInt32": "D8_U32",
|
|
279
|
+
"Int8/Int64": "D8_D64",
|
|
280
|
+
"Int8/UInt64": "D8_U64",
|
|
281
|
+
|
|
282
|
+
"UInt8/Int16": "U8_D16",
|
|
283
|
+
"UInt8/UInt16": "U8_U16",
|
|
284
|
+
"UInt8/Int32": "U8_D32",
|
|
285
|
+
"UInt8/UInt32": "U8_U32",
|
|
286
|
+
"UInt8/Int64": "U8_D64",
|
|
287
|
+
"UInt8/UInt64": "U8_U64",
|
|
288
|
+
|
|
289
|
+
"Int16/Int32": "D16_D32",
|
|
290
|
+
"Int16/UInt32": "D16_U32",
|
|
291
|
+
"Int16/Int64": "D16_D64",
|
|
292
|
+
"Int16/UInt64": "D16_U64",
|
|
293
|
+
|
|
294
|
+
"UInt16/Int32": "U16_D32",
|
|
295
|
+
"UInt16/UInt32": "U16_U32",
|
|
296
|
+
"UInt16/Int64": "U16_D64",
|
|
297
|
+
"UInt16/UInt64": "U16_U64",
|
|
298
|
+
|
|
299
|
+
"Int32/Int64": "D32_D64",
|
|
300
|
+
"Int32/UInt64": "D32_U64",
|
|
301
|
+
|
|
302
|
+
"UInt32/Int64": "U32_D64",
|
|
303
|
+
"UInt32/UInt64": "U32_U64",
|
|
304
|
+
|
|
305
|
+
"boolean/boolean1": "B_B1",
|
|
306
|
+
"boolean1/boolean": "B1_B",
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
def __init__(self, cport_from: ContainerPort):
|
|
310
|
+
self.name = cport_from.fmu.id + "." + cport_from.port.name # strip .fmu suffix
|
|
311
|
+
self.cport_from = cport_from
|
|
312
|
+
self.cport_to_list: List[ContainerPort] = []
|
|
313
|
+
|
|
314
|
+
self.vr: Optional[int] = None
|
|
315
|
+
self.vr_converted: Dict[str, Optional[int]] = {}
|
|
316
|
+
|
|
317
|
+
if not cport_from.port.causality == "output":
|
|
318
|
+
raise FMUContainerError(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT")
|
|
319
|
+
|
|
320
|
+
def add_target(self, cport_to: ContainerPort):
|
|
321
|
+
if not cport_to.port.causality == "input":
|
|
322
|
+
raise FMUContainerError(f"{cport_to} is {cport_to.port.causality} instead of INPUT")
|
|
323
|
+
|
|
324
|
+
if cport_to.port.type_name == self.cport_from.port.type_name:
|
|
325
|
+
self.cport_to_list.append(cport_to)
|
|
326
|
+
elif self.get_conversion(cport_to):
|
|
327
|
+
self.cport_to_list.append(cport_to)
|
|
328
|
+
self.vr_converted[cport_to.port.type_name] = None
|
|
329
|
+
else:
|
|
330
|
+
raise FMUContainerError(f"failed to connect {self.cport_from} to {cport_to} due to type.")
|
|
331
|
+
|
|
332
|
+
def get_conversion(self, cport_to: ContainerPort) -> Optional[str]:
|
|
333
|
+
try:
|
|
334
|
+
conversion = f"{self.cport_from.port.type_name}/{cport_to.port.type_name}"
|
|
335
|
+
return self.CONVERSION_FUNCTION[conversion]
|
|
336
|
+
except KeyError:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
def nb_local(self) -> int:
|
|
340
|
+
return 1+len(self.vr_converted)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ValueReferenceTable:
|
|
344
|
+
def __init__(self):
|
|
345
|
+
self.vr_table:Dict[str, int] = {}
|
|
346
|
+
self.masks: Dict[str, int] = {}
|
|
347
|
+
self.nb_local_variable:Dict[str, int] = {}
|
|
348
|
+
for i, type_name in enumerate(EmbeddedFMUPort.ALL_TYPES):
|
|
349
|
+
self.vr_table[type_name] = 0
|
|
350
|
+
self.masks[type_name] = i << 24
|
|
351
|
+
self.nb_local_variable[type_name] = 0
|
|
352
|
+
|
|
353
|
+
def add_vr(self, port_or_type_name: Union[ContainerPort, str], local: bool = False) -> int:
|
|
354
|
+
if isinstance(port_or_type_name, ContainerPort):
|
|
355
|
+
type_name = port_or_type_name.port.type_name
|
|
356
|
+
else:
|
|
357
|
+
type_name = port_or_type_name
|
|
358
|
+
|
|
359
|
+
if local:
|
|
360
|
+
self.nb_local_variable[type_name] += 1
|
|
361
|
+
|
|
362
|
+
vr = self.vr_table[type_name]
|
|
363
|
+
self.vr_table[type_name] += 1
|
|
364
|
+
|
|
365
|
+
return vr | self.masks[type_name]
|
|
366
|
+
|
|
367
|
+
def set_link_vr(self, link: Link):
|
|
368
|
+
link.vr = self.add_vr(link.cport_from, local=True)
|
|
369
|
+
for type_name in link.vr_converted.keys():
|
|
370
|
+
link.vr_converted[type_name] = self.add_vr(type_name, local=True)
|
|
371
|
+
|
|
372
|
+
def nb_local(self, type_name: str) -> int:
|
|
373
|
+
return self.nb_local_variable[type_name]
|
|
374
|
+
|
|
375
|
+
class AutoWired:
|
|
376
|
+
def __init__(self):
|
|
377
|
+
self.rule_input = []
|
|
378
|
+
self.rule_output = []
|
|
379
|
+
self.rule_link = []
|
|
380
|
+
self.nb_param = 0
|
|
381
|
+
|
|
382
|
+
def __repr__(self):
|
|
383
|
+
return (f"{self.nb_param} parameters, {len(self.rule_input) - self.nb_param} inputs,"
|
|
384
|
+
f" {len(self.rule_output)} outputs, {len(self.rule_link)} links.")
|
|
385
|
+
|
|
386
|
+
def add_input(self, from_port, to_fmu, to_port):
|
|
387
|
+
self.rule_input.append([from_port, to_fmu, to_port])
|
|
388
|
+
|
|
389
|
+
def add_parameter(self, from_port, to_fmu, to_port):
|
|
390
|
+
self.rule_input.append([from_port, to_fmu, to_port])
|
|
391
|
+
self.nb_param += 1
|
|
392
|
+
|
|
393
|
+
def add_output(self, from_fmu, from_port, to_port):
|
|
394
|
+
self.rule_output.append([from_fmu, from_port, to_port])
|
|
395
|
+
|
|
396
|
+
def add_link(self, from_fmu, from_port, to_fmu, to_port):
|
|
397
|
+
self.rule_link.append([from_fmu, from_port, to_fmu, to_port])
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class FMUContainer:
|
|
401
|
+
HEADER_XML_2 = """<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
402
|
+
<fmiModelDescription
|
|
403
|
+
fmiVersion="2.0"
|
|
404
|
+
modelName="{identifier}"
|
|
405
|
+
generationTool="FMUContainer-{tool_version}"
|
|
406
|
+
generationDateAndTime="{timestamp}"
|
|
407
|
+
guid="{guid}"
|
|
408
|
+
description="FMUContainer with {embedded_fmu}"
|
|
409
|
+
author="{author}"
|
|
410
|
+
license="Proprietary"
|
|
411
|
+
copyright="See Embedded FMU's copyrights."
|
|
412
|
+
variableNamingConvention="structured">
|
|
413
|
+
|
|
414
|
+
<CoSimulation
|
|
415
|
+
modelIdentifier="{identifier}"
|
|
416
|
+
canHandleVariableCommunicationStepSize="true"
|
|
417
|
+
canBeInstantiatedOnlyOncePerProcess="{only_once}"
|
|
418
|
+
canNotUseMemoryManagementFunctions="true"
|
|
419
|
+
canGetAndSetFMUstate="false"
|
|
420
|
+
canSerializeFMUstate="false"
|
|
421
|
+
providesDirectionalDerivative="false"
|
|
422
|
+
needsExecutionTool="{execution_tool}">
|
|
423
|
+
</CoSimulation>
|
|
424
|
+
|
|
425
|
+
<LogCategories>
|
|
426
|
+
<Category name="fmucontainer"/>
|
|
427
|
+
</LogCategories>
|
|
428
|
+
|
|
429
|
+
<DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
|
|
430
|
+
|
|
431
|
+
<ModelVariables>
|
|
432
|
+
<ScalarVariable valueReference="0" name="time" causality="independent"><Real /></ScalarVariable>
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
HEADER_XML_3 = """<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
436
|
+
<fmiModelDescription
|
|
437
|
+
fmiVersion="3.0"
|
|
438
|
+
modelName="{identifier}"
|
|
439
|
+
generationTool="FMUContainer-{tool_version}"
|
|
440
|
+
generationDateAndTime="{timestamp}"
|
|
441
|
+
instantiationToken="{guid}"
|
|
442
|
+
description="FMUContainer with {embedded_fmu}"
|
|
443
|
+
author="{author}"
|
|
444
|
+
license="Proprietary"
|
|
445
|
+
copyright="See Embedded FMU's copyrights."
|
|
446
|
+
variableNamingConvention="structured">
|
|
447
|
+
|
|
448
|
+
<CoSimulation
|
|
449
|
+
modelIdentifier="{identifier}"
|
|
450
|
+
canHandleVariableCommunicationStepSize="true"
|
|
451
|
+
canBeInstantiatedOnlyOncePerProcess="{only_once}"
|
|
452
|
+
canNotUseMemoryManagementFunctions="true"
|
|
453
|
+
canGetAndSetFMUstate="false"
|
|
454
|
+
canSerializeFMUstate="false"
|
|
455
|
+
providesDirectionalDerivative="false"
|
|
456
|
+
needsExecutionTool="{execution_tool}">
|
|
457
|
+
</CoSimulation>
|
|
458
|
+
|
|
459
|
+
<LogCategories>
|
|
460
|
+
<Category name="fmucontainer"/>
|
|
461
|
+
</LogCategories>
|
|
462
|
+
|
|
463
|
+
<DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
|
|
464
|
+
|
|
465
|
+
<ModelVariables>
|
|
466
|
+
<Float64 valueReference="0" name="time" causality="independent"/>
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None, fmi_version=2):
|
|
470
|
+
self.fmu_directory = Path(fmu_directory)
|
|
471
|
+
self.identifier = identifier
|
|
472
|
+
if not self.fmu_directory.is_dir():
|
|
473
|
+
raise FMUContainerError(f"{self.fmu_directory} is not a valid directory")
|
|
474
|
+
self.involved_fmu: OrderedDict[str, EmbeddedFMU] = OrderedDict()
|
|
475
|
+
|
|
476
|
+
self.description_pathname = description_pathname
|
|
477
|
+
self.fmi_version = fmi_version
|
|
478
|
+
|
|
479
|
+
self.start_time = None
|
|
480
|
+
self.stop_time = None
|
|
481
|
+
|
|
482
|
+
# Rules
|
|
483
|
+
self.inputs: Dict[str, ContainerInput] = {}
|
|
484
|
+
self.outputs: Dict[str, ContainerPort] = {}
|
|
485
|
+
self.links: Dict[ContainerPort, Link] = {}
|
|
486
|
+
|
|
487
|
+
self.rules: Dict[ContainerPort, str] = {}
|
|
488
|
+
self.start_values: Dict[ContainerPort, str] = {}
|
|
489
|
+
|
|
490
|
+
self.vr_table = ValueReferenceTable()
|
|
491
|
+
|
|
492
|
+
def get_fmu(self, fmu_filename: str) -> EmbeddedFMU:
|
|
493
|
+
if fmu_filename in self.involved_fmu:
|
|
494
|
+
return self.involved_fmu[fmu_filename]
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
fmu = EmbeddedFMU(self.fmu_directory / fmu_filename)
|
|
498
|
+
if not fmu.fmi_version == self.fmi_version:
|
|
499
|
+
logger.warning(f"Try to embed FMU-{fmu.fmi_version} into container FMI-{self.fmi_version}.")
|
|
500
|
+
self.involved_fmu[fmu.name] = fmu
|
|
501
|
+
|
|
502
|
+
logger.debug(f"Adding FMU #{len(self.involved_fmu)}: {fmu}")
|
|
503
|
+
except (FMUContainerError, FMUError) as e:
|
|
504
|
+
raise FMUContainerError(f"Cannot load '{fmu_filename}': {e}")
|
|
505
|
+
|
|
506
|
+
return fmu
|
|
507
|
+
|
|
508
|
+
def mark_ruled(self, cport: ContainerPort, rule: str):
|
|
509
|
+
if cport in self.rules:
|
|
510
|
+
previous_rule = self.rules[cport]
|
|
511
|
+
if rule not in ("OUTPUT", "LINK") and previous_rule not in ("OUTPUT", "LINK"):
|
|
512
|
+
raise FMUContainerError(f"try to {rule} port {cport} which is already {previous_rule}")
|
|
513
|
+
|
|
514
|
+
self.rules[cport] = rule
|
|
515
|
+
|
|
516
|
+
def get_all_cports(self):
|
|
517
|
+
return [ContainerPort(fmu, port_name) for fmu in self.involved_fmu.values() for port_name in fmu.ports]
|
|
518
|
+
|
|
519
|
+
def add_input(self, container_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
520
|
+
if not container_port_name:
|
|
521
|
+
container_port_name = to_port_name
|
|
522
|
+
cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
|
|
523
|
+
if cport_to.port.causality not in ("input", "parameter"): # check causality
|
|
524
|
+
raise FMUContainerError(f"Tried to use '{cport_to}' as INPUT of the container but FMU causality is "
|
|
525
|
+
f"'{cport_to.port.causality}'.")
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
input_port = self.inputs[container_port_name]
|
|
529
|
+
input_port.add_cport(cport_to)
|
|
530
|
+
except KeyError:
|
|
531
|
+
self.inputs[container_port_name] = ContainerInput(container_port_name, cport_to)
|
|
532
|
+
|
|
533
|
+
logger.debug(f"INPUT: {to_fmu_filename}:{to_port_name}")
|
|
534
|
+
self.mark_ruled(cport_to, 'INPUT')
|
|
535
|
+
|
|
536
|
+
def add_output(self, from_fmu_filename: str, from_port_name: str, container_port_name: str):
|
|
537
|
+
if not container_port_name: # empty is allowed
|
|
538
|
+
container_port_name = from_port_name
|
|
539
|
+
|
|
540
|
+
cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
|
|
541
|
+
if cport_from.port.causality not in ("output", "local"): # check causality
|
|
542
|
+
raise FMUContainerError(f"Tried to use '{cport_from}' as OUTPUT of the container but FMU causality is "
|
|
543
|
+
f"'{cport_from.port.causality}'.")
|
|
544
|
+
|
|
545
|
+
if container_port_name in self.outputs:
|
|
546
|
+
raise FMUContainerError(f"Duplicate OUTPUT {container_port_name} already connected to {cport_from}")
|
|
547
|
+
|
|
548
|
+
logger.debug(f"OUTPUT: {from_fmu_filename}:{from_port_name}")
|
|
549
|
+
self.mark_ruled(cport_from, 'OUTPUT')
|
|
550
|
+
self.outputs[container_port_name] = cport_from
|
|
551
|
+
|
|
552
|
+
def drop_port(self, from_fmu_filename: str, from_port_name: str):
|
|
553
|
+
cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
|
|
554
|
+
if not cport_from.port.causality == "output": # check causality
|
|
555
|
+
raise FMUContainerError(f"{cport_from}: trying to DROP {cport_from.port.causality}")
|
|
556
|
+
|
|
557
|
+
logger.debug(f"DROP: {from_fmu_filename}:{from_port_name}")
|
|
558
|
+
self.mark_ruled(cport_from, 'DROP')
|
|
559
|
+
|
|
560
|
+
def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
561
|
+
cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
|
|
562
|
+
try:
|
|
563
|
+
local = self.links[cport_from]
|
|
564
|
+
except KeyError:
|
|
565
|
+
local = Link(cport_from)
|
|
566
|
+
|
|
567
|
+
cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
|
|
568
|
+
local.add_target(cport_to) # Causality is check in the add() function
|
|
569
|
+
|
|
570
|
+
logger.debug(f"LINK: {cport_from} -> {cport_to}")
|
|
571
|
+
self.mark_ruled(cport_from, 'LINK')
|
|
572
|
+
self.mark_ruled(cport_to, 'LINK')
|
|
573
|
+
self.links[cport_from] = local
|
|
574
|
+
|
|
575
|
+
def add_start_value(self, fmu_filename: str, port_name: str, value: str):
|
|
576
|
+
cport = ContainerPort(self.get_fmu(fmu_filename), port_name)
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
if cport.port.type_name in ('Real', 'Float64', 'Float32'):
|
|
580
|
+
value = float(value)
|
|
581
|
+
elif cport.port.type_name in ('Integer', 'Int8', 'UInt8', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64'):
|
|
582
|
+
value = int(value)
|
|
583
|
+
elif cport.port.type_name == 'Boolean':
|
|
584
|
+
value = int(bool(value))
|
|
585
|
+
else:
|
|
586
|
+
value = value
|
|
587
|
+
except ValueError:
|
|
588
|
+
raise FMUContainerError(f"Start value is not conforming to '{cport.port.type_name}' format.")
|
|
589
|
+
|
|
590
|
+
self.start_values[cport] = value
|
|
591
|
+
|
|
592
|
+
def find_inputs(self, port_to_connect: EmbeddedFMUPort) -> List[ContainerPort]:
|
|
593
|
+
candidates = []
|
|
594
|
+
for cport in self.get_all_cports():
|
|
595
|
+
if (cport.port.causality == 'input' and cport not in self.rules and cport.port.name == port_to_connect.name
|
|
596
|
+
and cport.port.type_name == port_to_connect.type_name):
|
|
597
|
+
candidates.append(cport)
|
|
598
|
+
return candidates
|
|
599
|
+
|
|
600
|
+
def add_implicit_rule(self, auto_input=True, auto_output=True, auto_link=True, auto_parameter=False,
|
|
601
|
+
auto_local=False) -> AutoWired:
|
|
602
|
+
|
|
603
|
+
auto_wired = AutoWired()
|
|
604
|
+
# Auto Link outputs
|
|
605
|
+
for cport in self.get_all_cports():
|
|
606
|
+
if cport.port.causality == 'output':
|
|
607
|
+
candidates_cport_list = self.find_inputs(cport.port)
|
|
608
|
+
if auto_link and candidates_cport_list:
|
|
609
|
+
for candidate_cport in candidates_cport_list:
|
|
610
|
+
logger.info(f"AUTO LINK: {cport} -> {candidate_cport}")
|
|
611
|
+
self.add_link(cport.fmu.name, cport.port.name,
|
|
612
|
+
candidate_cport.fmu.name, candidate_cport.port.name)
|
|
613
|
+
auto_wired.add_link(cport.fmu.name, cport.port.name,
|
|
614
|
+
candidate_cport.fmu.name, candidate_cport.port.name)
|
|
615
|
+
elif auto_output and cport not in self.rules:
|
|
616
|
+
logger.info(f"AUTO OUTPUT: Expose {cport}")
|
|
617
|
+
self.add_output(cport.fmu.name, cport.port.name, cport.port.name)
|
|
618
|
+
auto_wired.add_output(cport.fmu.name, cport.port.name, cport.port.name)
|
|
619
|
+
elif cport.port.causality == 'local':
|
|
620
|
+
local_portname = None
|
|
621
|
+
if cport.port.name.startswith("container."):
|
|
622
|
+
local_portname = "container." + cport.fmu.id + "." + cport.port.name[10:]
|
|
623
|
+
logger.info(f"PROFILING: Expose {cport}")
|
|
624
|
+
elif auto_local:
|
|
625
|
+
local_portname = cport.fmu.id + "." + cport.port.name
|
|
626
|
+
logger.info(f"AUTO LOCAL: Expose {cport}")
|
|
627
|
+
if local_portname:
|
|
628
|
+
self.add_output(cport.fmu.name, cport.port.name, local_portname)
|
|
629
|
+
auto_wired.add_output(cport.fmu.name, cport.port.name, local_portname)
|
|
630
|
+
|
|
631
|
+
if auto_input:
|
|
632
|
+
# Auto link inputs
|
|
633
|
+
for cport in self.get_all_cports():
|
|
634
|
+
if cport not in self.rules:
|
|
635
|
+
if cport.port.causality == 'parameter' and auto_parameter:
|
|
636
|
+
parameter_name = cport.fmu.id + "." + cport.port.name
|
|
637
|
+
logger.info(f"AUTO PARAMETER: {cport} as {parameter_name}")
|
|
638
|
+
self.add_input(parameter_name, cport.fmu.name, cport.port.name)
|
|
639
|
+
auto_wired.add_parameter(parameter_name, cport.fmu.name, cport.port.name)
|
|
640
|
+
elif cport.port.causality == 'input':
|
|
641
|
+
logger.info(f"AUTO INPUT: Expose {cport}")
|
|
642
|
+
self.add_input(cport.port.name, cport.fmu.name, cport.port.name)
|
|
643
|
+
auto_wired.add_input(cport.port.name, cport.fmu.name, cport.port.name)
|
|
644
|
+
|
|
645
|
+
logger.info(f"Auto-wiring: {auto_wired}")
|
|
646
|
+
|
|
647
|
+
return auto_wired
|
|
648
|
+
|
|
649
|
+
def default_step_size(self) -> float:
|
|
650
|
+
freq_set = set()
|
|
651
|
+
for fmu in self.involved_fmu.values():
|
|
652
|
+
if fmu.step_size and fmu.capabilities["canHandleVariableCommunicationStepSize"] == "false":
|
|
653
|
+
freq_set.add(int(1.0/fmu.step_size))
|
|
654
|
+
|
|
655
|
+
common_freq = math.gcd(*freq_set)
|
|
656
|
+
try:
|
|
657
|
+
step_size = 1.0 / float(common_freq)
|
|
658
|
+
except ZeroDivisionError:
|
|
659
|
+
step_size = 0.1
|
|
660
|
+
logger.warning(f"Defaulting to step_size={step_size}")
|
|
661
|
+
|
|
662
|
+
return step_size
|
|
663
|
+
|
|
664
|
+
def sanity_check(self, step_size: Optional[float]):
|
|
665
|
+
for fmu in self.involved_fmu.values():
|
|
666
|
+
if fmu.step_size and fmu.capabilities["canHandleVariableCommunicationStepSize"] == "false":
|
|
667
|
+
ts_ratio = step_size / fmu.step_size
|
|
668
|
+
logger.debug(f"container step_size: {step_size} = {fmu.step_size} x {ts_ratio} for {fmu.name}")
|
|
669
|
+
if ts_ratio < 1.0:
|
|
670
|
+
logger.warning(f"Container step_size={step_size}s is lower than FMU '{fmu.name}' "
|
|
671
|
+
f"step_size={fmu.step_size}s.")
|
|
672
|
+
if ts_ratio != int(ts_ratio):
|
|
673
|
+
logger.warning(f"Container step_size={step_size}s should divisible by FMU '{fmu.name}' "
|
|
674
|
+
f"step_size={fmu.step_size}s.")
|
|
675
|
+
for port_name in fmu.ports:
|
|
676
|
+
cport = ContainerPort(fmu, port_name)
|
|
677
|
+
if cport not in self.rules:
|
|
678
|
+
if cport.port.causality == 'input':
|
|
679
|
+
logger.error(f"{cport} is not connected")
|
|
680
|
+
if cport.port.causality == 'output':
|
|
681
|
+
logger.warning(f"{cport} is not connected")
|
|
682
|
+
|
|
683
|
+
def make_fmu(self, fmu_filename: Union[str, Path], step_size: Optional[float] = None, debug=False, mt=False,
|
|
684
|
+
profiling=False, sequential=False):
|
|
685
|
+
if isinstance(fmu_filename, str):
|
|
686
|
+
fmu_filename = Path(fmu_filename)
|
|
687
|
+
|
|
688
|
+
if step_size is None:
|
|
689
|
+
logger.info(f"step_size will be deduced from the embedded FMU's")
|
|
690
|
+
step_size = self.default_step_size()
|
|
691
|
+
self.sanity_check(step_size)
|
|
692
|
+
|
|
693
|
+
logger.info(f"Building FMU '{fmu_filename}', step_size={step_size}")
|
|
694
|
+
|
|
695
|
+
base_directory = self.fmu_directory / fmu_filename.with_suffix('')
|
|
696
|
+
resources_directory = self.make_fmu_skeleton(base_directory)
|
|
697
|
+
with open(base_directory / "modelDescription.xml", "wt") as xml_file:
|
|
698
|
+
self.make_fmu_xml(xml_file, step_size, profiling)
|
|
699
|
+
with open(resources_directory / "container.txt", "wt") as txt_file:
|
|
700
|
+
self.make_fmu_txt(txt_file, step_size, mt, profiling, sequential)
|
|
701
|
+
|
|
702
|
+
self.make_fmu_package(base_directory, fmu_filename)
|
|
703
|
+
if not debug:
|
|
704
|
+
self.make_fmu_cleanup(base_directory)
|
|
705
|
+
|
|
706
|
+
def make_fmu_xml(self, xml_file, step_size: float, profiling: bool):
|
|
707
|
+
timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
708
|
+
guid = str(uuid.uuid4())
|
|
709
|
+
embedded_fmu = ", ".join([fmu_name for fmu_name in self.involved_fmu])
|
|
710
|
+
try:
|
|
711
|
+
author = getpass.getuser()
|
|
712
|
+
except OSError:
|
|
713
|
+
author = "Unspecified"
|
|
714
|
+
|
|
715
|
+
capabilities = {}
|
|
716
|
+
for capability in EmbeddedFMU.capability_list:
|
|
717
|
+
capabilities[capability] = "false"
|
|
718
|
+
for fmu in self.involved_fmu.values():
|
|
719
|
+
if fmu.capabilities[capability] == "true":
|
|
720
|
+
capabilities[capability] = "true"
|
|
721
|
+
|
|
722
|
+
first_fmu = next(iter(self.involved_fmu.values()))
|
|
723
|
+
if self.start_time is None:
|
|
724
|
+
self.start_time = first_fmu.start_time
|
|
725
|
+
logger.info(f"start_time={self.start_time} (deduced from '{first_fmu.name}')")
|
|
726
|
+
else:
|
|
727
|
+
logger.info(f"start_time={self.start_time}")
|
|
728
|
+
|
|
729
|
+
if self.stop_time is None:
|
|
730
|
+
self.stop_time = first_fmu.stop_time
|
|
731
|
+
logger.info(f"stop_time={self.stop_time} (deduced from '{first_fmu.name}')")
|
|
732
|
+
else:
|
|
733
|
+
logger.info(f"stop_time={self.stop_time}")
|
|
734
|
+
|
|
735
|
+
if self.fmi_version == 2:
|
|
736
|
+
xml_file.write(self.HEADER_XML_2.format(identifier=self.identifier, tool_version=tool_version,
|
|
737
|
+
timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
|
|
738
|
+
author=author,
|
|
739
|
+
only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
|
|
740
|
+
execution_tool=capabilities['needsExecutionTool'],
|
|
741
|
+
start_time=self.start_time, stop_time=self.stop_time,
|
|
742
|
+
step_size=step_size))
|
|
743
|
+
elif self.fmi_version == 3:
|
|
744
|
+
xml_file.write(self.HEADER_XML_3.format(identifier=self.identifier, tool_version=tool_version,
|
|
745
|
+
timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
|
|
746
|
+
author=author,
|
|
747
|
+
only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
|
|
748
|
+
execution_tool=capabilities['needsExecutionTool'],
|
|
749
|
+
start_time=self.start_time, stop_time=self.stop_time,
|
|
750
|
+
step_size=step_size))
|
|
751
|
+
|
|
752
|
+
vr_time = self.vr_table.add_vr("real64", local=True)
|
|
753
|
+
logger.debug(f"Time vr = {vr_time}")
|
|
754
|
+
|
|
755
|
+
if profiling:
|
|
756
|
+
for fmu in self.involved_fmu.values():
|
|
757
|
+
vr = self.vr_table.add_vr("real64", local=True)
|
|
758
|
+
port = EmbeddedFMUPort("real64", {"valueReference": vr,
|
|
759
|
+
"name": f"container.{fmu.id}.rt_ratio",
|
|
760
|
+
"description": f"RT ratio for embedded FMU '{fmu.name}'"})
|
|
761
|
+
print(f" {port.xml(vr, fmi_version=self.fmi_version)}", file=xml_file)
|
|
762
|
+
|
|
763
|
+
index_offset = 2 # index of output ports. Start at 2 to skip "time" port
|
|
764
|
+
|
|
765
|
+
# Local variable should be first to ensure to attribute them the lowest VR.
|
|
766
|
+
for link in self.links.values():
|
|
767
|
+
self.vr_table.set_link_vr(link)
|
|
768
|
+
port_local_def = link.cport_from.port.xml(link.vr, name=link.name, causality='local',
|
|
769
|
+
fmi_version=self.fmi_version)
|
|
770
|
+
if port_local_def:
|
|
771
|
+
print(f" {port_local_def}", file=xml_file)
|
|
772
|
+
index_offset += 1
|
|
773
|
+
|
|
774
|
+
for input_port_name, input_port in self.inputs.items():
|
|
775
|
+
input_port.vr = self.vr_table.add_vr(input_port.type_name)
|
|
776
|
+
# Get Start and XML from first connected input
|
|
777
|
+
start = self.start_values.get(input_port.cport_list[0], None)
|
|
778
|
+
port_input_def = input_port.cport_list[0].port.xml(input_port.vr, name=input_port_name,
|
|
779
|
+
start=start, fmi_version=self.fmi_version)
|
|
780
|
+
if port_input_def:
|
|
781
|
+
print(f" {port_input_def}", file=xml_file)
|
|
782
|
+
index_offset += 1
|
|
783
|
+
|
|
784
|
+
for output_port_name, output_port in self.outputs.items():
|
|
785
|
+
output_port.vr = self.vr_table.add_vr(output_port)
|
|
786
|
+
port_output_def = output_port.port.xml(output_port.vr, name=output_port_name,
|
|
787
|
+
fmi_version=self.fmi_version)
|
|
788
|
+
if port_output_def:
|
|
789
|
+
print(f" {port_output_def}", file=xml_file)
|
|
790
|
+
|
|
791
|
+
if self.fmi_version == 2:
|
|
792
|
+
self.make_fmu_xml_epilog_2(xml_file, index_offset)
|
|
793
|
+
elif self.fmi_version == 3:
|
|
794
|
+
self.make_fmu_xml_epilog_3(xml_file)
|
|
795
|
+
|
|
796
|
+
def make_fmu_xml_epilog_2(self, xml_file, index_offset):
|
|
797
|
+
xml_file.write(" </ModelVariables>\n"
|
|
798
|
+
"\n"
|
|
799
|
+
" <ModelStructure>\n")
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
if self.outputs:
|
|
803
|
+
xml_file.write(" <Outputs>\n")
|
|
804
|
+
index = index_offset
|
|
805
|
+
for output in self.outputs.values():
|
|
806
|
+
if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[2]:
|
|
807
|
+
print(f' <Unknown index="{index}"/>', file=xml_file)
|
|
808
|
+
index += 1
|
|
809
|
+
xml_file.write(" </Outputs>\n"
|
|
810
|
+
" <InitialUnknowns>\n")
|
|
811
|
+
index = index_offset
|
|
812
|
+
for output in self.outputs.values():
|
|
813
|
+
if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[2]:
|
|
814
|
+
print(f' <Unknown index="{index}"/>', file=xml_file)
|
|
815
|
+
index += 1
|
|
816
|
+
xml_file.write(" </InitialUnknowns>\n")
|
|
817
|
+
|
|
818
|
+
xml_file.write(" </ModelStructure>\n"
|
|
819
|
+
"\n"
|
|
820
|
+
"</fmiModelDescription>")
|
|
821
|
+
|
|
822
|
+
def make_fmu_xml_epilog_3(self, xml_file):
|
|
823
|
+
xml_file.write(" </ModelVariables>\n"
|
|
824
|
+
"\n"
|
|
825
|
+
" <ModelStructure>\n")
|
|
826
|
+
for output in self.outputs.values():
|
|
827
|
+
if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[3]:
|
|
828
|
+
print(f' <Output valueReference="{output.vr}"/>', file=xml_file)
|
|
829
|
+
for output in self.outputs.values():
|
|
830
|
+
if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[3]:
|
|
831
|
+
print(f' <InitialUnknown valueReference="{output.vr}"/>', file=xml_file)
|
|
832
|
+
xml_file.write(" </ModelStructure>\n"
|
|
833
|
+
"\n"
|
|
834
|
+
"</fmiModelDescription>")
|
|
835
|
+
|
|
836
|
+
def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
|
|
837
|
+
print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
|
|
838
|
+
flags = [ str(int(flag == True)) for flag in (mt, profiling, sequential)]
|
|
839
|
+
print(" ".join(flags), file=txt_file)
|
|
840
|
+
|
|
841
|
+
print(f"# Internal time step in seconds", file=txt_file)
|
|
842
|
+
print(f"{step_size}", file=txt_file)
|
|
843
|
+
print(f"# NB of embedded FMU's", file=txt_file)
|
|
844
|
+
print(f"{len(self.involved_fmu)}", file=txt_file)
|
|
845
|
+
fmu_rank: Dict[str, int] = {}
|
|
846
|
+
for i, fmu in enumerate(self.involved_fmu.values()):
|
|
847
|
+
print(f"{fmu.name} {fmu.fmi_version}", file=txt_file)
|
|
848
|
+
print(f"{fmu.model_identifier}", file=txt_file)
|
|
849
|
+
print(f"{fmu.guid}", file=txt_file)
|
|
850
|
+
fmu_rank[fmu.name] = i
|
|
851
|
+
|
|
852
|
+
# Prepare data structure
|
|
853
|
+
inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
|
|
854
|
+
outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
|
|
855
|
+
|
|
856
|
+
inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
|
|
857
|
+
start_values_fmu_per_type = {}
|
|
858
|
+
outputs_fmu_per_type = {}
|
|
859
|
+
local_per_type: Dict[str, List[int]] = {}
|
|
860
|
+
links_per_fmu: Dict[str, List[Link]] = {}
|
|
861
|
+
|
|
862
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
863
|
+
inputs_per_type[type_name] = []
|
|
864
|
+
outputs_per_type[type_name] = []
|
|
865
|
+
local_per_type[type_name] = []
|
|
866
|
+
|
|
867
|
+
inputs_fmu_per_type[type_name] = {}
|
|
868
|
+
start_values_fmu_per_type[type_name] = {}
|
|
869
|
+
outputs_fmu_per_type[type_name] = {}
|
|
870
|
+
|
|
871
|
+
for fmu in self.involved_fmu.values():
|
|
872
|
+
inputs_fmu_per_type[type_name][fmu.name] = {}
|
|
873
|
+
start_values_fmu_per_type[type_name][fmu.name] = {}
|
|
874
|
+
outputs_fmu_per_type[type_name][fmu.name] = {}
|
|
875
|
+
|
|
876
|
+
# Fill data structure
|
|
877
|
+
# Inputs
|
|
878
|
+
for input_port_name, input_port in self.inputs.items():
|
|
879
|
+
inputs_per_type[input_port.type_name].append(input_port)
|
|
880
|
+
for input_port, value in self.start_values.items():
|
|
881
|
+
start_values_fmu_per_type[input_port.port.type_name][input_port.fmu.name][input_port] = value
|
|
882
|
+
# Outputs
|
|
883
|
+
for output_port_name, output_port in self.outputs.items():
|
|
884
|
+
outputs_per_type[output_port.port.type_name].append(output_port)
|
|
885
|
+
# Links
|
|
886
|
+
for link in self.links.values():
|
|
887
|
+
local_per_type[link.cport_from.port.type_name].append(link.vr)
|
|
888
|
+
outputs_fmu_per_type[link.cport_from.port.type_name][link.cport_from.fmu.name][link.cport_from] = link.vr
|
|
889
|
+
for cport_to in link.cport_to_list:
|
|
890
|
+
if cport_to.port.type_name == link.cport_from.port.type_name:
|
|
891
|
+
inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = link.vr
|
|
892
|
+
else:
|
|
893
|
+
local_per_type[cport_to.port.type_name].append(link.vr_converted[cport_to.port.type_name])
|
|
894
|
+
links_per_fmu.setdefault(link.cport_from.fmu.name, []).append(link)
|
|
895
|
+
inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = link.vr_converted[cport_to.port.type_name]
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
print(f"# NB local variables:", ", ".join(EmbeddedFMUPort.ALL_TYPES), file=txt_file)
|
|
899
|
+
nb_local = [f"{self.vr_table.nb_local(type_name)}" for type_name in EmbeddedFMUPort.ALL_TYPES]
|
|
900
|
+
print(" ".join(nb_local), file=txt_file, end='')
|
|
901
|
+
print("", file=txt_file)
|
|
902
|
+
|
|
903
|
+
print("# CONTAINER I/O: <VR> <NB> <FMU_INDEX> <FMU_VR> [<FMU_INDEX> <FMU_VR>]", file=txt_file)
|
|
904
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
905
|
+
print(f"# {type_name}" , file=txt_file)
|
|
906
|
+
nb_local = (len(inputs_per_type[type_name]) +
|
|
907
|
+
len(outputs_per_type[type_name]) +
|
|
908
|
+
self.vr_table.nb_local(type_name))
|
|
909
|
+
nb_input_link = 0
|
|
910
|
+
for input_port in inputs_per_type[type_name]:
|
|
911
|
+
nb_input_link += len(input_port.cport_list) - 1
|
|
912
|
+
print(f"{nb_local} {nb_local + nb_input_link}", file=txt_file)
|
|
913
|
+
if type_name == "real64":
|
|
914
|
+
print(f"0 1 -1 0", file=txt_file) # Time slot
|
|
915
|
+
if profiling:
|
|
916
|
+
for profiling_port, _ in enumerate(self.involved_fmu.values()):
|
|
917
|
+
print(f"{profiling_port + 1} 1 -2 {profiling_port + 1}", file=txt_file)
|
|
918
|
+
|
|
919
|
+
for input_port in inputs_per_type[type_name]:
|
|
920
|
+
cport_string = [f"{fmu_rank[cport.fmu.name]} {cport.port.vr}" for cport in input_port.cport_list]
|
|
921
|
+
print(f"{input_port.vr} {len(input_port.cport_list)}", " ".join(cport_string), file=txt_file)
|
|
922
|
+
for output_port in outputs_per_type[type_name]:
|
|
923
|
+
print(f"{output_port.vr} 1 {fmu_rank[output_port.fmu.name]} {output_port.port.vr}", file=txt_file)
|
|
924
|
+
for local_vr in local_per_type[type_name]:
|
|
925
|
+
print(f"{local_vr} 1 -1 {local_vr & 0xFFFFFF}", file=txt_file)
|
|
926
|
+
|
|
927
|
+
# LINKS
|
|
928
|
+
for fmu in self.involved_fmu.values():
|
|
929
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
930
|
+
print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
931
|
+
print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
932
|
+
for input_port, vr in inputs_fmu_per_type[type_name][fmu.name].items():
|
|
933
|
+
print(f"{vr} {input_port.port.vr}", file=txt_file)
|
|
934
|
+
|
|
935
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
936
|
+
print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
|
|
937
|
+
print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
938
|
+
for input_port, value in start_values_fmu_per_type[type_name][fmu.name].items():
|
|
939
|
+
reset = 1 if input_port.port.causality == "input" else 0
|
|
940
|
+
print(f"{input_port.port.vr} {reset} {value}", file=txt_file)
|
|
941
|
+
|
|
942
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
943
|
+
print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
944
|
+
print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
945
|
+
for output_port, vr in outputs_fmu_per_type[type_name][fmu.name].items():
|
|
946
|
+
print(f"{vr} {output_port.port.vr}", file=txt_file)
|
|
947
|
+
|
|
948
|
+
print(f"# Conversion table of {fmu.name}: <VR_FROM> <VR_TO> <CONVERSION>", file=txt_file)
|
|
949
|
+
try:
|
|
950
|
+
nb = 0
|
|
951
|
+
for link in links_per_fmu[fmu.name]:
|
|
952
|
+
nb += len(link.vr_converted)
|
|
953
|
+
print(f"{nb}", file=txt_file)
|
|
954
|
+
for link in links_per_fmu[fmu.name]:
|
|
955
|
+
for cport_to in link.cport_to_list:
|
|
956
|
+
conversion = link.get_conversion(cport_to)
|
|
957
|
+
if conversion:
|
|
958
|
+
print(f"{link.vr} {link.vr_converted[cport_to.port.type_name]} {conversion}",
|
|
959
|
+
file=txt_file)
|
|
960
|
+
except KeyError:
|
|
961
|
+
print("0", file=txt_file)
|
|
962
|
+
|
|
963
|
+
@staticmethod
|
|
964
|
+
def long_path(path: Union[str, Path]) -> str:
|
|
965
|
+
# https://stackoverflow.com/questions/14075465/copy-a-file-with-a-too-long-path-to-another-directory-in-python
|
|
966
|
+
if os.name == 'nt':
|
|
967
|
+
return "\\\\?\\" + os.path.abspath(str(path))
|
|
968
|
+
else:
|
|
969
|
+
return path
|
|
970
|
+
|
|
971
|
+
@staticmethod
|
|
972
|
+
def copyfile(origin, destination):
|
|
973
|
+
logger.debug(f"Copying {origin} in {destination}")
|
|
974
|
+
shutil.copy(origin, destination)
|
|
975
|
+
|
|
976
|
+
def get_bindir_and_suffixe(self) -> Tuple[str, str, str]:
|
|
977
|
+
suffixes = {
|
|
978
|
+
"Windows": "dll",
|
|
979
|
+
"Linux": "so",
|
|
980
|
+
"Darwin": "dylib"
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
origin_bindirs = {
|
|
984
|
+
"Windows": "win64",
|
|
985
|
+
"Linux": "linux64",
|
|
986
|
+
"Darwin": "darwin64"
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if self.fmi_version == 3:
|
|
990
|
+
target_bindirs = {
|
|
991
|
+
"Windows": "x86_64-windows",
|
|
992
|
+
"Linux": "x86_64-linux",
|
|
993
|
+
"Darwin": "aarch64-darwin"
|
|
994
|
+
}
|
|
995
|
+
else:
|
|
996
|
+
target_bindirs = origin_bindirs
|
|
997
|
+
|
|
998
|
+
os_name = platform.system()
|
|
999
|
+
try:
|
|
1000
|
+
return origin_bindirs[os_name], suffixes[os_name], target_bindirs[os_name]
|
|
1001
|
+
except KeyError:
|
|
1002
|
+
raise FMUContainerError(f"OS '{os_name}' is not supported.")
|
|
1003
|
+
|
|
1004
|
+
def make_fmu_skeleton(self, base_directory: Path) -> Path:
|
|
1005
|
+
logger.debug(f"Initialize directory '{base_directory}'")
|
|
1006
|
+
|
|
1007
|
+
origin = Path(__file__).parent / "resources"
|
|
1008
|
+
resources_directory = base_directory / "resources"
|
|
1009
|
+
documentation_directory = base_directory / "documentation"
|
|
1010
|
+
binaries_directory = base_directory / "binaries"
|
|
1011
|
+
|
|
1012
|
+
base_directory.mkdir(exist_ok=True)
|
|
1013
|
+
resources_directory.mkdir(exist_ok=True)
|
|
1014
|
+
binaries_directory.mkdir(exist_ok=True)
|
|
1015
|
+
documentation_directory.mkdir(exist_ok=True)
|
|
1016
|
+
|
|
1017
|
+
if self.description_pathname:
|
|
1018
|
+
self.copyfile(self.description_pathname, documentation_directory)
|
|
1019
|
+
|
|
1020
|
+
self.copyfile(origin / "model.png", base_directory)
|
|
1021
|
+
|
|
1022
|
+
origin_bindir, suffixe, target_bindir = self.get_bindir_and_suffixe()
|
|
1023
|
+
|
|
1024
|
+
library_filename = origin / origin_bindir / f"container.{suffixe}"
|
|
1025
|
+
if not library_filename.is_file():
|
|
1026
|
+
raise FMUContainerError(f"File {library_filename} not found")
|
|
1027
|
+
binary_directory = binaries_directory / target_bindir
|
|
1028
|
+
binary_directory.mkdir(exist_ok=True)
|
|
1029
|
+
self.copyfile(library_filename, binary_directory / f"{self.identifier}.{suffixe}")
|
|
1030
|
+
|
|
1031
|
+
for i, fmu in enumerate(self.involved_fmu.values()):
|
|
1032
|
+
shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
|
|
1033
|
+
self.long_path(resources_directory / f"{i:02x}"), dirs_exist_ok=True)
|
|
1034
|
+
|
|
1035
|
+
return resources_directory
|
|
1036
|
+
|
|
1037
|
+
def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
|
|
1038
|
+
logger.debug(f"Zipping directory '{base_directory}' => '{fmu_filename}'")
|
|
1039
|
+
zip_directory = self.long_path(str(base_directory.absolute()))
|
|
1040
|
+
offset = len(zip_directory) + 1
|
|
1041
|
+
with zipfile.ZipFile(self.fmu_directory / fmu_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
1042
|
+
def add_file(directory: Path):
|
|
1043
|
+
for entry in directory.iterdir():
|
|
1044
|
+
if entry.is_dir():
|
|
1045
|
+
add_file(directory / entry)
|
|
1046
|
+
elif entry.is_file:
|
|
1047
|
+
zip_file.write(str(entry), str(entry)[offset:])
|
|
1048
|
+
|
|
1049
|
+
add_file(Path(zip_directory))
|
|
1050
|
+
logger.info(f"'{fmu_filename}' is available.")
|
|
1051
|
+
|
|
1052
|
+
def make_fmu_cleanup(self, base_directory: Path):
|
|
1053
|
+
logger.debug(f"Delete directory '{base_directory}'")
|
|
1054
|
+
shutil.rmtree(self.long_path(base_directory))
|