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,489 @@
1
+ import csv
2
+ import html
3
+ import os
4
+ import re
5
+ import shutil
6
+ import tempfile
7
+ import xml.parsers.expat
8
+ import zipfile
9
+ import hashlib
10
+ from pathlib import Path
11
+
12
+
13
+ class FMU:
14
+ """Unpack and Repack facilities for FMU package. Once unpacked, we can process Operation on
15
+ modelDescription.xml file."""
16
+ def __init__(self, fmu_filename):
17
+ self.fmu_filename = fmu_filename
18
+ self.tmp_directory = tempfile.mkdtemp()
19
+
20
+ try:
21
+ with zipfile.ZipFile(self.fmu_filename) as zin:
22
+ zin.extractall(self.tmp_directory)
23
+ except FileNotFoundError:
24
+ raise FMUException(f"'{fmu_filename}' does not exist")
25
+ self.descriptor_filename = os.path.join(self.tmp_directory, "modelDescription.xml")
26
+ if not os.path.isfile(self.descriptor_filename):
27
+ raise FMUException(f"'{fmu_filename}' is not valid: {self.descriptor_filename} not found")
28
+
29
+ def __del__(self):
30
+ shutil.rmtree(self.tmp_directory)
31
+
32
+ def save_descriptor(self, filename):
33
+ shutil.copyfile(os.path.join(self.tmp_directory, "modelDescription.xml"), filename)
34
+
35
+ def repack(self, filename):
36
+ with zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) as zout:
37
+ for root, dirs, files in os.walk(self.tmp_directory):
38
+ for file in files:
39
+ zout.write(os.path.join(root, file),
40
+ os.path.relpath(os.path.join(root, file), self.tmp_directory))
41
+ # TODO: Add check on output file
42
+
43
+ def apply_operation(self, operation, apply_on=None):
44
+ manipulation = Manipulation(operation, self)
45
+ manipulation.manipulate(self.descriptor_filename, apply_on)
46
+
47
+
48
+ class FMUException(Exception):
49
+ def __init__(self, reason):
50
+ self.reason = reason
51
+
52
+ def __repr__(self):
53
+ return self.reason
54
+
55
+
56
+ class Manipulation:
57
+ """Parse modelDescription.xml file and create a modified version"""
58
+ def __init__(self, operation, fmu):
59
+ self.output_filename = tempfile.mktemp()
60
+ self.out = None
61
+ self.operation = operation
62
+ self.parser = xml.parsers.expat.ParserCreate()
63
+ self.parser.StartElementHandler = self.start_element
64
+ self.parser.EndElementHandler = self.end_element
65
+ self.parser.CharacterDataHandler = self.char_data
66
+ self.skip_until = None
67
+ self.operation.set_fmu(fmu)
68
+
69
+ self.current_port = 0
70
+ self.port_translation = []
71
+ self.port_name = []
72
+ self.apply_on = None
73
+
74
+ @staticmethod
75
+ def escape(value):
76
+ if isinstance(value, str):
77
+ return html.escape(html.unescape(value))
78
+ else:
79
+ return value
80
+
81
+ def start_element(self, name, attrs):
82
+ if self.skip_until:
83
+ return
84
+ try:
85
+ if name == 'ScalarVariable':
86
+ causality = OperationAbstract.scalar_get_causality(attrs)
87
+ if not self.apply_on or causality in self.apply_on:
88
+ if self.operation.scalar_attrs(attrs):
89
+ self.remove_port(attrs['name'])
90
+ else:
91
+ self.keep_port(attrs['name'])
92
+ else:
93
+ self.keep_port(attrs['name'])
94
+ self.skip_until = name # do not read inner tags
95
+ elif name == 'CoSimulation':
96
+ self.operation.cosimulation_attrs(attrs)
97
+ elif name == 'DefaultExperiment':
98
+ self.operation.experiment_attrs(attrs)
99
+ elif name == 'fmiModelDescription':
100
+ self.operation.fmi_attrs(attrs)
101
+ elif name == 'Unknown':
102
+ self.unknown_attrs(attrs)
103
+ elif name in ('Real', 'Integer', 'String', 'Boolean'):
104
+ self.operation.scalar_type(name, attrs)
105
+
106
+ except ManipulationSkipTag:
107
+ self.skip_until = name
108
+ return
109
+
110
+ if attrs:
111
+ attrs_list = [f'{key}="{self.escape(value)}"' for (key, value) in attrs.items()]
112
+ print(f"<{name}", " ".join(attrs_list), ">", end='', file=self.out)
113
+ else:
114
+ print(f"<{name}>", end='', file=self.out)
115
+
116
+ def end_element(self, name):
117
+ if self.skip_until:
118
+ if self.skip_until == name:
119
+ self.skip_until = None
120
+ return
121
+ else:
122
+ print(f"</{name}>", end='', file=self.out)
123
+
124
+ def char_data(self, data):
125
+ if not self.skip_until:
126
+ print(data, end='', file=self.out)
127
+
128
+ def remove_port(self, name):
129
+ self.port_name.append(name)
130
+ self.port_translation.append(None)
131
+ raise ManipulationSkipTag
132
+
133
+ def keep_port(self, name):
134
+ self.port_name.append(name)
135
+ self.current_port += 1
136
+ self.port_translation.append(self.current_port)
137
+
138
+ def unknown_attrs(self, attrs):
139
+ index = int(attrs['index']) - 1
140
+ new_index = self.port_translation[index]
141
+ if new_index:
142
+ attrs['index'] = self.port_translation[int(attrs['index']) - 1]
143
+ else:
144
+ print(f"WARNING: Removed port '{self.port_name[index]}' is involved in dependencies tree.")
145
+ raise ManipulationSkipTag
146
+
147
+ def manipulate(self, descriptor_filename, apply_on=None):
148
+ self.apply_on = apply_on
149
+ with open(self.output_filename, "w", encoding="utf-8") as self.out, open(descriptor_filename, "rb") as file:
150
+ self.parser.ParseFile(file)
151
+ self.operation.closure()
152
+ os.replace(self.output_filename, descriptor_filename)
153
+
154
+
155
+ class ManipulationSkipTag(Exception):
156
+ """Exception: We need to skip every thing until matching closing tag"""
157
+
158
+
159
+ class OperationAbstract:
160
+ """This class hold hooks called during parsing"""
161
+ fmu: FMU = None
162
+
163
+ def set_fmu(self, fmu):
164
+ self.fmu = fmu
165
+
166
+ def fmi_attrs(self, attrs):
167
+ pass
168
+
169
+ def scalar_attrs(self, attrs) -> int:
170
+ """ return 0 to keep port, otherwise remove it"""
171
+ return 0
172
+
173
+ def cosimulation_attrs(self, attrs):
174
+ pass
175
+
176
+ def experiment_attrs(self, attrs):
177
+ pass
178
+
179
+ def scalar_type(self, type_name, attrs):
180
+ pass
181
+
182
+ def closure(self):
183
+ pass
184
+
185
+ @staticmethod
186
+ def scalar_get_causality(attrs) -> str:
187
+ try:
188
+ causality = attrs['causality']
189
+ except KeyError:
190
+ causality = 'local' # Default value according to FMI Specifications.
191
+
192
+ return causality
193
+
194
+
195
+ class OperationSaveNamesToCSV(OperationAbstract):
196
+ def __repr__(self):
197
+ return f"Dump names into '{self.output_filename}'"
198
+
199
+ def __init__(self, filename):
200
+ self.output_filename = filename
201
+ self.csvfile = open(filename, 'w', newline='')
202
+ self.writer = csv.writer(self.csvfile, delimiter=';', quotechar="'", quoting=csv.QUOTE_MINIMAL)
203
+ self.writer.writerow(['name', 'newName', 'valueReference', 'causality', 'variability', 'scalarType',
204
+ 'startValue'])
205
+ self.name = None
206
+ self.vr = None
207
+ self.variability = None
208
+ self.causality = None
209
+
210
+ def reset(self):
211
+ self.name = None
212
+ self.vr = None
213
+ self.variability = None
214
+ self.causality = None
215
+
216
+ def closure(self):
217
+ self.csvfile.close()
218
+
219
+ def scalar_attrs(self, attrs):
220
+ self.name = attrs['name']
221
+ self.vr = attrs['valueReference']
222
+ self.causality = self.scalar_get_causality(attrs)
223
+
224
+ try:
225
+ self.variability = attrs['variability']
226
+ except KeyError:
227
+ self.variability = 'continuous' # Default value according to FMI Specifications.
228
+
229
+ return 0
230
+
231
+ def scalar_type(self, type_name, attrs):
232
+ if "start" in attrs:
233
+ start = attrs["start"]
234
+ else:
235
+ start = ""
236
+ self.writer.writerow([self.name, self.name, self.vr, self.causality, self.variability, type_name, start])
237
+ self.reset()
238
+
239
+
240
+ class OperationStripTopLevel(OperationAbstract):
241
+ def __repr__(self):
242
+ return "Remove Top Level Bus"
243
+
244
+ def scalar_attrs(self, attrs):
245
+ new_name = attrs['name'].split('.', 1)[-1]
246
+ attrs['name'] = new_name
247
+ return 0
248
+
249
+
250
+ class OperationMergeTopLevel(OperationAbstract):
251
+ def __repr__(self):
252
+ return "Merge Top Level Bus with signal names"
253
+
254
+ def scalar_attrs(self, attrs):
255
+ old = attrs['name']
256
+ attrs['name'] = old.replace('.', '_', 1)
257
+ return 0
258
+
259
+
260
+ class OperationRenameFromCSV(OperationAbstract):
261
+ def __repr__(self):
262
+ return f"Rename according to '{self.csv_filename}'"
263
+
264
+ def __init__(self, csv_filename):
265
+ self.csv_filename = csv_filename
266
+ self.translations = {}
267
+ self.current_port = 0
268
+ self.port_translation = []
269
+ try:
270
+ with open(csv_filename, newline='') as csvfile:
271
+ reader = csv.reader(csvfile, delimiter=';', quotechar="'")
272
+ for row in reader:
273
+ self.translations[row[0]] = row[1]
274
+ except FileNotFoundError:
275
+ raise OperationException(f"file '{csv_filename}' is not found")
276
+ except KeyError:
277
+ raise OperationException(f"file '{csv_filename}' should contain two columns")
278
+
279
+ def scalar_attrs(self, attrs):
280
+ name = attrs['name']
281
+ try:
282
+ new_name = self.translations[attrs['name']]
283
+ except KeyError:
284
+ new_name = name # if port is not in CSV file, keep old name
285
+
286
+ if new_name:
287
+ attrs['name'] = new_name
288
+ return 0
289
+ else:
290
+ # we want to delete this name!
291
+ return 1
292
+
293
+
294
+ class OperationAddRemotingWinAbstract(OperationAbstract):
295
+ bitness_from = None
296
+ bitness_to = None
297
+
298
+ def __repr__(self):
299
+ return f"Add '{self.bitness_to}' remoting on '{self.bitness_from}' FMU"
300
+
301
+ def cosimulation_attrs(self, attrs):
302
+ fmu_bin = {
303
+ "win32": os.path.join(self.fmu.tmp_directory, "binaries", f"win32"),
304
+ "win64": os.path.join(self.fmu.tmp_directory, "binaries", f"win64"),
305
+ }
306
+
307
+ if not os.path.isdir(fmu_bin[self.bitness_from]):
308
+ raise OperationException(f"{self.bitness_from} interface does not exist")
309
+
310
+ if os.path.isdir(fmu_bin[self.bitness_to]):
311
+ print(f"INFO: {self.bitness_to} already exists. Add front-end.")
312
+ shutil.move(os.path.join(fmu_bin[self.bitness_to], attrs['modelIdentifier'] + ".dll"),
313
+ os.path.join(fmu_bin[self.bitness_to], attrs['modelIdentifier'] + "-remoted.dll"))
314
+ else:
315
+ os.mkdir(fmu_bin[self.bitness_to])
316
+
317
+ from_path = Path(__file__).parent / "resources" / self.bitness_to
318
+ shutil.copyfile(from_path / "client_sm.dll",
319
+ Path(fmu_bin[self.bitness_to]) / Path(attrs['modelIdentifier']).with_suffix(".dll"))
320
+
321
+ shutil.copyfile(from_path / "server_sm.exe",
322
+ Path(fmu_bin[self.bitness_from]) / "server_sm.exe")
323
+
324
+ shutil.copyfile(Path(__file__).parent / "resources" / "license.txt",
325
+ Path(fmu_bin[self.bitness_to]) / "license.txt")
326
+
327
+
328
+ class OperationAddRemotingWin64(OperationAddRemotingWinAbstract):
329
+ bitness_from = "win32"
330
+ bitness_to = "win64"
331
+
332
+
333
+ class OperationAddFrontendWin32(OperationAddRemotingWinAbstract):
334
+ bitness_from = "win32"
335
+ bitness_to = "win32"
336
+
337
+
338
+ class OperationAddFrontendWin64(OperationAddRemotingWinAbstract):
339
+ bitness_from = "win64"
340
+ bitness_to = "win64"
341
+
342
+
343
+ class OperationAddRemotingWin32(OperationAddRemotingWinAbstract):
344
+ bitness_from = "win64"
345
+ bitness_to = "win32"
346
+
347
+
348
+ class OperationRemoveRegexp(OperationAbstract):
349
+ def __repr__(self):
350
+ return f"Remove ports matching '{self.regex_string}'"
351
+
352
+ def __init__(self, regex_string):
353
+ self.regex_string = regex_string
354
+ self.regex = re.compile(regex_string)
355
+ self.current_port = 0
356
+ self.port_translation = []
357
+
358
+ def scalar_attrs(self, attrs):
359
+ name = attrs['name']
360
+ if self.regex.match(name):
361
+ return 1 # Remove port
362
+ else:
363
+ return 0
364
+
365
+
366
+ class OperationKeepOnlyRegexp(OperationAbstract):
367
+ def __repr__(self):
368
+ return f"Keep only ports matching '{self.regex_string}'"
369
+
370
+ def __init__(self, regex_string):
371
+ self.regex_string = regex_string
372
+ self.regex = re.compile(regex_string)
373
+
374
+ def scalar_attrs(self, attrs):
375
+ name = attrs['name']
376
+ if self.regex.match(name):
377
+ return 0
378
+ else:
379
+ return 1 # Remove port
380
+
381
+
382
+ class OperationSummary(OperationAbstract):
383
+ def __init__(self):
384
+ self.nb_port_per_causality = {}
385
+
386
+ def __repr__(self):
387
+ return f"FMU Summary"
388
+
389
+ def fmi_attrs(self, attrs):
390
+ print(f"| fmu filename = {self.fmu.fmu_filename}")
391
+ print(f"| temporary directory = {self.fmu.tmp_directory}")
392
+ hash_md5 = hashlib.md5()
393
+ with open(self.fmu.fmu_filename, "rb") as f:
394
+ for chunk in iter(lambda: f.read(4096), b""):
395
+ hash_md5.update(chunk)
396
+ digest = hash_md5.hexdigest()
397
+ print(f"| MD5Sum = {digest}")
398
+
399
+ print(f"|\n| FMI properties: ")
400
+ for (k, v) in attrs.items():
401
+ print(f"| - {k} = {v}")
402
+ print(f"|")
403
+
404
+ def cosimulation_attrs(self, attrs):
405
+ print("| Co-Simulation capabilities: ")
406
+ for (k, v) in attrs.items():
407
+ print(f"| - {k} = {v}")
408
+ print(f"|")
409
+
410
+ def experiment_attrs(self, attrs):
411
+ print("| Default Experiment values: ")
412
+ for (k, v) in attrs.items():
413
+ print(f"| - {k} = {v}")
414
+ print(f"|")
415
+
416
+ def scalar_attrs(self, attrs) -> int:
417
+ causality = self.scalar_get_causality(attrs)
418
+
419
+ try:
420
+ self.nb_port_per_causality[causality] += 1
421
+ except KeyError:
422
+ self.nb_port_per_causality[causality] = 1
423
+
424
+ return 0
425
+
426
+ def closure(self):
427
+ print("| Supported platforms: ")
428
+ try:
429
+ for platform in os.listdir(os.path.join(self.fmu.tmp_directory, "binaries")):
430
+ print(f"| - {platform}")
431
+ except FileNotFoundError:
432
+ pass # no binaries
433
+
434
+ if os.path.isdir(os.path.join(self.fmu.tmp_directory, "sources")):
435
+ print(f"| - RT (sources available)")
436
+
437
+ resource_dir = os.path.join(self.fmu.tmp_directory, "resources")
438
+ if os.path.isdir(resource_dir):
439
+ print("|\n| Embedded resources:")
440
+ for resource in os.listdir(resource_dir):
441
+ print(f"| - {resource}")
442
+
443
+ extra_dir = os.path.join(self.fmu.tmp_directory, "extra")
444
+ if os.path.isdir(extra_dir):
445
+ print("|\n| Additional (meta-)data:")
446
+ for extra in os.listdir(extra_dir):
447
+ print(f"| - {extra}")
448
+
449
+ print("|\n| Number of signals")
450
+ for causality, nb_ports in self.nb_port_per_causality.items():
451
+ print(f"| {causality} : {nb_ports}")
452
+
453
+ print("|\n| [End of report]")
454
+
455
+
456
+ class OperationRemoveSources(OperationAbstract):
457
+ def __repr__(self):
458
+ return f"Remove sources"
459
+
460
+ def cosimulation_attrs(self, attrs):
461
+ try:
462
+ shutil.rmtree(os.path.join(self.fmu.tmp_directory, "sources"))
463
+ except FileNotFoundError:
464
+ print("This FMU does not embed sources.")
465
+
466
+
467
+ class OperationTrimUntil(OperationAbstract):
468
+ def __init__(self, separator):
469
+ self.separator = separator
470
+
471
+ def __repr__(self):
472
+ return f"Trim names until (and including) '{self.separator}'"
473
+
474
+ def scalar_attrs(self, attrs) -> int:
475
+ name = attrs['name']
476
+ try:
477
+ attrs['name'] = name[name.index(self.separator)+len(self.separator):-1]
478
+ except KeyError:
479
+ pass # no separator
480
+
481
+ return 0
482
+
483
+
484
+ class OperationException(Exception):
485
+ def __init__(self, reason):
486
+ self.reason = reason
487
+
488
+ def __repr__(self):
489
+ return self.reason