fmu-manipulation-toolbox 1.9.1.3__py3-none-any.whl → 1.9.2b2__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/__main__.py +2 -2
- fmu_manipulation_toolbox/__version__.py +1 -1
- fmu_manipulation_toolbox/assembly.py +12 -8
- fmu_manipulation_toolbox/checker.py +4 -2
- fmu_manipulation_toolbox/cli/fmucontainer.py +27 -19
- fmu_manipulation_toolbox/cli/fmusplit.py +5 -2
- fmu_manipulation_toolbox/cli/fmutool.py +8 -3
- fmu_manipulation_toolbox/cli/utils.py +11 -1
- fmu_manipulation_toolbox/container.py +292 -82
- fmu_manipulation_toolbox/ls.py +35 -0
- fmu_manipulation_toolbox/operations.py +3 -2
- fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
- fmu_manipulation_toolbox/resources/linux32/client_sm.so +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/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 +59 -3
- fmu_manipulation_toolbox/terminals.py +137 -0
- fmu_manipulation_toolbox/version.py +1 -1
- fmu_manipulation_toolbox-1.9.2b2.dist-info/METADATA +42 -0
- {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/RECORD +29 -29
- {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/entry_points.txt +1 -1
- {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/licenses/LICENSE.txt +1 -1
- fmu_manipulation_toolbox/gui.py +0 -749
- fmu_manipulation_toolbox/gui_style.py +0 -252
- fmu_manipulation_toolbox-1.9.1.3.dist-info/METADATA +0 -30
- {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/WHEEL +0 -0
- {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/top_level.txt +0 -0
|
@@ -6,11 +6,14 @@ import shutil
|
|
|
6
6
|
import uuid
|
|
7
7
|
import platform
|
|
8
8
|
import zipfile
|
|
9
|
+
from collections import defaultdict
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import *
|
|
12
13
|
|
|
14
|
+
from .ls import LayeredStandard
|
|
13
15
|
from .operations import FMU, OperationAbstract, FMUError, FMUPort
|
|
16
|
+
from .terminals import Terminals
|
|
14
17
|
from .version import __version__ as tool_version
|
|
15
18
|
|
|
16
19
|
|
|
@@ -37,7 +40,9 @@ class EmbeddedFMUPort:
|
|
|
37
40
|
'Int64': 'integer64',
|
|
38
41
|
'UInt64': 'uinteger64',
|
|
39
42
|
'String': 'string',
|
|
40
|
-
'Boolean': 'boolean1'
|
|
43
|
+
'Boolean': 'boolean1',
|
|
44
|
+
'Binary': 'binary',
|
|
45
|
+
'Clock': 'clock'
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -60,7 +65,9 @@ class EmbeddedFMUPort:
|
|
|
60
65
|
'integer64': 'Int64' ,
|
|
61
66
|
'uinteger64': 'UInt64' ,
|
|
62
67
|
'string': 'String' ,
|
|
63
|
-
'boolean1': 'Boolean'
|
|
68
|
+
'boolean1': 'Boolean',
|
|
69
|
+
'binary': 'Binary',
|
|
70
|
+
'clock': 'Clock'
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -68,12 +75,14 @@ class EmbeddedFMUPort:
|
|
|
68
75
|
"real64", "real32",
|
|
69
76
|
"integer8", "uinteger8", "integer16", "uinteger16", "integer32", "uinteger32", "integer64", "uinteger64",
|
|
70
77
|
"boolean", "boolean1",
|
|
71
|
-
"string"
|
|
78
|
+
"string",
|
|
79
|
+
"binary", "clock"
|
|
72
80
|
)
|
|
73
81
|
|
|
74
82
|
def __init__(self, fmi_type, attrs: Union[FMUPort, Dict[str, str]], fmi_version=0):
|
|
75
83
|
self.causality = attrs.get("causality", "local")
|
|
76
|
-
self.variability = attrs.get("variability",
|
|
84
|
+
self.variability = attrs.get("variability", None)
|
|
85
|
+
self.interval_variability = attrs.get("intervalVariability", None)
|
|
77
86
|
self.name = attrs["name"]
|
|
78
87
|
self.vr = int(attrs["valueReference"])
|
|
79
88
|
self.description = attrs.get("description", None)
|
|
@@ -85,6 +94,8 @@ class EmbeddedFMUPort:
|
|
|
85
94
|
|
|
86
95
|
self.start_value = attrs.get("start", None)
|
|
87
96
|
self.initial = attrs.get("initial", None)
|
|
97
|
+
self.clock = attrs.get("clocks", None)
|
|
98
|
+
|
|
88
99
|
|
|
89
100
|
def xml(self, vr: int, name=None, causality=None, start=None, fmi_version=2) -> str:
|
|
90
101
|
if name is None:
|
|
@@ -93,13 +104,15 @@ class EmbeddedFMUPort:
|
|
|
93
104
|
causality = self.causality
|
|
94
105
|
if start is None:
|
|
95
106
|
start = self.start_value
|
|
107
|
+
if start is None and self.type_name == "binary" and self.initial == "exact":
|
|
108
|
+
start = ""
|
|
96
109
|
if self.variability is None:
|
|
97
110
|
self.variability = "continuous" if "real" in self.type_name else "discrete"
|
|
98
111
|
|
|
99
112
|
try:
|
|
100
113
|
fmi_type = self.CONTAINER_TO_FMI[fmi_version][self.type_name]
|
|
101
114
|
except KeyError:
|
|
102
|
-
logger.error(f"Cannot expose '{name}' because type '{self.type_name}' is not compatible "
|
|
115
|
+
logger.error(f"Cannot expose ({causality}) '{name}' because type '{self.type_name}' is not compatible "
|
|
103
116
|
f"with FMI-{fmi_version}.0")
|
|
104
117
|
return ""
|
|
105
118
|
|
|
@@ -124,9 +137,10 @@ class EmbeddedFMUPort:
|
|
|
124
137
|
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
125
138
|
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
126
139
|
return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
|
|
127
|
-
|
|
140
|
+
|
|
141
|
+
elif fmi_version == 3:
|
|
128
142
|
if fmi_type in ('String', 'Binary'):
|
|
129
|
-
if start:
|
|
143
|
+
if start is not None:
|
|
130
144
|
child_str = f'<Start value="{start}"/>'
|
|
131
145
|
else:
|
|
132
146
|
child_str = ''
|
|
@@ -149,12 +163,16 @@ class EmbeddedFMUPort:
|
|
|
149
163
|
"variability": self.variability,
|
|
150
164
|
"initial": self.initial,
|
|
151
165
|
"description": self.description,
|
|
152
|
-
"start": start
|
|
166
|
+
"start": start,
|
|
167
|
+
"intervalVariability": self.interval_variability
|
|
153
168
|
}
|
|
154
169
|
filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
|
|
155
170
|
scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
|
|
156
171
|
|
|
157
172
|
return f'<{fmi_type} {scalar_attrs_str}/>'
|
|
173
|
+
else:
|
|
174
|
+
logger.critical(f"Unknown version {fmi_version}. BUG?")
|
|
175
|
+
return ''
|
|
158
176
|
|
|
159
177
|
|
|
160
178
|
class EmbeddedFMU(OperationAbstract):
|
|
@@ -167,6 +185,10 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
167
185
|
self.name = Path(filename).name
|
|
168
186
|
self.id = Path(filename).stem.lower()
|
|
169
187
|
|
|
188
|
+
logger.debug(f"Analysing {self.name}")
|
|
189
|
+
self.terminals = Terminals(self.fmu.tmp_directory)
|
|
190
|
+
self.ls = LayeredStandard(self.fmu.tmp_directory)
|
|
191
|
+
|
|
170
192
|
self.step_size = None
|
|
171
193
|
self.start_time = None
|
|
172
194
|
self.stop_time = None
|
|
@@ -175,6 +197,7 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
175
197
|
self.fmi_version = None
|
|
176
198
|
self.ports: Dict[str, EmbeddedFMUPort] = {}
|
|
177
199
|
|
|
200
|
+
self.has_event_mode = False
|
|
178
201
|
self.capabilities: Dict[str, str] = {}
|
|
179
202
|
self.current_port = None # used during apply_operation()
|
|
180
203
|
|
|
@@ -187,12 +210,14 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
187
210
|
if fmi_version == "2.0":
|
|
188
211
|
self.guid = attrs['guid']
|
|
189
212
|
self.fmi_version = 2
|
|
190
|
-
if fmi_version
|
|
213
|
+
if fmi_version.startswith("3."):
|
|
191
214
|
self.guid = attrs['instantiationToken']
|
|
192
215
|
self.fmi_version = 3
|
|
193
216
|
|
|
194
217
|
def cosimulation_attrs(self, attrs: Dict[str, str]):
|
|
195
218
|
self.model_identifier = attrs['modelIdentifier']
|
|
219
|
+
if attrs.get("hasEventMode", "false") == "true":
|
|
220
|
+
self.has_event_mode = True
|
|
196
221
|
for capability in self.capability_list:
|
|
197
222
|
self.capabilities[capability] = attrs.get(capability, "false")
|
|
198
223
|
|
|
@@ -215,7 +240,12 @@ class EmbeddedFMU(OperationAbstract):
|
|
|
215
240
|
self.ports[port.name] = port
|
|
216
241
|
|
|
217
242
|
def __repr__(self):
|
|
218
|
-
|
|
243
|
+
properties = f"{len(self.ports)} variables, ts={self.step_size}s"
|
|
244
|
+
if len(self.terminals) > 0:
|
|
245
|
+
properties += f", {len(self.terminals)} terminals"
|
|
246
|
+
if len(self.ls) > 0:
|
|
247
|
+
properties += f", {self.ls}"
|
|
248
|
+
return f"'{self.name}' ({properties})"
|
|
219
249
|
|
|
220
250
|
|
|
221
251
|
class FMUContainerError(Exception):
|
|
@@ -315,13 +345,19 @@ class Link:
|
|
|
315
345
|
self.vr_converted: Dict[str, Optional[int]] = {}
|
|
316
346
|
|
|
317
347
|
if not cport_from.port.causality == "output":
|
|
318
|
-
|
|
348
|
+
if cport_from.port.type_name == "clock":
|
|
349
|
+
# LS-BUS allows to connected to input clocks.
|
|
350
|
+
self.cport_from = None
|
|
351
|
+
self.add_target(cport_from)
|
|
352
|
+
else:
|
|
353
|
+
raise FMUContainerError(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT")
|
|
319
354
|
|
|
320
355
|
def add_target(self, cport_to: ContainerPort):
|
|
321
356
|
if not cport_to.port.causality == "input":
|
|
322
357
|
raise FMUContainerError(f"{cport_to} is {cport_to.port.causality} instead of INPUT")
|
|
323
358
|
|
|
324
|
-
if cport_to.port.type_name == self.cport_from
|
|
359
|
+
if (cport_to.port.type_name == "clock" and self.cport_from is None or
|
|
360
|
+
cport_to.port.type_name == self.cport_from.port.type_name):
|
|
325
361
|
self.cport_to_list.append(cport_to)
|
|
326
362
|
elif self.get_conversion(cport_to):
|
|
327
363
|
self.cport_to_list.append(cport_to)
|
|
@@ -345,6 +381,7 @@ class ValueReferenceTable:
|
|
|
345
381
|
self.vr_table:Dict[str, int] = {}
|
|
346
382
|
self.masks: Dict[str, int] = {}
|
|
347
383
|
self.nb_local_variable:Dict[str, int] = {}
|
|
384
|
+
self.local_clock = {}
|
|
348
385
|
for i, type_name in enumerate(EmbeddedFMUPort.ALL_TYPES):
|
|
349
386
|
self.vr_table[type_name] = 0
|
|
350
387
|
self.masks[type_name] = i << 24
|
|
@@ -365,13 +402,28 @@ class ValueReferenceTable:
|
|
|
365
402
|
return vr | self.masks[type_name]
|
|
366
403
|
|
|
367
404
|
def set_link_vr(self, link: Link):
|
|
368
|
-
|
|
405
|
+
if link.cport_from is None:
|
|
406
|
+
link.vr = self.add_vr("clock", local=True)
|
|
407
|
+
else:
|
|
408
|
+
link.vr = self.add_vr(link.cport_from, local=True)
|
|
409
|
+
if link.cport_from.port.type_name == "clock":
|
|
410
|
+
self.local_clock[(link.cport_from.fmu, link.cport_from.port.vr)] = link.vr
|
|
411
|
+
|
|
412
|
+
for cport_to in link.cport_to_list:
|
|
413
|
+
if cport_to.port.type_name == "clock":
|
|
414
|
+
self.local_clock[(cport_to.fmu, cport_to.port.vr)] = link.vr
|
|
415
|
+
|
|
369
416
|
for type_name in link.vr_converted.keys():
|
|
370
417
|
link.vr_converted[type_name] = self.add_vr(type_name, local=True)
|
|
371
418
|
|
|
419
|
+
def get_local_clock(self, cport: ContainerPort) -> int:
|
|
420
|
+
return self.local_clock[(cport.fmu, int(cport.port.clock))]
|
|
421
|
+
|
|
422
|
+
|
|
372
423
|
def nb_local(self, type_name: str) -> int:
|
|
373
424
|
return self.nb_local_variable[type_name]
|
|
374
425
|
|
|
426
|
+
|
|
375
427
|
class AutoWired:
|
|
376
428
|
def __init__(self):
|
|
377
429
|
self.rule_input = []
|
|
@@ -397,6 +449,102 @@ class AutoWired:
|
|
|
397
449
|
self.rule_link.append([from_fmu, from_port, to_fmu, to_port])
|
|
398
450
|
|
|
399
451
|
|
|
452
|
+
class FMUIOList:
|
|
453
|
+
def __init__(self, vr_table: ValueReferenceTable):
|
|
454
|
+
self.vr_table = vr_table
|
|
455
|
+
self.inputs = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) # [type][fmu][clock_vr][(fmu_vr, vr])
|
|
456
|
+
self.nb_clocked_inputs = defaultdict(lambda: defaultdict(lambda: 0))
|
|
457
|
+
self.outputs = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) # [type][fmu][clock_vr][(fmu_vr, vr])
|
|
458
|
+
self.nb_clocked_outputs = defaultdict(lambda: defaultdict(lambda: 0))
|
|
459
|
+
self.start_values = defaultdict(lambda: defaultdict(list)) # [type][fmu][(cport, value)]
|
|
460
|
+
|
|
461
|
+
def add_input(self, cport: ContainerPort, local_vr: int):
|
|
462
|
+
if cport.port.clock is None:
|
|
463
|
+
clock = None
|
|
464
|
+
else:
|
|
465
|
+
try:
|
|
466
|
+
clock = self.vr_table.get_local_clock(cport)
|
|
467
|
+
except KeyError:
|
|
468
|
+
logger.error(f"Cannot expose clocked input: {cport}")
|
|
469
|
+
return
|
|
470
|
+
self.nb_clocked_inputs[cport.port.type_name][cport.fmu.name] += 1
|
|
471
|
+
self.inputs[cport.port.type_name][cport.fmu.name][clock].append((cport.port.vr, local_vr))
|
|
472
|
+
|
|
473
|
+
def add_output(self, cport: ContainerPort, local_vr: int):
|
|
474
|
+
if cport.port.clock is None:
|
|
475
|
+
clock = None
|
|
476
|
+
else:
|
|
477
|
+
try:
|
|
478
|
+
clock = self.vr_table.get_local_clock(cport)
|
|
479
|
+
except KeyError:
|
|
480
|
+
logger.error(f"Cannot expose clocked output: {cport}")
|
|
481
|
+
return
|
|
482
|
+
self.nb_clocked_outputs[cport.port.type_name][cport.fmu.name] += 1
|
|
483
|
+
self.outputs[cport.port.type_name][cport.fmu.name][clock].append((cport.port.vr, local_vr))
|
|
484
|
+
|
|
485
|
+
def add_start_value(self, cport: ContainerPort, value: str):
|
|
486
|
+
reset = 1 if cport.port.causality == "input" else 0
|
|
487
|
+
self.start_values[cport.port.type_name][cport.fmu.name].append((cport.port.vr, reset, value))
|
|
488
|
+
|
|
489
|
+
def write_txt(self, fmu_name, txt_file):
|
|
490
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
491
|
+
print(f"# Inputs of {fmu_name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
492
|
+
print(len(self.inputs[type_name][fmu_name][None]), file=txt_file)
|
|
493
|
+
for fmu_vr, vr in self.inputs[type_name][fmu_name][None]:
|
|
494
|
+
print(f"{vr} {fmu_vr}", file=txt_file)
|
|
495
|
+
if not type_name == "clock":
|
|
496
|
+
print(f"# Clocked Inputs of {fmu_name} - {type_name}: <FMU_VR_CLOCK> <n> <VR> <FMU_VR>", file=txt_file)
|
|
497
|
+
print(f"{len(self.inputs[type_name][fmu_name])-1} {self.nb_clocked_inputs[type_name][fmu_name]}",
|
|
498
|
+
file=txt_file)
|
|
499
|
+
for clock, table in self.inputs[type_name][fmu_name].items():
|
|
500
|
+
if not clock is None:
|
|
501
|
+
s = " ".join([f"{vr} {fmu_vr}" for fmu_vr, vr in table])
|
|
502
|
+
print(f"{clock} {len(table)} {s}", file=txt_file)
|
|
503
|
+
|
|
504
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES[:-2]: # No start values for binary or clock
|
|
505
|
+
print(f"# Start values of {fmu_name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
|
|
506
|
+
print(len(self.start_values[type_name][fmu_name]), file=txt_file)
|
|
507
|
+
for vr, reset, value in self.start_values[type_name][fmu_name]:
|
|
508
|
+
print(f"{vr} {reset} {value}", file=txt_file)
|
|
509
|
+
|
|
510
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
511
|
+
print(f"# Outputs of {fmu_name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
512
|
+
print(len(self.outputs[type_name][fmu_name][None]), file=txt_file)
|
|
513
|
+
for fmu_vr, vr in self.outputs[type_name][fmu_name][None]:
|
|
514
|
+
print(f"{vr} {fmu_vr}", file=txt_file)
|
|
515
|
+
|
|
516
|
+
if not type_name == "clock":
|
|
517
|
+
print(f"# Clocked Outputs of {fmu_name} - {type_name}: <FMU_VR_CLOCK> <n> <VR> <FMU_VR>", file=txt_file)
|
|
518
|
+
print(f"{len(self.outputs[type_name][fmu_name])-1} {self.nb_clocked_outputs[type_name][fmu_name]}",
|
|
519
|
+
file=txt_file)
|
|
520
|
+
for clock, translation in self.outputs[type_name][fmu_name].items():
|
|
521
|
+
if not clock is None:
|
|
522
|
+
s = " ".join([f"{vr} {fmu_vr}" for fmu_vr, vr in translation])
|
|
523
|
+
print(f"{clock} {len(translation)} {s}", file=txt_file)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class ClockList:
|
|
527
|
+
def __init__(self, involved_fmu: OrderedDict[str, EmbeddedFMU]):
|
|
528
|
+
self.clocks_per_fmu: DefaultDict[int, List[Tuple[int, int]]] = defaultdict(list)
|
|
529
|
+
self.fmu_index: Dict[str, int] = {}
|
|
530
|
+
for i, fmu_name in enumerate(involved_fmu):
|
|
531
|
+
self.fmu_index[fmu_name] = i
|
|
532
|
+
|
|
533
|
+
def append(self, cport: ContainerPort, vr: int):
|
|
534
|
+
self.clocks_per_fmu[self.fmu_index[cport.fmu.name]].append((cport.port.vr, vr))
|
|
535
|
+
|
|
536
|
+
def write_txt(self, txt_file):
|
|
537
|
+
print(f"# importer CLOCKS: <FMU_INDEX> <NB> <FMU_VR> <VR> [<FMU_VR> <VR>]", file=txt_file)
|
|
538
|
+
nb_total_clocks = 0
|
|
539
|
+
for clocks in self.clocks_per_fmu.values():
|
|
540
|
+
nb_total_clocks += len(clocks)
|
|
541
|
+
|
|
542
|
+
print(f"{len(self.clocks_per_fmu)} {nb_total_clocks}", file=txt_file)
|
|
543
|
+
for index, clocks in self.clocks_per_fmu.items():
|
|
544
|
+
clocks_str = " ".join([f"{clock[0]} {clock[1]}" for clock in clocks])
|
|
545
|
+
print(f"{index} {len(clocks)} {clocks_str}", file=txt_file)
|
|
546
|
+
|
|
547
|
+
|
|
400
548
|
class FMUContainer:
|
|
401
549
|
HEADER_XML_2 = """<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
402
550
|
<fmiModelDescription
|
|
@@ -505,7 +653,7 @@ class FMUContainer:
|
|
|
505
653
|
logger.warning(f"Try to embed FMU-{fmu.fmi_version} into container FMI-{self.fmi_version}.")
|
|
506
654
|
self.involved_fmu[fmu.name] = fmu
|
|
507
655
|
|
|
508
|
-
logger.
|
|
656
|
+
logger.info(f"Involved FMU #{len(self.involved_fmu)}: {fmu}")
|
|
509
657
|
except (FMUContainerError, FMUError) as e:
|
|
510
658
|
raise FMUContainerError(f"Cannot load '{fmu_filename}': {e}")
|
|
511
659
|
|
|
@@ -564,34 +712,63 @@ class FMUContainer:
|
|
|
564
712
|
self.mark_ruled(cport_from, 'DROP')
|
|
565
713
|
|
|
566
714
|
def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
715
|
+
fmu_from = self.get_fmu(from_fmu_filename)
|
|
716
|
+
fmu_to = self.get_fmu(to_fmu_filename)
|
|
717
|
+
|
|
718
|
+
if from_port_name in fmu_from.terminals and to_port_name in fmu_to.terminals:
|
|
719
|
+
# TERMINAL Connection
|
|
720
|
+
terminal1 = fmu_from.terminals[from_port_name]
|
|
721
|
+
terminal2 = fmu_to.terminals[to_port_name]
|
|
722
|
+
if terminal1 == terminal2:
|
|
723
|
+
logger.debug(f"Plugging terminals: {terminal1} <-> {terminal2}")
|
|
724
|
+
for terminal1_port_name, terminal2_port_name in terminal1.connect(terminal2):
|
|
725
|
+
self.add_link_regular(fmu_from, terminal1_port_name, fmu_to, terminal2_port_name)
|
|
726
|
+
else:
|
|
727
|
+
logger.error(f"Cannot plug incompatible terminals: {terminal1} <-> {terminal2}")
|
|
728
|
+
else:
|
|
729
|
+
# REGULAR port connection
|
|
730
|
+
self.add_link_regular(fmu_from, from_port_name, fmu_to, to_port_name)
|
|
572
731
|
|
|
573
|
-
|
|
574
|
-
|
|
732
|
+
def add_link_regular(self, fmu_from: EmbeddedFMU, from_port_name: str, fmu_to: EmbeddedFMU, to_port_name: str):
|
|
733
|
+
cport_from = ContainerPort(fmu_from, from_port_name)
|
|
734
|
+
cport_to = ContainerPort(fmu_to, to_port_name)
|
|
735
|
+
|
|
736
|
+
if cport_to.port.causality == "output" and cport_from.port.causality == "input":
|
|
737
|
+
logger.debug("Invert link orientation")
|
|
738
|
+
tmp = cport_to
|
|
739
|
+
cport_to = cport_from
|
|
740
|
+
cport_from = tmp
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
local = self.links[cport_from]
|
|
744
|
+
except KeyError:
|
|
745
|
+
local = Link(cport_from)
|
|
746
|
+
self.links[cport_from] = local
|
|
747
|
+
|
|
748
|
+
local.add_target(cport_to) # Causality is check in the add() function
|
|
749
|
+
|
|
750
|
+
logger.debug(f"LINK: {cport_from} -> {cport_to}")
|
|
751
|
+
self.mark_ruled(cport_from, 'LINK')
|
|
752
|
+
self.mark_ruled(cport_to, 'LINK')
|
|
575
753
|
|
|
576
|
-
logger.debug(f"LINK: {cport_from} -> {cport_to}")
|
|
577
|
-
self.mark_ruled(cport_from, 'LINK')
|
|
578
|
-
self.mark_ruled(cport_to, 'LINK')
|
|
579
|
-
self.links[cport_from] = local
|
|
580
754
|
|
|
581
755
|
def add_start_value(self, fmu_filename: str, port_name: str, value: str):
|
|
582
756
|
cport = ContainerPort(self.get_fmu(fmu_filename), port_name)
|
|
583
757
|
|
|
584
758
|
try:
|
|
585
|
-
if cport.port.type_name
|
|
759
|
+
if cport.port.type_name.startswith('real'):
|
|
586
760
|
value = float(value)
|
|
587
|
-
elif cport.port.type_name
|
|
761
|
+
elif cport.port.type_name.startswith('integer') or cport.port.type_name.startswith('uinteger'):
|
|
588
762
|
value = int(value)
|
|
589
|
-
elif cport.port.type_name
|
|
763
|
+
elif cport.port.type_name.startswith('boolean'):
|
|
590
764
|
value = int(bool(value))
|
|
591
|
-
|
|
765
|
+
elif cport.port.type_name == 'String':
|
|
592
766
|
value = value
|
|
767
|
+
else:
|
|
768
|
+
logger.error(f"Start value cannot be set on '{cport.port.type_name}'")
|
|
769
|
+
return
|
|
593
770
|
except ValueError:
|
|
594
|
-
raise FMUContainerError(f"Start value is not conforming to
|
|
771
|
+
raise FMUContainerError(f"Start value is not conforming to {cport.port.type_name} format.")
|
|
595
772
|
|
|
596
773
|
self.start_values[cport] = value
|
|
597
774
|
|
|
@@ -658,6 +835,14 @@ class FMUContainer:
|
|
|
658
835
|
if fmu.step_size and fmu.capabilities["canHandleVariableCommunicationStepSize"] == "false":
|
|
659
836
|
freq_set.add(int(1.0/fmu.step_size))
|
|
660
837
|
|
|
838
|
+
if not freq_set:
|
|
839
|
+
# all involved FMUs can Handle Variable Communication StepSize
|
|
840
|
+
step_size_max = 0
|
|
841
|
+
for fmu in self.involved_fmu.values():
|
|
842
|
+
if fmu.step_size > step_size_max:
|
|
843
|
+
step_size_max = fmu.step_size
|
|
844
|
+
return step_size_max
|
|
845
|
+
|
|
661
846
|
common_freq = math.gcd(*freq_set)
|
|
662
847
|
try:
|
|
663
848
|
step_size = 1.0 / float(common_freq)
|
|
@@ -687,7 +872,7 @@ class FMUContainer:
|
|
|
687
872
|
logger.warning(f"{cport} is not connected")
|
|
688
873
|
|
|
689
874
|
def make_fmu(self, fmu_filename: Union[str, Path], step_size: Optional[float] = None, debug=False, mt=False,
|
|
690
|
-
profiling=False, sequential=False, ts_multiplier=False):
|
|
875
|
+
profiling=False, sequential=False, ts_multiplier=False, datalog=False):
|
|
691
876
|
if isinstance(fmu_filename, str):
|
|
692
877
|
fmu_filename = Path(fmu_filename)
|
|
693
878
|
|
|
@@ -700,11 +885,16 @@ class FMUContainer:
|
|
|
700
885
|
|
|
701
886
|
base_directory = self.fmu_directory / fmu_filename.with_suffix('')
|
|
702
887
|
resources_directory = self.make_fmu_skeleton(base_directory)
|
|
888
|
+
|
|
703
889
|
with open(base_directory / "modelDescription.xml", "wt") as xml_file:
|
|
704
890
|
self.make_fmu_xml(xml_file, step_size, profiling, ts_multiplier)
|
|
705
891
|
with open(resources_directory / "container.txt", "wt") as txt_file:
|
|
706
892
|
self.make_fmu_txt(txt_file, step_size, mt, profiling, sequential)
|
|
707
893
|
|
|
894
|
+
if datalog:
|
|
895
|
+
with open(resources_directory / "datalog.txt", "wt") as datalog_file:
|
|
896
|
+
self.make_datalog(datalog_file)
|
|
897
|
+
|
|
708
898
|
self.make_fmu_package(base_directory, fmu_filename)
|
|
709
899
|
if not debug:
|
|
710
900
|
self.make_fmu_cleanup(base_directory)
|
|
@@ -787,10 +977,20 @@ class FMUContainer:
|
|
|
787
977
|
index_offset = 2 # index of output ports. Start at 2 to skip "time" port
|
|
788
978
|
|
|
789
979
|
# Local variable should be first to ensure to attribute them the lowest VR.
|
|
980
|
+
nb_clocks = 0
|
|
790
981
|
for link in self.links.values():
|
|
791
982
|
self.vr_table.set_link_vr(link)
|
|
792
|
-
|
|
793
|
-
|
|
983
|
+
if link.cport_from:
|
|
984
|
+
port_local_def = link.cport_from.port.xml(link.vr, name=link.name, causality='local',
|
|
985
|
+
fmi_version=self.fmi_version)
|
|
986
|
+
else:
|
|
987
|
+
# LS-BUS allow Clock generated by fmi-importer
|
|
988
|
+
port = EmbeddedFMUPort("Clock",
|
|
989
|
+
{"name": "", "valueReference": -1, "intervalVariability": "triggered"},
|
|
990
|
+
fmi_version=3)
|
|
991
|
+
port_local_def = port.xml(link.vr, name=f"container.clock{nb_clocks}", causality='local', fmi_version=self.fmi_version)
|
|
992
|
+
nb_clocks += 1
|
|
993
|
+
|
|
794
994
|
if port_local_def:
|
|
795
995
|
print(f" {port_local_def}", file=xml_file)
|
|
796
996
|
index_offset += 1
|
|
@@ -858,6 +1058,7 @@ class FMUContainer:
|
|
|
858
1058
|
"</fmiModelDescription>")
|
|
859
1059
|
|
|
860
1060
|
def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
|
|
1061
|
+
print("# Version 3", file=txt_file)
|
|
861
1062
|
print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
|
|
862
1063
|
flags = [ str(int(flag == True)) for flag in (mt, profiling, sequential)]
|
|
863
1064
|
print(" ".join(flags), file=txt_file)
|
|
@@ -868,56 +1069,61 @@ class FMUContainer:
|
|
|
868
1069
|
print(f"{len(self.involved_fmu)}", file=txt_file)
|
|
869
1070
|
fmu_rank: Dict[str, int] = {}
|
|
870
1071
|
for i, fmu in enumerate(self.involved_fmu.values()):
|
|
871
|
-
print(f"{fmu.name} {fmu.fmi_version}", file=txt_file)
|
|
1072
|
+
print(f"{fmu.name} {fmu.fmi_version} {int(fmu.has_event_mode)}", file=txt_file)
|
|
872
1073
|
print(f"{fmu.model_identifier}", file=txt_file)
|
|
873
1074
|
print(f"{fmu.guid}", file=txt_file)
|
|
874
1075
|
fmu_rank[fmu.name] = i
|
|
875
1076
|
|
|
876
1077
|
# Prepare data structure
|
|
877
|
-
inputs_per_type: Dict[str, List[ContainerInput]] =
|
|
878
|
-
outputs_per_type: Dict[str, List[ContainerPort]] =
|
|
879
|
-
|
|
880
|
-
inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
|
|
881
|
-
start_values_fmu_per_type = {}
|
|
882
|
-
outputs_fmu_per_type = {}
|
|
883
|
-
local_per_type: Dict[str, List[int]] = {}
|
|
884
|
-
links_per_fmu: Dict[str, List[Link]] = {}
|
|
885
|
-
|
|
886
|
-
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
887
|
-
inputs_per_type[type_name] = []
|
|
888
|
-
outputs_per_type[type_name] = []
|
|
889
|
-
local_per_type[type_name] = []
|
|
1078
|
+
inputs_per_type: Dict[str, List[ContainerInput]] = defaultdict(list) # Container's INPUT
|
|
1079
|
+
outputs_per_type: Dict[str, List[ContainerPort]] = defaultdict(list) # Container's OUTPUT
|
|
890
1080
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
outputs_fmu_per_type[type_name] = {}
|
|
1081
|
+
fmu_io_list = FMUIOList(self.vr_table)
|
|
1082
|
+
clock_list = ClockList(self.involved_fmu)
|
|
894
1083
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
start_values_fmu_per_type[type_name][fmu.name] = {}
|
|
898
|
-
outputs_fmu_per_type[type_name][fmu.name] = {}
|
|
1084
|
+
local_per_type: Dict[str, List[int]] = defaultdict(list)
|
|
1085
|
+
links_per_fmu: Dict[str, List[Link]] = defaultdict(list)
|
|
899
1086
|
|
|
900
1087
|
# Fill data structure
|
|
901
1088
|
# Inputs
|
|
902
1089
|
for input_port_name, input_port in self.inputs.items():
|
|
903
1090
|
inputs_per_type[input_port.type_name].append(input_port)
|
|
1091
|
+
|
|
1092
|
+
# Start values
|
|
904
1093
|
for input_port, value in self.start_values.items():
|
|
905
|
-
|
|
1094
|
+
fmu_io_list.add_start_value(input_port, value)
|
|
1095
|
+
|
|
906
1096
|
# Outputs
|
|
907
1097
|
for output_port_name, output_port in self.outputs.items():
|
|
908
1098
|
outputs_per_type[output_port.port.type_name].append(output_port)
|
|
1099
|
+
|
|
909
1100
|
# Links
|
|
910
1101
|
for link in self.links.values():
|
|
911
|
-
|
|
912
|
-
|
|
1102
|
+
# FMU Outputs
|
|
1103
|
+
if link.cport_from:
|
|
1104
|
+
local_per_type[link.cport_from.port.type_name].append(link.vr)
|
|
1105
|
+
fmu_io_list.add_output(link.cport_from, link.vr)
|
|
1106
|
+
else:
|
|
1107
|
+
local_per_type["clock"].append(link.vr)
|
|
1108
|
+
for cport_to in link.cport_to_list:
|
|
1109
|
+
if cport_to.fmu.ls.is_bus:
|
|
1110
|
+
logger.info(f"LS-BUS: importer scheduling for '{cport_to.fmu.name}' '{cport_to.port.name}' (clock={cport_to.port.vr}, {link.vr})")
|
|
1111
|
+
clock_list.append(cport_to, link.vr)
|
|
1112
|
+
break
|
|
1113
|
+
|
|
1114
|
+
# FMU Inputs
|
|
913
1115
|
for cport_to in link.cport_to_list:
|
|
914
|
-
if
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1116
|
+
if link.cport_from is not None or not cport_to.fmu.ls.is_bus:
|
|
1117
|
+
# LS-BUS allows, importer to feed clock signal. In this case, cport_from is None
|
|
1118
|
+
# FMU will be fed directly by importer, no need to add inpunt link!
|
|
1119
|
+
if link.cport_from is None or cport_to.port.type_name == link.cport_from.port.type_name:
|
|
1120
|
+
local_vr = link.vr
|
|
1121
|
+
else:
|
|
1122
|
+
local_per_type[cport_to.port.type_name].append(link.vr_converted[cport_to.port.type_name])
|
|
1123
|
+
links_per_fmu[link.cport_from.fmu.name].append(link)
|
|
1124
|
+
local_vr = link.vr_converted[cport_to.port.type_name]
|
|
1125
|
+
|
|
1126
|
+
fmu_io_list.add_input(cport_to, local_vr)
|
|
921
1127
|
|
|
922
1128
|
print(f"# NB local variables:", ", ".join(EmbeddedFMUPort.ALL_TYPES), file=txt_file)
|
|
923
1129
|
nb_local = [f"{self.vr_table.nb_local(type_name)}" for type_name in EmbeddedFMUPort.ALL_TYPES]
|
|
@@ -952,24 +1158,7 @@ class FMUContainer:
|
|
|
952
1158
|
|
|
953
1159
|
# LINKS
|
|
954
1160
|
for fmu in self.involved_fmu.values():
|
|
955
|
-
|
|
956
|
-
print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
957
|
-
print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
958
|
-
for input_port, vr in inputs_fmu_per_type[type_name][fmu.name].items():
|
|
959
|
-
print(f"{vr} {input_port.port.vr}", file=txt_file)
|
|
960
|
-
|
|
961
|
-
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
962
|
-
print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
|
|
963
|
-
print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
964
|
-
for input_port, value in start_values_fmu_per_type[type_name][fmu.name].items():
|
|
965
|
-
reset = 1 if input_port.port.causality == "input" else 0
|
|
966
|
-
print(f"{input_port.port.vr} {reset} {value}", file=txt_file)
|
|
967
|
-
|
|
968
|
-
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
969
|
-
print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
|
|
970
|
-
print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
|
|
971
|
-
for output_port, vr in outputs_fmu_per_type[type_name][fmu.name].items():
|
|
972
|
-
print(f"{vr} {output_port.port.vr}", file=txt_file)
|
|
1161
|
+
fmu_io_list.write_txt(fmu.name, txt_file)
|
|
973
1162
|
|
|
974
1163
|
print(f"# Conversion table of {fmu.name}: <VR_FROM> <VR_TO> <CONVERSION>", file=txt_file)
|
|
975
1164
|
try:
|
|
@@ -986,6 +1175,27 @@ class FMUContainer:
|
|
|
986
1175
|
except KeyError:
|
|
987
1176
|
print("0", file=txt_file)
|
|
988
1177
|
|
|
1178
|
+
# CLOCKS
|
|
1179
|
+
clock_list.write_txt(txt_file)
|
|
1180
|
+
|
|
1181
|
+
def make_datalog(self, datalog_file):
|
|
1182
|
+
print(f"# Datalog filename")
|
|
1183
|
+
print(f"{self.identifier}-datalog.csv", file=datalog_file)
|
|
1184
|
+
|
|
1185
|
+
ports = defaultdict(list)
|
|
1186
|
+
for input_port_name, input_port in self.inputs.items():
|
|
1187
|
+
ports[input_port.type_name].append((input_port.vr, input_port_name))
|
|
1188
|
+
for output_port_name, output_port in self.outputs.items():
|
|
1189
|
+
ports[output_port.port.type_name].append((output_port.vr, output_port_name))
|
|
1190
|
+
for link in self.links.values():
|
|
1191
|
+
ports[link.cport_from.port.type_name].append((link.vr, link.name))
|
|
1192
|
+
|
|
1193
|
+
for type_name in EmbeddedFMUPort.ALL_TYPES:
|
|
1194
|
+
print(f"# {type_name}: <VR> <NAME>" , file=datalog_file)
|
|
1195
|
+
print(f"{len(ports[type_name])}", file=datalog_file)
|
|
1196
|
+
for port in ports[type_name]:
|
|
1197
|
+
print(f"{port[0]} {port[1]}", file=datalog_file)
|
|
1198
|
+
|
|
989
1199
|
@staticmethod
|
|
990
1200
|
def long_path(path: Union[str, Path]) -> str:
|
|
991
1201
|
# https://stackoverflow.com/questions/14075465/copy-a-file-with-a-too-long-path-to-another-directory-in-python
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import *
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("fmu_manipulation_toolbox")
|
|
8
|
+
|
|
9
|
+
class LayeredStandard:
|
|
10
|
+
def __init__(self, directory: Union[Path, str]):
|
|
11
|
+
self.is_bus = False
|
|
12
|
+
self.standards: List[str] = []
|
|
13
|
+
|
|
14
|
+
if isinstance(directory, Path):
|
|
15
|
+
self.directory = directory
|
|
16
|
+
else:
|
|
17
|
+
self.directory = Path(directory)
|
|
18
|
+
|
|
19
|
+
self.parse_lsbus()
|
|
20
|
+
|
|
21
|
+
def parse_lsbus(self):
|
|
22
|
+
filename = self.directory / "extra" / "org.fmi-standard.fmi-ls-bus" / "fmi-ls-manifest.xml"
|
|
23
|
+
if filename.exists():
|
|
24
|
+
xml = ET.parse(filename)
|
|
25
|
+
root = xml.getroot()
|
|
26
|
+
root.get("isBusSimulationFMU", "")
|
|
27
|
+
self.is_bus = root.get("isBusSimulationFMU") == "true"
|
|
28
|
+
|
|
29
|
+
self.standards.append("LS-BUS")
|
|
30
|
+
|
|
31
|
+
def __len__(self):
|
|
32
|
+
return len(self.standards)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return ", ".join(self.standards)
|