fmu-manipulation-toolbox 1.7.5__py3-none-any.whl → 1.8.1__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 (25) hide show
  1. fmu_manipulation_toolbox/__version__.py +1 -1
  2. fmu_manipulation_toolbox/assembly.py +484 -0
  3. fmu_manipulation_toolbox/cli.py +27 -17
  4. fmu_manipulation_toolbox/fmu_container.py +41 -189
  5. fmu_manipulation_toolbox/gui.py +58 -45
  6. fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  7. fmu_manipulation_toolbox/resources/icon-round.png +0 -0
  8. fmu_manipulation_toolbox/resources/icon.png +0 -0
  9. fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  10. fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  11. fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  12. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  13. fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
  14. fmu_manipulation_toolbox/resources/mask.png +0 -0
  15. fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  16. fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  17. fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  18. fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  19. fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  20. {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.1.dist-info}/METADATA +1 -1
  21. {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.1.dist-info}/RECORD +25 -22
  22. {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.1.dist-info}/LICENSE.txt +0 -0
  23. {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.1.dist-info}/WHEEL +0 -0
  24. {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.1.dist-info}/entry_points.txt +0 -0
  25. {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- 'V1.7.5'
1
+ 'V1.8.1'
@@ -0,0 +1,484 @@
1
+ import csv
2
+ import json
3
+ import logging
4
+ from typing import *
5
+ from pathlib import Path
6
+ import uuid
7
+ import xml.parsers.expat
8
+ import zipfile
9
+
10
+ from .fmu_container import FMUContainer
11
+
12
+ logger = logging.getLogger("fmu_manipulation_toolbox")
13
+
14
+
15
+ class Port:
16
+ def __init__(self, fmu_name: str, port_name: str):
17
+ self.fmu_name = fmu_name
18
+ self.port_name = port_name
19
+
20
+ def __hash__(self):
21
+ return hash(f"{self.fmu_name}/{self.port_name}")
22
+
23
+ def __eq__(self, other):
24
+ return str(self) == str(other)
25
+
26
+
27
+ class Connection:
28
+ def __init__(self, from_port: Port, to_port: Port):
29
+ self.from_port = from_port
30
+ self.to_port = to_port
31
+
32
+
33
+ class AssemblyNode:
34
+ def __init__(self, name: Optional[str], step_size: float = None, mt=False, profiling=False,
35
+ auto_link=True, auto_input=True, auto_output=True):
36
+ self.name = name
37
+ if step_size:
38
+ try:
39
+ self.step_size = float(step_size)
40
+ except ValueError:
41
+ logger.warning(f"Step size '{step_size}' is incorrect format.")
42
+ self.step_size = None
43
+ else:
44
+ self.step_size = None
45
+ self.mt = mt
46
+ self.profiling = profiling
47
+ self.auto_link = auto_link
48
+ self.auto_input = auto_input
49
+ self.auto_output = auto_output
50
+
51
+ self.parent: Optional[AssemblyNode] = None
52
+ self.children: List[AssemblyNode] = [] # sub-containers
53
+ self.fmu_names_list: Set[str] = set() # FMUs contained at this level
54
+ self.input_ports: Dict[Port, str] = {}
55
+ self.output_ports: Dict[Port, str] = {}
56
+ self.start_values: Dict[Port, str] = {}
57
+ self.drop_ports: List[Port] = []
58
+ self.links: List[Connection] = []
59
+
60
+ def add_sub_node(self, sub_node):
61
+ if sub_node.name is None:
62
+ sub_node.name = str(uuid.uuid4())+".fmu"
63
+
64
+ if sub_node.parent is not None:
65
+ raise AssemblyError(f"Internal Error: AssemblyNode {sub_node.name} is already parented.")
66
+
67
+ sub_node.parent = self
68
+ self.fmu_names_list.add(sub_node.name)
69
+ self.children.append(sub_node)
70
+
71
+
72
+ def add_fmu(self, fmu_name: str):
73
+ self.fmu_names_list.add(fmu_name)
74
+
75
+ def add_input(self, from_port_name: str, to_fmu_filename: str, to_port_name: str):
76
+ self.input_ports[Port(to_fmu_filename, to_port_name)] = from_port_name
77
+
78
+ def add_output(self, from_fmu_filename: str, from_port_name: str, to_port_name: str):
79
+ self.output_ports[Port(from_fmu_filename, from_port_name)] = to_port_name
80
+
81
+ def add_drop_port(self, fmu_filename: str, port_name: str):
82
+ self.drop_ports.append(Port(fmu_filename, port_name))
83
+
84
+ def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
85
+ self.links.append(Connection(Port(from_fmu_filename, from_port_name),
86
+ Port(to_fmu_filename, to_port_name)))
87
+
88
+ def add_start_value(self, fmu_filename: str, port_name: str, value: str):
89
+ self.start_values[Port(fmu_filename, port_name)] = value
90
+
91
+ def make_fmu(self, fmu_directory: Path, debug=False, description_pathname=None):
92
+ for node in self.children:
93
+ node.make_fmu(fmu_directory, debug=debug)
94
+
95
+ container = FMUContainer(self.name, fmu_directory, description_pathname=description_pathname)
96
+
97
+ for fmu_name in sorted(self.fmu_names_list):
98
+ container.get_fmu(fmu_name)
99
+
100
+ for port, source in self.input_ports.items():
101
+ container.add_input(source, port.fmu_name, port.port_name)
102
+
103
+ for port, target in self.output_ports.items():
104
+ container.add_output(port.fmu_name, port.port_name, target)
105
+
106
+ for link in self.links:
107
+ container.add_link(link.from_port.fmu_name, link.from_port.port_name,
108
+ link.to_port.fmu_name, link.to_port.port_name)
109
+
110
+ for drop in self.drop_ports:
111
+ container.drop_port(drop.fmu_name, drop.port_name)
112
+
113
+ for port, value in self.start_values.items():
114
+ container.add_start_value(port.fmu_name, port.port_name, value)
115
+
116
+ container.add_implicit_rule(auto_input=self.auto_input,
117
+ auto_output=self.auto_output,
118
+ auto_link=self.auto_link)
119
+
120
+ container.make_fmu(self.name, self.step_size, mt=self.mt, profiling=self.profiling, debug=debug)
121
+
122
+ for node in self.children:
123
+ logger.info(f"Deleting transient FMU Container '{node.name}'")
124
+ (fmu_directory / node.name).unlink()
125
+
126
+
127
+ class AssemblyError(Exception):
128
+ def __init__(self, reason: str):
129
+ self.reason = reason
130
+
131
+ def __repr__(self):
132
+ return f"{self.reason}"
133
+
134
+
135
+ class Assembly:
136
+ def __init__(self, filename: str, step_size=None, auto_link=True, auto_input=True, debug=False,
137
+ auto_output=True, mt=False, profiling=False, fmu_directory: Path = "."):
138
+ self.filename = Path(filename)
139
+ self.default_auto_input = auto_input
140
+ self.debug = debug
141
+ self.default_auto_output = auto_output
142
+ self.default_step_size = step_size
143
+ self.default_auto_link = auto_link
144
+ self.default_mt = mt
145
+ self.default_profiling = profiling
146
+ self.fmu_directory = fmu_directory
147
+ self.transient_filenames: List[Path] = []
148
+ self.transient_dirnames: Set[Path] = set()
149
+
150
+ if not fmu_directory.is_dir():
151
+ raise AssemblyError(f"FMU directory is not valid: '{fmu_directory}'")
152
+
153
+ self.input_pathname = fmu_directory / self.filename
154
+ self.description_pathname = self.input_pathname # For inclusion in FMU
155
+ self.root = None
156
+ self.read()
157
+
158
+ def add_transient_file(self, filename: str):
159
+ self.transient_filenames.append(self.fmu_directory / filename)
160
+ self.transient_dirnames.add(Path(filename).parent)
161
+
162
+ def __del__(self):
163
+ if not self.debug:
164
+ for filename in self.transient_filenames:
165
+ try:
166
+ filename.unlink()
167
+ except FileNotFoundError:
168
+ pass
169
+ for dirname in self.transient_dirnames:
170
+ while not str(dirname) == ".":
171
+ try:
172
+ (self.fmu_directory / dirname).rmdir()
173
+ except FileNotFoundError:
174
+ pass
175
+ dirname = dirname.parent
176
+
177
+ def read(self):
178
+ logger.info(f"Reading '{self.filename}'")
179
+ if self.filename.suffix == ".json":
180
+ self.read_json()
181
+ elif self.filename.suffix == ".ssp":
182
+ self.read_ssp()
183
+ elif self.filename.suffix == ".csv":
184
+ self.read_csv()
185
+ else:
186
+ raise AssemblyError(f"Not supported file format '{self.filename}")
187
+
188
+ def write(self, filename: str):
189
+ if filename.endswith(".csv"):
190
+ return self.write_csv(filename)
191
+ elif filename.endswith(".json"):
192
+ return self.write_json(filename)
193
+ else:
194
+ logger.critical(f"Unable to write to '{filename}': format unsupported.")
195
+
196
+ def read_csv(self):
197
+ name = str(self.filename.with_suffix(".fmu"))
198
+ self.root = AssemblyNode(name, step_size=self.default_step_size, auto_link=self.default_auto_link,
199
+ mt=self.default_mt, profiling=self.default_profiling,
200
+ auto_input=self.default_auto_input, auto_output=self.default_auto_output)
201
+
202
+ with open(self.input_pathname) as file:
203
+ reader = csv.reader(file, delimiter=';')
204
+ self._check_csv_headers(reader)
205
+ for i, row in enumerate(reader):
206
+ if not row or row[0][0] == '#': # skip blank line of comment
207
+ continue
208
+
209
+ try:
210
+ rule, from_fmu_filename, from_port_name, to_fmu_filename, to_port_name = row
211
+ except ValueError:
212
+ logger.error(f"Line #{i+2}: expecting 5 columns. Line skipped.")
213
+ continue
214
+
215
+ try:
216
+ self._read_csv_rule(self.root, rule.upper(),
217
+ from_fmu_filename, from_port_name, to_fmu_filename, to_port_name)
218
+ except AssemblyError as e:
219
+ logger.error(f"Line #{i+2}: {e}. Line skipped.")
220
+ continue
221
+
222
+ @staticmethod
223
+ def _check_csv_headers(reader):
224
+ headers = next(reader)
225
+ headers_lowered = [h.lower() for h in headers]
226
+ if not headers_lowered == ["rule", "from_fmu", "from_port", "to_fmu", "to_port"]:
227
+ raise AssemblyError("Header (1st line of the file) is not well formatted.")
228
+
229
+ @staticmethod
230
+ def _read_csv_rule(node: AssemblyNode, rule: str, from_fmu_filename: str, from_port_name: str,
231
+ to_fmu_filename: str, to_port_name: str):
232
+ if rule == "FMU":
233
+ if not from_fmu_filename:
234
+ raise AssemblyError("Missing FMU information.")
235
+ node.add_fmu(from_fmu_filename)
236
+
237
+ elif rule == "INPUT":
238
+ if not to_fmu_filename or not to_port_name:
239
+ raise AssemblyError("Missing INPUT ports information.")
240
+ if not from_port_name:
241
+ from_port_name = to_port_name
242
+ node.add_input(from_port_name, to_fmu_filename, to_port_name)
243
+
244
+ elif rule == "OUTPUT":
245
+ if not from_fmu_filename or not from_port_name:
246
+ raise AssemblyError("Missing OUTPUT ports information.")
247
+ if not to_port_name:
248
+ to_port_name = from_port_name
249
+ node.add_output(from_fmu_filename, from_port_name, to_port_name)
250
+
251
+ elif rule == "DROP":
252
+ if not from_fmu_filename or not from_port_name:
253
+ raise AssemblyError("Missing DROP ports information.")
254
+ node.add_drop_port(from_fmu_filename, from_port_name)
255
+
256
+ elif rule == "LINK":
257
+ node.add_link(from_fmu_filename, from_port_name, to_fmu_filename, to_port_name)
258
+
259
+ elif rule == "START":
260
+ if not from_fmu_filename or not from_port_name or not to_fmu_filename:
261
+ raise AssemblyError("Missing START ports information.")
262
+
263
+ node.add_start_value(from_fmu_filename, from_port_name, to_fmu_filename)
264
+ else:
265
+ raise AssemblyError(f"unexpected rule '{rule}'. Line skipped.")
266
+
267
+ def write_csv(self, filename: Union[str, Path]):
268
+ if self.root.children:
269
+ raise AssemblyError("This assembly is not flat. Cannot export to CSV file.")
270
+
271
+ with open(self.fmu_directory / filename, "wt") as outfile:
272
+ outfile.write("rule;from_fmu;from_port;to_fmu;to_port\n")
273
+ for fmu in self.root.fmu_names_list:
274
+ outfile.write(f"FMU;{fmu};;;\n")
275
+ for port, source in self.root.input_ports.items():
276
+ outfile.write(f"INPUT;;{source};{port.fmu_name};{port.port_name}\n")
277
+ for port, target in self.root.output_ports.items():
278
+ outfile.write(f"OUTPUT;{port.fmu_name};{port.port_name};;{target}\n")
279
+ for link in self.root.links:
280
+ outfile.write(f"LINK;{link.from_port.fmu_name};{link.from_port.port_name};"
281
+ f"{link.to_port.fmu_name};{link.to_port.port_name}\n")
282
+ for port, value in self.root.start_values.items():
283
+ outfile.write(f"START;{port.fmu_name};{port.port_name};{value};\n")
284
+ for port in self.root.drop_ports:
285
+ outfile.write(f"DROP;{port.fmu_name};{port.port_name};;\n")
286
+
287
+ def read_json(self):
288
+ with open(self.input_pathname) as file:
289
+ try:
290
+ data = json.load(file)
291
+ except json.decoder.JSONDecodeError as e:
292
+ raise AssemblyError(f"Cannot read json: {e}")
293
+ self.root = self._json_decode_node(data)
294
+ if not self.root.name:
295
+ self.root.name = str(self.filename.with_suffix(".fmu"))
296
+
297
+ def _json_decode_node(self, data:Dict) -> AssemblyNode:
298
+ name = data.get("name", None) # 1
299
+ step_size = data.get("step_size", self.default_step_size) # 7
300
+ auto_link = data.get("auto_link", self.default_auto_link) # 4
301
+ auto_input = data.get("auto_input", self.default_auto_input) # 5
302
+ auto_output = data.get("auto_output", self.default_auto_output) # 6
303
+ mt = data.get("mt", self.default_mt) # 2
304
+ profiling = data.get("profiling", self.default_profiling) # 3
305
+
306
+ node = AssemblyNode(name, step_size=step_size, auto_link=auto_link, mt=mt, profiling=profiling,
307
+ auto_input=auto_input, auto_output=auto_output)
308
+
309
+ for key, value in data.items():
310
+ if key in ('name', 'step_size', 'auto_link', 'auto_input', 'auto_output', 'mt', 'profiling'):
311
+ continue # Already read
312
+
313
+ elif key == "container": # 8
314
+ if not isinstance(value, list):
315
+ raise AssemblyError("JSON: 'container' keyword should define a list.")
316
+ for sub_data in value:
317
+ node.add_sub_node(self._json_decode_node(sub_data))
318
+
319
+ elif key == "fmu": # 9
320
+ if not isinstance(value, list):
321
+ raise AssemblyError("JSON: 'fmu' keyword should define a list.")
322
+ for fmu in value:
323
+ node.add_fmu(fmu)
324
+
325
+ elif key == "input": # 10
326
+ self._json_decode_keyword('input', value, node.add_input)
327
+
328
+ elif key == "output": # 11
329
+ self._json_decode_keyword('output', value, node.add_output)
330
+
331
+ elif key == "link": # 12
332
+ self._json_decode_keyword('link', value, node.add_link)
333
+
334
+ elif key == "start": # 13
335
+ self._json_decode_keyword('start', value, node.add_start_value)
336
+
337
+ elif key == "drop": #14
338
+ self._json_decode_keyword('drop', value, node.add_drop_port)
339
+
340
+ else:
341
+ logger.error(f"JSON: unexpected keyword {key}. Skipped.")
342
+
343
+ return node
344
+
345
+ @staticmethod
346
+ def _json_decode_keyword(keyword: str, value, function):
347
+ if not isinstance(value, list):
348
+ raise AssemblyError(f"JSON: '{keyword}' keyword should define a list.")
349
+ for line in value:
350
+ if not isinstance(line, list):
351
+ raise AssemblyError(f"JSON: unexpected '{keyword}' value: {line}.")
352
+ try:
353
+ function(*line)
354
+ except TypeError:
355
+ raise AssemblyError(f"JSON: '{keyword}' value does not contain right number of fields: {line}.")
356
+
357
+
358
+ def write_json(self, filename: Union[str, Path]):
359
+ with open(self.fmu_directory / filename, "wt") as file:
360
+ data = self._json_encode_node(self.root)
361
+ json.dump(data, file, indent=2)
362
+
363
+ def _json_encode_node(self, node: AssemblyNode) -> Dict[str, Any]:
364
+ json_node = dict()
365
+ json_node["name"] = node.name # 1
366
+ json_node["mt"] = node.mt # 2
367
+ json_node["profiling"] = node.profiling # 3
368
+ json_node["auto_link"] = node.auto_link # 4
369
+ json_node["auto_input"] = node.auto_input # 5
370
+ json_node["auto_output"] = node.auto_output # 6
371
+
372
+ if node.step_size:
373
+ json_node["step_size"] = node.step_size # 7
374
+
375
+ if node.children:
376
+ json_node["container"] = [self._json_encode_node(child) for child in node.children] # 8
377
+
378
+ if node.fmu_names_list:
379
+ json_node["fmu"] = [f"{fmu_name}" for fmu_name in sorted(node.fmu_names_list)] #9
380
+
381
+ if node.input_ports:
382
+ json_node["input"] = [[f"{source}", f"{port.fmu_name}", f"{port.port_name}"] # 10
383
+ for port, source in node.input_ports.items()]
384
+
385
+ if node.output_ports:
386
+ json_node["output"] = [[f"{port.fmu_name}", f"{port.port_name}", f"{target}"] # 11
387
+ for port, target in node.output_ports.items()]
388
+
389
+ if node.links:
390
+ json_node["link"] = [[f"{link.from_port.fmu_name}", f"{link.from_port.port_name}", # 12
391
+ f"{link.to_port.fmu_name}", f"{link.to_port.port_name}"]
392
+ for link in node.links]
393
+
394
+ if node.start_values:
395
+ json_node["start"] = [[f"{port.fmu_name}", f"{port.port_name}", value] # 13
396
+ for port, value in node.start_values.items()]
397
+
398
+ if node.drop_ports:
399
+ json_node["drop"] = [[f"{port.fmu_name}", f"{port.port_name}"] for port in node.drop_ports] # 14
400
+
401
+ return json_node
402
+
403
+ def read_ssp(self):
404
+ logger.warning("This feature is ALPHA stage.")
405
+
406
+ with zipfile.ZipFile(self.fmu_directory / self.filename) as zin:
407
+ for file in zin.filelist:
408
+ if file.filename.endswith(".fmu") or file.filename.endswith(".ssd"):
409
+ zin.extract(file, path=self.fmu_directory)
410
+ logger.debug(f"Extracted: {file.filename}")
411
+ self.add_transient_file(file.filename)
412
+
413
+ self.description_pathname = self.fmu_directory / "SystemStructure.ssd"
414
+ if self.description_pathname.is_file():
415
+ sdd = SSDParser(step_size=self.default_step_size, auto_link=self.default_auto_link,
416
+ mt=self.default_mt, profiling=self.default_profiling,
417
+ auto_input=self.default_auto_input, auto_output=self.default_auto_output)
418
+ self.root = sdd.parse(self.description_pathname)
419
+ self.root.name = str(self.filename.with_suffix(".fmu"))
420
+
421
+ def make_fmu(self, dump_json=False):
422
+ if dump_json:
423
+ dump_file = Path(self.input_pathname.stem + "-dump").with_suffix(".json")
424
+ logger.info(f"Dump Json '{dump_file}'")
425
+ self.write_json(dump_file)
426
+ self.root.make_fmu(self.fmu_directory, debug=self.debug, description_pathname=self.description_pathname)
427
+
428
+
429
+ class SSDParser:
430
+ def __init__(self, **kwargs):
431
+ self.node_stack: List[AssemblyNode] = []
432
+ self.root = None
433
+ self.fmu_filenames: Dict[str, str] = {} # Component name => FMU filename
434
+ self.node_attrs = kwargs
435
+
436
+ def parse(self, ssd_filepath: Path) -> AssemblyNode:
437
+ logger.debug(f"Analysing {ssd_filepath}")
438
+ with open(ssd_filepath, "rb") as file:
439
+ parser = xml.parsers.expat.ParserCreate()
440
+ parser.StartElementHandler = self.start_element
441
+ parser.EndElementHandler = self.end_element
442
+ parser.ParseFile(file)
443
+
444
+ return self.root
445
+
446
+ def start_element(self, tag_name, attrs):
447
+ if tag_name == 'ssd:Connection':
448
+ if 'startElement' in attrs:
449
+ if 'endElement' in attrs:
450
+ fmu_start = self.fmu_filenames[attrs['startElement']]
451
+ fmu_end = self.fmu_filenames[attrs['endElement']]
452
+ self.node_stack[-1].add_link(fmu_start, attrs['startConnector'],
453
+ fmu_end, attrs['endConnector'])
454
+ else:
455
+ fmu_start = self.fmu_filenames[attrs['startElement']]
456
+ self.node_stack[-1].add_output(fmu_start, attrs['startConnector'],
457
+ attrs['endConnector'])
458
+ else:
459
+ fmu_end = self.fmu_filenames[attrs['endElement']]
460
+ self.node_stack[-1].add_input(attrs['startConnector'],
461
+ fmu_end, attrs['endConnector'])
462
+
463
+ elif tag_name == 'ssd:System':
464
+ logger.info(f"SSP System: {attrs['name']}")
465
+ filename = attrs['name'] + ".fmu"
466
+ self.fmu_filenames[attrs['name']] = filename
467
+ node = AssemblyNode(filename, **self.node_attrs)
468
+ if self.node_stack:
469
+ self.node_stack[-1].add_sub_node(node)
470
+ else:
471
+ self.root = node
472
+
473
+ self.node_stack.append(node)
474
+
475
+ elif tag_name == 'ssd:Component':
476
+ filename = attrs['source']
477
+ name = attrs['name']
478
+ self.fmu_filenames[name] = filename
479
+ self.node_stack[-1].add_fmu(filename)
480
+ logger.debug(f"Component {name} => {filename}")
481
+
482
+ def end_element(self, tag_name):
483
+ if tag_name == 'ssd:System':
484
+ self.node_stack.pop()
@@ -4,7 +4,8 @@ import sys
4
4
  from colorama import Fore, Style, init
5
5
 
6
6
  from .fmu_operations import *
7
- from .fmu_container import FMUContainerSpecReader, FMUContainerError
7
+ from .fmu_container import FMUContainerError
8
+ from .assembly import Assembly, AssemblyError
8
9
  from .checker import checker_list
9
10
  from .version import __version__ as version
10
11
  from .help import Help
@@ -158,11 +159,12 @@ def fmucontainer():
158
159
 
159
160
  parser.add_argument('-h', '-help', action="help")
160
161
 
161
- parser.add_argument("-fmu-directory", action="store", dest="fmu_directory", required=True,
162
- help="Directory containing initial FMU’s and used to generate containers.")
162
+ parser.add_argument("-fmu-directory", action="store", dest="fmu_directory", required=False, default=".",
163
+ help="Directory containing initial FMU’s and used to generate containers. "
164
+ "If not defined, current directory is used.")
163
165
 
164
166
  parser.add_argument("-container", action="append", dest="container_descriptions_list", default=[],
165
- metavar="filename.csv:step_size",
167
+ metavar="filename.{csv|json|ssp},[:step_size]", required=True,
166
168
  help="Description of the container to create.")
167
169
 
168
170
  parser.add_argument("-debug", action="store_true", dest="debug",
@@ -183,34 +185,42 @@ def fmucontainer():
183
185
  parser.add_argument("-profile", action="store_true", dest="profiling", default=False,
184
186
  help="Enable Profiling mode for the generated container.")
185
187
 
188
+ parser.add_argument("-dump-json", action="store_true", dest="dump", default=False,
189
+ help="Dump a JSON file for each container.")
190
+
186
191
  config = parser.parse_args()
187
192
 
188
193
  if config.debug:
189
194
  logger.setLevel(logging.DEBUG)
190
195
 
196
+ fmu_directory = Path(config.fmu_directory)
197
+ logger.info(f"FMU directory: '{fmu_directory}'")
198
+
191
199
  for description in config.container_descriptions_list:
192
200
  try:
193
- filename_description, step_size = description.split(":")
201
+ filename, step_size = description.split(":")
194
202
  step_size = float(step_size)
195
203
  except ValueError:
196
204
  step_size = None
197
- filename_description = description
205
+ filename = description
206
+ try:
207
+ assembly = Assembly(filename, step_size=step_size, auto_link=config.auto_link,
208
+ auto_input=config.auto_input, auto_output=config.auto_output, mt=config.mt,
209
+ profiling=config.profiling, fmu_directory=fmu_directory, debug=config.debug)
198
210
 
199
- container_filename = Path(filename_description).with_suffix(".fmu")
211
+ except FileNotFoundError as e:
212
+ logger.fatal(f"Cannot read file: {e}")
213
+ continue
214
+ except (FMUContainerError, AssemblyError) as e:
215
+ logger.fatal(f"{filename}: {e}")
216
+ continue
200
217
 
201
218
  try:
202
- csv_reader = FMUContainerSpecReader(Path(config.fmu_directory))
203
- container = csv_reader.read(filename_description)
204
- container.add_implicit_rule(auto_input=config.auto_input,
205
- auto_output=config.auto_output,
206
- auto_link=config.auto_link)
207
- container.make_fmu(container_filename, step_size=step_size, debug=config.debug, mt=config.mt,
208
- profiling=config.profiling)
209
- except (FileNotFoundError, FMUContainerError, FMUException) as e:
210
- logger.error(f"Cannot build container from '{filename_description}': {e}")
219
+ assembly.make_fmu(dump_json=config.dump)
220
+ except FMUContainerError as e:
221
+ logger.fatal(f"{filename}: {e}")
211
222
  continue
212
223
 
213
224
 
214
- # for debug purpose
215
225
  if __name__ == "__main__":
216
226
  fmucontainer()
@@ -1,4 +1,3 @@
1
- import csv
2
1
  import logging
3
2
  import os
4
3
  import shutil
@@ -8,9 +7,10 @@ from datetime import datetime
8
7
  from pathlib import Path
9
8
  from typing import *
10
9
 
11
- from .fmu_operations import FMU, OperationAbstract, FMUException
10
+ from .fmu_operations import FMU, OperationAbstract
12
11
  from .version import __version__ as tool_version
13
12
 
13
+
14
14
  logger = logging.getLogger("fmu_manipulation_toolbox")
15
15
 
16
16
 
@@ -30,15 +30,14 @@ class FMUPort:
30
30
  def set_port_type(self, type_name: str, attrs: Dict[str, str]):
31
31
  self.type_name = type_name
32
32
  self.child = attrs.copy()
33
- try:
34
- self.child.pop("unit") # Unit are not supported
35
- except KeyError:
36
- pass
33
+ for unsupported in ("unit", "declaredType"):
34
+ if unsupported in self.child:
35
+ self.child.pop(unsupported)
37
36
 
38
37
  def xml(self, vr: int, name=None, causality=None, start=None):
39
38
 
40
39
  if self.child is None:
41
- raise FMUException(f"FMUPort has no child. Bug?")
40
+ raise FMUContainerError(f"FMUPort has no child. Bug?")
42
41
 
43
42
  child_str = f"<{self.type_name}"
44
43
  if self.child:
@@ -70,7 +69,6 @@ class FMUPort:
70
69
 
71
70
  class EmbeddedFMU(OperationAbstract):
72
71
  capability_list = ("needsExecutionTool",
73
- "canHandleVariableCommunicationStepSize",
74
72
  "canBeInstantiatedOnlyOncePerProcess")
75
73
 
76
74
  def __init__(self, filename):
@@ -87,6 +85,8 @@ class EmbeddedFMU(OperationAbstract):
87
85
  self.current_port = None # used during apply_operation()
88
86
 
89
87
  self.fmu.apply_operation(self) # Should be the last command in constructor!
88
+ if self.model_identifier is None:
89
+ raise FMUContainerError(f"FMU '{self.name}' does not implement Co-Simulation mode.")
90
90
 
91
91
  def fmi_attrs(self, attrs):
92
92
  self.guid = attrs['guid']
@@ -104,10 +104,16 @@ class EmbeddedFMU(OperationAbstract):
104
104
  self.capabilities[capability] = attrs.get(capability, "false")
105
105
 
106
106
  def experiment_attrs(self, attrs):
107
- self.step_size = float(attrs['stepSize'])
107
+ try:
108
+ self.step_size = float(attrs['stepSize'])
109
+ except KeyError:
110
+ logger.warning(f"FMU '{self.name}' does not specify preferred step size")
111
+ pass
108
112
 
109
113
  def scalar_type(self, type_name, attrs):
110
- self.current_port.set_port_type(type_name, attrs)
114
+ if self.current_port:
115
+ self.current_port.set_port_type(type_name, attrs)
116
+ self.current_port = None
111
117
 
112
118
  def __repr__(self):
113
119
  return f"FMU '{self.name}' ({len(self.ports)} variables)"
@@ -172,14 +178,14 @@ class ValueReferenceTable:
172
178
  def get_vr(self, cport: ContainerPort) -> int:
173
179
  return self.add_vr(cport.port.type_name)
174
180
 
175
- def add_vr(self, type_name:str) -> int:
181
+ def add_vr(self, type_name: str) -> int:
176
182
  vr = self.vr_table[type_name]
177
183
  self.vr_table[type_name] += 1
178
184
  return vr
179
185
 
180
186
 
181
187
  class FMUContainer:
182
- def __init__(self, identifier: str, fmu_directory: Union[str, Path]):
188
+ def __init__(self, identifier: str, fmu_directory: Union[str, Path], description_pathname=None):
183
189
  self.fmu_directory = Path(fmu_directory)
184
190
  self.identifier = identifier
185
191
  if not self.fmu_directory.is_dir():
@@ -187,8 +193,7 @@ class FMUContainer:
187
193
  self.involved_fmu: Dict[str, EmbeddedFMU] = {}
188
194
  self.execution_order: List[EmbeddedFMU] = []
189
195
 
190
- self.description_pathname = None # Will be set up by FMUContainerSpecReader
191
- self.period = None # Will be set up by FMUContainerSpecReader
196
+ self.description_pathname = description_pathname
192
197
 
193
198
  # Rules
194
199
  self.inputs: Dict[str, ContainerPort] = {}
@@ -207,10 +212,10 @@ class FMUContainer:
207
212
  self.involved_fmu[fmu_filename] = fmu
208
213
  self.execution_order.append(fmu)
209
214
  if not fmu.fmi_version == "2.0":
210
- raise FMUException("Only FMI-2.0 is supported by FMUContainer")
215
+ raise FMUContainerError("Only FMI-2.0 is supported by FMUContainer")
211
216
  logger.debug(f"Adding FMU #{len(self.execution_order)}: {fmu}")
212
217
  except Exception as e:
213
- raise FMUException(f"Cannot load '{fmu_filename}': {e}")
218
+ raise FMUContainerError(f"Cannot load '{fmu_filename}': {e}")
214
219
 
215
220
  return fmu
216
221
 
@@ -227,7 +232,8 @@ class FMUContainer:
227
232
  container_port_name = to_port_name
228
233
  cport_to = ContainerPort(self.get_fmu(to_fmu_filename), to_port_name)
229
234
  if not cport_to.port.causality == "input": # check causality
230
- raise FMUException(f"{cport_to} is {cport_to.port.causality} instead of INPUT.")
235
+ raise FMUContainerError(f"Tried to use '{cport_to}' as INPUT of the container but FMU causality is "
236
+ f"'{cport_to.port.causality}'.")
231
237
 
232
238
  logger.debug(f"INPUT: {to_fmu_filename}:{to_port_name}")
233
239
  self.mark_ruled(cport_to, 'INPUT')
@@ -239,7 +245,8 @@ class FMUContainer:
239
245
 
240
246
  cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
241
247
  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")
248
+ raise FMUContainerError(f"Tried to use '{cport_from}' as INPUT of the container but FMU causality is "
249
+ f"'{cport_from.port.causality}'.")
243
250
 
244
251
  logger.debug(f"OUTPUT: {from_fmu_filename}:{from_port_name}")
245
252
  self.mark_ruled(cport_from, 'OUTPUT')
@@ -248,7 +255,7 @@ class FMUContainer:
248
255
  def drop_port(self, from_fmu_filename: str, from_port_name: str):
249
256
  cport_from = ContainerPort(self.get_fmu(from_fmu_filename), from_port_name)
250
257
  if not cport_from.port.causality == "output": # check causality
251
- raise FMUException(f"{cport_from}: trying to DROP {cport_from.port.causality}")
258
+ raise FMUContainerError(f"{cport_from}: trying to DROP {cport_from.port.causality}")
252
259
 
253
260
  logger.debug(f"DROP: {from_fmu_filename}:{from_port_name}")
254
261
  self.mark_ruled(cport_from, 'DROP')
@@ -284,7 +291,7 @@ class FMUContainer:
284
291
 
285
292
  self.start_values[cport] = value
286
293
 
287
- def find_input(self, port_to_connect: FMUPort) -> Union[ContainerPort, None]:
294
+ def find_input(self, port_to_connect: FMUPort) -> Optional[ContainerPort]:
288
295
  for fmu in self.execution_order:
289
296
  for port in fmu.ports.values():
290
297
  if (port.causality == 'input' and port.name == port_to_connect.name
@@ -339,29 +346,26 @@ class FMUContainer:
339
346
 
340
347
  return step_size
341
348
 
342
- def sanity_check(self, step_size: Union[float, None]):
343
- nb_error = 0
349
+ def sanity_check(self, step_size: Optional[float]):
344
350
  for fmu in self.execution_order:
351
+ if not fmu.step_size:
352
+ continue
345
353
  ts_ratio = step_size / fmu.step_size
346
354
  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")
355
+ logger.warning(f"Container step_size={step_size}s is lower than FMU '{fmu.name}' "
356
+ f"step_size={fmu.step_size}s.")
349
357
  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")
358
+ logger.warning(f"Container step_size={step_size}s should divisible by FMU '{fmu.name}' "
359
+ f"step_size={fmu.step_size}s.")
352
360
  for port_name in fmu.ports:
353
361
  cport = ContainerPort(fmu, port_name)
354
362
  if cport not in self.rules:
355
363
  if cport.port.causality == 'input':
356
364
  logger.error(f"{cport} is not connected")
357
- nb_error += 1
358
365
  if cport.port.causality == 'output':
359
366
  logger.warning(f"{cport} is not connected")
360
367
 
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,
368
+ def make_fmu(self, fmu_filename: Union[str, Path], step_size: Optional[float] = None, debug=False, mt=False,
365
369
  profiling=False):
366
370
  if isinstance(fmu_filename, str):
367
371
  fmu_filename = Path(fmu_filename)
@@ -412,12 +416,12 @@ class FMUContainer:
412
416
  description="FMUContainer with {embedded_fmu}"
413
417
  author="{author}"
414
418
  license="Proprietary"
415
- copyright="© Renault S.A.S"
419
+ copyright="See Embedded FMU's copyrights."
416
420
  variableNamingConvention="structured">
417
421
 
418
422
  <CoSimulation
419
423
  modelIdentifier="{self.identifier}"
420
- canHandleVariableCommunicationStepSize="{capabilities['canHandleVariableCommunicationStepSize']}"
424
+ canHandleVariableCommunicationStepSize="true"
421
425
  canBeInstantiatedOnlyOncePerProcess="{capabilities['canBeInstantiatedOnlyOncePerProcess']}"
422
426
  canNotUseMemoryManagementFunctions="true"
423
427
  canGetAndSetFMUstate="false"
@@ -438,7 +442,8 @@ class FMUContainer:
438
442
  for fmu in self.execution_order:
439
443
  vr = vr_table.add_vr("Real")
440
444
  name = f"container.{fmu.model_identifier}.rt_ratio"
441
- print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local"><Real /></ScalarVariable>', file=xml_file)
445
+ print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local">'
446
+ f'<Real /></ScalarVariable>', file=xml_file)
442
447
 
443
448
  # Local variable should be first to ensure to attribute them the lowest VR.
444
449
  for local in self.locals.values():
@@ -555,7 +560,7 @@ class FMUContainer:
555
560
  if profiling and type_name == "Real":
556
561
  nb += len(self.execution_order)
557
562
  print(nb, file=txt_file)
558
- for profiling_port,_ in enumerate(self.execution_order):
563
+ for profiling_port, _ in enumerate(self.execution_order):
559
564
  print(f"{profiling_port} -2 {profiling_port}", file=txt_file)
560
565
  else:
561
566
  print(nb, file=txt_file)
@@ -600,6 +605,7 @@ class FMUContainer:
600
605
  documentation_directory.mkdir(exist_ok=True)
601
606
 
602
607
  if self.description_pathname:
608
+ logger.debug(f"Copying {self.description_pathname}")
603
609
  shutil.copy(self.description_pathname, documentation_directory)
604
610
 
605
611
  shutil.copy(origin / "model.png", base_directory)
@@ -628,157 +634,3 @@ class FMUContainer:
628
634
  def make_fmu_cleanup(base_directory: Path):
629
635
  logger.debug(f"Delete directory '{base_directory}'")
630
636
  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)
@@ -1,10 +1,10 @@
1
1
  import os.path
2
2
  import sys
3
3
  from .version import __version__ as version
4
- from PyQt5.QtCore import Qt, QObject, QUrl, pyqtSignal, QDir
5
- from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog,
6
- QTextBrowser, QInputDialog, QMenu, QAction)
7
- from PyQt5.QtGui import QPixmap, QImage, QFont, QTextCursor, QIcon, QColor, QPainter, QBrush, QDesktopServices
4
+ from PyQt6.QtCore import Qt, QObject, QUrl, pyqtSignal, QDir, QSize, QPoint
5
+ from PyQt6.QtWidgets import (QApplication, QWidget, QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog,
6
+ QTextBrowser, QInputDialog, QMenu)
7
+ from PyQt6.QtGui import (QPixmap, QImage, QFont, QTextCursor, QIcon, QDesktopServices, QAction, QPainter, QColor)
8
8
  import textwrap
