Skip to content

Commit b39cc2c

Browse files
committed
custom scattering plots
Additional updates for plotting and display of custom scattering factors. classes_crystal.py - Change Atoms.type to always be array type with <U8 classes_scattering.py - update scattering_factors classes_plotting.py - added plot_scattering_factors All tests pass.
1 parent b59e945 commit b39cc2c

File tree

5 files changed

+97
-35
lines changed

5 files changed

+97
-35
lines changed

Dans_Diffraction/classes_crystal.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
22/05/23 3.2.4 Added Symmetry.wyckoff_label(), Symmetry.spacegroup_dict
5252
06/05/24 3.3.0 Symmetry.from_cif now loads operations from find_spacegroup if not already loaded
5353
06/04/25 3.3.1 scale parameter of superlattice improved
54+
15/09/25 3.3.2 Atoms.type changed to always be array type
5455
5556
@author: DGPorter
5657
"""
@@ -1088,6 +1089,7 @@ class Atoms:
10881089
"_atom_site_fract_y",
10891090
"_atom_site_fract_z",
10901091
]
1092+
_type_str_fmt = '<U8'
10911093

10921094
def __init__(self, u=[0], v=[0], w=[0], type=None,
10931095
label=None, occupancy=None, uiso=None, mxmymz=None):
@@ -1100,9 +1102,9 @@ def __init__(self, u=[0], v=[0], w=[0], type=None,
11001102
# ---Defaults---
11011103
# type
11021104
if type is None:
1103-
self.type = np.asarray([self._default_atom] * Natoms)
1105+
self.type = np.asarray([self._default_atom] * Natoms, dtype=self._type_str_fmt)
11041106
else:
1105-
self.type = np.asarray(type, dtype=str).reshape(-1)
1107+
self.type = np.asarray(type, dtype=self._type_str_fmt).reshape(-1)
11061108
# label
11071109
if label is None:
11081110
self.label = self.type.copy()
@@ -1136,7 +1138,7 @@ def __call__(self, u=[0], v=[0], w=[0], type=None,
11361138

11371139
def __getitem__(self, idx):
11381140
if isinstance(idx, str):
1139-
idx = self.label.index(idx)
1141+
idx = list(self.label).index(idx)
11401142
return self.atom(idx)
11411143

11421144
def fromcif(self, cifvals):
@@ -1221,8 +1223,8 @@ def fromcif(self, cifvals):
12211223
self.u = u
12221224
self.v = v
12231225
self.w = w
1224-
self.type = element
1225-
self.label = label
1226+
self.type = np.array(element, dtype=self._type_str_fmt)
1227+
self.label = np.array(label, dtype=self._type_str_fmt)
12261228
self.occupancy = occ
12271229
self.uiso = uiso
12281230
self.mx = mx
@@ -1276,19 +1278,21 @@ def atom(self, idx):
12761278
return atoms[0]
12771279
return atoms
12781280

1279-
def changeatom(self, idx=None, u=None, v=None, w=None, type=None,
1281+
def changeatom(self, idx, u=None, v=None, w=None, type=None,
12801282
label=None, occupancy=None, uiso=None, mxmymz=None):
12811283
"""
1282-
Change an atoms properties
1283-
:param idx:
1284-
:param u:
1285-
:param v:
1286-
:param w:
1287-
:param type:
1288-
:param label:
1289-
:param occupancy:
1290-
:param uiso:
1291-
:param mxmymz:
1284+
Change an atom site's properties.
1285+
If properties are given as None, they are not changed.
1286+
1287+
:param idx: atom array index
1288+
:param u: atomic position u in relative coordinates along basis vector a
1289+
:param v: atomic position u in relative coordinates along basis vector b
1290+
:param w: atomic position u in relative coordinates along basis vector c
1291+
:param type: atomic element type
1292+
:param label: atomic site label
1293+
:param occupancy: atom site occupancy
1294+
:param uiso: atom site isotropic thermal parameter
1295+
:param mxmymz: atom site magnetic vector (mx, my, mz)
12921296
:return: None
12931297
"""
12941298

@@ -1307,7 +1311,7 @@ def changeatom(self, idx=None, u=None, v=None, w=None, type=None,
13071311
if label is not None:
13081312
old_labels = list(self.label)
13091313
old_labels[idx] = label
1310-
self.label = np.array(old_labels)
1314+
self.label = np.array(old_labels, dtype=self._type_str_fmt)
13111315

13121316
if occupancy is not None:
13131317
self.occupancy[idx] = occupancy
@@ -1433,11 +1437,11 @@ def remove_duplicates(self, min_distance=0.01, all_types=False):
14331437
"""
14341438

