fmu-manipulation-toolbox 1.7.5__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 +1 -0
- fmu_manipulation_toolbox/__main__.py +25 -0
- fmu_manipulation_toolbox/__version__.py +1 -0
- fmu_manipulation_toolbox/checker.py +61 -0
- fmu_manipulation_toolbox/cli.py +216 -0
- fmu_manipulation_toolbox/fmu_container.py +784 -0
- fmu_manipulation_toolbox/fmu_operations.py +489 -0
- fmu_manipulation_toolbox/gui.py +493 -0
- fmu_manipulation_toolbox/help.py +87 -0
- fmu_manipulation_toolbox/resources/checkbox-checked-disabled.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-checked-hover.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-checked.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-unchecked-disabled.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-unchecked-hover.png +0 -0
- fmu_manipulation_toolbox/resources/checkbox-unchecked.png +0 -0
- fmu_manipulation_toolbox/resources/drop_fmu.png +0 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Annotation.xsd +58 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2AttributeGroups.xsd +78 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ModelDescription.xsd +345 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2ScalarVariable.xsd +218 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Type.xsd +89 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2Unit.xsd +116 -0
- fmu_manipulation_toolbox/resources/fmi-2.0/fmi2VariableDependency.xsd +92 -0
- fmu_manipulation_toolbox/resources/fmu.png +0 -0
- fmu_manipulation_toolbox/resources/fmu_manipulation_toolbox.png +0 -0
- fmu_manipulation_toolbox/resources/help.png +0 -0
- fmu_manipulation_toolbox/resources/icon.png +0 -0
- fmu_manipulation_toolbox/resources/license.txt +34 -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/model.png +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/version.py +9 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/LICENSE.txt +22 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/METADATA +20 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/RECORD +46 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/WHEEL +5 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/entry_points.txt +3 -0
- fmu_manipulation_toolbox-1.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import sys
|
|
3
|
+
from .version import __version__ as version
|
|
4
|
+
from PyQt5.QtCore import Qt, QObject, QUrl, pyqtSignal, QDir
|
|
5
|
+
from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog,
|
|
6
|
+
QTextBrowser, QInputDialog, QMenu, QAction)
|
|
7
|
+
from PyQt5.QtGui import QPixmap, QImage, QFont, QTextCursor, QIcon, QColor, QPainter, QBrush, QDesktopServices
|
|
8
|
+
import textwrap
|
|
9
|
+
from functools import partial
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .fmu_operations import *
|
|
13
|
+
from .checker import checker_list
|
|
14
|
+
from .help import Help
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DropZoneWidget(QLabel):
|
|
18
|
+
WIDTH = 150
|
|
19
|
+
HEIGHT = 150
|
|
20
|
+
fmu = None
|
|
21
|
+
last_directory = None
|
|
22
|
+
clicked = pyqtSignal()
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.setAcceptDrops(True)
|
|
27
|
+
self.set_image(None)
|
|
28
|
+
self.setProperty("class", "dropped_fmu")
|
|
29
|
+
self.setFixedSize(self.WIDTH, self.HEIGHT)
|
|
30
|
+
|
|
31
|
+
def dragEnterEvent(self, event):
|
|
32
|
+
if event.mimeData().hasImage:
|
|
33
|
+
event.accept()
|
|
34
|
+
else:
|
|
35
|
+
event.ignore()
|
|
36
|
+
|
|
37
|
+
def dragMoveEvent(self, event):
|
|
38
|
+
if event.mimeData().hasImage:
|
|
39
|
+
event.accept()
|
|
40
|
+
else:
|
|
41
|
+
event.ignore()
|
|
42
|
+
|
|
43
|
+
def dropEvent(self, event):
|
|
44
|
+
if event.mimeData().hasImage:
|
|
45
|
+
event.setDropAction(Qt.CopyAction)
|
|
46
|
+
try:
|
|
47
|
+
file_path = event.mimeData().urls()[0].toLocalFile()
|
|
48
|
+
except IndexError:
|
|
49
|
+
print("Please select a regular file.")
|
|
50
|
+
return
|
|
51
|
+
self.set_fmu(file_path)
|
|
52
|
+
event.accept()
|
|
53
|
+
else:
|
|
54
|
+
event.ignore()
|
|
55
|
+
|
|
56
|
+
def mousePressEvent(self, event):
|
|
57
|
+
if self.last_directory:
|
|
58
|
+
default_directory = self.last_directory
|
|
59
|
+
else:
|
|
60
|
+
default_directory = os.path.expanduser('~')
|
|
61
|
+
|
|
62
|
+
fmu_filename, _ = QFileDialog.getOpenFileName(self, 'Select FMU to Manipulate',
|
|
63
|
+
default_directory, "FMU files (*.fmu)")
|
|
64
|
+
if fmu_filename:
|
|
65
|
+
self.set_fmu(fmu_filename)
|
|
66
|
+
|
|
67
|
+
def set_image(self, filename=None):
|
|
68
|
+
if not filename:
|
|
69
|
+
filename = os.path.join(os.path.dirname(__file__), "resources", "drop_fmu.png")
|
|
70
|
+
elif not os.path.isfile(filename):
|
|
71
|
+
filename = os.path.join(os.path.dirname(__file__), "resources", "fmu.png")
|
|
72
|
+
|
|
73
|
+
image = QImage(filename).scaled(self.WIDTH-4, self.HEIGHT-4, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
74
|
+
pixmap = QPixmap.fromImage(image)
|
|
75
|
+
rounded = self.make_pixmap_rounded(pixmap)
|
|
76
|
+
self.setPixmap(rounded)
|
|
77
|
+
|
|
78
|
+
def make_pixmap_rounded(self, pixmap):
|
|
79
|
+
rounded = QPixmap(pixmap.size())
|
|
80
|
+
rounded.fill(QColor("transparent"))
|
|
81
|
+
|
|
82
|
+
painter = QPainter(rounded)
|
|
83
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
84
|
+
painter.setBrush(QBrush(pixmap))
|
|
85
|
+
painter.setPen(Qt.NoPen)
|
|
86
|
+
painter.drawRoundedRect(pixmap.rect(), 20, 20)
|
|
87
|
+
del painter # Mandatory to avoid a crash
|
|
88
|
+
self.update() # Mandatory to avoid a crash
|
|
89
|
+
return rounded
|
|
90
|
+
|
|
91
|
+
def set_fmu(self, filename):
|
|
92
|
+
try:
|
|
93
|
+
self.last_directory = os.path.dirname(filename)
|
|
94
|
+
self.fmu = FMU(filename)
|
|
95
|
+
self.set_image(os.path.join(self.fmu.tmp_directory, "model.png"))
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"ERROR: Cannot load this FMU: {e}")
|
|
98
|
+
self.set_image(None)
|
|
99
|
+
self.fmu = None
|
|
100
|
+
self.clicked.emit()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LogWidget(QTextBrowser):
|
|
104
|
+
class XStream(QObject):
|
|
105
|
+
_stdout = None
|
|
106
|
+
_stderr = None
|
|
107
|
+
messageWritten = pyqtSignal(str)
|
|
108
|
+
|
|
109
|
+
def flush(self):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def fileno():
|
|
114
|
+
return -1
|
|
115
|
+
|
|
116
|
+
def write(self, msg):
|
|
117
|
+
if not self.signalsBlocked():
|
|
118
|
+
self.messageWritten.emit(msg)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def stdout():
|
|
122
|
+
if not LogWidget.XStream._stdout:
|
|
123
|
+
LogWidget.XStream._stdout = LogWidget.XStream()
|
|
124
|
+
sys.stdout = LogWidget.XStream._stdout
|
|
125
|
+
return LogWidget.XStream._stdout
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def stderr():
|
|
129
|
+
if not LogWidget.XStream._stderr:
|
|
130
|
+
LogWidget.XStream._stderr = LogWidget.XStream()
|
|
131
|
+
sys.stderr = LogWidget.XStream._stderr
|
|
132
|
+
return LogWidget.XStream._stderr
|
|
133
|
+
|
|
134
|
+
def __init__(self):
|
|
135
|
+
super().__init__()
|
|
136
|
+
if os.name == 'nt':
|
|
137
|
+
font = QFont('Consolas')
|
|
138
|
+
font.setPointSize(10)
|
|
139
|
+
else:
|
|
140
|
+
font = QFont('Courier New')
|
|
141
|
+
font.setPointSize(12)
|
|
142
|
+
self.setFont(font)
|
|
143
|
+
self.setMinimumWidth(800)
|
|
144
|
+
self.setMinimumHeight(480)
|
|
145
|
+
|
|
146
|
+
self.insertHtml('<center><img src="fmu_manipulation_toolbox.png"/></center><br>')
|
|
147
|
+
LogWidget.XStream.stdout().messageWritten.connect(self.insertPlainText)
|
|
148
|
+
LogWidget.XStream.stderr().messageWritten.connect(self.insertPlainText)
|
|
149
|
+
|
|
150
|
+
def loadResource(self, type, name):
|
|
151
|
+
image_path = os.path.join(os.path.dirname(__file__), "resources", name.toString())
|
|
152
|
+
return QPixmap(image_path)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class HelpWidget(QLabel):
|
|
156
|
+
HELP_URL = "https://github.com/grouperenault/fmu_manipulation_toolbox/blob/main/README.md"
|
|
157
|
+
|
|
158
|
+
def __init__(self):
|
|
159
|
+
super().__init__()
|
|
160
|
+
self.setProperty("class", "help")
|
|
161
|
+
|
|
162
|
+
filename = os.path.join(os.path.dirname(__file__), "resources", "help.png")
|
|
163
|
+
image = QPixmap(filename)
|
|
164
|
+
self.setPixmap(image)
|
|
165
|
+
self.setAlignment(Qt.AlignRight)
|
|
166
|
+
|
|
167
|
+
def mousePressEvent(self, event):
|
|
168
|
+
QDesktopServices.openUrl(QUrl(self.HELP_URL))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class FilterWidget(QPushButton):
|
|
172
|
+
def __init__(self, items: Optional[list[str]] = (), parent=None):
|
|
173
|
+
super().__init__(parent)
|
|
174
|
+
self.items_selected = set(items)
|
|
175
|
+
self.nb_items = len(items)
|
|
176
|
+
self.update_filter_text()
|
|
177
|
+
if items:
|
|
178
|
+
menu = QMenu()
|
|
179
|
+
for item in items:
|
|
180
|
+
action = QAction(item, self)
|
|
181
|
+
action.setCheckable(True)
|
|
182
|
+
action.setChecked(True)
|
|
183
|
+
action.triggered.connect(partial(self.toggle_item, action))
|
|
184
|
+
menu.addAction(action)
|
|
185
|
+
self.setMenu(menu)
|
|
186
|
+
|
|
187
|
+
def toggle_item(self, action: QAction):
|
|
188
|
+
if not action.isChecked() and len(self.items_selected) == 1:
|
|
189
|
+
action.setChecked(True)
|
|
190
|
+
|
|
191
|
+
if action.isChecked():
|
|
192
|
+
self.items_selected.add(action.text())
|
|
193
|
+
else:
|
|
194
|
+
self.items_selected.remove(action.text())
|
|
195
|
+
|
|
196
|
+
self.update_filter_text()
|
|
197
|
+
|
|
198
|
+
def update_filter_text(self):
|
|
199
|
+
if len(self.items_selected) == self.nb_items:
|
|
200
|
+
self.setText("All causalities")
|
|
201
|
+
else:
|
|
202
|
+
self.setText(", ".join(sorted(self.items_selected)))
|
|
203
|
+
|
|
204
|
+
def get(self):
|
|
205
|
+
if len(self.items_selected) == self.nb_items:
|
|
206
|
+
return []
|
|
207
|
+
else:
|
|
208
|
+
return sorted(self.items_selected)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class FMUManipulationToolboxlMainWindow(QWidget):
|
|
212
|
+
def __init__(self, app, *args, **kwargs):
|
|
213
|
+
super().__init__(*args, **kwargs)
|
|
214
|
+
|
|
215
|
+
self.setWindowTitle('FMU Manipulation Toolbox')
|
|
216
|
+
self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), 'resources', 'icon.png')))
|
|
217
|
+
|
|
218
|
+
# set the grid layout
|
|
219
|
+
self.layout = QGridLayout()
|
|
220
|
+
self.setLayout(self.layout)
|
|
221
|
+
|
|
222
|
+
self.dropped_fmu = DropZoneWidget()
|
|
223
|
+
self.dropped_fmu.clicked.connect(self.update_fmu)
|
|
224
|
+
self.layout.addWidget(self.dropped_fmu, 0, 0, 4, 1)
|
|
225
|
+
|
|
226
|
+
font = QFont('Verdana')
|
|
227
|
+
font.setPointSize(14)
|
|
228
|
+
font.setBold(True)
|
|
229
|
+
self.fmu_title = QLabel()
|
|
230
|
+
self.fmu_title.setFont(font)
|
|
231
|
+
self.layout.addWidget(self.fmu_title, 0, 1, 1, 4)
|
|
232
|
+
|
|
233
|
+
help_widget = HelpWidget()
|
|
234
|
+
self.layout.addWidget(help_widget, 0, 5, 1, 1)
|
|
235
|
+
|
|
236
|
+
# Operations
|
|
237
|
+
self.help = Help()
|
|
238
|
+
operations_list = [
|
|
239
|
+
("Save port names", '-dump-csv', 'save', OperationSaveNamesToCSV, {"prompt_file": "write"}),
|
|
240
|
+
("Rename ports from CSV", '-rename-from-csv', 'modify', OperationRenameFromCSV, {"prompt_file": "read"}),
|
|
241
|
+
("Remove Toplevel", '-remove-toplevel', 'modify', OperationStripTopLevel),
|
|
242
|
+
("Remove Regexp", '-remove-regexp', 'removal', OperationRemoveRegexp, {"prompt": "regexp"}),
|
|
243
|
+
("Keep only Regexp", '-keep-only-regexp', 'removal', OperationKeepOnlyRegexp, {"prompt": "regexp"}),
|
|
244
|
+
("Save description.xml", '-extract-descriptor', 'save', None, {"func": self.save_descriptor}),
|
|
245
|
+
("Trim Until", '-trim-until', 'modify', OperationTrimUntil, {"prompt": "Prefix"}),
|
|
246
|
+
("Merge Toplevel", '-merge-toplevel', 'modify', OperationMergeTopLevel),
|
|
247
|
+
("Remove all", '-remove-all', 'removal', OperationRemoveRegexp, {"arg": ".*"}),
|
|
248
|
+
("Remove sources", '-remove-sources', 'removal', OperationRemoveSources),
|
|
249
|
+
("Add Win32 remoting", '-add-remoting-win32', 'info', OperationAddRemotingWin32),
|
|
250
|
+
("Add Win64 remoting", '-add-remoting-win64', 'info', OperationAddRemotingWin64),
|
|
251
|
+
("Add Win32 frontend", '-add-frontend-win32', 'info', OperationAddFrontendWin32),
|
|
252
|
+
("Add Win64 frontend", '-add-frontend-win64', 'info', OperationAddFrontendWin64),
|
|
253
|
+
("Check", '-check', 'info', checker_list),
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
width = 5
|
|
257
|
+
line = 1
|
|
258
|
+
for i, operation in enumerate(operations_list):
|
|
259
|
+
col = i % width + 1
|
|
260
|
+
line = int(i / width) + 1
|
|
261
|
+
|
|
262
|
+
if len(operation) < 5:
|
|
263
|
+
self.add_operation(operation[0], operation[1], operation[2], operation[3], line, col)
|
|
264
|
+
else:
|
|
265
|
+
self.add_operation(operation[0], operation[1], operation[2], operation[3], line, col, **operation[4])
|
|
266
|
+
|
|
267
|
+
line += 1
|
|
268
|
+
self.apply_filter_label = QLabel("Apply modification only on: ")
|
|
269
|
+
self.layout.addWidget(self.apply_filter_label, line, 1, 1, 2, alignment=Qt.AlignRight)
|
|
270
|
+
self.set_tooltip(self.apply_filter_label, 'gui-apply-only')
|
|
271
|
+
|
|
272
|
+
causality = ["parameter", "calculatedParameter", "input", "output", "local", "independent"]
|
|
273
|
+
self.filter_list = FilterWidget(items=causality)
|
|
274
|
+
self.layout.addWidget(self.filter_list, line, 3, 1, 3)
|
|
275
|
+
self.filter_list.setProperty("class", "quit")
|
|
276
|
+
|
|
277
|
+
# Text
|
|
278
|
+
line += 1
|
|
279
|
+
self.log_widget = LogWidget()
|
|
280
|
+
self.layout.addWidget(self.log_widget, line, 0, 1, width + 1)
|
|
281
|
+
|
|
282
|
+
# buttons
|
|
283
|
+
line += 1
|
|
284
|
+
|
|
285
|
+
reload_button = QPushButton('Reload')
|
|
286
|
+
self.layout.addWidget(reload_button, 4, 0, 1, 1)
|
|
287
|
+
reload_button.clicked.connect(self.reload_fmu)
|
|
288
|
+
reload_button.setProperty("class", "quit")
|
|
289
|
+
|
|
290
|
+
exit_button = QPushButton('Exit')
|
|
291
|
+
self.layout.addWidget(exit_button, line, 0, 1, 2)
|
|
292
|
+
exit_button.clicked.connect(app.exit)
|
|
293
|
+
exit_button.setProperty("class", "quit")
|
|
294
|
+
|
|
295
|
+
save_log_button = QPushButton('Save log as')
|
|
296
|
+
self.layout.addWidget(save_log_button, line, 2, 1, 2)
|
|
297
|
+
save_log_button.clicked.connect(self.save_log)
|
|
298
|
+
save_log_button.setProperty("class", "save")
|
|
299
|
+
|
|
300
|
+
save_fmu_button = QPushButton('Save modified FMU as')
|
|
301
|
+
self.layout.addWidget(save_fmu_button, line, 4, 1, 2)
|
|
302
|
+
save_fmu_button.clicked.connect(self.save_fmu)
|
|
303
|
+
save_fmu_button.setProperty("class", "save")
|
|
304
|
+
self.set_tooltip(save_fmu_button, '-output')
|
|
305
|
+
|
|
306
|
+
# show the window
|
|
307
|
+
self.show()
|
|
308
|
+
|
|
309
|
+
def set_tooltip(self, widget, usage):
|
|
310
|
+
widget.setToolTip("\n".join(textwrap.wrap(self.help.usage(usage))))
|
|
311
|
+
|
|
312
|
+
def reload_fmu(self):
|
|
313
|
+
if self.dropped_fmu.fmu:
|
|
314
|
+
filename = self.dropped_fmu.fmu.fmu_filename
|
|
315
|
+
self.dropped_fmu.fmu = None
|
|
316
|
+
self.dropped_fmu.set_fmu(filename)
|
|
317
|
+
|
|
318
|
+
def save_descriptor(self):
|
|
319
|
+
if self.dropped_fmu.fmu:
|
|
320
|
+
fmu = self.dropped_fmu.fmu
|
|
321
|
+
filename, ok = QFileDialog.getSaveFileName(self, "Select a file",
|
|
322
|
+
os.path.dirname(fmu.fmu_filename),
|
|
323
|
+
"XML files (*.xml)")
|
|
324
|
+
if ok and filename:
|
|
325
|
+
fmu.save_descriptor(filename)
|
|
326
|
+
|
|
327
|
+
def save_fmu(self):
|
|
328
|
+
if self.dropped_fmu.fmu:
|
|
329
|
+
fmu = self.dropped_fmu.fmu
|
|
330
|
+
filename, ok = QFileDialog.getSaveFileName(self, "Select a file",
|
|
331
|
+
os.path.dirname(fmu.fmu_filename),
|
|
332
|
+
"FMU files (*.fmu)")
|
|
333
|
+
if ok and filename:
|
|
334
|
+
fmu.repack(filename)
|
|
335
|
+
print(f"Modified version saved as {filename}.")
|
|
336
|
+
|
|
337
|
+
def save_log(self):
|
|
338
|
+
if self.dropped_fmu.fmu:
|
|
339
|
+
default_dir = os.path.dirname(self.dropped_fmu.fmu.fmu_filename)
|
|
340
|
+
else:
|
|
341
|
+
default_dir = None
|
|
342
|
+
filename, ok = QFileDialog.getSaveFileName(self, "Select a file",
|
|
343
|
+
default_dir,
|
|
344
|
+
"TXT files (*.txt)")
|
|
345
|
+
if ok and filename:
|
|
346
|
+
try:
|
|
347
|
+
with open(filename, "wt") as file:
|
|
348
|
+
file.write(str(self.log_widget.toPlainText()))
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f"ERROR: {e}")
|
|
351
|
+
|
|
352
|
+
def add_operation(self, name, usage, severity, operation, x, y, prompt=None, prompt_file=None, arg=None,
|
|
353
|
+
func=None):
|
|
354
|
+
if prompt:
|
|
355
|
+
def operation_handler():
|
|
356
|
+
local_arg = self.prompt_string(prompt)
|
|
357
|
+
if local_arg:
|
|
358
|
+
self.apply_operation(operation(local_arg))
|
|
359
|
+
elif prompt_file:
|
|
360
|
+
def operation_handler():
|
|
361
|
+
local_arg = self.prompt_file(prompt_file)
|
|
362
|
+
if local_arg:
|
|
363
|
+
self.apply_operation(operation(local_arg))
|
|
364
|
+
elif arg:
|
|
365
|
+
def operation_handler():
|
|
366
|
+
self.apply_operation(operation(arg))
|
|
367
|
+
else:
|
|
368
|
+
def operation_handler():
|
|
369
|
+
# Checker can be a list of operations!
|
|
370
|
+
if isinstance(operation, list):
|
|
371
|
+
for op in operation:
|
|
372
|
+
self.apply_operation(op())
|
|
373
|
+
else:
|
|
374
|
+
self.apply_operation(operation())
|
|
375
|
+
|
|
376
|
+
button = QPushButton(name)
|
|
377
|
+
self.set_tooltip(button, usage)
|
|
378
|
+
button.setProperty("class", severity)
|
|
379
|
+
if func:
|
|
380
|
+
button.clicked.connect(func)
|
|
381
|
+
else:
|
|
382
|
+
button.clicked.connect(operation_handler)
|
|
383
|
+
self.layout.addWidget(button, x, y)
|
|
384
|
+
|
|
385
|
+
def prompt_string(self, message):
|
|
386
|
+
text, ok = QInputDialog().getText(self, "Enter value", f"{message}:", QLineEdit.Normal, "")
|
|
387
|
+
|
|
388
|
+
if ok and text:
|
|
389
|
+
return text
|
|
390
|
+
else:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
def prompt_file(self, access):
|
|
394
|
+
if self.dropped_fmu.fmu:
|
|
395
|
+
default_dir = os.path.dirname(self.dropped_fmu.fmu.fmu_filename)
|
|
396
|
+
|
|
397
|
+
if access == 'read':
|
|
398
|
+
filename, ok = QFileDialog.getOpenFileName(self, "Select a file",
|
|
399
|
+
default_dir, "CSV files (*.csv)")
|
|
400
|
+
else:
|
|
401
|
+
filename, ok = QFileDialog.getSaveFileName(self, "Select a file",
|
|
402
|
+
default_dir, "CSV files (*.csv)")
|
|
403
|
+
|
|
404
|
+
if ok and filename:
|
|
405
|
+
return filename
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
def update_fmu(self):
|
|
409
|
+
if self.dropped_fmu.fmu:
|
|
410
|
+
self.fmu_title.setText(os.path.basename(self.dropped_fmu.fmu.fmu_filename))
|
|
411
|
+
self.log_widget.clear()
|
|
412
|
+
self.apply_operation(OperationSummary())
|
|
413
|
+
else:
|
|
414
|
+
self.fmu_title.setText('')
|
|
415
|
+
|
|
416
|
+
def apply_operation(self, operation):
|
|
417
|
+
if self.dropped_fmu.fmu:
|
|
418
|
+
self.log_widget.moveCursor(QTextCursor.End)
|
|
419
|
+
fmu_filename = os.path.basename(self.dropped_fmu.fmu.fmu_filename)
|
|
420
|
+
print('-' * 100)
|
|
421
|
+
self.log_widget.insertHtml(f"<strong>{fmu_filename}: {operation}</strong><br>")
|
|
422
|
+
|
|
423
|
+
apply_on = self.filter_list.get()
|
|
424
|
+
if apply_on:
|
|
425
|
+
self.log_widget.insertHtml(f"<i>Applied only for ports with causality = " +
|
|
426
|
+
", ".join(apply_on) + "</i><br>")
|
|
427
|
+
print('-' * 100)
|
|
428
|
+
try:
|
|
429
|
+
self.dropped_fmu.fmu.apply_operation(operation, apply_on=apply_on)
|
|
430
|
+
except Exception as e:
|
|
431
|
+
print(f"ERROR: {e}")
|
|
432
|
+
|
|
433
|
+
scroll_bar = self.log_widget.verticalScrollBar()
|
|
434
|
+
scroll_bar.setValue(scroll_bar.maximum())
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class Application:
|
|
438
|
+
"""
|
|
439
|
+
Analyse and modify your FMUs.
|
|
440
|
+
|
|
441
|
+
Note: modifying the modelDescription.xml can damage your FMU ! Communicating with the FMU-developer and adapting the
|
|
442
|
+
way the FMU is generated, is preferable when possible.
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
def __init__(self):
|
|
446
|
+
QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), "resources"))
|
|
447
|
+
self.app = QApplication(sys.argv)
|
|
448
|
+
font = QFont("Verdana")
|
|
449
|
+
font.setPointSize(10)
|
|
450
|
+
self.app.setFont(font)
|
|
451
|
+
css_dark = """
|
|
452
|
+
QWidget {background: #4b4e51; color: #b5bab9}
|
|
453
|
+
QPushButton { min-height: 30px; padding: 1px 1px 0.2em 0.2em; border: 1px solid #282830; border-radius: 5px;}
|
|
454
|
+
QComboBox { min-height: 30px; padding: 1px 1px 0.2em 0.2em; border: 1px solid #282830; border-radius: 5px;}
|
|
455
|
+
QPushButton:pressed { border: 2px solid #282830; }
|
|
456
|
+
QPushButton.info {background-color: #4e6749; color: #dddddd;}
|
|
457
|
+
QPushButton.info:hover {background-color: #5f7850; color: #dddddd;}
|
|
458
|
+
QPushButton.info:hover {background-color: #5f7850; color: #dddddd;}
|
|
459
|
+
QPushButton.modify {background-color: #98763f; color: #dddddd;}
|
|
460
|
+
QPushButton.modify:hover {background-color: #a9874f; color: #dddddd;}
|
|
461
|
+
QPushButton.removal {background-color: #692e2e; color: #dddddd;}
|
|
462
|
+
QPushButton.removal:hover{background-color: #7a3f3f; color: #dddddd;}
|
|
463
|
+
QPushButton.save {background-color: #564967; color: #dddddd;}
|
|
464
|
+
QPushButton.save:hover {background-color: #675a78; color: #dddddd;}
|
|
465
|
+
QPushButton.quit {background-color: #4571a4; color: #dddddd;}
|
|
466
|
+
QPushButton.quit:hover {background-color: #5682b5; color: #dddddd;}
|
|
467
|
+
QToolTip {color: black}
|
|
468
|
+
QLabel.dropped_fmu {background-color: #b5bab9; border: 2px dashed #282830; border-radius: dashed 20px;}
|
|
469
|
+
QLabel.dropped_fmu:hover {background-color: #c6cbca; border: 2px dashed #282830; border-radius: dashed 20px;}
|
|
470
|
+
QTextBrowser {background-color: #282830; color: #b5bab9;}
|
|
471
|
+
QMenu {font-size: 18px;}
|
|
472
|
+
QMenu::item {padding: 2px 250px 2px 20px; border: 1px solid transparent;}
|
|
473
|
+
QMenu::item::indicator {width: 32px; height: 32px; }
|
|
474
|
+
QMenu::indicator:checked {image: url(images:checkbox-checked.png);}
|
|
475
|
+
QMenu::indicator:checked:hover {image: url(images:checkbox-checked-hover.png);}
|
|
476
|
+
QMenu::indicator:checked:disabled {image: url(images:checkbox-checked-disabled.png);}
|
|
477
|
+
QMenu::indicator:unchecked {IconWidth: 50px; image: url(images:checkbox-unchecked.png); }
|
|
478
|
+
QMenu::indicator:unchecked:hover {width: 35px; image: url(images:checkbox-unchecked-hover.png); }
|
|
479
|
+
QMenu::indicator:unchecked:disabled {width: 35px; image: url(images:checkbox-unchecked-disabled.png); }
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
self.app.setStyleSheet(css_dark)
|
|
483
|
+
self.window = FMUManipulationToolboxlMainWindow(self.app)
|
|
484
|
+
print(" "*80, f"Version {version}")
|
|
485
|
+
print(self.__doc__)
|
|
486
|
+
sys.exit(self.app.exec())
|
|
487
|
+
|
|
488
|
+
def exit(self):
|
|
489
|
+
self.app.exit()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def main():
|
|
493
|
+
Application()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class Help:
|
|
2
|
+
_usage = {
|
|
3
|
+
'-h': "display help.",
|
|
4
|
+
'-input': "this option is mandatory to specify the filename of the FMU to be loaded.",
|
|
5
|
+
|
|
6
|
+
'-output': "this option is used to specify the filename of the FMU to be created after manipulations."
|
|
7
|
+
" If it is not provided, no new fmu will be saved and some manipulations can be lost.",
|
|
8
|
+
|
|
9
|
+
'-remove-toplevel': "rename the ports of the input fmu by striping all characters until the first '.' "
|
|
10
|
+
"(toplevel bus). If no '.' is present, the port won't be renamed. Resulting fmu should be "
|
|
11
|
+
"saved by using -output option. Note: before version 1.2.6, this option was spelled "
|
|
12
|
+
"-remove-toplel.",
|
|
13
|
+
|
|
14
|
+
'-merge-toplevel': "replace first '.' by an '_' on every port name.",
|
|
15
|
+
|
|
16
|
+
'-trim-until': "remove a prefix from port name. Example '-trim-until _' : will rename port names of the"
|
|
17
|
+
" FMU by removing part of the name until the first '_'. Prefix can be longer than a "
|
|
18
|
+
"single character. ",
|
|
19
|
+
|
|
20
|
+
'-remove-regexp': "remove ports that match the regular-expression. Other ports will be kept. Resulting "
|
|
21
|
+
"fmu should be saved by using -output option. This option is available from version 1.1. "
|
|
22
|
+
"See https://en.wikipedia.org/wiki/Regular_expression to have more detail of expected "
|
|
23
|
+
"format.",
|
|
24
|
+
|
|
25
|
+
'-keep-only-regexp': "keep only ports that match the regular-expression. Other ports will be removed. "
|
|
26
|
+
"Resulting fmu should be saved by using -output option. This option is available from "
|
|
27
|
+
"version 1.1. See https://en.wikipedia.org/wiki/Regular_expression to have more detail "
|
|
28
|
+
"of expected format.",
|
|
29
|
+
|
|
30
|
+
'-remove-all': "equivalent to '-remove-regexp .*'. Typical use case is to use it with -only-* options. "
|
|
31
|
+
"Example: in order ro suppress all parameters of FMU: -only-parameters -remove-all",
|
|
32
|
+
|
|
33
|
+
'-dump-csv': "list all names of the ports of the input fmu and store them inside path/to/list.csv. "
|
|
34
|
+
"This file is ';' separated. It contains two columns in order to be easily reused by "
|
|
35
|
+
"-rename-from-csv option.",
|
|
36
|
+
|
|
37
|
+
'-rename-from-csv': "rename the ports of fmu accordingly to path/to/translation.csv. This file is ';' "
|
|
38
|
+
"separated. It contains two columns. First column contains original names. Second column "
|
|
39
|
+
"contains new names. * If a port is not found in the file, it won't be renamed. This is "
|
|
40
|
+
"working with version > 1.2.6. It is safer to keep ALL port in csv. * If the new name is"
|
|
41
|
+
" empty, the port will be removed. This is working starting version 1.1. * If a name in "
|
|
42
|
+
"the file is not present in input FMU, it will be ignored. (no warning will be issued). "
|
|
43
|
+
"Resulting fmu should be saved by using -output option.",
|
|
44
|
+
|
|
45
|
+
'-add-remoting-win32': "this option is windows specific. It will add 'win32' interface to a 'win64' fmu."
|
|
46
|
+
" Please upgrade to version 1.2.1 before using this option. Resulting fmu should be"
|
|
47
|
+
" saved by using -output option.",
|
|
48
|
+
|
|
49
|
+
'-add-remoting-win64': "this option is windows specific. It will add 'win64' interface to a 'win32' fmu."
|
|
50
|
+
" Please upgrade to version 1.2.1 before using this option. Resulting fmu should be"
|
|
51
|
+
" saved by using -output option.",
|
|
52
|
+
|
|
53
|
+
'-add-frontend-win32': "this option is windows specific. It can be used with 'win32' fmu. At simulation time, "
|
|
54
|
+
"the FMU will spawn a dedicated process tu run the model. This option is available from "
|
|
55
|
+
"version 1.4. Resulting fmu should be saved by using -output option.",
|
|
56
|
+
|
|
57
|
+
'-add-frontend-win64': "this option is windows specific. It can be used with 'win64' fmu. At simulation time, "
|
|
58
|
+
"the FMU will spawn a dedicated process tu run the model. This option is available from "
|
|
59
|
+
"version 1.4. Resulting fmu should be saved by using -output option.",
|
|
60
|
+
|
|
61
|
+
'-extract-descriptor': "save the modelDescription.xml into the specified location. If modification options "
|
|
62
|
+
"(like -rename-from-csv or -remove-toplevel are set), the saved file will contain "
|
|
63
|
+
"modification. This option is available from version 1.1.",
|
|
64
|
+
|
|
65
|
+
'-remove-sources': "Remove sources folder from the FMU. This option is available from version 1.3.",
|
|
66
|
+
|
|
67
|
+
'-only-parameters': "apply operation only on ports with causality = 'parameter'. This "
|
|
68
|
+
"option is available from version 1.3.",
|
|
69
|
+
|
|
70
|
+
'-only-inputs': "apply operation only on ports with causality = 'parameter'. This "
|
|
71
|
+
"option is available from version 1.3.",
|
|
72
|
+
|
|
73
|
+
'-only-outputs': "apply operation only on ports with causality = 'output'. This "
|
|
74
|
+
"option is available from version 1.3.",
|
|
75
|
+
|
|
76
|
+
'-summary': "display useful information regarding the FMU.",
|
|
77
|
+
|
|
78
|
+
'-check': "performs some check of FMU and display Errors or Warnings. This is useful to avoid later "
|
|
79
|
+
"issues when using the FMU.",
|
|
80
|
+
|
|
81
|
+
# GUI message
|
|
82
|
+
"gui-apply-only": "Apply operation only on ports with specified causality. If selected, at least one causality "
|
|
83
|
+
"should be selected."
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def usage(self, option):
|
|
87
|
+
return self._usage[option]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
|
|
3
|
+
<xs:annotation>
|
|
4
|
+
<xs:documentation>
|
|
5
|
+
Copyright(c) 2008-2011 MODELISAR consortium,
|
|
6
|
+
2012-2022 Modelica Association Project "FMI".
|
|
7
|
+
All rights reserved.
|
|
8
|
+
|
|
9
|
+
This schema file is part of the FMI 2.0.4 standard.
|
|
10
|
+
|
|
11
|
+
This file is licensed by the copyright holders under the 2-Clause BSD License
|
|
12
|
+
(https://opensource.org/licenses/BSD-2-Clause):
|
|
13
|
+
|
|
14
|
+
----------------------------------------------------------------------------
|
|
15
|
+
Redistribution and use in source and binary forms, with or without
|
|
16
|
+
modification, are permitted provided that the following conditions are met:
|
|
17
|
+
|
|
18
|
+
- Redistributions of source code must retain the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer.
|
|
20
|
+
|
|
21
|
+
- Redistributions in binary form must reproduce the above copyright notice,
|
|
22
|
+
this list of conditions and the following disclaimer in the documentation
|
|
23
|
+
and/or other materials provided with the distribution.
|
|
24
|
+
|
|
25
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
26
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
27
|
+
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
28
|
+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
29
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
30
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
31
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
32
|
+
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
33
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
34
|
+
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
35
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
----------------------------------------------------------------------------
|
|
37
|
+
</xs:documentation>
|
|
38
|
+
</xs:annotation>
|
|
39
|
+
<xs:complexType name="fmi2Annotation">
|
|
40
|
+
<xs:sequence maxOccurs="unbounded">
|
|
41
|
+
<xs:element name="Tool">
|
|
42
|
+
<xs:annotation>
|
|
43
|
+
<xs:documentation>Tool specific annotation (ignored by other tools).</xs:documentation>
|
|
44
|
+
</xs:annotation>
|
|
45
|
+
<xs:complexType>
|
|
46
|
+
<xs:sequence>
|
|
47
|
+
<xs:any namespace="##any" processContents="lax" minOccurs="0"/>
|
|
48
|
+
</xs:sequence>
|
|
49
|
+
<xs:attribute name="name" type="xs:normalizedString" use="required">
|
|
50
|
+
<xs:annotation>
|
|
51
|
+
<xs:documentation>Name of tool that can interpret the annotation. "name" must be unique with respect to all other elements of the VendorAnnotation list.</xs:documentation>
|
|
52
|
+
</xs:annotation>
|
|
53
|
+
</xs:attribute>
|
|
54
|
+
</xs:complexType>
|
|
55
|
+
</xs:element>
|
|
56
|
+
</xs:sequence>
|
|
57
|
+
</xs:complexType>
|
|
58
|
+
</xs:schema>
|