Skip to content

Commit 759a303

Browse files
authored
Merge pull request #765 from HEXRD/powder-tests
Add some powder tests
2 parents 0f7d097 + 25e8455 commit 759a303

File tree

4 files changed

+337
-13
lines changed

4 files changed

+337
-13
lines changed

hexrd/instrument/hedm_instrument.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,9 @@ def simulate_powder_pattern(self,
15261526
assert len(origin) == 3, \
15271527
"origin must be a 3-element sequence"
15281528

1529+
if bkgmethod is None:
1530+
bkgmethod = {'chebyshev': 3}
1531+
15291532
'''
15301533
if params is none, fill in some sane default values
15311534
only the first value is used. the rest of the values are
@@ -1547,7 +1550,9 @@ def simulate_powder_pattern(self,
15471550
# }
15481551
params = wppfsupport._generate_default_parameters_LeBail(
15491552
mat_list,
1550-
1)
1553+
1,
1554+
bkgmethod,
1555+
)
15511556
'''
15521557
use the material list to obtain the dictionary of initial intensities
15531558
we need to make sure that the intensities are properly scaled by the
@@ -1608,7 +1613,7 @@ def simulate_powder_pattern(self,
16081613

16091614
intensity[mat.name] = {}
16101615
intensity[mat.name]['synchrotron'] = \
1611-
mat.planeData.get_structFact() * LP * multiplicity
1616+
mat.planeData.structFact * LP * multiplicity
16121617

16131618
kwargs = {
16141619
'expt_spectrum': expt,
@@ -1645,6 +1650,10 @@ def simulate_powder_pattern(self,
16451650
img_dict[det_key] = img
16461651

16471652
else:
1653+
# Rescale to be between 0 and 1 so random_noise() will work
1654+
prev_max = img.max()
1655+
img /= prev_max
1656+
16481657
if noise.lower() == 'poisson':
16491658
im_noise = random_noise(img,
16501659
mode='poisson',
@@ -1654,26 +1663,23 @@ def simulate_powder_pattern(self,
16541663
if ma > mi:
16551664
im_noise = (im_noise - mi)/(ma - mi)
16561665

1657-
img_dict[det_key] = im_noise
1658-
16591666
elif noise.lower() == 'gaussian':
1660-
img_dict[det_key] = random_noise(img,
1661-
mode='gaussian',
1662-
clip=True)
1667+
im_noise = random_noise(img, mode='gaussian', clip=True)
16631668

16641669
elif noise.lower() == 'salt':
1665-
img_dict[det_key] = random_noise(img, mode='salt')
1670+
im_noise = random_noise(img, mode='salt')
16661671

16671672
elif noise.lower() == 'pepper':
1668-
img_dict[det_key] = random_noise(img, mode='pepper')
1673+
im_noise = random_noise(img, mode='pepper')
16691674

16701675
elif noise.lower() == 's&p':
1671-
img_dict[det_key] = random_noise(img, mode='s&p')
1676+
im_noise = random_noise(img, mode='s&p')
16721677

16731678
elif noise.lower() == 'speckle':
1674-
img_dict[det_key] = random_noise(img,
1675-
mode='speckle',
1676-
clip=True)
1679+
im_noise = random_noise(img, mode='speckle', clip=True)
1680+
1681+
# Now scale back up
1682+
img_dict[det_key] = im_noise * prev_max
16771683

16781684
return img_dict
16791685

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from pathlib import Path
2+
3+
import h5py
4+
import numpy as np
5+
6+
import pytest
7+
8+
from hexrd import imageseries
9+
from hexrd.fitting.calibration import PowderCalibrator
10+
from hexrd.material.material import load_materials_hdf5, Material
11+
from hexrd.imageseries.process import ProcessedImageSeries
12+
from hexrd.instrument.hedm_instrument import HEDMInstrument
13+
14+
15+
@pytest.fixture
16+
def eiger_examples_path(example_repo_path: Path) -> Path:
17+
return Path(example_repo_path) / 'eiger'
18+
19+
20+
@pytest.fixture
21+
def ceria_examples_path(eiger_examples_path: Path) -> Path:
22+
return eiger_examples_path / 'first_ceria'
23+
24+
25+
@pytest.fixture
26+
def eiger_instrument(ceria_examples_path: Path) -> HEDMInstrument:
27+
instr_path = (
28+
ceria_examples_path / 'eiger_ceria_calibrated_composite.hexrd'
29+
)
30+
with h5py.File(instr_path, 'r') as rf:
31+
return HEDMInstrument(rf)
32+
33+
34+
@pytest.fixture
35+
def ceria_example_data(ceria_examples_path: Path) -> np.ndarray:
36+
data_path = ceria_examples_path / 'ff_000_data_000001.h5'
37+
with h5py.File(data_path, 'r') as rf:
38+
# Just return the first frame
39+
return rf['/entry/data/data'][0]
40+
41+
42+
@pytest.fixture
43+
def ceria_material(ceria_examples_path: Path) -> Material:
44+
path = ceria_examples_path / 'ceria.h5'
45+
materials = load_materials_hdf5(path)
46+
return materials['CeO2']
47+
48+
49+
def test_powder_auto_pick(
50+
eiger_instrument: HEDMInstrument,
51+
ceria_material: Material,
52+
ceria_example_data: np.ndarray,
53+
):
54+
instr = eiger_instrument
55+
material = ceria_material
56+
image_data = ceria_example_data
57+
pd = material.planeData
58+
59+
# Disable all exclusions on the plane data
60+
pd.exclusions = None
61+
hkls = pd.getHKLs()
62+
tth_values = pd.getTTh()
63+
64+
def hkl_idx(hkl: tuple | list) -> int | None:
65+
hkl = list(hkl)
66+
for i, hkl_ref in enumerate(hkls.tolist()):
67+
if hkl == hkl_ref:
68+
return i
69+
70+
return None
71+
72+
# Break up the image data into separate images for each detector
73+
# It's easiest to do this using hexrd's imageseries and
74+
# ProcessedImageSeries
75+
ims_dict = {}
76+
ims = imageseries.open(None, format='array', data=image_data)
77+
for det_key, panel in instr.detectors.items():
78+
ims_dict[det_key] = ProcessedImageSeries(
79+
ims, oplist=[('rectangle', panel.roi)]
80+
)
81+
82+
# Create the img_dict
83+
img_dict = {k: v[0] for k, v in ims_dict.items()}
84+
85+
calibrator = PowderCalibrator(
86+
instr,
87+
material,
88+
img_dict,
89+
tth_tol=0.25,
90+
eta_tol=1.0,
91+
pktype='gaussian',
92+
)
93+
94+
calibrator.autopick_points(
95+
fit_tth_tol=1.,
96+
int_cutoff=1e-4,
97+
)
98+
99+
picks = calibrator.calibration_picks
100+
101+
tth_tol = np.radians(calibrator.tth_tol)
102+
103+
unique_hkls = []
104+
num_picks_per_det = {}
105+
for det_key, det_picks in picks.items():
106+
panel = instr.detectors[det_key]
107+
num_picks_per_det[det_key] = 0
108+
for hkl_str, hkl_picks in det_picks.items():
109+
num_picks_per_det[det_key] += len(hkl_picks)
110+
if hkl_str not in unique_hkls:
111+
unique_hkls.append(hkl_str)
112+
113+
idx = hkl_idx(list(map(int, hkl_str.split())))
114+
assert idx is not None
115+
116+
# Verify that all picks are within tolerance distance of
117+
# the two theta values.
118+
tth = tth_values[idx]
119+
angles = panel.cart_to_angles(hkl_picks)[0]
120+
assert np.allclose(angles[:, 0], tth, atol=tth_tol)
121+
122+
# Verify eta values are different if there are enough picks
123+
if len(hkl_picks) > 5:
124+
assert not np.allclose(angles[0, 1], angles[-1, 1])
125+
126+
# There should be at least 12 unique hkls and 2000 total picks
127+
total_num_picks = sum(num_picks_per_det.values())
128+
assert len(unique_hkls) >= 12
129+
assert total_num_picks > 2000
130+
131+
# There should be at least 30 picks per detector
132+
assert all(v > 30 for v in num_picks_per_det.values())
462 KB
Binary file not shown.

tests/test_powder.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import copy
2+
from pathlib import Path
3+
4+
import h5py
5+
import numpy as np
6+
7+
import pytest
8+
9+
from hexrd.instrument.hedm_instrument import HEDMInstrument
10+
from hexrd.material.material import load_materials_hdf5, Material
11+
from hexrd.projections.polar import PolarView
12+
13+
14+
@pytest.fixture
15+
def eiger_examples_path(example_repo_path: Path) -> Path:
16+
return Path(example_repo_path) / 'eiger'
17+
18+
19+
@pytest.fixture
20+
def ceria_examples_path(eiger_examples_path: Path) -> Path:
21+
return eiger_examples_path / 'first_ceria'
22+
23+
24+
@pytest.fixture
25+
def eiger_instrument(ceria_examples_path: Path) -> HEDMInstrument:
26+
instr_path = (
27+
ceria_examples_path / 'eiger_ceria_calibrated_composite.hexrd'
28+
)
29+
with h5py.File(instr_path, 'r') as rf:
30+
return HEDMInstrument(rf)
31+
32+
33+
@pytest.fixture
34+
def ceria_material(ceria_examples_path: Path) -> Material:
35+
path = ceria_examples_path / 'ceria.h5'
36+
materials = load_materials_hdf5(path)
37+
return materials['CeO2']
38+
39+
40+
@pytest.fixture
41+
def expected_simulated_powder_lines_results(
42+
test_data_dir: Path,
43+
) -> dict[str, dict]:
44+
path = test_data_dir / 'expected_simulated_powder_lines_results.npy'
45+
return np.load(path, allow_pickle=True).item()
46+
47+
48+
def test_simulate_powder_lines(
49+
eiger_instrument: HEDMInstrument,
50+
ceria_material: Material,
51+
expected_simulated_powder_lines_results: dict[str, dict],
52+
):
53+
instr = eiger_instrument
54+
material = copy.deepcopy(ceria_material)
55+
ref = expected_simulated_powder_lines_results
56+
57+
pd = material.planeData
58+
59+
pd.exclusions = None
60+
61+
pd.tThWidth = np.radians(0.25)
62+
63+
hkls = pd.getHKLs()
64+
tth = pd.getTTh()
65+
66+
# There should be exactly 22 two HKLs generated
67+
assert len(tth) == 22
68+
69+
def hkl_idx(hkl):
70+
hkl = list(hkl)
71+
for i, hkl_ref in enumerate(hkls.tolist()):
72+
if hkl_ref == hkl:
73+
return i
74+
75+
return None
76+
77+
# Verify a few manual cases
78+
# The four corners should all have these HKLs
79+
corner_hkls = {
80+
(4, 2, 0): 9.583035640913781,
81+
# 333 and 511 are nearly identical.
82+
(3, 3, 3): 11.139041445220014,
83+
(5, 1, 1): 11.139041445220013,
84+
(6, 2, 0): 13.568350297337751,
85+
}
86+
corner_hkls = {k: np.radians(v) for k, v in corner_hkls.items()}
87+
88+
# The four inner subpanels should *only* have these HKLs
89+
inner_hkls = {
90+
(1, 1, 1): 3.7078160949754677,
91+
(2, 0, 0): 4.281666411410814,
92+
}
93+
inner_hkls = {k: np.radians(v) for k, v in inner_hkls.items()}
94+
95+
inner_dets = ['eiger_3_1', 'eiger_3_2', 'eiger_4_1', 'eiger_4_2']
96+
corner_dets = ['eiger_0_0', 'eiger_0_3', 'eiger_7_0', 'eiger_7_3']
97+
98+
full_results = {}
99+
for det_key, panel in instr.detectors.items():
100+
angs, xys, ranges = panel.make_powder_rings(
101+
pd,
102+
# Have to make a smaller delta eta to get all intersections,
103+
# since the Eiger subpanels are small.
104+
delta_eta=0.25,
105+
)
106+
full_results[det_key] = {
107+
'angs': angs,
108+
'xys': xys,
109+
'ranges': ranges,
110+
}
111+
112+
valid_angs = [x for x in angs if x.size > 0]
113+
if det_key in inner_dets:
114+
for hkl, value in inner_hkls.items():
115+
idx = hkl_idx(hkl)
116+
assert np.allclose(angs[idx][0][0], value)
117+
118+
# Also verify that there are no other HKLs
119+
assert len(valid_angs) == len(inner_hkls)
120+
121+
if det_key in corner_dets:
122+
for hkl, value in corner_hkls.items():
123+
idx = hkl_idx(hkl)
124+
assert np.allclose(angs[idx][0][0], value)
125+
126+
# There should be at least 9 valid HKLs on every corner detector
127+
assert len(valid_angs) >= 9
128+
129+
# Verify that a few of the HKLs were combined
130+
indices, ranges = pd.getMergedRanges()
131+
132+
num_merged_hkls = sum([len(x) > 1 for x in indices])
133+
assert num_merged_hkls == 5
134+
135+
# Verify that 3, 3, 3 and 5, 1, 1 were merged
136+
idx1 = hkl_idx([5, 1, 1])
137+
idx2 = hkl_idx([3, 3, 3])
138+
assert [idx1, idx2] in indices
139+
140+
# Now just verify that everything matches what it was before...
141+
for det_key, det_results in full_results.items():
142+
for entry_key, results in det_results.items():
143+
ref_results = ref[det_key][entry_key]
144+
for i in range(len(results)):
145+
assert np.allclose(results[i], ref_results[i], equal_nan=True)
146+
147+
148+
def test_simulate_powder_pattern_image(
149+
eiger_instrument: HEDMInstrument,
150+
ceria_material: Material,
151+
):
152+
instr = eiger_instrument
153+
material = ceria_material
154+
155+
img_dict = instr.simulate_powder_pattern(
156+
[material],
157+
noise='poisson',
158+
)
159+
160+
# Now do a warp to the polar view, create the lineout, and verify there
161+
# is intensity in a few places we'd expect.
162+
tth_range = [0.1, 14]
163+
eta_min = -180
164+
eta_max = 180
165+
pixel_size = (0.1, 0.1)
166+
pv = PolarView(tth_range, instr, eta_min, eta_max, pixel_size)
167+
img = pv.warp_image(img_dict, pad_with_nans=True,
168+
do_interpolation=True)
169+
170+
lineout = img.mean(axis=0).filled(np.nan)
171+
172+
# Now verify there is intensity at the predicted two theta values
173+
# Sort by structure factor and check the 4 most intense lines
174+
sf_sorting = np.argsort(-material.planeData.structFact)
175+
for i in range(4):
176+
tth_idx = sf_sorting[i]
177+
tth_val = np.degrees(material.planeData.getTTh()[tth_idx])
178+
179+
# Compute the index for this tth value in the lineout
180+
idx = int(np.floor((tth_val - tth_range[0]) / pixel_size[1]))
181+
182+
# Verify we are at a maximum, and that the value is higher
183+
# than the background.
184+
assert lineout[idx] > lineout[0]
185+
assert lineout[idx] > lineout[idx - 1]
186+
assert lineout[idx] > lineout[idx + 1]

0 commit comments

Comments
 (0)