fmu-manipulation-toolbox 1.9rc0__py3-none-any.whl → 1.9rc2__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 (23) hide show
  1. fmu_manipulation_toolbox/__init__.py +0 -1
  2. fmu_manipulation_toolbox/__main__.py +1 -1
  3. fmu_manipulation_toolbox/__version__.py +1 -1
  4. fmu_manipulation_toolbox/assembly.py +8 -6
  5. fmu_manipulation_toolbox/checker.py +11 -8
  6. fmu_manipulation_toolbox/container.py +492 -206
  7. fmu_manipulation_toolbox/gui.py +45 -55
  8. fmu_manipulation_toolbox/gui_style.py +8 -0
  9. fmu_manipulation_toolbox/operations.py +184 -120
  10. fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
  11. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  12. fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  13. fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  14. fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  15. fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  16. fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  17. fmu_manipulation_toolbox/split.py +8 -0
  18. {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc2.dist-info}/METADATA +1 -1
  19. {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc2.dist-info}/RECORD +23 -22
  20. {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc2.dist-info}/WHEEL +0 -0
  21. {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc2.dist-info}/entry_points.txt +0 -0
  22. {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc2.dist-info}/licenses/LICENSE.txt +0 -0
  23. {fmu_manipulation_toolbox-1.9rc0.dist-info → fmu_manipulation_toolbox-1.9rc2.dist-info}/top_level.txt +0 -0
@@ -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,24 +16,80 @@ 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
+ CONTAINER_TO_FMI = {
44
+ 2: {
45
+ 'real64': 'Real',
46
+ 'integer32': 'Integer',
47
+ 'string': 'String',
48
+ 'boolean': 'Boolean'
49
+ },
50
+ 3: {
51
+ 'real64': 'Float64' ,
52
+ 'real32': 'Float32' ,
53
+ 'integer8': 'Int8' ,
54
+ 'uinteger8': 'UInt8' ,
55
+ 'integer16': 'Int16' ,
56
+ 'uinteger16': 'UInt16' ,
57
+ 'integer32': 'Int32' ,
58
+ 'uinteger32': 'UInt32' ,
59
+ 'integer64': 'Int64' ,
60
+ 'uinteger64': 'UInt64' ,
61
+ 'string': 'String' ,
62
+ 'boolean1': 'Boolean'
63
+ }
64
+ }
65
+
66
+ ALL_TYPES = (
67
+ "real64", "real32",
68
+ "integer8", "uinteger8", "integer16", "uinteger16", "integer32", "uinteger32", "integer64", "uinteger64",
69
+ "boolean", "boolean1",
70
+ "string"
71
+ )
72
+
73
+
74
+
75
+ def __init__(self, fmi_type, attrs: Dict[str, str], fmi_version=0):
19
76
  self.causality = attrs.get("causality", "local")
20
77
  self.variability = attrs.get("variability", "continuous")
21
- self.name = attrs.get("name")
22
- self.vr = int(attrs.get("valueReference"))
78
+ self.name = attrs["name"]
79
+ self.vr = int(attrs["valueReference"])
23
80
  self.description = attrs.get("description", None)
24
81
 
25
- self.type_name = attrs.get("type_name", None)
82
+ if fmi_version > 0:
83
+ self.type_name = self.FMI_TO_CONTAINER[fmi_version][fmi_type]
84
+ else:
85
+ self.type_name = fmi_type
86
+
26
87
  self.start_value = attrs.get("start", None)
27
88
  self.initial = attrs.get("initial", None)
28
89
 
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
90
 
34
- def xml(self, vr: int, name=None, causality=None, start=None, fmi_version=2):
91
+
92
+ def xml(self, vr: int, name=None, causality=None, start=None, fmi_version=2) -> str:
35
93
  if name is None:
36
94
  name = self.name
37
95
  if causality is None:
@@ -39,23 +97,23 @@ class FMUPort:
39
97
  if start is None:
40
98
  start = self.start_value
41
99
  if self.variability is None:
42
- self.variability = "continuous" if self.type_name == "Real" else "discrete"
100
+ self.variability = "continuous" if "real" in self.type_name else "discrete"
43
101
 
102
+ try:
103
+ fmi_type = self.CONTAINER_TO_FMI[fmi_version][self.type_name]
104
+ except KeyError:
105
+ logger.error(f"Cannot expose '{name}' because type '{self.type_name}' is not compatible "
106
+ f"with FMI-{fmi_version}.0")
107
+ return ""
44
108
 
45
109
  if fmi_version == 2:
46
- child_dict = {
110
+ child_attrs = {
47
111
  "start": start,
48
112
  }
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
113
 
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()]) +
114
+ filtered_child_attrs = {key: value for key, value in child_attrs.items() if value is not None}
115
+ child_str = (f"<{fmi_type} " +
116
+ " ".join([f'{key}="{value}"' for (key, value) in filtered_child_attrs.items()]) +
59
117
  "/>")
60
118
 
61
119
  scalar_attrs = {
@@ -66,11 +124,40 @@ class FMUPort:
66
124
  "initial": self.initial,
67
125
  "description": self.description,
68
126
  }
