From ba581be3112d352813ad9c48b0b0c6ac4598634d Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 12:21:38 -0600 Subject: [PATCH 01/15] Use `footprint_rectangle` from skimage The old `rectangle` function is deprecated, and `footprint_rectangle` is necessary. Signed-off-by: Patrick Avery --- hexrdgui/calibration/polarview.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hexrdgui/calibration/polarview.py b/hexrdgui/calibration/polarview.py index cadbec2f3..84287a972 100644 --- a/hexrdgui/calibration/polarview.py +++ b/hexrdgui/calibration/polarview.py @@ -1,7 +1,7 @@ import numpy as np from skimage.filters.edges import binary_erosion -from skimage.morphology import rectangle +from skimage.morphology import footprint_rectangle from skimage.transform import warp from hexrd.transforms.xfcapi import mapAngle @@ -428,10 +428,10 @@ def apply_snip(self, img): # NOT done inside snip computation! if HexrdConfig().polar_apply_erosion: niter = HexrdConfig().polar_snip1d_numiter - structure = rectangle( + structure = footprint_rectangle(( 1, int(np.ceil(2.25*niter*snip_width_pixels())) - ) + )) mask = binary_erosion(~self.raw_img.mask, structure) img[~mask] = np.nan From b3dd0fe20da85479148314591aa652adfd597d96 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 12:22:32 -0600 Subject: [PATCH 02/15] Use sequence to set data of vlines/hlines A sequence is now required in the newer versions of matplotlib... Signed-off-by: Patrick Avery --- hexrdgui/zoom_canvas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrdgui/zoom_canvas.py b/hexrdgui/zoom_canvas.py index f1dad10fc..dbd1dee38 100644 --- a/hexrdgui/zoom_canvas.py +++ b/hexrdgui/zoom_canvas.py @@ -204,8 +204,8 @@ def update_vhlines(self, event): return vline, hline = self.vhlines - vline.set_xdata(event.xdata) - hline.set_ydata(event.ydata) + vline.set_xdata([event.xdata]) + hline.set_ydata([event.ydata]) def main_canvas_mouse_moved(self, event): if self.disabled: From 849da65b3e0b742f4de03fa7ca8f6cba12d00a5d Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 13:01:01 -0600 Subject: [PATCH 03/15] Reset overlay picks on detector change We should not keep the same calibration picks on the overlays if the detectors are changed. Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_runner.py | 3 +++ hexrdgui/hexrd_config.py | 4 ++++ hexrdgui/main_window.py | 1 + hexrdgui/overlays/overlay.py | 8 -------- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/hexrdgui/calibration/calibration_runner.py b/hexrdgui/calibration/calibration_runner.py index 49f8a0483..fbbbe27dc 100644 --- a/hexrdgui/calibration/calibration_runner.py +++ b/hexrdgui/calibration/calibration_runner.py @@ -87,6 +87,7 @@ def enable_focus_mode(self, b): def clear_all_overlay_picks(self): for overlay in self.active_overlays: overlay.reset_calibration_picks() + overlay.pad_picks_data() @property def overlays(self): @@ -152,6 +153,7 @@ def use_current_pick_points(self): def hand_pick_points(self): overlay = self.active_overlay overlay.reset_calibration_picks() + overlay.pad_picks_data() title = overlay.name @@ -805,6 +807,7 @@ def auto_pick_points(self): raise NotImplementedError(overlay.type) overlay.reset_calibration_picks() + overlay.pad_picks_data() return funcs[overlay.type]() def auto_pick_powder_points(self): diff --git a/hexrdgui/hexrd_config.py b/hexrdgui/hexrd_config.py index 409064e5b..e377bc091 100644 --- a/hexrdgui/hexrd_config.py +++ b/hexrdgui/hexrd_config.py @@ -2215,6 +2215,10 @@ def clear_overlay_data(self): for overlay in self.overlays: overlay.update_needed = True + def reset_overlay_calibration_picks(self): + for overlay in self.overlays: + overlay.reset_calibration_picks() + def flag_overlay_updates_for_active_material(self): self.flag_overlay_updates_for_material(self.active_material_name) diff --git a/hexrdgui/main_window.py b/hexrdgui/main_window.py index 951d5bfb9..b4afae4c0 100644 --- a/hexrdgui/main_window.py +++ b/hexrdgui/main_window.py @@ -507,6 +507,7 @@ def open_grain_fitting_results(self): self._fit_grains_results_dialog = dialog def on_detectors_changed(self): + HexrdConfig().reset_overlay_calibration_picks() HexrdConfig().clear_overlay_data() HexrdConfig().current_imageseries_idx = 0 self.load_dummy_images() diff --git a/hexrdgui/overlays/overlay.py b/hexrdgui/overlays/overlay.py index 89a2db3da..927874dcd 100644 --- a/hexrdgui/overlays/overlay.py +++ b/hexrdgui/overlays/overlay.py @@ -385,14 +385,6 @@ def reset_calibration_picks(self): # Make an empty list for each detector self._calibration_picks.clear() - if self.display_mode != ViewType.cartesian: - # Cartesian uses a fake detector, so we don't want to - # use that for the calibration picks. - # But all other display modes should work. - self._calibration_picks |= {k: {} for k in self.data} - - self.pad_picks_data() - def pad_picks_data(self): # Subclasses only need to override this if they actually need it pass From f48a7074452002d99895e17fdf9afe8ec085dfea Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 13:21:16 -0600 Subject: [PATCH 04/15] Verify detectors for engineering constraints If any required detectors are missing, we should not auto-set it. Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_dialog.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/hexrdgui/calibration/calibration_dialog.py b/hexrdgui/calibration/calibration_dialog.py index 6fa6dc6e4..36cc08552 100644 --- a/hexrdgui/calibration/calibration_dialog.py +++ b/hexrdgui/calibration/calibration_dialog.py @@ -790,11 +790,10 @@ def guess_engineering_constraints(instr) -> str | None: # First guess the instrument type. instr_type = guess_instrument_type(instr.detectors) - # If it matches one of our expected engineering constraints, use it. - expected_options = [ - 'TARDIS', - ] - if instr_type in expected_options: - return instr_type + if instr_type == 'TARDIS': + # Make sure it contains both image plates + required_detectors = ['IMAGE-PLATE-2', 'IMAGE-PLATE-4'] + if all(x in instr.detectors for x in required_detectors): + return instr_type return None From 0c58a99b4bd9f31751cec9072bcd52ce1569f813 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 13:26:06 -0600 Subject: [PATCH 05/15] Ensure overlay is not None before accessing... This follows the same pattern we used for the other setters. Signed-off-by: Patrick Avery --- hexrdgui/powder_overlay_editor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hexrdgui/powder_overlay_editor.py b/hexrdgui/powder_overlay_editor.py index 536972375..6a22ba2ef 100644 --- a/hexrdgui/powder_overlay_editor.py +++ b/hexrdgui/powder_overlay_editor.py @@ -375,6 +375,9 @@ def xray_source_config(self) -> str | None: @xray_source_config.setter def xray_source_config(self, v: str | None): + if self.overlay is None: + return + if v is not None and not HexrdConfig().has_multi_xrs: raise Exception(v) From 693c06ae5ac4211228b4e714a1920e4e23f79946 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 13:54:28 -0600 Subject: [PATCH 06/15] Don't reset parameters after calibration We were accidentally resetting the calibration parameters if the relative constraints were set to None. Don't do that, and just make sure that the instrument constraints are reset... Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_dialog_callbacks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hexrdgui/calibration/calibration_dialog_callbacks.py b/hexrdgui/calibration/calibration_dialog_callbacks.py index 8d9b84783..657f9e16d 100644 --- a/hexrdgui/calibration/calibration_dialog_callbacks.py +++ b/hexrdgui/calibration/calibration_dialog_callbacks.py @@ -190,9 +190,9 @@ def on_reset_relative_params_to_zero_clicked(self): def save_constraint_params(self): constraints = self.calibrator.relative_constraints - if constraints.type != RelativeConstraintsType.system: - # Instead of saving, reset them - self.reset_saved_constraint_params() + if constraints.type == RelativeConstraintsType.none: + # Nothing to save... Just make sure the old one is cleared. + HexrdConfig()._instrument_rigid_body_params.clear() return HexrdConfig()._instrument_rigid_body_params = copy.deepcopy( From b221dfa0313f2ca4c092dc64780ff0f72f770b1f Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 8 Jan 2025 17:11:36 -0600 Subject: [PATCH 07/15] Allow multiple picks when inserting picks We'll need to add this to the structureless calibration too soon... Signed-off-by: Patrick Avery --- .../calibration/hkl_picks_tree_view_dialog.py | 6 ++- hexrdgui/tree_views/hkl_picks_tree_view.py | 52 ++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/hexrdgui/calibration/hkl_picks_tree_view_dialog.py b/hexrdgui/calibration/hkl_picks_tree_view_dialog.py index f81750c3c..67c4fdbcd 100644 --- a/hexrdgui/calibration/hkl_picks_tree_view_dialog.py +++ b/hexrdgui/calibration/hkl_picks_tree_view_dialog.py @@ -50,7 +50,7 @@ def dictionary(self, v): def setup_connections(self): # Use accepted/rejected so these are called before on_finished() - self.ui.accepted.connect(self.on_finished) + self.ui.accepted.connect(self.on_accepted) self.ui.rejected.connect(self.on_finished) self.ui.export_picks.clicked.connect(self.export_picks_clicked) @@ -64,6 +64,10 @@ def update_gui(self): self.ui.show_overlays.setChecked(HexrdConfig().show_overlays) self.ui.show_all_picks.setChecked(self.tree_view.show_all_picks) + def on_accepted(self): + self.tree_view.on_accepted() + self.on_finished() + def on_finished(self): self.tree_view.clear_artists() self.tree_view.clear_highlights() diff --git a/hexrdgui/tree_views/hkl_picks_tree_view.py b/hexrdgui/tree_views/hkl_picks_tree_view.py index 93e6692ed..cb24da60b 100644 --- a/hexrdgui/tree_views/hkl_picks_tree_view.py +++ b/hexrdgui/tree_views/hkl_picks_tree_view.py @@ -4,6 +4,7 @@ from PySide6.QtWidgets import QMenu from hexrdgui.hexrd_config import HexrdConfig +from hexrdgui.line_picker_dialog import LinePickerDialog from hexrdgui.overlays import Overlay from hexrdgui.tree_views.base_dict_tree_item_model import BaseTreeItemModel from hexrdgui.tree_views.generic_picks_tree_view import ( @@ -120,16 +121,51 @@ def insert_item(): else: raise NotImplementedError - new_item = TreeItem([position, 0., 0.]) - model.insert_items([new_item], parent_item, position) + if not is_hand_pickable: + new_item = TreeItem([position, 0., 0.]) + model.insert_items([new_item], parent_item, position) - # Select the new item - index = model.createIndex(new_item.row(), 0, new_item) - self.setCurrentIndex(index) + # Select the new item + index = model.createIndex(new_item.row(), 0, new_item) + self.setCurrentIndex(index) + return - if is_hand_pickable: - # Go ahead and get the user to hand pick the point... - self.hand_pick_point(new_item, hkl_str) + pick_label = f'Inserting points into: {hkl_str}' + kwargs = { + 'canvas': self.canvas, + 'parent': self, + } + + picker = LinePickerDialog(**kwargs) + picker.current_pick_label = pick_label + picker.ui.setWindowTitle(pick_label) + picker.ui.view_picks.setVisible(False) + picker.start() + + def on_line_completed(): + # Just accept it + picker.ui.accept() + + def on_accepted(): + nonlocal position + original_position = position + new_line = picker.line_data[0] + new_items = [] + for x, y in new_line.tolist(): + new_items.append(TreeItem([position, x, y])) + position += 1 + + model.insert_items(new_items, parent_item, original_position) + + # Select the last new item + last_item = new_items[-1] + index = model.createIndex(last_item.row(), 0, last_item) + self.setCurrentIndex(index) + + picker.accepted.connect(on_accepted) + picker.line_completed.connect(on_line_completed) + + self._current_picker = picker def hand_pick_item(): self.hand_pick_point(item, hkl_str) From bd370bb1c59b181891e8a831ea993bc4d4fb9d02 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 9 Jan 2025 15:10:02 -0600 Subject: [PATCH 08/15] Insert multiple picks for structureless too Signed-off-by: Patrick Avery --- .../tree_views/generic_picks_tree_view.py | 60 +++++++++++++++---- hexrdgui/tree_views/hkl_picks_tree_view.py | 45 +------------- 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/hexrdgui/tree_views/generic_picks_tree_view.py b/hexrdgui/tree_views/generic_picks_tree_view.py index ceea2468b..cfe05cea6 100644 --- a/hexrdgui/tree_views/generic_picks_tree_view.py +++ b/hexrdgui/tree_views/generic_picks_tree_view.py @@ -433,16 +433,8 @@ def insert_item(): else: raise NotImplementedError - new_item = TreeItem([position, 0., 0.]) - model.insert_items([new_item], parent_item, position) - - # Select the new item - index = model.createIndex(new_item.row(), 0, new_item) - self.setCurrentIndex(index) - - if is_hand_pickable: - # Go ahead and get the user to hand pick the point... - self.hand_pick_point(new_item, line_name) + pick_label = f'Inserting points into: {line_name}' + return self._insert_picks(parent_item, position, pick_label) def hand_pick_item(): self.hand_pick_point(item, line_name) @@ -475,6 +467,54 @@ def hand_pick_item(): # Run the function for the action that was chosen actions[action_chosen]() + def _insert_picks(self, parent_item, position, pick_label): + model = self.model() + + if not self.is_hand_pickable: + new_item = TreeItem([position, 0., 0.]) + model.insert_items([new_item], parent_item, position) + + # Select the new item + index = model.createIndex(new_item.row(), 0, new_item) + self.setCurrentIndex(index) + return + + kwargs = { + 'canvas': self.canvas, + 'parent': self, + } + + picker = LinePickerDialog(**kwargs) + picker.current_pick_label = pick_label + picker.ui.setWindowTitle(pick_label) + picker.ui.view_picks.setVisible(False) + picker.start() + + def on_line_completed(): + # Just accept it + picker.ui.accept() + + def on_accepted(): + nonlocal position + original_position = position + new_line = picker.line_data[0] + new_items = [] + for x, y in new_line.tolist(): + new_items.append(TreeItem([position, x, y])) + position += 1 + + model.insert_items(new_items, parent_item, original_position) + + # Select the last new item + last_item = new_items[-1] + index = model.createIndex(last_item.row(), 0, last_item) + self.setCurrentIndex(index) + + picker.accepted.connect(on_accepted) + picker.line_completed.connect(on_line_completed) + + self._current_picker = picker + @property def has_canvas(self): return self.canvas is not None diff --git a/hexrdgui/tree_views/hkl_picks_tree_view.py b/hexrdgui/tree_views/hkl_picks_tree_view.py index cb24da60b..c2d38b13b 100644 --- a/hexrdgui/tree_views/hkl_picks_tree_view.py +++ b/hexrdgui/tree_views/hkl_picks_tree_view.py @@ -121,51 +121,8 @@ def insert_item(): else: raise NotImplementedError - if not is_hand_pickable: - new_item = TreeItem([position, 0., 0.]) - model.insert_items([new_item], parent_item, position) - - # Select the new item - index = model.createIndex(new_item.row(), 0, new_item) - self.setCurrentIndex(index) - return - pick_label = f'Inserting points into: {hkl_str}' - kwargs = { - 'canvas': self.canvas, - 'parent': self, - } - - picker = LinePickerDialog(**kwargs) - picker.current_pick_label = pick_label - picker.ui.setWindowTitle(pick_label) - picker.ui.view_picks.setVisible(False) - picker.start() - - def on_line_completed(): - # Just accept it - picker.ui.accept() - - def on_accepted(): - nonlocal position - original_position = position - new_line = picker.line_data[0] - new_items = [] - for x, y in new_line.tolist(): - new_items.append(TreeItem([position, x, y])) - position += 1 - - model.insert_items(new_items, parent_item, original_position) - - # Select the last new item - last_item = new_items[-1] - index = model.createIndex(last_item.row(), 0, last_item) - self.setCurrentIndex(index) - - picker.accepted.connect(on_accepted) - picker.line_completed.connect(on_line_completed) - - self._current_picker = picker + return self._insert_picks(parent_item, position, pick_label) def hand_pick_item(): self.hand_pick_point(item, hkl_str) From 883d0ae30fa1a9ed9a01b027a32dcf9d4a1e9751 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 9 Jan 2025 15:31:32 -0600 Subject: [PATCH 09/15] Increase line picker dialog height and zoom height Signed-off-by: Patrick Avery --- hexrdgui/resources/ui/line_picker_dialog.ui | 4 ++-- hexrdgui/resources/ui/zoom_canvas_dialog.ui | 2 +- hexrdgui/zoom_canvas.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hexrdgui/resources/ui/line_picker_dialog.ui b/hexrdgui/resources/ui/line_picker_dialog.ui index bee86fea7..b9b8ad26b 100644 --- a/hexrdgui/resources/ui/line_picker_dialog.ui +++ b/hexrdgui/resources/ui/line_picker_dialog.ui @@ -7,7 +7,7 @@ 0 0 635 - 689 + 1000 @@ -103,7 +103,7 @@ 0.200000000000000 - 60.000000000000000 + 150.000000000000000 diff --git a/hexrdgui/resources/ui/zoom_canvas_dialog.ui b/hexrdgui/resources/ui/zoom_canvas_dialog.ui index d7a7a28f2..fa1f428e2 100644 --- a/hexrdgui/resources/ui/zoom_canvas_dialog.ui +++ b/hexrdgui/resources/ui/zoom_canvas_dialog.ui @@ -57,7 +57,7 @@ 0.200000000000000 - 60.000000000000000 + 150.000000000000000 diff --git a/hexrdgui/zoom_canvas.py b/hexrdgui/zoom_canvas.py index dbd1dee38..16918e3c8 100644 --- a/hexrdgui/zoom_canvas.py +++ b/hexrdgui/zoom_canvas.py @@ -60,7 +60,7 @@ def __init__(self, main_canvas, draw_crosshairs=True, # user-specified ROI (from interactors) self.zoom_width = 15 - self.zoom_height = 60 + self.zoom_height = 150 # Keep track of whether we should skip a render (due to point picking) self.skip_next_render = False From c5b9112ddf772c0071592d25d51596b33f4d3231 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 9 Jan 2025 19:28:24 -0600 Subject: [PATCH 10/15] Return an empty dict if physics package missing This is needed to work properly with `update_physics_package()` Signed-off-by: Patrick Avery --- hexrdgui/hexrd_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrdgui/hexrd_config.py b/hexrdgui/hexrd_config.py index e377bc091..40f45011f 100644 --- a/hexrdgui/hexrd_config.py +++ b/hexrdgui/hexrd_config.py @@ -3049,7 +3049,7 @@ def apply_absorption_correction(self, v): @property def physics_package_dictified(self): if not self.has_physics_package: - return None + return {} return self.physics_package.serialize() From be180fe839e735678edf32a7029fbc31017ad5c0 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Fri, 10 Jan 2025 14:10:31 -0600 Subject: [PATCH 11/15] Include radius in calibration flags For cylindrical detectors, we need to take into account the radius for calibration flags and parameters. This uses the old, non-lmfit style for defining calibration flags and parameters. We will be able to eliminate all of this when we upgrade the HEDM calibration to use the new lmfit calibration style... But for now, we need to maintain this. Signed-off-by: Patrick Avery --- hexrdgui/hexrd_config.py | 15 ++++++++++++++- .../calibration/calibration_flags_order.yml | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/hexrdgui/hexrd_config.py b/hexrdgui/hexrd_config.py index 40f45011f..efebfa9b8 100644 --- a/hexrdgui/hexrd_config.py +++ b/hexrdgui/hexrd_config.py @@ -1419,6 +1419,19 @@ def get_statuses_instrument_format(self): statuses.append(status[i]) continue + if path[0] == 'radius': + # Special case for radius + full_path = ['detectors', name] + path + try: + status = self.get_instrument_config_val(full_path) + except KeyError: + # There must not be a radius. Just skip over it. + pass + else: + statuses.append(status) + + continue + full_path = ['detectors', name] + path status = self.get_instrument_config_val(full_path) @@ -1674,7 +1687,7 @@ def get_instrument_config_val(self, path): except KeyError: msg = ('Path: ' + str(path) + '\nwas not found in dict: ' + str(self.config['instrument'])) - raise Exception(msg) + raise KeyError(msg) return cur_val diff --git a/hexrdgui/resources/calibration/calibration_flags_order.yml b/hexrdgui/resources/calibration/calibration_flags_order.yml index 6879ef709..97b70e8e1 100644 --- a/hexrdgui/resources/calibration/calibration_flags_order.yml +++ b/hexrdgui/resources/calibration/calibration_flags_order.yml @@ -8,5 +8,6 @@ instrument: [ detectors: [ ['transform', 'tilt', 'status'], ['transform', 'translation', 'status'], - ['distortion', 'parameters', 'status'] + ['distortion', 'parameters', 'status'], + ['radius', 'status'] ] From 199fbf49aae556735467dd1c87b10ac95751d57b Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Sat, 11 Jan 2025 17:17:17 -0600 Subject: [PATCH 12/15] Disable value and checkbox for expression Disable the value and the checkbox for the Y value of IP4 when TARDIS engineering constraints are applied. We cannot vary or modify the value of an lmfit parameter whose value is computed via an expression in lmfit. Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_dialog.py | 56 ++++++++++++++++++ .../calibration_dialog_callbacks.py | 13 +++++ hexrdgui/calibration/tree_item_models.py | 12 ++++ .../tree_views/multi_column_dict_tree_view.py | 57 ++++++++++++++++++- 4 files changed, 135 insertions(+), 3 deletions(-) diff --git a/hexrdgui/calibration/calibration_dialog.py b/hexrdgui/calibration/calibration_dialog.py index 36cc08552..3cf16f9e4 100644 --- a/hexrdgui/calibration/calibration_dialog.py +++ b/hexrdgui/calibration/calibration_dialog.py @@ -327,6 +327,9 @@ def validate_parameters(self): errors = [] path = [] + has_tardis_constraints = self.has_tardis_constraints + tardis_ip4_y_path = self.tardis_ip4_y_path + def recurse(cur): for k, v in cur.items(): path.append(k) @@ -341,6 +344,26 @@ def recurse(cur): # from raising an exception. param.min -= 1e-8 param.max += 1e-8 + elif ( + has_tardis_constraints and + tuple(path) == tardis_ip4_y_path + ): + # Don't allow the min/max to invalidate the value, + # because the value is computed, not set. + msg = ( + 'When TARDIS engineering constraints are set, ' + 'the Y translation of IMAGE-PLATE-4 is computed. ' + 'The min must be less than the computed value, ' + 'and the max must be greater than the computed ' + 'value.' + ) + # We can't use `param.value`, because the min/max might + # affect that. Let's compute the expression instead. + value = param._expr_eval(param._expr) + if param.min > value: + errors.append(msg) + elif param.max < value: + errors.append(msg) elif isinstance(v, dict): recurse(v) path.pop(-1) @@ -458,8 +481,39 @@ def on_tilt_center_of_rotation_changed(self): self.tilt_center_of_rotation_changed.emit(self.tilt_center_of_rotation) def on_engineering_constraints_changed(self): + self.update_disabled_editor_paths() self.engineering_constraints_changed.emit(self.engineering_constraints) + def update_disabled_editor_paths(self): + uneditable_paths = self.tree_view.model().uneditable_paths + disabled_paths = self.tree_view.disabled_editor_paths + + uneditable_paths.clear() + disabled_paths.clear() + if self.has_tardis_constraints: + value_idx = self.tree_view_model_class.VALUE_IDX + vary_idx = self.tree_view_model_class.VARY_IDX + + # The checkbox is disabled + disabled_paths.append(self.tardis_ip4_y_path + (vary_idx,)) + + # The value is uneditable + uneditable_paths.append(self.tardis_ip4_y_path + (value_idx,)) + + @property + def has_tardis_constraints(self): + return self.engineering_constraints == 'TARDIS' + + @property + def tardis_ip4_y_path(self) -> tuple[str]: + return ( + 'detectors', + 'IMAGE-PLATE-4', + 'transform', + 'translation', + 'Y', + ) + def on_delta_boundaries_toggled(self, b): # The columns have changed, so we need to reinitialize the tree view self.reinitialize_tree_view() @@ -720,6 +774,8 @@ def initialize_tree_view(self): # Make the key section a little larger self.tree_view.header().resizeSection(0, 300) + self.update_disabled_editor_paths() + def reinitialize_tree_view(self): # Keep the same scroll position scrollbar = self.tree_view.verticalScrollBar() diff --git a/hexrdgui/calibration/calibration_dialog_callbacks.py b/hexrdgui/calibration/calibration_dialog_callbacks.py index 657f9e16d..9650d559b 100644 --- a/hexrdgui/calibration/calibration_dialog_callbacks.py +++ b/hexrdgui/calibration/calibration_dialog_callbacks.py @@ -243,8 +243,17 @@ def on_constraints_changed(self): 'brute_step', 'user_data', ] + blacklist_params = [] + + if self.has_tardis_constraints: + # If TARDIS engineering constraints are on, do not remember + # the previous value for the expression. + blacklist_params.append('IMAGE_PLATE_4_tvec_y') for param_key, param in self.dialog.params_dict.items(): + if param_key in blacklist_params: + continue + if param_key in self.calibrator.params: current = self.calibrator.params[param_key] for attr in to_remember: @@ -252,6 +261,10 @@ def on_constraints_changed(self): self.dialog.params_dict = self.calibrator.params + @property + def has_tardis_constraints(self) -> bool: + return self.calibrator.engineering_constraints == 'TARDIS' + def on_run_clicked(self): self.async_runner.progress_title = 'Running calibration...' self.async_runner.success_callback = self.on_calibration_finished diff --git a/hexrdgui/calibration/tree_item_models.py b/hexrdgui/calibration/tree_item_models.py index f5d3be2b9..45e597cf5 100644 --- a/hexrdgui/calibration/tree_item_models.py +++ b/hexrdgui/calibration/tree_item_models.py @@ -64,6 +64,7 @@ class DefaultCalibrationTreeItemModel(CalibrationTreeItemModel): COLUMN_INDICES = _tree_columns_to_indices(COLUMNS) VALUE_IDX = COLUMN_INDICES['Value'] + VARY_IDX = COLUMN_INDICES['Vary'] MAX_IDX = COLUMN_INDICES['Maximum'] MIN_IDX = COLUMN_INDICES['Minimum'] BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX) @@ -85,6 +86,16 @@ def data(self, index, role): if abs(item.data(pair[0]) - item.data(pair[1])) < atol: return QColor('red') + if ( + role == Qt.ForegroundRole and + index.column() == self.VALUE_IDX and + self.has_uneditable_paths + ): + # Check if this value is uneditable. If so, gray it out. + item = self.get_item(index) + path = tuple(self.path_to_item(item) + [index.column()]) + if path in self.uneditable_paths: + return QColor('gray') return super().data(index, role) @@ -99,6 +110,7 @@ class DeltaCalibrationTreeItemModel(CalibrationTreeItemModel): COLUMN_INDICES = _tree_columns_to_indices(COLUMNS) VALUE_IDX = COLUMN_INDICES['Value'] + VARY_IDX = COLUMN_INDICES['Vary'] DELTA_IDX = COLUMN_INDICES['Delta'] BOUND_INDICES = (VALUE_IDX, DELTA_IDX) diff --git a/hexrdgui/tree_views/multi_column_dict_tree_view.py b/hexrdgui/tree_views/multi_column_dict_tree_view.py index 50d364bff..9e8ab9d64 100644 --- a/hexrdgui/tree_views/multi_column_dict_tree_view.py +++ b/hexrdgui/tree_views/multi_column_dict_tree_view.py @@ -22,6 +22,7 @@ def __init__(self, dictionary, columns, parent=None): self.column_labels = list(columns.keys()) self.column_keys = list(columns.values()) + self.uneditable_paths = [] self.root_item = TreeItem(['Key'] + self.column_labels) self.rebuild_tree() @@ -42,11 +43,21 @@ def flags(self, index): flags = super().flags(index) + # Make sure the editable flag is removed. We'll add it later + # after some checks. + flags = flags & ~Qt.ItemIsEditable + column = index.column() item = self.get_item(index) if column != KEY_COL and item.data(column) is not None: - # All columns after the first that isn't set to None is editable - flags = flags | Qt.ItemIsEditable + if self.has_uneditable_paths: + # Need to check if it is uneditable + path = tuple(self.path_to_item(item) + [column]) + if path not in self.uneditable_paths: + flags = flags | Qt.ItemIsEditable + else: + # All columns after the first that isn't None is editable + flags = flags | Qt.ItemIsEditable return flags @@ -90,6 +101,9 @@ def non_column_keys(): def path_to_value(self, tree_item, column): return self.path_to_item(tree_item) + [self.column_keys[column - 1]] + def has_uneditable_paths(self) -> bool: + return bool(self.uneditable_paths) + class MultiColumnDictTreeView(BaseDictTreeView): @@ -103,6 +117,11 @@ def __init__(self, dictionary, columns, parent=None, # menu actions "Check All" and "Uncheck All" self.check_selection_index = None + # These are tree view paths to editors that should be disabled + # Each item in this list is a path tuple (which includes the + # column at the end), like so: ('beam', 'XRS1', 'energy', 1) + self.disabled_editor_paths = [] + self.setModel(model_class(dictionary, columns, parent=self)) self.resizeColumnToContents(0) @@ -245,6 +264,13 @@ def restore_vertical_bar(): # it may require the GUI to finish updating. QTimer.singleShot(0, restore_vertical_bar) + @property + def has_disabled_editors(self) -> bool: + return bool(self.disabled_editor_paths) + + def editor_is_disabled(self, path: list[str] | tuple[str]): + return tuple(path) in self.disabled_editor_paths + class MultiColumnDictTreeViewDialog(QDialog): @@ -268,15 +294,40 @@ def setup_connections(self): class ColumnDelegate(QStyledItemDelegate): - def __init__(self, parent=None): + def __init__(self, parent: MultiColumnDictTreeView): super().__init__(parent) editor_factory = ColumnEditorFactory(self, parent) self.setItemEditorFactory(editor_factory) + @property + def tree_view(self) -> MultiColumnDictTreeView: + return self.parent() + + @property + def model(self) -> MultiColumnDictTreeItemModel: + return self.tree_view.model() + def state_changed(self): self.commitData.emit(self.sender()) + def createEditor(self, parent, option, index): + editor = super().createEditor(parent, option, index) + if self.tree_view.has_disabled_editors: + item = self.model.get_item(index) + path = self.model.path_to_item(item) + [index.column()] + if self.tree_view.editor_is_disabled(path): + editor.setEnabled(False) + + if isinstance(editor, QCheckBox): + # For some reason, checkboxes are not being grayed out + # automatically, so we must gray it out here. + editor.setStyleSheet( + 'QCheckBox::indicator {background-color: gray}' + ) + + return editor + class ColumnEditorFactory(QItemEditorFactory): def __init__(self, delegate, parent=None): From af8ed08880f498f97693331414316bb2951f60ba Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Mon, 13 Jan 2025 09:49:49 -0600 Subject: [PATCH 13/15] Add aliases for dynamic compression instruments Signed-off-by: Patrick Avery --- hexrdgui/main_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hexrdgui/main_window.py b/hexrdgui/main_window.py index b4afae4c0..2dceba048 100644 --- a/hexrdgui/main_window.py +++ b/hexrdgui/main_window.py @@ -1639,7 +1639,10 @@ def load_recent_state_file(self, path): def on_action_open_preconfigured_instrument_file_triggered(self): # Should we put this in HEXRD? aliases = { + 'dcs.yml': 'DCS', 'dual_dexelas.yml': 'Dual Dexelas', + 'rigaku.yml': 'Rigaku', + 'varex.yml': 'Varex', } # Create a dict of options for loading an instrument, mapping file @@ -1653,6 +1656,9 @@ def on_action_open_preconfigured_instrument_file_triggered(self): options[name] = f + # Sort them in alphabetical order + options = {k: options[k] for k in sorted(options)} + # Provide simple dialog for selecting instrument to import msg = 'Select pre-configured instrument to load' instr_name, ok = QInputDialog.getItem( From d80b1b6755fc87724073600aa39fe6e6d24213f9 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Mon, 13 Jan 2025 10:37:14 -0600 Subject: [PATCH 14/15] Use gray background, white text for disabled value Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_dialog.py | 3 ++ hexrdgui/calibration/tree_item_models.py | 31 ++++++++++++++------- hexrdgui/resources/ui/calibration_dialog.ui | 2 +- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/hexrdgui/calibration/calibration_dialog.py b/hexrdgui/calibration/calibration_dialog.py index 3cf16f9e4..a6e9cb193 100644 --- a/hexrdgui/calibration/calibration_dialog.py +++ b/hexrdgui/calibration/calibration_dialog.py @@ -500,6 +500,9 @@ def update_disabled_editor_paths(self): # The value is uneditable uneditable_paths.append(self.tardis_ip4_y_path + (value_idx,)) + # A tree view update is necessary after changing the disabled editors + self.update_tree_view() + @property def has_tardis_constraints(self): return self.engineering_constraints == 'TARDIS' diff --git a/hexrdgui/calibration/tree_item_models.py b/hexrdgui/calibration/tree_item_models.py index 45e597cf5..58cabefd1 100644 --- a/hexrdgui/calibration/tree_item_models.py +++ b/hexrdgui/calibration/tree_item_models.py @@ -52,6 +52,27 @@ def set_config_val(self, path, value): setattr(param, attribute, value) + def data(self, index, role): + if ( + role in (Qt.BackgroundRole, Qt.ForegroundRole) and + index.column() in (self.VALUE_IDX, self.VARY_IDX) and + self.has_uneditable_paths + ): + # Check if this value is uneditable. If so, gray it out. + item = self.get_item(index) + path = tuple(self.path_to_item(item) + [self.VALUE_IDX]) + if path in self.uneditable_paths: + color = 'gray' + if ( + index.column() == self.VALUE_IDX and + role == Qt.ForegroundRole + ): + color = 'white' + + return QColor(color) + + return super().data(index, role) + class DefaultCalibrationTreeItemModel(CalibrationTreeItemModel): """This model uses minimum/maximum for the boundary constraints""" @@ -86,16 +107,6 @@ def data(self, index, role): if abs(item.data(pair[0]) - item.data(pair[1])) < atol: return QColor('red') - if ( - role == Qt.ForegroundRole and - index.column() == self.VALUE_IDX and - self.has_uneditable_paths - ): - # Check if this value is uneditable. If so, gray it out. - item = self.get_item(index) - path = tuple(self.path_to_item(item) + [index.column()]) - if path in self.uneditable_paths: - return QColor('gray') return super().data(index, role) diff --git a/hexrdgui/resources/ui/calibration_dialog.ui b/hexrdgui/resources/ui/calibration_dialog.ui index 07742fb14..6ccaeab42 100644 --- a/hexrdgui/resources/ui/calibration_dialog.ui +++ b/hexrdgui/resources/ui/calibration_dialog.ui @@ -50,7 +50,7 @@ - <html><head/><body><p>Add engineering constraints for certain instrument types. This may add extra parameters to the table.</p><p><br/></p><p>For example, for TARDIS, the distance between IMAGE-PLATE-2 and IMAGE-PLATE-4 must be within a certain range. If TARDIS is selected, a new parameter is added with default values for this distance.</p><p><br/></p><p>If the instrument type can be guessed, it will be selected automatically when the dialog first appears. For example, TARDIS is automatically selected if any of the detector names are IMAGE-PLATE-2, IMAGE-PLATE-3, or IMAGE-PLATE-4.</p></body></html> + <html><head/><body><p>Add engineering constraints for certain instrument types. This may add extra parameters to the table.</p><p><br/></p><p>For example, for TARDIS, the distance between IMAGE-PLATE-2 and IMAGE-PLATE-4 must be within a certain range. If TARDIS is selected, a new parameter is added with default values for this distance, and the y value of IMAGE-PLATE-4 is disabled for editing/refinement since it is computed from the distance instead.</p><p><br/></p><p>If the instrument type can be guessed, it will be selected automatically when the dialog first appears. For example, TARDIS is automatically selected if IMAGE-PLATE-2 and IMAGE-PLATE-4 are present.</p></body></html> From 2a55d4923f8cb5aacc8d423c9a807913a2962a1c Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Mon, 13 Jan 2025 12:37:04 -0600 Subject: [PATCH 15/15] Disable non-selected x-ray source pick paths If "Show picks from all x-ray sources" is not checked, picks for the non-selected x-ray source are now grayed out and not editable. This is much better than before, where an error would simply occur if users tried to edit non-selected x-ray source picks (or worse, in structureless calibration, the point would be added inadvertently to the wrong source). Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_runner.py | 23 ++++++++++++++++- hexrdgui/calibration/structureless/runner.py | 9 +++++++ .../tree_views/base_dict_tree_item_model.py | 25 +++++++++++++++++++ .../tree_views/generic_picks_tree_view.py | 4 +++ hexrdgui/tree_views/hkl_picks_tree_view.py | 4 +++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/hexrdgui/calibration/calibration_runner.py b/hexrdgui/calibration/calibration_runner.py index fbbbe27dc..a825aa26a 100644 --- a/hexrdgui/calibration/calibration_runner.py +++ b/hexrdgui/calibration/calibration_runner.py @@ -981,8 +981,11 @@ def clear_drawn_picks(self): def on_edit_picks_clicked(self): dialog = self.edit_picks_dialog + tree_view = dialog.tree_view + model = tree_view.model() + model.disabled_paths.clear() + dialog.button_box_visible = True - dialog.ui.show() def on_finished(): self.dialog.show() @@ -994,6 +997,24 @@ def on_finished(): self.draw_picks_on_canvas() self.dialog.hide() + # After the tree view is updated, disable paths that + # don't match this XRS. + if ( + HexrdConfig().has_multi_xrs and + not self.showing_picks_from_all_xray_sources + ): + # Disable paths that don't match this XRS + for item in model.root_item.child_items: + overlay_name = item.data(0) + overlay = Overlay.from_name(overlay_name) + + if overlay.xray_source != HexrdConfig().active_beam_name: + model.disabled_paths.append((overlay_name,)) + + tree_view.collapse_disabled_paths() + + dialog.ui.show() + def save_picks_to_file(self, selected_file): # Reuse the same logic from the HKLPicksTreeViewDialog self.edit_picks_dialog.export_picks(selected_file) diff --git a/hexrdgui/calibration/structureless/runner.py b/hexrdgui/calibration/structureless/runner.py index 188443482..b7d7edda5 100644 --- a/hexrdgui/calibration/structureless/runner.py +++ b/hexrdgui/calibration/structureless/runner.py @@ -499,6 +499,7 @@ def draw_picks_on_canvas(self): def on_edit_picks_clicked(self): # Convert to polar lines data = cart_to_polar_lines(self.calibrator_lines, self.instr) + disabled_paths = [] # Now convert to a dictionary for the line labels dictionary = {} @@ -510,6 +511,12 @@ def on_edit_picks_clicked(self): this_xrs[name] = v.tolist() ring_indices[xray_source] = ring_idx + if ( + not self.showing_picks_from_all_xray_sources and + xray_source != HexrdConfig().active_beam_name + ): + disabled_paths.append((xray_source,)) + def new_line_name_generator(path): # Get the x-ray source xray_source = path[0] @@ -519,12 +526,14 @@ def new_line_name_generator(path): dialog = GenericPicksTreeViewDialog(dictionary, canvas=self.canvas, parent=self.canvas) dialog.tree_view.new_line_name_generator = new_line_name_generator + dialog.tree_view.model().disabled_paths = disabled_paths dialog.accepted.connect(self.on_edit_picks_accepted) dialog.finished.connect(self.on_edit_picks_finished) dialog.show() self.edit_picks_dictionary = dictionary self.edit_picks_dialog = dialog + dialog.tree_view.collapse_disabled_paths() self.clear_drawn_picks() self.dialog.hide() diff --git a/hexrdgui/tree_views/base_dict_tree_item_model.py b/hexrdgui/tree_views/base_dict_tree_item_model.py index 30036c922..40e84f421 100644 --- a/hexrdgui/tree_views/base_dict_tree_item_model.py +++ b/hexrdgui/tree_views/base_dict_tree_item_model.py @@ -22,6 +22,7 @@ def __init__(self, dictionary, parent=None): # These can be modified anytime self.lists_resizable = True self._blacklisted_paths = [] + self.disabled_paths = [] self.editable = True self.config = dictionary @@ -101,6 +102,12 @@ def flags(self, index): flags = super().flags(index) item = self.get_item(index) + if self.has_disabled_paths: + path = self.path_to_item(item) + if self.is_disabled_path(path): + flags = flags & ~Qt.ItemIsEnabled + return flags + # Items are selectable if they have no children # and none of the data values in the row are `None`. is_selectable = all(( @@ -246,6 +253,18 @@ def blacklisted_paths(self, v): self._blacklisted_paths = [list(x) for x in v] self.rebuild_tree() + @property + def has_disabled_paths(self) -> bool: + return bool(self.disabled_paths) + + def is_disabled_path(self, path: list[str] | tuple[str]) -> bool: + path = tuple(path) + for disabled_path in self.disabled_paths: + if path[:len(disabled_path)] == disabled_path: + return True + + return False + class BaseDictTreeView(QTreeView): @@ -285,6 +304,12 @@ def expand_rows(self, parent=QModelIndex(), rows=None): self.expand(index) self.expand_rows(index) + def collapse_disabled_paths(self): + model = self.model() + for path in model.disabled_paths: + index = model.create_index(path) + self.collapse(index) + @property def lists_resizable(self): return self.model().lists_resizable diff --git a/hexrdgui/tree_views/generic_picks_tree_view.py b/hexrdgui/tree_views/generic_picks_tree_view.py index cfe05cea6..89c23b7fd 100644 --- a/hexrdgui/tree_views/generic_picks_tree_view.py +++ b/hexrdgui/tree_views/generic_picks_tree_view.py @@ -411,6 +411,10 @@ def contextMenuEvent(self, event): num_selected = len(selected_items) is_hand_pickable = self.is_hand_pickable + if self.model().is_disabled_path(path): + # If it is a disabled path, do not create the context menu + return + menu = QMenu(self) # Helper functions diff --git a/hexrdgui/tree_views/hkl_picks_tree_view.py b/hexrdgui/tree_views/hkl_picks_tree_view.py index c2d38b13b..7bab1b953 100644 --- a/hexrdgui/tree_views/hkl_picks_tree_view.py +++ b/hexrdgui/tree_views/hkl_picks_tree_view.py @@ -99,6 +99,10 @@ def contextMenuEvent(self, event): num_selected = len(selected_items) is_hand_pickable = self.is_hand_pickable + if self.model().is_disabled_path(path): + # If it is a disabled path, do not create the context menu + return + menu = QMenu(self) # Helper functions