Skip to content

Commit 2f39608

Browse files
ajjacksonpeterfpeterson
authored andcommitted
Abins: improve handling of isotopic cross-sections
This is a squashed version of mantidproject#38448 Create failing test: Abins get_cross_section for Zn65 This isotope currently gives a NaN cross-section. We should ensure that unsupported isotopes give a noisy error instead. Implement ValueError for NaN cross-section data Fix missing return value, test good cases Begin refactoring Abins isotope handling - remove "substitution" logic branch - instead delegate isotope detection and workspace naming to a new class - in this commit a new Atom is still (unnecessarily) spawned to get cross-section Continue refactor: streamline _create_workspace We can now pass on the AtomInfo object rather than its consitituent data. Continue refactor: move AtomInfo into _fill_s_workspace methods Simplify get_cross_section from an established AtomInfo Handle neutron data for atoms where one isotope = mixture In cases such as F, one isotope is so dominant that essentially we would always use the properties of the standard mixture. Oddly, in this case Mantid will not find neutron data if the atomic mass was provided. i.e. Atom('F').neutron() works, but Atom('F', 19).neutron() returns a dict full of NaN, despite them having exactly the same mass. Update Abins2D to use AtomInfo class Handle some more Abins isotope challenging cases Abins input files usually give the element symbol and mass. These masses often do not match exactly to those in Mantid's data. - In the case that the nearest isotope has no data, but the "mixture" is _quite_ near the target mass (0.01 amu, for now), log a warning and use the mixture. This helps handle nasty cases like 127I where Mantid has _slightly_ different masses for the pure isotope and mixture, but essentially it is a 100% mixture and there is no neutron-scattering data for the isolated isotope. - If the nearest isotope has no data and the standard mixture is too far away, raise an error. Direct unit test for AtomInfo Test refactor: move error check closer to source The error is now raised by the data class when we try to access a property that uses Atom. As such it is not really a get_cross_section test any more and can be moved out. Add release note Tweak release note format Update scripts/abins/abinsalgorithm.py Co-authored-by: Adri Diaz <146007827+adriazalvarez@users.noreply.github.com> Move AtomInfo to separate file Tighten epsilon for "same" mass values, re-use in atominfo This gets rid of a hard-coded epsilon. Strictly these two comparisons are not quite doing the same thing, but this precision should be good for both.
1 parent b53674e commit 2f39608

File tree

9 files changed

+220
-126
lines changed

9 files changed

+220
-126
lines changed

Framework/PythonInterface/plugins/algorithms/Abins.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77

88
from typing import Dict
99

10+
import numpy as np
11+
1012
from mantid.api import AlgorithmFactory, FileAction, FileProperty, PythonAlgorithm, Progress
1113
from mantid.api import WorkspaceFactory, AnalysisDataService
1214

1315
# noinspection PyProtectedMember
1416
from mantid.simpleapi import ConvertUnits, GroupWorkspaces, Load
1517
from mantid.kernel import Direction, StringListValidator
1618
import abins
17-
from abins.abinsalgorithm import AbinsAlgorithm
19+
from abins.abinsalgorithm import AbinsAlgorithm, AtomInfo
1820

1921

2022
# noinspection PyPep8Naming,PyMethodMayBeStatic
@@ -184,14 +186,13 @@ def PyExec(self):
184186
self.setProperty("OutputWorkspace", self._out_ws_name)
185187
prog_reporter.report("Group workspace with all required dynamical structure factors has been constructed.")
186188

187-
def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None, nucleons_number=None):
189+
def _fill_s_workspace(self, *, s_points: np.ndarray, workspace: str, species: AtomInfo | None = None):
188190
"""
189191
Puts S into workspace(s).
190192
191193
:param s_points: dynamical factor for the given atom
192194
:param workspace: workspace to be filled with S
193-
:param protons_number: number of protons in the given type fo atom
194-
:param nucleons_number: number of nucleons in the given type of atom
195+
:param species: atom/isotope identity and data
195196
"""
196197