69
- scalar_attrs_str = " ".join([f'{key}="{value}"' if value is not None else ""
70
- for (key, value) in scalar_attrs.items()])
127
+ filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
128
+ scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
71
129
  return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
72
130
  else:
73
- return f'FIX ME'
131
+ if fmi_type in ('String', 'Binary'):
132
+ if start:
133
+ child_str = f'<Start value="{start}"/>'
134
+ else:
135
+ child_str = ''
136
+ scalar_attrs = {
137
+ "name": name,
138
+ "valueReference": vr,
139
+ "causality": causality,
140
+ "variability": self.variability,
141
+ "initial": self.initial,
142
+ "description": self.description,
143
+ }
144
+ filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
145
+ scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
146
+ return f'<{fmi_type} {scalar_attrs_str}>{child_str}</{fmi_type}>'
147
+ else:
148
+ scalar_attrs = {
149
+ "name": name,
150
+ "valueReference": vr,
151
+ "causality": causality,
152
+ "variability": self.variability,
153
+ "initial": self.initial,
154
+ "description": self.description,
155
+ "start": start
156
+ }
157
+ filtered_attrs = {key: value for key, value in scalar_attrs.items() if value is not None}
158
+ scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in filtered_attrs.items()])
159
+
160
+ return f'<{fmi_type} {scalar_attrs_str}/>'
74
161
 
75
162
 
76
163
  class EmbeddedFMU(OperationAbstract):
@@ -80,14 +167,15 @@ class EmbeddedFMU(OperationAbstract):
80
167
  def __init__(self, filename):
81
168
  self.fmu = FMU(filename)
82
169
  self.name = Path(filename).name
83
- self.id = Path(filename).stem
170
+ self.id = Path(filename).stem.lower()
84
171
 
85
172
  self.step_size = None
86
173
  self.start_time = None
87
174
  self.stop_time = None
88
175
  self.model_identifier = None
89
176
  self.guid = None
90
- self.ports: Dict[str, FMUPort] = {}
177
+ self.fmi_version = None
178
+ self.ports: Dict[str, EmbeddedFMUPort] = {}
91
179
 
92
180
  self.capabilities: Dict[str, str] = {}
93
181
  self.current_port = None # used during apply_operation()
@@ -100,21 +188,10 @@ class EmbeddedFMU(OperationAbstract):
100
188
  fmi_version = attrs['fmiVersion']
101
189
  if fmi_version == "2.0":
102
190
  self.guid = attrs['guid']
191
+ self.fmi_version = 2
103
192
  if fmi_version == "3.0":
104
193
  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
194
+ self.fmi_version = 3
118
195
 
119
196
  def cosimulation_attrs(self, attrs: Dict[str, str]):
120
197
  self.model_identifier = attrs['modelIdentifier']
@@ -129,12 +206,9 @@ class EmbeddedFMU(OperationAbstract):
129
206
  self.start_time = float(attrs.get("startTime", 0.0))
130
207
  self.stop_time = float(attrs.get("stopTime", self.start_time + 1.0))
131
208
 
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
209
+ def port_attrs(self, fmu_port):
210
+ port = EmbeddedFMUPort(fmu_port.fmi_type, fmu_port, fmi_version=self.fmi_version)
211
+ self.ports[port.name] = port
138
212
 
139
213
  def __repr__(self):
140
214
  return f"FMU '{self.name}' ({len(self.ports)} variables)"
@@ -190,12 +264,51 @@ class ContainerInput:
190
264
  self.cport_list.append(cport_to)
191
265
 
192
266
 
193
- class Local:
267
+ class Link:
268
+ CONVERSION_FUNCTION = {
269
+ "real32/real64": "F32_F64",
270
+
271
+ "Int8/Int16": "D8_D16",
272
+ "Int8/UInt16": "D8_U16",
273
+ "Int8/Int32": "D8_D32",
274
+ "Int8/UInt32": "D8_U32",
275
+ "Int8/Int64": "D8_D64",
276
+ "Int8/UInt64": "D8_U64",
277
+
278
+ "UInt8/Int16": "U8_D16",
279
+ "UInt8/UInt16": "U8_U16",
280
+ "UInt8/Int32": "U8_D32",
281
+ "UInt8/UInt32": "U8_U32",
282
+ "UInt8/Int64": "U8_D64",
283
+ "UInt8/UInt64": "U8_U64",
284
+
285
+ "Int16/Int32": "D16_D32",
286
+ "Int16/UInt32": "D16_U32",
287
+ "Int16/Int64": "D16_D64",
288
+ "Int16/UInt64": "D16_U64",
289
+
290
+ "UInt16/Int32": "U16_D32",
291
+ "UInt16/UInt32": "U16_U32",
292
+ "UInt16/Int64": "U16_D64",
293
+ "UInt16/UInt64": "U16_U64",
294
+
295
+ "Int32/Int64": "D32_D64",
296
+ "Int32/UInt64": "D32_U64",
297
+
298
+ "UInt32/Int64": "U32_D64",
299
+ "UInt32/UInt64": "U32_U64",
300
+
301
+ "boolean/boolean1": "B_B1",
302
+ "boolean1/boolean": "B1_B",
303
+ }
304
+
194
305
  def __init__(self, cport_from: ContainerPort):
