Skip to content

Commit a579fca

Browse files
authored
Merge pull request #1980 from HEXRD/spot-diagnostics-dialog
Add dialog for HEDM calibration diagnostics
2 parents dbf06df + 4b007da commit a579fca

File tree

11 files changed

+1317
-52
lines changed

11 files changed

+1317
-52
lines changed

hexrdgui/calibration/hedm/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
compute_xyo,
33
HEDMCalibrationCallbacks,
44
HEDMCalibrationDialog,
5-
parse_spots_data,
65
)
6+
from hexrdgui.utils.spots import parse_spots_data
77
from .calibration_options_dialog import HEDMCalibrationOptionsDialog
88
from .calibration_results_dialog import HEDMCalibrationResultsDialog
99
from .calibration_runner import HEDMCalibrationRunner
10+
from .spot_diagnostics_dialog import SpotDiagnosticsDialog
1011

1112
__all__ = [
1213
'compute_xyo',
@@ -15,5 +16,6 @@
1516
'HEDMCalibrationOptionsDialog',
1617
'HEDMCalibrationResultsDialog',
1718
'HEDMCalibrationRunner',
19+
'SpotDiagnosticsDialog',
1820
'parse_spots_data',
1921
]

hexrdgui/calibration/hedm/calibration_dialog.py

Lines changed: 171 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@
1010
import hexrd.constants as cnst
1111
from hexrd.fitting.calibration.calibrator import Calibrator
1212
from hexrd.fitting.calibration.lmfit_param_handling import fix_detector_y
13+
from hexrd.transforms import xfcapi
14+
from hexrd.xrdutil import apply_correction_to_wavelength
1315

1416
from hexrdgui.calibration.calibration_dialog import CalibrationDialog
1517
from hexrdgui.calibration.hedm.calibration_results_dialog import (
1618
HEDMCalibrationResultsDialog,
1719
)
20+
from hexrdgui.calibration.hedm.spot_diagnostics_dialog import (
21+
SpotDiagnosticsDialog,
22+
)
23+
from hexrdgui.utils.spots import extract_spot_angles, parse_spots_data
1824
from hexrdgui.calibration.tree_item_models import CalibrationTreeItemModel
1925
from hexrdgui.calibration.material_calibration_dialog_callbacks import (
2026
MaterialCalibrationDialogCallbacks,
@@ -27,6 +33,7 @@
2733

2834
class HEDMCalibrationDialog(CalibrationDialog):
2935
apply_refinement_selections_needed = Signal()
36+
spot_diagnostics_requested = Signal()
3037

3138
def __init__(self, *args: Any, **kwargs: Any) -> None:
3239
# Need to initialize this before setup_connections() is called
@@ -75,6 +82,10 @@ def setup_connections(self) -> None:
7582
self.save_refit_settings
7683
)
7784

85+
self.extra_ui.spot_diagnostics.clicked.connect(
86+
self.spot_diagnostics_requested.emit,
87+
)
88+
7889
def show_refinements(self, b: bool) -> None:
7990
self.tree_view.setVisible(b)
8091
if b:
@@ -272,6 +283,9 @@ def setup_connections(self) -> None:
272283
self.dialog.apply_refinement_selections_needed.connect(
273284
self.apply_refinement_selections
274285
)
286+
self.dialog.spot_diagnostics_requested.connect(
287+
self.show_spot_diagnostics,
288+
)
275289

276290
@property
277291
def grain_ids(self) -> np.ndarray:
@@ -297,6 +311,26 @@ def xyo_det(self) -> dict[str, list[Any]]:
297311

298312
return self._xyo_det
299313

314+
def show_spot_diagnostics(self) -> None:
315+
pred_angs, meas_angs = extract_spot_angles(
316+
self.spots_data,
317+
self.instr,
318+
self.grain_ids,
319+
)
320+
xyo_pred = compute_xyo(self.calibrators)
321+
322+
self._spot_diagnostics_dialog = SpotDiagnosticsDialog(
323+
instr=self.instr,
324+
spots_data=self.spots_data,
325+
grain_ids=self.grain_ids,
326+
pred_angs=pred_angs,
327+
meas_angs=meas_angs,
328+
xyo_pred=xyo_pred,
329+
xyo_det=self.xyo_det,
330+
parent=self.dialog.ui,
331+
)
332+
self._spot_diagnostics_dialog.show()
333+
300334
def on_calibration_finished(self) -> None:
301335
super().on_calibration_finished()
302336

