def contextMenuEvent(self, event): print("Label tries to open menu") menu = QMenu() menu.addAction("Rename") menu.actions()[0].triggered.connect(self.mouseDoubleClickEvent) menu.addAction("Set as current object") menu.actions()[1].triggered.connect(lambda: self.parent_object.parent_application.set_active_object(self.parent_object)) menu.addAction("Delete") menu.actions()[2].triggered.connect(self.parent_object.remove) menu.exec_(event.globalPos()) del menu
def createCorrectionsMenu(self, cursor, parent=None): """Create and return a menu for correcting the selected word.""" if not cursor: return None text = cursor.selectedText() suggests = trim_suggestions(text, self.highlighter.dict().suggest(text), self.max_suggestions) spell_menu = QMenu('Spelling Suggestions', parent) for word in suggests: action = QAction(word, spell_menu) action.setData((cursor, word)) spell_menu.addAction(action) # Only return the menu if it's non-empty if spell_menu.actions(): spell_menu.triggered.connect(self.cb_correct_word) return spell_menu return None
def setup(self): "constructs the gui" Fixed = QSizePolicy() MinimumExpanding = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.minKeyLen = 8 self.maxKeyLen = 4096 self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Horizontal) self.splitter.splitterMoved.connect(self.splitterChanged) # left column self.leftColumn = QWidget() self.vl01 = QVBoxLayout() # left column - first item (0; horizonal layout 0) self.hl00 = QHBoxLayout() self.hl00.setSpacing(5) self.openButton = QPushButton('&Open') self.openButton.setToolTip('Open folder') self.openButton.setMinimumSize(60, 20) self.openButton.setMaximumSize(60, 20) self.openButton.setSizePolicy(Fixed) self.openButton.clicked.connect(self.getFolder) #ico = self.style().standardIcon(QStyle.SP_DirIcon) #self.openButton.setIcon(ico) self.folderLabel = QLabel() self.folderLabel.setMinimumSize(135, 20) self.folderLabel.setMaximumSize(16777215, 20) self.folderLabel.setSizePolicy(MinimumExpanding) self.hl00.insertWidget(0, self.openButton) self.hl00.insertWidget(1, self.folderLabel) # left column - second item (1) self.folderTable = QTableWidget() self.folderTable.setMinimumSize(200, 32) self.folderTable.horizontalHeader().setVisible(False) self.folderTable.horizontalHeader().setStretchLastSection(True) self.folderTable.verticalHeader().setVisible(False) self.folderTable.verticalHeader().setDefaultSectionSize(15) self.folderTable.itemChanged.connect(self.editFileName) # left column - third item (2) self.extraLabel = QLabel() self.extraLabel.setMinimumSize(200, 20) self.extraLabel.setMaximumSize(16777215, 20) self.extraLabel.setSizePolicy(MinimumExpanding) self.extraLabel.setTextInteractionFlags(Qt.LinksAccessibleByMouse) # finalize left column self.vl01.insertLayout(0, self.hl00) self.vl01.insertWidget(1, self.folderTable) self.vl01.insertWidget(2, self.extraLabel) self.leftColumn.setLayout(self.vl01) # right column self.rightColumn = QWidget() self.vl02 = QVBoxLayout() # right column - first item (0) self.messageLabel = QLabel() self.messageLabel.setMinimumSize(290, 20) self.messageLabel.setMaximumSize(16777215, 20) self.messageLabel.setSizePolicy(MinimumExpanding) self.messageLabel.setAlignment(Qt.AlignCenter) # right column - second item (2; horizontal layout 1) self.hl01 = QHBoxLayout() self.hl01.setSpacing(5) self.encryptButton = QPushButton('&Encrypt') #\U0001F512 self.encryptButton.setToolTip('Encrypt selected file') self.encryptButton.setMinimumSize(60, 20) self.encryptButton.setMaximumSize(60, 20) self.encryptButton.setSizePolicy(Fixed) self.encryptButton.clicked.connect(self.encrypt) self.encryptPbar = QProgressBar() self.encryptPbar.setMinimumSize(225, 20) self.encryptPbar.setMaximumSize(16777215, 20) self.encryptPbar.setSizePolicy(MinimumExpanding) self.encryptPbar.setTextVisible(False) palette = self.encryptPbar.palette() # color of progress bar color = QColor(211, 70, 0) palette.setColor(QPalette.Highlight, color) self.encryptPbar.setPalette(palette) self.hl01.insertWidget(0, self.encryptButton) self.hl01.insertWidget(1, self.encryptPbar) # right column - third item (3; horizontal layout 2) self.hl02 = QHBoxLayout() self.hl02.setSpacing(5) self.cancelButton = QPushButton('C&ANCEL') self.cancelButton.setToolTip('Cancels current operation') self.cancelButton.setMinimumSize(70, 24) self.cancelButton.setMaximumSize(70, 24) self.cancelButton.setSizePolicy(Fixed) self.cancelButton.clicked.connect(self.setCancel) font = self.cancelButton.font() font.setBold(True) self.cancelButton.setFont(font) self.cancelButton.blockSignals(True) self.cancelButton.setEnabled(False) self.cancelButton.hide() self._requestStop = False self.keyInput = QLineEdit() self.keyInput.setMinimumSize(225, 20) self.keyInput.setMaximumSize(16777215, 20) self.keyInput.setSizePolicy(MinimumExpanding) self.keyInput.setPlaceholderText('key') self.keyInput.setMaxLength(self.maxKeyLen) self.keyInput.setAlignment(Qt.AlignCenter) self.keyInput.textEdited.connect(self.showKeyLen) self.genKeyButton = QPushButton('&Gen Key') #\U0001F511 self.genKeyButton.setToolTip('Generate a random key') self.genKeyButton.setMinimumSize(60, 20) self.genKeyButton.setMaximumSize(60, 20) self.genKeyButton.setSizePolicy(Fixed) self.genKeyButton.clicked.connect(self.genKey) self.keySizeSB = QSpinBox() self.keySizeSB.setToolTip('Length of key to generate') self.keySizeSB.setRange(32, 1024) self.keySizeSB.setMinimumSize(40, 20) self.keySizeSB.setMaximumSize(40, 20) self.keySizeSB.setSizePolicy(Fixed) self.keySizeSB.setAlignment(Qt.AlignCenter) self.keySizeSB.setButtonSymbols(QSpinBox.NoButtons) self.keySizeSB.setWrapping(True) self.hl02.insertWidget(0, self.cancelButton) self.hl02.insertWidget(1, self.keyInput) self.hl02.insertWidget(2, self.genKeyButton) self.hl02.insertWidget(3, self.keySizeSB) # right column - fourth item (4; horizontal layout 3) self.hl03 = QHBoxLayout() self.hl03.setSpacing(5) self.decryptButton = QPushButton('&Decrypt') #\U0001F513 self.decryptButton.setToolTip('Decrypt selected file') self.decryptButton.setMinimumSize(60, 20) self.decryptButton.setMaximumSize(60, 20) self.decryptButton.setSizePolicy(Fixed) self.decryptButton.clicked.connect(self.decrypt) self.decryptPbar = QProgressBar() self.decryptPbar.setMinimumSize(225, 20) self.decryptPbar.setMaximumSize(16777215, 20) self.decryptPbar.setSizePolicy(MinimumExpanding) self.decryptPbar.setTextVisible(False) self.decryptPbar.setInvertedAppearance(True) palette = self.decryptPbar.palette() # color of progress bar color = QColor(0, 170, 255) palette.setColor(QPalette.Highlight, color) self.decryptPbar.setPalette(palette) self.hl03.insertWidget(0, self.decryptButton) self.hl03.insertWidget(1, self.decryptPbar) # right column - fifth item (7; horizontal layout 4) self.hl04 = QHBoxLayout() self.hl04.setSpacing(5) self.showKeyCB = QCheckBox('&Show Key') self.showKeyCB.setToolTip('Show/Hide key value') self.showKeyCB.setMinimumSize(75, 20) self.showKeyCB.setMaximumSize(75, 20) self.showKeyCB.setSizePolicy(Fixed) self.showKeyCB.clicked.connect(self.showKey) self.showKeyCB.setChecked(True) self.hashPbar = QProgressBar() self.hashPbar.setMinimumSize(150, 20) self.hashPbar.setMaximumSize(16777215, 20) self.hashPbar.setSizePolicy(MinimumExpanding) self.hashPbar.setTextVisible(False) palette = self.hashPbar.palette() # color of progress bar color = QColor(31, 120, 73) palette.setColor(QPalette.Highlight, color) self.hashPbar.setPalette(palette) self.hashButton = QPushButton('&Hash') self.hashButton.setToolTip('Determine file hash') self.hashButton.setMinimumSize(60, 20) self.hashButton.setMaximumSize(60, 20) self.hashButton.setSizePolicy(Fixed) menu = QMenu(self.hashButton) ico = self.style().standardIcon(QStyle.SP_DialogYesButton) for alg in sorted( filter(lambda x: 'shake' not in x, hashlib.algorithms_guaranteed), key=lambda n: (len(n), sorted(hashlib.algorithms_guaranteed).index(n))): menu.addAction( ico, alg ) # drop shake algs as their .hexdigest requires an argument - the rest don't menu.addAction(ico, 'Party') for i in menu.actions(): i.setIconVisibleInMenu(False) self.hashButton.setMenu(menu) menu.triggered.connect(self.genHash) self.hl04.insertWidget(0, self.showKeyCB) self.hl04.insertWidget(1, self.hashPbar) self.hl04.insertWidget(2, self.hashButton) # right column - sixth item (8; horizontal layout 5) self.hl05 = QHBoxLayout() self.hl05.setSpacing(5) self.copyButton = QPushButton('&Copy') #\U0001F4CB self.copyButton.setToolTip('Copy key or hash to clipboard') self.copyButton.setMinimumSize(60, 20) self.copyButton.setMaximumSize(60, 20) self.copyButton.setSizePolicy(Fixed) menu2 = QMenu(self.copyButton) menu2.addAction('Copy Key') menu2.addAction('Copy Hash') self.copyButton.setMenu(menu2) menu2.triggered.connect(self.copyKeyHash) self.hashLabel = QLabel() self.hashLabel.setMinimumSize(225, 20) self.hashLabel.setMaximumSize(16777215, 20) self.hashLabel.setSizePolicy(MinimumExpanding) self.hashLabel.setTextFormat(Qt.PlainText) self.hashLabel.setAlignment(Qt.AlignCenter) self.hashLabel.setTextInteractionFlags(Qt.TextSelectableByMouse) self.hl05.insertWidget(0, self.copyButton) self.hl05.insertWidget(1, self.hashLabel) # finalize right column self.vl02.insertWidget(0, self.messageLabel) self.vl02.insertSpacerItem(1, QSpacerItem(0, 0)) self.vl02.insertLayout(2, self.hl01) self.vl02.insertLayout(3, self.hl02) self.vl02.insertLayout(4, self.hl03) self.vl02.insertSpacerItem(5, QSpacerItem(0, 0)) self.vl02.insertWidget(6, QFrame()) self.vl02.insertLayout(7, self.hl04) self.vl02.insertLayout(8, self.hl05) self.rightColumn.setLayout(self.vl02) # finalize main window self.splitter.insertWidget(0, self.leftColumn) self.splitter.insertWidget(1, self.rightColumn) layout = QHBoxLayout(self) layout.addWidget(self.splitter) self.setLayout(layout) self.setWindowTitle('Simple File Encryptor/Decryptor') self.resize(self.sizeHint())
class MainWindow(QMainWindow): def __init__(self, path_to_rom=""): super(MainWindow, self).__init__() self.setWindowIcon(icon("foundry.ico")) file_menu = QMenu("File") open_rom_action = file_menu.addAction("&Open ROM") open_rom_action.triggered.connect(self.on_open_rom) self.open_m3l_action = file_menu.addAction("&Open M3L") self.open_m3l_action.triggered.connect(self.on_open_m3l) file_menu.addSeparator() self.save_rom_action = file_menu.addAction("&Save ROM") self.save_rom_action.triggered.connect(self.on_save_rom) self.save_rom_as_action = file_menu.addAction("&Save ROM as ...") self.save_rom_as_action.triggered.connect(self.on_save_rom_as) """ file_menu.AppendSeparator() """ self.save_m3l_action = file_menu.addAction("&Save M3L") self.save_m3l_action.triggered.connect(self.on_save_m3l) """ file_menu.Append(ID_SAVE_LEVEL_TO, "&Save Level to", "") file_menu.AppendSeparator() file_menu.Append(ID_APPLY_IPS_PATCH, "&Apply IPS Patch", "") file_menu.AppendSeparator() file_menu.Append(ID_ROM_PRESET, "&ROM Preset", "") """ file_menu.addSeparator() settings_action = file_menu.addAction("&Settings") settings_action.triggered.connect(show_settings) file_menu.addSeparator() exit_action = file_menu.addAction("&Exit") exit_action.triggered.connect(lambda _: self.close()) self.menuBar().addMenu(file_menu) """ edit_menu = wx.Menu() edit_menu.Append(ID_EDIT_LEVEL, "&Edit Level", "") edit_menu.Append(ID_EDIT_OBJ_DEFS, "&Edit Object Definitions", "") edit_menu.Append(ID_EDIT_PALETTE, "&Edit Palette", "") edit_menu.Append(ID_EDIT_GRAPHICS, "&Edit Graphics", "") edit_menu.Append(ID_EDIT_MISC, "&Edit Miscellaneous", "") edit_menu.AppendSeparator() edit_menu.Append(ID_FREE_FORM_MODE, "&Free form Mode", "") edit_menu.Append(ID_LIMIT_SIZE, "&Limit Size", "") """ self.level_menu = QMenu("Level") self.select_level_action = self.level_menu.addAction("&Select Level") self.select_level_action.triggered.connect(self.open_level_selector) self.reload_action = self.level_menu.addAction("&Reload Level") self.reload_action.triggered.connect(self.reload_level) self.level_menu.addSeparator() self.edit_header_action = self.level_menu.addAction("&Edit Header") self.edit_header_action.triggered.connect(self.on_header_editor) self.edit_autoscroll = self.level_menu.addAction("Edit Autoscrolling") self.edit_autoscroll.triggered.connect(self.on_edit_autoscroll) self.menuBar().addMenu(self.level_menu) self.object_menu = QMenu("Objects") view_blocks_action = self.object_menu.addAction("&View Blocks") view_blocks_action.triggered.connect(self.on_block_viewer) view_objects_action = self.object_menu.addAction("&View Objects") view_objects_action.triggered.connect(self.on_object_viewer) self.object_menu.addSeparator() view_palettes_action = self.object_menu.addAction( "View Object Palettes") view_palettes_action.triggered.connect(self.on_palette_viewer) self.menuBar().addMenu(self.object_menu) self.view_menu = QMenu("View") self.view_menu.triggered.connect(self.on_menu) action = self.view_menu.addAction("Mario") action.setProperty(ID_PROP, ID_MARIO) action.setCheckable(True) action.setChecked(SETTINGS["draw_mario"]) action = self.view_menu.addAction("&Jumps on objects") action.setProperty(ID_PROP, ID_JUMP_OBJECTS) action.setCheckable(True) action.setChecked(SETTINGS["draw_jump_on_objects"]) action = self.view_menu.addAction("Items in blocks") action.setProperty(ID_PROP, ID_ITEM_BLOCKS) action.setCheckable(True) action.setChecked(SETTINGS["draw_items_in_blocks"]) action = self.view_menu.addAction("Invisible items") action.setProperty(ID_PROP, ID_INVISIBLE_ITEMS) action.setCheckable(True) action.setChecked(SETTINGS["draw_invisible_items"]) action = self.view_menu.addAction("Autoscroll Path") action.setProperty(ID_PROP, ID_AUTOSCROLL) action.setCheckable(True) action.setChecked(SETTINGS["draw_autoscroll"]) self.view_menu.addSeparator() action = self.view_menu.addAction("Jump Zones") action.setProperty(ID_PROP, ID_JUMPS) action.setCheckable(True) action.setChecked(SETTINGS["draw_jumps"]) action = self.view_menu.addAction("&Grid lines") action.setProperty(ID_PROP, ID_GRID_LINES) action.setCheckable(True) action.setChecked(SETTINGS["draw_grid"]) action = self.view_menu.addAction("Resize Type") action.setProperty(ID_PROP, ID_RESIZE_TYPE) action.setCheckable(True) action.setChecked(SETTINGS["draw_expansion"]) self.view_menu.addSeparator() action = self.view_menu.addAction("&Block Transparency") action.setProperty(ID_PROP, ID_TRANSPARENCY) action.setCheckable(True) action.setChecked(SETTINGS["block_transparency"]) self.view_menu.addSeparator() self.view_menu.addAction( "&Save Screenshot of Level").triggered.connect(self.on_screenshot) """ self.view_menu.Append(ID_BACKGROUND_FLOOR, "&Background & Floor", "") self.view_menu.Append(ID_TOOLBAR, "&Toolbar", "") self.view_menu.AppendSeparator() self.view_menu.Append(ID_ZOOM, "&Zoom", "") self.view_menu.AppendSeparator() self.view_menu.Append(ID_USE_ROM_GRAPHICS, "&Use ROM Graphics", "") self.view_menu.Append(ID_PALETTE, "&Palette", "") self.view_menu.AppendSeparator() self.view_menu.Append(ID_MORE, "&More", "") """ self.menuBar().addMenu(self.view_menu) help_menu = QMenu("Help") """ help_menu.Append(ID_ENEMY_COMPATIBILITY, "&Enemy Compatibility", "") help_menu.Append(ID_TROUBLESHOOTING, "&Troubleshooting", "") help_menu.AppendSeparator() help_menu.Append(ID_PROGRAM_WEBSITE, "&Program Website", "") help_menu.Append(ID_MAKE_A_DONATION, "&Make a Donation", "") help_menu.AppendSeparator() """ update_action = help_menu.addAction("Check for updates") update_action.triggered.connect(self.on_check_for_update) help_menu.addSeparator() video_action = help_menu.addAction("Feature Video on YouTube") video_action.triggered.connect(lambda: open_url(feature_video_link)) github_action = help_menu.addAction("Github Repository") github_action.triggered.connect(lambda: open_url(github_link)) discord_action = help_menu.addAction("SMB3 Rom Hacking Discord") discord_action.triggered.connect(lambda: open_url(discord_link)) help_menu.addSeparator() about_action = help_menu.addAction("&About") about_action.triggered.connect(self.on_about) self.menuBar().addMenu(help_menu) self.block_viewer = None self.object_viewer = None self.level_ref = LevelRef() self.level_ref.data_changed.connect(self._on_level_data_changed) self.context_menu = ContextMenu(self.level_ref) self.context_menu.triggered.connect(self.on_menu) self.level_view = LevelView(self, self.level_ref, self.context_menu) self.scroll_panel = QScrollArea() self.scroll_panel.setWidgetResizable(True) self.scroll_panel.setWidget(self.level_view) self.setCentralWidget(self.scroll_panel) self.spinner_panel = SpinnerPanel(self, self.level_ref) self.spinner_panel.zoom_in_triggered.connect(self.level_view.zoom_in) self.spinner_panel.zoom_out_triggered.connect(self.level_view.zoom_out) self.spinner_panel.object_change.connect(self.on_spin) self.object_list = ObjectList(self, self.level_ref, self.context_menu) self.object_dropdown = ObjectDropdown(self) self.object_dropdown.object_selected.connect( self._on_placeable_object_selected) self.level_size_bar = LevelSizeBar(self, self.level_ref) self.enemy_size_bar = EnemySizeBar(self, self.level_ref) self.jump_list = JumpList(self, self.level_ref) self.jump_list.add_jump.connect(self.on_jump_added) self.jump_list.edit_jump.connect(self.on_jump_edit) self.jump_list.remove_jump.connect(self.on_jump_removed) splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.object_list) splitter.setStretchFactor(0, 1) splitter.addWidget(self.jump_list) splitter.setChildrenCollapsible(False) level_toolbar = QToolBar("Level Info Toolbar", self) level_toolbar.setContextMenuPolicy(Qt.PreventContextMenu) level_toolbar.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) level_toolbar.setOrientation(Qt.Horizontal) level_toolbar.setFloatable(False) level_toolbar.addWidget(self.spinner_panel) level_toolbar.addWidget(self.object_dropdown) level_toolbar.addWidget(self.level_size_bar) level_toolbar.addWidget(self.enemy_size_bar) level_toolbar.addWidget(splitter) level_toolbar.setAllowedAreas(Qt.LeftToolBarArea | Qt.RightToolBarArea) self.addToolBar(Qt.RightToolBarArea, level_toolbar) self.object_toolbar = ObjectToolBar(self) self.object_toolbar.object_selected.connect( self._on_placeable_object_selected) object_toolbar = QToolBar("Object Toolbar", self) object_toolbar.setContextMenuPolicy(Qt.PreventContextMenu) object_toolbar.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) object_toolbar.setFloatable(False) object_toolbar.addWidget(self.object_toolbar) object_toolbar.setAllowedAreas(Qt.LeftToolBarArea | Qt.RightToolBarArea) self.addToolBar(Qt.LeftToolBarArea, object_toolbar) self.menu_toolbar = QToolBar("Menu Toolbar", self) self.menu_toolbar.setOrientation(Qt.Horizontal) self.menu_toolbar.setIconSize(QSize(20, 20)) self.menu_toolbar.addAction( icon("settings.svg"), "Editor Settings").triggered.connect(show_settings) self.menu_toolbar.addSeparator() self.menu_toolbar.addAction( icon("folder.svg"), "Open ROM").triggered.connect(self.on_open_rom) self.menu_toolbar.addAction( icon("save.svg"), "Save Level").triggered.connect(self.on_save_rom) self.menu_toolbar.addSeparator() self.undo_action = self.menu_toolbar.addAction(icon("rotate-ccw.svg"), "Undo Action") self.undo_action.triggered.connect(self.level_ref.undo) self.undo_action.setEnabled(False) self.redo_action = self.menu_toolbar.addAction(icon("rotate-cw.svg"), "Redo Action") self.redo_action.triggered.connect(self.level_ref.redo) self.redo_action.setEnabled(False) self.menu_toolbar.addSeparator() play_action = self.menu_toolbar.addAction(icon("play-circle.svg"), "Play Level") play_action.triggered.connect(self.on_play) play_action.setWhatsThis( "Opens an emulator with the current Level set to 1-1.\nSee Settings." ) self.menu_toolbar.addSeparator() self.menu_toolbar.addAction(icon("zoom-out.svg"), "Zoom Out").triggered.connect( self.level_view.zoom_out) self.menu_toolbar.addAction(icon("zoom-in.svg"), "Zoom In").triggered.connect( self.level_view.zoom_in) self.menu_toolbar.addSeparator() header_action = self.menu_toolbar.addAction(icon("tool.svg"), "Edit Level Header") header_action.triggered.connect(self.on_header_editor) header_action.setWhatsThis( "<b>Header Editor</b><br/>" "Many configurations regarding the level are done in its header, like the length of " "the timer, or where and how Mario enters the level.<br/>") self.jump_destination_action = self.menu_toolbar.addAction( icon("arrow-right-circle.svg"), "Go to Jump Destination") self.jump_destination_action.triggered.connect( self._go_to_jump_destination) self.jump_destination_action.setWhatsThis( "Opens the level, that can be reached from this one, e.g. by entering a pipe." ) self.menu_toolbar.addSeparator() whats_this_action = QWhatsThis.createAction() whats_this_action.setWhatsThis( "Click on parts of the editor, to receive help information.") whats_this_action.setIcon(icon("help-circle.svg")) whats_this_action.setText("Starts 'What's this?' mode") self.menu_toolbar.addAction(whats_this_action) self.menu_toolbar.addSeparator() self.warning_list = WarningList(self, self.level_ref) warning_action = self.menu_toolbar.addAction( icon("alert-triangle.svg"), "Warning Panel") warning_action.setWhatsThis("Shows a list of warnings.") warning_action.triggered.connect(self.warning_list.show) warning_action.setDisabled(True) self.warning_list.warnings_updated.connect(warning_action.setEnabled) self.addToolBar(Qt.TopToolBarArea, self.menu_toolbar) self.status_bar = ObjectStatusBar(self, self.level_ref) self.setStatusBar(self.status_bar) self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self, self.remove_selected_objects) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_X), self, self._cut_objects) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_C), self, self._copy_objects) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_V), self, self._paste_objects) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Z), self, self.level_ref.undo) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Y), self, self.level_ref.redo) QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Z), self, self.level_ref.redo) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Plus), self, self.level_view.zoom_in) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Minus), self, self.level_view.zoom_out) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_A), self, self.level_view.select_all) self.on_open_rom(path_to_rom) self.showMaximized() def _on_level_data_changed(self): self.undo_action.setEnabled(self.level_ref.undo_stack.undo_available) self.redo_action.setEnabled(self.level_ref.undo_stack.redo_available) self.jump_destination_action.setEnabled( self.level_ref.level.has_next_area) def _go_to_jump_destination(self): if not self.safe_to_change(): return level_address = self.level_ref.level.next_area_objects enemy_address = self.level_ref.level.next_area_enemies + 1 object_set = self.level_ref.level.next_area_object_set world, level = world_and_level_for_level_address(level_address) self.update_level(f"Level {world}-{level}", level_address, enemy_address, object_set) def on_play(self): """ Copies the ROM, including the current level, to a temporary directory, saves the current level as level 1-1 and opens the rom in an emulator. """ temp_dir = pathlib.Path(tempfile.gettempdir()) / "smb3foundry" temp_dir.mkdir(parents=True, exist_ok=True) path_to_temp_rom = temp_dir / "instaplay.rom" ROM().save_to(path_to_temp_rom) if not self._put_current_level_to_level_1_1(path_to_temp_rom): return if not self._set_default_powerup(path_to_temp_rom): return arguments = SETTINGS["instaplay_arguments"].replace( "%f", str(path_to_temp_rom)) arguments = shlex.split(arguments, posix=False) emu_path = pathlib.Path(SETTINGS["instaplay_emulator"]) if emu_path.is_absolute(): if emu_path.exists(): emulator = str(emu_path) else: QMessageBox.critical( self, "Emulator not found", f"Check it under File > Settings.\nFile {emu_path} not found." ) return else: emulator = SETTINGS["instaplay_emulator"] try: subprocess.run([emulator, *arguments]) except Exception as e: QMessageBox.critical(self, "Emulator command failed.", f"Check it under File > Settings.\n{str(e)}") def _open_rom(self, path_to_rom): with open(path_to_rom, "rb") as smb3_rom: data = smb3_rom.read() rom = SMB3Rom(bytearray(data)) return rom def _put_current_level_to_level_1_1(self, path_to_rom) -> bool: rom = self._open_rom(path_to_rom) # load world-1 data world_1 = SMB3World.from_world_number(rom, 1) # find position of "level 1" tile in world map for position in world_1.gen_positions(): if position.tile() == TILE_LEVEL_1: break else: QMessageBox.critical( self, "Couldn't place level", "Could not find a level 1 tile in World 1 to put your level at." ) return False if not self.level_ref.level.attached_to_rom: QMessageBox.critical( self, "Couldn't place level", "The Level is not part of the rom yet (M3L?). Try saving it into the ROM first.", ) return False # write level and enemy data of current level (layout_address, layout_bytes), (enemy_address, enemy_bytes) = self.level_ref.level.to_bytes() rom.write(layout_address, layout_bytes) rom.write(enemy_address, enemy_bytes) # replace level information with that of current level object_set_number = self.level_ref.object_set_number world_1.replace_level_at_position( (layout_address, enemy_address - 1, object_set_number), position) # save rom rom.save_to(path_to_rom) return True def _set_default_powerup(self, path_to_rom) -> bool: rom = self._open_rom(path_to_rom) *_, powerup, hasPWing = POWERUPS[SETTINGS["default_powerup"]] rom.write(Title_PrepForWorldMap + 0x1, bytes([powerup])) nop = 0xEA rts = 0x60 lda = 0xA9 staAbsolute = 0x8D # If a P-wing powerup is selected, another variable needs to be set with the P-wing value # This piece of code overwrites a part of Title_DebugMenu if hasPWing: Map_Power_DispHigh = 0x03 Map_Power_DispLow = 0xF3 # We need to start one byte before Title_DebugMenu to remove the RTS of Title_PrepForWorldMap # The assembly code below reads as follows: # LDA 0x08 # STA $03F3 # RTS rom.write( Title_DebugMenu - 0x1, bytes([ lda, 0x8, staAbsolute, Map_Power_DispLow, Map_Power_DispHigh, # The RTS to get out of the now extended Title_PrepForWorldMap rts, ]), ) # Remove code that resets the powerup value by replacing it with no-operations # Otherwise this code would copy the value of the normal powerup here # (So if the powerup would be Raccoon Mario, Map_Power_Disp would also be # set as Raccoon Mario instead of P-wing Map_Power_DispResetLocation = 0x3C5A2 rom.write(Map_Power_DispResetLocation, bytes([nop, nop, nop])) rom.save_to(path_to_rom) return True def on_screenshot(self, _) -> bool: if self.level_view is None: return False recommended_file = f"{os.path.expanduser('~')}/{ROM.name} - {self.level_view.level_ref.name}.png" pathname, _ = QFileDialog.getSaveFileName(self, caption="Save Screenshot", dir=recommended_file, filter=IMG_FILE_FILTER) if not pathname: return False # Proceed loading the file chosen by the user self.level_view.make_screenshot().save(pathname) return True def update_title(self): if self.level_view.level_ref is not None and ROM is not None: title = f"{self.level_view.level_ref.name} - {ROM.name}" else: title = "SMB3Foundry" self.setWindowTitle(title) def on_open_rom(self, path_to_rom="") -> bool: if not self.safe_to_change(): return False if not path_to_rom: # otherwise ask the user what new file to open path_to_rom, _ = QFileDialog.getOpenFileName( self, caption="Open ROM", filter=ROM_FILE_FILTER) if not path_to_rom: self._enable_disable_gui_elements() return False # Proceed loading the file chosen by the user try: ROM.load_from_file(path_to_rom) return self.open_level_selector(None) except IOError as exp: QMessageBox.warning(self, type(exp).__name__, f"Cannot open file '{path_to_rom}'.") return False finally: self._enable_disable_gui_elements() def on_open_m3l(self, _) -> bool: if not self.safe_to_change(): return False # otherwise ask the user what new file to open pathname, _ = QFileDialog.getOpenFileName(self, caption="Open M3L file", filter=M3L_FILE_FILTER) if not pathname: return False # Proceed loading the file chosen by the user try: with open(pathname, "rb") as m3l_file: self.level_view.from_m3l(bytearray(m3l_file.read())) except IOError as exp: QMessageBox.warning(self, type(exp).__name__, f"Cannot open file '{pathname}'.") return False self.level_view.level_ref.name = os.path.basename(pathname) self.update_gui_for_level() return True def safe_to_change(self) -> bool: if not self.level_ref: return True if self.level_ref.level.changed: answer = QMessageBox.question( self, "Please confirm", "Current content has not been saved! Proceed?", QMessageBox.No | QMessageBox.Yes, QMessageBox.No, ) return answer == QMessageBox.Yes else: return True def on_save_rom(self, _): self.save_rom(False) def on_save_rom_as(self, _): self.save_rom(True) def save_rom(self, is_save_as): safe_to_save, reason, additional_info = self.level_view.level_safe_to_save( ) if not safe_to_save: answer = QMessageBox.warning( self, reason, f"{additional_info}\n\nDo you want to proceed?", QMessageBox.No | QMessageBox.Yes, QMessageBox.No, ) if answer == QMessageBox.No: return if not self.level_ref.attached_to_rom: QMessageBox.information( self, "Importing M3L into ROM", "Please select the positions in the ROM you want the level objects and enemies/items to be stored.", QMessageBox.Ok, ) level_selector = LevelSelector(self) answer = level_selector.exec_() if answer == QMessageBox.Accepted: self.level_view.level_ref.attach_to_rom( level_selector.object_data_offset, level_selector.enemy_data_offset) if is_save_as: # if we save to another rom, don't consider the level # attached (to the current rom) self.level_view.level_ref.attached_to_rom = False else: return if is_save_as: pathname, _ = QFileDialog.getSaveFileName(self, caption="Save ROM as", filter=ROM_FILE_FILTER) if not pathname: return # the user changed their mind else: pathname = ROM.path level = self.level_ref.level for offset, data in level.to_bytes(): ROM().bulk_write(data, offset) try: ROM().save_to_file(pathname) except IOError as exp: QMessageBox.warning(self, f"{type(exp).__name__}", f"Cannot save ROM data to file '{pathname}'.") self.update_title() if not is_save_as: level.changed = False def on_save_m3l(self, _): suggested_file = self.level_view.level_ref.name if not suggested_file.endswith(".m3l"): suggested_file += ".m3l" pathname, _ = QFileDialog.getSaveFileName(self, caption="Save M3L as", filter=M3L_FILE_FILTER) if not pathname: return level = self.level_view.level_ref try: with open(pathname, "wb") as m3l_file: m3l_file.write(level.to_m3l()) except IOError as exp: QMessageBox.warning(self, type(exp).__name__, f"Couldn't save level to '{pathname}'.") def on_check_for_update(self): self.setCursor(Qt.WaitCursor) current_version = get_current_version_name() try: latest_version = get_latest_version_name() except ValueError as ve: QMessageBox.critical(self, "Error while checking for updates", f"Error: {ve}") return if current_version != latest_version: latest_release_url = f"{releases_link}/tag/{latest_version}" go_to_github_button = QPushButton(icon("external-link.svg"), "Go to latest release") go_to_github_button.clicked.connect( lambda: open_url(latest_release_url)) info_box = QMessageBox( QMessageBox.Information, "New release available", f"New Version {latest_version} is available.") info_box.addButton(QMessageBox.Cancel) info_box.addButton(go_to_github_button, QMessageBox.AcceptRole) info_box.exec_() else: QMessageBox.information( self, "No newer release", f"Version {current_version} is up to date.") self.setCursor(Qt.ArrowCursor) def on_menu(self, action: QAction): item_id = action.property(ID_PROP) if item_id in CHECKABLE_MENU_ITEMS: self.on_menu_item_checked(action) self.level_view.update() # if setting a checkbox, keep the menu open menu_of_action: QMenu = self.sender() menu_of_action.exec_() elif item_id in self.context_menu.get_all_menu_item_ids(): x, y = self.context_menu.get_position() if item_id == CMAction.REMOVE: self.remove_selected_objects() elif item_id == CMAction.ADD_OBJECT: selected_object = self.object_dropdown.currentIndex() if selected_object != -1: self.place_object_from_dropdown((x, y)) else: self.create_object_at(x, y) elif item_id == CMAction.CUT: self._cut_objects() elif item_id == CMAction.COPY: self._copy_objects() elif item_id == CMAction.PASTE: self._paste_objects(x, y) elif item_id == CMAction.FOREGROUND: self.bring_objects_to_foreground() elif item_id == CMAction.BACKGROUND: self.bring_objects_to_background() self.level_view.update() def reload_level(self): if not self.safe_to_change(): return level_name = self.level_view.level_ref.name object_data = self.level_view.level_ref.header_offset enemy_data = self.level_view.level_ref.enemy_offset object_set = self.level_view.level_ref.object_set_number self.update_level(level_name, object_data, enemy_data, object_set) def _on_placeable_object_selected(self, level_object: Union[LevelObject, EnemyObject]): if self.sender() is self.object_toolbar: self.object_dropdown.select_object(level_object) else: self.object_toolbar.select_object(level_object) @undoable def bring_objects_to_foreground(self): self.level_ref.level.bring_to_foreground( self.level_ref.selected_objects) @undoable def bring_objects_to_background(self): self.level_ref.level.bring_to_background( self.level_ref.selected_objects) @undoable def create_object_at(self, x, y): self.level_view.create_object_at(x, y) @undoable def create_enemy_at(self, x, y): self.level_view.create_enemy_at(x, y) def _cut_objects(self): self._copy_objects() self.remove_selected_objects() def _copy_objects(self): selected_objects = self.level_view.get_selected_objects().copy() if selected_objects: self.context_menu.set_copied_objects(selected_objects) @undoable def _paste_objects(self, x=None, y=None): self.level_view.paste_objects_at( self.context_menu.get_copied_objects(), x, y) @undoable def remove_selected_objects(self): self.level_view.remove_selected_objects() self.level_view.update() self.spinner_panel.disable_all() def on_menu_item_checked(self, action: QAction): item_id = action.property(ID_PROP) checked = action.isChecked() if item_id == ID_GRID_LINES: self.level_view.draw_grid = checked elif item_id == ID_TRANSPARENCY: self.level_view.transparency = checked elif item_id == ID_JUMPS: self.level_view.draw_jumps = checked elif item_id == ID_MARIO: self.level_view.draw_mario = checked elif item_id == ID_RESIZE_TYPE: self.level_view.draw_expansions = checked elif item_id == ID_JUMP_OBJECTS: self.level_view.draw_jumps_on_objects = checked elif item_id == ID_ITEM_BLOCKS: self.level_view.draw_items_in_blocks = checked elif item_id == ID_INVISIBLE_ITEMS: self.level_view.draw_invisible_items = checked elif item_id == ID_AUTOSCROLL: self.level_view.draw_autoscroll = checked SETTINGS["draw_mario"] = self.level_view.draw_mario SETTINGS["draw_jumps"] = self.level_view.draw_jumps SETTINGS["draw_grid"] = self.level_view.draw_grid SETTINGS["draw_expansion"] = self.level_view.draw_expansions SETTINGS[ "draw_jump_on_objects"] = self.level_view.draw_jumps_on_objects SETTINGS["draw_items_in_blocks"] = self.level_view.draw_items_in_blocks SETTINGS["draw_invisible_items"] = self.level_view.draw_invisible_items SETTINGS["draw_autoscroll"] = self.level_view.draw_autoscroll SETTINGS["block_transparency"] = self.level_view.transparency save_settings() @undoable def on_spin(self, _): selected_objects = self.level_ref.selected_objects if len(selected_objects) != 1: logging.error(selected_objects, RuntimeWarning) return selected_object = selected_objects[0] obj_type = self.spinner_panel.get_type() if isinstance(selected_object, LevelObject): domain = self.spinner_panel.get_domain() if selected_object.is_4byte: length = self.spinner_panel.get_length() else: length = None self.level_view.replace_object(selected_object, domain, obj_type, length) else: self.level_view.replace_enemy(selected_object, obj_type) self.level_ref.data_changed.emit() def fill_object_list(self): self.object_list.Clear() self.object_list.SetItems(self.level_view.get_object_names()) def open_level_selector(self, _): if not self.safe_to_change(): return level_selector = LevelSelector(self) level_was_selected = level_selector.exec_() == QDialog.Accepted if level_was_selected: self.update_level( level_selector.level_name, level_selector.object_data_offset, level_selector.enemy_data_offset, level_selector.object_set, ) return level_was_selected def on_block_viewer(self, _): if self.block_viewer is None: self.block_viewer = BlockViewer(parent=self) if self.level_ref.level is not None: self.block_viewer.object_set = self.level_ref.object_set.number self.block_viewer.palette_group = self.level_ref.object_palette_index self.block_viewer.show() def on_object_viewer(self, _): if self.object_viewer is None: self.object_viewer = ObjectViewer(parent=self) if self.level_ref.level is not None: object_set = self.level_ref.object_set.number graphics_set = self.level_ref.graphic_set self.object_viewer.set_object_and_graphic_set( object_set, graphics_set) if len(self.level_view.get_selected_objects()) == 1: selected_object = self.level_view.get_selected_objects()[0] if isinstance(selected_object, LevelObject): self.object_viewer.set_object(selected_object.domain, selected_object.obj_index, selected_object.length) self.object_viewer.show() def on_palette_viewer(self, _): PaletteViewer(self, self.level_ref).exec_() def on_edit_autoscroll(self, _): AutoScrollEditor(self, self.level_ref).exec_() def on_header_editor(self, _): HeaderEditor(self, self.level_ref).exec_() def update_level(self, level_name: str, object_data_offset: int, enemy_data_offset: int, object_set: int): try: self.level_ref.load_level(level_name, object_data_offset, enemy_data_offset, object_set) except IndexError: QMessageBox.critical( self, "Please confirm", "Failed loading level. The level offsets don't match.") return self.update_gui_for_level() def update_gui_for_level(self): self._enable_disable_gui_elements() self.update_title() self.jump_list.update() is_a_world_map = isinstance(self.level_ref.level, WorldMap) self.save_m3l_action.setEnabled(not is_a_world_map) self.edit_header_action.setEnabled(not is_a_world_map) if is_a_world_map: self.object_dropdown.Clear() self.object_dropdown.setEnabled(False) self.jump_list.setEnabled(False) self.jump_list.Clear() else: self.object_dropdown.setEnabled(True) self.object_dropdown.set_object_set( self.level_ref.object_set_number, self.level_ref.graphic_set) self.jump_list.setEnabled(True) self.object_toolbar.set_object_set(self.level_ref.object_set_number, self.level_ref.graphic_set) self.level_view.update() def _enable_disable_gui_elements(self): rom_elements = [ # entries in file menu self.open_m3l_action, self.save_rom_action, self.save_rom_as_action, # entry in level menu self.select_level_action, ] level_elements = [ # entry in file menu self.save_m3l_action, # top toolbar self.menu_toolbar, # other gui elements self.level_view, self.spinner_panel, self.object_toolbar, self.level_size_bar, self.enemy_size_bar, self.object_list, self.jump_list, self.object_toolbar, ] level_elements.extend(self.level_menu.actions()) level_elements.remove(self.select_level_action) level_elements.extend(self.object_menu.actions()) level_elements.extend(self.view_menu.actions()) for gui_element in rom_elements: gui_element.setEnabled(ROM.is_loaded()) for gui_element in level_elements: gui_element.setEnabled(ROM.is_loaded() and self.level_ref.level is not None) def on_jump_edit(self): index = self.jump_list.currentIndex().row() updated_jump = JumpEditor.edit_jump( self, self.level_view.level_ref.jumps[index]) self.on_jump_edited(updated_jump) @undoable def on_jump_added(self): self.level_view.add_jump() @undoable def on_jump_removed(self): self.level_view.remove_jump(self.jump_list.currentIndex().row()) @undoable def on_jump_edited(self, jump): index = self.jump_list.currentIndex().row() assert index >= 0 if isinstance(self.level_ref.level, Level): self.level_view.level_ref.jumps[index] = jump self.jump_list.item(index).setText(str(jump)) def on_jump_list_change(self, event): self.jump_list.set_jumps(event) def mouseReleaseEvent(self, event: QMouseEvent): if event.button() == Qt.MiddleButton: pos = self.level_view.mapFromGlobal(self.mapToGlobal( event.pos())).toTuple() self.place_object_from_dropdown(pos) @undoable def place_object_from_dropdown(self, pos: Tuple[int, int]) -> None: # the dropdown is synchronized with the toolbar, so it doesn't matter where to take it from level_object = self.object_dropdown.currentData(Qt.UserRole) self.object_toolbar.add_recent_object(level_object) if isinstance(level_object, LevelObject): self.level_view.create_object_at(*pos, level_object.domain, level_object.obj_index) elif isinstance(level_object, EnemyObject): self.level_view.add_enemy(level_object.obj_index, *pos, -1) def on_about(self, _): about = AboutDialog(self) about.show() def closeEvent(self, event: QCloseEvent): if not self.safe_to_change(): event.ignore() return super(MainWindow, self).closeEvent(event)
class FittingResultViewer(QDialog): PAGE_ROWS = 20 logger = logging.getLogger("root.QGrain.ui.FittingResultViewer") result_marked = Signal(SSUResult) def __init__(self, reference_viewer: ReferenceResultViewer, parent=None): super().__init__(parent=parent, f=Qt.Window) self.setWindowTitle(self.tr("SSU Fitting Result Viewer")) self.__fitting_results = [] # type: list[SSUResult] self.retry_tasks = {} # type: dict[UUID, SSUTask] self.__reference_viewer = reference_viewer self.init_ui() self.boxplot_chart = BoxplotChart(parent=self, toolbar=True) self.typical_chart = SSUTypicalComponentChart(parent=self, toolbar=True) self.distance_chart = DistanceCurveChart(parent=self, toolbar=True) self.mixed_distribution_chart = MixedDistributionChart( parent=self, toolbar=True, use_animation=True) self.file_dialog = QFileDialog(parent=self) self.async_worker = AsyncWorker() self.async_worker.background_worker.task_succeeded.connect( self.on_fitting_succeeded) self.async_worker.background_worker.task_failed.connect( self.on_fitting_failed) self.update_page_list() self.update_page(self.page_index) self.normal_msg = QMessageBox(self) self.remove_warning_msg = QMessageBox(self) self.remove_warning_msg.setStandardButtons(QMessageBox.No | QMessageBox.Yes) self.remove_warning_msg.setDefaultButton(QMessageBox.No) self.remove_warning_msg.setWindowTitle(self.tr("Warning")) self.remove_warning_msg.setText( self.tr("Are you sure to remove all SSU results?")) self.outlier_msg = QMessageBox(self) self.outlier_msg.setStandardButtons(QMessageBox.Discard | QMessageBox.Retry | QMessageBox.Ignore) self.outlier_msg.setDefaultButton(QMessageBox.Ignore) self.retry_progress_msg = QMessageBox() self.retry_progress_msg.addButton(QMessageBox.Ok) self.retry_progress_msg.button(QMessageBox.Ok).hide() self.retry_progress_msg.setWindowTitle(self.tr("Progress")) self.retry_timer = QTimer(self) self.retry_timer.setSingleShot(True) self.retry_timer.timeout.connect( lambda: self.retry_progress_msg.exec_()) def init_ui(self): self.data_table = QTableWidget(100, 100) self.data_table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.data_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.data_table.setAlternatingRowColors(True) self.data_table.setContextMenuPolicy(Qt.CustomContextMenu) self.main_layout = QGridLayout(self) self.main_layout.addWidget(self.data_table, 0, 0, 1, 3) self.previous_button = QPushButton( qta.icon("mdi.skip-previous-circle"), self.tr("Previous")) self.previous_button.setToolTip( self.tr("Click to back to the previous page.")) self.previous_button.clicked.connect(self.on_previous_button_clicked) self.current_page_combo_box = QComboBox() self.current_page_combo_box.addItem(self.tr("Page {0}").format(1)) self.current_page_combo_box.currentIndexChanged.connect( self.update_page) self.next_button = QPushButton(qta.icon("mdi.skip-next-circle"), self.tr("Next")) self.next_button.setToolTip(self.tr("Click to jump to the next page.")) self.next_button.clicked.connect(self.on_next_button_clicked) self.main_layout.addWidget(self.previous_button, 1, 0) self.main_layout.addWidget(self.current_page_combo_box, 1, 1) self.main_layout.addWidget(self.next_button, 1, 2) self.distance_label = QLabel(self.tr("Distance")) self.distance_label.setToolTip( self. tr("It's the function to calculate the difference (on the contrary, similarity) between two samples." )) self.distance_combo_box = QComboBox() self.distance_combo_box.addItems(built_in_distances) self.distance_combo_box.setCurrentText("log10MSE") self.distance_combo_box.currentTextChanged.connect( lambda: self.update_page(self.page_index)) self.main_layout.addWidget(self.distance_label, 2, 0) self.main_layout.addWidget(self.distance_combo_box, 2, 1, 1, 2) self.menu = QMenu(self.data_table) self.menu.setShortcutAutoRepeat(True) self.mark_action = self.menu.addAction( qta.icon("mdi.marker-check"), self.tr("Mark Selection(s) as Reference")) self.mark_action.triggered.connect(self.mark_selections) self.remove_selection_action = self.menu.addAction( qta.icon("fa.remove"), self.tr("Remove Selection(s)")) self.remove_selection_action.triggered.connect(self.remove_selections) self.remove_all_action = self.menu.addAction(qta.icon("fa.remove"), self.tr("Remove All")) self.remove_all_action.triggered.connect(self.remove_all_results) self.plot_loss_chart_action = self.menu.addAction( qta.icon("mdi.chart-timeline-variant"), self.tr("Plot Loss Chart")) self.plot_loss_chart_action.triggered.connect(self.show_distance) self.plot_distribution_chart_action = self.menu.addAction( qta.icon("fa5s.chart-area"), self.tr("Plot Distribution Chart")) self.plot_distribution_chart_action.triggered.connect( self.show_distribution) self.plot_distribution_animation_action = self.menu.addAction( qta.icon("fa5s.chart-area"), self.tr("Plot Distribution Chart (Animation)")) self.plot_distribution_animation_action.triggered.connect( self.show_history_distribution) self.detect_outliers_menu = self.menu.addMenu( qta.icon("mdi.magnify"), self.tr("Detect Outliers")) self.check_nan_and_inf_action = self.detect_outliers_menu.addAction( self.tr("Check NaN and Inf")) self.check_nan_and_inf_action.triggered.connect(self.check_nan_and_inf) self.check_final_distances_action = self.detect_outliers_menu.addAction( self.tr("Check Final Distances")) self.check_final_distances_action.triggered.connect( self.check_final_distances) self.check_component_mean_action = self.detect_outliers_menu.addAction( self.tr("Check Component Mean")) self.check_component_mean_action.triggered.connect( lambda: self.check_component_moments("mean")) self.check_component_std_action = self.detect_outliers_menu.addAction( self.tr("Check Component STD")) self.check_component_std_action.triggered.connect( lambda: self.check_component_moments("std")) self.check_component_skewness_action = self.detect_outliers_menu.addAction( self.tr("Check Component Skewness")) self.check_component_skewness_action.triggered.connect( lambda: self.check_component_moments("skewness")) self.check_component_kurtosis_action = self.detect_outliers_menu.addAction( self.tr("Check Component Kurtosis")) self.check_component_kurtosis_action.triggered.connect( lambda: self.check_component_moments("kurtosis")) self.check_component_fractions_action = self.detect_outliers_menu.addAction( self.tr("Check Component Fractions")) self.check_component_fractions_action.triggered.connect( self.check_component_fractions) self.degrade_results_action = self.detect_outliers_menu.addAction( self.tr("Degrade Results")) self.degrade_results_action.triggered.connect(self.degrade_results) self.try_align_components_action = self.detect_outliers_menu.addAction( self.tr("Try Align Components")) self.try_align_components_action.triggered.connect( self.try_align_components) self.analyse_typical_components_action = self.menu.addAction( qta.icon("ei.tags"), self.tr("Analyse Typical Components")) self.analyse_typical_components_action.triggered.connect( self.analyse_typical_components) self.load_dump_action = self.menu.addAction( qta.icon("fa.database"), self.tr("Load Binary Dump")) self.load_dump_action.triggered.connect(self.load_dump) self.save_dump_action = self.menu.addAction( qta.icon("fa.save"), self.tr("Save Binary Dump")) self.save_dump_action.triggered.connect(self.save_dump) self.save_excel_action = self.menu.addAction( qta.icon("mdi.microsoft-excel"), self.tr("Save Excel")) self.save_excel_action.triggered.connect( lambda: self.on_save_excel_clicked(align_components=False)) self.save_excel_align_action = self.menu.addAction( qta.icon("mdi.microsoft-excel"), self.tr("Save Excel (Force Alignment)")) self.save_excel_align_action.triggered.connect( lambda: self.on_save_excel_clicked(align_components=True)) self.data_table.customContextMenuRequested.connect(self.show_menu) # necessary to add actions of menu to this widget itself, # otherwise, the shortcuts will not be triggered self.addActions(self.menu.actions()) def show_menu(self, pos: QPoint): self.menu.popup(QCursor.pos()) def show_message(self, title: str, message: str): self.normal_msg.setWindowTitle(title) self.normal_msg.setText(message) self.normal_msg.exec_() def show_info(self, message: str): self.show_message(self.tr("Info"), message) def show_warning(self, message: str): self.show_message(self.tr("Warning"), message) def show_error(self, message: str): self.show_message(self.tr("Error"), message) @property def distance_name(self) -> str: return self.distance_combo_box.currentText() @property def distance_func(self) -> typing.Callable: return get_distance_func_by_name(self.distance_combo_box.currentText()) @property def page_index(self) -> int: return self.current_page_combo_box.currentIndex() @property def n_pages(self) -> int: return self.current_page_combo_box.count() @property def n_results(self) -> int: return len(self.__fitting_results) @property def selections(self): start = self.page_index * self.PAGE_ROWS temp = set() for item in self.data_table.selectedRanges(): for i in range(item.topRow(), min(self.PAGE_ROWS + 1, item.bottomRow() + 1)): temp.add(i + start) indexes = list(temp) indexes.sort() return indexes def update_page_list(self): last_page_index = self.page_index if self.n_results == 0: n_pages = 1 else: n_pages, left = divmod(self.n_results, self.PAGE_ROWS) if left != 0: n_pages += 1 self.current_page_combo_box.blockSignals(True) self.current_page_combo_box.clear() self.current_page_combo_box.addItems( [self.tr("Page {0}").format(i + 1) for i in range(n_pages)]) if last_page_index >= n_pages: self.current_page_combo_box.setCurrentIndex(n_pages - 1) else: self.current_page_combo_box.setCurrentIndex(last_page_index) self.current_page_combo_box.blockSignals(False) def update_page(self, page_index: int): def write(row: int, col: int, value: str): if isinstance(value, str): pass elif isinstance(value, int): value = str(value) elif isinstance(value, float): value = f"{value: 0.4f}" else: value = value.__str__() item = QTableWidgetItem(value) item.setTextAlignment(Qt.AlignCenter) self.data_table.setItem(row, col, item) # necessary to clear self.data_table.clear() if page_index == self.n_pages - 1: start = page_index * self.PAGE_ROWS end = self.n_results else: start, end = page_index * self.PAGE_ROWS, (page_index + 1) * self.PAGE_ROWS self.data_table.setRowCount(end - start) self.data_table.setColumnCount(7) self.data_table.setHorizontalHeaderLabels([ self.tr("Resolver"), self.tr("Distribution Type"), self.tr("N_components"), self.tr("N_iterations"), self.tr("Spent Time [s]"), self.tr("Final Distance"), self.tr("Has Reference") ]) sample_names = [ result.sample.name for result in self.__fitting_results[start:end] ] self.data_table.setVerticalHeaderLabels(sample_names) for row, result in enumerate(self.__fitting_results[start:end]): write(row, 0, result.task.resolver) write(row, 1, self.get_distribution_name(result.task.distribution_type)) write(row, 2, result.task.n_components) write(row, 3, result.n_iterations) write(row, 4, result.time_spent) write( row, 5, self.distance_func(result.sample.distribution, result.distribution)) has_ref = result.task.initial_guess is not None or result.task.reference is not None write(row, 6, self.tr("Yes") if has_ref else self.tr("No")) self.data_table.resizeColumnsToContents() def on_previous_button_clicked(self): if self.page_index > 0: self.current_page_combo_box.setCurrentIndex(self.page_index - 1) def on_next_button_clicked(self): if self.page_index < self.n_pages - 1: self.current_page_combo_box.setCurrentIndex(self.page_index + 1) def get_distribution_name(self, distribution_type: DistributionType): if distribution_type == DistributionType.Normal: return self.tr("Normal") elif distribution_type == DistributionType.Weibull: return self.tr("Weibull") elif distribution_type == DistributionType.SkewNormal: return self.tr("Skew Normal") else: raise NotImplementedError(distribution_type) def add_result(self, result: SSUResult): if self.n_results == 0 or \ (self.page_index == self.n_pages - 1 and \ divmod(self.n_results, self.PAGE_ROWS)[-1] != 0): need_update = True else: need_update = False self.__fitting_results.append(result) self.update_page_list() if need_update: self.update_page(self.page_index) def add_results(self, results: typing.List[SSUResult]): if self.n_results == 0 or \ (self.page_index == self.n_pages - 1 and \ divmod(self.n_results, self.PAGE_ROWS)[-1] != 0): need_update = True else: need_update = False self.__fitting_results.extend(results) self.update_page_list() if need_update: self.update_page(self.page_index) def mark_selections(self): for index in self.selections: self.result_marked.emit(self.__fitting_results[index]) def remove_results(self, indexes): results = [] for i in reversed(indexes): res = self.__fitting_results.pop(i) results.append(res) self.update_page_list() self.update_page(self.page_index) def remove_selections(self): indexes = self.selections self.remove_results(indexes) def remove_all_results(self): res = self.remove_warning_msg.exec_() if res == QMessageBox.Yes: self.__fitting_results.clear() self.update_page_list() self.update_page(0) def show_distance(self): results = [self.__fitting_results[i] for i in self.selections] if results is None or len(results) == 0: return result = results[0] self.distance_chart.show_distance_series(result.get_distance_series( self.distance_name), title=result.sample.name) self.distance_chart.show() def show_distribution(self): results = [self.__fitting_results[i] for i in self.selections] if results is None or len(results) == 0: return result = results[0] self.mixed_distribution_chart.show_model(result.view_model) self.mixed_distribution_chart.show() def show_history_distribution(self): results = [self.__fitting_results[i] for i in self.selections] if results is None or len(results) == 0: return result = results[0] self.mixed_distribution_chart.show_result(result) self.mixed_distribution_chart.show() def load_dump(self): filename, _ = self.file_dialog.getOpenFileName( self, self.tr("Select a binary dump file of SSU results"), None, self.tr("Binary dump (*.dump)")) if filename is None or filename == "": return with open(filename, "rb") as f: results = pickle.load(f) # type: list[SSUResult] valid = True if isinstance(results, list): for result in results: if not isinstance(result, SSUResult): valid = False break else: valid = False if valid: if self.n_results != 0 and len(results) != 0: old_classes = self.__fitting_results[0].classes_φ new_classes = results[0].classes_φ classes_inconsistent = False if len(old_classes) != len(new_classes): classes_inconsistent = True else: classes_error = np.abs(old_classes - new_classes) if not np.all(np.less_equal(classes_error, 1e-8)): classes_inconsistent = True if classes_inconsistent: self.show_error( self. tr("The results in the dump file has inconsistent grain-size classes with that in your list." )) return self.add_results(results) else: self.show_error(self.tr("The binary dump file is invalid.")) def save_dump(self): if self.n_results == 0: self.show_warning(self.tr("There is not any result in the list.")) return filename, _ = self.file_dialog.getSaveFileName( self, self.tr("Save the SSU results to binary dump file"), None, self.tr("Binary dump (*.dump)")) if filename is None or filename == "": return with open(filename, "wb") as f: pickle.dump(self.__fitting_results, f) def save_excel(self, filename, align_components=False): if self.n_results == 0: return results = self.__fitting_results.copy() classes_μm = results[0].classes_μm n_components_list = [ result.n_components for result in self.__fitting_results ] count_dict = Counter(n_components_list) max_n_components = max(count_dict.keys()) self.logger.debug( f"N_components: {count_dict}, Max N_components: {max_n_components}" ) flags = [] if not align_components: for result in results: flags.extend(range(result.n_components)) else: n_components_desc = "\n".join([ self.tr("{0} Component(s): {1}").format(n_components, count) for n_components, count in count_dict.items() ]) self.show_info( self.tr("N_components distribution of Results:\n{0}").format( n_components_desc)) stacked_components = [] for result in self.__fitting_results: for component in result.components: stacked_components.append(component.distribution) stacked_components = np.array(stacked_components) cluser = KMeans(n_clusters=max_n_components) flags = cluser.fit_predict(stacked_components) # check flags to make it unique flag_index = 0 for i, result in enumerate(self.__fitting_results): result_flags = set() for component in result.components: if flags[flag_index] in result_flags: if flags[flag_index] == max_n_components: flags[flag_index] = max_n_components - 1 else: flag_index[flag_index] += 1 result_flags.add(flags[flag_index]) flag_index += 1 flag_set = set(flags) picked = [] for target_flag in flag_set: for i, flag in enumerate(flags): if flag == target_flag: picked.append( (target_flag, logarithmic(classes_μm, stacked_components[i])["mean"])) break picked.sort(key=lambda x: x[1]) flag_map = {flag: index for index, (flag, _) in enumerate(picked)} flags = np.array([flag_map[flag] for flag in flags]) wb = openpyxl.Workbook() prepare_styles(wb) ws = wb.active ws.title = self.tr("README") description = \ """ This Excel file was generated by QGrain ({0}). Please cite: Liu, Y., Liu, X., Sun, Y., 2021. QGrain: An open-source and easy-to-use software for the comprehensive analysis of grain size distributions. Sedimentary Geology 423, 105980. https://doi.org/10.1016/j.sedgeo.2021.105980 It contanins 4 + max(N_components) sheets: 1. The first sheet is the sample distributions of SSU results. 2. The second sheet is used to put the infomation of fitting. 3. The third sheet is the statistic parameters calculated by statistic moment method. 4. The fouth sheet is the distributions of unmixed components and their sum of each sample. 5. Other sheets are the unmixed end-member distributions which were discretely stored. The SSU algorithm is implemented by QGrain. """.format(QGRAIN_VERSION) def write(row, col, value, style="normal_light"): cell = ws.cell(row + 1, col + 1, value=value) cell.style = style lines_of_desc = description.split("\n") for row, line in enumerate(lines_of_desc): write(row, 0, line, style="description") ws.column_dimensions[column_to_char(0)].width = 200 ws = wb.create_sheet(self.tr("Sample Distributions")) write(0, 0, self.tr("Sample Name"), style="header") ws.column_dimensions[column_to_char(0)].width = 16 for col, value in enumerate(classes_μm, 1): write(0, col, value, style="header") ws.column_dimensions[column_to_char(col)].width = 10 for row, result in enumerate(results, 1): if row % 2 == 0: style = "normal_dark" else: style = "normal_light" write(row, 0, result.sample.name, style=style) for col, value in enumerate(result.sample.distribution, 1): write(row, col, value, style=style) QCoreApplication.processEvents() ws = wb.create_sheet(self.tr("Information of Fitting")) write(0, 0, self.tr("Sample Name"), style="header") ws.column_dimensions[column_to_char(0)].width = 16 headers = [ self.tr("Distribution Type"), self.tr("N_components"), self.tr("Resolver"), self.tr("Resolver Settings"), self.tr("Initial Guess"), self.tr("Reference"), self.tr("Spent Time [s]"), self.tr("N_iterations"), self.tr("Final Distance [log10MSE]") ] for col, value in enumerate(headers, 1): write(0, col, value, style="header") if col in (4, 5, 6): ws.column_dimensions[column_to_char(col)].width = 10 else: ws.column_dimensions[column_to_char(col)].width = 10 for row, result in enumerate(results, 1): if row % 2 == 0: style = "normal_dark" else: style = "normal_light" write(row, 0, result.sample.name, style=style) write(row, 1, result.distribution_type.name, style=style) write(row, 2, result.n_components, style=style) write(row, 3, result.task.resolver, style=style) write(row, 4, self.tr("Default") if result.task.resolver_setting is None else result.task.resolver_setting.__str__(), style=style) write(row, 5, self.tr("None") if result.task.initial_guess is None else result.task.initial_guess.__str__(), style=style) write(row, 6, self.tr("None") if result.task.reference is None else result.task.reference.__str__(), style=style) write(row, 7, result.time_spent, style=style) write(row, 8, result.n_iterations, style=style) write(row, 9, result.get_distance("log10MSE"), style=style) ws = wb.create_sheet(self.tr("Statistic Moments")) write(0, 0, self.tr("Sample Name"), style="header") ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=1) ws.column_dimensions[column_to_char(0)].width = 16 headers = [] sub_headers = [ self.tr("Proportion"), self.tr("Mean [φ]"), self.tr("Mean [μm]"), self.tr("STD [φ]"), self.tr("STD [μm]"), self.tr("Skewness"), self.tr("Kurtosis") ] for i in range(max_n_components): write(0, i * len(sub_headers) + 1, self.tr("C{0}").format(i + 1), style="header") ws.merge_cells(start_row=1, start_column=i * len(sub_headers) + 2, end_row=1, end_column=(i + 1) * len(sub_headers) + 1) headers.extend(sub_headers) for col, value in enumerate(headers, 1): write(1, col, value, style="header") ws.column_dimensions[column_to_char(col)].width = 10 flag_index = 0 for row, result in enumerate(results, 2): if row % 2 == 0: style = "normal_light" else: style = "normal_dark" write(row, 0, result.sample.name, style=style) for component in result.components: index = flags[flag_index] write(row, index * len(sub_headers) + 1, component.fraction, style=style) write(row, index * len(sub_headers) + 2, component.logarithmic_moments["mean"], style=style) write(row, index * len(sub_headers) + 3, component.geometric_moments["mean"], style=style) write(row, index * len(sub_headers) + 4, component.logarithmic_moments["std"], style=style) write(row, index * len(sub_headers) + 5, component.geometric_moments["std"], style=style) write(row, index * len(sub_headers) + 6, component.logarithmic_moments["skewness"], style=style) write(row, index * len(sub_headers) + 7, component.logarithmic_moments["kurtosis"], style=style) flag_index += 1 ws = wb.create_sheet(self.tr("Unmixed Components")) ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=2) write(0, 0, self.tr("Sample Name"), style="header") ws.column_dimensions[column_to_char(0)].width = 16 for col, value in enumerate(classes_μm, 2): write(0, col, value, style="header") ws.column_dimensions[column_to_char(col)].width = 10 row = 1 for result_index, result in enumerate(results, 1): if result_index % 2 == 0: style = "normal_dark" else: style = "normal_light" write(row, 0, result.sample.name, style=style) ws.merge_cells(start_row=row + 1, start_column=1, end_row=row + result.n_components + 1, end_column=1) for component_i, component in enumerate(result.components, 1): write(row, 1, self.tr("C{0}").format(component_i), style=style) for col, value in enumerate( component.distribution * component.fraction, 2): write(row, col, value, style=style) row += 1 write(row, 1, self.tr("Sum"), style=style) for col, value in enumerate(result.distribution, 2): write(row, col, value, style=style) row += 1 ws_dict = {} flag_set = set(flags) for flag in flag_set: ws = wb.create_sheet(self.tr("Unmixed EM{0}").format(flag + 1)) write(0, 0, self.tr("Sample Name"), style="header") ws.column_dimensions[column_to_char(0)].width = 16 for col, value in enumerate(classes_μm, 1): write(0, col, value, style="header") ws.column_dimensions[column_to_char(col)].width = 10 ws_dict[flag] = ws flag_index = 0 for row, result in enumerate(results, 1): if row % 2 == 0: style = "normal_dark" else: style = "normal_light" for component in result.components: flag = flags[flag_index] ws = ws_dict[flag] write(row, 0, result.sample.name, style=style) for col, value in enumerate(component.distribution, 1): write(row, col, value, style=style) flag_index += 1 wb.save(filename) wb.close() def on_save_excel_clicked(self, align_components=False): if self.n_results == 0: self.show_warning(self.tr("There is not any SSU result.")) return filename, _ = self.file_dialog.getSaveFileName( None, self.tr("Choose a filename to save SSU Results"), None, "Microsoft Excel (*.xlsx)") if filename is None or filename == "": return try: self.save_excel(filename, align_components) self.show_info( self.tr("SSU results have been saved to:\n {0}").format( filename)) except Exception as e: self.show_error( self. tr("Error raised while save SSU results to Excel file.\n {0}" ).format(e.__str__())) def on_fitting_succeeded(self, result: SSUResult): result_replace_index = self.retry_tasks[result.task.uuid] self.__fitting_results[result_replace_index] = result self.retry_tasks.pop(result.task.uuid) self.retry_progress_msg.setText( self.tr("Tasks to be retried: {0}").format(len(self.retry_tasks))) if len(self.retry_tasks) == 0: self.retry_progress_msg.close() self.logger.debug( f"Retried task succeeded, sample name={result.task.sample.name}, distribution_type={result.task.distribution_type.name}, n_components={result.task.n_components}" ) self.update_page(self.page_index) def on_fitting_failed(self, failed_info: str, task: SSUTask): # necessary to remove it from the dict self.retry_tasks.pop(task.uuid) if len(self.retry_tasks) == 0: self.retry_progress_msg.close() self.show_error( self.tr("Failed to retry task, sample name={0}.\n{1}").format( task.sample.name, failed_info)) self.logger.warning( f"Failed to retry task, sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}" ) def retry_results(self, indexes, results): assert len(indexes) == len(results) if len(results) == 0: return self.retry_progress_msg.setText( self.tr("Tasks to be retried: {0}").format(len(results))) self.retry_timer.start(1) for index, result in zip(indexes, results): query = self.__reference_viewer.query_reference(result.sample) ref_result = None if query is None: nearby_results = self.__fitting_results[ index - 5:index] + self.__fitting_results[index + 1:index + 6] ref_result = self.__reference_viewer.find_similar( result.sample, nearby_results) else: ref_result = query keys = ["mean", "std", "skewness"] # reference = [{key: comp.logarithmic_moments[key] for key in keys} for comp in ref_result.components] task = SSUTask( result.sample, ref_result.distribution_type, ref_result.n_components, resolver=ref_result.task.resolver, resolver_setting=ref_result.task.resolver_setting, # reference=reference) initial_guess=ref_result.last_func_args) self.logger.debug( f"Retry task: sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}" ) self.retry_tasks[task.uuid] = index self.async_worker.execute_task(task) def degrade_results(self): degrade_results = [] # type: list[SSUResult] degrade_indexes = [] # type: list[int] for i, result in enumerate(self.__fitting_results): for component in result.components: if component.fraction < 1e-3: degrade_results.append(result) degrade_indexes.append(i) break self.logger.debug( f"Results should be degrade (have a redundant component): {[result.sample.name for result in degrade_results]}" ) if len(degrade_results) == 0: self.show_info( self.tr("No fitting result was evaluated as an outlier.")) return self.show_info( self. tr("The results below should be degrade (have a redundant component:\n {0}" ).format(", ".join( [result.sample.name for result in degrade_results]))) self.retry_progress_msg.setText( self.tr("Tasks to be retried: {0}").format(len(degrade_results))) self.retry_timer.start(1) for index, result in zip(degrade_indexes, degrade_results): reference = [] n_redundant = 0 for component in result.components: if component.fraction < 1e-3: n_redundant += 1 else: reference.append( dict(mean=component.logarithmic_moments["mean"], std=component.logarithmic_moments["std"], skewness=component.logarithmic_moments["skewness"] )) task = SSUTask( result.sample, result.distribution_type, result.n_components - n_redundant if result.n_components > n_redundant else 1, resolver=result.task.resolver, resolver_setting=result.task.resolver_setting, reference=reference) self.logger.debug( f"Retry task: sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}" ) self.retry_tasks[task.uuid] = index self.async_worker.execute_task(task) def ask_deal_outliers(self, outlier_results: typing.List[SSUResult], outlier_indexes: typing.List[int]): assert len(outlier_indexes) == len(outlier_results) if len(outlier_results) == 0: self.show_info( self.tr("No fitting result was evaluated as an outlier.")) else: if len(outlier_results) > 100: self.outlier_msg.setText( self. tr("The fitting results have the component that its fraction is near zero:\n {0}...(total {1} outliers)\nHow to deal with them?" ).format( ", ".join([ result.sample.name for result in outlier_results[:100] ]), len(outlier_results))) else: self.outlier_msg.setText( self. tr("The fitting results have the component that its fraction is near zero:\n {0}\nHow to deal with them?" ).format(", ".join([ result.sample.name for result in outlier_results ]))) res = self.outlier_msg.exec_() if res == QMessageBox.Discard: self.remove_results(outlier_indexes) elif res == QMessageBox.Retry: self.retry_results(outlier_indexes, outlier_results) else: pass def check_nan_and_inf(self): if self.n_results == 0: self.show_warning(self.tr("There is not any result in the list.")) return outlier_results = [] outlier_indexes = [] for i, result in enumerate(self.__fitting_results): if not result.is_valid: outlier_results.append(result) outlier_indexes.append(i) self.logger.debug( f"Outlier results with the nan or inf value(s): {[result.sample.name for result in outlier_results]}" ) self.ask_deal_outliers(outlier_results, outlier_indexes) def check_final_distances(self): if self.n_results == 0: self.show_warning(self.tr("There is not any result in the list.")) return elif self.n_results < 10: self.show_warning(self.tr("The results in list are too less.")) return distances = [] for result in self.__fitting_results: distances.append(result.get_distance(self.distance_name)) distances = np.array(distances) self.boxplot_chart.show_dataset([distances], xlabels=[self.distance_name], ylabel=self.tr("Distance")) self.boxplot_chart.show() # calculate the 1/4, 1/2, and 3/4 postion value to judge which result is invalid # 1. the mean squared errors are much higher in the results which are lack of components # 2. with the component number getting higher, the mean squared error will get lower and finally reach the minimum median = np.median(distances) upper_group = distances[np.greater(distances, median)] lower_group = distances[np.less(distances, median)] value_1_4 = np.median(lower_group) value_3_4 = np.median(upper_group) distance_QR = value_3_4 - value_1_4 outlier_results = [] outlier_indexes = [] for i, (result, distance) in enumerate(zip(self.__fitting_results, distances)): if distance > value_3_4 + distance_QR * 1.5: # which error too small is not outlier # if distance > value_3_4 + distance_QR * 1.5 or distance < value_1_4 - distance_QR * 1.5: outlier_results.append(result) outlier_indexes.append(i) self.logger.debug( f"Outlier results with too greater distances: {[result.sample.name for result in outlier_results]}" ) self.ask_deal_outliers(outlier_results, outlier_indexes) def check_component_moments(self, key: str): if self.n_results == 0: self.show_warning(self.tr("There is not any result in the list.")) return elif self.n_results < 10: self.show_warning(self.tr("The results in list are too less.")) return max_n_components = 0 for result in self.__fitting_results: if result.n_components > max_n_components: max_n_components = result.n_components moments = [] for i in range(max_n_components): moments.append([]) for result in self.__fitting_results: for i, component in enumerate(result.components): if np.isnan(component.logarithmic_moments[key]) or np.isinf( component.logarithmic_moments[key]): pass else: moments[i].append(component.logarithmic_moments[key]) # key_trans = {"mean": self.tr("Mean"), "std": self.tr("STD"), "skewness": self.tr("Skewness"), "kurtosis": self.tr("Kurtosis")} key_label_trans = { "mean": self.tr("Mean [φ]"), "std": self.tr("STD [φ]"), "skewness": self.tr("Skewness"), "kurtosis": self.tr("Kurtosis") } self.boxplot_chart.show_dataset( moments, xlabels=[f"C{i+1}" for i in range(max_n_components)], ylabel=key_label_trans[key]) self.boxplot_chart.show() outlier_dict = {} for i in range(max_n_components): stacked_moments = np.array(moments[i]) # calculate the 1/4, 1/2, and 3/4 postion value to judge which result is invalid # 1. the mean squared errors are much higher in the results which are lack of components # 2. with the component number getting higher, the mean squared error will get lower and finally reach the minimum median = np.median(stacked_moments) upper_group = stacked_moments[np.greater(stacked_moments, median)] lower_group = stacked_moments[np.less(stacked_moments, median)] value_1_4 = np.median(lower_group) value_3_4 = np.median(upper_group) distance_QR = value_3_4 - value_1_4 for j, result in enumerate(self.__fitting_results): if result.n_components > i: distance = result.components[i].logarithmic_moments[key] if distance > value_3_4 + distance_QR * 1.5 or distance < value_1_4 - distance_QR * 1.5: outlier_dict[j] = result outlier_results = [] outlier_indexes = [] for index, result in sorted(outlier_dict.items(), key=lambda x: x[0]): outlier_indexes.append(index) outlier_results.append(result) self.logger.debug( f"Outlier results with abnormal {key} values of their components: {[result.sample.name for result in outlier_results]}" ) self.ask_deal_outliers(outlier_results, outlier_indexes) def check_component_fractions(self): outlier_results = [] outlier_indexes = [] for i, result in enumerate(self.__fitting_results): for component in result.components: if component.fraction < 1e-3: outlier_results.append(result) outlier_indexes.append(i) break self.logger.debug( f"Outlier results with the component that its fraction is near zero: {[result.sample.name for result in outlier_results]}" ) self.ask_deal_outliers(outlier_results, outlier_indexes) def try_align_components(self): if self.n_results == 0: self.show_warning(self.tr("There is not any result in the list.")) return elif self.n_results < 10: self.show_warning(self.tr("The results in list are too less.")) return import matplotlib.pyplot as plt n_components_list = [ result.n_components for result in self.__fitting_results ] count_dict = Counter(n_components_list) max_n_components = max(count_dict.keys()) self.logger.debug( f"N_components: {count_dict}, Max N_components: {max_n_components}" ) n_components_desc = "\n".join([ self.tr("{0} Component(s): {1}").format(n_components, count) for n_components, count in count_dict.items() ]) self.show_info( self.tr("N_components distribution of Results:\n{0}").format( n_components_desc)) x = self.__fitting_results[0].classes_μm stacked_components = [] for result in self.__fitting_results: for component in result.components: stacked_components.append(component.distribution) stacked_components = np.array(stacked_components) cluser = KMeans(n_clusters=max_n_components) flags = cluser.fit_predict(stacked_components) figure = plt.figure(figsize=(6, 4)) cmap = plt.get_cmap("tab10") axes = figure.add_subplot(1, 1, 1) for flag, distribution in zip(flags, stacked_components): plt.plot(x, distribution, c=cmap(flag), zorder=flag) axes.set_xscale("log") axes.set_xlabel(self.tr("Grain-size [μm]")) axes.set_ylabel(self.tr("Frequency")) figure.tight_layout() figure.show() outlier_results = [] outlier_indexes = [] flag_index = 0 for i, result in enumerate(self.__fitting_results): result_flags = set() for component in result.components: if flags[flag_index] in result_flags: outlier_results.append(result) outlier_indexes.append(i) break else: result_flags.add(flags[flag_index]) flag_index += 1 self.logger.debug( f"Outlier results that have two components in the same cluster: {[result.sample.name for result in outlier_results]}" ) self.ask_deal_outliers(outlier_results, outlier_indexes) def analyse_typical_components(self): if self.n_results == 0: self.show_warning(self.tr("There is not any result in the list.")) return elif self.n_results < 10: self.show_warning(self.tr("The results in list are too less.")) return self.typical_chart.show_typical(self.__fitting_results) self.typical_chart.show()