195
306
  self.name = cport_from.fmu.id + "." + cport_from.port.name # strip .fmu suffix
196
307
  self.cport_from = cport_from
197
308
  self.cport_to_list: List[ContainerPort] = []
198
- self.vr = None
309
+
310
+ self.vr: Optional[int] = None
311
+ self.vr_converted: Dict[str, Optional[int]] = {}
199
312
 
200
313
  if not cport_from.port.causality == "output":
201
314
  raise FMUContainerError(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT")
@@ -206,28 +319,54 @@ class Local:
206
319
 
207
320
  if cport_to.port.type_name == self.cport_from.port.type_name:
208
321
  self.cport_to_list.append(cport_to)
322
+ elif self.get_conversion(cport_to):
323
+ self.cport_to_list.append(cport_to)
324
+ self.vr_converted[cport_to.port.type_name] = None
209
325
  else:
210
326
  raise FMUContainerError(f"failed to connect {self.cport_from} to {cport_to} due to type.")
211
327
 
328
+ def get_conversion(self, cport_to: ContainerPort) -> Optional[str]:
329
+ try:
330
+ conversion = f"{self.cport_from.port.type_name}/{cport_to.port.type_name}"
331
+ return self.CONVERSION_FUNCTION[conversion]
332
+ except KeyError:
333
+ return None
334
+
335
+ def nb_local(self) -> int:
336
+ return 1+len(self.vr_converted)
337
+
212
338
 
213
339
  class ValueReferenceTable:
214
340
  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
- }
341
+ self.vr_table:Dict[str, int] = {}
342
+ self.masks: Dict[str, int] = {}
343
+ self.nb_local_variable:Dict[str, int] = {}
344
+ for i, type_name in enumerate(EmbeddedFMUPort.ALL_TYPES):
345
+ self.vr_table[type_name] = 0
346
+ self.masks[type_name] = i << 24
347
+ self.nb_local_variable[type_name] = 0
348
+
349
+ def add_vr(self, port_or_type_name: Union[ContainerPort, str], local: bool = False) -> int:
350
+ if isinstance(port_or_type_name, ContainerPort):
351
+ type_name = port_or_type_name.port.type_name
352
+ else:
353
+ type_name = port_or_type_name
222
354
 
223
- def get_vr(self, cport: ContainerPort) -> int:
224
- return self.add_vr(cport.port.type_name)
355
+ if local:
356
+ self.nb_local_variable[type_name] += 1
225
357
 
226
- def add_vr(self, type_name: str) -> int:
227
358
  vr = self.vr_table[type_name]
228
359
  self.vr_table[type_name] += 1
229
- return vr
230
360
 
361
+ return vr | self.masks[type_name]
362
+
363
+ def set_link_vr(self, link: Link):
364
+ link.vr = self.add_vr(link.cport_from, local=True)
365
+ for type_name in link.vr_converted.keys():
366
+ link.vr_converted[type_name] = self.add_vr(type_name, local=True)
367
+
368
+ def nb_local(self, type_name: str) -> int:
369
+ return self.nb_local_variable[type_name]
231
370
 
232
371
  class AutoWired:
233
372
  def __init__(self):
@@ -255,15 +394,83 @@ class AutoWired:
255
394
 
256
395
 
257
396
  class FMUContainer:
258
- def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None):
397
+ HEADER_XML_2 = """<?xml version="1.0" encoding="ISO-8859-1"?>
398
+ <fmiModelDescription
399
+ fmiVersion="2.0"
400
+ modelName="{identifier}"
401
+ generationTool="FMUContainer-{tool_version}"
402
+ generationDateAndTime="{timestamp}"
403
+ guid="{guid}"
404
+ description="FMUContainer with {embedded_fmu}"
405
+ author="{author}"
406
+ license="Proprietary"
407
+ copyright="See Embedded FMU's copyrights."
408
+ variableNamingConvention="structured">
409
+
410
+ <CoSimulation
411
+ modelIdentifier="{identifier}"
412
+ canHandleVariableCommunicationStepSize="true"
413
+ canBeInstantiatedOnlyOncePerProcess="{only_once}"
414
+ canNotUseMemoryManagementFunctions="true"
415
+ canGetAndSetFMUstate="false"
416
+ canSerializeFMUstate="false"
417
+ providesDirectionalDerivative="false"
418
+ needsExecutionTool="{execution_tool}">
419
+ </CoSimulation>
420
+
421
+ <LogCategories>
422
+ <Category name="fmucontainer"/>
423
+ </LogCategories>
424
+
425
+ <DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
426
+
427
+ <ModelVariables>
428
+ <ScalarVariable valueReference="0" name="time" causality="independent"><Real /></ScalarVariable>
429
+ """
430
+
431
+ HEADER_XML_3 = """<?xml version="1.0" encoding="ISO-8859-1"?>
432
+ <fmiModelDescription
433
+ fmiVersion="3.0"
434
+ modelName="{identifier}"
435
+ generationTool="FMUContainer-{tool_version}"
436
+ generationDateAndTime="{timestamp}"
437
+ instantiationToken="{guid}"
438
+ description="FMUContainer with {embedded_fmu}"
439
+ author="{author}"
440
+ license="Proprietary"
441
+ copyright="See Embedded FMU's copyrights."
442
+ variableNamingConvention="structured">
443
+
444
+ <CoSimulation
445
+ modelIdentifier="{identifier}"
446
+ canHandleVariableCommunicationStepSize="true"
447
+ canBeInstantiatedOnlyOncePerProcess="{only_once}"
448
+ canNotUseMemoryManagementFunctions="true"
449
+ canGetAndSetFMUstate="false"
450
+ canSerializeFMUstate="false"
451
+ providesDirectionalDerivative="false"
452
+ needsExecutionTool="{execution_tool}">
453
+ </CoSimulation>
454
+
455
+ <LogCategories>
456
+ <Category name="fmucontainer"/>
457
+ </LogCategories>
458
+
459
+ <DefaultExperiment stepSize="{step_size}" startTime="{start_time}" stopTime="{stop_time}"/>
460
+
461
+ <ModelVariables>
462
+ <Float64 valueReference="0" name="time" causality="independent"/>
463
+ """
464
+
465
+ def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None, fmi_version=2):
259
466
  self.fmu_directory = Path(fmu_directory)
