fmu-manipulation-toolbox 1.7.5__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 (46) hide show
  1. fmu_manipulation_toolbox/__init__.py +1 -0
  2. fmu_manipulation_toolbox/__main__.py +25 -0
  3. fmu_manipulation_toolbox/__version__.py +1 -0
  4. fmu_manipulation_toolbox/checker.py +61 -0
  5. fmu_manipulation_toolbox/cli.py +216 -0
  6. fmu_manipulation_toolbox/fmu_container.py +784 -0
  7. fmu_manipulation_toolbox/fmu_operations.py +489 -0
  8. fmu_manipulation_toolbox/gui.py +493 -0
  9. fmu_manipulation_toolbox/help.py +87 -0
  10. fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
  11. fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
  12. fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
  13. fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
  14. fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
  15. fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
  16. fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  17. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +58 -0
  18. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +78 -0
  19. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +345 -0
  20. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +218 -0
  21. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +89 -0
  22. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +116 -0
  23. fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +92 -0
  24. fmu_manipulation_toolbox/resources/fmu.png +0 -0
  25. fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
  26. fmu_manipulation_toolbox/resources/help.png +0 -0
  27. fmu_manipulation_toolbox/resources/icon.png +0 -0
  28. fmu_manipulation_toolbox/resources/license.txt +34 -0
  29. fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  30. fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  31. fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  32. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  33. fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
  34. fmu_manipulation_toolbox/resources/model.png +0 -0
  35. fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  36. fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  37. fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  38. fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  39. fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  40. fmu_manipulation_toolbox/version.py +9 -0
  41. fmu_manipulation_toolbox-1.7.5.dist-info/LICENSE.txt +22 -0
  42. fmu_manipulation_toolbox-1.7.5.dist-info/METADATA +20 -0
  43. fmu_manipulation_toolbox-1.7.5.dist-info/RECORD +46 -0
  44. fmu_manipulation_toolbox-1.7.5.dist-info/WHEEL +5 -0
  45. fmu_manipulation_toolbox-1.7.5.dist-info/entry_points.txt +3 -0
  46. fmu_manipulation_toolbox-1.7.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,784 @@