197198
from abins.constants import FUNDAMENTALS, ONE_DIMENSIONAL_INSTRUMENTS, ONE_DIMENSIONAL_SPECTRUM
@@ -201,15 +202,11 @@ def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None,
201202

202203
# only FUNDAMENTALS [data is 2d with one row]
203204
if s_points.shape[0] == FUNDAMENTALS:
204-
self._fill_s_1d_workspace(
205-
s_points=s_points[0], workspace=workspace, protons_number=protons_number, nucleons_number=nucleons_number
206-
)
205+
self._fill_s_1d_workspace(s_points=s_points[0], workspace=workspace, species=species)
207206

208207
# total workspaces [data is 1d vector]
209208
elif len(s_points.shape) == ONE_DIMENSIONAL_SPECTRUM:
210-
self._fill_s_1d_workspace(
211-
s_points=s_points, workspace=workspace, protons_number=protons_number, nucleons_number=nucleons_number
212-
)
209+
self._fill_s_1d_workspace(s_points=s_points, workspace=workspace, species=species)
213210

214211
# quantum order events (fundamentals or overtones + combinations for the given order)
215212
# [data is 2d table of S with a row for each quantum order]
@@ -221,28 +218,19 @@ def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None,
221218
wrk_name = f"{workspace}_quantum_event_{n + 1}"
222219
partial_wrk_names.append(wrk_name)
223220

224-
self._fill_s_1d_workspace(
225-
s_points=s_points[n], workspace=wrk_name, protons_number=protons_number, nucleons_number=nucleons_number
226-
)
221+
self._fill_s_1d_workspace(s_points=s_points[n], workspace=wrk_name, species=species)
227222

228223
GroupWorkspaces(InputWorkspaces=partial_wrk_names, OutputWorkspace=workspace)
229224

230-
def _fill_s_1d_workspace(self, s_points=None, workspace=None, protons_number=None, nucleons_number=None):
225+
def _fill_s_1d_workspace(self, *, s_points: np.ndarray, workspace: str, species: AtomInfo | None = None):
231226
"""
232227
Puts 1D S into workspace.
233-
:param protons_number: number of protons in the given type of atom
234-
:param nucleons_number: number of nucleons in the given type of atom
235228
:param s_points: dynamical factor for the given atom
236229
:param workspace: workspace to be filled with S
230+
:param species: atom/isotope identity and data
237231
"""
238-
if protons_number is not None:
239-
s_points = (
240-
s_points
241-
* self._scale
242-
* self.get_cross_section(
243-
scattering=self._scale_by_cross_section, protons_number=protons_number, nucleons_number=nucleons_number
244-
)
245-
)
232+
if species is not None:
233+
s_points = s_points * self._scale * self.get_cross_section(scattering=self._scale_by_cross_section, species=species)
246234
dim = 1
247235
length = s_points.size
248236

Framework/PythonInterface/plugins/algorithms/Abins2D.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
import numbers
99
from typing import Dict
1010

11+
import numpy as np
12+
1113
from mantid.api import AlgorithmFactory, PythonAlgorithm, Progress
1214
from mantid.api import WorkspaceFactory, AnalysisDataService
1315
from mantid.kernel import logger
1416

1517
# noinspection PyProtectedMember
1618
from mantid.simpleapi import GroupWorkspaces
1719
import abins
18-
from abins.abinsalgorithm import AbinsAlgorithm
20+
from abins.abinsalgorithm import AbinsAlgorithm, AtomInfo
1921

2022

2123
# noinspection PyPep8Naming,PyMethodMayBeStatic
@@ -184,14 +186,13 @@ def PyExec(self):
184186
self.setProperty("OutputWorkspace", self._out_ws_name)
185187
prog_reporter.report("Group workspace with all required dynamical structure factors has been constructed.")
186188

