fmu-manipulation-toolbox 1.8.4.3b0__py3-none-any.whl → 1.9rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
1
  import logging
2
+ import getpass
2
3
  import os
3
4
  import shutil
4
5
  import uuid
6
+ import platform
5
7
  import zipfile
6
8
  from datetime import datetime
7
9
  from pathlib import Path
@@ -14,22 +16,101 @@ from .version import __version__ as tool_version
14
16
  logger = logging.getLogger("fmu_manipulation_toolbox")
15
17
 
16
18
 
17
- class FMUPort:
18
- def __init__(self, attrs: Dict[str, str]):
19
- self.causality = attrs.pop("causality", "local")
20
- self.variability = attrs.pop("variability", "continuous")
21
- self.name = attrs.pop("name")
22
- self.vr = int(attrs.pop("valueReference"))
23
- self.description = attrs.pop("description", None)
19
+ class EmbeddedFMUPort:
20
+ FMI_TO_CONTAINER = {
21
+ 2: {
22
+ 'Real': 'real64',
23
+ 'Integer': 'integer32',
24
+ 'String': 'string',
25
+ 'Boolean': 'boolean'
26
+ },
27
+ 3: {
28
+ 'Float64': 'real64',
29
+ 'Float32': 'real32',
30
+ 'Int8': 'integer8',
31
+ 'UInt8': 'uinteger8',
32
+ 'Int16': 'integer16',
33
+ 'UInt16': 'uinteger16',
34
+ 'Int32': 'integer32',
35
+ 'UInt32': 'uinteger32',
36
+ 'Int64': 'integer64',
37
+ 'UInt64': 'uinteger64',
38
+ 'String': 'string',
39
+ 'Boolean': 'boolean1'
40
+ }
41
+ }
42
+
43
+ FMI_TO_CONTAINER = {
44
+ 2: {
45
+ 'Real': 'real64',
46
+ 'Integer': 'integer32',
47
+ 'String': 'string',
48
+ 'Boolean': 'boolean'
49
+ },
50
+ 3: {
51
+ 'Float64': 'real64',
52
+ 'Float32': 'real32',
53
+ 'Int8': 'integer8',
54
+ 'UInt8': 'uinteger8',
55
+ 'Int16': 'integer16',
56
+ 'UInt16': 'uinteger16',
57
+ 'Int32': 'integer32',
58
+ 'UInt32': 'uinteger32',
59
+ 'Int64': 'integer64',
60
+ 'UInt64': 'uinteger64',
61
+ 'String': 'string',
62
+ 'Boolean': 'boolean1'
63
+ }
64
+ }
65
+
66
+ CONTAINER_TO_FMI = {
67
+ 2: {
68
+ 'real64': 'Real',
69
+ 'integer32': 'Integer',
70
+ 'string': 'String',
71
+ 'boolean': 'Boolean'
72
+ },
73
+ 3: {
74
+ 'real64': 'Float64' ,
75
+ 'real32': 'Float32' ,
76
+ 'integer8': 'Int8' ,
77
+ 'uinteger8': 'UInt8' ,
78
+ 'integer16': 'Int16' ,
79
+ 'uinteger16': 'UInt16' ,
80
+ 'integer32': 'Int32' ,
81
+ 'uinteger32': 'UInt32' ,
82
+ 'integer64': 'Int64' ,
83
+ 'uinteger64': 'UInt64' ,
84
+ 'string': 'String' ,
85
+ 'boolean1': 'Boolean'
86
+ }
87
+ }
88
+
89
+ ALL_TYPES = (
90
+ "real64", "real32",
91
+ "integer8", "uinteger8", "integer16", "uinteger16", "integer32", "uinteger32", "integer64", "uinteger64",
92
+ "boolean", "boolean1",
93
+ "strings"
94
+ )
95
+
96
+
97
+
98
+ def __init__(self, fmi_type, attrs: Dict[str, str], fmi_version=0):
99
+ self.causality = attrs.get("causality", "local")
100
+ self.variability = attrs.get("variability", "continuous")
101
+ self.name = attrs["name"]
102
+ self.vr = int(attrs["valueReference"])
103
+ self.description = attrs.get("description", None)
104
+
105
+ if fmi_version > 0:
106
+ self.type_name = self.FMI_TO_CONTAINER[fmi_version][fmi_type]
107
+ else:
108
+ self.type_name = fmi_type
109
+
110
+ self.start_value = attrs.get("start", None)
111
+ self.initial = attrs.get("initial", None)
24
112
 
25
- self.type_name = attrs.pop("type_name", None)
26
- self.start_value = attrs.pop("start", None)
27
- self.initial = attrs.pop("initial", None)
28
113
 