9
9
  from functools import partial
10
10
  from typing import Optional
@@ -42,7 +42,7 @@ class DropZoneWidget(QLabel):
42
42
 
43
43
  def dropEvent(self, event):
44
44
  if event.mimeData().hasImage:
45
- event.setDropAction(Qt.CopyAction)
45
+ event.setDropAction(Qt.DropAction.CopyAction)
46
46
  try:
47
47
  file_path = event.mimeData().urls()[0].toLocalFile()
48
48
  except IndexError:
@@ -59,8 +59,8 @@ class DropZoneWidget(QLabel):
59
59
  else:
60
60
  default_directory = os.path.expanduser('~')
61
61
 
62
- fmu_filename, _ = QFileDialog.getOpenFileName(self, 'Select FMU to Manipulate',
63
- default_directory, "FMU files (*.fmu)")
62
+ fmu_filename, _ = QFileDialog.getOpenFileName(parent=self, caption='Select FMU to Manipulate',
63
+ directory=default_directory, filter="FMU files (*.fmu)")
64
64
  if fmu_filename:
65
65
  self.set_fmu(fmu_filename)
66
66
 
@@ -70,23 +70,23 @@ class DropZoneWidget(QLabel):
70
70
  elif not os.path.isfile(filename):
71
71
  filename = os.path.join(os.path.dirname(__file__), "resources", "fmu.png")
