Skip to content

Commit a3745da

Browse files
authored
Merge pull request #497 from scipp/scattering-table
Add a function to access scattering parameters
2 parents d357133 + 48faefa commit a3745da

File tree

7 files changed

+566
-1
lines changed

7 files changed

+566
-1
lines changed

docs/api-reference/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ and possible confusion of `theta` (from Bagg's law) with `theta` in spherical co
6868
:template: module-template.rst
6969
:recursive:
7070
71+
atoms
7172
conversion
7273
io
7374
logging

docs/bibliography.bib

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ @article{busing:1967
2020
volume = {22},
2121
pages = {457--464},
2222
}
23+
24+
@article{sears:1992,
25+
author = {Varley F. Sears},
26+
title = {Neutron scattering lengths and cross sections},
27+
journal = {Neutron News},
28+
volume = {3},
29+
number = {3},
30+
pages = {26-37},
31+
year = {1992},
32+
publisher = {Taylor & Francis},
33+
doi = {10.1080/10448639208218770},
34+
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ exclude_dirs = ["docs/conf.py", "tests", "tools"]
9595

9696
[tool.codespell]
9797
ignore-words-list = "elemt"
98-
skip = "./.git,./.tox,*/.virtual_documents,*/.ipynb_checkpoints,*.pdf,*.svg"
98+
skip = "./.git,./.tox,*/.virtual_documents,*/.ipynb_checkpoints,*.pdf,*.svg,*.csv"
9999

100100
[tool.black]
101101
skip-string-normalization = true

src/scippneutron/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from .instrument_view import instrument_view
4242
from .io.nexus.load_nexus import load_nexus, load_nexus_json
4343
from .data_streaming.data_stream import data_stream
44+
from . import atoms
4445
from . import data
4546

4647
del importlib

src/scippneutron/atoms/__init__.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
"""Parameters for neutron interactions with atoms."""
4+
from __future__ import annotations
5+
6+
import dataclasses
7+
import importlib.resources
8+
from functools import lru_cache
9+
from typing import Optional, TextIO, Union
10+
11+
import scipp as sc
12+
13+
14+
def reference_wavelength() -> sc.Variable:
15+
"""Return the reference wavelength for absorption cross-sections.
16+
17+
Returns
18+
-------
19+
:
20+
1.7982 Å
21+
"""
22+
return sc.scalar(1.7982, unit='angstrom')
23+
24+
25+
@dataclasses.dataclass(frozen=True, eq=False)
26+
class ScatteringParams:
27+
"""Scattering parameters for neutrons with a specific element / isotope.
28+
29+
Provides access to the scattering lengths and cross-sections of neutrons
30+
with a given element or isotope.
31+
Values have been retrieved at 2024-02-19T17:00:00Z from the list at
32+
https://www.ncnr.nist.gov/resources/n-lengths/list.html
33+
which is based on :cite:`sears:1992`.
34+
Values are ``None`` where the table does not provide values.
35+
36+
The absorption cross-section applies to neutrons with a wavelength
37+
of 1.7982 Å.
38+
See :func:`reference_wavelength`.
39+
"""
40+
41+
isotope: str
42+
"""Element / isotope name."""
43+
coherent_scattering_length_re: Optional[sc.Variable]
44+
"""Bound coherent scattering length (real part)."""
45+
coherent_scattering_length_im: Optional[sc.Variable]
46+
"""Bound coherent scattering length (imaginary part)."""
47+
incoherent_scattering_length_re: Optional[sc.Variable]
48+
"""Bound incoherent scattering length (real part)."""
49+
incoherent_scattering_length_im: Optional[sc.Variable]
50+
"""Bound incoherent scattering length (imaginary part)."""
51+
coherent_scattering_cross_section: Optional[sc.Variable]
52+
"""Bound coherent scattering cross-section."""
53+
incoherent_scattering_cross_section: Optional[sc.Variable]
54+
"""Bound incoherent scattering cross-section."""
55+
total_scattering_cross_section: Optional[sc.Variable]
56+
"""Total bound scattering cross-section."""
57+
absorption_cross_section: Optional[sc.Variable]
58+
"""Absorption cross-section for λ = 1.7982 Å neutrons."""
59+
60+
def __eq__(self, other: object) -> Union[bool, type(NotImplemented)]:
61+
if not isinstance(other, ScatteringParams):
62+
return NotImplemented
63+
return all(
64+
self.isotope == other.isotope
65+
if field.name == 'isotope'
66+
else _eq_or_identical(getattr(self, field.name), getattr(other, field.name))
67+
for field in dataclasses.fields(self)
68+
)
69+
70+
@staticmethod
71+
@lru_cache()
72+
def for_isotope(isotope: str) -> ScatteringParams:
73+
"""Return the scattering parameters for the given element / isotope.
74+
75+
Parameters
76+
----------
77+
isotope:
78+
Name of the element or isotope.
79+
For example, 'H', '3He', 'V', '50V'.
80+
81+
Returns
82+
-------
83+
:
84+
Neutron scattering parameters.
85+
"""
86+
with _open_scattering_parameters_file() as f:
87+
while line := f.readline():
88+
name, rest = line.split(',', 1)
89+
if name == isotope:
90+
return _parse_line(isotope, rest)
91+
raise ValueError(f"No entry for element / isotope '{isotope}'")
92+
93+
94+
def _open_scattering_parameters_file() -> TextIO:
95+
return (
96+
importlib.resources.files('scippneutron.atoms')
97+
.joinpath('scattering_parameters.csv')
98+
.open('r')
99+
)
100+
101+
102+
def _parse_line(isotope: str, line: str) -> ScatteringParams:
103+
line = line.rstrip().split(',')
104+
return ScatteringParams(
105+
isotope=isotope,
106+
coherent_scattering_length_re=_assemble_scalar(line[0], line[1], 'fm'),
107+
coherent_scattering_length_im=_assemble_scalar(line[2], line[3], 'fm'),
108+
incoherent_scattering_length_re=_assemble_scalar(line[4], line[5], 'fm'),
109+
incoherent_scattering_length_im=_assemble_scalar(line[6], line[7], 'fm'),
110+
coherent_scattering_cross_section=_assemble_scalar(line[8], line[9], 'barn'),
111+
incoherent_scattering_cross_section=_assemble_scalar(
112+
line[10], line[11], 'barn'
113+
),
114+
total_scattering_cross_section=_assemble_scalar(line[12], line[13], 'barn'),
115+
absorption_cross_section=_assemble_scalar(line[14], line[15], 'barn'),
116+
)
117+
118+
119+
def _assemble_scalar(value: str, std: str, unit: str) -> Optional[sc.Variable]:
120+
if not value:
121+
return None
122+
value = float(value)
123+
variance = float(std) ** 2 if std else None
124+
return sc.scalar(value, variance=variance, unit=unit)
125+
126+
127+
def _eq_or_identical(a: Optional[sc.Variable], b: Optional[sc.Variable]) -> bool:
128+
if a is None:
129+
return b is None
130+
return sc.identical(a, b)

0 commit comments

Comments
 (0)