29
- def set_port_type(self, type_name: str, attrs: Dict[str, str]):
30
- self.type_name = type_name
31
- self.start_value = attrs.pop("start", None)
32
- self.initial = attrs.pop("initial", None)
33
114
 
34
115
  def xml(self, vr: int, name=None, causality=None, start=None, fmi_version=2):
35
116
  if name is None:
@@ -39,23 +120,18 @@ class FMUPort:
39
120
  if start is None:
40
121
  start = self.start_value
41
122
  if self.variability is None:
42
- self.variability = "continuous" if self.type_name == "Real" else "discrete"
123
+ self.variability = "continuous" if "real" in self.type_name else "discrete"
43
124
 
125
+ fmi_type = self.CONTAINER_TO_FMI[fmi_version][self.type_name]
44
126
 
45
127
  if fmi_version == 2:
46
- child_dict = {
128
+ child_attrs = {
47
129
  "start": start,
48
130
  }
49
- if "Float" in self.type_name:
50
- type_name = "Real"
51
- elif "Int" in self.type_name:
52
- type_name = "Integer"
53
- else:
54
- type_name = self.type_name
55
131
 
56
- child_str = (f"<{type_name} " +
57
- " ".join([f'{key}="{value}"' if value is not None else ""
58
- for (key, value) in child_dict.items()]) +
132
+ filtered_child_attrs = {key: value for key, value in child_attrs.items() if value is not None}
133
+ child_str = (f"<{fmi_type} " +
134
+ " ".join([f'{key}="{value}"' for (key, value) in filtered_child_attrs.items()]) +
59
135
  "/>")
60
136
 
61
137
  scalar_attrs = {
@@ -66,11 +142,23 @@ class FMUPort:
66
142
  "initial": self.initial,
67
143
  "description": self.description,
68
144
  }
69
- scalar_attrs_str = " ".join([f'{key}="{value}"' if value is not None else ""
70
- for (key, value) in scalar_attrs.items()])
145
+ filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
146
+ scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
71
147
  return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
72
148
  else:
73
- return f'FIX ME'
149
+ scalar_attrs = {
150
+ "name": name,
151
+ "valueReference": vr,
152
+ "causality": causality,
153
+ "variability": self.variability,
154
+ "initial": self.initial,
155
+ "description": self.description,
156
+ "start": start
157
+ }
158
+ filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
159
+ scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
160
+
161
+ return f'<{fmi_type} {scalar_attrs_str}/>'
74
162
 
75
163
 
76
164
  class EmbeddedFMU(OperationAbstract):
@@ -80,14 +168,15 @@ class EmbeddedFMU(OperationAbstract):
80
168
  def __init__(self, filename):
81
169
  self.fmu = FMU(filename)
82
170
  self.name = Path(filename).name
83
- self.id = Path(filename).stem
171
+ self.id = Path(filename).stem.lower()
84
172
 
85
173
  self.step_size = None
86
174
  self.start_time = None
87
175
  self.stop_time = None
88
176
  self.model_identifier = None
89
177
  self.guid = None
90
- self.ports: Dict[str, FMUPort] = {}
178
+ self.fmi_version = None
179
+ self.ports: Dict[str, EmbeddedFMUPort] = {}
91
180
 
92
181
  self.capabilities: Dict[str, str] = {}
93
182
  self.current_port = None # used during apply_operation()
@@ -100,21 +189,10 @@ class EmbeddedFMU(OperationAbstract):
100
189
  fmi_version = attrs['fmiVersion']
101
190
  if fmi_version == "2.0":
102
191
  self.guid = attrs['guid']
192
+ self.fmi_version = 2
103
193
  if fmi_version == "3.0":
104
194
  self.guid = attrs['instantiationToken']
105
-
106
-
107
- def scalar_attrs(self, attrs) -> int:
108
- if 'type_name' in attrs: # FMI 3.0
109
- type_name = attrs.pop('type_name')
110
- port = FMUPort(attrs)
111
- port.type_name = type_name
112
- self.ports[port.name] = port
113
- else: # FMI 2.0
114
- self.current_port = FMUPort(attrs)
115
- self.ports[self.current_port.name] = self.current_port
116
-
117
- return 0
195
+ self.fmi_version = 3
118
196
 
119
197
  def cosimulation_attrs(self, attrs: Dict[str, str]):
120
198
  self.model_identifier = attrs['modelIdentifier']
@@ -129,12 +207,9 @@ class EmbeddedFMU(OperationAbstract):
129
207
  self.start_time = float(attrs.get("startTime", 0.0))
130
208
  self.stop_time = float(attrs.get("stopTime", self.start_time + 1.0))
131
209
 
132
- def scalar_type(self, type_name, attrs):
133
- if self.current_port:
134
- if type_name == "Enumeration":
135
- type_name = "Integer"
136
- self.current_port.set_port_type(type_name, attrs)
137
- self.current_port = None
210
+ def port_attrs(self, fmu_port):
211
+ port = EmbeddedFMUPort(fmu_port.fmi_type, fmu_port, fmi_version=self.fmi_version)
212
+ self.ports[port.name] = port
138
213
 
139
214
  def __repr__(self):
140
215
  return f"FMU '{self.name}' ({len(self.ports)} variables)"
@@ -212,21 +287,23 @@ class Local:
212
287
 
213
288
  class ValueReferenceTable:
214
289
  def __init__(self):
215
- self.vr_table: Dict[str, int] = {
216
- "Real": 0,
217
- "Float64": 0,
218
- "Integer": 0,
219
- "Boolean": 0,
220
- "String": 0,
221
- }
290
+ self.vr_table = {}
291
+ self.masks = {}
292
+ for i, type_name in enumerate(EmbeddedFMUPort.ALL_TYPES):
293
+ self.vr_table[type_name] = 0
294
+ self.masks[type_name] = i << 24
222
295
 
223
- def get_vr(self, cport: ContainerPort) -> int:
224
- return self.add_vr(cport.port.type_name)
225
296
 
226
- def add_vr(self, type_name: str) -> int:
297
+ def add_vr(self, port_or_type_name: Union[ContainerPort, str]) -> int:
298
+ if isinstance(port_or_type_name, ContainerPort):
299
+ type_name = port_or_type_name.port.type_name
300
+ else:
301
+ type_name = port_or_type_name
302
+
227
303
  vr = self.vr_table[type_name]
228
304
  self.vr_table[type_name] += 1
229
- return vr
305
+
306
+ return vr | self.masks[type_name]
230
307
 
231
308
 
232
309
  class AutoWired:
@@ -255,15 +332,83 @@ class AutoWired:
255
332
 
256
333
 
257
334
  class FMUContainer:
258
- def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None):
335
+ HEADER_XML_2 = """<?xml version="1.0" encoding="ISO-8859-1"?>
336
+ <fmiModelDescription
337
+ fmiVersion="2.0"
338
+ modelName="{identifier}"
339
+ generationTool="FMUContainer-{tool_version}"
340
+ generationDateAndTime="{timestamp}"
341
+ guid="{guid}"
342
+ description="FMUContainer with {embedded_fmu}"
343
+ author="{author}"
344
+ license="Proprietary"
345
+ copyright="See Embedded FMU's copyrights."
346
+ variableNamingConvention="structured">
347
+
348
+ <CoSimulation
349
+ modelIdentifier="{identifier}"
350
+ canHandleVariableCommunicationStepSize="true"
351
+ canBeInstantiatedOnlyOncePerProcess="{only_once}"
352
+ canNotUseMemoryManagementFunctions="true"
353
+ canGetAndSetFMUstate="false"
354
+ canSerializeFMUstate="false"
355
+ providesDirectionalDerivative="false"
356
+ needsExecutionTool="{execution_tool}">
357
+ </CoSimulation>
358
+
359
+ <LogCategories>
360
+ <Category name="fmucontainer"/>
361
+ </LogCategories>
362
+
363
+ <DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
364
+
365
+ <ModelVariables>
366
+ <ScalarVariable valueReference="0" name="time" causality="independent"><Real /></ScalarVariable>
367
+ """
368
+
369
+ HEADER_XML_3 = """<?xml version="1.0" encoding="ISO-8859-1"?>
370
+ <fmiModelDescription
371
+ fmiVersion="3.0"
372
+ modelName="{identifier}"
373
+ generationTool="FMUContainer-{tool_version}"
374
+ generationDateAndTime="{timestamp}"
375
+ instantiationToken="{guid}"
376
+ description="FMUContainer with {embedded_fmu}"
377
+ author="{author}"
378
+ license="Proprietary"
379
+ copyright="See Embedded FMU's copyrights."
380
+ variableNamingConvention="structured">
381
+
382
+ <CoSimulation
383
+ modelIdentifier="{identifier}"
384
+ canHandleVariableCommunicationStepSize="true"
385
+ canBeInstantiatedOnlyOncePerProcess="{only_once}"
386
+ canNotUseMemoryManagementFunctions="true"
387
+ canGetAndSetFMUstate="false"
388
+ canSerializeFMUstate="false"
389
+ providesDirectionalDerivative="false"
390
+ needsExecutionTool="{execution_tool}">
391
+ </CoSimulation>
392
+
393
+ <LogCategories>
394
+ <Category name="fmucontainer"/>
395
+ </LogCategories>
396
+
397
+ <DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
398
+
399
+ <ModelVariables>
400
+ <Float64 valueReference="0" name="time" causality="independent"/>
401
+ """
402
+
403
+ def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None, fmi_version=2):
259
404
  self.fmu_directory = Path(fmu_directory)
