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.
Files changed (32) hide show
  1. fmu_manipulation_toolbox/__main__.py +2 -2
  2. fmu_manipulation_toolbox/__version__.py +1 -1
  3. fmu_manipulation_toolbox/assembly.py +12 -8
  4. fmu_manipulation_toolbox/checker.py +4 -2
  5. fmu_manipulation_toolbox/cli/fmucontainer.py +27 -19
  6. fmu_manipulation_toolbox/cli/fmusplit.py +5 -2
  7. fmu_manipulation_toolbox/cli/fmutool.py +8 -3
  8. fmu_manipulation_toolbox/cli/utils.py +11 -1
  9. fmu_manipulation_toolbox/container.py +292 -82
  10. fmu_manipulation_toolbox/ls.py +35 -0
  11. fmu_manipulation_toolbox/operations.py +3 -2
  12. fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
  13. fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  14. fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  15. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  16. fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  17. fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  18. fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  19. fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  20. fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  21. fmu_manipulation_toolbox/split.py +59 -3
  22. fmu_manipulation_toolbox/terminals.py +137 -0
  23. fmu_manipulation_toolbox/version.py +1 -1
  24. fmu_manipulation_toolbox-1.9.2b2.dist-info/METADATA +42 -0
  25. {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/RECORD +29 -29
  26. {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/entry_points.txt +1 -1
  27. {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/licenses/LICENSE.txt +1 -1
  28. fmu_manipulation_toolbox/gui.py +0 -749
  29. fmu_manipulation_toolbox/gui_style.py +0 -252
  30. fmu_manipulation_toolbox-1.9.1.3.dist-info/METADATA +0 -30
  31. {fmu_manipulation_toolbox-1.9.1.3.dist-info → fmu_manipulation_toolbox-1.9.2b2.dist-info}/WHEEL +0 -0
  32. {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", "continuous")
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
- else:
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 == "3.0": # TODO: handle 3.x cases
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
- return f"FMU '{self.name}' ({len(self.ports)} variables, ts={self.step_size}s)"
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
- raise FMUContainerError(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT")
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.port.type_name:
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
- link.vr = self.add_vr(link.cport_from, local=True)
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.debug(f"Adding FMU #{len(self.involved_fmu)}: {fmu}")
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
- cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
568
- try:
569
- local = self.links[cport_from]
570
- except KeyError:
571
- local = Link(cport_from)
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
- cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
574
- local.add_target(cport_to) # Causality is check in the add() function
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 in ('Real', 'Float64', 'Float32'):
759
+ if cport.port.type_name.startswith('real'):
586
760
  value = float(value)
587
- elif cport.port.type_name in ('Integer', 'Int8', 'UInt8', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64'):
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 == 'Boolean':
763
+ elif cport.port.type_name.startswith('boolean'):
590
764
  value = int(bool(value))
591
- else:
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 '{cport.port.type_name}' format.")
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
- port_local_def = link.cport_from.port.xml(link.vr, name=link.name, causality='local',
793
- fmi_version=self.fmi_version)
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]] = {} # Container's INPUT
878
- outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
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
- inputs_fmu_per_type[type_name] = {}
892
- start_values_fmu_per_type[type_name] = {}
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
- for fmu in self.involved_fmu.values():
896
- inputs_fmu_per_type[type_name][fmu.name] = {}
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
- start_values_fmu_per_type[input_port.port.type_name][input_port.fmu.name][input_port] = value
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
- local_per_type[link.cport_from.port.type_name].append(link.vr)
912
- outputs_fmu_per_type[link.cport_from.port.type_name][link.cport_from.fmu.name][link.cport_from] = link.vr
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 cport_to.port.type_name == link.cport_from.port.type_name:
915
- inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = link.vr
916
- else:
917
- local_per_type[cport_to.port.type_name].append(link.vr_converted[cport_to.port.type_name])
918
- links_per_fmu.setdefault(link.cport_from.fmu.name, []).append(link)
919
- inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = link.vr_converted[cport_to.port.type_name]
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
- for type_name in EmbeddedFMUPort.ALL_TYPES:
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)