@@ -339,6 +373,21 @@ def on_calibration_finished(self) -> None:
339373
# Do an "undo"
340374
self.pop_undo_stack()
341375

376+
self.update_spot_diagnostics()
377+
378+
def update_spot_diagnostics(self) -> None:
379+
dialog = getattr(self, '_spot_diagnostics_dialog', None)
380+
if dialog is not None and dialog.is_visible:
381+
xyo_pred = compute_xyo(self.calibrators)
382+
pred_angs = compute_pred_angs(self.calibrators)
383+
meas_angs = compute_meas_angs(self.calibrators, self.xyo_det)
384+
dialog.update_data(
385+
self.instr,
386+
xyo_pred=xyo_pred,
387+
pred_angs=pred_angs,
388+
meas_angs=meas_angs,
389+
)
390+
342391
def push_undo_stack(self) -> Any:
343392
self.extra_ui_undo_stack.append(self.dialog.extra_ui_settings)
344393
return super().push_undo_stack()
@@ -602,64 +651,138 @@ def compute_xyo(calibrators: list[Calibrator]) -> dict[str, list]:
602651
return xyo
603652

604653

605-
def parse_spots_data(
606-
spots_data: Any,
607-
instr: Any,
608-
grain_ids: np.ndarray,
609-
ome_period: np.ndarray | None = None,
610-
refit_idx: dict[str, list[Any]] | None = None,
611-
) -> tuple[dict[str, list[Any]], dict[str, list[Any]], dict[str, list[Any]]]:
612-
hkls: dict[str, Any] = {}
613-
xyo_det: dict[str, Any] = {}
614-
idx_0: dict[str, Any] = {}
615-
for det_key, panel in instr.detectors.items():
616-
hkls[det_key] = []
617-
xyo_det[det_key] = []
618-
idx_0[det_key] = []
619-
620-
for ig, grain_id in enumerate(grain_ids):
621-
data = spots_data[grain_id][1][det_key]
622-
# Convert to numpy array to make operations easier
623-
data = np.array(data, dtype=object)
624-
625-
# FIXME: hexrd is not happy if some detectors end up with no
626-
# grain data, which sometimes happens with subpanels like Dexelas
627-
if data.size == 0:
628-
idx_0[det_key].append(np.empty((0,)))
629-
hkls[det_key].append(np.empty((0, 3)))
630-
xyo_det[det_key].append(np.empty((0, 3)))
654+
def compute_pred_angs(
655+
calibrators: list[Calibrator],
656+
) -> dict[str, list[np.ndarray]]:
657+
"""Recompute predicted (tth, eta, ome) using current grain/instrument state.
658+
659+
For each calibrator (grain) and detector, calls oscill_angles_of_hkls()
660+
with current grain parameters and selects the omega solution closest
661+
to the measured omega.
662+
"""
663+
instr = calibrators[0].instr
664+
chi = instr.chi
665+
bvec = instr.beam_vector
666+
tvec_s = instr.tvec
667+
wavelength = instr.beam_wavelength
668+
energy_correction = instr.energy_correction
669+
670+
pred_angs: dict[str, list[np.ndarray]] = {}
671+
for calibrator in calibrators:
672+
grain = calibrator.grain_params
673+
rmat_c = xfcapi.make_rmat_of_expmap(grain[:3])
674+
tvec_c = grain[3:6]
675+
vinv_s = grain[6:]
676+
bmat = calibrator.bmatx
677+
ome_period = calibrator.ome_period
678+
679+
corrected_wavelength = apply_correction_to_wavelength(
680+
wavelength,
681+
energy_correction,
682+
tvec_s,
683+
tvec_c,
684+
)
685+
686+
for det_key in instr.detectors:
687+
pred_angs.setdefault(det_key, [])
688+
689+
hkls = np.asarray(
690+
calibrator.data_dict['hkls'][det_key],
691+
dtype=float,
692+
)
693+
xyo = np.asarray(
694+
calibrator.data_dict['pick_xys'][det_key],
695+
dtype=float,
696+
)
697+
698+
if hkls.size == 0:
699+
pred_angs[det_key].append(np.empty((0, 3)))
631700
continue
632701

