Skip to content

Commit 3226d35

Browse files
authored
Merge pull request #819 from HEXRD/grain-energy-y-correction
Add beam energy correction to grains
2 parents 096b091 + a0275ac commit 3226d35

File tree

6 files changed

+173
-9
lines changed

6 files changed

+173
-9
lines changed

hexrd/fitting/calibration/grain.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from hexrd import matrixutil as mutil
66
from hexrd.rotations import angularDifference
77
from hexrd.transforms import xfcapi
8+
from hexrd import xrdutil
89

910
from .abstract_grain import AbstractGrainCalibrator
1011
from .lmfit_param_handling import DEFAULT_EULER_CONVENTION
@@ -81,6 +82,7 @@ def sxcal_obj_func(grain_params, instr, xyo_det, hkls_idx,
8182
bvec = instr.beam_vector
8283
chi = instr.chi
8384
tvec_s = instr.tvec
85+
energy_correction = instr.energy_correction
8486

8587
# right now just stuck on the end and assumed
8688
# to all be the same length... FIX THIS
@@ -127,9 +129,17 @@ def sxcal_obj_func(grain_params, instr, xyo_det, hkls_idx,
127129
ghat_s = mutil.unitVector(np.dot(vmat_s, np.dot(rmat_c, gvec_c)))
128130
ghat_c = np.dot(rmat_c.T, ghat_s)
129131

132+
# Apply an energy correction according to grain position
133+
corrected_wavelength = xrdutil.apply_correction_to_wavelength(
134+
wavelength,
135+
energy_correction,
136+
tvec_s,
137+
tvec_c,
138+
)
139+
130140
match_omes, calc_omes_tmp = grainutil.matchOmegas(
131141
xyo, ghkls.T,
132-
chi, rmat_c, bmat, wavelength,
142+
chi, rmat_c, bmat, corrected_wavelength,
133143
vInv=vinv_s,
134144
beamVec=bvec,
135145
omePeriod=ome_period)

hexrd/fitting/calibration/lmfit_param_handling.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ def create_instr_params(instr, euler_convention=DEFAULT_EULER_CONVENTION,
5151
parms_list.append((
5252
names['beam_energy'], energy, False, energy - 0.2, energy + 0.2
5353
))
54+
if beam.get('energy_correction') is not None:
55+
base_name = names['beam_energy_correction']
56+
intercept = beam['energy_correction']['intercept']
57+
slope = beam['energy_correction']['slope']
58+
parms_list.append((
59+
f'{base_name}_intercept',
60+
intercept,
61+
False,
62+
-np.inf,
63+
np.inf,
64+
))
65+
parms_list.append((
66+
f'{base_name}_slope', slope, False, -np.inf, np.inf
67+
))
5468

5569
parms_list.append(('instr_chi', np.degrees(instr.chi),
5670
False, np.degrees(instr.chi)-1,
@@ -210,6 +224,7 @@ def create_beam_param_names(instr: HEDMInstrument) -> dict[str, str]:
210224
'beam_polar': f'{prefix}beam_polar',
211225
'beam_azimuth': f'{prefix}beam_azimuth',
212226
'beam_energy': f'{prefix}beam_energy',
227+
'beam_energy_correction': f'{prefix}beam_energy_correction',
213228
}
214229
return param_names
215230

@@ -258,12 +273,21 @@ def update_instrument_from_params(
258273
# This supports single XRS or multi XRS
259274
beam_param_names = create_beam_param_names(instr)
260275
for xrs_name, param_names in beam_param_names.items():
276+
beam = instr.beam_dict[xrs_name]
261277
energy = params[param_names['beam_energy']].value
262278
azim = params[param_names['beam_azimuth']].value
263279
pola = params[param_names['beam_polar']].value
264280

265-
instr.beam_dict[xrs_name]['energy'] = energy
266-
instr.beam_dict[xrs_name]['vector'] = calc_beam_vec(azim, pola)
281+
beam['energy'] = energy
282+
beam['vector'] = calc_beam_vec(azim, pola)
283+
284+
if beam.get('energy_correction'):
285+
base_name = param_names['beam_energy_correction']
286+
slope = params[f'{base_name}_slope'].value
287+
intercept = params[f'{base_name}_intercept'].value
288+
289+
beam['energy_correction']['slope'] = slope
290+
beam['energy_correction']['intercept'] = intercept
267291

268292
# Trigger any needed updates from beam modifications
269293
instr.beam_dict_modified()

hexrd/fitting/grains.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from hexrd import constants
1111
from hexrd import rotations
1212

13-
from hexrd.xrdutil import extract_detector_transformation
13+
from hexrd.xrdutil import (
14+
apply_correction_to_wavelength,
15+
extract_detector_transformation,
16+
)
1417

1518
return_value_flag = None
1619

@@ -185,6 +188,8 @@ def objFuncFitGrain(gFit, gFull, gFlag,
185188
"""
186189
bVec = instrument.beam_vector
187190
eVec = instrument.eta_vector
191+
tVec_s = instrument.tvec
192+
energy_correction = instrument.energy_correction
188193

189194
# fill out parameters
190195
gFull[gFlag] = gFit
@@ -195,6 +200,14 @@ def objFuncFitGrain(gFit, gFull, gFlag,
195200
vInv_s = gFull[6:]
196201
vMat_s = mutil.vecMVToSymm(vInv_s) # NOTE: Inverse of V from F = V * R
197202

203+
# Apply an energy correction according to grain position
204+
corrected_wavelength = apply_correction_to_wavelength(
205+
wavelength,
206+
energy_correction,
207+
tVec_s,
208+
tVec_c,
209+
)
210+
198211
# loop over instrument panels
199212
# CAVEAT: keeping track of key ordering in the "detectors" attribute of
200213
# instrument here because I am not sure if instatiating them using
@@ -265,7 +278,7 @@ def objFuncFitGrain(gFit, gFull, gFlag,
265278

266279
# !!!: check that this operates on UNWARPED xy
267280
match_omes, calc_omes = matchOmegas(
268-
meas_xyo, hkls, chi, rMat_c, bMat, wavelength,
281+
meas_xyo, hkls, chi, rMat_c, bMat, corrected_wavelength,
269282
vInv=vInv_s, beamVec=bVec, etaVec=eVec,
270283
omePeriod=omePeriod)
271284

hexrd/instrument/detector.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,7 @@ def simulate_rotation_series(
14931493
chi=0.0,
14941494
tVec_s=ct.zeros_3,
14951495
wavelength=None,
1496+
energy_correction=None,
14961497
):
14971498
"""
14981499
Simulate a monochromatic rotation series for a list of grains.
@@ -1561,6 +1562,14 @@ def simulate_rotation_series(
15611562
tVec_c = gparm[3:6]
15621563
vInv_s = gparm[6:]
15631564

1565+
# Apply an energy correction according to grain position
1566+
corrected_wavelength = xrdutil.apply_correction_to_wavelength(
1567+
wavelength,
1568+
energy_correction,
1569+
tVec_s,
1570+
tVec_c,
1571+
)
1572+
15641573
# All possible bragg conditions as vstacked [tth, eta, ome]
15651574
# for each omega solution
15661575
angList = np.vstack(
@@ -1569,7 +1578,7 @@ def simulate_rotation_series(
15691578
chi,
15701579
rMat_c,
15711580
bMat,
1572-
wavelength,
1581+
corrected_wavelength,
15731582
v_inv=vInv_s,
15741583
beam_vec=self.bvec,
15751584
)

hexrd/instrument/hedm_instrument.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import os
3737
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
3838
from functools import partial
39-
from typing import Optional
39+
from typing import Optional, Union
4040

4141
from tqdm import tqdm
4242

@@ -604,6 +604,7 @@ def __init__(self, instrument_config=None,
604604
beam['vector']['polar_angle'],
605605
),
606606
'distance': beam.get('source_distance', np.inf),
607+
'energy_correction': beam.get('energy_correction', None),
607608
}
608609

609610
# Set the active beam name if not set already
@@ -811,6 +812,7 @@ def _create_default_beam(self):
811812
'energy': beam_energy_DFLT,
812813
'vector': beam_vec_DFLT.copy(),
813814
'distance': np.inf,
815+
'energy_correction': None,
814816
}
815817

816818
if self._active_beam_name is None:
@@ -889,6 +891,51 @@ def source_distance(self, x):
889891
self.active_beam['distance'] = x
890892
self.beam_dict_modified()
891893

894+
@property
895+
def energy_correction(self) -> Union[dict, None]:
896+
"""Energy correction dict appears as follows:
897+
898+
{
899+
# The beam energy gradient center, along the specified
900+
# axis, in millimeters.
901+
'intercept': 0.0,
902+
903+
# The slope of the beam energy gradient along the
904+
# specified axis, in eV/mm.
905+
'slope': 0.0,
906+
907+
# The specified axis for the beam energy gradient,
908+
# either 'x' or 'y'.
909+
'axis': 'y',
910+
}
911+
"""
912+
return self.active_beam['energy_correction']
913+
914+
@energy_correction.setter
915+
def energy_correction(self, v: Union[dict, None]):
916+
if v is not None:
917+
# First validate
918+
keys = sorted(list(v))
919+
default_keys = sorted(list(
920+
self.create_default_energy_correction()
921+
))
922+
if keys != default_keys:
923+
msg = (
924+
f'Keys in energy correction dict, "{keys}", do not match '
925+
f'the required keys: "{default_keys}"'
926+
)
927+
raise RuntimeError(msg)
928+
929+
self.active_beam['energy_correction'] = v
930+
931+
@staticmethod
932+
def create_default_energy_correction() -> dict[str, float]:
933+
return {
934+
'intercept': 0.0, # in mm
935+
'slope': 0.0, # eV/mm
936+
'axis': 'y',
937+
}
938+
892939
@property
893940
def eta_vector(self):
894941
return self._eta_vector
@@ -932,6 +979,11 @@ def write_config(self, file=None, style='yaml', calibration_dict={}):
932979
if beam['distance'] != np.inf:
933980
beam_dict[beam_name]['source_distance'] = beam['distance']
934981

982+
if beam.get('energy_correction') is not None:
983+
beam_dict[beam_name]['energy_correction'] = beam[
984+
'energy_correction'
985+
]
986+
935987
if len(beam_dict) == 1:
936988
# Just write it out a single beam (classical way)
937989
beam_dict = next(iter(beam_dict.values()))
@@ -1508,7 +1560,8 @@ def simulate_rotation_series(self, plane_data, grain_param_list,
15081560
ome_ranges=ome_ranges,
15091561
ome_period=ome_period,
15101562
chi=self.chi, tVec_s=self.tvec,
1511-
wavelength=wavelength)
1563+
wavelength=wavelength,
1564+
energy_correction=self.energy_correction)
15121565
return results
15131566

15141567
def pull_spots(self, plane_data, grain_params,

hexrd/xrdutil/utils.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,7 @@ def simulateGVecs(
941941
pixel_pitch: tuple[float] = (0.2, 0.2),
942942
distortion: DistortionABC = None,
943943
beam_vector: np.ndarray = constants.beam_vec,
944+
energy_correction: Union[dict, None] = None,
944945
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
945946
"""
946947
returns valid_ids, valid_hkl, valid_ang, valid_xy, ang_ps
@@ -988,10 +989,18 @@ def simulateGVecs(
988989
vInv_s = np.ascontiguousarray(grain_params[6:12])
989990
beam_vector = np.ascontiguousarray(beam_vector)
990991

992+
# Apply an energy correction according to grain position
993+
corrected_wlen = apply_correction_to_wavelength(
994+
wlen,
995+
energy_correction,
996+
tVec_s,
997+
tVec_c,
998+
)
999+
9911000
# first find valid G-vectors
9921001
angList = np.vstack(
9931002
xfcapi.oscill_angles_of_hkls(
994-
full_hkls[:, 1:], chi, rMat_c, bMat, wlen, v_inv=vInv_s,
1003+
full_hkls[:, 1:], chi, rMat_c, bMat, corrected_wlen, v_inv=vInv_s,
9951004
beam_vec=beam_vector
9961005
)
9971006
)
@@ -1514,3 +1523,49 @@ def extract_detector_transformation(
15141523
chi = detector_params[6]
15151524
tVec_s = np.ascontiguousarray(detector_params[7:10])
15161525
return rMat_d, tVec_d, chi, tVec_s
1526+
1527+
1528+
def apply_correction_to_wavelength(
1529+
wavelength: float,
1530+
energy_correction: Union[dict, None],
1531+
tvec_s: np.ndarray,
1532+
tvec_c: np.ndarray,
1533+
) -> float:
1534+
"""Apply an energy correction to the wavelength according to grain position
1535+
1536+
The energy correction dict appears as follows:
1537+
1538+
{
1539+
# The beam energy gradient center, in millimeters,
1540+
# along the specified axis
1541+
'intercept': 0.0,
1542+
1543+
# The slope of the beam energy gradient along the
1544+
# specified axis, in eV/mm.
1545+
'slope': 0.0,
1546+
1547+
# The specified axis for the beam energy gradient,
1548+
# either 'x' or 'y'.
1549+
'axis': 'y',
1550+
}
1551+
1552+
If the energy_correction dict is `None`, then no correction
1553+
is performed.
1554+
"""
1555+
if not energy_correction:
1556+
# No correction
1557+
return wavelength
1558+
1559+
# 'c' here is the conversion factor between keV and angstrom
1560+
c = constants.keVToAngstrom(1)
1561+
1562+
ind = 1 if energy_correction['axis'] == 'y' else 0
1563+
1564+
# Correct wavelength according to grain position. Position is in mm.
1565+
position = tvec_c[ind] + tvec_s[ind] - energy_correction['intercept']
1566+
1567+
# The slope is in eV/mm. Convert to keV.
1568+
adjustment = position * energy_correction['slope'] / 1e3
1569+
1570+
# Convert to keV, apply the adjustment, and then convert back to wavelength
1571+
return c / (c / wavelength + adjustment)

0 commit comments

Comments
 (0)