260
405
  self.identifier = identifier
261
406
  if not self.fmu_directory.is_dir():
262
407
  raise FMUContainerError(f"{self.fmu_directory} is not a valid directory")
263
- self.involved_fmu: Dict[str, EmbeddedFMU] = {}
264
- self.execution_order: List[EmbeddedFMU] = []
408
+ self.involved_fmu: OrderedDict[str, EmbeddedFMU] = OrderedDict()
265
409
 
266
410
  self.description_pathname = description_pathname
411
+ self.fmi_version = fmi_version
267
412
 
268
413
  self.start_time = None
269
414
  self.stop_time = None
@@ -276,15 +421,31 @@ class FMUContainer:
276
421
  self.rules: Dict[ContainerPort, str] = {}
277
422
  self.start_values: Dict[ContainerPort, str] = {}
278
423
 
424
+
425
+ def convert_type_name(self, type_name: str) -> str:
426
+ if self.fmi_version == 2:
427
+ table = {}
428
+ elif self.fmi_version == 3:
429
+ table = {}
430
+ else:
431
+ table = {}
432
+
433
+ try:
434
+ return table[type_name]
435
+ except KeyError:
436
+ return type_name
437
+
279
438
  def get_fmu(self, fmu_filename: str) -> EmbeddedFMU:
