|
9 | 9 | import numpy as np |
10 | 10 | from monty.serialization import loadfn |
11 | 11 | from scipy.interpolate import interp1d |
12 | | -from scipy.special import erf |
| 12 | +from scipy.special import erf, wofz |
13 | 13 | from pyxtal.database.element import Element |
14 | 14 |
|
15 | 15 | with importlib.resources.as_file( |
@@ -558,10 +558,171 @@ def get_profile(self, method="gaussian", res=0.01, user_kwargs=None): |
558 | 558 | np.degrees(self.max2theta), |
559 | 559 | ) |
560 | 560 |
|
| 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. |
561 | 568 |
|
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. |
563 | 580 |
|
| 581 | + Returns: |
| 582 | + tuple: Simulated 2-theta values and corresponding intensities. |
| 583 | + """ |
564 | 584 |
|
| 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 ------------------------------ |
565 | 726 | class Profile: |
566 | 727 | """ |
567 | 728 | This class applies a profiling function to simulated or |
|
0 commit comments