Skip to content

Commit 5591f96

Browse files
committed
modify the map_intensity
1 parent a212370 commit 5591f96

File tree

1 file changed

+144
-25
lines changed

1 file changed

+144
-25
lines changed

pyxtal/XRD.py

Lines changed: 144 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -585,39 +585,60 @@ def get_plot(self, grainsize=20, orientation=0.1, thermo=0.1,
585585
# Marked locations and intensities
586586
x, y = self.pxrd[:, 0], self.pxrd[:, -1] * 100
587587
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))
588+
589+
# Calculate FWHM using Scherrer equation
590+
# Standard Scherrer: FWHM_L = K*λ/(L*cosθ) where K≈0.9
591+
# For Lorentzian HWHM: γ = FWHM/2
592+
K = 0.9 # Scherrer constant (shape factor)
593+
fwhm = K * self.wavelength / (grainsize * np.cos(thetas) + 1e-10) # FWHM in radians
594+
gamma = fwhm / 2 # Lorentzian HWHM
595+
596+
# Convert HWHM to Gaussian variance for Voigt profile
597+
# For pure Gaussian: HWHM = sqrt(2*ln2) * σ
598+
# Therefore: σ² = HWHM² / (2*ln2)
599+
sigma2 = gamma ** 2 / (2 * np.log(2)) # Gaussian variance
600+
601+
# Apply preferred orientation and Debye-Waller factor
590602
ori_m, ori_p = 1 - orientation, 1 + orientation
591603
ori = np.clip(np.random.normal(loc=1, scale=0.2), ori_m, ori_p)
604+
605+
# Apply Debye-Waller factor
592606
deb = np.exp(-16/3 * np.pi**2 * thermo**2 * (np.sin(thetas) / self.wavelength)**2)
593607
y *= ori * deb
594608
#print(x, y, gamma, sigma2)
595609

596610
# Get profiles
597611
theta_min, theta_max = np.degrees(self.min2theta), min(90.0, np.degrees(self.max2theta))
598612
x_sim = np.arange(theta_min, theta_max, dx)
599-
y_sim = 0
613+
y_sim = np.zeros_like(x_sim)
614+
615+
# Add each peak contribution
600616
for k in range(len(x)):
601-
if x[k] < 90:
617+
if x[k] < theta_max:
618+
#print("Adding peak at 2theta =", x[k])
602619
y_sim += add_peak(x_sim, x[k], gamma[k], sigma2[k], L, H, S, dx) * y[k]
603620

604-
# normalization x_sim, y_sim
621+
# Add each peak contribution
605622
area = np.trapz(y_sim, x_sim)
606623
y_sim /= area#; print(area, y_sim.max())
607624

608625
# Add background
609-
bg_fun = np.poly1d(np.random.randn(bg_order + 1))
626+
bg_coeffs = np.abs(np.random.randn(bg_order + 1))
627+
bg_coeffs[0] = -bg_coeffs[0] # Ensure decreasing trend
628+
bg_fun = np.poly1d(bg_coeffs)
629+
#bg_fun = np.poly1d(np.random.randn(bg_order + 1))
610630
bg = bg_fun(x_sim)
611631
bg -= bg.min()
612632
bg_y = bg / bg.max() * y_sim.max() * bg_ratio
613633
mixture = np.random.uniform(0, y_sim.max() * mix_ratio, size=len(x_sim))
614-
y_sim += np.flip(bg_y) + mixture
634+
y_sim += bg_y + mixture
615635

616636
# Scale to (0, 100)
617-
y_sim -= y_sim.min()
618-
y_sim /= y_sim.max()
619-
y_sim *= 100
620-
637+
y_min, y_max = y_sim.min(), y_sim.max()
638+
if y_max > y_min: # Avoid division by zero
639+
y_sim = (y_sim - y_min) / (y_max - y_min) * 100
640+
else:
641+
y_sim = np.zeros_like(y_sim)
621642
#import matplotlib.pyplot as plt
622643
#plt.plot(x_sim, y_sim)
623644
#plt.show()
@@ -631,7 +652,7 @@ def add_peak(twotheta, mu, gamma, sigma2, L, H, S, step=0.02, width=0.1, sigma2_
631652
Args:
632653
twotheta (array-like): Array of 2-theta
633654
mu (float): Peak center (2-theta) in degrees.
634-
gamma (float): Lorentzian FWHM parameter.
655+
gamma (float): Lorentzian HWHM parameter.
635656
sigma2 (float): Gaussian variance parameter.
636657
L (float): Axial divergence length.
637658
H (float): Axial divergence height.
@@ -659,8 +680,9 @@ def add_peak(twotheta, mu, gamma, sigma2, L, H, S, step=0.02, width=0.1, sigma2_
659680
x = np.arange(np.round(mu - l_gap, 2), np.round(mu + l_gap, 2), step)
660681

661682
# 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)))
683+
sigma = np.sqrt(sigma2)
684+
z = ((x - mu) + 1j * gamma) / (sigma * np.sqrt(2))
685+
voigt = np.real(wofz(z) / (sigma * np.sqrt(2 * np.pi)))
664686

665687
# Axial divergence calculation
666688
axial = axial_div(x, mu, L, H, S)
@@ -669,6 +691,10 @@ def add_peak(twotheta, mu, gamma, sigma2, L, H, S, step=0.02, width=0.1, sigma2_
669691
height = 1.0 / width
670692
slit = np.where((x >= mu - width / 2) & (x <= mu + width / 2), height, 0)
671693

694+
#slit = np.zeros_like(x)
695+
#mask = np.abs(x - mu) <= width / 2
696+
#slit[mask] = 1.0 / width # Normalized rectangular function
697+
672698
# Lattice distortion calculation
673699
sigma = np.sqrt(sigma2_distor)
674700
distor = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mu) / sigma)**2)
@@ -679,12 +705,11 @@ def add_peak(twotheta, mu, gamma, sigma2, L, H, S, step=0.02, width=0.1, sigma2_
679705
combined = np.convolve(combined, distor, mode='same')
680706
if np.sum(combined) > 0:
681707
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)
708+
return map_intensity(combined, x, twotheta)
684709
else:
685710
return np.zeros_like(twotheta)
686711