280
439
  if fmu_filename in self.involved_fmu:
281
440
  return self.involved_fmu[fmu_filename]
282
441
 
283
442
  try:
284
443
  fmu = EmbeddedFMU(self.fmu_directory / fmu_filename)
444
+ if fmu.fmi_version > self.fmi_version:
445
+ logger.fatal(f"Try to embed FMU-{fmu.fmi_version} into container FMI-{self.fmi_version}")
285
446
  self.involved_fmu[fmu.name] = fmu
286
- self.execution_order.append(fmu)
287
- logger.debug(f"Adding FMU #{len(self.execution_order)}: {fmu}")
447
+
448
+ logger.debug(f"Adding FMU #{len(self.involved_fmu)}: {fmu}")
288
449
  except (FMUContainerError, FMUError) as e:
289
450
  raise FMUContainerError(f"Cannot load '{fmu_filename}': {e}")
290
451
 
@@ -299,7 +460,7 @@ class FMUContainer:
299
460
  self.rules[cport] = rule
300
461
 
301
462
  def get_all_cports(self):
302
- return [ContainerPort(fmu, port_name) for fmu in self.execution_order for port_name in fmu.ports]
463
+ return [ContainerPort(fmu, port_name) for fmu in self.involved_fmu.values() for port_name in fmu.ports]
303
464
 
304
465
  def add_input(self, container_port_name: str, to_fmu_filename: str, to_port_name: str):
305
466
  if not container_port_name:
@@ -373,7 +534,7 @@ class FMUContainer:
373
534
 
374
535
  self.start_values[cport] = value
375
536
 
376
- def find_inputs(self, port_to_connect: FMUPort) -> List[ContainerPort]:
537
+ def find_inputs(self, port_to_connect: EmbeddedFMUPort) -> List[ContainerPort]:
377
538
  candidates = []
378
539
  for cport in self.get_all_cports():
379
540
  if (cport.port.causality == 'input' and cport not in self.rules and cport.port.name == port_to_connect.name
@@ -432,7 +593,7 @@ class FMUContainer:
432
593
 
433
594
  def minimum_step_size(self) -> float:
434
595
  step_size = None
435
- for fmu in self.execution_order:
596
+ for fmu in self.involved_fmu.values():
436
597
  if step_size:
437
598
  if fmu.step_size and fmu.step_size < step_size:
438
599
  step_size = fmu.step_size
@@ -446,7 +607,7 @@ class FMUContainer:
446
607
  return step_size
447
608
 
448
609
  def sanity_check(self, step_size: Optional[float]):
449
- for fmu in self.execution_order:
610
+ for fmu in self.involved_fmu.values():
450
611
  if not fmu.step_size:
451
612
  continue
452
613
  ts_ratio = step_size / fmu.step_size
@@ -465,7 +626,7 @@ class FMUContainer:
465
626
  logger.warning(f"{cport} is not connected")
466
627
 
467
628
  def make_fmu(self, fmu_filename: Union[str, Path], step_size: Optional[float] = None, debug=False, mt=False,
468
- profiling=False):
629
+ profiling=False, sequential=False):
469
630
  if isinstance(fmu_filename, str):
