fmu-manipulation-toolbox 1.9rc0__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
+ 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):
19
99
  self.causality = attrs.get("causality", "local")
20
100
  self.variability = attrs.get("variability", "continuous")
21
- self.name = attrs.get("name")
22
- self.vr = int(attrs.get("valueReference"))
101
+ self.name = attrs["name"]
102
+ self.vr = int(attrs["valueReference"])
23
103
  self.description = attrs.get("description", None)
24
104
 
25
- self.type_name = attrs.get("type_name", None)
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
+
26
110
  self.start_value = attrs.get("start", None)
27
111
  self.initial = attrs.get("initial", None)
28
112
 
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)
113
+
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
295
+
222
296
 
223
- def get_vr(self, cport: ContainerPort) -> int:
224
- return self.add_vr(cport.port.type_name)
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
225
302
 
226
- def add_vr(self, type_name: str) -> int:
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
@@ -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,94 +666,91 @@ 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>")
747
+
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)
752
+
753
+ xml_file.write(" </ModelStructure>\n\n</fmiModelDescription>\n""")
596
754
 
597
755
  def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
598
756
  print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
@@ -604,15 +762,14 @@ class FMUContainer:
604
762
  print(f"# NB of embedded FMU's", file=txt_file)
605
763
  print(f"{len(self.involved_fmu)}", file=txt_file)
606
764
  fmu_rank: Dict[str, int] = {}
607
- for i, fmu in enumerate(self.execution_order):
608
- 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)
609
767
  print(f"{fmu.model_identifier}", file=txt_file)
610
768
  print(f"{fmu.guid}", file=txt_file)
611
769
  fmu_rank[fmu.name] = i
612
770
 
613
771
  # Prepare data structure
614
- type_names_list = ("Real", "Integer", "Boolean", "String") # Ordered list
615
- inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
772
+ inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
616
773
  outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
617
774
 
618
775
  inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
@@ -620,7 +777,7 @@ class FMUContainer:
620
777
  outputs_fmu_per_type = {}
621
778
  locals_per_type: Dict[str, List[Local]] = {}
622
779
 
623
- for type_name in type_names_list:
780
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
624
781
  inputs_per_type[type_name] = []
625
782
  outputs_per_type[type_name] = []
626
783
  locals_per_type[type_name] = []
@@ -629,7 +786,7 @@ class FMUContainer:
629
786
  start_values_fmu_per_type[type_name] = {}
630
787
  outputs_fmu_per_type[type_name] = {}
631
788
 
632
- for fmu in self.execution_order:
789
+ for fmu in self.involved_fmu.values():
633
790
  inputs_fmu_per_type[type_name][fmu.name] = {}
634
791
  start_values_fmu_per_type[type_name][fmu.name] = {}
635
792
  outputs_fmu_per_type[type_name][fmu.name] = {}
@@ -651,27 +808,35 @@ class FMUContainer:
651
808
  for cport_to in local.cport_to_list:
652
809
  inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
653
810
 
654
- print(f"# NB local variables Real, Integer, Boolean, String", file=txt_file)
655
- 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:
656
814
  nb = len(locals_per_type[type_name])
657
- if profiling and type_name == "Real":
658
- nb += len(self.execution_order)
659
- 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='')
660
821
  print("", file=txt_file)
661
822
 
662
823
  print("# CONTAINER I/O: <VR> <NB> <FMU_INDEX> <FMU_VR> [<FMU_INDEX> <FMU_VR>]", file=txt_file)
663
- for type_name in type_names_list:
824
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
664
825
  print(f"# {type_name}", file=txt_file)
665
826
  nb = len(inputs_per_type[type_name]) + len(outputs_per_type[type_name]) + len(locals_per_type[type_name])
666
827
  nb_input_link = 0
667
828
  for input_port in inputs_per_type[type_name]:
668
829
  nb_input_link += len(input_port.cport_list) - 1
669
830
 
670
- if profiling and type_name == "Real":
671
- nb += len(self.execution_order)
672
- print(f"{nb} {nb+nb_input_link}", file=txt_file)
673
- for profiling_port, _ in enumerate(self.execution_order):
674
- 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)
675
840
  else:
676
841
  print(f"{nb} {nb+nb_input_link}", file=txt_file)
677
842
  for input_port in inputs_per_type[type_name]:
@@ -680,24 +845,24 @@ class FMUContainer:
680
845
  for cport in outputs_per_type[type_name]:
681
846
  print(f"{cport.vr} 1 {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
682
847
  for local in locals_per_type[type_name]:
683
- print(f"{local.vr} 1 -1 {local.vr}", file=txt_file)
848
+ print(f"{local.vr} 1 -1 {local.vr & 0xFFFFFF}", file=txt_file)
684
849
 
685
850
  # LINKS
686
- for fmu in self.execution_order:
687
- for type_name in type_names_list:
851
+ for fmu in self.involved_fmu.values():
852
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
688
853
  print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
689
854
  print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
690
855
  for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
691
856
  print(f"{vr} {cport.port.vr}", file=txt_file)
692
857
 
693
- for type_name in type_names_list:
858
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
694
859
  print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
695
860
  print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
696
861
  for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
697
862
  reset = 1 if cport.port.causality == "input" else 0
698
863
  print(f"{cport.port.vr} {reset} {value}", file=txt_file)
699
864
 
700
- for type_name in type_names_list:
865
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
701
866
  print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
702
867
  print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
703
868
  for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
@@ -711,6 +876,39 @@ class FMUContainer:
711
876
  else:
712
877
  return path
713
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
+
714
912
  def make_fmu_skeleton(self, base_directory: Path) -> Path:
715
913
  logger.debug(f"Initialize directory '{base_directory}'")
716
914
 
@@ -725,20 +923,23 @@ class FMUContainer:
725
923
  documentation_directory.mkdir(exist_ok=True)
726
924
 
727
925
  if self.description_pathname:
728
- logger.debug(f"Copying {self.description_pathname}")
729
- shutil.copy(self.description_pathname, documentation_directory)
926
+ self.copyfile(self.description_pathname, documentation_directory)
927
+
928
+ self.copyfile(origin / "model.png", base_directory)
730
929
 
731
- shutil.copy(origin / "model.png", base_directory)
732
- for bitness in ('win32', 'win64'):
733
- library_filename = origin / bitness / "container.dll"
734
- if library_filename.is_file():
735
- binary_directory = binaries_directory / bitness
736
- binary_directory.mkdir(exist_ok=True)
737
- 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}")
738
938
 
739
939
  for i, fmu in enumerate(self.involved_fmu.values()):
740
940
  shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
741
941
  self.long_path(resources_directory / f"{i:02x}"), dirs_exist_ok=True)
942
+
742
943
  return resources_directory
743
944
 
744
945
  def make_fmu_package(self, base_directory: Path, fmu_filename: Path):