abinslib 0.1.0__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.
- abinslib/__init__.py +1 -0
- abinslib/almost_isotropic_incoherent.py +499 -0
- abinslib/bose.py +82 -0
- abinslib/data.py +59 -0
- abinslib/displacements.py +203 -0
- abinslib/isotropic_incoherent.py +256 -0
- abinslib/util.py +35 -0
- abinslib-0.1.0.dist-info/METADATA +11 -0
- abinslib-0.1.0.dist-info/RECORD +12 -0
- abinslib-0.1.0.dist-info/WHEEL +5 -0
- abinslib-0.1.0.dist-info/licenses/LICENSE +674 -0
- abinslib-0.1.0.dist-info/top_level.txt +1 -0
abinslib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dynamical structure factor from phonon data."""
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Semi-analytic powder averaging approximations in CLIMAX/AbINS lineage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from euphonic import QpointPhononModes, Quantity, ureg
|
|
6
|
+
from euphonic.spectra import Spectrum1DCollection
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .displacements import Displacements
|
|
10
|
+
from .isotropic_incoherent import (
|
|
11
|
+
_bin_mode_intensities,
|
|
12
|
+
_get_total_cross_sections,
|
|
13
|
+
calculate_isotropic_dw_factor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calculate_almost_isotropic_incoherent_fundamentals(
|
|
18
|
+
mode_displacements: Displacements,
|
|
19
|
+
atomic_displacements: Quantity,
|
|
20
|
+
nominal_q2: Quantity,
|
|
21
|
+
) -> np.ndarray:
|
|
22
|
+
"""Calculate fundamental mode intensities in almost-isotropic approximation.
|
|
23
|
+
|
|
24
|
+
S = exp(-(Q^2 tr(A + 2 tr(A:B)/tr(B))/5)) Q^2 tr(B) / 3
|
|
25
|
+
|
|
26
|
+
- Fundamentals only
|
|
27
|
+
- Atomic cross sections not applied
|
|
28
|
+
- Ignore actual q-points and use nominal Q^2 instead
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
mode_displacements: phonon mode displacement dataset
|
|
32
|
+
atomic_displacements: thermal average atomic displacements indexed
|
|
33
|
+
(atom, direction, direction)
|
|
34
|
+
nominal_q2:
|
|
35
|
+
Scalar Q^2 values corresponding to modes; note that all q-points
|
|
36
|
+
are used and this is typically related to the mode frequency by
|
|
37
|
+
neutron instrument parameters.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dimensionless mode intensities with array indices (qpt, mode, atom)
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
q2_term = (
|
|
44
|
+
np.einsum(
|
|
45
|
+
"ij,ijkll->ijk",
|
|
46
|
+
nominal_q2.to("bohr^-2").magnitude,
|
|
47
|
+
mode_displacements.n_plus_one.to("bohr^2").magnitude,
|
|
48
|
+
)
|
|
49
|
+
/ 3
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
a = atomic_displacements.to("bohr^2").magnitude * 2
|
|
53
|
+
b = mode_displacements.n_plus_one.to("bohr^2").magnitude
|
|
54
|
+
a_trace = np.einsum("...ii", a)
|
|
55
|
+
b_trace = np.einsum("...ii", b)
|
|
56
|
+
ba_trace = np.einsum("ijklm, kml->ijk", b, a)
|
|
57
|
+
inv_b_trace = np.divide(
|
|
58
|
+
1.0,
|
|
59
|
+
b_trace,
|
|
60
|
+
out=np.zeros_like(b_trace),
|
|
61
|
+
where=(b_trace != 0.0),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
exp_term = np.exp(
|
|
65
|
+
-nominal_q2[:, :, None].to("bohr^-2").magnitude
|
|
66
|
+
* (a_trace[None, None, :] + 2.0 * ba_trace * inv_b_trace)
|
|
67
|
+
/ 5.0
|
|
68
|
+
)
|
|
69
|
+
return q2_term * exp_term
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def calculate_almost_isotropic_incoherent_combinations(
|
|
73
|
+
mode_displacements: Displacements,
|
|
74
|
+
atomic_displacements: Quantity,
|
|
75
|
+
nominal_q2: Quantity,
|
|
76
|
+
include_dw: bool = False,
|
|
77
|
+
) -> np.ndarray:
|
|
78
|
+
"""Calculate second-order mode intensities in almost-isotropic approximation.
|
|
79
|
+
|
|
80
|
+
S(Q, ω_ν + ω_ν') =
|
|
81
|
+
exp(-Q^2 tr(A/3)) Q^4 / 15C (tr(B_ν)tr(B_ν') + B_ν:B_ν' + B_ν':B_ν)
|
|
82
|
+
|
|
83
|
+
for some atom, where C = 2 if ν=ν' else 1
|
|
84
|
+
|
|
85
|
+
- Atomic cross sections not applied
|
|
86
|
+
- Ignore actual q-points and use nominal Q^2 instead
|
|
87
|
+
|
|
88
|
+
Note that this has cubic scaling with system size as n_modes ∝ n_atoms;
|
|
89
|
+
while this reference implementation constructs the whole array,
|
|
90
|
+
memory-efficient approaches need to reduce the data to binned spectra
|
|
91
|
+
on-the-fly.
|
|
92
|
+
|
|
93
|
+
It is also possible to reduce the calculation effort by calculating at
|
|
94
|
+
constant Q and rescaling the intensity based on post-binning Q values;
|
|
95
|
+
this is implemented in
|
|
96
|
+
:func:`q_scaling_almost_isotropic_incoherent_combination_spectra`
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
mode_displacements: phonon mode displacement dataset
|
|
100
|
+
atomic_displacements: thermal average atomic displacements indexed
|
|
101
|
+
(atom, direction, direction)
|
|
102
|
+
nominal_q2:
|
|
103
|
+
Scalar Q^2 values corresponding to modes; note that all q-points
|
|
104
|
+
are used and this is typically related to the mode frequency by
|
|
105
|
+
neutron instrument parameters.
|
|
106
|
+
include_dw:
|
|
107
|
+
Include mode-by-mode Debye-Waller intensity scaling
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dimensionless combination mode intensities with array indices
|
|
111
|
+
(qpt1, mode1, qpt2, mode2, atom)
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
if len(nominal_q2.shape) != 4:
|
|
115
|
+
msg = (
|
|
116
|
+
"Expected 4-D nominal Q^2 for combination modes "
|
|
117
|
+
"with indices (qpt1, mode1, qpt2, mode2)"
|
|
118
|
+
)
|
|
119
|
+
raise ValueError(msg)
|
|
120
|
+
|
|
121
|
+
q4 = nominal_q2.to("bohr^-2").magnitude ** 2
|
|
122
|
+
|
|
123
|
+
b = mode_displacements.n_plus_one.to("bohr^2").magnitude
|
|
124
|
+
b_trace = np.einsum("...ii", b)
|
|
125
|
+
tr_term = np.einsum("ijk,lmk->ijlmk", b_trace, b_trace)
|
|
126
|
+
|
|
127
|
+
# Double contraction M_ij:M_ij should be commutative! Not clear why this is
|
|
128
|
+
# traditionally written out as a sum over both orders, we can just *2?
|
|
129
|
+
# Also as B are symmetric there is no difference between
|
|
130
|
+
# B_ij:B_ij and B_ij:B_ji
|
|
131
|
+
double_contraction = np.einsum("ijklm,opklm->ijopk", b, b)
|
|
132
|
+
|
|
133
|
+
# Factor 2 if (q, ν) = (q', ν'), else 1
|
|
134
|
+
n_q = b.shape[0]
|
|
135
|
+
n_bands = b.shape[1]
|
|
136
|
+
c = np.eye(n_q * n_bands, n_q * n_bands).reshape(
|
|
137
|
+
n_q, n_bands, n_q, n_bands
|
|
138
|
+
) + np.ones((n_q, n_bands, n_q, n_bands))
|
|
139
|
+
|
|
140
|
+
# No funny business here, just expand Q4 and C over atom index m
|
|
141
|
+
q4_term = np.einsum(
|
|
142
|
+
"ijkl,ijkl,ijklm->ijklm", q4, 1 / (15 * c), tr_term + 2 * double_contraction
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if include_dw:
|
|
146
|
+
dw_factor = calculate_isotropic_dw_factor(atomic_displacements, nominal_q2)
|
|
147
|
+
else:
|
|
148
|
+
dw_factor = 1.0
|
|
149
|
+
|
|
150
|
+
return dw_factor * q4_term
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def calculate_almost_isotropic_incoherent_spectra(
|
|
154
|
+
modes: QpointPhononModes,
|
|
155
|
+
mode_displacements: Displacements,
|
|
156
|
+
atomic_displacements: Quantity,
|
|
157
|
+
nominal_q2: Quantity,
|
|
158
|
+
bins: Quantity,
|
|
159
|
+
apply_cross_section: bool = True,
|
|
160
|
+
) -> Spectrum1DCollection:
|
|
161
|
+
"""Calculate INS intensities in almost-isotropic incoherent approximation.
|
|
162
|
+
|
|
163
|
+
Actual q-points of phonon modes will be disregarded; instead each mode
|
|
164
|
+
intensity will be based on a separate array of nominal Q^2 values
|
|
165
|
+
corresponding to modes. This is intended to approximate powder-averaging
|
|
166
|
+
with kinematic constraints: for indirect geometry the energy-Q^2
|
|
167
|
+
relationship can be determined using abinslib.utils.calculate_indirect_q2.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
modes: phonon frequency and eigenvector dataset
|
|
171
|
+
mode_displacements: phonon mode displacement dataset
|
|
172
|
+
(This can be obtained using :func:`Displacements.from_modes(modes)`.)
|
|
173
|
+
atomic_displacements: thermal average atomic displacements indexed
|
|
174
|
+
(atom, direction, direction)
|
|
175
|
+
nominal_q2:
|
|
176
|
+
Scalar Q^2 values corresponding to modes; note that all q-points
|
|
177
|
+
are used and this is typically related to the mode frequency by
|
|
178
|
+
neutron instrument parameters.
|
|
179
|
+
bins:
|
|
180
|
+
Energy or frequency bins used as x_data in resulting spectra
|
|
181
|
+
apply_cross_section:
|
|
182
|
+
Multiply each atom/isotope spectrum by a corresponding total
|
|
183
|
+
neutron scattering cross-section (σ_tot).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
binned spectra of contribution from each nucleus
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
intensities = calculate_almost_isotropic_incoherent_fundamentals(
|
|
190
|
+
mode_displacements=mode_displacements,
|
|
191
|
+
atomic_displacements=atomic_displacements,
|
|
192
|
+
nominal_q2=nominal_q2,
|
|
193
|
+
)
|
|
194
|
+
y_data = _bin_mode_intensities(
|
|
195
|
+
modes=modes,
|
|
196
|
+
intensities=intensities,
|
|
197
|
+
bins=bins,
|
|
198
|
+
apply_cross_section=apply_cross_section,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
metadata = {
|
|
202
|
+
"method": "almost-isotropic incoherent approximation",
|
|
203
|
+
"cross sections": ("incoherent + coherent" if apply_cross_section else "none"),
|
|
204
|
+
"line_data": [
|
|
205
|
+
{"atom_index": i, "atom_symbol": symbol, "quantum_order": 1}
|
|
206
|
+
for i, symbol in enumerate(modes.crystal.atom_type)
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
return Spectrum1DCollection(x_data=bins, y_data=y_data, metadata=metadata)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def calculate_almost_isotropic_incoherent_combination_spectra(
|
|
213
|
+
modes: QpointPhononModes,
|
|
214
|
+
mode_displacements: Displacements,
|
|
215
|
+
atomic_displacements: Quantity,
|
|
216
|
+
nominal_q2: Quantity,
|
|
217
|
+
bins: Quantity,
|
|
218
|
+
apply_cross_section: bool = True,
|
|
219
|
+
) -> Spectrum1DCollection:
|
|
220
|
+
"""Calculate two-phonon intensities in almost-isotropic incoherent approximation.
|
|
221
|
+
|
|
222
|
+
Actual q-points of phonon modes will be disregarded; instead each mode
|
|
223
|
+
intensity will be based on a separate array of nominal Q^2 values
|
|
224
|
+
corresponding to modes. This is intended to approximate powder-averaging
|
|
225
|
+
with kinematic constraints: for indirect geometry the energy-Q^2
|
|
226
|
+
relationship can be determined using abinslib.utils.calculate_indirect_q2.
|
|
227
|
+
|
|
228
|
+
These should be determined for each two-phonon combination
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
modes: phonon frequency and eigenvector dataset
|
|
232
|
+
mode_displacements: phonon mode displacement dataset
|
|
233
|
+
(This can be obtained using :func:`Displacements.from_modes(modes)`.)
|
|
234
|
+
atomic_displacements: thermal average atomic displacements indexed
|
|
235
|
+
(atom, direction, direction)
|
|
236
|
+
nominal_q2:
|
|
237
|
+
Scalar Q^2 values for each combination of two fundamental modes, indexed by
|
|
238
|
+
(q, band, q, band). This is typically related to the combination mode
|
|
239
|
+
frequency by neutron instrument parameters.
|
|
240
|
+
bins:
|
|
241
|
+
Energy or frequency bins used as x_data in resulting spectra
|
|
242
|
+
apply_cross_section:
|
|
243
|
+
Multiply each atom/isotope spectrum by a corresponding total
|
|
244
|
+
neutron scattering cross-section (σ_tot).
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
binned spectra of contribution from each nucleus
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
intensities = calculate_almost_isotropic_incoherent_combinations(
|
|
251
|
+
mode_displacements=mode_displacements,
|
|
252
|
+
atomic_displacements=atomic_displacements,
|
|
253
|
+
nominal_q2=nominal_q2,
|
|
254
|
+
include_dw=True,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
y_data = _bin_combination_modes(
|
|
258
|
+
modes=modes,
|
|
259
|
+
intensities=intensities,
|
|
260
|
+
bins=bins,
|
|
261
|
+
apply_cross_section=apply_cross_section,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
metadata = {
|
|
265
|
+
"method": "almost-isotropic incoherent approximation",
|
|
266
|
+
"cross sections": ("incoherent + coherent" if apply_cross_section else "none"),
|
|
267
|
+
"line_data": [
|
|
268
|
+
{"atom_index": i, "atom_symbol": symbol, "quantum_order": 1}
|
|
269
|
+
for i, symbol in enumerate(modes.crystal.atom_type)
|
|
270
|
+
],
|
|
271
|
+
}
|
|
272
|
+
return Spectrum1DCollection(x_data=bins, y_data=y_data, metadata=metadata)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def q_scaling_almost_isotropic_incoherent_combination_spectra(
|
|
276
|
+
modes: QpointPhononModes,
|
|
277
|
+
mode_displacements: Displacements,
|
|
278
|
+
atomic_displacements: Quantity,
|
|
279
|
+
nominal_q2: Quantity,
|
|
280
|
+
bins: Quantity,
|
|
281
|
+
apply_cross_section: bool = True,
|
|
282
|
+
) -> Spectrum1DCollection:
|
|
283
|
+
"""Calculate two-phonon intensities in almost-isotropic incoherent approximation.
|
|
284
|
+
|
|
285
|
+
Actual q-points of phonon modes will be disregarded; instead each mode
|
|
286
|
+
intensity will be based on a separate array of nominal Q^2 values
|
|
287
|
+
corresponding to modes. This is intended to approximate powder-averaging
|
|
288
|
+
with kinematic constraints.
|
|
289
|
+
|
|
290
|
+
Here we also make the "optimisation" that intensities are initially
|
|
291
|
+
calculated at Q=1 and then re-scaled after binning. (Not actually a big
|
|
292
|
+
computational optimisation here as we still multiply a large Q2 array, but
|
|
293
|
+
it imitates the Mantid implementation.)
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
modes: phonon frequency and eigenvector dataset
|
|
297
|
+
mode_displacements: phonon mode displacement dataset
|
|
298
|
+
(This can be obtained using :func:`Displacements.from_modes(modes)`.)
|
|
299
|
+
atomic_displacements: thermal average atomic displacements indexed
|
|
300
|
+
(atom, direction, direction)
|
|
301
|
+
nominal_q2:
|
|
302
|
+
Scalar Q^2 values corresponding to bin centres. For indirect geometry the
|
|
303
|
+
energy-Q^2 relationship can be determined using
|
|
304
|
+
abinslib.utils.calculate_indirect_q2.
|
|
305
|
+
bins:
|
|
306
|
+
Energy or frequency bins used as x_data in resulting spectra
|
|
307
|
+
apply_cross_section:
|
|
308
|
+
Multiply each atom/isotope spectrum by a corresponding total
|
|
309
|
+
neutron scattering cross-section (σ_tot).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
binned spectra of contribution from each nucleus
|
|
313
|
+
|
|
314
|
+
"""
|
|
315
|
+
intensities = calculate_almost_isotropic_incoherent_combinations(
|
|
316
|
+
mode_displacements=mode_displacements,
|
|
317
|
+
atomic_displacements=atomic_displacements,
|
|
318
|
+
nominal_q2=Quantity(
|
|
319
|
+
np.ones((*modes.frequencies.shape, *modes.frequencies.shape)), "Å^-2"
|
|
320
|
+
),
|
|
321
|
+
include_dw=False,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
y_data = _bin_combination_modes(
|
|
325
|
+
modes=modes,
|
|
326
|
+
intensities=intensities,
|
|
327
|
+
bins=bins,
|
|
328
|
+
apply_cross_section=apply_cross_section,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
metadata = {
|
|
332
|
+
"method": "almost-isotropic incoherent approximation",
|
|
333
|
+
"cross sections": ("incoherent + coherent" if apply_cross_section else "none"),
|
|
334
|
+
"line_data": [
|
|
335
|
+
{"atom_index": i, "atom_symbol": symbol, "quantum_order": 2}
|
|
336
|
+
for i, symbol in enumerate(modes.crystal.atom_type)
|
|
337
|
+
],
|
|
338
|
+
}
|
|
339
|
+
spectra = Spectrum1DCollection(x_data=bins, y_data=y_data, metadata=metadata)
|
|
340
|
+
|
|
341
|
+
q4_scale = nominal_q2**2 / Quantity(1, "Å^-4")
|
|
342
|
+
dw = calculate_isotropic_dw_factor(
|
|
343
|
+
atomic_displacements=atomic_displacements,
|
|
344
|
+
q2=nominal_q2,
|
|
345
|
+
)
|
|
346
|
+
spectra.y_data = spectra.y_data * q4_scale * np.swapaxes(dw, -1, -2)[0]
|
|
347
|
+
|
|
348
|
+
return spectra
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def mantid_like_combination_spectra(
|
|
352
|
+
modes: QpointPhononModes,
|
|
353
|
+
mode_displacements: Displacements,
|
|
354
|
+
atomic_displacements: Quantity,
|
|
355
|
+
nominal_q2: Quantity,
|
|
356
|
+
bins: Quantity,
|
|
357
|
+
apply_cross_section: bool = True,
|
|
358
|
+
) -> Spectrum1DCollection:
|
|
359
|
+
"""Calculate two-phonon intensities with approximations from Abins-Mantid.
|
|
360
|
+
|
|
361
|
+
Currently the emphasis is on reproducibility, not efficiency.
|
|
362
|
+
|
|
363
|
+
- DOS-like almost-isotropic incoherent approximation (i.e. semi-analytic
|
|
364
|
+
powder-averaging equations with traces and contractions)
|
|
365
|
+
- Calculate at nominal Q=1, rescale for Q4 relation and apply Debye-Waller
|
|
366
|
+
_after_ binning
|
|
367
|
+
- Treat each input q-point independently:
|
|
368
|
+
- only consider combination modes at each q
|
|
369
|
+
- weight each of these spectra with the weight of corresponding q
|
|
370
|
+
- Order-2 scale factor is 1/60 for overtones and 1/30 for combinations
|
|
371
|
+
|
|
372
|
+
- DW factor *is* still correctly averaged over q-point contributions
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
modes: phonon frequency and eigenvector dataset
|
|
376
|
+
mode_displacements: phonon mode displacement dataset
|
|
377
|
+
(This can be obtained using :func:`Displacements.from_modes(modes)`.)
|
|
378
|
+
atomic_displacements: thermal average atomic displacements indexed
|
|
379
|
+
(atom, direction, direction)
|
|
380
|
+
nominal_q2:
|
|
381
|
+
Scalar Q^2 values corresponding to bin centres. For indirect geometry the
|
|
382
|
+
energy-Q^2 relationship can be determined using
|
|
383
|
+
abinslib.utils.calculate_indirect_q2.
|
|
384
|
+
bins:
|
|
385
|
+
Energy or frequency bins used as x_data in resulting spectra
|
|
386
|
+
apply_cross_section:
|
|
387
|
+
Multiply each atom/isotope spectrum by a corresponding total
|
|
388
|
+
neutron scattering cross-section (σ_tot).
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
binned spectra of contribution from each nucleus
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
spectra = Spectrum1DCollection(
|
|
395
|
+
bins, np.empty((0, len(bins))) * ureg("barn") / bins.units
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
for q_index, weight in enumerate(modes.weights):
|
|
399
|
+
qpt_modes = QpointPhononModes(
|
|
400
|
+
crystal=modes.crystal,
|
|
401
|
+
qpts=modes.qpts[np.newaxis, q_index],
|
|
402
|
+
frequencies=modes.frequencies[np.newaxis, q_index],
|
|
403
|
+
eigenvectors=modes.eigenvectors[np.newaxis, q_index],
|
|
404
|
+
weights=np.array([1.0]),
|
|
405
|
+
)
|
|
406
|
+
qpt_displacements = Displacements(
|
|
407
|
+
displacements=mode_displacements.displacements[np.newaxis, q_index],
|
|
408
|
+
weights=np.array([1.0]),
|
|
409
|
+
bose_n=mode_displacements.bose_n[np.newaxis, q_index],
|
|
410
|
+
temperature=mode_displacements.temperature,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
qpt_spectra = q_scaling_almost_isotropic_incoherent_combination_spectra(
|
|
414
|
+
modes=qpt_modes,
|
|
415
|
+
mode_displacements=qpt_displacements,
|
|
416
|
+
atomic_displacements=atomic_displacements,
|
|
417
|
+
nominal_q2=nominal_q2,
|
|
418
|
+
bins=bins,
|
|
419
|
+
apply_cross_section=apply_cross_section,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Apply a couple of quirks from Mantid-Abins implementation:
|
|
423
|
+
#
|
|
424
|
+
# Exact origins/implications are being investigated, but these are
|
|
425
|
+
# needed to reproduce established Mantid-Abins results (which agree
|
|
426
|
+
# with expt well enough...)
|
|
427
|
+
#
|
|
428
|
+
# - Re-weight for current q-point (instead of product of weights)
|
|
429
|
+
# - Apply 1/n! weighting on top of 1/15C mode weighting
|
|
430
|
+
|
|
431
|
+
qpt_spectra.y_data = qpt_spectra.y_data * weight * 0.5
|
|
432
|
+
qpt_spectra.metadata["qpt"] = str(modes.qpts[q_index])
|
|
433
|
+
|
|
434
|
+
spectra = spectra + qpt_spectra
|
|
435
|
+
|
|
436
|
+
spectra.group_by("atom_index") # Combine q-point contributions
|
|
437
|
+
|
|
438
|
+
return spectra
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _bin_combination_modes(
|
|
442
|
+
modes: QpointPhononModes,
|
|
443
|
+
intensities: np.ndarray,
|
|
444
|
+
bins: Quantity,
|
|
445
|
+
apply_cross_section: bool = True,
|
|
446
|
+
) -> Quantity:
|
|
447
|
+
"""Bin intensities corresponding to QpointPhononModes to 1D spectra.
|
|
448
|
+
|
|
449
|
+
This version is intended for the 2-phonon combination modes, so intensities
|
|
450
|
+
has shape (q, band, q, band). Each contribution is weighted by the product
|
|
451
|
+
of q-point weights and positioned at the sum of intensities
|
|
452
|
+
|
|
453
|
+
Output array has shape (atom_indices, bin_indices)
|
|
454
|
+
"""
|
|
455
|
+
bin_width = bins[1] - bins[0]
|
|
456
|
+
|
|
457
|
+
if not np.isclose(modes.weights.sum(), 1):
|
|
458
|
+
raise ValueError(
|
|
459
|
+
"q-point weights sum to more than 1, this would lead to incorrect "
|
|
460
|
+
"scaling between order-1 and order-2 spectra."
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if apply_cross_section:
|
|
464
|
+
atom_weights = _get_total_cross_sections(modes.crystal).to("barn").magnitude
|
|
465
|
+
else:
|
|
466
|
+
atom_weights = np.ones_like(modes.crystal.atom_mass)
|
|
467
|
+
|
|
468
|
+
weighted_intensities = np.einsum(
|
|
469
|
+
"i,k,m,ijklm->ijklm", modes.weights, modes.weights, atom_weights, intensities
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
frequencies = modes.frequencies.to(bins.units).magnitude
|
|
473
|
+
combination_frequencies = (
|
|
474
|
+
frequencies[:, :, None, None] + frequencies[None, None, :, :]
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# reshape for single iteration of frequencies per atom
|
|
478
|
+
combination_frequencies = combination_frequencies.reshape(-1)
|
|
479
|
+
|
|
480
|
+
weighted_intensities = np.moveaxis(weighted_intensities, -1, 0)
|
|
481
|
+
weighted_intensities = weighted_intensities.reshape(
|
|
482
|
+
weighted_intensities.shape[0], -1
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
y_data = np.zeros([modes.crystal.n_atoms, len(bins) - 1])
|
|
486
|
+
|
|
487
|
+
# Swap atom and freq axes for clean iteration
|
|
488
|
+
for atom_index, atom_data in enumerate(weighted_intensities):
|
|
489
|
+
y_q_atom, _ = np.histogram(
|
|
490
|
+
combination_frequencies,
|
|
491
|
+
bins=bins.magnitude,
|
|
492
|
+
weights=atom_data,
|
|
493
|
+
density=False,
|
|
494
|
+
)
|
|
495
|
+
y_data[atom_index] = y_q_atom
|
|
496
|
+
|
|
497
|
+
# Apply correct spectral scaling / units
|
|
498
|
+
y_data = y_data * ureg("barn") / bin_width
|
|
499
|
+
return y_data
|
abinslib/bose.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Bose occupation enum and calculations."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
|
|
5
|
+
from euphonic import Quantity, ureg
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NegativeTemperatureError(ValueError):
|
|
10
|
+
"""Bose factor is not meaningful for negative temperature."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BoseOccupation(Enum):
|
|
14
|
+
"""Occupation number for Bose-Einstein statistics.
|
|
15
|
+
|
|
16
|
+
Typically we use 2N+1 in Debye-Waller factor (i.e. atomic displacements),
|
|
17
|
+
N+1 for energy transfer to the sample and N for energy transfer from the
|
|
18
|
+
sample.
|
|
19
|
+
|
|
20
|
+
ONE is simply 1: i.e. unscaled temperature-insensitive occupation
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
ONE = auto()
|
|
24
|
+
N = auto()
|
|
25
|
+
N_PLUS_ONE = auto()
|
|
26
|
+
TWO_N_PLUS_ONE = auto()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def calculate_bose_factor(
|
|
30
|
+
frequencies: Quantity,
|
|
31
|
+
temperature: Quantity,
|
|
32
|
+
occupation: BoseOccupation,
|
|
33
|
+
) -> np.array:
|
|
34
|
+
"""Get Bose factors corresponding to an array of frequency or energy.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
frequencies: phonon frequencies, usually indexed (qpt, mode)
|
|
38
|
+
temperature: determines magnitude of Bose factors
|
|
39
|
+
occupation: typically N_PLUS_ONE is used for phonon excitations
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Bose factor array corresponding to input frequencies (i.e. usually
|
|
43
|
+
indexed (qpt, mode))
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
if temperature == Quantity(0.0, "kelvin"):
|
|
47
|
+
return _zero_t_bose_factor(frequencies, occupation)
|
|
48
|
+
if temperature < Quantity(0.0, "kelvin"):
|
|
49
|
+
raise NegativeTemperatureError("Temperature must not be negative")
|
|
50
|
+
|
|
51
|
+
frequencies = frequencies.to("hartree").magnitude
|
|
52
|
+
# Cast T to Kelvin first in case of non-multiplicative unit (celsius)
|
|
53
|
+
kT = (ureg.k * temperature.to("kelvin")).to("hartree").magnitude
|
|
54
|
+
|
|
55
|
+
two_n_plus_one = 1 / (np.tanh(frequencies / (2 * kT)))
|
|
56
|
+
|
|
57
|
+
match occupation:
|
|
58
|
+
case BoseOccupation.TWO_N_PLUS_ONE:
|
|
59
|
+
return two_n_plus_one
|
|
60
|
+
case BoseOccupation.N_PLUS_ONE:
|
|
61
|
+
return two_n_plus_one * 0.5 + 0.5
|
|
62
|
+
case BoseOccupation.N:
|
|
63
|
+
return two_n_plus_one * 0.5 - 0.5
|
|
64
|
+
case BoseOccupation.ONE:
|
|
65
|
+
return np.ones_like(two_n_plus_one)
|
|
66
|
+
case other:
|
|
67
|
+
raise TypeError(f"Not a valid occupation number: {other}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _zero_t_bose_factor(
|
|
71
|
+
frequencies: Quantity, occupation: BoseOccupation
|
|
72
|
+
) -> np.ndarray:
|
|
73
|
+
"""Get ideal occupation values if T=0, avoiding divide-by-zero."""
|
|
74
|
+
match occupation:
|
|
75
|
+
case BoseOccupation.N_PLUS_ONE | BoseOccupation.TWO_N_PLUS_ONE:
|
|
76
|
+
return np.ones_like(frequencies.magnitude)
|
|
77
|
+
case BoseOccupation.ONE:
|
|
78
|
+
return np.ones_like(frequencies.magnitude)
|
|
79
|
+
case BoseOccupation.N:
|
|
80
|
+
return np.zeros_like(frequencies.magnitude)
|
|
81
|
+
case other:
|
|
82
|
+
raise TypeError(f"Not a valid occupation number: {other}")
|
abinslib/data.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Wrapper to fetch reference data for tutorials etc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pooch import Pooch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _setup_ref_data() -> Pooch:
|
|
13
|
+
import pooch
|
|
14
|
+
|
|
15
|
+
return pooch.create(
|
|
16
|
+
path=pooch.os_cache("abinslib"),
|
|
17
|
+
# Currently this location is overridden by the only file in registry.
|
|
18
|
+
# Later it will point to something like github release or Zenodo archive,
|
|
19
|
+
# where most ref data can be kept in flat structure with short names.
|
|
20
|
+
base_url=(
|
|
21
|
+
"https://github.com/pace-neutrons/Euphonic/raw/"
|
|
22
|
+
"master/tests_and_analysis/test/data/"
|
|
23
|
+
),
|
|
24
|
+
registry={
|
|
25
|
+
"NaH.phonon": (
|
|
26
|
+
"ccb30647b5cc9a2f3ab470dda77bc6f3ccc19cb1b8adaf35f5c40ccdaabccde1"
|
|
27
|
+
)
|
|
28
|
+
},
|
|
29
|
+
urls={
|
|
30
|
+
"NaH.phonon": (
|
|
31
|
+
"https://github.com/pace-neutrons/Euphonic/raw/master/"
|
|
32
|
+
"tests_and_analysis/test/data/castep_files/NaH/NaH.phonon"
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_data(filename: str) -> Path:
|
|
39
|
+
"""Get external reference data by filename."""
|
|
40
|
+
if _EUPHONIC_TEST_DATA is None:
|
|
41
|
+
msg = (
|
|
42
|
+
"Could not construct reference data collection. Ensure 'pooch' was"
|
|
43
|
+
" installed, e.g. with 'pip install abinslib[tutorials]'."
|
|
44
|
+
)
|
|
45
|
+
raise ImportError(msg)
|
|
46
|
+
|
|
47
|
+
return Path(_EUPHONIC_TEST_DATA.fetch(filename))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ref_data_or_none() -> Pooch | None:
|
|
51
|
+
try:
|
|
52
|
+
import pooch # noqa: F401
|
|
53
|
+
except ImportError:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
return _setup_ref_data()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_EUPHONIC_TEST_DATA: Pooch | None = _ref_data_or_none()
|