Skip to content

Commit 638fb6b

Browse files
committed
to generate realistic PXRD
1 parent a491133 commit 638fb6b

File tree

3 files changed

+339
-18
lines changed

3 files changed

+339
-18
lines changed

pyxtal/XRD.py

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import numpy as np
1010
from monty.serialization import loadfn
1111
from scipy.interpolate import interp1d
12-
from scipy.special import erf
12+
from scipy.special import erf, wofz
1313
from pyxtal.database.element import Element
1414

1515
with importlib.resources.as_file(
@@ -558,10 +558,171 @@ def get_profile(self, method="gaussian", res=0.01, user_kwargs=None):
558558
np.degrees(self.max2theta),
559559
)
560560

561+
def get_plot(self, grainsize=20, orientation=0.1, thermo=0.1,
562+
L=500, H=50, S=25, bg_order=6, bg_ratio=0.05,
563+
mix_ratio=0.02, dx=0.02):
564+
"""
565+
Generate a simulated XRD plot with various parameters.
566+
Inspired from Pysimxrd at PyPI.
567+
Needs to double check the parameters.
561568
562-
# ----------------------------- Profile functions ------------------------------
569+
Args:
570+
grainsize (float): Grain size in micrometers.
571+
orientation (float): Preferred orientation factor.
572+
thermo (float): Thermal vibration factor.
573+
L (float): Axial divergence length.
574+
H (float): Axial divergence height.
575+
S (float): Slit width.
576+
bg_order (int): Order of the polynomial background.
577+
bg_ratio (float): Ratio of background intensity.
578+
mix_ratio (float): Ratio of random noise intensity.
579+
dx (float): Step size for the simulated XRD.
563580
581+
Returns:
582+
tuple: Simulated 2-theta values and corresponding intensities.
583+
"""
564584