72
72
 
73
- image = QImage(filename).scaled(self.WIDTH-4, self.HEIGHT-4, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
74
- pixmap = QPixmap.fromImage(image)
75
- rounded = self.make_pixmap_rounded(pixmap)
76
- self.setPixmap(rounded)
73
+ base_image = QImage(filename).scaled(self.WIDTH, self.HEIGHT, Qt.AspectRatioMode.IgnoreAspectRatio,
74
+ Qt.TransformationMode.SmoothTransformation)
75
+ mask_filename = os.path.join(os.path.dirname(__file__), "resources", "mask.png")
76
+ mask_image = QImage(mask_filename).scaled(self.WIDTH, self.HEIGHT, Qt.AspectRatioMode.IgnoreAspectRatio,
77
+ Qt.TransformationMode.SmoothTransformation)
78
+ rounded_image = QImage(self.WIDTH, self.HEIGHT, QImage.Format.Format_ARGB32)
79
+ rounded_image.fill(QColor(0, 0, 0, 0))
80
+ painter = QPainter()
81
+ painter.begin(rounded_image)
82
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
83
+ painter.drawImage(QPoint(0, 0), base_image)
84
+ painter.drawImage(QPoint(0, 0), mask_image)
85
+ painter.end()
86
+ pixmap = QPixmap.fromImage(rounded_image)
77
87
 
78
- def make_pixmap_rounded(self, pixmap):
79
- rounded = QPixmap(pixmap.size())
80
- rounded.fill(QColor("transparent"))
88
+ self.setPixmap(pixmap)
81
89
 
82
- painter = QPainter(rounded)
83
- painter.setRenderHint(QPainter.Antialiasing)
84
- painter.setBrush(QBrush(pixmap))
85
- painter.setPen(Qt.NoPen)
86
- painter.drawRoundedRect(pixmap.rect(), 20, 20)
87
- del painter # Mandatory to avoid a crash
88
- self.update() # Mandatory to avoid a crash
89
- return rounded
90
90
 
91
91
  def set_fmu(self, filename):
92
92
  try:
@@ -147,7 +147,7 @@ class LogWidget(QTextBrowser):
147
147
  LogWidget.XStream.stdout().messageWritten.connect(self.insertPlainText)
148
148
  LogWidget.XStream.stderr().messageWritten.connect(self.insertPlainText)
149
149
 
150
- def loadResource(self, type, name):
150
+ def loadResource(self, _, name):
151
151
  image_path = os.path.join(os.path.dirname(__file__), "resources", name.toString())
152
152
  return QPixmap(image_path)
153
153
 
@@ -162,7 +162,7 @@ class HelpWidget(QLabel):
162
162
  filename = os.path.join(os.path.dirname(__file__), "resources", "help.png")
163
163
  image = QPixmap(filename)
164
164
  self.setPixmap(image)
165
- self.setAlignment(Qt.AlignRight)
165
+ self.setAlignment(Qt.AlignmentFlag.AlignRight)
166
166
 
167
167
  def mousePressEvent(self, event):
168
168
  QDesktopServices.openUrl(QUrl(self.HELP_URL))
@@ -209,14 +209,16 @@ class FilterWidget(QPushButton):
209
209
 
210
210
 
211
211
  class FMUManipulationToolboxlMainWindow(QWidget):
212
- def __init__(self, app, *args, **kwargs):
212
+ def __init__(self, *args, **kwargs):
213
213
  super().__init__(*args, **kwargs)
214
214
 
215
215
  self.setWindowTitle('FMU Manipulation Toolbox')
216
- self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), 'resources', 'icon.png')))
217
216
 
