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.
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/PKG-INFO +1 -1
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/README.md +6 -4
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/__version__.py +1 -0
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/assembly.py +484 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/cli.py +27 -17
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/fmu_container.py +41 -189
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/gui.py +58 -45
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/icon-round.png +0 -0
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/icon.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux32/server_sm +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/linux64/server_sm +0 -0
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/mask.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
- fmu_manipulation_toolbox-1.8.1/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/PKG-INFO +1 -1
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/SOURCES.txt +3 -0
- fmu_manipulation_toolbox-1.8.1/tests/test_suite.py +93 -0
- fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/__version__.py +0 -1
- fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
- fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/resources/icon.png +0 -0
- fmu_manipulation_toolbox-1.7.5/fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
- fmu_manipulation_toolbox-1.7.5/tests/test_suite.py +0 -67
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/LICENSE.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/__init__.py +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/__main__.py +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/checker.py +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/fmu_operations.py +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/help.py +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmu.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/help.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/license.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/resources/model.png +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/version.py +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/dependency_links.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/entry_points.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/requires.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox.egg-info/top_level.txt +0 -0
- {fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/setup.cfg +0 -0
- {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.
|
|
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
|
|
126
|
-
|
|
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.
|
|
133
|
-
|
|
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 @@
|
|
|
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()
|
{fmu_manipulation_toolbox-1.7.5 → fmu_manipulation_toolbox-1.8.1}/fmu_manipulation_toolbox/cli.py
RENAMED
|
@@ -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, 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=
|
|
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, AssemblyError) 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()
|