Source code for dynasor.post_processing.neutron_scattering_lengths

import json
from importlib.resources import files
from typing import Dict, List

import numpy as np
from pandas import DataFrame
from .weights import Weights


[docs]class NeutronScatteringLengths(Weights): """This class provides sample weights corresponding to neutron scattering lengths. By default, the coherent and incoherent scattering lengths are weighted by the natural abundance of each isotope of the considered atomic species. This weighting can be overwritten using the :attr:`abundances` argument. The scattering lengths have been extracted from `this NIST database <https://www.ncnr.nist.gov/resources/n-lengths/list.html>`__, which in turn have been taken from Table 1 of Neutron News **3**, 26 (1992); `doi: 10.1080/10448639208218770 <https://doi.org/10.1080/10448639208218770>`_. Parameters ---------- atom_types List of atomic species for which to retrieve scattering lengths. abundances Dict of the desired fractional abundance of each isotope for each species in the sample. For example, to use an equal weighting of all isotopes of oxygen, one can write ``abundances['O'] = dict(16=1/3, 17=1/3, 18=1/3)``. Note that the abundance for any isotopes that are *not* included in this dict is automatically set to zero. In other words, you need to ensure that the abundances provided sum up to 1. By default the neutron scattering lengths are weighted proportionally to the natural abundance of each isotope. """ def __init__( self, atom_types: List[str], abundances: Dict[str, Dict[int, float]] = None, ): scat_lengths = _read_scattering_lengths() # Sub select only the relevant species scat_lengths = scat_lengths[scat_lengths.species.isin(atom_types)].reset_index() # Update the abundances if another weighting is desired if abundances is not None: for species in abundances: scat_lengths.loc[scat_lengths.species == species, 'abundance'] = 0 for Z, frac in abundances[species].items(): match = (scat_lengths.species == species) & (scat_lengths.isotope == Z) if not np.any(match): # Check if any row+column matches raise ValueError(f'No match in database for {species} and isotope {Z}') scat_lengths.loc[match, 'abundance'] = frac self._scattering_lengths = scat_lengths # Check if any of the fetched scattering lengths is None, # indicating that it is missing in the experimental database. # Only raise an error if the desired abundance is greater than 0. nan_rows = scat_lengths[scat_lengths.isnull().any(axis=1) & (scat_lengths.abundance > 0.0)] if not nan_rows.empty: # Grab first offending entry row = nan_rows.iloc[0] raise ValueError(f'Non-zero abundance of {row.isotope}{row.species}' ' with missing tabulated scattering length.') # Make sure abundances add up to 100% by_species = self._scattering_lengths.groupby('species') for species, species_df in by_species: if not np.isclose(species_df.abundance.sum(), 1): raise ValueError(f'Abundance values for {species} do not sum up to 1.0') # Compute scattering lengths weighted by abundance weights_coh = by_species.apply( lambda s: (s.b_coh * s.abundance) .sum().real, # weights in dynasor can only be real atm include_groups=False ).to_dict() # First compute the average scattering length, then take the square # since the incoherent scattering lengths enter as b_incoh**2, but # dynasor only applies a single weighting factor w_incoh. weights_inc = by_species.apply( lambda s: ((s.b_inc * s.abundance).sum()**2).real, include_groups=False ).to_dict() supports_currents = False super().__init__(weights_coh, weights_inc, supports_currents) @property def abundances(self) -> Dict[str, Dict[int, float]]: abundance_dict = {} for (species), species_df in self._scattering_lengths.groupby('species'): abundance_dict[species] = {} for (isotope, abundance), _ in species_df.groupby(['isotope', 'abundance']): abundance_dict[species][isotope] = abundance return abundance_dict
def _read_scattering_lengths() -> DataFrame: """ Extracts the scattering lengths from the file `neutron_scattering_lengths.json` for each of the supplied species. Scattering lengths are in units of fm. The scattering lengths have been extracted from the following NIST database: https://www.ncnr.nist.gov/resources/n-lengths/list.html, which in turn have been extracted from Neutron News **3**, No. 3***, 26 (1992). """ data_file = files(__package__) / 'neutron_scattering_lengths.json' with open(data_file) as fp: scattering_lengths = json.load(fp) data = [] for species in scattering_lengths: for isotope in scattering_lengths[species]: for fld in 'b_c b_i'.split(): val = scattering_lengths[species][isotope][fld] if 'None' in val: scattering_lengths[species][isotope][fld] = np.nan elif 'j' in val: scattering_lengths[species][isotope][fld] = complex(val) else: scattering_lengths[species][isotope][fld] = float(val) data.append( dict( species=species, isotope=int(isotope), abundance=scattering_lengths[species][isotope]['abundance'] / 100, # % -> fraction b_coh=scattering_lengths[species][isotope]['b_c'], b_inc=scattering_lengths[species][isotope]['b_i'], ) ) scattering_lengths = DataFrame.from_dict(data) return scattering_lengths