260
467
  self.identifier = identifier
261
468
  if not self.fmu_directory.is_dir():
262
469
  raise FMUContainerError(f"{self.fmu_directory} is not a valid directory")
263
- self.involved_fmu: Dict[str, EmbeddedFMU] = {}
264
- self.execution_order: List[EmbeddedFMU] = []
470
+ self.involved_fmu: OrderedDict[str, EmbeddedFMU] = OrderedDict()
265
471
 
266
472
  self.description_pathname = description_pathname
473
+ self.fmi_version = fmi_version
267
474
 
268
475
  self.start_time = None
269
476
  self.stop_time = None
@@ -271,20 +478,24 @@ class FMUContainer:
271
478
  # Rules
272
479
  self.inputs: Dict[str, ContainerInput] = {}
273
480
  self.outputs: Dict[str, ContainerPort] = {}
274
- self.locals: Dict[ContainerPort, Local] = {}
481
+ self.links: Dict[ContainerPort, Link] = {}
275
482
 
276
483
  self.rules: Dict[ContainerPort, str] = {}
277
484
  self.start_values: Dict[ContainerPort, str] = {}
278
485
 
486
+ self.vr_table = ValueReferenceTable()
487
+
279
488
  def get_fmu(self, fmu_filename: str) -> EmbeddedFMU:
280
489
  if fmu_filename in self.involved_fmu:
281
490
  return self.involved_fmu[fmu_filename]
282
491
 
283
492
  try:
284
493
  fmu = EmbeddedFMU(self.fmu_directory / fmu_filename)
494
+ if fmu.fmi_version > self.fmi_version:
495
+ logger.warning(f"Try to embed FMU-{fmu.fmi_version} into container FMI-{self.fmi_version}")
285
496
  self.involved_fmu[fmu.name] = fmu
286
- self.execution_order.append(fmu)
287
- logger.debug(f"Adding FMU #{len(self.execution_order)}: {fmu}")
497
+
498
+ logger.debug(f"Adding FMU #{len(self.involved_fmu)}: {fmu}")
288
499
  except (FMUContainerError, FMUError) as e:
289
500
  raise FMUContainerError(f"Cannot load '{fmu_filename}': {e}")
290
501
 
@@ -299,7 +510,7 @@ class FMUContainer:
299
510
  self.rules[cport] = rule
300
511
 
301
512
  def get_all_cports(self):
302
- return [ContainerPort(fmu, port_name) for fmu in self.execution_order for port_name in fmu.ports]
513
+ return [ContainerPort(fmu, port_name) for fmu in self.involved_fmu.values() for port_name in fmu.ports]
303
514
 
304
515
  def add_input(self, container_port_name: str, to_fmu_filename: str, to_port_name: str):
305
516
  if not container_port_name:
@@ -345,16 +556,17 @@ class FMUContainer:
345
556
  def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
346
557
  cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
347
558
  try:
348
- local = self.locals[cport_from]
559
+ local = self.links[cport_from]
349
560
  except KeyError:
350
- local = Local(cport_from)
561
+ local = Link(cport_from)
351
562
 
352
563
  cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
353
564
  local.add_target(cport_to) # Causality is check in the add() function
354
565
 
566
+ logger.debug(f"LINK: {cport_from} -> {cport_to}")
355
567
  self.mark_ruled(cport_from, 'LINK')
356
568
  self.mark_ruled(cport_to, 'LINK')
357
- self.locals[cport_from] = local
569
+ self.links[cport_from] = local
358
570
 
359
571
  def add_start_value(self, fmu_filename: str, port_name: str, value: str):
360
572
  cport = ContainerPort(self.get_fmu(fmu_filename), port_name)