470
631
  fmu_filename = Path(fmu_filename)
471
632
 
@@ -481,7 +642,7 @@ class FMUContainer:
481
642
  with open(base_directory / "modelDescription.xml", "wt") as xml_file:
482
643
  self.make_fmu_xml(xml_file, step_size, profiling)
483
644
  with open(resources_directory / "container.txt", "wt") as txt_file:
484
- self.make_fmu_txt(txt_file, step_size, mt, profiling)
645
+ self.make_fmu_txt(txt_file, step_size, mt, profiling, sequential)
485
646
 
486
647
  self.make_fmu_package(base_directory, fmu_filename)
487
648
  if not debug:
@@ -494,7 +655,7 @@ class FMUContainer:
494
655
  guid = str(uuid.uuid4())
495
656
  embedded_fmu = ", ".join([fmu_name for fmu_name in self.involved_fmu])
496
657
  try:
497
- author = os.getlogin()
658
+ author = getpass.getuser()
498
659
  except OSError:
499
660
  author = "Unspecified"
500
661
 
@@ -505,120 +666,110 @@ class FMUContainer:
505
666
  if fmu.capabilities[capability] == "true":
506
667
  capabilities[capability] = "true"
507
668
 
669
+ first_fmu = next(iter(self.involved_fmu.values()))
508
670
  if self.start_time is None:
509
- self.start_time = self.execution_order[0].start_time
510
- logger.info(f"start_time={self.start_time} (deduced from '{self.execution_order[0].name}')")
671
+ self.start_time = first_fmu.start_time
672
+ logger.info(f"start_time={self.start_time} (deduced from '{first_fmu.name}')")
511
673
  else:
512
674
  logger.info(f"start_time={self.start_time}")
513
675
 
514
676
  if self.stop_time is None:
515
- self.stop_time = self.execution_order[0].stop_time
516
- logger.info(f"stop_time={self.stop_time} (deduced from '{self.execution_order[0].name}')")
677
+ self.stop_time = first_fmu.stop_time
678
+ logger.info(f"stop_time={self.stop_time} (deduced from '{first_fmu.name}')")
517
679
  else:
518
680
  logger.info(f"stop_time={self.stop_time}")
519
681
 
520
- xml_file.write(f"""<?xml version="1.0" encoding="ISO-8859-1"?>
521
- <fmiModelDescription
522
- fmiVersion="2.0"
523
- modelName="{self.identifier}"
524
- generationTool="FMUContainer-{tool_version}"
525
- generationDateAndTime="{timestamp}"
526
- guid="{guid}"
527
- description="FMUContainer with {embedded_fmu}"
528
- author="{author}"
529
- license="Proprietary"
530
- copyright="See Embedded FMU's copyrights."
531
- variableNamingConvention="structured">
532
-
533
- <CoSimulation
534
- modelIdentifier="{self.identifier}"
535
- canHandleVariableCommunicationStepSize="true"
536
- canBeInstantiatedOnlyOncePerProcess="{capabilities['canBeInstantiatedOnlyOncePerProcess']}"
537
- canNotUseMemoryManagementFunctions="true"
538
- canGetAndSetFMUstate="false"
539
- canSerializeFMUstate="false"
540
- providesDirectionalDerivative="false"
541
- needsExecutionTool="{capabilities['needsExecutionTool']}">
542
- </CoSimulation>
543
-
544
- <LogCategories>
545
- <Category name="fmucontainer"/>
546
- </LogCategories>
547
-
548
- <DefaultExperiment stepSize="{step_size}" startTime="{self.start_time}" stopTime="{self.stop_time}"/>
549
-
550
- <ModelVariables>
551
- """)
682
+ if self.fmi_version == 2:
683
+ xml_file.write(self.HEADER_XML_2.format(identifier=self.identifier, tool_version=tool_version,
684
+ timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
685
+ author=author,
686
+ only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
687
+ execution_tool=capabilities['needsExecutionTool'],
688
+ start_time=self.start_time, stop_time=self.stop_time,
689
+ step_size=step_size))
690
+ elif self.fmi_version == 3:
691
+ xml_file.write(self.HEADER_XML_3.format(identifier=self.identifier, tool_version=tool_version,
692
+ timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
693
+ author=author,
694
+ only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
695
+ execution_tool=capabilities['needsExecutionTool'],
696
+ start_time=self.start_time, stop_time=self.stop_time,
697
+ step_size=step_size))
698
+
699
+ vr_time = vr_table.add_vr("real64")
700
+ logger.debug(f"Time vr = {vr_time}")
552
701
  if profiling:
553
- for fmu in self.execution_order:
554
- vr = vr_table.add_vr("Real")
555
- name = f"container.{fmu.id}.rt_ratio"
556
- print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local">'
557
- f'<Real /></ScalarVariable>', file=xml_file)
702
+ for fmu in self.involved_fmu.values():
703
+ vr = vr_table.add_vr("real64")
704
+ port = EmbeddedFMUPort("real64", {"valueReference": vr,
705
+ "name": f"container.{fmu.id}.rt_ratio",
706
+ "description": f"RT ratio for embedded FMU '{fmu.name}'"})
707
+ print(f" {port.xml(vr, fmi_version=self.fmi_version)}", file=xml_file)
558
708
 
559
709
  # Local variable should be first to ensure to attribute them the lowest VR.
560
710
  for local in self.locals.values():
561
- vr = vr_table.get_vr(local.cport_from)
562
- print(f' {local.cport_from.port.xml(vr, name=local.name, causality="local")}', file=xml_file)
711
+ vr = vr_table.add_vr(local.cport_from)
712
+ try:
713
+ print(f" {local.cport_from.port.xml(vr, name=local.name, causality='local', fmi_version=self.fmi_version)}", file=xml_file)
714
+ except KeyError:
715
+ logger.error(f"Cannot expose '{local.name}' because type '{local.cport_from.port.type_name}' is not compatible with FMI-{self.fmi_version}.0")
563
716
  local.vr = vr
564
717
 
565
718
  for input_port_name, input_port in self.inputs.items():
566
719
  vr = vr_table.add_vr(input_port.type_name)
567
720
  # Get Start and XML from first connected input
568
721
  start = self.start_values.get(input_port.cport_list[0], None)
569
- print(f" {input_port.cport_list[0].port.xml(vr, name=input_port_name, start=start)}", file=xml_file)
722
+ print(f" {input_port.cport_list[0].port.xml(vr, name=input_port_name, start=start, fmi_version=self.fmi_version)}", file=xml_file)
570
723
  input_port.vr = vr
571
724
 
572
725
  for output_port_name, cport in self.outputs.items():
573
- vr = vr_table.get_vr(cport)
574
- print(f" {cport.port.xml(vr, name=output_port_name)}", file=xml_file)
726
+ vr = vr_table.add_vr(cport)
727
+ print(f" {cport.port.xml(vr, name=output_port_name, fmi_version=self.fmi_version)}", file=xml_file)
575
728
  cport.vr = vr
576
729
 
577
- xml_file.write(""" </ModelVariables>
578
-
579
- <ModelStructure>
580
- <Outputs>
581
- """)
730
+ if self.fmi_version == 2:
731
+ self.make_fmu_xml_epilog_2(xml_file)
732
+ elif self.fmi_version == 3:
733
+ self.make_fmu_xml_epilog_3(xml_file)
582
734
 
735
+ def make_fmu_xml_epilog_2(self, xml_file):
736
+ xml_file.write(f" </ModelVariables>\n\n <ModelStructure>\n <Outputs>\n")
583
737
  index_offset = len(self.locals) + len(self.inputs) + 1
584
738
  for i, _ in enumerate(self.outputs.keys()):
585
739
  print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
586
- xml_file.write(""" </Outputs>
587
- <InitialUnknowns>
588
- """)
740
+
741
+ xml_file.write(" </Outputs>\n <InitialUnknowns>\n")
742
+
589
743
  for i, _ in enumerate(self.outputs.keys()):
590
744
  print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
591
- xml_file.write(""" </InitialUnknowns>
592
- </ModelStructure>
593
745
 
594
- </fmiModelDescription>
595
- """)
746
+ xml_file.write(" </InitialUnknowns>\n </ModelStructure>\n\n</fmiModelDescription>")
596
747
 
597
- def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool):
598
- if mt:
599
- print("# Use MT\n1", file=txt_file)
600
- else:
601
- print("# Don't use MT\n0", file=txt_file)
748
+ def make_fmu_xml_epilog_3(self, xml_file):
749
+ xml_file.write(f" </ModelVariables>\n\n <ModelStructure>\n")
750
+ for output in self.outputs.values():
751
+ print(f' <Output valueReference="{output.vr}"/>', file=xml_file)
602
752
 