218
217
  # set the grid layout
219
218
  self.layout = QGridLayout()
219
+ self.layout.setVerticalSpacing(4)
220
+ self.layout.setHorizontalSpacing(4)
221
+ self.layout.setContentsMargins(10, 10, 10, 10)
220
222
  self.setLayout(self.layout)
221
223
 
222
224
  self.dropped_fmu = DropZoneWidget()
@@ -266,7 +268,7 @@ class FMUManipulationToolboxlMainWindow(QWidget):
266
268
 
267
269
  line += 1
268
270
  self.apply_filter_label = QLabel("Apply modification only on: ")
269
- self.layout.addWidget(self.apply_filter_label, line, 1, 1, 2, alignment=Qt.AlignRight)
271
+ self.layout.addWidget(self.apply_filter_label, line, 1, 1, 2, alignment=Qt.AlignmentFlag.AlignRight)
270
272
  self.set_tooltip(self.apply_filter_label, 'gui-apply-only')
271
273
 
272
274
  causality = ["parameter", "calculatedParameter", "input", "output", "local", "independent"]
@@ -289,7 +291,7 @@ class FMUManipulationToolboxlMainWindow(QWidget):
289
291
 
290
292
  exit_button = QPushButton('Exit')
291
293
  self.layout.addWidget(exit_button, line, 0, 1, 2)
