Skip to content

Commit 8fc125e

Browse files
committed
Add dialog for HEDM calibration diagnostics
This dialog provides several plots indicating how good of a fit the calibration is. It is a new "Spot Diagnostics" button in the HEDM calibration dialog. It is also available in the HEDM workflow, after fit-grains has finished. Signed-off-by: Patrick Avery <patrick.avery@kitware.com>
1 parent dbf06df commit 8fc125e

File tree

11 files changed

+1313
-52
lines changed

11 files changed

+1313
-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: 164 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,131 @@ 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], dtype=float,
691+
)
692+
xyo = np.asarray(
693+
calibrator.data_dict['pick_xys'][det_key], dtype=float,
694+
)
695+
696+
if hkls.size == 0:
697+
pred_angs[det_key].append(np.empty((0, 3)))
631698
continue
632699

633-
valid_reflections = data[:, 0] >= 0
634-
not_saturated = data[:, 4] < panel.saturation_level
700+
# Two omega solutions per HKL
701+
oangs0, oangs1 = xfcapi.oscill_angles_of_hkls(
702+
hkls,
703+
chi,
704+
rmat_c,
705+
bmat,
706+
corrected_wavelength,
707+
v_inv=vinv_s,
708+
beam_vec=bvec,
709+
)
710+
711+
# Select the solution whose omega is closest to measured
712+
meas_omes = mapAngle(xyo[:, 2], ome_period)
713+
calc_omes = np.vstack([
714+
mapAngle(oangs0[:, 2], ome_period),
715+
mapAngle(oangs1[:, 2], ome_period),
716+
]) # (2, n)
717+
diff = np.abs(angularDifference(
718+
np.tile(meas_omes, (2, 1)), calc_omes,
719+
))
720+
best = np.argmin(diff, axis=0) # 0 or 1 per reflection
721+
722+
n = len(hkls)
723+
idx = np.arange(n)
724+
both = np.stack([oangs0, oangs1]) # (2, n, 3)
725+
pred_angs[det_key].append(both[best, idx])
726+
727+
return pred_angs
728+
729+
730+
def compute_meas_angs(
731+
calibrators: list[Calibrator],
732+
xyo_det: dict[str, list[np.ndarray]],
733+
) -> dict[str, list[np.ndarray]]:
734+
"""Convert measured detector XY to angular coordinates using current geometry.
735+
736+
Uses panel.cart_to_angles() with per-reflection rmat_s (from chi +
737+
measured omega) to account for grain position offset.
738+
"""
739+
instr = calibrators[0].instr
740+
chi = instr.chi
741+
tvec_s = instr.tvec
635742

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)
743+
meas_angs: dict[str, list[np.ndarray]] = {}
744+
for ig, calibrator in enumerate(calibrators):
745+
grain = calibrator.grain_params
746+
tvec_c = grain[3:6]
642747

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)))
748+
for det_key, panel in instr.detectors.items():
749+
meas_angs.setdefault(det_key, [])
750+
751+
xyo = xyo_det[det_key][ig]
752+
if xyo.size == 0:
753+
meas_angs[det_key].append(np.empty((0, 3)))
647754
continue
648755

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])
756+
xy = xyo[:, :2]
757+
omes = xyo[:, 2]
652758

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,
759+
# Undistort measured positions before converting to angles
760+
if panel.distortion is not None:
761+
xy = panel.distortion.apply(xy)
762+
763+
n = len(omes)
764+
result = np.empty((n, 3))
765+
for i in range(n):
766+
rmat_s = xfcapi.make_sample_rmat(chi, omes[i])
767+
tth_eta, _ = panel.cart_to_angles(
768+
xy[i:i+1],
769+
rmat_s=rmat_s,
770+
tvec_s=tvec_s,
771+
tvec_c=tvec_c,
658772
)
773+
result[i, :2] = tth_eta[0]
774+
result[i, 2] = omes[i]
659775

660-
xyo_det[det_key].append(xyo_det_values)
776+
meas_angs[det_key].append(result)
661777

662-
return hkls, xyo_det, idx_0
778+
return meas_angs
663779

664780

665781
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)