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.
- xrayscatteringtools/__init__.py +3 -0
- xrayscatteringtools/calib/__init__.py +0 -0
- xrayscatteringtools/calib/geometry_calibration.py +204 -0
- xrayscatteringtools/calib/scattering_corrections.py +488 -0
- xrayscatteringtools/epicsArch.py +392 -0
- xrayscatteringtools/io.py +310 -0
- xrayscatteringtools/plotting.py +88 -0
- xrayscatteringtools/theory/Compton_Factors.npy +0 -0
- xrayscatteringtools/theory/Scattering_Factors.npy +0 -0
- xrayscatteringtools/theory/__init__.py +1 -0
- xrayscatteringtools/theory/data/sf6.h5 +0 -0
- xrayscatteringtools/theory/iam.py +213 -0
- xrayscatteringtools/theory/patterns.py +36 -0
- xrayscatteringtools/utils.py +456 -0
- xrayscatteringtools-0.0.1.dist-info/METADATA +79 -0
- xrayscatteringtools-0.0.1.dist-info/RECORD +18 -0
- xrayscatteringtools-0.0.1.dist-info/WHEEL +4 -0
- xrayscatteringtools-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
@@ -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__)
|