585+
# Marked locations and intensities
586+
x, y = self.pxrd[:, 0], self.pxrd[:, -1] * 100
587+
thetas = np.radians(x/2)
588+
gamma = 0.444 * self.wavelength / (grainsize * np.cos(thetas)) + 1e-8
589+
sigma2 = gamma ** 2 / (2*np.sqrt(2))
590+
ori_m, ori_p = 1 - orientation, 1 + orientation
591+
ori = np.clip(np.random.normal(loc=1, scale=0.2), ori_m, ori_p)
592+
deb = np.exp(-16/3 * np.pi**2 * thermo**2 * (np.sin(thetas) / self.wavelength)**2)
593+
y *= ori * deb
594+
#print(x, y, gamma, sigma2)
595+
596+
# Get profiles
597+
theta_min, theta_max = np.degrees(self.min2theta), min(90.0, np.degrees(self.max2theta))
598+
x_sim = np.arange(theta_min, theta_max, dx)
599+
y_sim = 0
600+
for k in range(len(x)):
601+
if x[k] < 90:
602+
y_sim += add_peak(x_sim, x[k], gamma[k], sigma2[k], L, H, S, dx) * y[k]
603+
604+
# normalization x_sim, y_sim
605+
area = np.trapz(y_sim, x_sim)
606+
y_sim /= area#; print(area, y_sim.max())
607+
608+
# Add background
609+
bg_fun = np.poly1d(np.random.randn(bg_order + 1))
610+
bg = bg_fun(x_sim)
611+
bg -= bg.min()
612+
bg_y = bg / bg.max() * y_sim.max() * bg_ratio
613+
mixture = np.random.uniform(0, y_sim.max() * mix_ratio, size=len(x_sim))
614+
y_sim += np.flip(bg_y) + mixture
615+
616+
# Scale to (0, 100)
617+
y_sim -= y_sim.min()
618+
y_sim /= y_sim.max()
619+
y_sim *= 100
620+
621+
#import matplotlib.pyplot as plt
622+
#plt.plot(x_sim, y_sim)
623+
#plt.show()
624+
return x_sim, y_sim
625+
626+
def add_peak(twotheta, mu, gamma, sigma2, L, H, S, step=0.02, width=0.1, sigma2_distor=0.001):
627+
"""
628+
Add a single peak to the XRD pattern using Voigt profile,
629+
axial divergence, slit function, and lattice distortion.
630+
631+
Args:
632+
twotheta (array-like): Array of 2-theta
633+
mu (float): Peak center (2-theta) in degrees.
634+
gamma (float): Lorentzian FWHM parameter.
635+
sigma2 (float): Gaussian variance parameter.
636+
L (float): Axial divergence length.
637+
H (float): Axial divergence height.
638+
S (float): Slit half-width.
639+
step (float): Step size for the 2-theta array.
640+
width (float): Width of the slit function in degrees.
641+
sigma2_distor (float): Variance for lattice distortion Gaussian.
642+
643+
Returns:
644+
ndarray: Array of same shape as twotheta with the peak intensity.
645+
"""
646+
# Determine l_gap based on mu value
647+
if mu <= 10:
648+
l_gap = 7.8
649+
elif 10 < mu <= 15:
650+
l_gap = 10
651+
elif 15 < mu <= 20:
652+
l_gap = 15
653+
elif 20 < mu <= 30:
654+
l_gap = 20
655+
else:
656+
l_gap = 30
657+
658+
# Ensure mu-l_gap and mu+l_gap are recorded in twotheta or its extension
659+
x = np.arange(np.round(mu - l_gap, 2), np.round(mu + l_gap, 2), step)
660+
661+
# Voigt profile calculation
662+
z = ((x - mu) + 1j * gamma) / (np.sqrt(sigma2) * np.sqrt(2))
663+
voigt = np.real(wofz(z) / (np.sqrt(sigma2) * np.sqrt(2 * np.pi)))
664+
665+
# Axial divergence calculation
666+
axial = axial_div(x, mu, L, H, S)
667+
668+
# Slit function calculation
669+
height = 1.0 / width
670+
slit = np.where((x >= mu - width / 2) & (x <= mu + width / 2), height, 0)
671+
672+
# Lattice distortion calculation
673+
sigma = np.sqrt(sigma2_distor)
674+
distor = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mu) / sigma)**2)
675+
676+
# Convolve the peaks
677+
combined = np.convolve(voigt, axial, mode='same')
678+
combined = np.convolve(combined, slit, mode='same')
679+
combined = np.convolve(combined, distor, mode='same')
680+
if np.sum(combined) > 0:
681+
combined /= np.sum(combined) * step # Normalize peak and apply weight
682+
# Map the peak to the original locations
683+
return map_int(combined, x, twotheta)
684+
else:
685+
return np.zeros_like(twotheta)
686+
687+
def axial_div(x, mu, L, H, S):
688+
"""
689+
Calculate the axial divergence peak contribution using the Van Laar model.
690+
691+
Args:
692+
x (array-like): Array of 2-theta values in degrees.
693+
mu (float): Peak center (2-theta) in degrees.
694+
L (float): Axial divergence length (same units as H and S).
695+
H (float): Axial divergence height.
696+
S (float): Slit half-width (same units as H).
697+
698+
Returns:
699+
ndarray: Array of same shape as x with the axial divergence shape (unnormalized).
700+
"""
701+
axial_divergence = np.zeros_like(x) # Initialize axial_divergence to zeros
702+
valid_indices = x <= mu # Identify valid indices where x <= mu
703+
x_valid = np.radians(x[valid_indices]) # Get valid x values
704+
705+
h = L * np.sqrt((np.cos(x_valid) / np.cos(np.radians(mu)))**2 - 1) # Calculate h
706+
W = np.where((H - S <= h) & (h <= H + S), H + S - h, 0) # Calculate W for valid h
707+
axial_divergence[valid_indices] = L / (2 * H * S * h * np.cos(np.radians(x_valid))) * W
708+
#print('debug axial_div', mu, x_valid[-1], axial_divergence[valid_indices].max())
709+
axial_divergence /= (axial_divergence.max() + 1e-10 )# in case numerical err
710+
cdf = np.zeros_like(x)
711+
mask = x < mu
712+
cdf[mask] = np.cumsum(axial_divergence[mask])
713+
return cdf
714+
715+
def map_int(peak, x, twotheta):
716+
y_twotheta = np.zeros_like(twotheta) # Initialize y_twotheta array
717+
_x = x[(x >= twotheta[0]) & (x <= twotheta[-1])]
718+
_peak = peak[(x >= twotheta[0]) & (x <= twotheta[-1])]
719+
for angle in range(len(_x)):
720+
index = np.argmin(np.abs( twotheta- _x[angle])) # Find index for each angle
721+
if index.size > 0: # Check if indices are not empty
722+
y_twotheta[index] = _peak[angle] # Map peak intensity
723+
return y_twotheta
724+
725+
# ----------------------------- Profile functions ------------------------------
565726
class Profile:
566727
"""
567728
This class applies a profiling function to simulated or

0 commit comments

Comments
 (0)