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 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()