292
- exit_button.clicked.connect(app.exit)
294
+ exit_button.clicked.connect(self.close)
293
295
  exit_button.setProperty("class", "quit")
294
296
 
295
297
  save_log_button = QPushButton('Save log as')
@@ -415,7 +417,7 @@ class FMUManipulationToolboxlMainWindow(QWidget):
415
417
 
416
418
  def apply_operation(self, operation):
417
419
  if self.dropped_fmu.fmu:
418
- self.log_widget.moveCursor(QTextCursor.End)
420
+ self.log_widget.moveCursor(QTextCursor.MoveOperation.End)
419
421
  fmu_filename = os.path.basename(self.dropped_fmu.fmu.fmu_filename)
420
422
  print('-' * 100)
421
423
  self.log_widget.insertHtml(f"<strong>{fmu_filename}: {operation}</strong><br>")
@@ -434,20 +436,21 @@ class FMUManipulationToolboxlMainWindow(QWidget):
434
436
  scroll_bar.setValue(scroll_bar.maximum())
435
437
 
436
438
 
437
- class Application:
439
+ class Application(QApplication):
438
440
  """
439
441
  Analyse and modify your FMUs.
440
442
 
441
- Note: modifying the modelDescription.xml can damage your FMU ! Communicating with the FMU-developer and adapting the
442
- way the FMU is generated, is preferable when possible.
443
+ Note: modifying the modelDescription.xml can damage your FMU !
444
+ Communicating with the FMU-developer and adapting the way the FMU is generated, is preferable when possible.
443
445
 
444
446
  """