1
+ import csv
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import uuid
6
+ import zipfile
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import *
10
+
11
+ from .fmu_operations import FMU, OperationAbstract, FMUException
12
+ from .version import __version__ as tool_version
13
+
14
+ logger = logging.getLogger("fmu_manipulation_toolbox")
15
+
16
+
17
+ class FMUPort:
18
+ def __init__(self, attrs: Dict[str, str]):
19
+ self.name = attrs["name"]
20
+ self.vr = int(attrs["valueReference"])
21
+ self.causality = attrs.get("causality", "local")
22
+ self.attrs = attrs.copy()
23
+ self.attrs.pop("name")
24
+ self.attrs.pop("valueReference")
25
+ if "causality" in self.attrs:
26
+ self.attrs.pop("causality")
27
+ self.type_name = None
28
+ self.child = None
29
+
30
+ def set_port_type(self, type_name: str, attrs: Dict[str, str]):
31
+ self.type_name = type_name
32
+ self.child = attrs.copy()
33
+ try:
34
+ self.child.pop("unit") # Unit are not supported
35
+ except KeyError:
36
+ pass
37
+
38
+ def xml(self, vr: int, name=None, causality=None, start=None):
39
+
40
+ if self.child is None:
41
+ raise FMUException(f"FMUPort has no child. Bug?")
42
+
43
+ child_str = f"<{self.type_name}"
44
+ if self.child:
45
+ if start is not None and 'start' in self.child:
46
+ self.child['start'] = start
47
+ child_str += " " + " ".join([f'{key}="{value}"' for (key, value) in self.child.items()]) + "/>"
48
+ else:
49
+ child_str += "/>"
50
+
51
+ if name is None:
52
+ name = self.name
53
+ if causality is None:
54
+ causality = self.causality
55
+
56
+ variability = "continuous" if self.type_name == "Real" else "discrete"
57
+
58
+ scalar_attrs = {
59
+ "name": name,
60
+ "valueReference": vr,
61
+ "causality": causality,
62
+ "variability": variability,
63
+ }
64
+ scalar_attrs.update(self.attrs)
65
+
66
+ scalar_attrs_str = " ".join([f'{key}="{value}"' for (key, value) in scalar_attrs.items()])
67
+
68
+ return f'<ScalarVariable {scalar_attrs_str}>{child_str}</ScalarVariable>'
69
+
70
+
71
+ class EmbeddedFMU(OperationAbstract):
72
+ capability_list = ("needsExecutionTool",
73
+ "canHandleVariableCommunicationStepSize",
74
+ "canBeInstantiatedOnlyOncePerProcess")
75
+
76
+ def __init__(self, filename):
77
+ self.fmu = FMU(filename)
78
+ self.name = Path(filename).name
79
+
80
+ self.fmi_version = None
81
+ self.step_size = None
82
+ self.model_identifier = None
83
+ self.guid = None
84
+ self.ports: Dict[str, FMUPort] = {}
85
+
86
+ self.capabilities: Dict[str, str] = {}
87
+ self.current_port = None # used during apply_operation()
88
+
89
+ self.fmu.apply_operation(self) # Should be the last command in constructor!
90
+
91
+ def fmi_attrs(self, attrs):
92
+ self.guid = attrs['guid']
93
+ self.fmi_version = attrs['fmiVersion']
94
+
95
+ def scalar_attrs(self, attrs) -> int:
96
+ self.current_port = FMUPort(attrs)
97
+ self.ports[self.current_port.name] = self.current_port
98
+
99
+ return 0
100
+
101
+ def cosimulation_attrs(self, attrs: Dict[str, str]):
102
+ self.model_identifier = attrs['modelIdentifier']
103
+ for capability in self.capability_list:
104
+ self.capabilities[capability] = attrs.get(capability, "false")
105
+
106
+ def experiment_attrs(self, attrs):
107
+ self.step_size = float(attrs['stepSize'])
108
+
109
+ def scalar_type(self, type_name, attrs):
110
+ self.current_port.set_port_type(type_name, attrs)
111
+
112
+ def __repr__(self):
113
+ return f"FMU '{self.name}' ({len(self.ports)} variables)"
114
+
115
+
116
+ class FMUContainerError(Exception):
117
+ def __init__(self, reason: str):
118
+ self.reason = reason
119
+
120
+ def __repr__(self):
121
+ return f"{self.reason}"
122
+
123
+
124
+ class ContainerPort:
125
+ def __init__(self, fmu: EmbeddedFMU, port_name: str):
126
+ self.fmu = fmu
127
+ try:
128
+ self.port = fmu.ports[port_name]
129
+ except KeyError:
130
+ raise FMUContainerError(f"Port '{fmu.name}/{port_name}' does not exist")
131
+ self.vr = None
132
+
133
+ def __repr__(self):
134
+ return f"Port {self.fmu.name}/{self.port.name}"
135
+
136
+ def __hash__(self):
137
+ return hash(f"{self.fmu.name}/{self.port.name}")
138
+
139
+ def __eq__(self, other):
140
+ return str(self) == str(other)
141
+
142
+
143
+ class Local:
144
+ def __init__(self, cport_from: ContainerPort):
145
+ self.name = cport_from.fmu.name[:-4] + "." + cport_from.port.name # strip .fmu suffix
146
+ self.cport_from = cport_from
147
+ self.cport_to_list: List[ContainerPort] = []
148
+ self.vr = None
149
+
150
+ if not cport_from.port.causality == "output":
151
+ raise FMUContainerError(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT")
152
+
153
+ def add_target(self, cport_to: ContainerPort):
154
+ if not cport_to.port.causality == "input":
155
+ raise FMUContainerError(f"{cport_to} is {cport_to.port.causality} instead of INPUT")
156
+
157
+ if cport_to.port.type_name == self.cport_from.port.type_name:
158
+ self.cport_to_list.append(cport_to)
159
+ else:
160
+ raise FMUContainerError(f"failed to connect {self.cport_from} to {cport_to} due to type.")
161
+
162
+
163
+ class ValueReferenceTable:
164
+ def __init__(self):
165
+ self.vr_table: Dict[str, int] = {
166
+ "Real": 0,
167
+ "Integer": 0,
168
+ "Boolean": 0,
169
+ "String": 0,
170
+ }
171
+
172
+ def get_vr(self, cport: ContainerPort) -> int:
173
+ return self.add_vr(cport.port.type_name)
174
+
175
+ def add_vr(self, type_name:str) -> int:
176
+ vr = self.vr_table[type_name]
177
+ self.vr_table[type_name] += 1
178
+ return vr
179
+
180
+
181
+ class FMUContainer:
182
+ def __init__(self, identifier: str, fmu_directory: Union[str, Path]):
183
+ self.fmu_directory = Path(fmu_directory)
184
+ self.identifier = identifier
185
+ if not self.fmu_directory.is_dir():
186
+ raise FMUContainerError(f"{self.fmu_directory} is not a valid directory")
187
+ self.involved_fmu: Dict[str, EmbeddedFMU] = {}
188
+ self.execution_order: List[EmbeddedFMU] = []
189
+
190
+ self.description_pathname = None # Will be set up by FMUContainerSpecReader
191
+ self.period = None # Will be set up by FMUContainerSpecReader
192
+
193
+ # Rules
194
+ self.inputs: Dict[str, ContainerPort] = {}
195
+ self.outputs: Dict[str, ContainerPort] = {}
196
+ self.locals: Dict[ContainerPort, Local] = {}
197
+
198
+ self.rules: Dict[ContainerPort, str] = {}
199
+ self.start_values: Dict[ContainerPort, str] = {}
200
+
201
+ def get_fmu(self, fmu_filename: str) -> EmbeddedFMU:
202
+ if fmu_filename in self.involved_fmu:
203
+ return self.involved_fmu[fmu_filename]
204
+
205
+ try:
206
+ fmu = EmbeddedFMU(self.fmu_directory / fmu_filename)
207
+ self.involved_fmu[fmu_filename] = fmu
208
+ self.execution_order.append(fmu)
209
+ if not fmu.fmi_version == "2.0":
210
+ raise FMUException("Only FMI-2.0 is supported by FMUContainer")
211
+ logger.debug(f"Adding FMU #{len(self.execution_order)}: {fmu}")
212
+ except Exception as e:
213
+ raise FMUException(f"Cannot load '{fmu_filename}': {e}")
214
+
215
+ return fmu
216
+
217
+ def mark_ruled(self, cport: ContainerPort, rule: str):
218
+ if cport in self.rules:
219
+ previous_rule = self.rules[cport]
220
+ if rule not in ("OUTPUT", "LINK") and previous_rule not in ("OUTPUT", "LINK"):
221
+ raise FMUContainerError(f"try to {rule} port {cport} which is already {previous_rule}")
222
+
223
+ self.rules[cport] = rule
224
+
225
+ def add_input(self, container_port_name: str, to_fmu_filename: str, to_port_name: str):
226
+ if not container_port_name:
227
+ container_port_name = to_port_name
228
+ cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
229
+ if not cport_to.port.causality == "input": # check causality
230
+ raise FMUException(f"{cport_to} is {cport_to.port.causality} instead of INPUT.")
231
+
232
+ logger.debug(f"INPUT: {to_fmu_filename}:{to_port_name}")
233
+ self.mark_ruled(cport_to, 'INPUT')
234
+ self.inputs[container_port_name] = cport_to
235
+
236
+ def add_output(self, from_fmu_filename: str, from_port_name: str, container_port_name: str):
237
+ if not container_port_name: # empty is allowed
238
+ container_port_name = from_port_name
239
+
240
+ cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
241
+ if cport_from.port.causality not in ("output", "local"): # check causality
242
+ raise FMUException(f"{cport_from} is {cport_from.port.causality} instead of OUTPUT or LOCAL")
243
+
244
+ logger.debug(f"OUTPUT: {from_fmu_filename}:{from_port_name}")
245
+ self.mark_ruled(cport_from, 'OUTPUT')
246
+ self.outputs[container_port_name] = cport_from
247
+
248
+ def drop_port(self, from_fmu_filename: str, from_port_name: str):
249
+ cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
250
+ if not cport_from.port.causality == "output": # check causality
251
+ raise FMUException(f"{cport_from}: trying to DROP {cport_from.port.causality}")
252
+
253
+ logger.debug(f"DROP: {from_fmu_filename}:{from_port_name}")
254
+ self.mark_ruled(cport_from, 'DROP')
255
+
256
+ def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
257
+ cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
258
+ try:
259
+ local = self.locals[cport_from]
260
+ except KeyError:
261
+ local = Local(cport_from)
262
+
263
+ cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
264
+ local.add_target(cport_to) # Causality is check in the add() function
265
+
266
+ self.mark_ruled(cport_from, 'LINK')
267
+ self.mark_ruled(cport_to, 'LINK')
268
+ self.locals[cport_from] = local
269
+
270
+ def add_start_value(self, fmu_filename: str, port_name: str, value: str):
271
+ cport = ContainerPort(self.get_fmu(fmu_filename), port_name)
272
+
273
+ try:
274
+ if cport.port.type_name == 'Real':
275
+ value = float(value)
276
+ elif cport.port.type_name == 'Integer':
277
+ value = int(value)
278
+ elif cport.port.type_name == 'Boolean':
279
+ value = int(bool(value))
280
+ else:
281
+ value = value
282
+ except ValueError:
283
+ raise FMUContainerError(f"Start value is not conforming to '{cport.port.type_name}' format.")
284
+
285
+ self.start_values[cport] = value
286
+
287
+ def find_input(self, port_to_connect: FMUPort) -> Union[ContainerPort, None]:
288
+ for fmu in self.execution_order:
289
+ for port in fmu.ports.values():
290
+ if (port.causality == 'input' and port.name == port_to_connect.name
291
+ and port.type_name == port_to_connect.type_name):
292
+ return ContainerPort(fmu, port.name)
293
+ return None
294
+
295
+ def add_implicit_rule(self, auto_input: bool = True, auto_output: bool = True, auto_link: bool = True):
296
+ # Auto Link outputs
297
+ for fmu in self.execution_order:
298
+ for port_name in fmu.ports:
299
+ cport = ContainerPort(fmu, port_name)
300
+ if cport not in self.rules:
301
+ if cport.port.causality == 'output':
302
+ candidate_cport = self.find_input(cport.port)
303
+ if auto_link and candidate_cport:
304
+ local = Local(cport)
305
+ local.add_target(candidate_cport)
306
+ logger.info(f"AUTO LINK: {cport} -> {candidate_cport}")
307
+ self.mark_ruled(cport, 'LINK')
308
+ self.mark_ruled(candidate_cport, 'LINK')
309
+ self.locals[cport] = local
310
+ else:
311
+ if auto_output:
312
+ self.mark_ruled(cport, 'OUTPUT')
313
+ self.outputs[port_name] = cport
314
+ logger.info(f"AUTO OUTPUT: Expose {cport}")
315
+
316
+ if auto_input:
317
+ # Auto link inputs
318
+ for fmu in self.execution_order:
319
+ for port_name in fmu.ports:
320
+ cport = ContainerPort(fmu, port_name)
321
+ if cport not in self.rules:
322
+ if cport.port.causality == 'input':
323
+ self.mark_ruled(cport, 'INPUT')
324
+ self.inputs[port_name] = cport
325
+ logger.info(f"AUTO INPUT: Expose {cport}")
326
+
327
+ def minimum_step_size(self) -> float:
328
+ step_size = None
329
+ for fmu in self.execution_order:
330
+ if step_size:
331
+ if fmu.step_size and fmu.step_size < step_size:
332
+ step_size = fmu.step_size
333
+ else:
334
+ step_size = fmu.step_size
335
+
336
+ if not step_size:
337
+ step_size = 0.1
338
+ logger.warning(f"Defaulting to step_size={step_size}")
339
+
340
+ return step_size
341
+
342
+ def sanity_check(self, step_size: Union[float, None]):
343
+ nb_error = 0
344
+ for fmu in self.execution_order:
345
+ ts_ratio = step_size / fmu.step_size
346
+ if ts_ratio < 1.0:
347
+ logger.error(f"Container step_size={step_size}s is lower than FMU '{fmu.name}' "
348
+ f"step_size={fmu.step_size}s")
349
+ if ts_ratio != int(ts_ratio):
350
+ logger.error(f"Container step_size={step_size}s should divisible by FMU '{fmu.name}' "
351
+ f"step_size={fmu.step_size}s")
352
+ for port_name in fmu.ports:
353
+ cport = ContainerPort(fmu, port_name)
354
+ if cport not in self.rules:
355
+ if cport.port.causality == 'input':
356
+ logger.error(f"{cport} is not connected")
357
+ nb_error += 1
358
+ if cport.port.causality == 'output':
359
+ logger.warning(f"{cport} is not connected")
360
+
361
+ if nb_error:
362
+ raise FMUContainerError(f"Some ports are not connected.")
363
+
364
+ def make_fmu(self, fmu_filename: Union[str, Path], step_size: Union[float, None] = None, debug=False, mt=False,
365
+ profiling=False):
366
+ if isinstance(fmu_filename, str):
367
+ fmu_filename = Path(fmu_filename)
368
+
369
+ if step_size is None:
370
+ logger.info(f"step_size will be deduced from the embedded FMU's")
371
+ step_size = self.minimum_step_size()
372
+ self.sanity_check(step_size)
373
+
374
+ logger.info(f"Building FMU '{fmu_filename}', step_size={step_size}")
375
+
376
+ base_directory = self.fmu_directory / fmu_filename.with_suffix('')
377
+ resources_directory = self.make_fmu_skeleton(base_directory)
378
+ with open(base_directory / "modelDescription.xml", "wt") as xml_file:
379
+ self.make_fmu_xml(xml_file, step_size, profiling)
380
+ with open(resources_directory / "container.txt", "wt") as txt_file:
381
+ self.make_fmu_txt(txt_file, step_size, mt, profiling)
382
+
383
+ self.make_fmu_package(base_directory, fmu_filename)
384
+ if not debug:
385
+ self.make_fmu_cleanup(base_directory)
386
+
387
+ def make_fmu_xml(self, xml_file, step_size: float, profiling: bool):
388
+ vr_table = ValueReferenceTable()
389
+
390
+ timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
391
+ guid = str(uuid.uuid4())
392
+ embedded_fmu = ", ".join([fmu_name for fmu_name in self.involved_fmu])
393
+ try:
394
+ author = os.getlogin()
395
+ except OSError:
396
+ author = "Unspecified"
397
+
398
+ capabilities = {}
399
+ for capability in EmbeddedFMU.capability_list:
400
+ capabilities[capability] = "false"
401
+ for fmu in self.involved_fmu.values():
402
+ if fmu.capabilities[capability] == "true":
403
+ capabilities[capability] = "true"
404
+
405
+ xml_file.write(f"""<?xml version="1.0" encoding="ISO-8859-1"?>
406
+ <fmiModelDescription
407
+ fmiVersion="2.0"
408
+ modelName="{self.identifier}"
409
+ generationTool="FMUContainer-{tool_version}"
410
+ generationDateAndTime="{timestamp}"
411
+ guid="{guid}"
412
+ description="FMUContainer with {embedded_fmu}"
413
+ author="{author}"
414
+ license="Proprietary"
415
+ copyright="© Renault S.A.S"
416
+ variableNamingConvention="structured">
417
+
418
+ <CoSimulation
419
+ modelIdentifier="{self.identifier}"
420
+ canHandleVariableCommunicationStepSize="{capabilities['canHandleVariableCommunicationStepSize']}"
421
+ canBeInstantiatedOnlyOncePerProcess="{capabilities['canBeInstantiatedOnlyOncePerProcess']}"
422
+ canNotUseMemoryManagementFunctions="true"
423
+ canGetAndSetFMUstate="false"
424
+ canSerializeFMUstate="false"
425
+ providesDirectionalDerivative="false"
426
+ needsExecutionTool="{capabilities['needsExecutionTool']}">
427
+ </CoSimulation>
428
+
429
+ <LogCategories>
430
+ <Category name="fmucontainer"/>
431
+ </LogCategories>
432
+
433
+ <DefaultExperiment stepSize="{step_size}"/>
434
+
435
+ <ModelVariables>
436
+ """)
437
+ if profiling:
438
+ for fmu in self.execution_order:
439
+ vr = vr_table.add_vr("Real")
440
+ name = f"container.{fmu.model_identifier}.rt_ratio"
441
+ print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local"><Real /></ScalarVariable>', file=xml_file)
442
+
443
+ # Local variable should be first to ensure to attribute them the lowest VR.
444
+ for local in self.locals.values():
445
+ vr = vr_table.get_vr(local.cport_from)
446
+ print(f' {local.cport_from.port.xml(vr, name=local.name, causality="local")}', file=xml_file)
447
+ local.vr = vr
448
+
449
+ for input_port_name, cport in self.inputs.items():
450
+ vr = vr_table.get_vr(cport)
451
+ start = self.start_values.get(cport, None)
452
+ print(f" {cport.port.xml(vr, name=input_port_name, start=start)}", file=xml_file)
453
+ cport.vr = vr
454
+
455
+ for output_port_name, cport in self.outputs.items():
456
+ vr = vr_table.get_vr(cport)
457
+ print(f" {cport.port.xml(vr, name=output_port_name)}", file=xml_file)
458
+ cport.vr = vr
459
+
460
+ xml_file.write(""" </ModelVariables>
461
+
462
+ <ModelStructure>
463
+ <Outputs>
464
+ """)
465
+
466
+ index_offset = len(self.locals) + len(self.inputs) + 1
467
+ for i, _ in enumerate(self.outputs.keys()):
468
+ print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
469
+ xml_file.write(""" </Outputs>
470
+ <InitialUnknowns>
471
+ """)
472
+ for i, _ in enumerate(self.outputs.keys()):
473
+ print(f' <Unknown index="{index_offset+i}"/>', file=xml_file)
474
+ xml_file.write(""" </InitialUnknowns>
475
+ </ModelStructure>
476
+
477
+ </fmiModelDescription>
478
+ """)
479
+
480
+ def make_fmu_txt(self, txt_file, step_size: float, mt: bool, profiling: bool):
481
+ if mt:
482
+ print("# Use MT\n1", file=txt_file)
483
+ else:
484
+ print("# Don't use MT\n0", file=txt_file)
485
+
486
+ if profiling:
487
+ print("# Profiling ENABLED\n1", file=txt_file)
488
+ else:
489
+ print("# Profiling DISABLED\n0", file=txt_file)
490
+
491
+ print(f"# Internal time step in seconds", file=txt_file)
492
+ print(f"{step_size}", file=txt_file)
493
+ print(f"# NB of embedded FMU's", file=txt_file)
494
+ print(f"{len(self.involved_fmu)}", file=txt_file)
495
+ fmu_rank: Dict[str, int] = {}
496
+ for i, fmu in enumerate(self.execution_order):
497
+ print(f"{fmu.name}", file=txt_file)
498
+ print(f"{fmu.model_identifier}", file=txt_file)
499
+ print(f"{fmu.guid}", file=txt_file)
500
+ fmu_rank[fmu.name] = i
501
+
502
+ # Prepare data structure
503
+ type_names_list = ("Real", "Integer", "Boolean", "String") # Ordered list
504
+ inputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's INPUT
505
+ outputs_per_type: Dict[str, List[ContainerPort]] = {} # Container's OUTPUT
506
+
507
+ inputs_fmu_per_type: Dict[str, Dict[str, Dict[ContainerPort, int]]] = {} # [type][fmu]
508
+ start_values_fmu_per_type = {}
509
+ outputs_fmu_per_type = {}
510
+ locals_per_type: Dict[str, List[Local]] = {}
511
+
512
+ for type_name in type_names_list:
513
+ inputs_per_type[type_name] = []
514
+ outputs_per_type[type_name] = []
515
+ locals_per_type[type_name] = []
516
+
517
+ inputs_fmu_per_type[type_name] = {}
518
+ start_values_fmu_per_type[type_name] = {}
519
+ outputs_fmu_per_type[type_name] = {}
520
+
521
+ for fmu in self.execution_order:
522
+ inputs_fmu_per_type[type_name][fmu.name] = {}
523
+ start_values_fmu_per_type[type_name][fmu.name] = {}
524
+ outputs_fmu_per_type[type_name][fmu.name] = {}
525
+
526
+ # Fill data structure
527
+ # Inputs
528
+ for input_port_name, cport in self.inputs.items():
529
+ inputs_per_type[cport.port.type_name].append(cport)
530
+ for cport, value in self.start_values.items():
531
+ start_values_fmu_per_type[cport.port.type_name][cport.fmu.name][cport] = value
532
+ # Outputs
533
+ for output_port_name, cport in self.outputs.items():
534
+ outputs_per_type[cport.port.type_name].append(cport)
535
+ # Locals
536
+ for local in self.locals.values():
537
+ vr = local.vr
538
+ locals_per_type[local.cport_from.port.type_name].append(local)
539
+ outputs_fmu_per_type[local.cport_from.port.type_name][local.cport_from.fmu.name][local.cport_from] = vr
540
+ for cport_to in local.cport_to_list:
541
+ inputs_fmu_per_type[cport_to.port.type_name][cport_to.fmu.name][cport_to] = vr
542
+
543
+ print(f"# NB local variables Real, Integer, Boolean, String", file=txt_file)
544
+ for type_name in type_names_list:
545
+ nb = len(locals_per_type[type_name])
546
+ if profiling and type_name == "Real":
547
+ nb += len(self.execution_order)
548
+ print(f"{nb} ", file=txt_file, end='')
549
+ print("", file=txt_file)
550
+
551
+ print("# CONTAINER I/O: <VR> <FMU_INDEX> <FMU_VR>", file=txt_file)
552
+ for type_name in type_names_list:
553
+ print(f"# {type_name}", file=txt_file)
554
+ nb = len(inputs_per_type[type_name])+len(outputs_per_type[type_name])+len(locals_per_type[type_name])
555
+ if profiling and type_name == "Real":
556
+ nb += len(self.execution_order)
557
+ print(nb, file=txt_file)
558
+ for profiling_port,_ in enumerate(self.execution_order):
559
+ print(f"{profiling_port} -2 {profiling_port}", file=txt_file)
560
+ else:
561
+ print(nb, file=txt_file)
562
+ for cport in inputs_per_type[type_name]:
563
+ print(f"{cport.vr} {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
564
+ for cport in outputs_per_type[type_name]:
565
+ print(f"{cport.vr} {fmu_rank[cport.fmu.name]} {cport.port.vr}", file=txt_file)
566
+ for local in locals_per_type[type_name]:
567
+ print(f"{local.vr} -1 {local.vr}", file=txt_file)
568
+
569
+ # LINKS
570
+ for fmu in self.execution_order:
571
+ for type_name in type_names_list:
572
+ print(f"# Inputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
573
+ print(len(inputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
574
+ for cport, vr in inputs_fmu_per_type[type_name][fmu.name].items():
575
+ print(f"{vr} {cport.port.vr}", file=txt_file)
576
+
577
+ for type_name in type_names_list:
578
+ print(f"# Start values of {fmu.name} - {type_name}: <FMU_VR> <VALUE>", file=txt_file)
579
+ print(len(start_values_fmu_per_type[type_name][fmu.name]), file=txt_file)
580
+ for cport, value in start_values_fmu_per_type[type_name][fmu.name].items():
581
+ print(f"{cport.port.vr} {value}", file=txt_file)
582
+
583
+ for type_name in type_names_list:
584
+ print(f"# Outputs of {fmu.name} - {type_name}: <VR> <FMU_VR>", file=txt_file)
585
+ print(len(outputs_fmu_per_type[type_name][fmu.name]), file=txt_file)
586
+ for cport, vr in outputs_fmu_per_type[type_name][fmu.name].items():
587
+ print(f"{vr} {cport.port.vr}", file=txt_file)
588
+
589
+ def make_fmu_skeleton(self, base_directory: Path) -> Path:
590
+ logger.debug(f"Initialize directory '{base_directory}'")
591
+
592
+ origin = Path(__file__).parent / "resources"
593
+ resources_directory = base_directory / "resources"
594
+ documentation_directory = base_directory / "documentation"
595
+ binaries_directory = base_directory / "binaries"
596
+
597
+ base_directory.mkdir(exist_ok=True)
598
+ resources_directory.mkdir(exist_ok=True)
599
+ binaries_directory.mkdir(exist_ok=True)
600
+ documentation_directory.mkdir(exist_ok=True)
601
+
602
+ if self.description_pathname:
603
+ shutil.copy(self.description_pathname, documentation_directory)
604
+
605
+ shutil.copy(origin / "model.png", base_directory)
606
+ for bitness in ('win32', 'win64'):
607
+ library_filename = origin / bitness / "container.dll"
608
+ if library_filename.is_file():
609
+ binary_directory = binaries_directory / bitness
610
+ binary_directory.mkdir(exist_ok=True)
611
+ shutil.copy(library_filename, binary_directory / f"{self.identifier}.dll")
612
+
613
+ for fmu in self.involved_fmu.values():
614
+ shutil.copytree(fmu.fmu.tmp_directory, resources_directory / fmu.name, dirs_exist_ok=True)
615
+
616
+ return resources_directory
617
+
618
+ def make_fmu_package(self, base_directory: Path, fmu_filename: Path):
619
+ logger.debug(f"Zipping directory '{base_directory}' => '{fmu_filename}'")
620
+ with zipfile.ZipFile(self.fmu_directory / fmu_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
621
+ for root, dirs, files in os.walk(base_directory):
622
+ for file in files:
623
+ zip_file.write(os.path.join(root, file),
624
+ os.path.relpath(os.path.join(root, file), base_directory))
625
+ logger.info(f"'{fmu_filename}' is available.")
626
+
627
+ @staticmethod
628
+ def make_fmu_cleanup(base_directory: Path):
629
+ logger.debug(f"Delete directory '{base_directory}'")
630
+ shutil.rmtree(base_directory)
631
+
632
+
633
+ class FMUContainerSpecReader:
634
+ def __init__(self, fmu_directory: Union[Path, str]):
635
+ self.fmu_directory = Path(fmu_directory)
636
+
637
+ def read(self, description_filename: Union[str, Path]) -> FMUContainer:
638
+ if isinstance(description_filename, str):
639
+ description_filename = Path(description_filename)
640
+
641
+ if description_filename.suffix == ".csv":
642
+ return self.read_csv(description_filename)
643
+ else:
644
+ logger.critical(f"Unable to read from '{description_filename}': format unsupported.")
645
+
646
+ def read_csv(self, description_filename: Path) -> FMUContainer:
647
+ container = FMUContainer(description_filename.stem, self.fmu_directory)
648
+ container.description_pathname = self.fmu_directory / description_filename
649
+ logger.info(f"Building FMU Container from '{container.description_pathname}'")
650
+
651
+ with open(container.description_pathname) as file:
652
+ reader = csv.reader(file, delimiter=';')
653
+ self.check_headers(reader)
654
+ for i, row in enumerate(reader):
655
+ if not row or row[0][0] == '#': # skip blank line of comment
656
+ continue
657
+
658
+ try:
659
+ rule, from_fmu_filename, from_port_name, to_fmu_filename, to_port_name = row
660
+ except ValueError:
661
+ logger.error(f"Line #{i+2}: expecting 5 columns. Line skipped.")
662
+ continue
663
+
664
+ rule = rule.upper()
665
+ if rule in ("LINK", "INPUT", "OUTPUT", "DROP", "FMU", "START"):
666
+ try:
667
+ self._read_csv_rule(container, rule,
668
+ from_fmu_filename, from_port_name,
669
+ to_fmu_filename, to_port_name)
670
+ except FMUContainerError as e:
671
+ logger.error(f"Line #{i+2}: {e}. Line skipped.")
672
+ continue
673
+ except FMUException as e:
674
+ logger.critical(f"Line #{i + 2}: {e}.")
675
+ raise
676
+ else:
677
+ logger.error(f"Line #{i+2}: unexpected rule '{rule}'. Line skipped.")
678
+
679
+ return container
680
+
681
+ @staticmethod
682
+ def _read_csv_rule(container: FMUContainer, rule: str, from_fmu_filename: str, from_port_name: str,
683
+ to_fmu_filename: str, to_port_name: str):
684
+ if rule == "FMU":
685
+ if not from_fmu_filename:
686
+ raise FMUException("Missing FMU information.")
687
+ container.get_fmu(from_fmu_filename)
688
+
689
+ elif rule == "INPUT":
690
+ if not to_fmu_filename or not to_port_name:
691
+ raise FMUException("Missing INPUT ports information.")
692
+ container.add_input(from_port_name, to_fmu_filename, to_port_name)
693
+
694
+ elif rule == "OUTPUT":
695
+ if not from_fmu_filename or not from_port_name:
696
+ raise FMUException("Missing OUTPUT ports information.")
697
+ container.add_output(from_fmu_filename, from_port_name, to_port_name)
698
+
699
+ elif rule == "DROP":
700
+ if not from_fmu_filename or not from_port_name:
701
+ raise FMUException("Missing DROP ports information.")
702
+ container.drop_port(from_fmu_filename, from_port_name)
703
+
704
+ elif rule == "LINK":
705
+ container.add_link(from_fmu_filename, from_port_name, to_fmu_filename, to_port_name)
706
+
707
+ elif rule == "START":
708
+ if not from_fmu_filename or not from_port_name or not to_fmu_filename:
709
+ raise FMUException("Missing START ports information.")
710
+
711
+ container.add_start_value(from_fmu_filename, from_port_name, to_fmu_filename)
712
+ # no else: check on rule is already done in read_description()
713
+
714
+ @staticmethod
715
+ def check_headers(reader):
716
+ headers = next(reader)
717
+ if not headers == ["rule", "from_fmu", "from_port", "to_fmu", "to_port"]:
718
+ raise FMUContainerError("Header (1st line of the file) is not well formatted.")
719
+
720
+
721
+ class FMUContainerSpecWriter:
722
+ def __init__(self, container: FMUContainer):
723
+ self.container = container
724
+
725
+ def write(self, description_filename: Union[str, Path]):
726
+ if description_filename.endswith(".csv"):
727
+ return self.write_csv(description_filename)
728
+ elif description_filename.endswith(".json"):
729
+ return self.write_json(description_filename)
730
+ else:
731
+ logger.critical(f"Unable to write to '{description_filename}': format unsupported.")
732
+
733
+ def write_csv(self, description_filename: Union[str, Path]):
734
+ with open(description_filename, "wt") as outfile:
735
+ print("rule;from_fmu;from_port;to_fmu;to_port", file=outfile)
736
+ for fmu in self.container.involved_fmu.keys():
737
+ print(f"FMU;{fmu};;;", file=outfile)
738
+ for cport in self.container.inputs.values():
739
+ print(f"INPUT;;;{cport.fmu.name};{cport.port.name}", file=outfile)
740
+ for cport in self.container.outputs.values():
741
+ print(f"OUTPUT;{cport.fmu.name};{cport.port.name};;", file=outfile)
742
+ for local in self.container.locals.values():
743
+ for target in local.cport_to_list:
744
+ print(f"LINK;{local.cport_from.fmu.name};{local.cport_from.port.name};"
745
+ f"{target.fmu.name};{target.port.name}",
746
+ file=outfile)
747
+ for cport, value in self.container.start_values.items():
748
+ print(f"START;{cport.fmu.name};{cport.port.name};{value};", file=outfile)
749
+
750
+ def write_json(self, description_filename: Union[str, Path]):
751
+ with open(description_filename, "wt") as outfile:
752
+ print("{", file=outfile)
753
+
754
+ print(f' "fmu": [', file=outfile)
755
+ fmus = [f' "{fmu}"' for fmu in self.container.involved_fmu.keys()]
756
+ print(",\n".join(fmus), file=outfile)
757
+ print(f' ],', file=outfile)
758
+
759
+ print(f' "input": [', file=outfile)
760
+ inputs = [f' [{cport.fmu.name}, {cport.port.name}, {container_name}]'
761
+ for container_name, cport in self.container.inputs.items()]
762
+ print(",\n".join(inputs), file=outfile)
763
+ print(f' ],', file=outfile)
764
+
765
+ print(f' "output": [', file=outfile)
766
+ outputs = [f' ["{cport.fmu.name}", "{cport.port.name}", "{container_name}"]'
767
+ for container_name, cport in self.container.outputs.items()]
768
+ print(",\n".join(outputs), file=outfile)
769
+ print(f' ],', file=outfile)
770
+
771
+ print(f' "link": [', file=outfile)
772
+ links = [f' ["{local.cport_from.fmu.name}", "{local.cport_from.port.name}", '
773
+ f'"{target.fmu.name}", "{target.port.name}"]'
774
+ for local in self.container.locals.values()
775
+ for target in local.cport_to_list]
776
+ print(",\n".join(links), file=outfile)
777
+ print(f' ],', file=outfile)
778
+ print(f' "start": [', file=outfile)
779
+ start = [f' ["{cport.fmu.name}", "{cport.port.name}, "{value}"]'
780
+ for cport, value in self.container.start_values.items()]
781
+ print(f' ],', file=outfile)
782
+
783
+ #print(f' "period": {self.container.})
784
+ print("}", file=outfile)