xrayscatteringtools 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ def plot_jungfrau(x, y, f, ax=None, shading='auto', *args, **kwargs):
5
+ """Plot Jungfrau detector counts.
6
+
7
+ Parameters
8
+ ----------
9
+ x, y : list of np.ndarray
10
+ Coordinates for each tile of the detector.
11
+ f : list of np.ndarray
12
+ Data to be plotted for each tile.
13
+ ax : matplotlib.axes.Axes, optional
14
+ Axes to plot on. If None, uses current axes.
15
+ shading : str, optional
16
+ Shading style for pcolormesh (default: 'auto').
17
+
18
+ Returns
19
+ -------
20
+ pcm : matplotlib.collections.QuadMesh
21
+ The QuadMesh object created.
22
+ """
23
+
24
+ if kwargs.get('norm'):
25
+ # If a norm is provided, use it
26
+ norm = kwargs['norm']
27
+ else:
28
+ # Determine global vmin/vmax if not explicitly given
29
+ vmin = kwargs.get('vmin', None)
30
+ vmax = kwargs.get('vmax', None)
31
+
32
+ if vmax is None or vmin is None:
33
+ # Compute min and max across all tiles
34
+ all_data = np.concatenate([fi.ravel() for fi in f])
35
+ if vmin is None:
36
+ vmin = np.nanmin(all_data)
37
+ if vmax is None:
38
+ vmax = np.nanmax(all_data)
39
+ # Update kwargs so all tiles use the same scale
40
+ kwargs.update(vmin=vmin, vmax=vmax)
41
+
42
+ if ax is None:
43
+ ax = plt.gca()
44
+ for i in range(8):
45
+ Xedges = compute_pixel_edges(x[i])
46
+ Yedges = compute_pixel_edges(y[i])
47
+ pcm = ax.pcolormesh(Xedges, Yedges, f[i], shading=shading, *args, **kwargs)
48
+ ax.set_aspect('equal')
49
+ return pcm
50
+
51
+ def compute_pixel_edges(coord):
52
+ """
53
+ Compute the array of pixel edge coordinates from pixel center coordinates.
54
+
55
+ Parameters
56
+ ----------
57
+ coord : np.ndarray
58
+ 2D array (M x N) of pixel center coordinates.
59
+
60
+ Returns
61
+ -------
62
+ edges : np.ndarray
63
+ 2D array ((M+1) x (N+1)) of pixel edge coordinates.
64
+ """
65
+ M, N = coord.shape
66
+ edges = np.zeros((M+1, N+1))
67
+
68
+ # Interior points: average of four neighboring centers
69
+ edges[1:-1, 1:-1] = 0.25 * (
70
+ coord[:-1, :-1] + coord[1:, :-1] +
71
+ coord[:-1, 1:] + coord[1:, 1:]
72
+ )
73
+
74
+ # Edges along rows (top and bottom)
75
+ edges[0, 1:-1] = coord[0, :-1] - 0.5*(coord[1, :-1] - coord[0, :-1])
76
+ edges[-1, 1:-1] = coord[-1, :-1] + 0.5*(coord[-1, :-1] - coord[-2, :-1])
77
+
78
+ # Edges along columns (left and right)
79
+ edges[1:-1, 0] = coord[:-1, 0] - 0.5*(coord[:-1, 1] - coord[:-1, 0])
80
+ edges[1:-1, -1] = coord[:-1, -1] + 0.5*(coord[:-1, -1] - coord[:-1, -2])
81
+
82
+ # Corners
83
+ edges[0,0] = coord[0,0] - 0.5*(coord[1,0] - coord[0,0]) - 0.5*(coord[0,1] - coord[0,0])
84
+ edges[0,-1] = coord[0,-1] - 0.5*(coord[1,-1] - coord[0,-1]) + 0.5*(coord[0,-1] - coord[0,-2])
85
+ edges[-1,0] = coord[-1,0] + 0.5*(coord[-1,0] - coord[-2,0]) - 0.5*(coord[-1,1] - coord[-1,0])
86
+ edges[-1,-1] = coord[-1,-1] + 0.5*(coord[-1,-1] - coord[-2,-1]) + 0.5*(coord[-1,-1] - coord[-1,-2])
87
+
88
+ return edges
@@ -0,0 +1 @@
1
+ from .iam import iam_total_pattern, iam_inelastic_pattern, iam_elastic_pattern
Binary file
@@ -0,0 +1,213 @@
1
+ from xrayscatteringtools.io import read_xyz
2
+ from xrayscatteringtools.utils import element_symbol_to_number
3
+ import numpy as np
4
+ from scipy.interpolate import InterpolatedUnivariateSpline, interp1d
5
+ import pathlib
6
+ import xraylib
7
+ from scipy.constants import physical_constants
8
+
9
+ base_path = pathlib.Path(__file__).parent
10
+
11
+ def iam_elastic_pattern(xyzfile, q_arr):
12
+ """
13
+ Compute the elastic (coherent) X-ray scattering intensity (Debye scattering)
14
+ for a molecule or atomic cluster.
15
+
16
+ Parameters
17
+ ----------
18
+ xyzfile : str or Path
19
+ Path to an XYZ file containing the atomic coordinates and element symbols.
20
+ q_arr : array_like
21
+ Array of momentum transfer values (q) at which to evaluate the scattering intensity.
22
+
23
+ Returns
24
+ -------
25
+ debye_image : ndarray
26
+ Elastic scattering intensity evaluated at each q in `q_arr`.
27
+
28
+ Notes
29
+ -----
30
+ - Uses atomic scattering factors loaded from 'Scattering_Factors.npy'.
31
+ - Includes both atomic self-scattering and molecular interference terms.
32
+ - The molecular interference term is calculated using the Debye formula with np.sinc.
33
+ """
34
+ num_atoms, _, atoms, coords = read_xyz(xyzfile) # Load the data
35
+ coords = np.array(coords) # Ensure coords is a NumPy array for advanced indexing
36
+ atomic_numbers = [element_symbol_to_number(atom) for atom in atoms] # Get atomic numbers using mendeleev
37
+ scattering_factors_coeffs = np.load(base_path / 'Scattering_Factors.npy', allow_pickle=True)
38
+ scattering_factors = np.zeros((num_atoms, len(q_arr))) # Preallocation for the structure factor array
39
+ q4pi = q_arr / (4 * np.pi)
40
+ for i, atom in enumerate(atomic_numbers): # Loop all atoms
41
+ factor_coeff = scattering_factors_coeffs[atom-1] # Grab the factor coefficients for that element, -1 for zero-based indexing
42
+ # Calculate atomic scattering factor for each q in q_arr
43
+ scattering_factors[i,:] = (
44
+ factor_coeff[0] * np.exp(-factor_coeff[4] * q4pi ** 2) +
45
+ factor_coeff[1] * np.exp(-factor_coeff[5] * q4pi ** 2) +
46
+ factor_coeff[2] * np.exp(-factor_coeff[6] * q4pi ** 2) +
47
+ factor_coeff[3] * np.exp(-factor_coeff[7] * q4pi ** 2) +
48
+ factor_coeff[8]
49
+ )
50
+ # Compute all pairwise distance vectors between atoms
51
+ r_vectors = coords[:, np.newaxis, :] - coords[np.newaxis, :, :] # shape: (num_atoms, num_atoms, 3)
52
+ distances = np.linalg.norm(r_vectors, axis=2) # shape: (num_atoms, num_atoms)
53
+
54
+ # Atomic contribution (self-scattering)
55
+ atomic_contribution = np.sum(scattering_factors**2, axis=0)
56
+ elastic_pattern = np.copy(atomic_contribution)
57
+
58
+ # Molecular contribution (interference terms)
59
+ for i in range(num_atoms):
60
+ for j in range(i + 1, num_atoms):
61
+ a = scattering_factors[i, :]
62
+ b = scattering_factors[j, :]
63
+ r_ij = distances[i, j]
64
+ # np.sinc(x) = sin(pi*x)/(pi*x), so argument is q*r/pi
65
+ molecular_contribution = 2 * a * b * np.sinc(q_arr * r_ij / np.pi)
66
+ elastic_pattern += molecular_contribution
67
+
68
+ return elastic_pattern
69
+
70
+ def iam_inelastic_pattern(xyzfile, q_arr):
71
+ """
72
+ Compute the inelastic (Compton) X-ray scattering intensity for a molecule
73
+ or atomic cluster.
74
+
75
+ Parameters
76
+ ----------
77
+ xyzfile : str or Path
78
+ Path to an XYZ file containing the atomic coordinates and element symbols.
79
+ q_arr : array_like
80
+ Array of momentum transfer values (q) at which to evaluate the scattering intensity.
81
+
82
+ Returns
83
+ -------
84
+ inelastic_pattern : ndarray
85
+ Inelastic scattering intensity interpolated at each q in `q_arr`.
86
+
87
+ Notes
88
+ -----
89
+ - Uses Compton scattering factors loaded from 'Compton_Factors.npy'.
90
+ - Interpolation is performed using `InterpolatedUnivariateSpline` to return values at the requested q points.
91
+ - The q grid in the Compton factors is fixed; changing it requires updating the array.
92
+ """
93
+ num_atoms, _, atoms, coords = read_xyz(xyzfile) # Load the data
94
+ atomic_numbers = [element_symbol_to_number(atom) for atom in atoms] # Get atomic numbers using mendeleev
95
+ compton_factors = np.load(base_path / 'Compton_Factors.npy') # Load the data
96
+ q_inelastic = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1., 1.5, 2., 8., 15.]) * 4*np.pi #These go with the Compton factors array- don't change them unless Compton Array changes.
97
+ inelastic_scattering = np.zeros_like(q_inelastic)
98
+ for atom in atomic_numbers: # Loop through all the atoms
99
+ inelastic_scattering += compton_factors[atom-1,:] # Sum up the contribution
100
+ spline_interp = InterpolatedUnivariateSpline(q_inelastic, inelastic_scattering) # Interpolate the inelastic scattering
101
+ return spline_interp(q_arr) # Return the interpolated inelastic scattering to the desired q values
102
+
103
+ def iam_total_pattern(xyzfile, q_arr):
104
+ """
105
+ Compute the total X-ray scattering intensity (elastic + inelastic)
106
+ for a molecule or atomic cluster.
107
+
108
+ Parameters
109
+ ----------
110
+ xyzfile : str or Path
111
+ Path to an XYZ file containing the atomic coordinates and element symbols.
112
+ q_arr : array_like
113
+ Array of momentum transfer values (q) at which to evaluate the scattering intensity.
114
+
115
+ Returns
116
+ -------
117
+ total_pattern : ndarray
118
+ Total scattering intensity (elastic + inelastic) evaluated at each q in `q_arr`.
119
+
120
+ Notes
121
+ -----
122
+ - Combines the outputs of `iam_elastic_pattern` and `iam_inelastic_pattern`.
123
+ - Useful for simulating the full scattering signal from a molecular system.
124
+ """
125
+ return iam_elastic_pattern(xyzfile, q_arr) + iam_inelastic_pattern(xyzfile, q_arr)
126
+
127
+ def iam_compton_spectrum(formula, theta, EI_keV, EF_keV_array, pz_au_grid=np.linspace(0,100,2000)):
128
+ """
129
+ Compute the Compton scattering spectrum for a molecule or atomic cluster.
130
+
131
+ Parameters
132
+ ----------
133
+ formula : str
134
+ Chemical formula of the molecule (e.g., "H2O").
135
+ theta : float or array_like
136
+ Scattering angles in radians. This is the full angle, equal to 2theta in literature.
137
+ EI_keV : float
138
+ Initial photon energy in kilo-electron volts (keV).
139
+ EF_keV_array : array_like
140
+ Array of final photon energies in kilo-electron volts (keV).
141
+ pz_au_grid : array_like, optional
142
+ Array of momentum transfer values at which to evaluate the scattering intensity.
143
+ Default is np.linspace(0,100,2000).
144
+
145
+ Returns
146
+ -------
147
+ compton_spectrum : ndarray
148
+ If theta is scalar: shape (len(EF_keV_array),)
149
+ If theta is array: shape (len(theta), len(EF_keV_array))
150
+ """
151
+ # Ensure inputs are arrays
152
+ theta = np.atleast_1d(theta)
153
+ EF_keV_array = np.atleast_1d(EF_keV_array)
154
+
155
+ system = xraylib.CompoundParser(formula)
156
+ m_e_c2_keV = physical_constants['electron mass energy equivalent in MeV'][0] * 1000 # keV
157
+ c = physical_constants['speed of light in vacuum'][0]
158
+ p_au = physical_constants['atomic unit of momentum'][0]
159
+ eV2J = physical_constants['electron volt-joule relationship'][0]
160
+
161
+ # Convert energies to Joules
162
+ EI_J = EI_keV * 1e3 * eV2J
163
+ EF_J = EF_keV_array * 1e3 * eV2J
164
+ m_e_c2_J = m_e_c2_keV * 1e3 * eV2J
165
+
166
+ # Calculate pz for all theta and EF_keV_array (broadcasting)
167
+ cos_theta = np.cos(theta)[:, None] # shape (Ntheta,1)
168
+ numerator = (EI_J - EF_J)[None, :] * m_e_c2_J - EI_J * EF_J[None, :] * (1 - cos_theta)
169
+ term_under_sqrt = EI_J**2 + EF_J[None, :]**2 - 2*EI_J*EF_J[None, :]*cos_theta
170
+ term_under_sqrt = np.clip(term_under_sqrt, 0, None) # avoid negative
171
+ denominator = c * np.sqrt(term_under_sqrt)
172
+ pz_si = np.divide(numerator, denominator, out=np.zeros_like(denominator), where=denominator!=0)
173
+ pz_au = pz_si / p_au # shape (Ntheta, NE)
174
+
175
+ # Prepare result
176
+ compton_spectrum = np.zeros((len(theta), len(EF_keV_array)))
177
+
178
+ # Loop over elements because xraylib isn't vectorized
179
+ for Zidx, Z in enumerate(system['Elements']):
180
+ # Initialize contribution for this element
181
+ J_element = np.zeros_like(compton_spectrum)
182
+ nAtoms = system['nAtoms'][Zidx]
183
+ for shell in range(0,31):
184
+ try:
185
+ # Build Compton profile for this shell on the pz grid
186
+ J_partial_pz = [
187
+ xraylib.ComptonProfile_Partial(Z, shell, float(p)) *
188
+ xraylib.ElectronConfig(Z, shell)
189
+ for p in pz_au_grid
190
+ ]
191
+ J_partial_interp = interp1d(
192
+ pz_au_grid, J_partial_pz,
193
+ bounds_error=False, fill_value=0.0
194
+ )
195
+
196
+ # Energy cutoff for this shell
197
+ shell_liftoff = EI_keV - xraylib.EdgeEnergy(Z, shell)
198
+
199
+ # Evaluate profile at all |pz| for every theta/E
200
+ for i_th in range(len(theta)):
201
+ mask = EF_keV_array > shell_liftoff
202
+ J_vals = J_partial_interp(np.abs(pz_au[i_th, :]))
203
+ # Apply liftoff: zero contribution where EF > shell_liftoff
204
+ J_element[i_th, :] += np.where(mask, 0.0, J_vals)
205
+ except Exception:
206
+ # Skip invalid shells
207
+ continue
208
+ compton_spectrum += nAtoms * J_element
209
+
210
+ # If theta was scalar, return 1D array
211
+ if compton_spectrum.shape[0] == 1:
212
+ return compton_spectrum[0]
213
+ return compton_spectrum
@@ -0,0 +1,36 @@
1
+ import numpy as np
2
+ from types import SimpleNamespace
3
+ import h5py
4
+ import pathlib
5
+
6
+ __all__ = ['sf6'] # So that from 'xrayscatteringtools.theory.patterns import *' works
7
+ data_path = pathlib.Path(__file__).parent / "data"
8
+
9
+
10
+ def __getattr__(name):
11
+ if name == "sf6":
12
+ # Load from HDF5 only when first accessed
13
+ with h5py.File(f"{data_path}/sf6.h5", "r") as f:
14
+ q = f["q"][:] # read dataset into memory
15
+ I_q = f["I_q"][:]
16
+ obj = SimpleNamespace(q=q, I_q=I_q)
17
+ obj.__doc__ = """
18
+ Ab initio SF6 Scattering Data.
19
+
20
+ Attributes
21
+ ----------
22
+ q : ndarray of shape (250,)
23
+ Momentum transfer values (q) in inverse angstroms.
24
+ I_q : ndarray of shape (250,)
25
+ Scattering intensity values corresponding to `q`.
26
+
27
+ Notes
28
+ -----
29
+ This data was received from Andres M. Carrascosa on Aug 26, 2025. Andres notes that this data was calculated at the CCSD/AVDZ level of theory.
30
+ """
31
+ return obj
32
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
33
+
34
+ def __dir__():
35
+ # Tab completion for IPython
36
+ return sorted(__all__)