14351439
uvw = self.uvw()
1436-
type = self.type
1440+
atom_type = self.type
14371441
rem_atom_idx = []
1438-
for n in range(0, len(type) - 1):
1442+
for n in range(0, len(atom_type) - 1):
14391443
match_position = fg.mag(uvw[n, :] - uvw[n + 1:, :]) < min_distance
1440-
match_type = type[n] == type[n + 1:]
1444+
match_type = atom_type[n] == atom_type[n + 1:]
14411445
if all_types:
14421446
rem_atom_idx += list(1 + n + np.where(match_position)[0])
14431447
else:

Dans_Diffraction/classes_plotting.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,45 @@ def plot_ms_azimuth(self, hkl, energy_kev, azir=[0, 0, 1], pv=[1, 0], numsteps=3
13781378
fp.labels(ttl, r'$\psi$ (deg)', 'Intensity')
13791379
#plt.subplots_adjust(bottom=0.2)
13801380

1381+
def plot_scattering_factors(self, q_max=4, energy_range=None, q_range=None):
1382+
"""
1383+
Plot atomic scattering factors across wavevector or energy
1384+
1385+
if q_range has more values than energy_range, figures will plot scattering factor vs Q
1386+
for each energy.
1387+
if energy_range has more values than q_range, figures will plot scattering factor vs energy
1388+
for each value of Q.
1389+
1390+
:param q_max: use a q_range of 0-q_max
1391+
:param energy_range: energy range in keV
1392+
:param q_range: range of wavecectors, or None to use q_max
1393+
:return: None
1394+
"""
1395+
1396+
if q_range is None:
1397+
q_range = np.arange(0, q_max, 0.01)
1398+
atom_types, atom_idx = np.unique(self.xtl.Structure.type, return_index=True)
1399+
atom_scattering_factors = self.xtl.Scatter.scattering_factors(qmag=q_range, energy_kev=energy_range)
1400+
ttl = f"{self.xtl.Scatter._scattering_type}"
1401+
# plot multiple figures of lower dimension
1402+
if atom_scattering_factors.shape[2] > atom_scattering_factors.shape[0]: # energy_range > q_range
1403+
# different figures for different values of Q
1404+
for n in range(atom_scattering_factors.shape[0]):
1405+
plt.figure(figsize=self._figure_size, dpi=self._figure_dpi)
1406+
nttl = ttl + f" Q={q_range[n]:.2g}" + r" $\AA^{-1}$"
1407+
for atom, idx in zip(atom_types, atom_idx):
1408+
plt.plot(energy_range, atom_scattering_factors[n, idx, :], label=atom)
1409+
fp.labels(nttl, 'Energy [keV]', 'Atomic scattering factor', legend=True)
1410+
else:
1411+
# different figures for different values of E
1412+
for n in range(atom_scattering_factors.shape[2]):
1413+
plt.figure(figsize=self._figure_size, dpi=self._figure_dpi)
1414+
nttl = ttl + (f" E = {energy_range[n]} keV" if atom_scattering_factors.shape[2] > 1 else "")
1415+
for atom, idx in zip(atom_types, atom_idx):
1416+
plt.plot(q_range, np.abs(atom_scattering_factors[:, idx, n]), label=atom)
1417+
fp.labels(nttl, r'Q $\AA^{-1}$', 'Atomic scattering factor', legend=True)
1418+
1419+
13811420
r''' Remove tensor_scattering 26/05/20
13821421
def tensor_scattering_azimuth(self, atom_label, hkl, energy_kev, azir=[0, 0, 1], process='E1E1',
13831422
rank=2, time=+1, parity=+1, mk=None, lk=None, sk=None):

Dans_Diffraction/classes_scattering.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
16/05/24 2.3.4 Added printed progress bar to generate_intensity_cut during convolusion
4242
17/05/24 2.3.4 Changed generate_intensity_cut to make it much faster
4343
23/12/24 2.3.5 Added polarised neutron options
44+
15/09/25 2.4.0 Added custom scattering factors
4445
4546
@author: DGPorter
4647
"""
@@ -54,7 +55,7 @@
5455
from . import multiple_scattering as ms
5556
# from . import tensor_scattering as ts # Removed V1.7
5657

57-
__version__ = '2.3.5'
58+
__version__ = '2.4.0'
5859

5960

6061
class Scattering:
@@ -1635,13 +1636,14 @@ def scatteringbasis(self, hkl, azim_zero=(1, 0, 0), psi=0):
16351636
Jhat_psi = fg.norm(np.cross(Qhat, Ihat_psi))
16361637
return np.vstack([Ihat_psi, Jhat_psi, Qhat])
16371638

1638-
def scattering_factors(self, hkl, energy_kev=None):
1639+
def scattering_factors(self, hkl=(0, 0, 0), energy_kev=None, qmag=None):
16391640
"""Return the scattering factors[n, m] for each reflection, hkl[n,3] and each atom [m]"""
16401641

16411642
if energy_kev is None:
16421643
energy_kev = self._energy_kev
16431644

1644-
qmag = self.xtl.Cell.Qmag(hkl)
1645+
if qmag is None:
1646+
qmag = self.xtl.Cell.Qmag(hkl)
16451647
# Scattering factors
16461648
ff = fs.scattering_factors(
16471649
scattering_type=self._scattering_type,
@@ -1733,23 +1735,25 @@ def print_scattering_factor_coefficients(self):
17331735
else:
17341736
raise Exception(f"Unknown scattering type: {scattering_type}")
17351737

1736-
coefs = fc.scattering_factor_coefficients(*self.xtl.Structure.type, table=table)
1737-
if not np.any(coefs[:, -1]):
1738-
coefs = coefs[:, :-1] # trim final column if all zeros
1739-
n_doubles = coefs.shape[1] // 2
1740-
n_singles = coefs.shape[1] % 2
1738+
element_coefs = fc.scattering_factor_coefficients_custom(*self.xtl.Structure.type, default_table=table)
1739+
17411740

17421741
structrue = self.xtl.Structure
17431742
structure_list = zip(structrue.label, structrue.type, structrue.u, structrue.v, structrue.w)
17441743
out = f"{scattering_type}\n"
17451744
for n, (label, el, u, v, w) in enumerate(structure_list):
17461745
out += f"{n:3} {label:6} {el:5}: {u:8.3g}, {v:8.3g}, {w:8.3g} : "
1747-
out += ', '.join(f"a{d} = {coefs[n, 2*d]:.3f}, A{d} = {coefs[n, 2*d+1]:.3f}" for d in range(n_doubles))
1748-
out += (', ' * int(n_doubles > 0)) + (f"b = {coefs[n, -1]:.3f}" * n_singles)
1746+
1747+
coefs = element_coefs[el]
1748+
if abs(coefs[-1]) < 1e-6:
1749+
coefs = coefs[:-1] # trim final column if all zeros
1750+
n_doubles = len(coefs) // 2
1751+
n_singles = len(coefs) % 2
1752+
out += ', '.join(f"a{d} = {coefs[2*d]:.3f}, A{d} = {coefs[2*d+1]:.3f}" for d in range(n_doubles))
1753+
out += (', ' * int(n_doubles > 0)) + (f"b = {coefs[-1]:.3f}" * n_singles)
17491754
out += '\n'
17501755
return out
17511756

1752-
17531757
def old_intensity(self, HKL, scattering_type=None):
17541758
"""
17551759
Calculate the squared structure factor for the given HKL

Dans_Diffraction/functions_crystallography.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,7 +1317,7 @@ def scattering_factor_coefficients(*elements, table='itc'):
13171317
'sears': scattering_factor_coefficients_neutron_sears
13181318
}
13191319
table = table.lower()
1320-
if table.lower() not in tables:
1320+
if table not in tables:
13211321
raise ValueError(f'Unknown scattering factor table: {table}')
13221322
return tables[table](*elements)
13231323

@@ -1486,8 +1486,13 @@ def custom_scattering_factor_resonant(elements, q_mag, energy_kev, default_table
14861486
tab_en, tab_f1, tab_f2 = tables[el]
14871487
f0 = analytical_scattering_factor(q_mag, *coefs) # (n_q, )
14881488
f1 = np.interp(energy_kev, tab_en, tab_f1) # (n_en, )
1489-
f2 = np.interp(energy_kev, tab_en, tab_f2) # (n_ej, )
1490-
qff[:, n, :] = np.array([[_f0 + _f1 - 1j * _f2 for _f1, _f2 in zip(f1, f2)] for _f0 in f0])
1489+
f2 = np.interp(energy_kev, tab_en, tab_f2) # (n_en, )
1490+
qff[:, n, :] = np.array([
1491+
[
1492+
_f0 if np.isnan(_f1) or np.isnan(_f2) else _f0 + (_f1 - 1j * _f2)
1493+
for _f1, _f2 in zip(f1, f2) # loop over energy
1494+
] for _f0 in f0 # loop over q values
1495+
])
14911496
return qff
14921497

14931498

Examples/example_custom_scattering_factors.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,18 @@
2121
[-0.4833, -3.7933, -3.7862, -3.7791, -3.772, -3.765, -3.7579, -3.7508, -3.7437, -3.7367] # f2
2222
])
2323
dif.fc.add_custom_form_factor_coefs('Co3+', *coefs, dispersion_table=table)
24-
xtl.Structure.changeatom(3, type='Co3+')
24+
xtl.Atoms.changeatom(1, type='Co3+')
25+
xtl.generate_structure()
26+
xtl.Structure.type[3] = 'Co'
2527

28+
29+
# Display scattering factors
30+
print(xtl.Scatter.print_scattering_factor_coefficients())
31+
32+
xtl.Scatter.setup_scatter(scattering_type='custom', output=False)
33+
xtl.Plot.plot_scattering_factors(q_max=6)
34+
35+
# Calculate structure factors
2636
en = np.arange(7.7, 7.8, 0.01)
2737
ref = [0, 0, 3]
2838
inten1 = xtl.Scatter.xray_dispersion(ref, en)

0 commit comments

Comments
 (0)