@@ -124,6 +124,7 @@ def _get_y0(self, y0):
124124 if y0 is None :
125125 y0 = 0.5 * (self .dataset .ymax + self .dataset .ymin )
126126 self .dataset .y0 = y0
127+ self .dataset .save ()
127128 self .dataset .correct_bins_for_half_scan (self .dataset .y0 )
128129
129130 # --- Internal methods ---
@@ -516,18 +517,14 @@ def _extended_pair_selec(self, pair_id, subset_type='eta_bins'):
516517 # return sorted indices
517518 return np .sort (b1 ), np .sort (b2 )
518519
519- def check_symmetry (self , saveplot = False ):
520+ def check_symmetry (self ):
520521 """
521522 Plot N_peaks and total_intensity vs. position for mirror dty scans
522523 (y0+Δy, y0-Δy) or mirror eta bins (eta, eta+180°) and check their correlation.
523524
524525 If alignment is correct, values should match between mirror pairs.
525526 Significant mismatch suggests an incorrect y0 (for dty), sample movement,
526527 or a beam issue during scanning.
527-
528- Parameters
529- ----------
530- saveplot : bool, save figure to analysispath (dty mode only)
531528 """
532529 # ── auto-detect mode ─────────────────────────────────────────────────
533530 mode = None
@@ -611,13 +608,7 @@ def check_symmetry(self, saveplot=False):
611608 fig .suptitle (title , fontsize = 14 )
612609 plt .tight_layout ()
613610
614- if saveplot and mode == 'dty' :
615- fname = os .path .join (
616- self .dataset .analysispath ,
617- self .dataset .dsname + '_dty_alignment.svg' )
618- fig .savefig (fname , format = 'svg' )
619-
620- return fig
611+ return fig , axes
621612
622613
623614def _rle1 (a ):
@@ -705,9 +696,9 @@ def __repr__(self):
705696 n_steps = self .n_steps ,
706697 n_out = len (self .outputs )))
707698
708- def set_peak_subsets (self , n_eta_bins = 360 ):
699+ def set_peak_subsets (self , n_eta_bins = 360 , y0 = None ):
709700 self .logger .info ('--- SET PEAKSUBSETS ---' )
710- self .PeakSubsets = PeakSubsets (self .cf , self .ds , n_eta_bins )
701+ self .PeakSubsets = PeakSubsets (self .cf , self .ds , n_eta_bins , y0 )
711702
712703 def sort_peak_subsets (self , pair_type = 'omega' ):
713704 if pair_type == 'omega' :
@@ -880,8 +871,8 @@ def match_friedel_pairs_by_chunks(self,
880871
881872 # pilot_chunk = 'eta_bins' if chunk_type == 'eta_bins' else 'scans'
882873
883- # pick a pair near the middle — avoids edge scans with fewer peaks
884- s0 = len ( pairs_list ) // 2
874+ # pick a pair near the middle
875+ s0 = 0
885876 while not valid_pairs [s0 ]:
886877 s0 += 1
887878
@@ -1262,9 +1253,9 @@ def estimate_search_scales(self, idx1, idx2, dist_cutoff, pair_type='omega'):
12621253 d_gv , d_eta , d_logI = _physical_pair_distance ( cf , idx1 [rows ], idx2 [cols ], pair_type )
12631254
12641255 # scale factors: median absolute deviation — robust to outliers
1265- d_gv_med = np .median (d_gv )
1266- d_eta_med = np .median (d_eta )
1267- d_logI_med = np .median (d_logI )
1256+ d_gv_med = np .nanmedian (d_gv )
1257+ d_eta_med = np .nanmedian (d_eta )
1258+ d_logI_med = np .nanmedian (d_logI )
12681259
12691260 scales = np .array ([d_gv_med , d_gv_med , d_gv_med , d_eta_med , d_logI_med ])
12701261 scales = np .where (scales < 1e-3 * np .median (scales ), np .inf , scales ) # near zero -> inf
@@ -1436,8 +1427,6 @@ def merge_outputs(self, cf_target=None, pair_type='omega',
14361427 self .logger .info ('Done' )
14371428
14381429
1439-
1440-
14411430# ─────────────────────────────────────────────
14421431# Friedel Pair Indexing Helpers
14431432# ─────────────────────────────────────────────
@@ -1944,7 +1933,9 @@ def plot_pair_distances(cf, pair_type='omega', bins=50, log_scale=False, **kwarg
19441933def locate_eta_pairs (cf , pairs , ds = None , y0 = 0. ):
19451934 """
19461935 Fit the centre of mass position of eta-pairs and write results
1947- back into cf as new columns 'xs' and 'ys'.
1936+ back into cf as new columns 'sx' and 'sy'.
1937+ Works only for scanning-3DXRD. For Box-beam, the relocation problems
1938+ has two more degrees of freedom and cannot be solved with only one pair.
19481939
19491940 Parameters
19501941 ----------
@@ -1963,41 +1954,48 @@ def locate_eta_pairs(cf, pairs, ds=None, y0=0.):
19631954 R = np .transpose (((so [i1 ], co [i1 ]),
19641955 (so [i2 ], co [i2 ])), axes = (2 , 0 , 1 ))
19651956
1966- xs_pairs , ys_pairs = np .linalg .solve (R , y ).T
1957+ sx_pairs , sy_pairs = np .linalg .solve (R , y ).T
19671958
1968- # ── write into cf. Overwrite pre-existing xs,ys
1969- xs = np .full (cf .nrows , np .nan )
1970- ys = np .full (cf .nrows , np .nan )
1971- xs [i1 ] = xs_pairs ; xs [i2 ] = xs_pairs
1972- ys [i1 ] = ys_pairs ; ys [i2 ] = ys_pairs
1959+ # ── write into cf. Overwrite pre-existing sx, sy
1960+ sx = np .full (cf .nrows , np .nan )
1961+ sy = np .full (cf .nrows , np .nan )
1962+ sx [i1 ] = sx_pairs ; sx [i2 ] = sx_pairs
1963+ sy [i1 ] = sy_pairs ; sy [i2 ] = sy_pairs
19731964
1974- for name , col in (('xs ' , xs ), ('ys ' , ys )):
1965+ for name , col in (('sx ' , sx ), ('sy ' , sy )):
19751966 cf .addcolumn (col , name )
19761967
1977- return xs , ys
1968+ return sx , sy
19781969
19791970
1980- def locate_omega_pairs (cf , pairs , ds = None , y0 = 0 ):
1971+ def locate_omega_pairs (cf , pairs , ds = None , y0 = None ):
19811972 """
19821973 Fit the centre of mass position of omega-pairs and write results
1983- back into cf as new columns 'xs' and 'ys'.
1974+ back into cf as new columns 'sx' and 'sy'.
1975+ Works only for scanning-3DXRD. For Box-beam, the relocation problems
1976+ has two more degrees of freedom and cannot be solved with only one pair.
1977+
19841978 For omega-pairs the linear system used in 'locate_eta_pairs' is near-singular
19851979 (paired peaks are ~180 degrees apart in omega), so the position is recovered from
19861980 two-theta and dty:
19871981 dx : x-shift from rotation centre, derived from tth asymmetry
19881982 ylab: y-shift in lab frame, taken as dty - y0
19891983 Then rotate (dx, ylab) back into sample coordinates using omega.
1990- xlab, ylab and omega are averaged to make sure both peaks in each pair have the same (xs,ys ) coordinates
1984+ xlab, ylab and omega are averaged to make sure both peaks in each pair have the same (sx, sy ) coordinates
19911985 """
19921986 i1 , i2 = pairs
19931987 L = cf .parameters .get ('distance' )
1994- if ds is not None and 'frelon' in ds . detector :
1988+ if ds is not None and ds . dtymotor == 'diffty' : # diffty is in mm on TDXRD sation
19951989 L /= 1000
1996-
1990+ if hasattr (ds , 'y0' ):
1991+ y0 = ds .y0
1992+ elif y0 is None :
1993+ y0 = 0.5 * (ds .ymax + ds .ymin )
1994+
19971995 # ── dx from two-theta asymmetry —
19981996 tantth1 = np .tan (np .radians (cf .tth [i1 ]))
19991997 tantth2 = np .tan (np .radians (cf .tth [i2 ]))
2000- dx = L * (tantth1 - tantth2 ) / (tantth1 + tantth2 )
1998+ dx = L * (tantth1 - tantth2 ) / (tantth1 + tantth2 )
20011999
20022000 # ── ylab from averaged dty —
20032001 dy = (cf .dty [i1 ] - cf .dty [i2 ]) / 2
@@ -2012,31 +2010,101 @@ def locate_omega_pairs(cf, pairs, ds=None, y0=0):
20122010 so_av = (so [i1 ] - so [i2 ]) / 2
20132011
20142012 # ── lab -> sample rotation ───
2015- # xs = co_av * xlab + so_av * ylab
2016- # ys = -so_av * xlab + co_av * ylab
2017- xs_i1 = co_av * xlab_i1 + so_av * ylab_i1
2018- ys_i1 = - so_av * xlab_i1 + co_av * ylab_i1
2013+ # sx = co_av * xlab + so_av * ylab
2014+ # sy = -so_av * xlab + co_av * ylab
2015+ sx_i1 = co_av * xlab_i1 + so_av * ylab_i1
2016+ sy_i1 = - so_av * xlab_i1 + co_av * ylab_i1
20192017
2020- xs_i2 = - co_av * xlab_i2 - so_av * ylab_i2 # (-R applied)
2021- ys_i2 = so_av * xlab_i2 - co_av * ylab_i2
2018+ sx_i2 = - co_av * xlab_i2 - so_av * ylab_i2 # (-R applied)
2019+ sy_i2 = so_av * xlab_i2 - co_av * ylab_i2
20222020
2023- # xs_i1 == xs_i2 and ys_i1 == ys_i2 by construction — average for numerical safety
2024- xs_pairs = 0.5 * (xs_i1 + xs_i2 )
2025- ys_pairs = 0.5 * (ys_i1 + ys_i2 )
2021+ # sx_i1 == sx_i2 and sy_i1 == sy_i2 by construction — average for numerical safety
2022+ sx_pairs = 0.5 * (sx_i1 + sx_i2 )
2023+ sy_pairs = 0.5 * (sy_i1 + sy_i2 )
20262024
20272025 # ── scatter into cf-length arrays ───
2028- xs = np .full (cf .nrows , np .nan )
2029- ys = np .full (cf .nrows , np .nan )
2030- xs [i1 ] = xs_pairs ; xs [i2 ] = xs_pairs
2031- ys [i1 ] = ys_pairs ; ys [i2 ] = ys_pairs
2026+ sx = np .full (cf .nrows , np .nan )
2027+ sy = np .full (cf .nrows , np .nan )
2028+ sx [i1 ] = sx_pairs ; sx [i2 ] = sx_pairs
2029+ sy [i1 ] = sy_pairs ; sy [i2 ] = sy_pairs
20322030
20332031 # ── write to cf ──
2034- for name , col in (('xs ' , xs ), ('ys ' , ys )):
2032+ for name , col in (('sx ' , sx ), ('sy ' , sy )):
20352033 if name in cf .titles :
20362034 cf .getcolumn (name )[:] = col
20372035 else :
20382036 cf .addcolumn (col , name )
20392037
2040- return xs , ys
2038+ return sx , sy
2039+
2040+
2041+ def update_geometry_fpairs (cf , ds = None , add_xyz_lab = False , relocate_pairs = True ):
2042+ """
2043+ Update diffraction geometry for (scanning)-3DXRD data using Friedel-pairs (omega-pair) symmetry correction:
2044+ similar to cf.updateGeometry() but uses Friedel-pairs to correct for the offset from rotation-center.
2045+ Details in Jacob et al. 2024, https://doi.org/10.1107/S1600576724009634
2046+
2047+ Update the following columns in the ColumnFile:
2048+ 'tth', 'eta', 'ds', 'gx', 'gy', 'gz'
2049+ If ``add_xyz_lab`` True:
2050+ 'xl', 'yl', 'zl' also updated
2051+ If ``relocate_pairs`` True:
2052+ add 'sx' and 'sy' coordinates computed from omega pairs
2053+
2054+ Parameters
2055+ ----------
2056+ - cf : ImageD11 ColumnFile
2057+ - ds : ImageD11.sinogram dataset (optional).
2058+ - add_xyz_lab : bool, optional. Add corrected lab-frame coordinates to cf
2059+ - relocate_fpairs
2060+ """
2061+ if cf .parameters is None :
2062+ raise ValueError (
2063+ 'No parameters in cf'
2064+ )
2065+ pars = cf .parameters
2066+ assert "omega_pair_id" in cf .titles , 'no omega pairs in cf. Compute them first'
2067+ if "sc" in cf .titles and "fc" in cf .titles :
2068+ sc , fc = cf .sc , cf .fc
2069+ elif "xc" in cf .titles and "yc" in cf .titles :
2070+ sc , fc = cf .xc , cf .yc
2071+ else :
2072+ raise Exception ("columnfile file misses xc/yc or sc/fc" )
2073+
2074+ # xyz coordinates in non-corrected geometry.
2075+ comp = transform .Ctransform (pars .parameters )
2076+ xyz_non_corr = comp .sf2xyz (sc , fc )
2077+ i1 , i2 = get_pairs (cf , pair_type = 'omega' )
2078+
2079+ # SOURCE RELOCATION: use raw (non-corrected) tth
2080+ if relocate_pairs :
2081+ tth_raw = comp .xyz2geometry ( xyz_non_corr , cf .omega , tx = 0 , ty = 0 , tz = 0 )[:,0 ]
2082+ cf .addcolumn (tth_raw , 'tth' )
2083+ _ = locate_omega_pairs (cf , (i1 ,i2 ), ds )
2084+
2085+ # FRIEDEL PAIR CORRECTION
2086+ # Back-rotate half of the peaks to give a more intuitive geometry where paired peaks are aligned with the diffraction source origin
2087+ R = np .diag ([- 1 ,- 1 ,1 ])
2088+ xyz_corr = 1 / 2 * (xyz_non_corr [i1 ] - xyz_non_corr [i2 ].dot (R ))
2089+
2090+ # merge arrays to go back to original colf size
2091+ xyz_corr_full = np .full ( (cf .nrows , 3 ), np .nan , dtype = xyz_non_corr .dtype )
2092+ xyz_corr_full [i1 ] = xyz_corr
2093+ xyz_corr_full [i2 ] = xyz_corr .dot (- R )
2094+ del xyz_corr
2095+
2096+ # compute geometry with corrected peaks
2097+ out = comp .xyz2geometry ( xyz_corr_full , cf .omega , tx = 0 , ty = 0 , tz = 0 )
2098+ for i ,name in enumerate (("tth" , "eta" , "ds" , "gx" , "gy" , "gz" )):
2099+ cf .addcolumn ( out [:,i ], name )
2100+ if add_xyz_lab :
2101+ for i ,name in enumerate (['xl' ,'yl' ,'zl' ]):
2102+ cf .addcolumn ( xyz_corr_full [:,i ], name )
2103+
2104+
2105+
2106+
2107+
2108+
20412109
20422110
0 commit comments