@@ -373,7 +585,7 @@ class FMUContainer:
373
585
 
374
586
  self.start_values[cport] = value
375
587
 
376
- def find_inputs(self, port_to_connect: FMUPort) -> List[ContainerPort]:
588
+ def find_inputs(self, port_to_connect: EmbeddedFMUPort) -> List[ContainerPort]:
377
589
  candidates = []
378
590
  for cport in self.get_all_cports():
379
591
  if (cport.port.causality == 'input' and cport not in self.rules and cport.port.name == port_to_connect.name
@@ -432,7 +644,7 @@ class FMUContainer:
432
644
 
433
645
  def minimum_step_size(self) -> float:
434
646
  step_size = None
435
- for fmu in self.execution_order:
647
+ for fmu in self.involved_fmu.values():
436
648
  if step_size:
437
649
  if fmu.step_size and fmu.step_size < step_size:
438
650
  step_size = fmu.step_size
@@ -446,7 +658,7 @@ class FMUContainer:
446
658
  return step_size
447
659
 
448
660
  def sanity_check(self, step_size: Optional[float]):
449
- for fmu in self.execution_order:
661
+ for fmu in self.involved_fmu.values():
450
662
  if not fmu.step_size:
451
663
  continue
452
664
  ts_ratio = step_size / fmu.step_size
@@ -488,13 +700,11 @@ class FMUContainer:
488
700
  self.make_fmu_cleanup(base_directory)
489
701
 
490
702
  def make_fmu_xml(self, xml_file, step_size: float, profiling: bool):
491
- vr_table = ValueReferenceTable()
492
-
493
703
  timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
494
704
  guid = str(uuid.uuid4())
495
705
  embedded_fmu = ", ".join([fmu_name for fmu_name in self.involved_fmu])
496
706
  try:
497
- author = os.getlogin()
707
+ author = getpass.getuser()
498
708
  except OSError:
499
709
  author = "Unspecified"
500
710
 
@@ -505,94 +715,116 @@ class FMUContainer:
505
715
  if fmu.capabilities[capability] == "true":
506
716
  capabilities[capability] = "true"
507
717
 
718
+ first_fmu = next(iter(self.involved_fmu.values()))
508
719
  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}')")
720
+ self.start_time = first_fmu.start_time
721
+ logger.info(f"start_time={self.start_time} (deduced from '{first_fmu.name}')")
511
722
  else:
512
723
  logger.info(f"start_time={self.start_time}")
513
724
 
514
725
  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}')")
726
+ self.stop_time = first_fmu.stop_time
727
+ logger.info(f"stop_time={self.stop_time} (deduced from '{first_fmu.name}')")
517
728
  else:
518
729
  logger.info(f"stop_time={self.stop_time}")
