From d58ee28237f6e9f245d17c0e0d60a8070006252b Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 28 Aug 2025 12:14:38 -0500 Subject: [PATCH 1/5] Disable all intensity corrections for LLNL import The intensity corrections cause problems during LLNL import. The FIDDLE image plate, for example, doesn't work at all. Just disable all intensity corrections during the LLNL import, and the user can manually re-enable them afterward. Signed-off-by: Patrick Avery --- hexrdgui/hexrd_config.py | 23 ++++++++++++++++++----- hexrdgui/llnl_import_tool_dialog.py | 22 ++++++++++++++-------- hexrdgui/main_window.py | 2 ++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/hexrdgui/hexrd_config.py b/hexrdgui/hexrd_config.py index c7703b335..12e5dad13 100644 --- a/hexrdgui/hexrd_config.py +++ b/hexrdgui/hexrd_config.py @@ -2586,11 +2586,9 @@ def set_intensity_subtract_minimum(self, v): set_intensity_subtract_minimum) @property - def any_intensity_corrections(self): - """Are we to perform any intensity corrections on the images?""" - + def _intensity_correction_names(self) -> list[str]: # Add to the list here as needed - corrections = [ + return [ 'apply_pixel_solid_angle_correction', 'apply_polarization_correction', 'apply_lorentz_correction', @@ -2599,7 +2597,22 @@ def any_intensity_corrections(self): 'apply_median_filter_correction', ] - return any(getattr(self, x) for x in corrections) + @property + def any_intensity_corrections(self): + """Are we to perform any intensity corrections on the images?""" + return any(getattr(self, x) for x in self._intensity_correction_names) + + def disable_all_intensity_corrections(self): + if not self.any_intensity_corrections: + # Nothing to do... + return + + # Block our own signals as we disable all of these + with utils.block_signals(self): + for name in self._intensity_correction_names: + setattr(self, name, False) + + self.deep_rerender_needed.emit() def get_show_saturation_level(self): return self._show_saturation_level diff --git a/hexrdgui/llnl_import_tool_dialog.py b/hexrdgui/llnl_import_tool_dialog.py index 684840509..647bfdc7f 100644 --- a/hexrdgui/llnl_import_tool_dialog.py +++ b/hexrdgui/llnl_import_tool_dialog.py @@ -69,7 +69,7 @@ def optimization_function(params, start, finish): params['tvec_z'].value] )) - trans_start = (np.dot(rmat, start.T).T + + trans_start = (np.dot(rmat, start.T).T + np.repeat(trans, start.shape[0], axis=0)) residual = trans_start-finish @@ -101,7 +101,7 @@ def optimization_function(params, start, finish): return minimizer_result def _transform_coordinates(self, pts): - return (np.dot(self.rmat, pts.T).T + + return (np.dot(self.rmat, pts.T).T + np.repeat(self.tvec, pts.shape[0], axis=0)) def _get_icarus_corners_in_TCC(self): @@ -214,6 +214,10 @@ class LLNLImportToolDialog(QObject): # Emitted when new config is loaded new_config_loaded = Signal() + # Emitted when an instrument was selected + instrument_was_selected = Signal() + + # Emitted when the workflow is canceled cancel_workflow = Signal() # The boolean flag indicates whether this is a FIDDLE instrument or not @@ -449,9 +453,10 @@ def instrument_selected(self, idx): visible=needs_mask) self.import_in_progress = True - # Make sure we're not applying median filter during import - self.median_setting = HexrdConfig().apply_median_filter_correction - HexrdConfig().apply_median_filter_correction = False + + # We need to disable all intensity corrections during import. + # Users can just re-enable them if they are needed. + HexrdConfig().disable_all_intensity_corrections() HexrdConfig().set_image_mode_widget_tab.emit(ViewType.raw) HexrdConfig().enable_image_mode_widget.emit(False) @@ -469,6 +474,10 @@ def instrument_selected(self, idx): self.ui.bbox.setToolTip('The bounding box editors are not ' + 'available for the TARDIS instrument') + # Indicate that an instrument was selected so the main window can + # update anything it needs to update. + self.instrument_was_selected.emit() + def set_convention(self): new_conv = {'axes_order': 'zxz', 'extrinsic': False} HexrdConfig().set_euler_angle_convention(new_conv) @@ -1175,9 +1184,6 @@ def import_complete(self): HexrdConfig().enable_canvas_toolbar.emit(True) self.cmap.block_updates(False) - if self.instrument != 'FIDDLE': - # Re-enable median filter correction if it was set - HexrdConfig().apply_median_filter_correction = self.median_setting # If this is a FIDDLE instrument users will be prompted to set (or not) # the median filter after the import is complete self.complete_workflow.emit(self.instrument == 'FIDDLE') diff --git a/hexrdgui/main_window.py b/hexrdgui/main_window.py index 1d6d34e1a..33e172e29 100644 --- a/hexrdgui/main_window.py +++ b/hexrdgui/main_window.py @@ -277,6 +277,8 @@ def setup_connections(self): self.ui.status_bar.clearMessage) self.llnl_import_tool_dialog.new_config_loaded.connect( self.update_config_gui) + self.llnl_import_tool_dialog.instrument_was_selected.connect( + self.update_action_check_states) self.llnl_import_tool_dialog.cancel_workflow.connect( self.load_dummy_images) self.config_loaded.connect( From d937feb1d4ada9cd7f00fe4dd3247b38ded607d8 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 28 Aug 2025 15:38:12 -0500 Subject: [PATCH 2/5] Fix powder range highlighting Oftentimes, when a powder ring is highlighted, the powder range would be highlighted incorrectly (typically a range on a different ring to the left of the highlighted ring). This was due to the fact that `rbnd_indices` are indices into the two theta list, and not into the ring array. We added a ring map to fix this, and now the correct range is always highlighted. Signed-off-by: Patrick Avery --- hexrdgui/image_canvas.py | 57 ++++++++--------------------- hexrdgui/overlays/powder_overlay.py | 13 ++++++- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/hexrdgui/image_canvas.py b/hexrdgui/image_canvas.py index df3a9f38e..413e66470 100644 --- a/hexrdgui/image_canvas.py +++ b/hexrdgui/image_canvas.py @@ -457,18 +457,6 @@ def overlay_draw_func(self, type): return overlay_funcs[type] - def get_overlay_highlight_ids(self, overlay): - highlights = overlay.highlights - if not highlights: - return [] - - def recursive_get(cur, path): - for p in path: - cur = cur[p] - return cur - - return [id(recursive_get(overlay.data, h)) for h in highlights] - def draw_overlay(self, overlay): if not overlay.visible: return @@ -477,19 +465,19 @@ def draw_overlay(self, overlay): # It's already present. Skip it. return - # Keep track of any overlays we need to highlight - self.overlay_highlight_ids += self.get_overlay_highlight_ids(overlay) - type = overlay.type style = overlay.style highlight_style = overlay.highlight_style + self.overlay_axes_data(overlay) for axis, det_key, data in self.overlay_axes_data(overlay): + highlights = [x[2] for x in overlay.highlights if x[0] == det_key] kwargs = { 'artist_key': overlay.name, 'det_key': det_key, 'axis': axis, 'data': data, 'style': style, + 'highlight_indices': highlights, 'highlight_style': highlight_style, } self.overlay_draw_func(type)(**kwargs) @@ -498,10 +486,11 @@ def draw_overlay(self, overlay): self.draw_azimuthal_powder_lines(overlay) def draw_powder_overlay(self, artist_key, det_key, axis, data, style, - highlight_style): + highlight_indices, highlight_style): rings = data['rings'] ranges = data['rbnds'] rbnd_indices = data['rbnd_indices'] + ring_idx_map = data['ring_idx_map'] data_style = style['data'] ranges_style = style['ranges'] @@ -512,13 +501,6 @@ def draw_powder_overlay(self, artist_key, det_key, axis, data, style, overlay_artists = self.overlay_artists.setdefault(artist_key, {}) artists = overlay_artists.setdefault(det_key, {}) - highlight_indices = [] - - if self.overlay_highlight_ids: - # Split up highlighted and non-highlighted components for all - highlight_indices = [i for i, x in enumerate(rings) - if id(x) in self.overlay_highlight_ids] - def split(data): if not highlight_indices or len(data) == 0: return [], data @@ -533,13 +515,18 @@ def split(data): h_ranges = [] reg_ranges = [] + highlight_indices_mapped = [ring_idx_map[i] for i in highlight_indices] + found = False for i, ind in enumerate(rbnd_indices): if len(ind) > 1: merged_ranges.append(ranges[i]) found = True - if highlight_indices and any(x in highlight_indices for x in ind): + if ( + highlight_indices and + any(x in highlight_indices_mapped for x in ind) + ): h_ranges.append(ranges[i]) found = True @@ -620,7 +607,7 @@ def az_plot(data, key, kwargs): az_plot(merged_ranges, 'merged_ranges', merged_ranges_style) def draw_laue_overlay(self, artist_key, det_key, axis, data, style, - highlight_style): + highlight_indices, highlight_style): spots = data['spots'] ranges = data['ranges'] labels = data['labels'] @@ -630,13 +617,6 @@ def draw_laue_overlay(self, artist_key, det_key, axis, data, style, ranges_style = style['ranges'] label_style = style['labels'] - highlight_indices = [] - - if self.overlay_highlight_ids: - # Split up highlighted and non-highlighted components for all - highlight_indices = [i for i, x in enumerate(spots) - if id(x) in self.overlay_highlight_ids] - def split(data): if not highlight_indices or len(data) == 0: return [], data @@ -696,7 +676,8 @@ def plot_label(x, y, label, style): artists['h_labels'].append(plot_label(x, y, label, style)) def draw_rotation_series_overlay(self, artist_key, det_key, axis, data, - style, highlight_style): + style, highlight_indices, + highlight_style): is_aggregated = HexrdConfig().is_aggregated ome_range = HexrdConfig().omega_ranges aggregated = data['aggregated'] or is_aggregated or ome_range is None @@ -741,20 +722,13 @@ def draw_rotation_series_overlay(self, artist_key, det_key, axis, data, animated=True, **ranges_style) def draw_const_chi_overlay(self, artist_key, det_key, axis, data, style, - highlight_style): + highlight_indices, highlight_style): points = data['data'] data_style = style['data'] overlay_artists = self.overlay_artists.setdefault(artist_key, {}) artists = overlay_artists.setdefault(det_key, {}) - highlight_indices = [] - - if self.overlay_highlight_ids: - # Split up highlighted and non-highlighted components for all - highlight_indices = [i for i, x in enumerate(points) - if id(x) in self.overlay_highlight_ids] - def split(data): if not highlight_indices or len(data) == 0: return [], data @@ -807,7 +781,6 @@ def update_overlays(self): self.iviewer.update_overlay_data() - self.overlay_highlight_ids = [] for overlay in HexrdConfig().overlays: self.draw_overlay(overlay) diff --git a/hexrdgui/overlays/powder_overlay.py b/hexrdgui/overlays/powder_overlay.py index 6707692c7..a59f23df0 100644 --- a/hexrdgui/overlays/powder_overlay.py +++ b/hexrdgui/overlays/powder_overlay.py @@ -273,6 +273,18 @@ def generate_overlay(self): point_groups[det_key]['rings'] = ring_pts point_groups[det_key]['hkls'] = det_hkls + # Map of ring index to original two theta index + counter = 0 + ring_idx_map = {} + for i in range(len(tths)): + if i in skipped_tth: + counter += 1 + continue + + ring_idx_map[i - counter] = i + + point_groups[det_key]['ring_idx_map'] = ring_idx_map + if plane_data.tThWidth is not None: # Generate the ranges too lower_pts, lower_skipped = self.generate_ring_points( @@ -693,6 +705,5 @@ def default_highlight_style(self): } } - # Constants nans_row = np.nan * np.ones((1, 2)) From 3c6a1aa36c3600a7cedda4ec219aae174996b87c Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 28 Aug 2025 16:57:46 -0500 Subject: [PATCH 3/5] Make canvas toolbars invisible by default This fixes an issue where, when you switch to tabbed view and a bunch of canvases get allocated, a whole bunch of toolbars also appear. Now, only the correct toolbar will appear. Signed-off-by: Patrick Avery --- hexrdgui/image_tab_widget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hexrdgui/image_tab_widget.py b/hexrdgui/image_tab_widget.py index 0c451551c..aceacb439 100644 --- a/hexrdgui/image_tab_widget.py +++ b/hexrdgui/image_tab_widget.py @@ -182,10 +182,13 @@ def allocate_toolbars(self): # The new one to add idx = len(self.toolbars) tb = NavigationToolbar(self.image_canvases[idx], parent, False) + tb.setVisible(False) # Current detector ims = self.ims_for_name(self.image_names[idx]) sb = ImageSeriesToolbar(ims, self) ib = ImageSeriesInfoToolbar(self) + sb.set_visible(False) + ib.set_visible(False) # This will put it at the bottom of the central widget toolbar = QHBoxLayout() From 310a19db320b94fdd27a63f80870500601ffe57c Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Thu, 28 Aug 2025 17:24:48 -0500 Subject: [PATCH 4/5] Disable zoom canvas during highlight updates They both use blitting, and unfortunately the zoom canvas does not utilize the canvas's blit manager (which it should). Because of that, when a blit update is being performed by both the main canvas and the zoom canvas, there is some bouncing back and forth in the rendering. This mainly manifests itself by the highlighted line bouncing around. This commit fixes the issue. Signed-off-by: Patrick Avery --- hexrdgui/calibration/calibration_runner.py | 25 +++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/hexrdgui/calibration/calibration_runner.py b/hexrdgui/calibration/calibration_runner.py index 05d24f754..4bb934285 100644 --- a/hexrdgui/calibration/calibration_runner.py +++ b/hexrdgui/calibration/calibration_runner.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager import copy from functools import partial import itertools @@ -519,9 +520,14 @@ def restore_overlay_visibilities(self): HexrdConfig().overlay_config_changed.emit() def set_highlighting(self, highlighting): - self.active_overlay.highlights = highlighting - HexrdConfig().flag_overlay_updates_for_all_materials() - HexrdConfig().overlay_config_changed.emit() + # Disable the zoom canvas to prevent the highlight from + # strangely switching back and forth (which is just caused + # by the fact that we did not re-use the canvas's blit manager + # for the zoom box blitting - we probably should). + with zoom_canvas_disabled(self.line_picker): + self.active_overlay.highlights = highlighting + HexrdConfig().flag_overlay_updates_for_all_materials() + HexrdConfig().overlay_config_changed.emit() def remove_all_highlighting(self): for overlay in self.overlays: @@ -1046,3 +1052,16 @@ def load_picks_from_file(self, selected_file): dialog = self.edit_picks_dialog dialog.import_picks(selected_file) return dialog.dictionary + + +@contextmanager +def zoom_canvas_disabled(line_picker): + if line_picker: + prev = line_picker.zoom_canvas.disabled + line_picker.zoom_canvas.disabled = True + + try: + yield + finally: + if line_picker: + line_picker.zoom_canvas.disabled = prev From c48b96215d149e2f3b82c987e55d95a4c5f4c173 Mon Sep 17 00:00:00 2001 From: Patrick Avery Date: Wed, 3 Sep 2025 11:24:25 -0500 Subject: [PATCH 5/5] Consider tth distortion when saving picks We previously would not consider the tth distortion when saving picks, so the cartesian coordinates would be incorrect. Now we reverse the tth distortion if needed. Also, the two calibration workflows (Composite and Fast Powder) write the coordinates directly from the overlays, which is more accurate (some precision is lost when converting back and forth between cartesian and polar when the tth distortion is applied, because it is a field correction). Signed-off-by: Patrick Avery --- hexrdgui/calibration/auto/powder_runner.py | 2 +- hexrdgui/calibration/calibration_runner.py | 3 +- .../calibration/hkl_picks_tree_view_dialog.py | 61 ++++++++++++++----- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/hexrdgui/calibration/auto/powder_runner.py b/hexrdgui/calibration/auto/powder_runner.py index c7d59d69c..b94ded2a2 100644 --- a/hexrdgui/calibration/auto/powder_runner.py +++ b/hexrdgui/calibration/auto/powder_runner.py @@ -242,7 +242,7 @@ def on_finished(): def save_picks_to_file(self, selected_file): # Reuse the same logic from the HKLPicksTreeViewDialog dialog = self.create_hkl_picks_tree_view_dialog() - dialog.export_picks(selected_file) + dialog.export_picks_from_overlays(selected_file, self.overlays) def load_picks_from_file(self, selected_file): # Reuse the same logic from the HKLPicksTreeViewDialog diff --git a/hexrdgui/calibration/calibration_runner.py b/hexrdgui/calibration/calibration_runner.py index 4bb934285..e264d7f7b 100644 --- a/hexrdgui/calibration/calibration_runner.py +++ b/hexrdgui/calibration/calibration_runner.py @@ -1045,7 +1045,8 @@ def on_finished(): def save_picks_to_file(self, selected_file): # Reuse the same logic from the HKLPicksTreeViewDialog - self.edit_picks_dialog.export_picks(selected_file) + d = self.edit_picks_dialog + d.export_picks_from_overlays(selected_file, self.overlays) def load_picks_from_file(self, selected_file): # Reuse the same logic from the HKLPicksTreeViewDialog diff --git a/hexrdgui/calibration/hkl_picks_tree_view_dialog.py b/hexrdgui/calibration/hkl_picks_tree_view_dialog.py index 67c4fdbcd..cfe45e0f4 100644 --- a/hexrdgui/calibration/hkl_picks_tree_view_dialog.py +++ b/hexrdgui/calibration/hkl_picks_tree_view_dialog.py @@ -16,6 +16,7 @@ from hexrdgui.ui_loader import UiLoader from hexrdgui.utils.conversions import angles_to_cart, cart_to_angles from hexrdgui.utils.dicts import ndarrays_to_lists +from hexrdgui.utils.tth_distortion import apply_tth_distortion_if_needed class HKLPicksTreeViewDialog: @@ -92,18 +93,28 @@ def export_picks_clicked(self): return self.export_picks(selected_file) def export_picks(self, filename): + return self._export_dict_to_file(filename, { + 'angular': self.dictionary, + 'cartesian': self.dict_with_cart_coords, + }) + + def export_picks_from_overlays(self, filename, overlays): + # Export picks from overlays using the same export logic as + # the regular dictionary. + return self._export_dict_to_file(filename, { + 'angular': overlays_to_tree_format(overlays, polar=True), + 'cartesian': overlays_to_tree_format(overlays, polar=False), + }) + + def _export_dict_to_file(self, filename: str, export_data: dict): filename = Path(filename) if filename.exists(): filename.unlink() - # unwrap_dict_to_h5 unfortunately modifies the data - # make a deep copy to avoid the modification. - export_data = { - 'angular': copy.deepcopy(self.dictionary), - 'cartesian': self.dict_with_cart_coords, - } - + # unwrap_dict_to_h5 unfortunately modifies the data. + # Make a deep copy to avoid the modification. + export_data = copy.deepcopy(export_data) with h5py.File(filename, 'w') as wf: unwrap_dict_to_h5(wf, export_data) @@ -178,7 +189,7 @@ def button_box_visible(self, b): self.ui.button_box.setVisible(b) -def convert_picks(picks, conversion_function, **kwargs): +def convert_picks(picks, conversion_function): instr = create_hedm_instrument() ret = copy.deepcopy(picks) for name, detectors in ret.items(): @@ -191,25 +202,43 @@ def convert_picks(picks, conversion_function, **kwargs): # Avoid the runtime warning hkls[hkl] = [np.nan, np.nan] else: - hkls[hkl] = conversion_function([spot], panel, - **kwargs)[0] + hkls[hkl] = conversion_function([spot], panel)[0] continue # Must be powder for hkl, line in hkls.items(): if len(line) != 0: - hkls[hkl] = conversion_function(line, panel, **kwargs) + hkls[hkl] = conversion_function(line, panel) return ret def picks_angles_to_cartesian(picks): - return convert_picks(picks, angles_to_cart) + # Create the conversion function + def func(angs, panel): + # Reverse the tth distortion first + angs = apply_tth_distortion_if_needed( + angs, + in_degrees=True, + reverse=True, + ) + # Now convert to cart + return angles_to_cart(angs, panel) + + return convert_picks(picks, func) def picks_cartesian_to_angles(picks): - kwargs = {'eta_period': HexrdConfig().polar_res_eta_period} - return convert_picks(picks, cart_to_angles, **kwargs) + # Create the conversion function + eta_period = HexrdConfig().polar_res_eta_period + + def func(xys, panel): + angs = cart_to_angles(xys, panel, eta_period=eta_period) + + # Apply tth distortion now as well + return apply_tth_distortion_if_needed(angs, in_degrees=True) + + return convert_picks(picks, func) def generate_picks_results(overlays, polar=True): @@ -249,8 +278,8 @@ def generate_picks_results(overlays, polar=True): return pick_results -def overlays_to_tree_format(overlays): - picks = generate_picks_results(overlays) +def overlays_to_tree_format(overlays, polar=True): + picks = generate_picks_results(overlays, polar=polar) return picks_to_tree_format(picks)