fmu-manipulation-toolbox 1.8.4.2rc1__py3-none-any.whl → 1.9__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 (51) 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 +22 -13
  5. fmu_manipulation_toolbox/checker.py +16 -11
  6. fmu_manipulation_toolbox/cli/__init__.py +0 -0
  7. fmu_manipulation_toolbox/cli/fmucontainer.py +105 -0
  8. fmu_manipulation_toolbox/cli/fmusplit.py +48 -0
  9. fmu_manipulation_toolbox/cli/fmutool.py +127 -0
  10. fmu_manipulation_toolbox/cli/utils.py +36 -0
  11. fmu_manipulation_toolbox/container.py +1054 -0
  12. fmu_manipulation_toolbox/gui.py +48 -56
  13. fmu_manipulation_toolbox/gui_style.py +8 -0
  14. fmu_manipulation_toolbox/help.py +3 -0
  15. fmu_manipulation_toolbox/operations.py +577 -0
  16. fmu_manipulation_toolbox/remoting.py +107 -0
  17. fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
  18. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Annotation.xsd +51 -0
  19. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3AttributeGroups.xsd +119 -0
  20. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3BuildDescription.xsd +117 -0
  21. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3InterfaceType.xsd +80 -0
  22. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3LayeredStandardManifest.xsd +93 -0
  23. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3ModelDescription.xsd +131 -0
  24. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Terminal.xsd +87 -0
  25. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3TerminalsAndIcons.xsd +84 -0
  26. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Type.xsd +207 -0
  27. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Unit.xsd +69 -0
  28. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Variable.xsd +413 -0
  29. fmu_manipulation_toolbox/resources/fmi-3.0/fmi3VariableDependency.xsd +64 -0
  30. fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  31. fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  32. fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  33. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  34. fmu_manipulation_toolbox/resources/linux64/server_sm +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/split.py +331 -0
  41. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/METADATA +1 -1
  42. fmu_manipulation_toolbox-1.9.dist-info/RECORD +71 -0
  43. fmu_manipulation_toolbox-1.9.dist-info/entry_points.txt +7 -0
  44. fmu_manipulation_toolbox/cli.py +0 -235
  45. fmu_manipulation_toolbox/fmu_container.py +0 -753
  46. fmu_manipulation_toolbox/fmu_operations.py +0 -489
  47. fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/RECORD +0 -52
  48. fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/entry_points.txt +0 -3
  49. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/WHEEL +0 -0
  50. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/licenses/LICENSE.txt +0 -0
  51. {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,577 @@
1
+ import csv
2
+ import html
3
+ import logging
4
+ import os
5
+ import re
6
+ import shutil
7
+ import tempfile
8
+ import xml.parsers.expat
9
+ import zipfile
10
+ import hashlib
11
+ from typing import *
12
+
13
+ logger = logging.getLogger("fmu_manipulation_toolbox")
14
+
15
+ class FMU:
16
+ """Unpack and Repack facilities for FMU package. Once unpacked, we can process Operation on
17
+ modelDescription.xml file."""
18
+
19
+ FMI2_TYPES = ('Real', 'Integer', 'String', 'Boolean', 'Enumeration')
20
+ FMI3_TYPES = ('Float64', 'Float32',
21
+ 'Int8', 'UInt8', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64',
22
+ 'String', 'Boolean', 'Enumeration')
23
+
24
+ def __init__(self, fmu_filename):
25
+ self.fmu_filename = fmu_filename
26
+ self.tmp_directory = tempfile.mkdtemp()
27
+ self.fmi_version = None
28
+
29
+ try:
30
+ with zipfile.ZipFile(self.fmu_filename) as zin:
31
+ zin.extractall(self.tmp_directory)
32
+ except FileNotFoundError:
33
+ raise FMUError(f"'{fmu_filename}' does not exist")
34
+ self.descriptor_filename = os.path.join(self.tmp_directory, "modelDescription.xml")
35
+ if not os.path.isfile(self.descriptor_filename):
36
+ raise FMUError(f"'{fmu_filename}' is not valid: {self.descriptor_filename} not found")
37
+
38
+ def __del__(self):
39
+ shutil.rmtree(self.tmp_directory)
40
+
41
+ def save_descriptor(self, filename):
42
+ shutil.copyfile(os.path.join(self.tmp_directory, "modelDescription.xml"), filename)
43
+
44
+ def repack(self, filename):
45
+ with zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) as zout:
46
+ for root, dirs, files in os.walk(self.tmp_directory):
47
+ for file in files:
48
+ zout.write(os.path.join(root, file),
49
+ os.path.relpath(os.path.join(root, file), self.tmp_directory))
50
+ # TODO: Add check on output file
51
+
52
+ def apply_operation(self, operation, apply_on=None):
53
+ manipulation = Manipulation(operation, self)
54
+ manipulation.manipulate(self.descriptor_filename, apply_on)
55
+
56
+
57
+ class FMUPort:
58
+ def __init__(self):
59
+ self.fmi_type = None
60
+ self.attrs_list: List[Dict] = []
61
+ self.dimension = None
62
+
63
+ def dict_level(self, nb):
64
+ return " ".join([f'{key}="{value}"' for key, value in self.attrs_list[nb].items()])
65
+
66
+ def write_xml(self, fmi_version: int, file):
67
+ if fmi_version == 2:
68
+ print(f" <ScalarVariable {self.dict_level(0)}>", file=file)
69
+ print(f" <{self.fmi_type} {self.dict_level(1)}/>", file=file)
70
+ print(f" </ScalarVariable>", file=file)
71
+ elif fmi_version == 3:
72
+ print(f" <{self.fmi_type} {self.dict_level(0)}/>", file=file)
73
+ else:
74
+ raise FMUError(f"FMUPort writing: unsupported FMI version {fmi_version}")
75
+
76
+ def __contains__(self, item):
77
+ for attrs in self.attrs_list:
78
+ if item in attrs:
79
+ return True
80
+ return False
81
+
82
+ def __getitem__(self, item):
83
+ for attrs in self.attrs_list:
84
+ if item in attrs:
85
+ return attrs[item]
86
+ raise KeyError
87
+
88
+ def __setitem__(self, key, value):
89
+ for attrs in self.attrs_list:
90
+ if key in attrs:
91
+ attrs[key] = value
92
+ return
93
+ raise KeyError
94
+
95
+ def get(self, item, default_value):
96
+ try:
97
+ return self[item]
98
+ except KeyError:
99
+ return default_value
100
+
101
+ def push_attrs(self, attrs):
102
+ self.attrs_list.append(attrs)
103
+
104
+
105
+ class FMUError(Exception):
106
+ def __init__(self, reason):
107
+ self.reason = reason
108
+
109
+ def __repr__(self):
110
+ return self.reason
111
+
112
+
113
+ class Manipulation:
114
+ """Parse modelDescription.xml file and create a modified version"""
115
+ TAGS_MODEL_STRUCTURE = ("InitialUnknowns", "Derivatives", "Outputs")
116
+
117
+ def __init__(self, operation, fmu):
118
+ self.output_filename = tempfile.mktemp()
119
+ self.out = None
120
+ self.operation = operation
121
+ self.parser = xml.parsers.expat.ParserCreate()
122
+ self.parser.StartElementHandler = self.start_element
123
+ self.parser.EndElementHandler = self.end_element
124
+ self.parser.CharacterDataHandler = self.char_data
125
+
126
+ # used for filter
127
+ self.skip_until: Optional[str] = None
128
+
129
+ # used to remove empty sections
130
+ self.delayed_tag = None
131
+ self.delayed_tag_open = False
132
+
133
+ self.operation.set_fmu(fmu)
134
+ self.fmu = fmu
135
+
136
+ self.current_port: Optional[FMUPort] = None
137
+
138
+ self.current_port_number: int = 0
139
+ self.port_translation: List[Optional[int]] = []
140
+ self.port_names_list: List[str] = []
141
+ self.port_removed_vr: Set[str] = set()
142
+ self.apply_on = None
143
+
144
+ @staticmethod
145
+ def escape(value):
146
+ if isinstance(value, str):
147
+ return html.escape(html.unescape(value))
148
+ else:
149
+ return value
150
+
151
+ def handle_port(self):
152
+ causality = self.current_port.get('causality', 'local')
153
+ port_name = self.current_port['name']
154
+ vr = self.current_port['valueReference']
155
+ if not self.apply_on or causality in self.apply_on:
156
+ if self.operation.port_attrs(self.current_port):
157
+ self.remove_port(port_name, vr)
158
+ # Exception is raised by remove port !
159
+ else:
160
+ self.keep_port(port_name)
161
+ else: # Keep ScalarVariable as it is.
162
+ self.keep_port(port_name)
163
+
164
+ def start_element(self, name, attrs):
165
+ if self.skip_until:
166
+ return
167
+
168
+ try:
169
+ if name == 'ScalarVariable': # FMI 2.0 only
170
+ self.current_port = FMUPort()
171
+ self.current_port.push_attrs(attrs)
172
+ elif self.fmu.fmi_version == 2 and name in self.fmu.FMI2_TYPES:
173
+ if self.current_port: # <Enumeration> can be found before port defition. Ignored.
174
+ self.current_port.fmi_type = name
175
+ self.current_port.push_attrs(attrs)
176
+ elif self.fmu.fmi_version == 3 and name in self.fmu.FMI3_TYPES:
177
+ self.current_port = FMUPort()
178
+ self.current_port.fmi_type = name
179
+ self.current_port.push_attrs(attrs)
180
+ elif name == 'CoSimulation':
181
+ self.operation.cosimulation_attrs(attrs)
182
+ elif name == 'DefaultExperiment':
183
+ self.operation.experiment_attrs(attrs)
184
+ elif name == 'fmiModelDescription':
185
+ self.fmu.fmi_version = int(float(attrs["fmiVersion"]))
186
+ self.operation.fmi_attrs(attrs)
187
+ elif name == 'Unknown': # FMI-2.0 only
188
+ self.unknown_attrs(attrs)
189
+ elif name == 'Output' or name == "ContinuousStateDerivative" or "InitialUnknown": # FMI-3.0 only
190
+ self.handle_structure(attrs)
191
+
192
+ except ManipulationSkipTag:
193
+ self.skip_until = name
194
+ return
195
+
196
+ if self.current_port is None:
197
+ if self.delayed_tag and not self.delayed_tag_open:
198
+ print(f"<{self.delayed_tag}>", end='', file=self.out)
199
+ self.delayed_tag_open = True
200
+
201
+ if attrs:
202
+ attrs_list = [f'{key}="{self.escape(value)}"' for (key, value) in attrs.items()]
203
+ print(f"<{name}", " ".join(attrs_list), ">", end='', file=self.out)
204
+ else:
205
+ if name in self.TAGS_MODEL_STRUCTURE:
206
+ self.delayed_tag = name
207
+ self.delayed_tag_open = False
208
+ else:
209
+ print(f"<{name}>", end='', file=self.out)
210
+
211
+ def end_element(self, name):
212
+ if self.skip_until:
213
+ if self.skip_until == name:
214
+ self.skip_until = None
215
+ return
216
+ else:
217
+ if name == "ScalarVariable" or (self.fmu.fmi_version == 3 and name in FMU.FMI3_TYPES):
218
+ try:
219
+ self.handle_port()
220
+ self.current_port.write_xml(self.fmu.fmi_version, self.out)
221
+ except ManipulationSkipTag:
222
+ logger.info(f"Port '{self.current_port['name']}' is removed.")
223
+ self.current_port = None
224
+
225
+ elif self.current_port is None:
226
+ if self.delayed_tag and name == self.delayed_tag:
227
+ if self.delayed_tag_open:
228
+ print(f"</{self.delayed_tag}>", end='', file=self.out)
229
+ else:
230
+ logger.debug(f"Remove tag <{self.delayed_tag}> from modelDescription.xml")
231
+ self.delayed_tag = None
232
+ else:
233
+ print(f"</{name}>", end='', file=self.out)
234
+
235
+ def char_data(self, data):
236
+ if not self.skip_until:
237
+ print(data, end='', file=self.out)
238
+
239
+ def remove_port(self, name, vr):
240
+ self.port_names_list.append(name)
241
+ self.port_translation.append(None)
242
+ self.port_removed_vr.add(vr)
243
+ raise ManipulationSkipTag
244
+
245
+ def keep_port(self, name):
246
+ self.port_names_list.append(name)
247
+ self.current_port_number += 1
248
+ self.port_translation.append(self.current_port_number)
249
+
250
+ def unknown_attrs(self, attrs):
251
+ index = int(attrs['index'])
252
+ new_index = self.port_translation[index-1]
253
+ if new_index is not None:
254
+ attrs['index'] = str(new_index)
255
+ if attrs.get('dependencies', ""):
256
+ if 'dependenciesKind' in attrs:
257
+ new_dependencies = []
258
+ new_kinds = []
259
+ for dependency, kind in zip(attrs['dependencies'].split(' '), attrs['dependenciesKind'].split(' ')):
260
+ new_dependency = self.port_translation[int(dependency)-1]
261
+ if new_dependency is not None:
262
+ new_dependencies.append(str(new_dependency))
263
+ new_kinds.append(kind)
264
+ if new_dependencies:
265
+ attrs['dependencies'] = " ".join(new_dependencies)
266
+ attrs['dependenciesKind'] = " ".join(new_kinds)
267
+ else:
268
+ attrs.pop('dependencies')
269
+ attrs.pop('dependenciesKind')
270
+ else:
271
+ new_dependencies = []
272
+ for dependency in attrs['dependencies'].split(' '):
273
+ new_dependency = self.port_translation[int(dependency)-1]
274
+ if new_dependency is not None:
275
+ new_dependencies.append(str(new_dependency))
276
+ if new_dependencies:
277
+ attrs['dependencies'] = " ".join(new_dependencies)
278
+ else:
279
+ attrs.pop('dependencies')
280
+ else:
281
+ logger.warning(f"Removed port '{self.port_names_list[index-1]}' is involved in dependencies tree.")
282
+ raise ManipulationSkipTag
283
+
284
+ def handle_structure(self, attrs):
285
+ try:
286
+ vr = attrs['valueReference']
287
+ if vr in self.port_removed_vr:
288
+ logger.warning(f"Removed port vr={vr} is involved in dependencies tree.")
289
+ raise ManipulationSkipTag
290
+ except KeyError:
291
+ return
292
+
293
+ if attrs.get('dependencies', ""):
294
+ if 'dependenciesKind' in attrs:
295
+ new_dependencies = []
296
+ new_kinds = []
297
+ for dependency, kind in zip(attrs['dependencies'].split(' '), attrs['dependenciesKind'].split(' ')):
298
+ if dependency not in self.port_removed_vr:
299
+ new_dependencies.append(dependency)
300
+ new_kinds.append(kind)
301
+ if new_dependencies:
302
+ attrs['dependencies'] = " ".join(new_dependencies)
303
+ attrs['dependenciesKind'] = " ".join(new_kinds)
304
+ else:
305
+ attrs.pop('dependencies')
306
+ attrs.pop('dependenciesKind')
307
+ else:
308
+ new_dependencies = []
309
+ for dependency in attrs['dependencies'].split(' '):
310
+ if dependency not in self.port_removed_vr:
311
+ new_dependencies.append(dependency)
312
+ if new_dependencies:
313
+ attrs['dependencies'] = " ".join(new_dependencies)
314
+ else:
315
+ attrs.pop('dependencies')
316
+
317
+ def manipulate(self, descriptor_filename, apply_on=None):
318
+ self.apply_on = apply_on
319
+ with open(self.output_filename, "w", encoding="utf-8") as self.out, open(descriptor_filename, "rb") as file:
320
+ self.parser.ParseFile(file)
321
+ self.operation.closure()
322
+ os.replace(self.output_filename, descriptor_filename)
323
+
324
+
325
+ class ManipulationSkipTag(Exception):
326
+ """Exception: We need to skip every thing until matching closing tag"""
327
+
328
+
329
+ class OperationAbstract:
330
+ """This class hold hooks called during parsing"""
331
+ fmu: FMU = None
332
+
333
+ def set_fmu(self, fmu):
334
+ self.fmu = fmu
335
+
336
+ def fmi_attrs(self, attrs):
337
+ pass
338
+
339
+ def cosimulation_attrs(self, attrs):
340
+ pass
341
+
342
+ def experiment_attrs(self, attrs):
343
+ pass
344
+
345
+ def port_attrs(self, fmu_port: FMUPort) -> int:
346
+ """ return 0 to keep port, otherwise remove it"""
347
+ return 0
348
+
349
+ def closure(self):
350
+ pass
351
+
352
+
353
+ class OperationSaveNamesToCSV(OperationAbstract):
354
+ def __repr__(self):
355
+ return f"Dump names into '{self.output_filename}'"
356
+
357
+ def __init__(self, filename):
358
+ self.output_filename = filename
359
+ self.csvfile = open(filename, 'w', newline='')
360
+ self.writer = csv.writer(self.csvfile, delimiter=';', quotechar="'", quoting=csv.QUOTE_MINIMAL)
361
+ self.writer.writerow(['name', 'newName', 'valueReference', 'causality', 'variability', 'scalarType',
362
+ 'startValue'])
363
+
364
+ def closure(self):
365
+ self.csvfile.close()
366
+
367
+ def port_attrs(self, fmu_port: FMUPort) -> int:
368
+ self.writer.writerow([fmu_port["name"],
369
+ fmu_port["name"],
370
+ fmu_port["valueReference"],
371
+ fmu_port.get("causality", "local"),
372
+ fmu_port.get("variability", "continuous"),
373
+ fmu_port.fmi_type,
374
+ fmu_port.get("start", "")])
375
+
376
+ return 0
377
+
378
+
379
+ class OperationStripTopLevel(OperationAbstract):
380
+ def __repr__(self):
381
+ return "Remove Top Level Bus"
382
+
383
+ def port_attrs(self, fmu_port):
384
+ new_name = fmu_port['name'].split('.', 1)[-1]
385
+ fmu_port['name'] = new_name
386
+ return 0
387
+
388
+
389
+ class OperationMergeTopLevel(OperationAbstract):
390
+ def __repr__(self):
391
+ return "Merge Top Level Bus with signal names"
392
+
393
+ def port_attrs(self, fmu_port):
394
+ old = fmu_port['name']
395
+ fmu_port['name'] = old.replace('.', '_', 1)
396
+ return 0
397
+
398
+
399
+ class OperationRenameFromCSV(OperationAbstract):
400
+ def __repr__(self):
401
+ return f"Rename according to '{self.csv_filename}'"
402
+
403
+ def __init__(self, csv_filename):
404
+ self.csv_filename = csv_filename
405
+ self.translations = {}
406
+
407
+ try:
408
+ with open(csv_filename, newline='') as csvfile:
409
+ reader = csv.reader(csvfile, delimiter=';', quotechar="'")
410
+ for row in reader:
411
+ self.translations[row[0]] = row[1]
412
+ except FileNotFoundError:
413
+ raise OperationError(f"file '{csv_filename}' is not found")
414
+ except KeyError:
415
+ raise OperationError(f"file '{csv_filename}' should contain two columns")
416
+
417
+ def port_attrs(self, fmu_port):
418
+ name = fmu_port['name']
419
+ try:
420
+ new_name = self.translations[fmu_port['name']]
421
+ except KeyError:
422
+ new_name = name # if port is not in CSV file, keep old name
423
+
424
+ if new_name:
425
+ fmu_port['name'] = new_name
426
+ return 0
427
+ else:
428
+ # we want to delete this name!
429
+ return 1
430
+
431
+
432
+ class OperationRemoveRegexp(OperationAbstract):
433
+ def __repr__(self):
434
+ return f"Remove ports matching '{self.regex_string}'"
435
+
436
+ def __init__(self, regex_string):
437
+ self.regex_string = regex_string
438
+ self.regex = re.compile(regex_string)
439
+ self.current_port_number = 0
440
+ self.port_translation = []
441
+
442
+ def port_attrs(self, fmu_port):
443
+ name = fmu_port['name']
444
+ if self.regex.match(name):
445
+ return 1 # Remove port
446
+ else:
447
+ return 0
448
+
449
+
450
+ class OperationKeepOnlyRegexp(OperationAbstract):
451
+ def __repr__(self):
452
+ return f"Keep only ports matching '{self.regex_string}'"
453
+
454
+ def __init__(self, regex_string):
455
+ self.regex_string = regex_string
456
+ self.regex = re.compile(regex_string)
457
+
458
+ def port_attrs(self, fmu_port):
459
+ name = fmu_port['name']
460
+ if self.regex.match(name):
461
+ return 0
462
+ else:
463
+ return 1 # Remove port
464
+
465
+
466
+ class OperationSummary(OperationAbstract):
467
+ def __init__(self):
468
+ self.nb_port_per_causality = {}
469
+
470
+ def __repr__(self):
471
+ return f"FMU Summary"
472
+
473
+ def fmi_attrs(self, attrs):
474
+ logger.info(f"| fmu filename = {self.fmu.fmu_filename}")
475
+ logger.info(f"| temporary directory = {self.fmu.tmp_directory}")
476
+ hash_md5 = hashlib.md5()
477
+ with open(self.fmu.fmu_filename, "rb") as f:
478
+ for chunk in iter(lambda: f.read(4096), b""):
479
+ hash_md5.update(chunk)
480
+ digest = hash_md5.hexdigest()
481
+ logger.info(f"| MD5Sum = {digest}")
482
+ logger.info(f"|")
483
+ logger.info(f"| FMI properties: ")
484
+ for (k, v) in attrs.items():
485
+ logger.info(f"| - {k} = {v}")
486
+ logger.info(f"|")
487
+
488
+ def cosimulation_attrs(self, attrs):
489
+ logger.info("| Co-Simulation capabilities: ")
490
+ for (k, v) in attrs.items():
491
+ logger.info(f"| - {k} = {v}")
492
+ logger.info(f"|")
493
+
494
+ def experiment_attrs(self, attrs):
495
+ logger.info("| Default Experiment values: ")
496
+ for (k, v) in attrs.items():
497
+ logger.info(f"| - {k} = {v}")
498
+ logger.info(f"|")
499
+
500
+ def port_attrs(self, fmu_port) -> int:
501
+ causality = fmu_port.get("causality", "local")
502
+
503
+ try:
504
+ self.nb_port_per_causality[causality] += 1
505
+ except KeyError:
506
+ self.nb_port_per_causality[causality] = 1
507
+
508
+ return 0
509
+
510
+ def closure(self):
511
+ logger.info("| Supported platforms: ")
512
+ try:
513
+ for platform in os.listdir(os.path.join(self.fmu.tmp_directory, "binaries")):
514
+ logger.info(f"| - {platform}")
515
+ except FileNotFoundError:
516
+ pass # no binaries
517
+
518
+ if os.path.isdir(os.path.join(self.fmu.tmp_directory, "sources")):
519
+ logger.info(f"| - RT (sources available)")
520
+
521
+ resource_dir = os.path.join(self.fmu.tmp_directory, "resources")
522
+ if os.path.isdir(resource_dir):
523
+ logger.info("|")
524
+ logger.info("| Embedded resources:")
525
+ for resource in os.listdir(resource_dir):
526
+ logger.info(f"| - {resource}")
527
+
528
+ extra_dir = os.path.join(self.fmu.tmp_directory, "extra")
529
+ if os.path.isdir(extra_dir):
530
+ logger.info("|")
531
+ logger.info("| Additional (meta-)data:")
532
+ for extra in os.listdir(extra_dir):
533
+ logger.info(f"| - {extra}")
534
+
535
+ logger.info("|")
536
+ logger.info("| Number of ports")
537
+ for causality, nb_ports in self.nb_port_per_causality.items():
538
+ logger.info(f"| {causality} : {nb_ports}")
539
+
540
+ logger.info("|")
541
+ logger.info("| [End of report]")
542
+
543
+
544
+ class OperationRemoveSources(OperationAbstract):
545
+ def __repr__(self):
546
+ return f"Remove sources"
547
+
548
+ def cosimulation_attrs(self, attrs):
549
+ try:
550
+ shutil.rmtree(os.path.join(self.fmu.tmp_directory, "sources"))
551
+ except FileNotFoundError:
552
+ logger.info("This FMU does not embed sources.")
553
+
554
+
555
+ class OperationTrimUntil(OperationAbstract):
556
+ def __init__(self, separator):
557
+ self.separator = separator
558
+
559
+ def __repr__(self):
560
+ return f"Trim names until (and including) '{self.separator}'"
561
+
562
+ def port_attrs(self, fmu_port) -> int:
563
+ name = fmu_port['name']
564
+ try:
565
+ fmu_port['name'] = name[name.index(self.separator)+len(self.separator):-1]
566
+ except KeyError:
567
+ pass # no separator
568
+
569
+ return 0
570
+
571
+
572
+ class OperationError(Exception):
573
+ def __init__(self, reason):
574
+ self.reason = reason
575
+
576
+ def __repr__(self):
577
+ return self.reason
@@ -0,0 +1,107 @@
1
+ import logging
2
+ import shutil
3
+
4
+ from pathlib import Path
5
+ from .operations import OperationAbstract, OperationError
6
+
7
+ logger = logging.getLogger("fmu_manipulation_toolbox")
8
+
9
+ class OperationAddRemotingWinAbstract(OperationAbstract):
10
+ bitness_from = None
11
+ bitness_to = None
12
+
13
+ def __repr__(self):
14
+ return f"Add '{self.bitness_to}' remoting on '{self.bitness_from}' FMU"
15
+
16
+ def __init__(self):
17
+ self.vr = {
18
+ "Real": [],
19
+ "Integer": [],
20
+ "Boolean": []
21
+ }
22
+ self.nb_input = 0
23
+ self.nb_output = 0
24
+
25
+ def fmi_attrs(self, attrs):
26
+ if not attrs["fmiVersion"] == "2.0":
27
+ raise OperationError(f"Adding remoting is only available for FMI-2.0")
28
+
29
+ def cosimulation_attrs(self, attrs):
30
+ fmu_bin = {
31
+ "win32": Path(self.fmu.tmp_directory) / "binaries" / "win32",
32
+ "win64": Path(self.fmu.tmp_directory) / "binaries" / "win64",
33
+ }
34
+
35
+ if not fmu_bin[self.bitness_from].is_dir():
36
+ raise OperationError(f"{self.bitness_from} interface does not exist")
37
+
38
+ if fmu_bin[self.bitness_to].is_dir():
39
+ logger.info(f"{self.bitness_to} already exists. Add front-end.")
40
+ shutil.move(fmu_bin[self.bitness_to] / Path(attrs['modelIdentifier']).with_suffix(".dll"),
41
+ fmu_bin[self.bitness_to] / Path(attrs['modelIdentifier']).with_suffix("-remoted.dll"))
42
+ else:
43
+ fmu_bin[self.bitness_to].mkdir()
44
+
45
+ to_path = Path(__file__).parent / "resources" / self.bitness_to
46
+ try:
47
+ shutil.copyfile(to_path / "client_sm.dll",
48
+ fmu_bin[self.bitness_to] / Path(attrs['modelIdentifier']).with_suffix(".dll"))
49
+ except FileNotFoundError as e:
50
+ logger.critical(f"Cannot add remoting client: {e}")
51
+
52
+ from_path = Path(__file__).parent / "resources" / self.bitness_from
53
+ try:
54
+ shutil.copyfile(from_path / "server_sm.exe",
55
+ fmu_bin[self.bitness_from] / "server_sm.exe")
56
+ except FileNotFoundError as e:
57
+ logger.critical(f"Cannot add remoting server: {e}")
58
+
59
+ shutil.copyfile(Path(__file__).parent / "resources" / "license.txt",
60
+ fmu_bin[self.bitness_to] / "license.txt")
61
+
62
+ def port_attrs(self, fmu_port) -> int:
63
+ vr = int(fmu_port["valueReference"])
64
+ causality = fmu_port.get("causality", "local")
65
+ try:
66
+ self.vr[fmu_port.fmi_type].append(vr)
67
+ if causality in ("input", "parameter"):
68
+ self.nb_input += 1
69
+ else:
70
+ self.nb_output += 1
71
+ except KeyError:
72
+ logger.error(f"Type '{fmu_port.fmi_type}' is not supported by remoting.")
73
+
74
+ return 0
75
+
76
+ def closure(self):
77
+ target_dir = Path(self.fmu.tmp_directory) / "resources"
78
+ if not target_dir.is_dir():
79
+ target_dir.mkdir()
80
+
81
+ logger.info(f"Remoting nb input port: {self.nb_input}")
82
+ logger.info(f"Remoting nb output port: {self.nb_output}")
83
+ with open(target_dir/ "remoting_table.txt", "wt") as file:
84
+ for fmi_type in ('Real', 'Integer', 'Boolean'):
85
+ print(len(self.vr[fmi_type]), file=file)
86
+ for fmi_type in ('Real', 'Integer', 'Boolean'):
87
+ for vr in sorted(self.vr[fmi_type]):
88
+ print(vr, file=file)
89
+
90
+ class OperationAddRemotingWin64(OperationAddRemotingWinAbstract):
91
+ bitness_from = "win32"
92
+ bitness_to = "win64"
93
+
94
+
95
+ class OperationAddFrontendWin32(OperationAddRemotingWinAbstract):
96
+ bitness_from = "win32"
97
+ bitness_to = "win32"
98
+
99
+
100
+ class OperationAddFrontendWin64(OperationAddRemotingWinAbstract):
101
+ bitness_from = "win64"
102
+ bitness_to = "win64"
103
+
104
+
105
+ class OperationAddRemotingWin32(OperationAddRemotingWinAbstract):
106
+ bitness_from = "win64"
107
+ bitness_to = "win32"