1010import hexrd .constants as cnst
1111from hexrd .fitting .calibration .calibrator import Calibrator
1212from 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
1416from hexrdgui .calibration .calibration_dialog import CalibrationDialog
1517from 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
1824from hexrdgui .calibration .tree_item_models import CalibrationTreeItemModel
1925from hexrdgui .calibration .material_calibration_dialog_callbacks import (
2026 MaterialCalibrationDialogCallbacks ,
2733
2834class 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
665781REFINEMENT_OPTIONS = {
0 commit comments