Skip to content

Commit a1bcd23

Browse files
authored
Merge branch 'FABLE-3DXRD:master' into master
2 parents 600d6de + 0eaaaf9 commit a1bcd23

3 files changed

Lines changed: 1177 additions & 409 deletions

File tree

ImageD11/friedel_pairs.py

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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

623614
def _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
19441933
def 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

Comments
 (0)