187-
def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None, nucleons_number=None):
189+
def _fill_s_workspace(self, *, s_points=None, workspace: str, species: AtomInfo | None = None):
188190
"""
189191
Puts S into workspace(s).
190192
191193
:param s_points: dynamical factor for the given atom
192194
:param workspace: workspace to be filled with S
193-
:param protons_number: number of protons in the given type fo atom
194-
:param nucleons_number: number of nucleons in the given type of atom
195+
:param species: Atom/isotope data
195196
"""
196197
from abins.constants import FUNDAMENTALS, TWO_DIMENSIONAL_INSTRUMENTS
197198

@@ -200,15 +201,11 @@ def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None,
200201

201202
# only FUNDAMENTALS [data is 3d with length 1 in axis 0]
202203
if s_points.shape[0] == FUNDAMENTALS:
203-
self._fill_s_2d_workspace(
204-
s_points=s_points[0], workspace=workspace, protons_number=protons_number, nucleons_number=nucleons_number
205-
)
204+
self._fill_s_2d_workspace(s_points=s_points[0], workspace=workspace, species=species)
206205

207206
# total workspaces [data is 2d array of S]
208207
elif s_points.shape[0] == abins.parameters.instruments[self._instrument.get_name()]["q_size"]:
209-
self._fill_s_2d_workspace(
210-
s_points=s_points, workspace=workspace, protons_number=protons_number, nucleons_number=nucleons_number
211-
)
208+
self._fill_s_2d_workspace(s_points=s_points, workspace=workspace, species=species)
212209

213210
# Multiple quantum order events [data is 3d table of S using axis 0 for quantum orders]
214211
else:
@@ -219,9 +216,7 @@ def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None,
219216
wrk_name = f"{workspace}_quantum_event_{n + 1}"
220217
partial_wrk_names.append(wrk_name)
221218

222-
self._fill_s_2d_workspace(
223-
s_points=s_points[n], workspace=wrk_name, protons_number=protons_number, nucleons_number=nucleons_number
224-
)
219+
self._fill_s_2d_workspace(s_points=s_points[n], workspace=wrk_name, species=species)
225220

226221
GroupWorkspaces(InputWorkspaces=partial_wrk_names, OutputWorkspace=workspace)
227222

@@ -232,14 +227,12 @@ def _create_dummy_workspace(self, name):
232227
AnalysisDataService.addOrReplace(name, wrk)
233228
return wrk
234229

235-
def _fill_s_2d_workspace(self, s_points=None, workspace=None, protons_number=None, nucleons_number=None):
230+
def _fill_s_2d_workspace(self, *, s_points: np.ndarray, workspace: str, species: AtomInfo | None = None):
236231
from mantid.api import NumericAxis
237232
from abins.constants import MILLI_EV_TO_WAVENUMBER
238233

239-
if protons_number is not None:
240-
s_points = s_points * self.get_cross_section(
241-
scattering=self._scale_by_cross_section, protons_number=protons_number, nucleons_number=nucleons_number
242-
)
234+
if species is not None:
235+
s_points = s_points * self.get_cross_section(scattering=self._scale_by_cross_section, species=species)
243236

244237
n_q_values, n_freq_bins = s_points.shape
245238
n_q_bins = self._q_bins.size
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- The identification of isotopes has been improved in Abins/Abins2D.
2+
3+
- Previously, a species would only be identified as the standard
4+
isotopic mixture if the mass is very close to the Mantid reference
5+
data. In some cases the values used by external phonon calculators
6+
are significantly different and this could lead to misassignment.
7+
(e.g. Zn in CASTEP.) Now, Abins will initially choose the _nearest_
8+
mass option between an isotope and the standard isotopic mixture.
9+
- Many isotopes lack cross-section data and would lead to NaN
10+
intensities. Now, if a NaN cross-section is identified Abins
11+
will either use the standard mixture data (if the mass is within
12+
0.01) or raise an error.

scripts/abins/abins2.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77

88
from typing import Dict, Optional
99

10+
import numpy as np
11+
1012
from mantid.api import AlgorithmFactory, PythonAlgorithm, Progress
1113
from mantid.api import WorkspaceFactory, AnalysisDataService
1214