687-
def axial_div(x, mu, L, H, S):
712+
def axial_div_bak(x, mu, L, H, S):
688713
"""
689714
Calculate the axial divergence peak contribution using the Van Laar model.
690715
@@ -712,14 +737,108 @@ def axial_div(x, mu, L, H, S):
712737
cdf[mask] = np.cumsum(axial_divergence[mask])
713738
return cdf
714739

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
740+
def axial_div(x, mu, L, H, S):
741+
"""
742+
Van Laar axial divergence PDF (not CDF!)
743+
"""
744+
x = np.asarray(x)
745+
f = np.zeros_like(x)
746+
747+
mask = x < mu
748+
if not np.any(mask):
749+
return f
750+
751+
x_m = np.radians(x[mask])
752+
mu_r = np.radians(mu)
753+
cos_mu = np.cos(mu_r)
754+
cos_x = np.cos(x_m)
755+
756+
# Calculate h parameter with clipping to avoid negative square root
757+
cos_ratio_sq = (cos_x / cos_mu) ** 2
758+
h = L * np.sqrt(np.clip(cos_ratio_sq - 1, 0, None))
759+
760+
# Window function: non-zero only when H - S <= h <= H + S
761+
W = np.clip(H + S - h, 0.0, 2 * S)
762+
763+
# Van Laar axial divergence formula
764+
# Avoid division by zero
765+
denom = 2 * H * S * np.clip(h, 1e-10, None) * np.clip(cos_x, 1e-10, None)
766+
f[mask] = L * W / denom
767+
768+
# Remove numerical noise and ensure non-negative
769+
f[~np.isfinite(f)] = 0.0
770+
f[f < 0] = 0.0
771+
772+
# Normalize to unit area
773+
# Integrate only the non-zero part
774+
x_nonzero = x[mask]
775+
f_nonzero = f[mask]
776+
if len(x_nonzero) > 1 and np.sum(f_nonzero) > 0:
777+
area = np.trapz(f_nonzero, x_nonzero)
778+
if area > 0:
779+
f[mask] /= area
780+
return f
781+
782+
def map_intensity(peak, x, twotheta):
783+
"""
784+
Map peak intensities from fine grid (x) to coarse grid (twotheta).
785+
Uses cubic spline interpolation to produce continuous, smooth profiles.
786+
787+
Args:
788+
peak (array-like): Peak intensities on fine grid x.
789+
x (array-like): Fine grid positions (degrees).
790+
twotheta (array-like): Coarse grid positions (degrees).
791+
792+
Returns:
793+
ndarray: Interpolated intensities on twotheta grid.
794+
"""
795+
# Handle edge cases
796+
if len(peak) == 0 or len(x) == 0:
797+
return np.zeros_like(twotheta)
798+
799+
# Check if x is monotonically increasing
800+
if not np.all(np.diff(x) > 0):
801+
# Sort by x if not already sorted
802+
sort_idx = np.argsort(x)
803+
x = x[sort_idx]
804+
peak = peak[sort_idx]
805+
806+
# Use cubic spline interpolation for smooth results
807+
kind = 'linear' if len(x) < 4 else 'cubic'
808+
809+
try:
810+
f_interp = interp1d(x, peak, kind=kind, bounds_error=False,
811+
fill_value=0.0, assume_sorted=True)
812+
y_twotheta = f_interp(twotheta)
813+
# Ensure non-negative intensities
814+
y_twotheta[y_twotheta < 0] = 0.0
815+
return y_twotheta
816+
except (ValueError, RuntimeError) as e:
817+
print(f"Interpolation failed: {e}. Falling back to nearest-neighbor.")
818+
return _map_int_nearest_neighbor(peak, x, twotheta)
819+
820+
821+
def _map_int_nearest_neighbor(peak, x, twotheta):
822+
"""
823+
Fallback nearest-neighbor mapping when interpolation fails.
824+
825+
Args:
826+
peak (array-like): Peak intensities on fine grid.
827+
x (array-like): Fine grid positions.
828+
twotheta (array-like): Coarse grid positions.
829+
830+
Returns:
831+
ndarray: Nearest-neighbor intensities.
832+
"""
833+
y_twotheta = np.zeros_like(twotheta, dtype=float)
834+
835+
for x_val, peak_val in zip(x, peak):
836+
idx = np.argmin(np.abs(twotheta - x_val))
837+
if y_twotheta[idx] == 0: # Only assign if not already set
838+
y_twotheta[idx] = peak_val
839+
else:
840+
y_twotheta[idx] += peak_val * 0.5 # Average with existing value
841+
723842
return y_twotheta
724843

725844
# ----------------------------- Profile functions ------------------------------

0 commit comments

Comments
 (0)