445
- def __init__(self):
447
+ def __init__(self, *args, **kwargs):
448
+ super().__init__(*args, **kwargs)
449
+
446
450
  QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), "resources"))
447
- self.app = QApplication(sys.argv)
448
451
  font = QFont("Verdana")
449
452
  font.setPointSize(10)
450
- self.app.setFont(font)
453
+ self.setFont(font)
451
454
  css_dark = """
452
455
  QWidget {background: #4b4e51; color: #b5bab9}
453
456
  QPushButton { min-height: 30px; padding: 1px 1px 0.2em 0.2em; border: 1px solid #282830; border-radius: 5px;}
@@ -465,8 +468,8 @@ QPushButton.save:hover {background-color: #675a78; color: #dddddd;}
465
468
  QPushButton.quit {background-color: #4571a4; color: #dddddd;}
466
469
  QPushButton.quit:hover {background-color: #5682b5; color: #dddddd;}
467
470
  QToolTip {color: black}
468
- QLabel.dropped_fmu {background-color: #b5bab9; border: 2px dashed #282830; border-radius: dashed 20px;}
469
- QLabel.dropped_fmu:hover {background-color: #c6cbca; border: 2px dashed #282830; border-radius: dashed 20px;}
471
+ QLabel.dropped_fmu {background-color: #b5bab9}
472
+ QLabel.dropped_fmu:hover {background-color: #c6cbca}
470
473
  QTextBrowser {background-color: #282830; color: #b5bab9;}
471
474
  QMenu {font-size: 18px;}
472
475
  QMenu::item {padding: 2px 250px 2px 20px; border: 1px solid transparent;}
@@ -479,15 +482,25 @@ QMenu::indicator:unchecked:hover {width: 35px; image: url(images:checkbox-unc
479
482
  QMenu::indicator:unchecked:disabled {width: 35px; image: url(images:checkbox-unchecked-disabled.png); }
480
483
  """
