From 7203903e5ebed6f8460ca814c05498b6c0f6f401 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Fri, 5 Nov 2021 15:10:55 -0500 Subject: [PATCH] Add ability to load crystal parameters from grains In the calibration crystal editor (found in both the Laue overlay editor and the Rotation Series overlay editor), there is now a "Load Crystal Parameters" button at the bottom, that allows the user to load crystal parameters from grains. It gives the user the choice to display grains from the fit grains output, find orientations output, or from a file. The user may select a grain in the table and click "OK", and the crystal parameters will be populated with the parameters from the selected grain. Signed-off-by: Patrick Avery --- hexrd/ui/calibration_crystal_editor.py | 14 ++ hexrd/ui/hexrd_config.py | 2 + .../ui/indexing/fit_grains_options_dialog.py | 1 - .../ui/indexing/fit_grains_results_dialog.py | 1 - hexrd/ui/indexing/grains_table_view.py | 55 +++- hexrd/ui/indexing/run.py | 15 +- .../ui/calibration_crystal_editor.ui | 9 +- hexrd/ui/resources/ui/laue_overlay_editor.ui | 4 +- .../ui/rotation_series_overlay_editor.ui | 4 +- hexrd/ui/resources/ui/select_grains_dialog.ui | 131 ++++++++++ hexrd/ui/select_grains_dialog.py | 236 ++++++++++++++++++ 11 files changed, 451 insertions(+), 21 deletions(-) create mode 100644 hexrd/ui/resources/ui/select_grains_dialog.ui create mode 100644 hexrd/ui/select_grains_dialog.py diff --git a/hexrd/ui/calibration_crystal_editor.py b/hexrd/ui/calibration_crystal_editor.py index 961ea14cf..8c29f4d31 100644 --- a/hexrd/ui/calibration_crystal_editor.py +++ b/hexrd/ui/calibration_crystal_editor.py @@ -8,6 +8,7 @@ from hexrd.ui.constants import DEFAULT_CRYSTAL_REFINEMENTS from hexrd.ui.hexrd_config import HexrdConfig +from hexrd.ui.select_grains_dialog import SelectGrainsDialog from hexrd.ui.select_items_widget import SelectItemsWidget from hexrd.ui.ui_loader import UiLoader from hexrd.ui.utils import convert_angle_convention @@ -61,6 +62,8 @@ def setup_connections(self): self.refinements_selector.selection_changed.connect( self.refinements_edited) + self.ui.load.clicked.connect(self.load) + @property def params(self): return self._params @@ -262,3 +265,14 @@ def update_tab_gui(self): o_values = [x.value() for x in self.orientation_widgets] p_values = [x.value() for x in self.position_widgets] self.slider_widget.update_gui(o_values, p_values) + + def load(self): + dialog = SelectGrainsDialog(self.ui) + if not dialog.exec_(): + return + + self.load_from_grain(dialog.selected_grain) + + def load_from_grain(self, grain): + self.params = grain[3:15] + self.params_modified.emit() diff --git a/hexrd/ui/hexrd_config.py b/hexrd/ui/hexrd_config.py index d3aa9ee81..71fa51ca1 100644 --- a/hexrd/ui/hexrd_config.py +++ b/hexrd/ui/hexrd_config.py @@ -201,6 +201,8 @@ def __init__(self): self.logging_stderr_handler = None self.loading_state = False self.last_loaded_state_file = None + self.find_orientations_grains_table = None + self.fit_grains_grains_table = None self.setup_logging() diff --git a/hexrd/ui/indexing/fit_grains_options_dialog.py b/hexrd/ui/indexing/fit_grains_options_dialog.py index 9e2dc57e2..e984c3b8e 100644 --- a/hexrd/ui/indexing/fit_grains_options_dialog.py +++ b/hexrd/ui/indexing/fit_grains_options_dialog.py @@ -42,7 +42,6 @@ def __init__(self, grains_table, parent=None): view = self.ui.grains_table_view view.data_model = self.data_model view.material = self.material - view.grains_table = grains_table ok_button = self.ui.button_box.button(QDialogButtonBox.Ok) ok_button.setText('Fit Grains') diff --git a/hexrd/ui/indexing/fit_grains_results_dialog.py b/hexrd/ui/indexing/fit_grains_results_dialog.py index aef800e5d..5432473b0 100644 --- a/hexrd/ui/indexing/fit_grains_results_dialog.py +++ b/hexrd/ui/indexing/fit_grains_results_dialog.py @@ -466,7 +466,6 @@ def setup_tableview(self): # Update the variables on the table view view.data_model = self.data_model view.material = self.material - view.grains_table = self.data def show(self): self.ui.show() diff --git a/hexrd/ui/indexing/grains_table_view.py b/hexrd/ui/indexing/grains_table_view.py index 260df4692..99551a7e4 100644 --- a/hexrd/ui/indexing/grains_table_view.py +++ b/hexrd/ui/indexing/grains_table_view.py @@ -1,6 +1,6 @@ import numpy as np -from PySide2.QtCore import QSortFilterProxyModel, Qt +from PySide2.QtCore import QSortFilterProxyModel, Qt, Signal from PySide2.QtGui import QCursor from PySide2.QtWidgets import QMenu, QTableView @@ -19,11 +19,14 @@ class GrainsTableView(QTableView): + + selection_changed = Signal() + def __init__(self, parent=None): super().__init__(parent) self.material = None - self.grains_table = None + self.pull_spots_allowed = True self._data_model = None self._tolerances = [] self.selected_tol_id = -1 @@ -58,32 +61,51 @@ def add_actions(d): return super().contextMenuEvent(event) + @property + def grains_table(self): + if not self.source_model: + return None + + return self.source_model.full_grains_table + @property def proxy_model(self): return self.model() @property - def results_model(self): + def source_model(self): + if not self.proxy_model: + return None return self.proxy_model.sourceModel() @property def selected_rows(self): + if not self.selectionModel(): + return [] return self.selectionModel().selectedRows() @property def selected_grain_ids(self): - rows = self.selectionModel().selectedRows() # Map these rows through the proxy in case of sorting - rows = [self.proxy_model.mapToSource(x) for x in rows] - return [int(self.results_model.data(x)) for x in rows] + rows = [self.proxy_model.mapToSource(x) for x in self.selected_rows] + return [int(self.source_model.data(x)) for x in rows] + + @property + def selected_grains(self): + grain_ids = self.selected_grain_ids + if not grain_ids or self.grains_table is None: + return None + + return self.grains_table[grain_ids] @property def can_run_pull_spots(self): - return all(( - self.selected_grain_ids, - self.material is not None, - self.grains_table is not None, - )) + return ( + self.pull_spots_allowed and + self.selected_grain_ids and + self.material is not None and + self.grains_table is not None + ) @property def tolerances(self): @@ -214,8 +236,16 @@ def data_model(self): @data_model.setter def data_model(self, v): self._data_model = v + if v is None: + self.setModel(None) + return + self.setup_proxy() + # A new selection model is created each time a new data model is set. + self.selectionModel().selectionChanged.connect( + self.on_selection_changed) + def setup_proxy(self): # Subclass QSortFilterProxyModel to restrict sorting by column @@ -237,6 +267,9 @@ def sort(self, column, order): self.sortByColumn(0, Qt.AscendingOrder) self.horizontalHeader().setSortIndicatorShown(False) + def on_selection_changed(self): + return self.selection_changed.emit() + def on_sort_indicator_changed(self, index, order): """Shows sort indicator for sortable columns, hides for all others.""" horizontal_header = self.horizontalHeader() diff --git a/hexrd/ui/indexing/run.py b/hexrd/ui/indexing/run.py index cf66fea9e..112434028 100644 --- a/hexrd/ui/indexing/run.py +++ b/hexrd/ui/indexing/run.py @@ -1,3 +1,5 @@ +import copy + import numpy as np from PySide2.QtCore import QObject, QThreadPool, Qt, Signal @@ -299,6 +301,8 @@ def generate_grains_table(self): print(msg) self.grains_table = generate_grains_table(self.qbar) + HexrdConfig().find_orientations_grains_table = copy.deepcopy( + self.grains_table) def start_fit_grains_runner(self): # We will automatically start fit grains after the indexing @@ -396,7 +400,10 @@ def run_fit_grains(self): 'write_spots_files': write_spots, } self.fit_grains_results = fit_grains(**kwargs) + self.result_grains_table = create_grains_table(self.fit_grains_results) print('Fit Grains Complete') + HexrdConfig().fit_grains_grains_table = copy.deepcopy( + self.result_grains_table) # If we wrote out the spots, let's write out the grains.out file too if write_spots: @@ -412,7 +419,7 @@ def view_fit_grains_results(self): print(result) kwargs = { - 'fit_grains_results': self.fit_grains_results, + 'grains_table': self.result_grains_table, 'parent': self.parent, } dialog = create_fit_grains_results_dialog(**kwargs) @@ -420,8 +427,7 @@ def view_fit_grains_results(self): dialog.show_later() -def create_fit_grains_results_dialog(fit_grains_results, parent=None): - # Build grains table +def create_grains_table(fit_grains_results): num_grains = len(fit_grains_results) shape = (num_grains, 21) grains_table = np.empty(shape) @@ -429,7 +435,10 @@ def create_fit_grains_results_dialog(fit_grains_results, parent=None): for result in fit_grains_results: gw.dump_grain(*result) gw.close() + return grains_table + +def create_fit_grains_results_dialog(grains_table, parent=None): # Use the material to compute stress from strain indexing_config = HexrdConfig().indexing_config name = indexing_config.get('_selected_material') diff --git a/hexrd/ui/resources/ui/calibration_crystal_editor.ui b/hexrd/ui/resources/ui/calibration_crystal_editor.ui index 141e03ddf..171ecf54c 100644 --- a/hexrd/ui/resources/ui/calibration_crystal_editor.ui +++ b/hexrd/ui/resources/ui/calibration_crystal_editor.ui @@ -7,7 +7,7 @@ 0 0 767 - 385 + 398 @@ -525,6 +525,13 @@ + + + + Load Crystal Parameters + + + diff --git a/hexrd/ui/resources/ui/laue_overlay_editor.ui b/hexrd/ui/resources/ui/laue_overlay_editor.ui index 91eff9791..1a7ecadff 100644 --- a/hexrd/ui/resources/ui/laue_overlay_editor.ui +++ b/hexrd/ui/resources/ui/laue_overlay_editor.ui @@ -7,7 +7,7 @@ 0 0 550 - 565 + 610 @@ -19,7 +19,7 @@ 0 - 565 + 610 diff --git a/hexrd/ui/resources/ui/rotation_series_overlay_editor.ui b/hexrd/ui/resources/ui/rotation_series_overlay_editor.ui index d0c7b91e7..0aa6c1973 100644 --- a/hexrd/ui/resources/ui/rotation_series_overlay_editor.ui +++ b/hexrd/ui/resources/ui/rotation_series_overlay_editor.ui @@ -7,13 +7,13 @@ 0 0 550 - 680 + 710 0 - 680 + 710 diff --git a/hexrd/ui/resources/ui/select_grains_dialog.ui b/hexrd/ui/resources/ui/select_grains_dialog.ui new file mode 100644 index 000000000..c2007ec5e --- /dev/null +++ b/hexrd/ui/resources/ui/select_grains_dialog.ui @@ -0,0 +1,131 @@ + + + grains_select_dialog + + + + 0 + 0 + 635 + 372 + + + + Select Grains + + + + + + From: + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + + + 0 + + + + Find Orientations Output + + + + + + Fit Grains Output + + + + + + File + + + + + + false + + + + + + + Select File + + + + + + + + + + + + + + + GrainsTableView + QTableView +
grains_table_view.py
+
+
+ + method + tab_widget + table_view + file_name + select_file_button + + + + + button_box + accepted() + grains_select_dialog + accept() + + + 217 + 205 + + + 217 + 113 + + + + + button_box + rejected() + grains_select_dialog + reject() + + + 217 + 205 + + + 217 + 113 + + + + +
diff --git a/hexrd/ui/select_grains_dialog.py b/hexrd/ui/select_grains_dialog.py new file mode 100644 index 000000000..307444666 --- /dev/null +++ b/hexrd/ui/select_grains_dialog.py @@ -0,0 +1,236 @@ +import os + +import numpy as np + +from PySide2.QtCore import Signal, QItemSelectionModel, QObject, Qt +from PySide2.QtWidgets import QDialogButtonBox, QFileDialog, QMessageBox + +from hexrd.ui.hexrd_config import HexrdConfig +from hexrd.ui.indexing.grains_table_model import GrainsTableModel +from hexrd.ui.ui_loader import UiLoader + + +class SelectGrainsDialog(QObject): + + accepted = Signal() + rejected = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.ignore_errors = False + + loader = UiLoader() + self.ui = loader.load_file('select_grains_dialog.ui', parent) + + # Hide the tab bar. It gets selected by changes to the combo box. + self.ui.tab_widget.tabBar().hide() + + # pull_spots is not allowed with this grains table view + self.ui.table_view.pull_spots_allowed = False + + self.setup_methods() + self.update_gui() + + self.ignore_errors = True + try: + self.update_grains_table() + finally: + self.ignore_errors = False + + self.setup_connections() + + def setup_connections(self): + self.ui.select_file_button.pressed.connect(self.select_file) + self.ui.file_name.editingFinished.connect(self.file_name_changed) + self.ui.method.currentIndexChanged.connect(self.method_index_changed) + self.ui.accepted.connect(self.on_accepted) + self.ui.rejected.connect(self.on_rejected) + self.ui.table_view.selection_changed.connect(self.update_enable_states) + + @property + def find_orientations_grains_table(self): + return HexrdConfig().find_orientations_grains_table + + @property + def fit_grains_grains_table(self): + return HexrdConfig().fit_grains_grains_table + + def setup_methods(self): + fo_grains_table = self.find_orientations_grains_table + fg_grains_table = self.fit_grains_grains_table + methods_and_enable = { + 'fit_grains_output': fg_grains_table is not None, + 'find_orientations_output': fo_grains_table is not None, + 'file': True, + } + + methods = list(methods_and_enable.keys()) + self.ui.method.addItems([name_to_label(x) for x in methods]) + + # FIXME: it'd be nice to eventually put this QComboBox + # item disabling in a utility function. + model = self.ui.method.model() + for i, method in enumerate(methods): + enabled = methods_and_enable[method] + if enabled: + # Already enabled... + continue + + item = model.item(i) + item.setFlags(item.flags() & ~Qt.ItemIsEnabled) + + # Set the combo box to the first enabled entry + for i, method in enumerate(methods): + enabled = methods_and_enable[method] + if enabled: + self.ui.method.setCurrentIndex(i) + break + + def exec_(self): + return self.ui.exec_() + + def on_accepted(self): + self.update_config() + self.accepted.emit() + + def on_rejected(self): + self.rejected.emit() + + def select_file(self): + selected_file, selected_filter = QFileDialog.getOpenFileName( + self.ui, 'grains.out', HexrdConfig().working_dir, + 'Grains.out files (*.out)') + + if selected_file: + HexrdConfig().working_dir = os.path.dirname(selected_file) + self.file_name = selected_file + + @property + def file_name(self): + return self.ui.file_name.text() + + @file_name.setter + def file_name(self, v): + self.ui.file_name.setText(v) + self.file_name_changed() + + def file_name_changed(self): + file_name = self.file_name + if not file_name: + self.grains_table = None + return + + # Try to load the file + try: + self.grains_table = np.loadtxt(file_name, ndmin=2) + except Exception as e: + if not self.ignore_errors: + msg = f'Failed to load "{file_name}". Error was:\n\n{e}' + QMessageBox.critical(self.ui, 'HEXRD', msg) + self.grains_table = None + + def load_fo_grains_table(self): + self.grains_table = self.find_orientations_grains_table + + def load_fg_grains_table(self): + self.grains_table = self.fit_grains_grains_table + + @property + def grains_table(self): + return self.ui.table_view.grains_table + + @grains_table.setter + def grains_table(self, v): + # We make a new GrainsTableModel each time for now to save + # dev time, since the model wasn't designed to be mutable. + # FIXME: in the future, make GrainsTableModel grains mutable, + # and then just set the grains table on it, rather than + # creating a new one every time. + view = self.ui.table_view + if v is None: + view.data_model = None + self.update_enable_states() + return + + kwargs = { + 'grains_table': v, + 'excluded_columns': list(range(9, 15)), + 'parent': view, + } + view.data_model = GrainsTableModel(**kwargs) + + # If there is only one row, select it automatically + if len(v) == 1: + selection_model = view.selectionModel() + model_index = selection_model.model().index(0, 0) + command = QItemSelectionModel.Select | QItemSelectionModel.Rows + selection_model.select(model_index, command) + + def update_grains_table(self): + functions = { + 'find_orientations_output': self.load_fo_grains_table, + 'fit_grains_output': self.load_fg_grains_table, + 'file': self.file_name_changed, + } + if self.method_name not in functions: + raise NotImplementedError(self.method_name) + + functions[self.method_name]() + + @property + def selected_grain(self): + grains = self.ui.table_view.selected_grains + if grains is None or len(grains) == 0: + return + + # Should only be one + if len(grains) > 1: + raise Exception('Only one grain may be selected') + + return grains[0] + + def update_gui(self): + indexing_config = HexrdConfig().indexing_config + key = '_loaded_crystal_params_file' + self.file_name = indexing_config.get(key, '') + + self.update_method_tab() + self.update_enable_states() + + def update_config(self): + indexing_config = HexrdConfig().indexing_config + indexing_config['_loaded_crystal_params_file'] = self.file_name + + def update_enable_states(self): + button_box = self.ui.button_box + ok_button = button_box.button(QDialogButtonBox.Ok) + if ok_button: + ok_button.setEnabled(self.selected_grain is not None) + + @property + def method_name(self): + return label_to_name(self.ui.method.currentText()) + + @method_name.setter + def method_name(self, v): + self.ui.method.setCurrentText(name_to_label(v)) + + def method_index_changed(self): + self.update_method_tab() + self.update_grains_table() + + def update_method_tab(self): + # Take advantage of the naming scheme... + method_tab = getattr(self.ui, self.method_name + '_tab') + self.ui.tab_widget.setCurrentWidget(method_tab) + + visible = self.method_name == 'file' + self.ui.tab_widget.setVisible(visible) + + +def name_to_label(s): + return ' '.join(x.capitalize() for x in s.split('_')) + + +def label_to_name(s): + return '_'.join(x.lower() for x in s.split())