Coverage for dynasor/post_processing/neutron_scattering_lengths.py: 100%
62 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-20 20:02 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-20 20:02 +0000
1import json
2from importlib.resources import files
3from typing import Optional
5import numpy as np
6from pandas import DataFrame
7from .weights import Weights
10class NeutronScatteringLengths(Weights):
11 """This class provides sample weights corresponding to neutron scattering lengths.
12 By default, the coherent and incoherent scattering lengths are weighted by the natural
13 abundance of each isotope of the considered atomic species.
14 This weighting can be overwritten using the :attr:`abundances` argument.
16 The scattering lengths have been extracted from `this NIST
17 database <https://www.ncnr.nist.gov/resources/n-lengths/list.html>`__,
18 which in turn have been taken from Table 1 of Neutron News **3**, 26 (1992);
19 `doi: 10.1080/10448639208218770 <https://doi.org/10.1080/10448639208218770>`_.
21 Parameters
22 ----------
23 atom_types
24 List of atomic species for which to retrieve scattering lengths.
25 abundances
26 Dict of the desired fractional abundance of each isotope for
27 each species in the sample. For example, to use an equal
28 weighting of all isotopes of oxygen, one can write
29 ``abundances['O'] = dict(16=1/3, 17=1/3, 18=1/3)``. Note that
30 the abundance for any isotopes that are *not* included in this
31 dict is automatically set to zero. In other words, you need to
32 ensure that the abundances provided sum up to 1. By default
33 the neutron scattering lengths are weighted proportionally to
34 the natural abundance of each isotope.
35 """
37 def __init__(
38 self,
39 atom_types: list[str],
40 abundances: Optional[dict[str, dict[int, float]]] = None,
41 ):
42 scat_lengths = _read_scattering_lengths()
44 # Sub select only the relevant species
45 scat_lengths = scat_lengths[scat_lengths.species.isin(atom_types)].reset_index()
47 for species in atom_types:
48 if not np.any(scat_lengths.species == species):
49 raise ValueError('Missing tabulated values '
50 f'for requested species {species}.')
52 # Update the abundances if another weighting is desired
53 if abundances is not None:
54 for species in abundances:
55 scat_lengths.loc[scat_lengths.species == species, 'abundance'] = 0
56 for Z, frac in abundances[species].items():
57 match = (scat_lengths.species == species) & (scat_lengths.isotope == Z)
58 if not np.any(match): # Check if any row+column matches
59 raise ValueError(f'No match in database for {species} and isotope {Z}')
60 scat_lengths.loc[match, 'abundance'] = frac
62 self._scattering_lengths = scat_lengths
64 # Check if any of the fetched scattering lengths is None,
65 # indicating that it is missing in the experimental database.
66 # Only raise an error if the desired abundance is greater than 0.
67 nan_rows = scat_lengths[scat_lengths.isnull().any(axis=1) & (scat_lengths.abundance > 0.0)]
68 if not nan_rows.empty:
69 # Grab first offending entry
70 row = nan_rows.iloc[0]
71 raise ValueError(f'{row.isotope}{row.species} is missing tabulated values for either'
72 ' the coherent or incoherent scattering length.'
73 ' Adjust the abundance parameter to set the fraction of'
74 f' {row.isotope}{row.species} to zero.')
76 # Make sure abundances add up to 100%
77 by_species = self._scattering_lengths.groupby('species')
78 for species, species_df in by_species:
79 if not np.isclose(species_df.abundance.sum(), 1):
80 raise ValueError(f'Abundance values for {species} do not sum up to 1.0')
82 # Compute scattering lengths weighted by abundance
83 weights_coh = by_species.apply(
84 lambda s: (s.b_coh * s.abundance).sum(),
85 include_groups=False
86 ).to_dict()
87 # First compute the average scattering length, then take the square
88 # since the incoherent scattering lengths enter as b_incoh**2, but
89 # dynasor only applies a single weighting factor w_incoh.
90 weights_inc = by_species.apply(
91 lambda s: (s.b_inc * s.abundance).sum(),
92 include_groups=False
93 ).to_dict()
95 supports_currents = False
96 super().__init__(weights_coh, weights_inc, supports_currents)
98 @property
99 def abundances(self) -> dict[str, dict[int, float]]:
100 """Abundances used for calculating scattering lengths."""
101 abundance_dict = {}
102 for (species), species_df in self._scattering_lengths.groupby('species'):
103 abundance_dict[species] = {}
104 for (isotope, abundance), _ in species_df.groupby(['isotope', 'abundance']):
105 abundance_dict[species][isotope] = abundance
106 return abundance_dict
108 @property
109 def parameters(self) -> DataFrame:
110 """Scattering lengths used to compute the coherent and
111 incoherent weights for the selected isotopes.
112 """
113 return self._scattering_lengths
116def _read_scattering_lengths() -> DataFrame:
117 """
118 Extracts the scattering lengths from the file `neutron-scattering-lengths-nist-1992.json`
119 for each of the supplied species. Scattering lengths are in units of fm.
121 The scattering lengths have been extracted from the following NIST
122 database: https://www.ncnr.nist.gov/resources/n-lengths/list.html,
123 which in turn have been extracted from
124 Neutron News **3**, No. 3***, 26 (1992).
125 """
126 data_file = files(__package__) / 'form-factors/neutron-scattering-lengths-nist-1992.json'
127 with open(data_file) as fp:
128 scattering_lengths = json.load(fp)
130 data = []
131 for species in scattering_lengths:
132 for isotope in scattering_lengths[species]:
133 for fld in 'b_coh b_inc'.split():
134 val = scattering_lengths[species][isotope][fld]
135 if 'None' in val:
136 scattering_lengths[species][isotope][fld] = np.nan
137 elif 'j' in val:
138 scattering_lengths[species][isotope][fld] = complex(val)
139 else:
140 scattering_lengths[species][isotope][fld] = float(val)
141 data.append(
142 dict(
143 species=species,
144 isotope=int(isotope),
145 abundance=float(scattering_lengths[species][isotope]['abundance'])
146 / 100, # % -> fraction
147 b_coh=complex(scattering_lengths[species][isotope]['b_coh']),
148 b_inc=complex(scattering_lengths[species][isotope]['b_inc']),
149 )
150 )
151 scattering_lengths = DataFrame.from_dict(data)
152 return scattering_lengths