481
484
 
482
- self.app.setStyleSheet(css_dark)
483
- self.window = FMUManipulationToolboxlMainWindow(self.app)
484
- print(" "*80, f"Version {version}")
485
- print(self.__doc__)
486
- sys.exit(self.app.exec())
485
+ self.setStyleSheet(css_dark)
486
+
487
+ if os.name == 'nt':
488
+ self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), 'resources', 'icon-round.png')))
489
+
490
+ # https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105
491
+ import ctypes
492
+ application_id = 'FMU_Manipulation_Toolbox' # arbitrary string
493
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(application_id)
494
+ else:
495
+ self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), 'resources', 'icon.png')))
487
496
 
488
- def exit(self):
489
- self.app.exit()
497
+ self.window = FMUManipulationToolboxlMainWindow()
490
498
 
491
499
 
492
500
  def main():
493
- Application()
501
+ application = Application(sys.argv)
502
+
503
+ print(" " * 80, f"Version {version}")
504
+ print(application.__doc__)
505
+
506
+ sys.exit(application.exec())
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fmu_manipulation_toolbox
3
- Version: 1.7.5
3
+ Version: 1.8.1
4
4
  Summary: FMU Manipulation Toobox is a python application which help to modify a Functional Mock-up Units (FMUs) without recompilation or to group them into FMU Containers
5
5
  Home-page: https://github.com/grouperenault/fmu_manipulation_toolbox/
6
6
  Author: Nicolas.LAURENT@Renault.com
@@ -1,11 +1,12 @@
1
1
  fmu_manipulation_toolbox/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
2
  fmu_manipulation_toolbox/__main__.py,sha256=mAzrtkil506DS0F3g3CEbHdtZsZotzntcNhIn_lNJkw,344
3
- fmu_manipulation_toolbox/__version__.py,sha256=Wut6SKNbd8Xv-YaWqFSqRdSiynpuD2Qf2Lt-2Or4hcA,9
3
+ fmu_manipulation_toolbox/__version__.py,sha256=UHHZBWWhJrlslwhXubEi_Un0xr55BiMWPTaN6R2jGLM,9
4
+ fmu_manipulation_toolbox/assembly.py,sha256=i0BDfgAca5WgrIYDv1z1mMxaKTiP-tBlPtZJJqMxHgI,20776
4
5
  fmu_manipulation_toolbox/checker.py,sha256=lE2MpH4BAKCDjUvbr06N56u7ao8hWXaJgMKaLvmhFTQ,2272
5
- fmu_manipulation_toolbox/cli.py,sha256=QLBsocu0wRw64GRtZ6irFCyDyigqflQqGW3N3JFYkC4,9806
6
- fmu_manipulation_toolbox/fmu_container.py,sha256=kAPTNQhhKuw6S_8IZGQzxRHROGfoaLY8qj8USR_Lv50,33447
6
+ fmu_manipulation_toolbox/cli.py,sha256=yAyfpogj_NgGt0mQRFA9dBy0NvN8vFomKUUrAtOIA5c,10102
7
+ fmu_manipulation_toolbox/fmu_container.py,sha256=rbe71nS_kv2QyhcsaW70YlxR3iA_YO2_VX2ihRVxFQs,26338
7
8
  fmu_manipulation_toolbox/fmu_operations.py,sha256=Z3LVOnDvwzoBrqfibZPAn_Osw6MIuGrXtaboGFqp0DA,15836
8
- fmu_manipulation_toolbox/gui.py,sha256=uAluoN_ILEHsjO-6QftwEWQ_5Khp_I3iUuLg4eavbTc,19801
9
+ fmu_manipulation_toolbox/gui.py,sha256=VKhQpIEEilScBSwTSJkDUwSbtsmB_VMuX0QwhZczTq4,20628
9
10
  fmu_manipulation_toolbox/help.py,sha256=aklKiLrsE0adSzQ5uoEB1sBDmI6s4l231gavu4XxxzA,5856
10
11
  fmu_manipulation_toolbox/version.py,sha256=OhBLkZ1-nhC77kyvffPNAf6m8OZe1bYTnNf_PWs1NvM,392
11
12
  fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png,sha256=FWIuyrXlaNLLePHfXj7j9ca5rT8Hgr14KCe1EqTCZyk,2288
@@ -14,12 +15,14 @@ fmu_manipulation_toolbox/resources/checkbox-checked.png,sha256=gzyFqvRFsZixVh6Zl
14
15
  fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png,sha256=KNdiE8zJ8H-mH81spHL8Ck-V87dj-fPPPrPNZFRv3wA,1783
15
16
  fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png,sha256=7XT54vwzDfSK-i6oJ5BBKGXKz8duRRVtoUzaOlWX0WE,1797
16
17
  fmu_manipulation_toolbox/resources/checkbox-unchecked.png,sha256=w3MG3RwFeTwkVOAFi8ZBs6yNlmtVnXxXY5atNyvLw54,1793
17
- fmu_manipulation_toolbox/resources/drop_fmu.png,sha256=HreQjYhgo-bQXM3sXcY5uDVNLjvCkOWfCfr15LEorVc,10500
18
+ fmu_manipulation_toolbox/resources/drop_fmu.png,sha256=xt2YzoQVcvr95Ix-4kw6wT04yf660sUHGHkskHRE_l0,10365
18
19
  fmu_manipulation_toolbox/resources/fmu.png,sha256=7bI_cb3pcqEnwBCCL30v3MXPwOK8OtbhFFouRq9lTj8,12048
19
20
  fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png,sha256=hUhuGg88BU5j7KkqhnU981wSpzn9ftQuOSu8pMYuP-Y,37997
20
21
  fmu_manipulation_toolbox/resources/help.png,sha256=WrIbjmlgIqdo6UWYj6L6gG-BCGWlu4qma8HRgRk8k7o,1822
