fmu-manipulation-toolbox 1.7.5__py3-none-any.whl → 1.8__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.
- fmu_manipulation_toolbox/__version__.py +1 -1
- fmu_manipulation_toolbox/assembly.py +456 -0
- fmu_manipulation_toolbox/cli.py +27 -17
- fmu_manipulation_toolbox/fmu_container.py +34 -177
- fmu_manipulation_toolbox/gui.py +26 -15
- fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
- fmu_manipulation_toolbox/resources/icon-round.png +0 -0
- fmu_manipulation_toolbox/resources/icon.png +0 -0
- fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
- fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
- fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
- fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
- fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
- fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
- fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
- fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
- fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
- fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
- {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/METADATA +1 -1
- {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/RECORD +24 -22
- {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/LICENSE.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/WHEEL +0 -0
- {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/entry_points.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/top_level.txt +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
'V1.
|
|
1
|
+
'V1.8'
|
|
@@ -0,0 +1,456 @@
|
|
|
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, FMUContainerError
|
|
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: str, step_size: float = None, mt=False, profiling=False,
|
|
35
|
+
auto_link=True, auto_input=True, auto_output=True):
|
|
36
|
+
self.name = name
|
|
37
|
+
self.step_size = step_size
|
|
38
|
+
self.mt = mt
|
|
39
|
+
self.profiling = profiling
|
|
40
|
+
self.auto_link = auto_link
|
|
41
|
+
self.auto_input = auto_input
|
|
42
|
+
self.auto_output = auto_output
|
|
43
|
+
self.children: List[AssemblyNode] = []
|
|
44
|
+
|
|
45
|
+
self.fmu_names_list: Set[str] = set()
|
|
46
|
+
self.input_ports: Dict[Port, str] = {}
|
|
47
|
+
self.output_ports: Dict[Port, str] = {}
|
|
48
|
+
self.start_values: Dict[Port, str] = {}
|
|
49
|
+
self.drop_ports: List[Port] = []
|
|
50
|
+
self.links: List[Connection] = []
|
|
51
|
+
|
|
52
|
+
def add_sub_node(self, sub_node):
|
|
53
|
+
if sub_node.name is None:
|
|
54
|
+
sub_node.name = str(uuid.uuid4())+".fmu"
|
|
55
|
+
|
|
56
|
+
self.fmu_names_list.add(sub_node.name)
|
|
57
|
+
self.children.append(sub_node)
|
|
58
|
+
|
|
59
|
+
def add_fmu(self, fmu_name: str):
|
|
60
|
+
self.fmu_names_list.add(fmu_name)
|
|
61
|
+
|
|
62
|
+
def add_input(self, from_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
63
|
+
self.input_ports[Port(to_fmu_filename, to_port_name)] = from_port_name
|
|
64
|
+
|
|
65
|
+
def add_output(self, from_fmu_filename: str, from_port_name: str, to_port_name: str):
|
|
66
|
+
self.output_ports[Port(from_fmu_filename, from_port_name)] = to_port_name
|
|
67
|
+
|
|
68
|
+
def add_drop_port(self, fmu_filename: str, port_name: str):
|
|
69
|
+
self.drop_ports.append(Port(fmu_filename, port_name))
|
|
70
|
+
|
|
71
|
+
def add_link(self, from_fmu_filename: str, from_port_name: str, to_fmu_filename: str, to_port_name: str):
|
|
72
|
+
self.links.append(Connection(Port(from_fmu_filename, from_port_name),
|
|
73
|
+
Port(to_fmu_filename, to_port_name)))
|
|
74
|
+
|
|
75
|
+
def add_start_value(self, fmu_filename: str, port_name: str, value: str):
|
|
76
|
+
self.start_values[Port(fmu_filename, port_name)] = value
|
|
77
|
+
|
|
78
|
+
def make_fmu(self, fmu_directory: Path, debug=False, description_pathname=None):
|
|
79
|
+
for node in self.children:
|
|
80
|
+
node.make_fmu(fmu_directory, debug=debug)
|
|
81
|
+
|
|
82
|
+
container = FMUContainer(self.name, fmu_directory, description_pathname=description_pathname)
|
|
83
|
+
|
|
84
|
+
for fmu_name in sorted(self.fmu_names_list):
|
|
85
|
+
container.get_fmu(fmu_name)
|
|
86
|
+
|
|
87
|
+
for port, source in self.input_ports.items():
|
|
88
|
+
container.add_input(source, port.fmu_name, port.port_name)
|
|
89
|
+
|
|
90
|
+
for port, target in self.output_ports.items():
|
|
91
|
+
container.add_output(port.fmu_name, port.port_name, target)
|
|
92
|
+
|
|
93
|
+
for link in self.links:
|
|
94
|
+
container.add_link(link.from_port.fmu_name, link.from_port.port_name,
|
|
95
|
+
link.to_port.fmu_name, link.to_port.port_name)
|
|
96
|
+
|
|
97
|
+
for drop in self.drop_ports:
|
|
98
|
+
container.drop_port(drop.fmu_name, drop.port_name)
|
|
99
|
+
|
|
100
|
+
for port, value in self.start_values.items():
|
|
101
|
+
container.add_start_value(port.fmu_name, port.port_name, value)
|
|
102
|
+
|
|
103
|
+
container.add_implicit_rule(auto_input=self.auto_input,
|
|
104
|
+
auto_output=self.auto_output,
|
|
105
|
+
auto_link=self.auto_link)
|
|
106
|
+
|
|
107
|
+
container.make_fmu(self.name, self.step_size, mt=self.mt, profiling=self.profiling, debug=debug)
|
|
108
|
+
|
|
109
|
+
for node in self.children:
|
|
110
|
+
logger.info(f"Deleting transient FMU Container '{node.name}'")
|
|
111
|
+
(fmu_directory / node.name).unlink()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AssemblyError(Exception):
|
|
115
|
+
def __init__(self, reason: str):
|
|
116
|
+
self.reason = reason
|
|
117
|
+
|
|
118
|
+
def __repr__(self):
|
|
119
|
+
return f"{self.reason}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Assembly:
|
|
123
|
+
def __init__(self, filename: str, step_size=None, auto_link=True, auto_input=True, debug=False,
|
|
124
|
+
auto_output=True, mt=False, profiling=False, fmu_directory: Path = "."):
|
|
125
|
+
self.filename = Path(filename)
|
|
126
|
+
self.default_auto_input = auto_input
|
|
127
|
+
self.debug = debug
|
|
128
|
+
self.default_auto_output = auto_output
|
|
129
|
+
self.default_step_size = step_size
|
|
130
|
+
self.default_auto_link = auto_link
|
|
131
|
+
self.default_mt = mt
|
|
132
|
+
self.default_profiling = profiling
|
|
133
|
+
self.fmu_directory = fmu_directory
|
|
134
|
+
self.transient_filenames: List[Path] = []
|
|
135
|
+
self.transient_dirnames: Set[Path] = set()
|
|
136
|
+
|
|
137
|
+
if not fmu_directory.is_dir():
|
|
138
|
+
raise FMUContainerError(f"FMU directory is not valid: '{fmu_directory}'")
|
|
139
|
+
|
|
140
|
+
self.description_pathname = fmu_directory / self.filename # For inclusion in FMU
|
|
141
|
+
self.root = None
|
|
142
|
+
self.read()
|
|
143
|
+
|
|
144
|
+
def add_transient_file(self, filename: str):
|
|
145
|
+
self.transient_filenames.append(self.fmu_directory / filename)
|
|
146
|
+
self.transient_dirnames.add(Path(filename).parent)
|
|
147
|
+
|
|
148
|
+
def __del__(self):
|
|
149
|
+
if not self.debug:
|
|
150
|
+
for filename in self.transient_filenames:
|
|
151
|
+
try:
|
|
152
|
+
filename.unlink()
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
pass
|
|
155
|
+
for dirname in self.transient_dirnames:
|
|
156
|
+
while not str(dirname) == ".":
|
|
157
|
+
try:
|
|
158
|
+
(self.fmu_directory / dirname).rmdir()
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
pass
|
|
161
|
+
dirname = dirname.parent
|
|
162
|
+
|
|
163
|
+
def read(self):
|
|
164
|
+
logger.info(f"Reading '{self.filename}'")
|
|
165
|
+
if self.filename.suffix == ".json":
|
|
166
|
+
self.read_json()
|
|
167
|
+
elif self.filename.suffix == ".ssp":
|
|
168
|
+
self.read_ssp()
|
|
169
|
+
elif self.filename.suffix == ".csv":
|
|
170
|
+
self.read_csv()
|
|
171
|
+
else:
|
|
172
|
+
raise FMUContainerError(f"Not supported file format '{self.filename}")
|
|
173
|
+
|
|
174
|
+
def write(self, filename: str):
|
|
175
|
+
if filename.endswith(".csv"):
|
|
176
|
+
return self.write_csv(filename)
|
|
177
|
+
elif filename.endswith(".json"):
|
|
178
|
+
return self.write_json(filename)
|
|
179
|
+
else:
|
|
180
|
+
logger.critical(f"Unable to write to '{filename}': format unsupported.")
|
|
181
|
+
|
|
182
|
+
def read_csv(self):
|
|
183
|
+
name = str(self.filename.with_suffix(".fmu"))
|
|
184
|
+
self.root = AssemblyNode(name, step_size=self.default_step_size, auto_link=self.default_auto_link,
|
|
185
|
+
mt=self.default_mt, profiling=self.default_profiling,
|
|
186
|
+
auto_input=self.default_auto_input, auto_output=self.default_auto_output)
|
|
187
|
+
|
|
188
|
+
with open(self.description_pathname) as file:
|
|
189
|
+
reader = csv.reader(file, delimiter=';')
|
|
190
|
+
self.check_csv_headers(reader)
|
|
191
|
+
for i, row in enumerate(reader):
|
|
192
|
+
if not row or row[0][0] == '#': # skip blank line of comment
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
rule, from_fmu_filename, from_port_name, to_fmu_filename, to_port_name = row
|
|
197
|
+
except ValueError:
|
|
198
|
+
logger.error(f"Line #{i+2}: expecting 5 columns. Line skipped.")
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
rule = rule.upper()
|
|
202
|
+
if rule in ("LINK", "INPUT", "OUTPUT", "DROP", "FMU", "START"):
|
|
203
|
+
try:
|
|
204
|
+
self._read_csv_rule(self.root, rule,
|
|
205
|
+
from_fmu_filename, from_port_name, to_fmu_filename, to_port_name)
|
|
206
|
+
except AssemblyError as e:
|
|
207
|
+
logger.error(f"Line #{i+2}: {e}. Line skipped.")
|
|
208
|
+
continue
|
|
209
|
+
else:
|
|
210
|
+
logger.error(f"Line #{i+2}: unexpected rule '{rule}'. Line skipped.")
|
|
211
|
+
|
|
212
|
+
def write_csv(self, filename: Union[str, Path]):
|
|
213
|
+
if self.root.children:
|
|
214
|
+
raise AssemblyError("This assembly is not flat. Cannot export to CSV file.")
|
|
215
|
+
|
|
216
|
+
with open(self.fmu_directory / filename, "wt") as outfile:
|
|
217
|
+
outfile.write("rule;from_fmu;from_port;to_fmu;to_port\n")
|
|
218
|
+
for fmu in self.root.fmu_names_list:
|
|
219
|
+
outfile.write(f"FMU;{fmu};;;\n")
|
|
220
|
+
for port, source in self.root.input_ports.items():
|
|
221
|
+
outfile.write(f"INPUT;;{source};{port.fmu_name};{port.port_name}\n")
|
|
222
|
+
for port, target in self.root.output_ports.items():
|
|
223
|
+
outfile.write(f"OUTPUT;{port.fmu_name};{port.port_name};;{target}\n")
|
|
224
|
+
for link in self.root.links:
|
|
225
|
+
outfile.write(f"LINK;{link.from_port.fmu_name};{link.from_port.port_name};"
|
|
226
|
+
f"{link.to_port.fmu_name};{link.to_port.port_name}\n")
|
|
227
|
+
for port, value in self.root.start_values.items():
|
|
228
|
+
outfile.write(f"START;{port.fmu_name};{port.port_name};{value};\n")
|
|
229
|
+
for port in self.root.drop_ports:
|
|
230
|
+
outfile.write(f"DROP;{port.fmu_name};{port.port_name};;\n")
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def _read_csv_rule(node: AssemblyNode, rule: str, from_fmu_filename: str, from_port_name: str,
|
|
234
|
+
to_fmu_filename: str, to_port_name: str):
|
|
235
|
+
if rule == "FMU":
|
|
236
|
+
if not from_fmu_filename:
|
|
237
|
+
raise AssemblyError("Missing FMU information.")
|
|
238
|
+
node.add_fmu(from_fmu_filename)
|
|
239
|
+
|
|
240
|
+
elif rule == "INPUT":
|
|
241
|
+
if not to_fmu_filename or not to_port_name:
|
|
242
|
+
raise AssemblyError("Missing INPUT ports information.")
|
|
243
|
+
if not from_port_name:
|
|
244
|
+
from_port_name = to_port_name
|
|
245
|
+
node.add_input(from_port_name, to_fmu_filename, to_port_name)
|
|
246
|
+
|
|
247
|
+
elif rule == "OUTPUT":
|
|
248
|
+
if not from_fmu_filename or not from_port_name:
|
|
249
|
+
raise AssemblyError("Missing OUTPUT ports information.")
|
|
250
|
+
if not to_port_name:
|
|
251
|
+
to_port_name = from_port_name
|
|
252
|
+
node.add_output(from_fmu_filename, from_port_name, to_port_name)
|
|
253
|
+
|
|
254
|
+
elif rule == "DROP":
|
|
255
|
+
if not from_fmu_filename or not from_port_name:
|
|
256
|
+
raise AssemblyError("Missing DROP ports information.")
|
|
257
|
+
node.add_drop_port(from_fmu_filename, from_port_name)
|
|
258
|
+
|
|
259
|
+
elif rule == "LINK":
|
|
260
|
+
node.add_link(from_fmu_filename, from_port_name, to_fmu_filename, to_port_name)
|
|
261
|
+
|
|
262
|
+
elif rule == "START":
|
|
263
|
+
if not from_fmu_filename or not from_port_name or not to_fmu_filename:
|
|
264
|
+
raise AssemblyError("Missing START ports information.")
|
|
265
|
+
|
|
266
|
+
node.add_start_value(from_fmu_filename, from_port_name, to_fmu_filename)
|
|
267
|
+
# no else: check on rule is already done in read_description()
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def check_csv_headers(reader):
|
|
271
|
+
headers = next(reader)
|
|
272
|
+
if not headers == ["rule", "from_fmu", "from_port", "to_fmu", "to_port"]:
|
|
273
|
+
raise AssemblyError("Header (1st line of the file) is not well formatted.")
|
|
274
|
+
|
|
275
|
+
def read_json(self):
|
|
276
|
+
with open(self.description_pathname) as file:
|
|
277
|
+
try:
|
|
278
|
+
data = json.load(file)
|
|
279
|
+
except json.decoder.JSONDecodeError as e:
|
|
280
|
+
raise FMUContainerError(f"Cannot read json: {e}")
|
|
281
|
+
self.root = self.json_decode_node(data)
|
|
282
|
+
if not self.root.name:
|
|
283
|
+
self.root.name = str(self.filename.with_suffix(".fmu"))
|
|
284
|
+
|
|
285
|
+
def write_json(self, filename: Union[str, Path]):
|
|
286
|
+
with open(self.fmu_directory / filename, "wt") as file:
|
|
287
|
+
data = self.json_encode_node(self.root)
|
|
288
|
+
json.dump(data, file, indent=2)
|
|
289
|
+
|
|
290
|
+
def json_encode_node(self, node: AssemblyNode) -> Dict[str, Any]:
|
|
291
|
+
json_node = dict()
|
|
292
|
+
json_node["name"] = node.name
|
|
293
|
+
json_node["mt"] = node.mt
|
|
294
|
+
json_node["profiling"] = node.profiling
|
|
295
|
+
json_node["auto_link"] = node.auto_link
|
|
296
|
+
if node.step_size:
|
|
297
|
+
json_node["step_size"] = node.step_size
|
|
298
|
+
|
|
299
|
+
if node.children:
|
|
300
|
+
json_node["container"] = [self.json_encode_node(child) for child in node.children]
|
|
301
|
+
|
|
302
|
+
if node.fmu_names_list:
|
|
303
|
+
json_node["fmu"] = [f"{fmu_name}" for fmu_name in sorted(node.fmu_names_list)]
|
|
304
|
+
|
|
305
|
+
if node.input_ports:
|
|
306
|
+
json_node["input"] = [[f"{source}", f"{port.fmu_name}", f"{port.port_name}"]
|
|
307
|
+
for port, source in node.input_ports.items()]
|
|
308
|
+
|
|
309
|
+
if node.output_ports:
|
|
310
|
+
json_node["output"] = [[f"{port.fmu_name}", f"{port.port_name}", f"{target}"]
|
|
311
|
+
for port, target in node.output_ports.items()]
|
|
312
|
+
|
|
313
|
+
if node.links:
|
|
314
|
+
json_node["link"] = [[f"{link.from_port.fmu_name}", f"{link.from_port.port_name}",
|
|
315
|
+
f"{link.to_port.fmu_name}", f"{link.to_port.port_name}"]
|
|
316
|
+
for link in node.links]
|
|
317
|
+
|
|
318
|
+
if node.start_values:
|
|
319
|
+
json_node["start"] = [[f"{port.fmu_name}", f"{port.port_name}", value]
|
|
320
|
+
for port, value in node.start_values.items()]
|
|
321
|
+
|
|
322
|
+
if node.drop_ports:
|
|
323
|
+
json_node["drop"] = [[f"{port.fmu_name}", f"{port.port_name}"] for port in node.drop_ports]
|
|
324
|
+
|
|
325
|
+
return json_node
|
|
326
|
+
|
|
327
|
+
def json_decode_node(self, data) -> AssemblyNode:
|
|
328
|
+
name = data.get("name", None)
|
|
329
|
+
step_size = data.get("step_size", self.default_step_size)
|
|
330
|
+
auto_link = data.get("auto_link", self.default_auto_link)
|
|
331
|
+
auto_input = data.get("auto_input", self.default_auto_input)
|
|
332
|
+
auto_output = data.get("auto_output", self.default_auto_output)
|
|
333
|
+
mt = data.get("mt", self.default_mt)
|
|
334
|
+
profiling = data.get("profiling", self.default_profiling)
|
|
335
|
+
|
|
336
|
+
node = AssemblyNode(name, step_size=step_size, auto_link=auto_link, mt=mt, profiling=profiling,
|
|
337
|
+
auto_input=auto_input, auto_output=auto_output)
|
|
338
|
+
|
|
339
|
+
if "container" in data:
|
|
340
|
+
if not isinstance(data["container"], list):
|
|
341
|
+
raise FMUContainerError("JSON: 'container' keyword should define a list.")
|
|
342
|
+
for sub_data in data["container"]:
|
|
343
|
+
node.add_sub_node(self.json_decode_node(sub_data))
|
|
344
|
+
|
|
345
|
+
if "fmu" in data:
|
|
346
|
+
if not isinstance(data["fmu"], list):
|
|
347
|
+
raise FMUContainerError("JSON: 'fmu' keyword should define a list.")
|
|
348
|
+
for fmu in data["fmu"]:
|
|
349
|
+
node.add_fmu(fmu)
|
|
350
|
+
|
|
351
|
+
if "input" in data:
|
|
352
|
+
if not isinstance(data["input"], list):
|
|
353
|
+
raise FMUContainerError("JSON: 'input' keyword should define a list.")
|
|
354
|
+
for line in data["input"]:
|
|
355
|
+
node.add_input(line[1], line[2], line[0])
|
|
356
|
+
|
|
357
|
+
if "output" in data:
|
|
358
|
+
if not isinstance(data["output"], list):
|
|
359
|
+
raise FMUContainerError("JSON: 'output' keyword should define a list.")
|
|
360
|
+
for line in data["output"]:
|
|
361
|
+
node.add_output(line[0], line[1], line[2])
|
|
362
|
+
|
|
363
|
+
if "start" in data:
|
|
364
|
+
if not isinstance(data["start"], list):
|
|
365
|
+
raise FMUContainerError("JSON: 'start' keyword should define a list.")
|
|
366
|
+
for line in data["start"]:
|
|
367
|
+
node.add_start_value(line[0], line[1], line[2])
|
|
368
|
+
|
|
369
|
+
if "drop" in data:
|
|
370
|
+
for line in data["drop"]:
|
|
371
|
+
node.add_drop_port(line[0], line[1])
|
|
372
|
+
|
|
373
|
+
return node
|
|
374
|
+
|
|
375
|
+
def read_ssp(self):
|
|
376
|
+
logger.warning("This feature is ALPHA stage.")
|
|
377
|
+
|
|
378
|
+
with zipfile.ZipFile(self.fmu_directory / self.filename) as zin:
|
|
379
|
+
for file in zin.filelist:
|
|
380
|
+
if file.filename.endswith(".fmu") or file.filename.endswith(".ssd"):
|
|
381
|
+
zin.extract(file, path=self.fmu_directory)
|
|
382
|
+
logger.debug(f"Extracted: {file.filename}")
|
|
383
|
+
self.add_transient_file(file.filename)
|
|
384
|
+
|
|
385
|
+
self.description_pathname = self.fmu_directory / "SystemStructure.ssd"
|
|
386
|
+
if self.description_pathname.is_file():
|
|
387
|
+
sdd = SSDParser(step_size=self.default_step_size, auto_link=self.default_auto_link,
|
|
388
|
+
mt=self.default_mt, profiling=self.default_profiling,
|
|
389
|
+
auto_input=self.default_auto_input, auto_output=self.default_auto_output)
|
|
390
|
+
self.root = sdd.parse(self.description_pathname)
|
|
391
|
+
|
|
392
|
+
def make_fmu(self, dump_json=False):
|
|
393
|
+
if dump_json:
|
|
394
|
+
dump_file = (self.description_pathname.with_stem(self.description_pathname.stem + "-dump")
|
|
395
|
+
.with_suffix(".json"))
|
|
396
|
+
logger.info(f"Dump Json '{dump_file}'")
|
|
397
|
+
self.write_json(dump_file)
|
|
398
|
+
self.root.make_fmu(self.fmu_directory, debug=self.debug, description_pathname=self.description_pathname)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class SSDParser:
|
|
402
|
+
def __init__(self, **kwargs):
|
|
403
|
+
self.node_stack: List[AssemblyNode] = []
|
|
404
|
+
self.root = None
|
|
405
|
+
self.fmu_filenames: Dict[str, str] = {} # Component name => FMU filename
|
|
406
|
+
self.node_attrs = kwargs
|
|
407
|
+
|
|
408
|
+
def parse(self, ssd_filepath: Path) -> AssemblyNode:
|
|
409
|
+
logger.debug(f"Analysing {ssd_filepath}")
|
|
410
|
+
with open(ssd_filepath, "rb") as file:
|
|
411
|
+
parser = xml.parsers.expat.ParserCreate()
|
|
412
|
+
parser.StartElementHandler = self.start_element
|
|
413
|
+
parser.EndElementHandler = self.end_element
|
|
414
|
+
parser.ParseFile(file)
|
|
415
|
+
|
|
416
|
+
return self.root
|
|
417
|
+
|
|
418
|
+
def start_element(self, tag_name, attrs):
|
|
419
|
+
if tag_name == 'ssd:Connection':
|
|
420
|
+
if 'startElement' in attrs:
|
|
421
|
+
if 'endElement' in attrs:
|
|
422
|
+
fmu_start = self.fmu_filenames[attrs['startElement']]
|
|
423
|
+
fmu_end = self.fmu_filenames[attrs['endElement']]
|
|
424
|
+
self.node_stack[-1].add_link(fmu_start, attrs['startConnector'],
|
|
425
|
+
fmu_end, attrs['endConnector'])
|
|
426
|
+
else:
|
|
427
|
+
fmu_start = self.fmu_filenames[attrs['startElement']]
|
|
428
|
+
self.node_stack[-1].add_output(fmu_start, attrs['startConnector'],
|
|
429
|
+
attrs['endConnector'])
|
|
430
|
+
else:
|
|
431
|
+
fmu_end = self.fmu_filenames[attrs['endElement']]
|
|
432
|
+
self.node_stack[-1].add_input(attrs['startConnector'],
|
|
433
|
+
fmu_end, attrs['endConnector'])
|
|
434
|
+
|
|
435
|
+
elif tag_name == 'ssd:System':
|
|
436
|
+
logger.info(f"SSP System: {attrs['name']}")
|
|
437
|
+
filename = attrs['name'] + ".fmu"
|
|
438
|
+
self.fmu_filenames[attrs['name']] = filename
|
|
439
|
+
node = AssemblyNode(filename, **self.node_attrs)
|
|
440
|
+
if self.node_stack:
|
|
441
|
+
self.node_stack[-1].add_sub_node(node)
|
|
442
|
+
else:
|
|
443
|
+
self.root = node
|
|
444
|
+
|
|
445
|
+
self.node_stack.append(node)
|
|
446
|
+
|
|
447
|
+
elif tag_name == 'ssd:Component':
|
|
448
|
+
filename = attrs['source']
|
|
449
|
+
name = attrs['name']
|
|
450
|
+
self.fmu_filenames[name] = filename
|
|
451
|
+
self.node_stack[-1].add_fmu(filename)
|
|
452
|
+
logger.debug(f"Component {name} => {filename}")
|
|
453
|
+
|
|
454
|
+
def end_element(self, tag_name):
|
|
455
|
+
if tag_name == 'ssd:System':
|
|
456
|
+
self.node_stack.pop()
|
fmu_manipulation_toolbox/cli.py
CHANGED
|
@@ -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
|
|
7
|
+
from .fmu_container import FMUContainerError
|
|
8
|
+
from .assembly import Assembly
|
|
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=
|
|
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
|
-
|
|
201
|
+
filename, step_size = description.split(":")
|
|
194
202
|
step_size = float(step_size)
|
|
195
203
|
except ValueError:
|
|
196
204
|
step_size = None
|
|
197
|
-
|
|
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
|
-
|
|
211
|
+
except FileNotFoundError as e:
|
|
212
|
+
logger.fatal(f"Cannot read file: {e}")
|
|
213
|
+
continue
|
|
214
|
+
except FMUContainerError as e:
|
|
215
|
+
logger.fatal(f"{filename}: {e}")
|
|
216
|
+
continue
|
|
200
217
|
|
|
201
218
|
try:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
self.child
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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')
|
|
@@ -342,6 +349,8 @@ class FMUContainer:
|
|
|
342
349
|
def sanity_check(self, step_size: Union[float, None]):
|
|
343
350
|
nb_error = 0
|
|
344
351
|
for fmu in self.execution_order:
|
|
352
|
+
if not fmu.step_size:
|
|
353
|
+
continue
|
|
345
354
|
ts_ratio = step_size / fmu.step_size
|
|
346
355
|
if ts_ratio < 1.0:
|
|
347
356
|
logger.error(f"Container step_size={step_size}s is lower than FMU '{fmu.name}' "
|
|
@@ -412,12 +421,12 @@ class FMUContainer:
|
|
|
412
421
|
description="FMUContainer with {embedded_fmu}"
|
|
413
422
|
author="{author}"
|
|
414
423
|
license="Proprietary"
|
|
415
|
-
copyright="
|
|
424
|
+
copyright="See Embedded FMU's copyrights."
|
|
416
425
|
variableNamingConvention="structured">
|
|
417
426
|
|
|
418
427
|
<CoSimulation
|
|
419
428
|
modelIdentifier="{self.identifier}"
|
|
420
|
-
canHandleVariableCommunicationStepSize="
|
|
429
|
+
canHandleVariableCommunicationStepSize="true"
|
|
421
430
|
canBeInstantiatedOnlyOncePerProcess="{capabilities['canBeInstantiatedOnlyOncePerProcess']}"
|
|
422
431
|
canNotUseMemoryManagementFunctions="true"
|
|
423
432
|
canGetAndSetFMUstate="false"
|
|
@@ -438,7 +447,8 @@ class FMUContainer:
|
|
|
438
447
|
for fmu in self.execution_order:
|
|
439
448
|
vr = vr_table.add_vr("Real")
|
|
440
449
|
name = f"container.{fmu.model_identifier}.rt_ratio"
|
|
441
|
-
print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local"
|
|
450
|
+
print(f'<ScalarVariable valueReference="{vr}" name="{name}" causality="local">'
|
|
451
|
+
f'<Real /></ScalarVariable>', file=xml_file)
|
|
442
452
|
|
|
443
453
|
# Local variable should be first to ensure to attribute them the lowest VR.
|
|
444
454
|
for local in self.locals.values():
|
|
@@ -555,7 +565,7 @@ class FMUContainer:
|
|
|
555
565
|
if profiling and type_name == "Real":
|
|
556
566
|
nb += len(self.execution_order)
|
|
557
567
|
print(nb, file=txt_file)
|
|
558
|
-
for profiling_port,_ in enumerate(self.execution_order):
|
|
568
|
+
for profiling_port, _ in enumerate(self.execution_order):
|
|
559
569
|
print(f"{profiling_port} -2 {profiling_port}", file=txt_file)
|
|
560
570
|
else:
|
|
561
571
|
print(nb, file=txt_file)
|
|
@@ -600,6 +610,7 @@ class FMUContainer:
|
|
|
600
610
|
documentation_directory.mkdir(exist_ok=True)
|
|
601
611
|
|
|
602
612
|
if self.description_pathname:
|
|
613
|
+
logger.debug(f"Copying {self.description_pathname}")
|
|
603
614
|
shutil.copy(self.description_pathname, documentation_directory)
|
|
604
615
|
|
|
605
616
|
shutil.copy(origin / "model.png", base_directory)
|
|
@@ -628,157 +639,3 @@ class FMUContainer:
|
|
|
628
639
|
def make_fmu_cleanup(base_directory: Path):
|
|
629
640
|
logger.debug(f"Delete directory '{base_directory}'")
|
|
630
641
|
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)
|
fmu_manipulation_toolbox/gui.py
CHANGED
|
@@ -209,11 +209,10 @@ class FilterWidget(QPushButton):
|
|
|
209
209
|
|
|
210
210
|
|
|
211
211
|
class FMUManipulationToolboxlMainWindow(QWidget):
|
|
212
|
-
def __init__(self,
|
|
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()
|
|
@@ -289,7 +288,7 @@ class FMUManipulationToolboxlMainWindow(QWidget):
|
|
|
289
288
|
|
|
290
289
|
exit_button = QPushButton('Exit')
|
|
291
290
|
self.layout.addWidget(exit_button, line, 0, 1, 2)
|
|
292
|
-
exit_button.clicked.connect(
|
|
291
|
+
exit_button.clicked.connect(self.close)
|
|
293
292
|
exit_button.setProperty("class", "quit")
|
|
294
293
|
|
|
295
294
|
save_log_button = QPushButton('Save log as')
|
|
@@ -434,7 +433,7 @@ class FMUManipulationToolboxlMainWindow(QWidget):
|
|
|
434
433
|
scroll_bar.setValue(scroll_bar.maximum())
|
|
435
434
|
|
|
436
435
|
|
|
437
|
-
class Application:
|
|
436
|
+
class Application(QApplication):
|
|
438
437
|
"""
|
|
439
438
|
Analyse and modify your FMUs.
|
|
440
439
|
|
|
@@ -442,12 +441,13 @@ Note: modifying the modelDescription.xml can damage your FMU ! Communicating wit
|
|
|
442
441
|
way the FMU is generated, is preferable when possible.
|
|
443
442
|
|
|
444
443
|
"""
|
|
445
|
-
def __init__(self):
|
|
444
|
+
def __init__(self, *args, **kwargs):
|
|
445
|
+
super().__init__(*args, **kwargs)
|
|
446
|
+
|
|
446
447
|
QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), "resources"))
|
|
447
|
-
self.app = QApplication(sys.argv)
|
|
448
448
|
font = QFont("Verdana")
|
|
449
449
|
font.setPointSize(10)
|
|
450
|
-
self.
|
|
450
|
+
self.setFont(font)
|
|
451
451
|
css_dark = """
|
|
452
452
|
QWidget {background: #4b4e51; color: #b5bab9}
|
|
453
453
|
QPushButton { min-height: 30px; padding: 1px 1px 0.2em 0.2em; border: 1px solid #282830; border-radius: 5px;}
|
|
@@ -479,15 +479,26 @@ QMenu::indicator:unchecked:hover {width: 35px; image: url(images:checkbox-unc
|
|
|
479
479
|
QMenu::indicator:unchecked:disabled {width: 35px; image: url(images:checkbox-unchecked-disabled.png); }
|
|
480
480
|
"""
|
|
481
481
|
|
|
482
|
-
self.
|
|
483
|
-
self.window = FMUManipulationToolboxlMainWindow(self.app)
|
|
484
|
-
print(" "*80, f"Version {version}")
|
|
485
|
-
print(self.__doc__)
|
|
486
|
-
sys.exit(self.app.exec())
|
|
482
|
+
self.setStyleSheet(css_dark)
|
|
487
483
|
|
|
488
|
-
|
|
489
|
-
|
|
484
|
+
|
|
485
|
+
if os.name == 'nt':
|
|
486
|
+
self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), 'resources', 'icon-round.png')))
|
|
487
|
+
|
|
488
|
+
# https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105
|
|
489
|
+
import ctypes
|
|
490
|
+
application_id = 'FMU_Manipulation_Toolbox' # arbitrary string
|
|
491
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(application_id)
|
|
492
|
+
else:
|
|
493
|
+
self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), 'resources', 'icon.png')))
|
|
494
|
+
|
|
495
|
+
self.window = FMUManipulationToolboxlMainWindow()
|
|
490
496
|
|
|
491
497
|
|
|
492
498
|
def main():
|
|
493
|
-
Application()
|
|
499
|
+
application = Application(sys.argv)
|
|
500
|
+
|
|
501
|
+
print(" " * 80, f"Version {version}")
|
|
502
|
+
print(application.__doc__)
|
|
503
|
+
|
|
504
|
+
sys.exit(application.exec())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
{fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fmu_manipulation_toolbox
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8
|
|
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=
|
|
3
|
+
fmu_manipulation_toolbox/__version__.py,sha256=wgnOv0C5Ic8_OmpPXk8qES2otG4lRxHybApfWS9AX58,7
|
|
4
|
+
fmu_manipulation_toolbox/assembly.py,sha256=3YGVBUMjTNwzU-GxUCT-uER3DkX_r4V44ekfzwTmT8I,19364
|
|
4
5
|
fmu_manipulation_toolbox/checker.py,sha256=lE2MpH4BAKCDjUvbr06N56u7ao8hWXaJgMKaLvmhFTQ,2272
|
|
5
|
-
fmu_manipulation_toolbox/cli.py,sha256=
|
|
6
|
-
fmu_manipulation_toolbox/fmu_container.py,sha256=
|
|
6
|
+
fmu_manipulation_toolbox/cli.py,sha256=bIfQAGMyTmsx8HQSvu0Ic_UqDRvvm_Op3RCwHpjfqVI,10070
|
|
7
|
+
fmu_manipulation_toolbox/fmu_container.py,sha256=cPJ_f0-KrBpF8Ri9D9RhgGIuT9BiqWpus4nPet4a3PU,26488
|
|
7
8
|
fmu_manipulation_toolbox/fmu_operations.py,sha256=Z3LVOnDvwzoBrqfibZPAn_Osw6MIuGrXtaboGFqp0DA,15836
|
|
8
|
-
fmu_manipulation_toolbox/gui.py,sha256=
|
|
9
|
+
fmu_manipulation_toolbox/gui.py,sha256=4gD6D4f1jQm1UlVC0iV4nuFNOT-y-AuJ13_7LBVm944,20269
|
|
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,11 +15,12 @@ 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=
|
|
18
|
+
fmu_manipulation_toolbox/resources/drop_fmu.png,sha256=aynTWUw5220BXPZIhNco5AkR-ydCAwibWb5gdhR3cCQ,20077
|
|
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=
|
|
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
|
|
23
25
|
fmu_manipulation_toolbox/resources/model.png,sha256=EAf_HnZJe8zYGZygerG1MMt2U-tMMZlifzXPj4_iORA,208788
|
|
24
26
|
fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd,sha256=OGfyJtaJntKypX5KDpuZ-nV1oYLZ6HV16pkpKOmYox4,2731
|
|
@@ -28,19 +30,19 @@ fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd,sha256=hYZGmhv
|
|
|
28
30
|
fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd,sha256=8A0hl2wb1suoxv47xezkmvVTyJM2ZJp5RPQ2xQ_SjlY,3883
|
|
29
31
|
fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd,sha256=pB9Pe-yBMGZN-JQAu6VB_lXS99kz23mwiUSY74ONZd4,5403
|
|
30
32
|
fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd,sha256=Tc9RWKqvUXpB1qsoU64DAHO4Gfu5DotrQJ3Ece7GnTU,4647
|
|
31
|
-
fmu_manipulation_toolbox/resources/linux32/client_sm.so,sha256=
|
|
32
|
-
fmu_manipulation_toolbox/resources/linux32/server_sm,sha256=
|
|
33
|
-
fmu_manipulation_toolbox/resources/linux64/client_sm.so,sha256=
|
|
34
|
-
fmu_manipulation_toolbox/resources/linux64/container.so,sha256=
|
|
35
|
-
fmu_manipulation_toolbox/resources/linux64/server_sm,sha256=
|
|
36
|
-
fmu_manipulation_toolbox/resources/win32/client_sm.dll,sha256=
|
|
37
|
-
fmu_manipulation_toolbox/resources/win32/server_sm.exe,sha256=
|
|
38
|
-
fmu_manipulation_toolbox/resources/win64/client_sm.dll,sha256=
|
|
39
|
-
fmu_manipulation_toolbox/resources/win64/container.dll,sha256=
|
|
40
|
-
fmu_manipulation_toolbox/resources/win64/server_sm.exe,sha256=
|
|
41
|
-
fmu_manipulation_toolbox-1.
|
|
42
|
-
fmu_manipulation_toolbox-1.
|
|
43
|
-
fmu_manipulation_toolbox-1.
|
|
44
|
-
fmu_manipulation_toolbox-1.
|
|
45
|
-
fmu_manipulation_toolbox-1.
|
|
46
|
-
fmu_manipulation_toolbox-1.
|
|
33
|
+
fmu_manipulation_toolbox/resources/linux32/client_sm.so,sha256=xVdY2zy13pa2DcvFiweSNpp7E6DiONqeoBdlcJHrW_k,35940
|
|
34
|
+
fmu_manipulation_toolbox/resources/linux32/server_sm,sha256=1TLGqNPyM5UVOrCfzNqWyF6ClLksY3EiY3CSsrnp6c0,22836
|
|
35
|
+
fmu_manipulation_toolbox/resources/linux64/client_sm.so,sha256=EhY1XHo1YcQn6yqZ7wk5okqtZyp0MrcCsGcudqE-aIM,37000
|
|
36
|
+
fmu_manipulation_toolbox/resources/linux64/container.so,sha256=_uhkJYZa_z1tV0NBATWC8iAGt7lPY11_VA29a_5hXsM,45384
|
|
37
|
+
fmu_manipulation_toolbox/resources/linux64/server_sm,sha256=ulfoPvmaYe9nInYcVEyj7mD9zDzGk56OUoWx1mPKLiE,22768
|
|
38
|
+
fmu_manipulation_toolbox/resources/win32/client_sm.dll,sha256=Xuo3euqlAL1GTEseSN4I3oZgtyazVAtcPIURN1g5MGs,17920
|
|
39
|
+
fmu_manipulation_toolbox/resources/win32/server_sm.exe,sha256=BQVRmQlsRCNaPfvutK2HACHsOhM7eC-X7xYs2pdX2hc,15872
|
|
40
|
+
fmu_manipulation_toolbox/resources/win64/client_sm.dll,sha256=AEijF7jWf5bvI1V73eU4qOH4HF1ji0OZtnicm3dW5P4,22016
|
|
41
|
+
fmu_manipulation_toolbox/resources/win64/container.dll,sha256=6yK5n_2ImVvk3cJJ2Xvmps3OrOUJD_Yf0D_F7oRlK_Y,32768
|
|
42
|
+
fmu_manipulation_toolbox/resources/win64/server_sm.exe,sha256=Idy3JQhIkS3gauQVz5iDBgp9hw8nBSMgNExeoDnhJBA,19456
|
|
43
|
+
fmu_manipulation_toolbox-1.8.dist-info/LICENSE.txt,sha256=c_862mzyk6ownO3Gt6cVs0-53IXLi_-ZEQFNDVabESw,1285
|
|
44
|
+
fmu_manipulation_toolbox-1.8.dist-info/METADATA,sha256=dfoN8Q5lNqwgCThHbYBSuD9UoKl8rhBsts_L8z76vSM,965
|
|
45
|
+
fmu_manipulation_toolbox-1.8.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
46
|
+
fmu_manipulation_toolbox-1.8.dist-info/entry_points.txt,sha256=jCPLMBdS-eOvmRfDv7n0wHZSbJHccHviW03mz5vwO-Q,124
|
|
47
|
+
fmu_manipulation_toolbox-1.8.dist-info/top_level.txt,sha256=9D_h-5BMjSqf9z-XFkbJL_bMppR2XNYW3WNuPkXou0k,25
|
|
48
|
+
fmu_manipulation_toolbox-1.8.dist-info/RECORD,,
|
{fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{fmu_manipulation_toolbox-1.7.5.dist-info → fmu_manipulation_toolbox-1.8.dist-info}/top_level.txt
RENAMED
|
File without changes
|