fmu-manipulation-toolbox 1.7.5__tar.gz → 1.8.1__tar.gz

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 (60) hide show
  1. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/PKG-INFO +1 -1
  2. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/README.md +6 -4
  3. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/__version__.py +1 -0
  4. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/assembly.py +484 -0
  5. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/cli.py +27 -17
  6. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/fmu_container.py +41 -189
  7. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/gui.py +58 -45
  8. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  9. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/icon-round.png +0 -0
  10. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/icon.png +0 -0
  11. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  12. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
  13. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  14. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  15. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
  16. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/mask.png +0 -0
  17. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  18. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  19. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  20. fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  21. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  22. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/PKG-INFO +1 -1
  23. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/SOURCES.txt +3 -0
  24. fmu_manipulation_toolbox-1.8.1/tests/test_suite.py +93 -0
  25. fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/__version__.py +0 -1
  26. fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
  27. fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/resources/icon.png +0 -0
  28. fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  29. fmu_manipulation_toolbox-1.7.5/tests/test_suite.py +0 -67
  30. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/LICENSE.txt +0 -0
  31. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/__init__.py +0 -0
  32. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/__main__.py +0 -0
  33. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/checker.py +0 -0
  34. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/fmu_operations.py +0 -0
  35. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/help.py +0 -0
  36. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
  37. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
  38. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
  39. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
  40. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
  41. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
  42. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +0 -0
  43. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +0 -0
  44. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +0 -0
  45. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +0 -0
  46. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +0 -0
  47. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +0 -0
  48. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +0 -0
  49. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmu.png +0 -0
  50. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
  51. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/help.png +0 -0
  52. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/license.txt +0 -0
  53. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/model.png +0 -0
  54. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/version.py +0 -0
  55. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/dependency_links.txt +0 -0
  56. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/entry_points.txt +0 -0
  57. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/requires.txt +0 -0
  58. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/top_level.txt +0 -0
  59. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/setup.cfg +0 -0
  60. {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/setup.py +0 -0
@@ -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
@@ -122,15 +122,16 @@ optional arguments:
122
122
  ### FMU Containers
123
123
 
124
124
  ```
125
- fmucontainer [-h] -fmu-directory FMU_DIRECTORY [-container filename.csv:step_size] [-debug] [-no-auto-input]
126
- [-no-auto-output] [-no-auto-link] [-mt] [-profile]
125
+ usage: fmucontainer [-h] [-fmu-directory FMU_DIRECTORY] -container filename.{csv|json|ssp},[:step_size] [-debug]
126
+ [-no-auto-input] [-no-auto-output] [-no-auto-link] [-mt] [-profile] [-dump-json]
127
127
 
128
128
  Generate FMU from FMU's
129
129
 
130
130
  optional arguments:
131
131
  -h, -help
132
- -fmu-directory FMU_DIRECTORY Directory containing initial FMU’s and used to generate containers. (default: None)
133
- -container filename.csv:step_size
132
+ -fmu-directory FMU_DIRECTORY Directory containing initial FMU’s and used to generate containers. If not defined,
133
+ current directory is used. (default: .)
134
+ -container filename.{csv|json|ssp},[:step_size]
134
135
  Description of the container to create. (default: [])
135
136
  -debug Add lot of useful log during the process. (default: False)
136
137
  -no-auto-input Create ONLY explicit input. (default: True)
@@ -138,6 +139,7 @@ optional arguments:
138
139
  -no-auto-link Create ONLY explicit links. (default: True)
139
140
  -mt Enable Multi-Threaded mode for the generated container. (default: False)
140
141
  -profile Enable Profiling mode for the generated container. (default: False)
142
+ -dump-json Dump a JSON file for each container. (default: False)
141
143
  ```
142
144
 
143
145
  ## API
@@ -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()