633-
valid_reflections = data[:, 0] >= 0
634-
not_saturated = data[:, 4] < panel.saturation_level
702+
# Two omega solutions per HKL
703+
oangs0, oangs1 = xfcapi.oscill_angles_of_hkls(
704+
hkls,
705+
chi,
706+
rmat_c,
707+
bmat,
708+
corrected_wavelength,
709+
v_inv=vinv_s,
710+
beam_vec=bvec,
711+
)
712+
713+
# Select the solution whose omega is closest to measured
714+
meas_omes = mapAngle(xyo[:, 2], ome_period)
715+
calc_omes = np.vstack(
716+
[
717+
mapAngle(oangs0[:, 2], ome_period),
718+
mapAngle(oangs1[:, 2], ome_period),
719+
]
720+
) # (2, n)
721+
diff = np.abs(
722+
angularDifference(
723+
np.tile(meas_omes, (2, 1)),
724+
calc_omes,
725+
)
726+
)
727+
best = np.argmin(diff, axis=0) # 0 or 1 per reflection
728+
729+
n = len(hkls)
730+
idx = np.arange(n)
731+
both = np.stack([oangs0, oangs1]) # (2, n, 3)
732+
pred_angs[det_key].append(both[best, idx])
635733

636-
if refit_idx is None:
637-
idx = np.logical_and(valid_reflections, not_saturated)
638-
idx_0[det_key].append(idx)
639-
else:
640-
idx = refit_idx[det_key][ig]
641-
idx_0[det_key].append(idx)
734+
return pred_angs
642735

643-
if not np.any(idx):
644-
idx_0[det_key].append(np.empty((0,)))
645-
hkls[det_key].append(np.empty((0, 3)))
646-
xyo_det[det_key].append(np.empty((0, 3)))
736+
737+
def compute_meas_angs(
738+
calibrators: list[Calibrator],
739+
xyo_det: dict[str, list[np.ndarray]],
740+
) -> dict[str, list[np.ndarray]]:
741+
"""Convert measured detector XY to angular coordinates using current geometry.
742+
743+
Uses panel.cart_to_angles() with per-reflection rmat_s (from chi +
744+
measured omega) to account for grain position offset.
745+
"""
746+
instr = calibrators[0].instr
747+
chi = instr.chi
748+
tvec_s = instr.tvec
749+
750+
meas_angs: dict[str, list[np.ndarray]] = {}
751+
for ig, calibrator in enumerate(calibrators):
752+
grain = calibrator.grain_params
753+
tvec_c = grain[3:6]
754+
755+
for det_key, panel in instr.detectors.items():
756+
meas_angs.setdefault(det_key, [])
757+
758+
xyo = xyo_det[det_key][ig]
759+
if xyo.size == 0:
760+
meas_angs[det_key].append(np.empty((0, 3)))
647761
continue
648762

649-
hkls[det_key].append(np.vstack(data[idx, 2]))
650-
meas_omes = np.vstack(data[idx, 6])[:, 2].reshape(sum(idx), 1)
651-
xyo_det_values = np.hstack([np.vstack(data[idx, 7]), meas_omes])
763+
xy = xyo[:, :2]
764+
omes = xyo[:, 2]
652765

653-
# re-map omegas if need be
654-
if ome_period is not None:
655-
xyo_det_values[:, 2] = mapAngle(
656-
xyo_det_values[:, 2],
657-
ome_period,
766+
# Undistort measured positions before converting to angles
767+
if panel.distortion is not None:
768+
xy = panel.distortion.apply(xy)
769+
770+
n = len(omes)
771+
result = np.empty((n, 3))
772+
for i in range(n):
773+
rmat_s = xfcapi.make_sample_rmat(chi, omes[i])
774+
tth_eta, _ = panel.cart_to_angles(
775+
xy[i : i + 1],
776+
rmat_s=rmat_s,
777+
tvec_s=tvec_s,
778+
tvec_c=tvec_c,
658779
)
780+
result[i, :2] = tth_eta[0]
781+
result[i, 2] = omes[i]
659782

660-
xyo_det[det_key].append(xyo_det_values)
783+
meas_angs[det_key].append(result)
661784

662-
return hkls, xyo_det, idx_0
785+
return meas_angs
663786

664787

665788
REFINEMENT_OPTIONS = {

hexrdgui/calibration/hedm/calibration_results_dialog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def setup_canvas(self) -> None:
100100
self.ui.canvas_layout.addWidget(canvas)
101101

102102
ax[0].grid(True)
103-
ax[0].axis('equal')
103+
ax[0].set_aspect('equal', adjustable='box')
104104
ax[0].set_xlabel('detector X [mm]')
105105
ax[0].set_ylabel('detector Y [mm]')
106106

0 commit comments

Comments
 (0)