603
- if profiling:
604
- print("# Profiling ENABLED\n1", file=txt_file)
605
- else:
606
- print("# Profiling DISABLED\n0", file=txt_file)
753
+ xml_file.write(" </ModelStructure>\n\n</fmiModelDescription>\n""")
754
+
755
+ def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
756
+ print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
757
+ flags = [ str(int(flag == True)) for flag in (mt, profiling, sequential)]
758
+ print(" ".join(flags), file=txt_file)
607
759
 
608
760
  print(f"# Internal time step in seconds", file=txt_file)
609
761
  print(f"{step_size}", file=txt_file)
610
762
  print(f"# NB of embedded FMU's", file=txt_file)
611
763
  print(f"{len(self.involved_fmu)}", file=txt_file)
612
764
  fmu_rank: Dict[str, int] = {}
613
- for i, fmu in enumerate(self.execution_order):
614
- print(f"{fmu.name}", file=txt_file)
765
+ for i, fmu in enumerate(self.involved_fmu.values()):
766
+ print(f"{fmu.name} {fmu.fmi_version}", file=txt_file)
615
767
  print(f"{fmu.model_identifier}", file=txt_file)
616
768
  print(f"{fmu.guid}", file=txt_file)
617
769
  fmu_rank[fmu.name] = i
618
770
 
619
771
  # Prepare data structure
620
- type_names_list = ("Real", "Integer", "Boolean", "String") # Ordered list
621
- inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
772
+ inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
622
773
  outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
623
774
 
624
775
  inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
@@ -626,7 +777,7 @@ class FMUContainer:
626
777
  outputs_fmu_per_type = {}
627
778
  locals_per_type: Dict[str, List[Local]] = {}
628
779
 
629
- for type_name in type_names_list:
780
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
630
781
  inputs_per_type[type_name] = []
631
782
  outputs_per_type[type_name] = []
632
783
  locals_per_type[type_name] = []
@@ -635,7 +786,7 @@ class FMUContainer:
635
786
  start_values_fmu_per_type[type_name] = {}
636
787
  outputs_fmu_per_type[type_name] = {}
637
788
 
638
- for fmu in self.execution_order:
789
+ for fmu in self.involved_fmu.values():
639
790
  inputs_fmu_per_type[type_name][fmu.name] = {}
640
791
  start_values_fmu_per_type[type_name][fmu.name] = {}
641
792
  outputs_fmu_per_type[type_name][fmu.name] = {}
@@ -657,27 +808,35 @@ class FMUContainer:
657
808
  for cport_to in local.cport_to_list:
658
809
  inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
659
810
 
660
- print(f"# NB local variables Real, Integer, Boolean, String", file=txt_file)
661
- for type_name in type_names_list:
811
+ print(f"# NB local variables:", ", ".join(EmbeddedFMUPort.ALL_TYPES), file=txt_file)
812
+ nb_local = []
813
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
662
814
  nb = len(locals_per_type[type_name])
663
- if profiling and type_name == "Real":
664
- nb += len(self.execution_order)
665
- print(f"{nb} ", file=txt_file, end='')
815
+ if type_name == "real64":
816
+ nb += 1 # reserver a slot for "time"
817
+ if profiling:
818
+ nb += len(self.involved_fmu)
819
+ nb_local.append(str(nb))
820
+ print(" ".join(nb_local), file=txt_file, end='')
666
821
  print("", file=txt_file)
667
822
 
668
823
  print("# CONTAINER I/O: <VR> <NB> <FMU_INDEX> <FMU_VR> [<FMU_INDEX> <FMU_VR>]", file=txt_file)
669
- for type_name in type_names_list:
824
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
670
825
  print(f"# {type_name}", file=txt_file)
671
826
  nb = len(inputs_per_type[type_name]) + len(outputs_per_type[type_name]) + len(locals_per_type[type_name])
672
827
  nb_input_link = 0
673
828
  for input_port in inputs_per_type[type_name]:
674
829
  nb_input_link += len(input_port.cport_list) - 1
675
830
 
676
- if profiling and type_name == "Real":
677
- nb += len(self.execution_order)
678
- print(f"{nb} {nb+nb_input_link}", file=txt_file)
679
- for profiling_port, _ in enumerate(self.execution_order):
680
- print(f"{profiling_port} 1 -2 {profiling_port}", file=txt_file)
831
+ if type_name == "real64":
832
+ nb += 1 # reserver a slot for "time"
833
+ if profiling:
834
+ nb += len(self.involved_fmu)
835
+ print(f"{nb} {nb + nb_input_link}", file=txt_file)
836
+ print(f"0 1 -1 0", file=txt_file) # Time slot
837
+ if profiling:
838
+ for profiling_port, _ in enumerate(self.involved_fmu.values()):
839
+ print(f"{profiling_port+1} 1 -2 {profiling_port+1}", file=txt_file)
681
840
  else:
682
841
  print(f"{nb} {nb+nb_input_link}", file=txt_file)
683
842
  for input_port in inputs_per_type[type_name]:
@@ -686,24 +845,24 @@ class FMUContainer:
686
845
  for cport in outputs_per_type[type_name]:
687
846
  print(f"{cport.vr} 1 {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
688
847
  for local in locals_per_type[type_name]:
689
- print(f"{local.vr} 1 -1 {local.vr}", file=txt_file)
848
+ print(f"{local.vr} 1 -1 {local.vr & 0xFFFFFF}", file=txt_file)
690
849
 
691
850
  # LINKS
692
- for fmu in self.execution_order:
693
- for type_name in type_names_list:
851
+ for fmu in self.involved_fmu.values():
852
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
694
853
  print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
695
854
  print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
696
855
  for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
697
856
  print(f"{vr} {cport.port.vr}", file=txt_file)
698
857
 
699
- for type_name in type_names_list:
858
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
700
859
  print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
701
860
  print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
702
861
  for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
703
862
  reset = 1 if cport.port.causality == "input" else 0
704
863
  print(f"{cport.port.vr} {reset} {value}", file=txt_file)
705
864
 
706
- for type_name in type_names_list:
865
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
707
866
  print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
708
867
  print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
709
868
  for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
@@ -717,6 +876,39 @@ class FMUContainer:
717
876
  else:
718
877
  return path
719
878
 
879
+ @staticmethod
880
+ def copyfile(origin, destination):
881
+ logger.debug(f"Copying {origin} in {destination}")
882
+ shutil.copy(origin, destination)
883
+
884
+ def get_bindir_and_suffixe(self) -> (str, str, str):
885
+ suffixes = {
886
+ "Windows": "dll",
887
+ "Linux": "so",
888
+ "Darwin": "dylib"
889
+ }
890
+
891
+ origin_bindirs = {
892
+ "Windows": "win64",
893
+ "Linux": "linux64",
894
+ "Darwin": "darwin64"
895
+ }
896
+
897
+ if self.fmi_version == 3:
898
+ target_bindirs = {
899
+ "Windows": "x86_64-windows",
900
+ "Linux": "x86_64-linux",
901
+ "Darwin": "aarch64-darwin"
902
+ }
903
+ else:
904
+ target_bindirs = origin_bindirs
905
+
906
+ os_name = platform.system()
907
+ try:
908
+ return origin_bindirs[os_name], suffixes[os_name], target_bindirs[os_name]
909
+ except KeyError:
910
+ raise FMUContainerError(f"OS '{os_name}' is not supported.")
911
+
720
912
  def make_fmu_skeleton(self, base_directory: Path) -> Path:
721
913
  logger.debug(f"Initialize directory '{base_directory}'")
722
914
 
@@ -731,20 +923,23 @@ class FMUContainer:
731
923
  documentation_directory.mkdir(exist_ok=True)
732
924
 
733
925
  if self.description_pathname:
734
- logger.debug(f"Copying {self.description_pathname}")
735
- shutil.copy(self.description_pathname, documentation_directory)
926
+ self.copyfile(self.description_pathname, documentation_directory)
927
+
928
+ self.copyfile(origin / "model.png", base_directory)
736
929
 
737
- shutil.copy(origin / "model.png", base_directory)
738
- for bitness in ('win32', 'win64'):
739
- library_filename = origin / bitness / "container.dll"
740
- if library_filename.is_file():
741
- binary_directory = binaries_directory / bitness
742
- binary_directory.mkdir(exist_ok=True)
743
- shutil.copy(library_filename, binary_directory / f"{self.identifier}.dll")
930
+ origin_bindir, suffixe, target_bindir = self.get_bindir_and_suffixe()
931
+
932
+ library_filename = origin / origin_bindir / f"container.{suffixe}"
933
+ if not library_filename.is_file():
934
+ raise FMUContainerError(f"File {library_filename} not found")
935
+ binary_directory = binaries_directory / target_bindir
936
+ binary_directory.mkdir(exist_ok=True)
937
+ self.copyfile(library_filename, binary_directory / f"{self.identifier}.{suffixe}")
744
938
 
745
939
  for i, fmu in enumerate(self.involved_fmu.values()):
746
940
  shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
747
941
  self.long_path(resources_directory / f"{i:02x}"), dirs_exist_ok=True)
942
+
748
943
  return resources_directory
749
944
 
750
945
  def make_fmu_package(self, base_directory: Path, fmu_filename: Path):