@@ -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