fmu-manipulation-toolbox 1.8.4.2rc1__py3-none-any.whl → 1.9__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/__init__.py +0 -1
- fmu_manipulation_toolbox/__main__.py +1 -1
- fmu_manipulation_toolbox/__version__.py +1 -1
- fmu_manipulation_toolbox/assembly.py +22 -13
- fmu_manipulation_toolbox/checker.py +16 -11
- fmu_manipulation_toolbox/cli/__init__.py +0 -0
- fmu_manipulation_toolbox/cli/fmucontainer.py +105 -0
- fmu_manipulation_toolbox/cli/fmusplit.py +48 -0
- fmu_manipulation_toolbox/cli/fmutool.py +127 -0
- fmu_manipulation_toolbox/cli/utils.py +36 -0
- fmu_manipulation_toolbox/container.py +1054 -0
- fmu_manipulation_toolbox/gui.py +48 -56
- fmu_manipulation_toolbox/gui_style.py +8 -0
- fmu_manipulation_toolbox/help.py +3 -0
- fmu_manipulation_toolbox/operations.py +577 -0
- fmu_manipulation_toolbox/remoting.py +107 -0
- fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Annotation.xsd +51 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3AttributeGroups.xsd +119 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3BuildDescription.xsd +117 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3InterfaceType.xsd +80 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3LayeredStandardManifest.xsd +93 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3ModelDescription.xsd +131 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Terminal.xsd +87 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3TerminalsAndIcons.xsd +84 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Type.xsd +207 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Unit.xsd +69 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3Variable.xsd +413 -0
- fmu_manipulation_toolbox/resources/fmi-3.0/fmi3VariableDependency.xsd +64 -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/split.py +331 -0
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/METADATA +1 -1
- fmu_manipulation_toolbox-1.9.dist-info/RECORD +71 -0
- fmu_manipulation_toolbox-1.9.dist-info/entry_points.txt +7 -0
- fmu_manipulation_toolbox/cli.py +0 -235
- fmu_manipulation_toolbox/fmu_container.py +0 -753
- fmu_manipulation_toolbox/fmu_operations.py +0 -489
- fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/RECORD +0 -52
- fmu_manipulation_toolbox-1.8.4.2rc1.dist-info/entry_points.txt +0 -3
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/WHEEL +0 -0
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/licenses/LICENSE.txt +0 -0
- {fmu_manipulation_toolbox-1.8.4.2rc1.dist-info → fmu_manipulation_toolbox-1.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import html
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import xml.parsers.expat
|
|
9
|
+
import zipfile
|
|
10
|
+
import hashlib
|
|
11
|
+
from typing import *
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("fmu_manipulation_toolbox")
|
|
14
|
+
|
|
15
|
+
class FMU:
|
|
16
|
+
"""Unpack and Repack facilities for FMU package. Once unpacked, we can process Operation on
|
|
17
|
+
modelDescription.xml file."""
|
|
18
|
+
|
|
19
|
+
FMI2_TYPES = ('Real', 'Integer', 'String', 'Boolean', 'Enumeration')
|
|
20
|
+
FMI3_TYPES = ('Float64', 'Float32',
|
|
21
|
+
'Int8', 'UInt8', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64',
|
|
22
|
+
'String', 'Boolean', 'Enumeration')
|
|
23
|
+
|
|
24
|
+
def __init__(self, fmu_filename):
|
|
25
|
+
self.fmu_filename = fmu_filename
|
|
26
|
+
self.tmp_directory = tempfile.mkdtemp()
|
|
27
|
+
self.fmi_version = None
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with zipfile.ZipFile(self.fmu_filename) as zin:
|
|
31
|
+
zin.extractall(self.tmp_directory)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
raise FMUError(f"'{fmu_filename}' does not exist")
|
|
34
|
+
self.descriptor_filename = os.path.join(self.tmp_directory, "modelDescription.xml")
|
|
35
|
+
if not os.path.isfile(self.descriptor_filename):
|
|
36
|
+
raise FMUError(f"'{fmu_filename}' is not valid: {self.descriptor_filename} not found")
|
|
37
|
+
|
|
38
|
+
def __del__(self):
|
|
39
|
+
shutil.rmtree(self.tmp_directory)
|
|
40
|
+
|
|
41
|
+
def save_descriptor(self, filename):
|
|
42
|
+
shutil.copyfile(os.path.join(self.tmp_directory, "modelDescription.xml"), filename)
|
|
43
|
+
|
|
44
|
+
def repack(self, filename):
|
|
45
|
+
with zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
46
|
+
for root, dirs, files in os.walk(self.tmp_directory):
|
|
47
|
+
for file in files:
|
|
48
|
+
zout.write(os.path.join(root, file),
|
|
49
|
+
os.path.relpath(os.path.join(root, file), self.tmp_directory))
|
|
50
|
+
# TODO: Add check on output file
|
|
51
|
+
|
|
52
|
+
def apply_operation(self, operation, apply_on=None):
|
|
53
|
+
manipulation = Manipulation(operation, self)
|
|
54
|
+
manipulation.manipulate(self.descriptor_filename, apply_on)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FMUPort:
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.fmi_type = None
|
|
60
|
+
self.attrs_list: List[Dict] = []
|
|
61
|
+
self.dimension = None
|
|
62
|
+
|
|
63
|
+
def dict_level(self, nb):
|
|
64
|
+
return " ".join([f'{key}="{value}"' for key, value in self.attrs_list[nb].items()])
|
|
65
|
+
|
|
66
|
+
def write_xml(self, fmi_version: int, file):
|
|
67
|
+
if fmi_version == 2:
|
|
68
|
+
print(f" <ScalarVariable {self.dict_level(0)}>", file=file)
|
|
69
|
+
print(f" <{self.fmi_type} {self.dict_level(1)}/>", file=file)
|
|
70
|
+
print(f" </ScalarVariable>", file=file)
|
|
71
|
+
elif fmi_version == 3:
|
|
72
|
+
print(f" <{self.fmi_type} {self.dict_level(0)}/>", file=file)
|
|
73
|
+
else:
|
|
74
|
+
raise FMUError(f"FMUPort writing: unsupported FMI version {fmi_version}")
|
|
75
|
+
|
|
76
|
+
def __contains__(self, item):
|
|
77
|
+
for attrs in self.attrs_list:
|
|
78
|
+
if item in attrs:
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, item):
|
|
83
|
+
for attrs in self.attrs_list:
|
|
84
|
+
if item in attrs:
|
|
85
|
+
return attrs[item]
|
|
86
|
+
raise KeyError
|
|
87
|
+
|
|
88
|
+
def __setitem__(self, key, value):
|
|
89
|
+
for attrs in self.attrs_list:
|
|
90
|
+
if key in attrs:
|
|
91
|
+
attrs[key] = value
|
|
92
|
+
return
|
|
93
|
+
raise KeyError
|
|
94
|
+
|
|
95
|
+
def get(self, item, default_value):
|
|
96
|
+
try:
|
|
97
|
+
return self[item]
|
|
98
|
+
except KeyError:
|
|
99
|
+
return default_value
|
|
100
|
+
|
|
101
|
+
def push_attrs(self, attrs):
|
|
102
|
+
self.attrs_list.append(attrs)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FMUError(Exception):
|
|
106
|
+
def __init__(self, reason):
|
|
107
|
+
self.reason = reason
|
|
108
|
+
|
|
109
|
+
def __repr__(self):
|
|
110
|
+
return self.reason
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Manipulation:
|
|
114
|
+
"""Parse modelDescription.xml file and create a modified version"""
|
|
115
|
+
TAGS_MODEL_STRUCTURE = ("InitialUnknowns", "Derivatives", "Outputs")
|
|
116
|
+
|
|
117
|
+
def __init__(self, operation, fmu):
|
|
118
|
+
self.output_filename = tempfile.mktemp()
|
|
119
|
+
self.out = None
|
|
120
|
+
self.operation = operation
|
|
121
|
+
self.parser = xml.parsers.expat.ParserCreate()
|
|
122
|
+
self.parser.StartElementHandler = self.start_element
|
|
123
|
+
self.parser.EndElementHandler = self.end_element
|
|
124
|
+
self.parser.CharacterDataHandler = self.char_data
|
|
125
|
+
|
|
126
|
+
# used for filter
|
|
127
|
+
self.skip_until: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
# used to remove empty sections
|
|
130
|
+
self.delayed_tag = None
|
|
131
|
+
self.delayed_tag_open = False
|
|
132
|
+
|
|
133
|
+
self.operation.set_fmu(fmu)
|
|
134
|
+
self.fmu = fmu
|
|
135
|
+
|
|
136
|
+
self.current_port: Optional[FMUPort] = None
|
|
137
|
+
|
|
138
|
+
self.current_port_number: int = 0
|
|
139
|
+
self.port_translation: List[Optional[int]] = []
|
|
140
|
+
self.port_names_list: List[str] = []
|
|
141
|
+
self.port_removed_vr: Set[str] = set()
|
|
142
|
+
self.apply_on = None
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def escape(value):
|
|
146
|
+
if isinstance(value, str):
|
|
147
|
+
return html.escape(html.unescape(value))
|
|
148
|
+
else:
|
|
149
|
+
return value
|
|
150
|
+
|
|
151
|
+
def handle_port(self):
|
|
152
|
+
causality = self.current_port.get('causality', 'local')
|
|
153
|
+
port_name = self.current_port['name']
|
|
154
|
+
vr = self.current_port['valueReference']
|
|
155
|
+
if not self.apply_on or causality in self.apply_on:
|
|
156
|
+
if self.operation.port_attrs(self.current_port):
|
|
157
|
+
self.remove_port(port_name, vr)
|
|
158
|
+
# Exception is raised by remove port !
|
|
159
|
+
else:
|
|
160
|
+
self.keep_port(port_name)
|
|
161
|
+
else: # Keep ScalarVariable as it is.
|
|
162
|
+
self.keep_port(port_name)
|
|
163
|
+
|
|
164
|
+
def start_element(self, name, attrs):
|
|
165
|
+
if self.skip_until:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
if name == 'ScalarVariable': # FMI 2.0 only
|
|
170
|
+
self.current_port = FMUPort()
|
|
171
|
+
self.current_port.push_attrs(attrs)
|
|
172
|
+
elif self.fmu.fmi_version == 2 and name in self.fmu.FMI2_TYPES:
|
|
173
|
+
if self.current_port: # <Enumeration> can be found before port defition. Ignored.
|
|
174
|
+
self.current_port.fmi_type = name
|
|
175
|
+
self.current_port.push_attrs(attrs)
|
|
176
|
+
elif self.fmu.fmi_version == 3 and name in self.fmu.FMI3_TYPES:
|
|
177
|
+
self.current_port = FMUPort()
|
|
178
|
+
self.current_port.fmi_type = name
|
|
179
|
+
self.current_port.push_attrs(attrs)
|
|
180
|
+
elif name == 'CoSimulation':
|
|
181
|
+
self.operation.cosimulation_attrs(attrs)
|
|
182
|
+
elif name == 'DefaultExperiment':
|
|
183
|
+
self.operation.experiment_attrs(attrs)
|
|
184
|
+
elif name == 'fmiModelDescription':
|
|
185
|
+
self.fmu.fmi_version = int(float(attrs["fmiVersion"]))
|
|
186
|
+
self.operation.fmi_attrs(attrs)
|
|
187
|
+
elif name == 'Unknown': # FMI-2.0 only
|
|
188
|
+
self.unknown_attrs(attrs)
|
|
189
|
+
elif name == 'Output' or name == "ContinuousStateDerivative" or "InitialUnknown": # FMI-3.0 only
|
|
190
|
+
self.handle_structure(attrs)
|
|
191
|
+
|
|
192
|
+
except ManipulationSkipTag:
|
|
193
|
+
self.skip_until = name
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if self.current_port is None:
|
|
197
|
+
if self.delayed_tag and not self.delayed_tag_open:
|
|
198
|
+
print(f"<{self.delayed_tag}>", end='', file=self.out)
|
|
199
|
+
self.delayed_tag_open = True
|
|
200
|
+
|
|
201
|
+
if attrs:
|
|
202
|
+
attrs_list = [f'{key}="{self.escape(value)}"' for (key, value) in attrs.items()]
|
|
203
|
+
print(f"<{name}", " ".join(attrs_list), ">", end='', file=self.out)
|
|
204
|
+
else:
|
|
205
|
+
if name in self.TAGS_MODEL_STRUCTURE:
|
|
206
|
+
self.delayed_tag = name
|
|
207
|
+
self.delayed_tag_open = False
|
|
208
|
+
else:
|
|
209
|
+
print(f"<{name}>", end='', file=self.out)
|
|
210
|
+
|
|
211
|
+
def end_element(self, name):
|
|
212
|
+
if self.skip_until:
|
|
213
|
+
if self.skip_until == name:
|
|
214
|
+
self.skip_until = None
|
|
215
|
+
return
|
|
216
|
+
else:
|
|
217
|
+
if name == "ScalarVariable" or (self.fmu.fmi_version == 3 and name in FMU.FMI3_TYPES):
|
|
218
|
+
try:
|
|
219
|
+
self.handle_port()
|
|
220
|
+
self.current_port.write_xml(self.fmu.fmi_version, self.out)
|
|
221
|
+
except ManipulationSkipTag:
|
|
222
|
+
logger.info(f"Port '{self.current_port['name']}' is removed.")
|
|
223
|
+
self.current_port = None
|
|
224
|
+
|
|
225
|
+
elif self.current_port is None:
|
|
226
|
+
if self.delayed_tag and name == self.delayed_tag:
|
|
227
|
+
if self.delayed_tag_open:
|
|
228
|
+
print(f"</{self.delayed_tag}>", end='', file=self.out)
|
|
229
|
+
else:
|
|
230
|
+
logger.debug(f"Remove tag <{self.delayed_tag}> from modelDescription.xml")
|
|
231
|
+
self.delayed_tag = None
|
|
232
|
+
else:
|
|
233
|
+
print(f"</{name}>", end='', file=self.out)
|
|
234
|
+
|
|
235
|
+
def char_data(self, data):
|
|
236
|
+
if not self.skip_until:
|
|
237
|
+
print(data, end='', file=self.out)
|
|
238
|
+
|
|
239
|
+
def remove_port(self, name, vr):
|
|
240
|
+
self.port_names_list.append(name)
|
|
241
|
+
self.port_translation.append(None)
|
|
242
|
+
self.port_removed_vr.add(vr)
|
|
243
|
+
raise ManipulationSkipTag
|
|
244
|
+
|
|
245
|
+
def keep_port(self, name):
|
|
246
|
+
self.port_names_list.append(name)
|
|
247
|
+
self.current_port_number += 1
|
|
248
|
+
self.port_translation.append(self.current_port_number)
|
|
249
|
+
|
|
250
|
+
def unknown_attrs(self, attrs):
|
|
251
|
+
index = int(attrs['index'])
|
|
252
|
+
new_index = self.port_translation[index-1]
|
|
253
|
+
if new_index is not None:
|
|
254
|
+
attrs['index'] = str(new_index)
|
|
255
|
+
if attrs.get('dependencies', ""):
|
|
256
|
+
if 'dependenciesKind' in attrs:
|
|
257
|
+
new_dependencies = []
|
|
258
|
+
new_kinds = []
|
|
259
|
+
for dependency, kind in zip(attrs['dependencies'].split(' '), attrs['dependenciesKind'].split(' ')):
|
|
260
|
+
new_dependency = self.port_translation[int(dependency)-1]
|
|
261
|
+
if new_dependency is not None:
|
|
262
|
+
new_dependencies.append(str(new_dependency))
|
|
263
|
+
new_kinds.append(kind)
|
|
264
|
+
if new_dependencies:
|
|
265
|
+
attrs['dependencies'] = " ".join(new_dependencies)
|
|
266
|
+
attrs['dependenciesKind'] = " ".join(new_kinds)
|
|
267
|
+
else:
|
|
268
|
+
attrs.pop('dependencies')
|
|
269
|
+
attrs.pop('dependenciesKind')
|
|
270
|
+
else:
|
|
271
|
+
new_dependencies = []
|
|
272
|
+
for dependency in attrs['dependencies'].split(' '):
|
|
273
|
+
new_dependency = self.port_translation[int(dependency)-1]
|
|
274
|
+
if new_dependency is not None:
|
|
275
|
+
new_dependencies.append(str(new_dependency))
|
|
276
|
+
if new_dependencies:
|
|
277
|
+
attrs['dependencies'] = " ".join(new_dependencies)
|
|
278
|
+
else:
|
|
279
|
+
attrs.pop('dependencies')
|
|
280
|
+
else:
|
|
281
|
+
logger.warning(f"Removed port '{self.port_names_list[index-1]}' is involved in dependencies tree.")
|
|
282
|
+
raise ManipulationSkipTag
|
|
283
|
+
|
|
284
|
+
def handle_structure(self, attrs):
|
|
285
|
+
try:
|
|
286
|
+
vr = attrs['valueReference']
|
|
287
|
+
if vr in self.port_removed_vr:
|
|
288
|
+
logger.warning(f"Removed port vr={vr} is involved in dependencies tree.")
|
|
289
|
+
raise ManipulationSkipTag
|
|
290
|
+
except KeyError:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
if attrs.get('dependencies', ""):
|
|
294
|
+
if 'dependenciesKind' in attrs:
|
|
295
|
+
new_dependencies = []
|
|
296
|
+
new_kinds = []
|
|
297
|
+
for dependency, kind in zip(attrs['dependencies'].split(' '), attrs['dependenciesKind'].split(' ')):
|
|
298
|
+
if dependency not in self.port_removed_vr:
|
|
299
|
+
new_dependencies.append(dependency)
|
|
300
|
+
new_kinds.append(kind)
|
|
301
|
+
if new_dependencies:
|
|
302
|
+
attrs['dependencies'] = " ".join(new_dependencies)
|
|
303
|
+
attrs['dependenciesKind'] = " ".join(new_kinds)
|
|
304
|
+
else:
|
|
305
|
+
attrs.pop('dependencies')
|
|
306
|
+
attrs.pop('dependenciesKind')
|
|
307
|
+
else:
|
|
308
|
+
new_dependencies = []
|
|
309
|
+
for dependency in attrs['dependencies'].split(' '):
|
|
310
|
+
if dependency not in self.port_removed_vr:
|
|
311
|
+
new_dependencies.append(dependency)
|
|
312
|
+
if new_dependencies:
|
|
313
|
+
attrs['dependencies'] = " ".join(new_dependencies)
|
|
314
|
+
else:
|
|
315
|
+
attrs.pop('dependencies')
|
|
316
|
+
|
|
317
|
+
def manipulate(self, descriptor_filename, apply_on=None):
|
|
318
|
+
self.apply_on = apply_on
|
|
319
|
+
with open(self.output_filename, "w", encoding="utf-8") as self.out, open(descriptor_filename, "rb") as file:
|
|
320
|
+
self.parser.ParseFile(file)
|
|
321
|
+
self.operation.closure()
|
|
322
|
+
os.replace(self.output_filename, descriptor_filename)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class ManipulationSkipTag(Exception):
|
|
326
|
+
"""Exception: We need to skip every thing until matching closing tag"""
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class OperationAbstract:
|
|
330
|
+
"""This class hold hooks called during parsing"""
|
|
331
|
+
fmu: FMU = None
|
|
332
|
+
|
|
333
|
+
def set_fmu(self, fmu):
|
|
334
|
+
self.fmu = fmu
|
|
335
|
+
|
|
336
|
+
def fmi_attrs(self, attrs):
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
def cosimulation_attrs(self, attrs):
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
def experiment_attrs(self, attrs):
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
def port_attrs(self, fmu_port: FMUPort) -> int:
|
|
346
|
+
""" return 0 to keep port, otherwise remove it"""
|
|
347
|
+
return 0
|
|
348
|
+
|
|
349
|
+
def closure(self):
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class OperationSaveNamesToCSV(OperationAbstract):
|
|
354
|
+
def __repr__(self):
|
|
355
|
+
return f"Dump names into '{self.output_filename}'"
|
|
356
|
+
|
|
357
|
+
def __init__(self, filename):
|
|
358
|
+
self.output_filename = filename
|
|
359
|
+
self.csvfile = open(filename, 'w', newline='')
|
|
360
|
+
self.writer = csv.writer(self.csvfile, delimiter=';', quotechar="'", quoting=csv.QUOTE_MINIMAL)
|
|
361
|
+
self.writer.writerow(['name', 'newName', 'valueReference', 'causality', 'variability', 'scalarType',
|
|
362
|
+
'startValue'])
|
|
363
|
+
|
|
364
|
+
def closure(self):
|
|
365
|
+
self.csvfile.close()
|
|
366
|
+
|
|
367
|
+
def port_attrs(self, fmu_port: FMUPort) -> int:
|
|
368
|
+
self.writer.writerow([fmu_port["name"],
|
|
369
|
+
fmu_port["name"],
|
|
370
|
+
fmu_port["valueReference"],
|
|
371
|
+
fmu_port.get("causality", "local"),
|
|
372
|
+
fmu_port.get("variability", "continuous"),
|
|
373
|
+
fmu_port.fmi_type,
|
|
374
|
+
fmu_port.get("start", "")])
|
|
375
|
+
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class OperationStripTopLevel(OperationAbstract):
|
|
380
|
+
def __repr__(self):
|
|
381
|
+
return "Remove Top Level Bus"
|
|
382
|
+
|
|
383
|
+
def port_attrs(self, fmu_port):
|
|
384
|
+
new_name = fmu_port['name'].split('.', 1)[-1]
|
|
385
|
+
fmu_port['name'] = new_name
|
|
386
|
+
return 0
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class OperationMergeTopLevel(OperationAbstract):
|
|
390
|
+
def __repr__(self):
|
|
391
|
+
return "Merge Top Level Bus with signal names"
|
|
392
|
+
|
|
393
|
+
def port_attrs(self, fmu_port):
|
|
394
|
+
old = fmu_port['name']
|
|
395
|
+
fmu_port['name'] = old.replace('.', '_', 1)
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class OperationRenameFromCSV(OperationAbstract):
|
|
400
|
+
def __repr__(self):
|
|
401
|
+
return f"Rename according to '{self.csv_filename}'"
|
|
402
|
+
|
|
403
|
+
def __init__(self, csv_filename):
|
|
404
|
+
self.csv_filename = csv_filename
|
|
405
|
+
self.translations = {}
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
with open(csv_filename, newline='') as csvfile:
|
|
409
|
+
reader = csv.reader(csvfile, delimiter=';', quotechar="'")
|
|
410
|
+
for row in reader:
|
|
411
|
+
self.translations[row[0]] = row[1]
|
|
412
|
+
except FileNotFoundError:
|
|
413
|
+
raise OperationError(f"file '{csv_filename}' is not found")
|
|
414
|
+
except KeyError:
|
|
415
|
+
raise OperationError(f"file '{csv_filename}' should contain two columns")
|
|
416
|
+
|
|
417
|
+
def port_attrs(self, fmu_port):
|
|
418
|
+
name = fmu_port['name']
|
|
419
|
+
try:
|
|
420
|
+
new_name = self.translations[fmu_port['name']]
|
|
421
|
+
except KeyError:
|
|
422
|
+
new_name = name # if port is not in CSV file, keep old name
|
|
423
|
+
|
|
424
|
+
if new_name:
|
|
425
|
+
fmu_port['name'] = new_name
|
|
426
|
+
return 0
|
|
427
|
+
else:
|
|
428
|
+
# we want to delete this name!
|
|
429
|
+
return 1
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class OperationRemoveRegexp(OperationAbstract):
|
|
433
|
+
def __repr__(self):
|
|
434
|
+
return f"Remove ports matching '{self.regex_string}'"
|
|
435
|
+
|
|
436
|
+
def __init__(self, regex_string):
|
|
437
|
+
self.regex_string = regex_string
|
|
438
|
+
self.regex = re.compile(regex_string)
|
|
439
|
+
self.current_port_number = 0
|
|
440
|
+
self.port_translation = []
|
|
441
|
+
|
|
442
|
+
def port_attrs(self, fmu_port):
|
|
443
|
+
name = fmu_port['name']
|
|
444
|
+
if self.regex.match(name):
|
|
445
|
+
return 1 # Remove port
|
|
446
|
+
else:
|
|
447
|
+
return 0
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class OperationKeepOnlyRegexp(OperationAbstract):
|
|
451
|
+
def __repr__(self):
|
|
452
|
+
return f"Keep only ports matching '{self.regex_string}'"
|
|
453
|
+
|
|
454
|
+
def __init__(self, regex_string):
|
|
455
|
+
self.regex_string = regex_string
|
|
456
|
+
self.regex = re.compile(regex_string)
|
|
457
|
+
|
|
458
|
+
def port_attrs(self, fmu_port):
|
|
459
|
+
name = fmu_port['name']
|
|
460
|
+
if self.regex.match(name):
|
|
461
|
+
return 0
|
|
462
|
+
else:
|
|
463
|
+
return 1 # Remove port
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class OperationSummary(OperationAbstract):
|
|
467
|
+
def __init__(self):
|
|
468
|
+
self.nb_port_per_causality = {}
|
|
469
|
+
|
|
470
|
+
def __repr__(self):
|
|
471
|
+
return f"FMU Summary"
|
|
472
|
+
|
|
473
|
+
def fmi_attrs(self, attrs):
|
|
474
|
+
logger.info(f"| fmu filename = {self.fmu.fmu_filename}")
|
|
475
|
+
logger.info(f"| temporary directory = {self.fmu.tmp_directory}")
|
|
476
|
+
hash_md5 = hashlib.md5()
|
|
477
|
+
with open(self.fmu.fmu_filename, "rb") as f:
|
|
478
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
479
|
+
hash_md5.update(chunk)
|
|
480
|
+
digest = hash_md5.hexdigest()
|
|
481
|
+
logger.info(f"| MD5Sum = {digest}")
|
|
482
|
+
logger.info(f"|")
|
|
483
|
+
logger.info(f"| FMI properties: ")
|
|
484
|
+
for (k, v) in attrs.items():
|
|
485
|
+
logger.info(f"| - {k} = {v}")
|
|
486
|
+
logger.info(f"|")
|
|
487
|
+
|
|
488
|
+
def cosimulation_attrs(self, attrs):
|
|
489
|
+
logger.info("| Co-Simulation capabilities: ")
|
|
490
|
+
for (k, v) in attrs.items():
|
|
491
|
+
logger.info(f"| - {k} = {v}")
|
|
492
|
+
logger.info(f"|")
|
|
493
|
+
|
|
494
|
+
def experiment_attrs(self, attrs):
|
|
495
|
+
logger.info("| Default Experiment values: ")
|
|
496
|
+
for (k, v) in attrs.items():
|
|
497
|
+
logger.info(f"| - {k} = {v}")
|
|
498
|
+
logger.info(f"|")
|
|
499
|
+
|
|
500
|
+
def port_attrs(self, fmu_port) -> int:
|
|
501
|
+
causality = fmu_port.get("causality", "local")
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
self.nb_port_per_causality[causality] += 1
|
|
505
|
+
except KeyError:
|
|
506
|
+
self.nb_port_per_causality[causality] = 1
|
|
507
|
+
|
|
508
|
+
return 0
|
|
509
|
+
|
|
510
|
+
def closure(self):
|
|
511
|
+
logger.info("| Supported platforms: ")
|
|
512
|
+
try:
|
|
513
|
+
for platform in os.listdir(os.path.join(self.fmu.tmp_directory, "binaries")):
|
|
514
|
+
logger.info(f"| - {platform}")
|
|
515
|
+
except FileNotFoundError:
|
|
516
|
+
pass # no binaries
|
|
517
|
+
|
|
518
|
+
if os.path.isdir(os.path.join(self.fmu.tmp_directory, "sources")):
|
|
519
|
+
logger.info(f"| - RT (sources available)")
|
|
520
|
+
|
|
521
|
+
resource_dir = os.path.join(self.fmu.tmp_directory, "resources")
|
|
522
|
+
if os.path.isdir(resource_dir):
|
|
523
|
+
logger.info("|")
|
|
524
|
+
logger.info("| Embedded resources:")
|
|
525
|
+
for resource in os.listdir(resource_dir):
|
|
526
|
+
logger.info(f"| - {resource}")
|
|
527
|
+
|
|
528
|
+
extra_dir = os.path.join(self.fmu.tmp_directory, "extra")
|
|
529
|
+
if os.path.isdir(extra_dir):
|
|
530
|
+
logger.info("|")
|
|
531
|
+
logger.info("| Additional (meta-)data:")
|
|
532
|
+
for extra in os.listdir(extra_dir):
|
|
533
|
+
logger.info(f"| - {extra}")
|
|
534
|
+
|
|
535
|
+
logger.info("|")
|
|
536
|
+
logger.info("| Number of ports")
|
|
537
|
+
for causality, nb_ports in self.nb_port_per_causality.items():
|
|
538
|
+
logger.info(f"| {causality} : {nb_ports}")
|
|
539
|
+
|
|
540
|
+
logger.info("|")
|
|
541
|
+
logger.info("| [End of report]")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class OperationRemoveSources(OperationAbstract):
|
|
545
|
+
def __repr__(self):
|
|
546
|
+
return f"Remove sources"
|
|
547
|
+
|
|
548
|
+
def cosimulation_attrs(self, attrs):
|
|
549
|
+
try:
|
|
550
|
+
shutil.rmtree(os.path.join(self.fmu.tmp_directory, "sources"))
|
|
551
|
+
except FileNotFoundError:
|
|
552
|
+
logger.info("This FMU does not embed sources.")
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class OperationTrimUntil(OperationAbstract):
|
|
556
|
+
def __init__(self, separator):
|
|
557
|
+
self.separator = separator
|
|
558
|
+
|
|
559
|
+
def __repr__(self):
|
|
560
|
+
return f"Trim names until (and including) '{self.separator}'"
|
|
561
|
+
|
|
562
|
+
def port_attrs(self, fmu_port) -> int:
|
|
563
|
+
name = fmu_port['name']
|
|
564
|
+
try:
|
|
565
|
+
fmu_port['name'] = name[name.index(self.separator)+len(self.separator):-1]
|
|
566
|
+
except KeyError:
|
|
567
|
+
pass # no separator
|
|
568
|
+
|
|
569
|
+
return 0
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class OperationError(Exception):
|
|
573
|
+
def __init__(self, reason):
|
|
574
|
+
self.reason = reason
|
|
575
|
+
|
|
576
|
+
def __repr__(self):
|
|
577
|
+
return self.reason
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .operations import OperationAbstract, OperationError
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("fmu_manipulation_toolbox")
|
|
8
|
+
|
|
9
|
+
class OperationAddRemotingWinAbstract(OperationAbstract):
|
|
10
|
+
bitness_from = None
|
|
11
|
+
bitness_to = None
|
|
12
|
+
|
|
13
|
+
def __repr__(self):
|
|
14
|
+
return f"Add '{self.bitness_to}' remoting on '{self.bitness_from}' FMU"
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.vr = {
|
|
18
|
+
"Real": [],
|
|
19
|
+
"Integer": [],
|
|
20
|
+
"Boolean": []
|
|
21
|
+
}
|
|
22
|
+
self.nb_input = 0
|
|
23
|
+
self.nb_output = 0
|
|
24
|
+
|
|
25
|
+
def fmi_attrs(self, attrs):
|
|
26
|
+
if not attrs["fmiVersion"] == "2.0":
|
|
27
|
+
raise OperationError(f"Adding remoting is only available for FMI-2.0")
|
|
28
|
+
|
|
29
|
+
def cosimulation_attrs(self, attrs):
|
|
30
|
+
fmu_bin = {
|
|
31
|
+
"win32": Path(self.fmu.tmp_directory) / "binaries" / "win32",
|
|
32
|
+
"win64": Path(self.fmu.tmp_directory) / "binaries" / "win64",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if not fmu_bin[self.bitness_from].is_dir():
|
|
36
|
+
raise OperationError(f"{self.bitness_from} interface does not exist")
|
|
37
|
+
|
|
38
|
+
if fmu_bin[self.bitness_to].is_dir():
|
|
39
|
+
logger.info(f"{self.bitness_to} already exists. Add front-end.")
|
|
40
|
+
shutil.move(fmu_bin[self.bitness_to] / Path(attrs['modelIdentifier']).with_suffix(".dll"),
|
|
41
|
+
fmu_bin[self.bitness_to] / Path(attrs['modelIdentifier']).with_suffix("-remoted.dll"))
|
|
42
|
+
else:
|
|
43
|
+
fmu_bin[self.bitness_to].mkdir()
|
|
44
|
+
|
|
45
|
+
to_path = Path(__file__).parent / "resources" / self.bitness_to
|
|
46
|
+
try:
|
|
47
|
+
shutil.copyfile(to_path / "client_sm.dll",
|
|
48
|
+
fmu_bin[self.bitness_to] / Path(attrs['modelIdentifier']).with_suffix(".dll"))
|
|
49
|
+
except FileNotFoundError as e:
|
|
50
|
+
logger.critical(f"Cannot add remoting client: {e}")
|
|
51
|
+
|
|
52
|
+
from_path = Path(__file__).parent / "resources" / self.bitness_from
|
|
53
|
+
try:
|
|
54
|
+
shutil.copyfile(from_path / "server_sm.exe",
|
|
55
|
+
fmu_bin[self.bitness_from] / "server_sm.exe")
|
|
56
|
+
except FileNotFoundError as e:
|
|
57
|
+
logger.critical(f"Cannot add remoting server: {e}")
|
|
58
|
+
|
|
59
|
+
shutil.copyfile(Path(__file__).parent / "resources" / "license.txt",
|
|
60
|
+
fmu_bin[self.bitness_to] / "license.txt")
|
|
61
|
+
|
|
62
|
+
def port_attrs(self, fmu_port) -> int:
|
|
63
|
+
vr = int(fmu_port["valueReference"])
|
|
64
|
+
causality = fmu_port.get("causality", "local")
|
|
65
|
+
try:
|
|
66
|
+
self.vr[fmu_port.fmi_type].append(vr)
|
|
67
|
+
if causality in ("input", "parameter"):
|
|
68
|
+
self.nb_input += 1
|
|
69
|
+
else:
|
|
70
|
+
self.nb_output += 1
|
|
71
|
+
except KeyError:
|
|
72
|
+
logger.error(f"Type '{fmu_port.fmi_type}' is not supported by remoting.")
|
|
73
|
+
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
def closure(self):
|
|
77
|
+
target_dir = Path(self.fmu.tmp_directory) / "resources"
|
|
78
|
+
if not target_dir.is_dir():
|
|
79
|
+
target_dir.mkdir()
|
|
80
|
+
|
|
81
|
+
logger.info(f"Remoting nb input port: {self.nb_input}")
|
|
82
|
+
logger.info(f"Remoting nb output port: {self.nb_output}")
|
|
83
|
+
with open(target_dir/ "remoting_table.txt", "wt") as file:
|
|
84
|
+
for fmi_type in ('Real', 'Integer', 'Boolean'):
|
|
85
|
+
print(len(self.vr[fmi_type]), file=file)
|
|
86
|
+
for fmi_type in ('Real', 'Integer', 'Boolean'):
|
|
87
|
+
for vr in sorted(self.vr[fmi_type]):
|
|
88
|
+
print(vr, file=file)
|
|
89
|
+
|
|
90
|
+
class OperationAddRemotingWin64(OperationAddRemotingWinAbstract):
|
|
91
|
+
bitness_from = "win32"
|
|
92
|
+
bitness_to = "win64"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class OperationAddFrontendWin32(OperationAddRemotingWinAbstract):
|
|
96
|
+
bitness_from = "win32"
|
|
97
|
+
bitness_to = "win32"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OperationAddFrontendWin64(OperationAddRemotingWinAbstract):
|
|
101
|
+
bitness_from = "win64"
|
|
102
|
+
bitness_to = "win64"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class OperationAddRemotingWin32(OperationAddRemotingWinAbstract):
|
|
106
|
+
bitness_from = "win64"
|
|
107
|
+
bitness_to = "win32"
|
|
Binary file
|