519
730
 
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>
731
+ if self.fmi_version == 2:
732
+ xml_file.write(self.HEADER_XML_2.format(identifier=self.identifier, tool_version=tool_version,
733
+ timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
734
+ author=author,
735
+ only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
736
+ execution_tool=capabilities['needsExecutionTool'],
737
+ start_time=self.start_time, stop_time=self.stop_time,
738
+ step_size=step_size))
739
+ elif self.fmi_version == 3:
740
+ xml_file.write(self.HEADER_XML_3.format(identifier=self.identifier, tool_version=tool_version,
741
+ timestamp=timestamp, guid=guid, embedded_fmu=embedded_fmu,
742
+ author=author,
743
+ only_once=capabilities['canBeInstantiatedOnlyOncePerProcess'],
744
+ execution_tool=capabilities['needsExecutionTool'],
745
+ start_time=self.start_time, stop_time=self.stop_time,
746
+ step_size=step_size))
747
+
748
+ vr_time = self.vr_table.add_vr("real64", local=True)
749
+ logger.debug(f"Time vr = {vr_time}")
543
750
 
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
- """)
552
751
  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)
752
+ for fmu in self.involved_fmu.values():
753
+ vr = self.vr_table.add_vr("real64", local=True)
754
+ port = EmbeddedFMUPort("real64", {"valueReference": vr,
755
+ "name": f"container.{fmu.id}.rt_ratio",
756
+ "description": f"RT ratio for embedded FMU '{fmu.name}'"})
757
+ print(f" {port.xml(vr, fmi_version=self.fmi_version)}", file=xml_file)
758
+
759
+ index_offset = 2 # index of output ports. Start at 2 to skip "time" port
558
760
 
559
761
  # Local variable should be first to ensure to attribute them the lowest VR.
560
- 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)
563
- local.vr = vr
762
+ for link in self.links.values():
763
+ self.vr_table.set_link_vr(link)
764
+ port_local_def = link.cport_from.port.xml(link.vr, name=link.name, causality='local',
765
+ fmi_version=self.fmi_version)
766
+ if port_local_def:
767
+ print(f" {port_local_def}", file=xml_file)
768
+ index_offset += 1
564
769
 
565
770
  for input_port_name, input_port in self.inputs.items():
566
- vr = vr_table.add_vr(input_port.type_name)
771
+ input_port.vr = self.vr_table.add_vr(input_port.type_name)
567
772
  # Get Start and XML from first connected input
568
773
  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)
570
- input_port.vr = vr
571
-
572
- 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)
575
- cport.vr = vr
576
-
577
- xml_file.write(""" </ModelVariables>
578
-
579
- <ModelStructure>
580
- <Outputs>
581
- """)
582
-
583
- index_offset = len(self.locals) + len(self.inputs) + 1
584
- for i, _ in enumerate(self.outputs.keys()):
585
- print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
586
- xml_file.write(""" </Outputs>
587
- <InitialUnknowns>
588
- """)
589
- for i, _ in enumerate(self.outputs.keys()):
590
- print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
591
- xml_file.write(""" </InitialUnknowns>
592
- </ModelStructure>
593
-
594
- </fmiModelDescription>
595
- """)
774
+ port_input_def = input_port.cport_list[0].port.xml(input_port.vr, name=input_port_name,
775
+ start=start, fmi_version=self.fmi_version)
776
+ if port_input_def:
777
+ print(f" {port_input_def}", file=xml_file)
778
+ index_offset += 1
779
+
780
+ for output_port_name, output_port in self.outputs.items():
781
+ output_port.vr = self.vr_table.add_vr(output_port)
782
+ port_output_def = output_port.port.xml(output_port.vr, name=output_port_name,
783
+ fmi_version=self.fmi_version)
784
+ if port_output_def:
785
+ print(f" {port_output_def}", file=xml_file)
786
+
787
+ if self.fmi_version == 2:
788
+ self.make_fmu_xml_epilog_2(xml_file, index_offset)
789
+ elif self.fmi_version == 3:
790
+ self.make_fmu_xml_epilog_3(xml_file)
791
+
792
+ def make_fmu_xml_epilog_2(self, xml_file, index_offset):
793
+ xml_file.write(" </ModelVariables>\n"
794
+ "\n"
795
+ " <ModelStructure>\n"
796
+ " <Outputs>\n")
797
+
798
+ index = index_offset
799
+ for output in self.outputs.values():
800
+ if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[2]:
801
+ print(f' <Unknown index="{index}"/>', file=xml_file)
802
+ index += 1
803
+ xml_file.write(" </Outputs>\n"
804
+ " <InitialUnknowns>\n")
805
+ index = index_offset
806
+ for output in self.outputs.values():
807
+ if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[2]:
808
+ print(f' <Unknown index="{index}"/>', file=xml_file)
809
+ index += 1
810
+ xml_file.write(" </InitialUnknowns>\n"
811
+ " </ModelStructure>\n"
812
+ "\n"
813
+ "</fmiModelDescription>")
814
+
815
+ def make_fmu_xml_epilog_3(self, xml_file):
816
+ xml_file.write(" </ModelVariables>\n"
817
+ "\n"
818
+ " <ModelStructure>\n")
819
+ for output in self.outputs.values():
820
+ if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[3]:
821
+ print(f' <Output valueReference="{output.vr}"/>', file=xml_file)
822
+ for output in self.outputs.values():
823
+ if output.port.type_name in EmbeddedFMUPort.CONTAINER_TO_FMI[3]:
824
+ print(f' <InitialUnknown valueReference="{output.vr}"/>', file=xml_file)
825
+ xml_file.write(" </ModelStructure>\n"
826
+ "\n"
827
+ "</fmiModelDescription>")
596
828
 
597
829
  def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool, sequential: bool):
598
830
  print("# Container flags <MT> <Profiling> <Sequential>", file=txt_file)
@@ -604,32 +836,32 @@ class FMUContainer:
604
836
  print(f"# NB of embedded FMU's", file=txt_file)
605
837
  print(f"{len(self.involved_fmu)}", file=txt_file)
606
838
  fmu_rank: Dict[str, int] = {}
607
- for i, fmu in enumerate(self.execution_order):
608
- print(f"{fmu.name}", file=txt_file)
839
+ for i, fmu in enumerate(self.involved_fmu.values()):
840
+ print(f"{fmu.name} {fmu.fmi_version}", file=txt_file)
609
841
  print(f"{fmu.model_identifier}", file=txt_file)
610
842
  print(f"{fmu.guid}", file=txt_file)
611
843
  fmu_rank[fmu.name] = i
612
844
 
613
845
  # Prepare data structure
614
- type_names_list = ("Real", "Integer", "Boolean", "String") # Ordered list
615
- inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
846
+ inputs_per_type: Dict[str, List[ContainerInput]] = {} # Container's INPUT
616
847
  outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
617
848
 
618
849
  inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
619
850
  start_values_fmu_per_type = {}
620
851
  outputs_fmu_per_type = {}
621
- locals_per_type: Dict[str, List[Local]] = {}
852
+ local_per_type: Dict[str, List[int]] = {}
853
+ links_per_fmu: Dict[str, List[Link]] = {}
622
854
 
623
- for type_name in type_names_list:
855
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
624
856
  inputs_per_type[type_name] = []
625
857
  outputs_per_type[type_name] = []
626
- locals_per_type[type_name] = []
858
+ local_per_type[type_name] = []
627
859
 
628
860
  inputs_fmu_per_type[type_name] = {}
629
861
  start_values_fmu_per_type[type_name] = {}
630
862
  outputs_fmu_per_type[type_name] = {}
631
863
 
632
- for fmu in self.execution_order:
864
+ for fmu in self.involved_fmu.values():
633
865
  inputs_fmu_per_type[type_name][fmu.name] = {}
634
866
  start_values_fmu_per_type[type_name][fmu.name] = {}
635
867
  outputs_fmu_per_type[type_name][fmu.name] = {}
@@ -638,70 +870,88 @@ class FMUContainer:
638
870
  # Inputs
639
871
  for input_port_name, input_port in self.inputs.items():
640
872
  inputs_per_type[input_port.type_name].append(input_port)
641
- for cport, value in self.start_values.items():
642
- start_values_fmu_per_type[cport.port.type_name][cport.fmu.name][cport] = value
873
+ for input_port, value in self.start_values.items():
874
+ start_values_fmu_per_type[input_port.port.type_name][input_port.fmu.name][input_port] = value
643
875
  # Outputs
644
- for output_port_name, cport in self.outputs.items():
645
- outputs_per_type[cport.port.type_name].append(cport)
646
- # Locals
647
- for local in self.locals.values():
648
- vr = local.vr
649
- locals_per_type[local.cport_from.port.type_name].append(local)
650
- outputs_fmu_per_type[local.cport_from.port.type_name][local.cport_from.fmu.name][local.cport_from] = vr
651
- for cport_to in local.cport_to_list:
652
- inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
653
-
654
- print(f"# NB local variables Real, Integer, Boolean, String", file=txt_file)
655
- for type_name in type_names_list:
656
- 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='')
876
+ for output_port_name, output_port in self.outputs.items():
877
+ outputs_per_type[output_port.port.type_name].append(output_port)
878
+ # Links
879
+ for link in self.links.values():
880
+ local_per_type[link.cport_from.port.type_name].append(link.vr)
881
+ outputs_fmu_per_type[link.cport_from.port.type_name][link.cport_from.fmu.name][link.cport_from] = link.vr
882
+ for cport_to in link.cport_to_list:
883
+ if cport_to.port.type_name == link.cport_from.port.type_name:
884
+ inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = link.vr
885
+ else:
886
+ local_per_type[cport_to.port.type_name].append(link.vr_converted[cport_to.port.type_name])
887
+ links_per_fmu.setdefault(link.cport_from.fmu.name, []).append(link)
888
+ inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = link.vr_converted[cport_to.port.type_name]
889
+
890
+
891
+ print(f"# NB local variables:", ", ".join(EmbeddedFMUPort.ALL_TYPES), file=txt_file)
892
+ nb_local = [f"{self.vr_table.nb_local(type_name)}" for type_name in EmbeddedFMUPort.ALL_TYPES]
893
+ print(" ".join(nb_local), file=txt_file, end='')
660
894
  print("", file=txt_file)
661
895
 
662
896
  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:
664
- print(f"# {type_name}", file=txt_file)
665
- nb = len(inputs_per_type[type_name]) + len(outputs_per_type[type_name]) + len(locals_per_type[type_name])
897
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
898
+ print(f"# {type_name}" , file=txt_file)
899
+ nb_local = (len(inputs_per_type[type_name]) +
900
+ len(outputs_per_type[type_name]) +
901
+ self.vr_table.nb_local(type_name))
666
902
  nb_input_link = 0
667
903
  for input_port in inputs_per_type[type_name]:
668
904
  nb_input_link += len(input_port.cport_list) - 1
905
+ print(f"{nb_local} {nb_local + nb_input_link}", file=txt_file)
906
+ if type_name == "real64":
907
+ print(f"0 1 -1 0", file=txt_file) # Time slot
908
+ if profiling:
909
+ for profiling_port, _ in enumerate(self.involved_fmu.values()):
910
+ print(f"{profiling_port + 1} 1 -2 {profiling_port + 1}", file=txt_file)
669
911
 
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)
675
- else:
676
- print(f"{nb} {nb+nb_input_link}", file=txt_file)
677
912
  for input_port in inputs_per_type[type_name]:
678
913
  cport_string = [f"{fmu_rank[cport.fmu.name]} {cport.port.vr}" for cport in input_port.cport_list]
679
914
  print(f"{input_port.vr} {len(input_port.cport_list)}", " ".join(cport_string), file=txt_file)
680
- for cport in outputs_per_type[type_name]:
681
- print(f"{cport.vr} 1 {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
682
- for local in locals_per_type[type_name]:
683
- print(f"{local.vr} 1 -1 {local.vr}", file=txt_file)
915
+ for output_port in outputs_per_type[type_name]:
916
+ print(f"{output_port.vr} 1 {fmu_rank[output_port.fmu.name]} {output_port.port.vr}", file=txt_file)
917
+ for local_vr in local_per_type[type_name]:
918
+ print(f"{local_vr} 1 -1 {local_vr & 0xFFFFFF}", file=txt_file)
684
919
 
685
920
  # LINKS
686
- for fmu in self.execution_order:
687
- for type_name in type_names_list:
921
+ for fmu in self.involved_fmu.values():
922
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
688
923
  print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
689
924
  print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
690
- for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
691
- print(f"{vr} {cport.port.vr}", file=txt_file)
925
+ for input_port, vr in inputs_fmu_per_type[type_name][fmu.name].items():
926
+ print(f"{vr} {input_port.port.vr}", file=txt_file)
692
927
 
693
- for type_name in type_names_list:
928
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
694
929
  print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <RESET> <VALUE>", file=txt_file)
695
930
  print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
696
- for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
697
- reset = 1 if cport.port.causality == "input" else 0
698
- print(f"{cport.port.vr} {reset} {value}", file=txt_file)
931
+ for input_port, value in start_values_fmu_per_type[type_name][fmu.name].items():
932
+ reset = 1 if input_port.port.causality == "input" else 0
933
+ print(f"{input_port.port.vr} {reset} {value}", file=txt_file)
699
934
 
700
- for type_name in type_names_list:
935
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
701
936
  print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
702
937
  print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
703
- for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
704
- print(f"{vr} {cport.port.vr}", file=txt_file)
938
+ for output_port, vr in outputs_fmu_per_type[type_name][fmu.name].items():
939
+ print(f"{vr} {output_port.port.vr}", file=txt_file)
940
+
941
+ print(f"# Conversion table of {fmu.name}: <VR_FROM> <VR_TO> <CONVERSION>", file=txt_file)
942
+ try:
943
+ nb = 0
944
+ for link in links_per_fmu[fmu.name]:
945
+ nb += len(link.vr_converted)
946
+ print(f"{nb}", file=txt_file)
947
+ for link in links_per_fmu[fmu.name]:
948
+ for cport_to in link.cport_to_list:
949
+ conversion = link.get_conversion(cport_to)
950
+ if conversion:
951
+ print(f"{link.vr} {link.vr_converted[cport_to.port.type_name]} {conversion}",
952
+ file=txt_file)
953
+ except KeyError:
954
+ print("0", file=txt_file)
705
955
 
706
956
  @staticmethod
707
957
  def long_path(path: Union[str, Path]) -> str:
@@ -711,6 +961,39 @@ class FMUContainer:
711
961
  else:
712
962
  return path
713
963
 
964
+ @staticmethod
965
+ def copyfile(origin, destination):
966
+ logger.debug(f"Copying {origin} in {destination}")
967
+ shutil.copy(origin, destination)
968
+
969
+ def get_bindir_and_suffixe(self) -> Tuple[str, str, str]:
970
+ suffixes = {
971
+ "Windows": "dll",
972
+ "Linux": "so",
973
+ "Darwin": "dylib"
974
+ }
975
+
976
+ origin_bindirs = {
977
+ "Windows": "win64",
978
+ "Linux": "linux64",
979
+ "Darwin": "darwin64"
980
+ }
981
+
982
+ if self.fmi_version == 3:
983
+ target_bindirs = {
984
+ "Windows": "x86_64-windows",
985
+ "Linux": "x86_64-linux",
986
+ "Darwin": "aarch64-darwin"
987
+ }
988
+ else:
989
+ target_bindirs = origin_bindirs
990
+
991
+ os_name = platform.system()
992
+ try:
993
+ return origin_bindirs[os_name], suffixes[os_name], target_bindirs[os_name]
994
+ except KeyError:
995
+ raise FMUContainerError(f"OS '{os_name}' is not supported.")
996
+
714
997
  def make_fmu_skeleton(self, base_directory: Path) -> Path:
715
998
  logger.debug(f"Initialize directory '{base_directory}'")
716
999
 
@@ -725,20 +1008,23 @@ class FMUContainer:
725
1008
  documentation_directory.mkdir(exist_ok=True)
726
1009
 
727
1010
  if self.description_pathname:
728
- logger.debug(f"Copying {self.description_pathname}")
729
- shutil.copy(self.description_pathname, documentation_directory)
1011
+ self.copyfile(self.description_pathname, documentation_directory)
1012
+
1013
+ self.copyfile(origin / "model.png", base_directory)
730
1014
 
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")
1015
+ origin_bindir, suffixe, target_bindir = self.get_bindir_and_suffixe()
1016
+
1017
+ library_filename = origin / origin_bindir / f"container.{suffixe}"
1018
+ if not library_filename.is_file():
1019
+ raise FMUContainerError(f"File {library_filename} not found")
1020
+ binary_directory = binaries_directory / target_bindir
1021
+ binary_directory.mkdir(exist_ok=True)
1022
+ self.copyfile(library_filename, binary_directory / f"{self.identifier}.{suffixe}")
738
1023
 
739
1024
  for i, fmu in enumerate(self.involved_fmu.values()):
740
1025
  shutil.copytree(self.long_path(fmu.fmu.tmp_directory),
741
1026
  self.long_path(resources_directory / f"{i:02x}"), dirs_exist_ok=True)
1027
+
742
1028
  return resources_directory
743
1029
 
744
1030
  def make_fmu_package(self, base_directory: Path, fmu_filename: Path):