21
- fmu_manipulation_toolbox/resources/icon.png,sha256=_AhXOqIu_8rHO9ZLN_qU30UHhyHG9mrNJl0hy1ts7eg,47025
22
+ fmu_manipulation_toolbox/resources/icon-round.png,sha256=j7jk-9NVQQZJMNazjpj8WeC1q6ptXwTaiO38kPoDEnE,83754
23
+ fmu_manipulation_toolbox/resources/icon.png,sha256=Ovui-UDBARqdTlVQwTn5Ud8seSsVh9pdElwLq5s6xKg,69976
22
24
  fmu_manipulation_toolbox/resources/license.txt,sha256=5ODuU8g8pIkK-NMWXu_rjZ6k7gM7b-N2rmg87-2Kmqw,1583
25
+ fmu_manipulation_toolbox/resources/mask.png,sha256=px1U4hQGL0AmZ4BQPknOVREpMpTSejbah3ntkpqAzFA,3008
23
26
  fmu_manipulation_toolbox/resources/model.png,sha256=EAf_HnZJe8zYGZygerG1MMt2U-tMMZlifzXPj4_iORA,208788
24
27
  fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd,sha256=OGfyJtaJntKypX5KDpuZ-nV1oYLZ6HV16pkpKOmYox4,2731
25
28
  fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd,sha256=HwyV7LBse-PQSv4z1xjmtzPU3Hjnv4mluq9YdSBNHMQ,3704
@@ -28,19 +31,19 @@ fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd,sha256=hYZGmhv
28
31
  fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd,sha256=8A0hl2wb1suoxv47xezkmvVTyJM2ZJp5RPQ2xQ_SjlY,3883
29
32
  fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd,sha256=pB9Pe-yBMGZN-JQAu6VB_lXS99kz23mwiUSY74ONZd4,5403
30
33
  fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd,sha256=Tc9RWKqvUXpB1qsoU64DAHO4Gfu5DotrQJ3Ece7GnTU,4647
31
- fmu_manipulation_toolbox/resources/linux32/client_sm.so,sha256=y9noIe74sHfX26T_9ag6QzB5Q_RScjPhw5vRh8q7sSo,35936
32
- fmu_manipulation_toolbox/resources/linux32/server_sm,sha256=C9oGkyAw7AY_ogD2vKVUVWg9MLIVkwMdonbHjhWRq9k,22832
33
- fmu_manipulation_toolbox/resources/linux64/client_sm.so,sha256=swDZjFxC6LnOUzXGXlpPmhnX9jSVWmEvyWHSMs_rnfU,36992
34
- fmu_manipulation_toolbox/resources/linux64/container.so,sha256=nJ9TfzQKIclQTzGoMm5xTp0DfzjsG8PpOgmfQjfdENA,45376
35
- fmu_manipulation_toolbox/resources/linux64/server_sm,sha256=7TepHTwdMjb8nvd4ZFSs7FWjmQ7Zlse5V6NmRSbafdc,22760
36
- fmu_manipulation_toolbox/resources/win32/client_sm.dll,sha256=on58hE5TwGHcLNE-4ETASF3PB0J6UM5kjhOPQkJLXK4,17920
37
- fmu_manipulation_toolbox/resources/win32/server_sm.exe,sha256=jS_OgOVIZ8ovbLR00UILQBstbT22kayUeBW_PkJwsh4,15872
38
- fmu_manipulation_toolbox/resources/win64/client_sm.dll,sha256=W7Q1nnTV6XhFU-hUyHDTPTlACecvnpDzkGZOuBZtXS0,22016
39
- fmu_manipulation_toolbox/resources/win64/container.dll,sha256=SG8608-_NRdt6LUCdgAPtSLfHSpGlwUg7rClikhx7kk,32768
40
- fmu_manipulation_toolbox/resources/win64/server_sm.exe,sha256=bkrW_m5g83Hy8gOH3yENRYKAVnVLwz6J48lvBNeE55k,19456
41
- fmu_manipulation_toolbox-1.7.5.dist-info/LICENSE.txt,sha256=c_862mzyk6ownO3Gt6cVs0-53IXLi_-ZEQFNDVabESw,1285
42
- fmu_manipulation_toolbox-1.7.5.dist-info/METADATA,sha256=nheD9rxQotMPyvRnhb5yNuoUGEWv9WiYbUR6H7vBiwQ,967
43
- fmu_manipulation_toolbox-1.7.5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
44
- fmu_manipulation_toolbox-1.7.5.dist-info/entry_points.txt,sha256=jCPLMBdS-eOvmRfDv7n0wHZSbJHccHviW03mz5vwO-Q,124
45
- fmu_manipulation_toolbox-1.7.5.dist-info/top_level.txt,sha256=9D_h-5BMjSqf9z-XFkbJL_bMppR2XNYW3WNuPkXou0k,25
46
- fmu_manipulation_toolbox-1.7.5.dist-info/RECORD,,
34
+ fmu_manipulation_toolbox/resources/linux32/client_sm.so,sha256=xVdY2zy13pa2DcvFiweSNpp7E6DiONqeoBdlcJHrW_k,35940
35
+ fmu_manipulation_toolbox/resources/linux32/server_sm,sha256=1TLGqNPyM5UVOrCfzNqWyF6ClLksY3EiY3CSsrnp6c0,22836
36
+ fmu_manipulation_toolbox/resources/linux64/client_sm.so,sha256=EhY1XHo1YcQn6yqZ7wk5okqtZyp0MrcCsGcudqE-aIM,37000
37
+ fmu_manipulation_toolbox/resources/linux64/container.so,sha256=_uhkJYZa_z1tV0NBATWC8iAGt7lPY11_VA29a_5hXsM,45384
38
+ fmu_manipulation_toolbox/resources/linux64/server_sm,sha256=ulfoPvmaYe9nInYcVEyj7mD9zDzGk56OUoWx1mPKLiE,22768
39
+ fmu_manipulation_toolbox/resources/win32/client_sm.dll,sha256=usY6k86X-Y0De5-SxCvgJ3yRzPpR4KCNTShYzDeec80,17920
40
+ fmu_manipulation_toolbox/resources/win32/server_sm.exe,sha256=3570x6uDaKUIYBy9uOmID66ffvy-bxVa8Q2il4m5KZc,15872
41
+ fmu_manipulation_toolbox/resources/win64/client_sm.dll,sha256=G6bNin2QuwbTiToJNSsSM1Dil899MHnZ5FNm-4ExH5I,22016
42
+ fmu_manipulation_toolbox/resources/win64/container.dll,sha256=RBBo9kBBYhYe_KLPl5MtJJCVuL1Zf1LNT87mT88JCPg,32768
43
+ fmu_manipulation_toolbox/resources/win64/server_sm.exe,sha256=Pcx1z-2E7_iHmAzUDEFNTBCf02gfzg5pdYdso5GbP5A,19456
44
+ fmu_manipulation_toolbox-1.8.1.dist-info/LICENSE.txt,sha256=c_862mzyk6ownO3Gt6cVs0-53IXLi_-ZEQFNDVabESw,1285
45
+ fmu_manipulation_toolbox-1.8.1.dist-info/METADATA,sha256=24_m-PSpY9JZqU4LxMHDN3G1IdGydt_8aSXvDU7vm-8,967
46
+ fmu_manipulation_toolbox-1.8.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
47
+ fmu_manipulation_toolbox-1.8.1.dist-info/entry_points.txt,sha256=jCPLMBdS-eOvmRfDv7n0wHZSbJHccHviW03mz5vwO-Q,124
48
+ fmu_manipulation_toolbox-1.8.1.dist-info/top_level.txt,sha256=9D_h-5BMjSqf9z-XFkbJL_bMppR2XNYW3WNuPkXou0k,25
49
+ fmu_manipulation_toolbox-1.8.1.dist-info/RECORD,,