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.
Files changed (51) hide show
  1. fmu_manipulation_toolbox/__init__.py +0 -1
  2. fmu_manipulation_toolbox/__main__.py +1 -1
  3. fmu_manipulation_toolbox/__version__.py +1 -1
  4. fmu_manipulation_toolbox/assembly.py +22 -13
  5. fmu_manipulation_toolbox/checker.py +16 -11
  6. fmu_manipulation_toolbox/cli/__init__.py +0 -0
  7. fmu_manipulation_toolbox/cli/fmucontainer.py +105 -0
  8. fmu_manipulation_toolbox/cli/fmusplit.py +48 -0
  9. fmu_manipulation_toolbox/cli/fmutool.py +127 -0
  10. fmu_manipulation_toolbox/cli/utils.py +36 -0
  11. fmu_manipulation_toolbox/container.py +1054 -0
  12. fmu_manipulation_toolbox/gui.py +48 -56
  13. fmu_manipulation_toolbox/gui_style.py +8 -0
  14. fmu_manipulation_toolbox/help.py +3 -0
  15. fmu_manipulation_toolbox/operations.py +577 -0
  16. fmu_manipulation_toolbox/remoting.py +107 -0
  17. fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
  18. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Annotation.xsd +51 -0
  19. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3AttributeGroups.xsd +119 -0
  20. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3BuildDescription.xsd +117 -0
  21. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3InterfaceType.xsd +80 -0
  22. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3LayeredStandardManifest.xsd +93 -0
  23. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3ModelDescription.xsd +131 -0
  24. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Terminal.xsd +87 -0
  25. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3TerminalsAndIcons.xsd +84 -0
  26. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Type.xsd +207 -0
  27. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Unit.xsd +69 -0
  28. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Variable.xsd +413 -0
  29. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3VariableDependency.xsd +64 -0
  30. fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  31. fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  32. fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  33. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  34. fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
  35. fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  36. fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  37. fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  38. fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  39. fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  40. fmu_manipulation_toolbox/split.py +331 -0
  41. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/METADATA +1 -1
  42. fmu_manipulation_toolbox-1.9.dist-info/RECORD +71 -0
  43. fmu_manipulation_toolbox-1.9.dist-info/entry_points.txt +7 -0
  44. fmu_manipulation_toolbox/cli.py +0 -235
  45. fmu_manipulation_toolbox/fmu_container.py +0 -753
  46. fmu_manipulation_toolbox/fmu_operations.py +0 -489
  47. fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/RECORD +0 -52
  48. fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/entry_points.txt +0 -3
  49. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/WHEEL +0 -0
  50. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/licenses/LICENSE.txt +0 -0
  51. {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))