1315
# noinspection PyProtectedMember
1416
from mantid.simpleapi import ConvertUnits, GroupWorkspaces
1517
import abins
1618
from abins.abinsalgorithm import AbinsAlgorithm
19+
from abins.atominfo import AtomInfo
1720
from abins.logging import get_logger, Logger
1821

1922

@@ -170,14 +173,13 @@ def PyExec(self):
170173
self.setProperty("OutputWorkspace", self._out_ws_name)
171174
prog_reporter.report("Group workspace with all required dynamical structure factors has been constructed.")
172175

173-
def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None, nucleons_number=None):
176+
def _fill_s_workspace(self, *, s_points: np.ndarray, workspace: str, species: AtomInfo | None = None) -> None:
174177
"""
175178
Puts S into workspace(s).
176179
177180
:param s_points: dynamical factor for the given atom
178-
:param workspace: workspace to be filled with S
179-
:param protons_number: number of protons in the given type fo atom
180-
:param nucleons_number: number of nucleons in the given type of atom
181+
:param workspace: workspace to be filled with S
182+
:param species: Element/isotope data
181183
"""
182184

183185
from abins.constants import FUNDAMENTALS, ONE_DIMENSIONAL_INSTRUMENTS, ONE_DIMENSIONAL_SPECTRUM
@@ -187,15 +189,11 @@ def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None,
187189

188190
# only FUNDAMENTALS [data is 2d with one row]
189191
if s_points.shape[0] == FUNDAMENTALS:
190-
self._fill_s_1d_workspace(
191-
s_points=s_points[0], workspace=workspace, protons_number=protons_number, nucleons_number=nucleons_number
192-
)
192+
self._fill_s_1d_workspace(s_points=s_points[0], workspace=workspace, species=species)
193193

194194
# total workspaces [data is 1d vector]
195195
elif len(s_points.shape) == ONE_DIMENSIONAL_SPECTRUM:
196-
self._fill_s_1d_workspace(
197-
s_points=s_points, workspace=workspace, protons_number=protons_number, nucleons_number=nucleons_number
198-
)
196+
self._fill_s_1d_workspace(s_points=s_points, workspace=workspace, species=species)
199197

200198
# quantum order events (fundamentals or overtones + combinations for the given order)
201199
# [data is 2d table of S with a row for each quantum order]
@@ -207,24 +205,20 @@ def _fill_s_workspace(self, s_points=None, workspace=None, protons_number=None,
207205
wrk_name = f"{workspace}_quantum_event_{n + 1}"
208206
partial_wrk_names.append(wrk_name)
209207

210-
self._fill_s_1d_workspace(
211-
s_points=s_points[n], workspace=wrk_name, protons_number=protons_number, nucleons_number=nucleons_number
212-
)
208+
self._fill_s_1d_workspace(s_points=s_points[n], workspace=wrk_name, species=species)
213209

214210
GroupWorkspaces(InputWorkspaces=partial_wrk_names, OutputWorkspace=workspace)
215211

216-
def _fill_s_1d_workspace(self, s_points=None, workspace=None, protons_number=None, nucleons_number=None):
212+
def _fill_s_1d_workspace(self, s_points=None, workspace=None, species: AtomInfo | None = None):
217213
"""
218214
Puts 1D S into workspace.
219215
:param protons_number: number of protons in the given type of atom
220216
:param nucleons_number: number of nucleons in the given type of atom
221217
:param s_points: dynamical factor for the given atom
222218
:param workspace: workspace to be filled with S
223219
"""
224-
if protons_number is not None:
225-
s_points = s_points * self.get_cross_section(
226-
scattering=self._scale_by_cross_section, protons_number=protons_number, nucleons_number=nucleons_number
227-
)
220+
if species is not None:
221+
s_points = s_points * self.get_cross_section(scattering=self._scale_by_cross_section, species=species)
228222
dim = 1
229223
length = s_points.size
230224

0 commit comments

Comments
 (0)