Skip to content

Commit a414e4d

Browse files
authored
Merge pull request #3 from maclariz/adding-HOLZ-STEM
Update azimuth.py
2 parents 529027e + e3e8a87 commit a414e4d

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed

stem/azimuth.py

+276
Original file line numberDiff line numberDiff line change
@@ -1 +1,277 @@
1+
import numpy as np
2+
import scipy.optimize as op
3+
import matplotlib.pyplot as plt
4+
from matplotlib.ticker import MultipleLocator
5+
from alive_progress import alive_bar
16

7+
class stem:
8+
def discfloat(ci, cj, rmin, rmax, segments):
9+
# This is a helper function for polar transformation which generates a meshgrid
10+
# of float values corresponding to r and phi values back in a cartesian i, j frame.
11+
# These can (after rounding to integers) then be used to lookup all the right pixels
12+
# in a diffraction pattern to allow quick transformation from cartesian to polar
13+
# representations of the data using fancy lookup (i.e. passing lists of array indices
14+
# to slice an array).
15+
# I have chosen to use i and j to refer to vertical and horizontal axes in data
16+
# (rather than x and y, because some packages use x for horizontal,
17+
# and some use x for axis 0, which is vertical in python arrays)
18+
19+
coords = np.zeros(shape=(2,rmax-rmin,segments))
20+
phi = np.arange(0, 2*np.pi, 2*np.pi/segments)
21+
r = np.arange(rmin,rmax)
22+
r_phi_mesh = np.meshgrid(r,phi)
23+
i = -r_phi_mesh[0]*np.sin(r_phi_mesh[1]) + ci
24+
j = r_phi_mesh[0]*np.cos(r_phi_mesh[1]) + cj
25+
return np.array([i, j])
26+
27+
# returns array of dimensions:
28+
# 0 is the switch of i or j
29+
# 1 is the azimuth dimension
30+
# 2 is the radius / two theta dimension
31+
32+
def polarttransform(DP, ci, cj, rmin, rmax, segments, simple=True):
33+
# Performs a polar transform of one single diffraction pattern
34+
disc = discfloat(ci, cj, rmin, rmax, segments) # get basic disc of all transform positions
35+
# Note: if segments are too few, then each segment will cover too many pixels in
36+
# original dataset, simple mapping will be a very poor representation and even
37+
# simple == False mapping won't really represent all intensity in that patch of the original
38+
# You are better off using an appropriate number of segments to approximately match the
39+
# 2*pi*r at the largest radius of interest in your analysis to get a good sampling of the
40+
# original data in your transform
41+
42+
if simple==True:
43+
# simply works things out with nearest position to the r, theta positions mapped
44+
# back into Cartesian
45+
pos = np.round(disc,0).astype('int16') # round the disc array to nearest integer
46+
pos2 = pos.reshape((2,pos.shape[1]*pos.shape[2])) # turn to a 1D list
47+
pt = DP[pos2[0],pos2[1]].reshape(pos.shape[1],pos.shape[2]).T
48+
# calculate PT by using the pos2 array to slice the original array
49+
50+
if simple==False:
51+
# works out the weighted average of the four pixels surrounding the float
52+
# r, theta positions mapped back into Cartesian
53+
shape = disc[0].shape[0]*disc[0].shape[1]
54+
disc0 = disc[0].reshape(shape) # turn into linear array of i positions
55+
disc1 = disc[1].reshape(shape) # turn into linear array of j positions
56+
ui = np.floor(disc0).astype('int16') # find upper i pixel array
57+
li = np.ceil(disc0).astype('int16') # find lower i pixel array
58+
li = np.where(li==ui,li+1,li) # deals with the case of an exact hit on an i position
59+
lj = np.floor(disc1).astype('int16') # find left j pixel array
60+
rj = np.ceil(disc1).astype('int16') # find right j pixel array
61+
rj = np.where(rj==lj, lj+1, rj) # deals with the case of an exact hit on a j position
62+
wul = (1-(disc0-ui))*(1-(disc1-lj)) # weighting parameter upper left
63+
wur = (1-(disc0-ui))*(1-(rj-disc1)) # weighting parameter upper right
64+
wll = (1-(li -disc0))*(1-(disc1-lj)) # weighting parameter lower left
65+
wlr = (1-(li -disc0))*(1-(rj-disc1)) # weighting parameter lower right
66+
pt = (
67+
DP[ui,lj]*wul +
68+
DP[ui,rj]*wur +
69+
DP[li,lj]*wll +
70+
DP[li,rj]*wlr
71+
).reshape(disc[0].shape[0],disc[0].shape[1]).T
72+
73+
# Now weight result by pixel area in transform image
74+
radweight = np.arange(rmin, rmax)*2*np.pi/segments
75+
azi = np.ones(shape=(segments))
76+
rweighting = np.meshgrid(azi,radweight)[1]
77+
PTDP = pt*rweighting
78+
79+
return PTDP
80+
81+
def PT4Dinone(dataset, ci, cj, rmin, rmax, segments, simple=True):
82+
# This function runs the polar transform over an entire 4DSTEM dataset
83+
# dataset is a 4DSTEM dataset as a numpy array
84+
# dimensions 0 and 1 are the vertical and horizontal dimensions of the image
85+
# dimensions 2 and 3 are the vertical and horizontal dimensions of the diffraction patterns
86+
# ci and cj are the pattern centres, either as floats or as arrays of floats to match dataset
87+
# rmin and rmax are the minimum and maximum radii for the output transform
88+
# you will save time and memory if you do not transform everything but focus on the radii of most interest
89+
# segments is the number of segments in azimuthal angle to split into (e.g. 360 gives 1 degree segments)
90+
# simple == True just calculates mapping one pixel in dataset to one in output
91+
# simple == False maps a weighted average of four pixels in dataset to one in output
92+
93+
Ri, Rj = dataset.shape[0], dataset.shape[1]
94+
with alive_bar(Ri*Rj, force_tty=True) as bar:
95+
PT4D = np.zeros(shape=(Ri,Rj,rmax-rmin,segments))
96+
97+
# version of calculation for a single value for pattern centre (probably okay if there is not much
98+
# movement of pattern centre in dataset).
99+
100+
if isinstance(ci, int):
101+
for i in range(Ri):
102+
for j in range(Rj):
103+
PT4D[i,j,:,:] = polarttransform(
104+
dataset[i,j,:,:],
105+
ci, cj,
106+
rmin, rmax,
107+
segments,
108+
simple=simple
109+
)
110+
bar()
111+
return PT4D
112+
113+
# version of calculation for an array of pattern centres (more robust of there are some descan issues)
114+
115+
elif isinstance(ci, np.ndarray):
116+
if ci.shape[0]==Ri and cj.shape[1]==Rj:
117+
for i in range(Ri):
118+
for j in range(Rj):
119+
PT4D[i,j,:,:] = polarttransform(
120+
dataset[i,j,:,:],
121+
ci[i,j], cj[i,j],
122+
rmin, rmax,
123+
segments,
124+
simple=simple
125+
)
126+
bar()
127+
return PT4D
128+
else:
129+
print('The array size for the pattern centres does not match the dataset')
130+
return
131+
132+
def plotpolar(polar, rmin, rmax, lines, title):
133+
# a little function for just plotting polar transformed datasets as a sanity check
134+
# no return, just an inline plot with appropriate angle labels
135+
136+
# Parameters:
137+
# polar: a single polar transformed diffraction pattern
138+
# rmin: minimum radius of the transform in pixels
139+
# rmax: maximum radius of the transform in pixels
140+
# lines: a set of five line positions to delineate the Laue zone, between 2 and 3 is used
141+
# in this notebook for the fitted area
142+
fig, ax = plt.subplots(figsize=(12,6))
143+
ax.imshow(
144+
polar,
145+
vmin=np.percentile(polar,5),
146+
vmax=np.percentile(polar,98),
147+
cmap='turbo',
148+
extent = [0, 360, rmax, rmin],
149+
aspect=3
150+
) #plotting transform
151+
#with some background removed
152+
ax.set_xlabel(r'$\phi,$ deg')
153+
ax.set_ylabel('radius, pixels')
154+
ax.yaxis.set_minor_locator(MultipleLocator(5))
155+
ax.xaxis.set_minor_locator(MultipleLocator(10))
156+
ax.xaxis.set_major_locator(MultipleLocator(60))
157+
158+
ax.hlines(lines+rmin,0,359,color=['m','k','w','w','m']) #adding lines to mark out position of HOLZ ring
159+
ax.set_title(title)
160+
161+
def fun_cos_sq(phi,A2,phi2,A1,phi1,B):
162+
#intensity function for fitting of polar transformed data
163+
return (A2*np.cos(np.radians(phi-phi2))**2+
164+
A1*np.cos(np.radians(phi-phi1))+
165+
B)
166+
167+
def fitIntensitypattern(
168+
func,
169+
pattern,
170+
p0=[2000,90,2000,0,1000],
171+
bounds=([0, 0, 0, -180, 0], [np.inf, 180, np.inf, 180, np.inf])
172+
):
173+
'''
174+
fit HOLZ ring intensity to a sinusoidal function in a single pattern
175+
176+
Parameters
177+
----------
178+
179+
func : sinusoidal function to fit data to
180+
181+
pattern: 1D numpy array of intensity values as a function of i, j and azimuthal angle
182+
183+
184+
Returns
185+
-------
186+
187+
fitParams : 3D numpy array of fit parameters (i_max x j_max x 5 array)
188+
189+
fitCov: 4D numpy array with covariance matrix of fitted parameters (i_max x j_max x 5 x 5 array)
190+
191+
Note
192+
----
193+
194+
p0 and bounds values are set to appropriate value for some experimental datasets of mine, but won't
195+
be suitable for everything.
196+
A2, A1 and B can range from 0 to infinity but a suitable starting value is best found by fitting one
197+
pattern and checking these parameters before fitting a dataset.
198+
phi2 ranges in a 180 degree range (being a 2-fold function). It's up to you what is the most sensible range.
199+
If the value is significantly away from 0, then 0-180 is probably sensible. But if around 0, then -90 to 90
200+
is probably better.
201+
phi1 ranges in a 360 degree range, and it's up to you to work out if 0 - 360 or -180 - 180 (or some other range)
202+
is more sensible.
203+
'''
204+
xDim, yDim, zDim = data.shape
205+
fitParams = np.zeros((xDim,yDim,5))
206+
fitCov = np.zeros((xDim,yDim,5,5))
207+
208+
with alive_bar(yDim*xDim, force_tty=True) as bar:
209+
for i in range(yDim):
210+
for j in range(xDim):
211+
pop, pcov = op.curve_fit(
212+
fun_cos_sq,
213+
np.arange(zDim),
214+
data[j,i],
215+
p0=p0,
216+
bounds=bounds
217+
)
218+
fitParams[j,i] = pop
219+
fitCov[j,i] = pcov
220+
bar()
221+
return fitParams, fitCov
222+
223+
def fitIntensitydataset(
224+
func,
225+
data,
226+
p0=[2000,90,2000,0,1000],
227+
bounds=([0, 0, 0, -180, 0], [np.inf, 180, np.inf, 180, np.inf])
228+
):
229+
'''
230+
fit HOLZ ring intensity to a sinusoidal function over entire 3D dataset
231+
232+
Parameters
233+
----------
234+
235+
func : sinusoidal function to fit data to
236+
237+
data: 3D numpy array of intensity values as a function of i, j and azimuthal angle
238+
239+
240+
Returns
241+
-------
242+
243+
fitParams : 3D numpy array of fit parameters (i_max x j_max x 5 array)
244+
245+
fitCov: 4D numpy array with covariance matrix of fitted parameters (i_max x j_max x 5 x 5 array)
246+
247+
Note
248+
----
249+
250+
p0 and bounds values are set to appropriate value for some experimental datasets of mine, but won't
251+
be suitable for everything.
252+
A2, A1 and B can range from 0 to infinity but a suitable starting value is best found by fitting one
253+
pattern and checking these parameters before fitting a dataset.
254+
phi2 ranges in a 180 degree range (being a 2-fold function). It's up to you what is the most sensible range.
255+
If the value is significantly away from 0, then 0-180 is probably sensible. But if around 0, then -90 to 90
256+
is probably better.
257+
phi1 ranges in a 360 degree range, and it's up to you to work out if 0 - 360 or -180 - 180 (or some other range)
258+
is more sensible.
259+
'''
260+
xDim, yDim, zDim = data.shape
261+
fitParams = np.zeros((xDim,yDim,5))
262+
fitCov = np.zeros((xDim,yDim,5,5))
263+
264+
with alive_bar(yDim*xDim, force_tty=True) as bar:
265+
for i in range(yDim):
266+
for j in range(xDim):
267+
pop, pcov = op.curve_fit(
268+
fun_cos_sq,
269+
np.arange(zDim),
270+
data[j,i],
271+
p0=p0,
272+
bounds=bounds
273+
)
274+
fitParams[j,i] = pop
275+
fitCov[j,i] = pcov
276+
bar()
277+
return fitParams, fitCov

0 commit comments

Comments
 (0)