Skip to content

Commit e650d35

Browse files
committed
Initial implementation of scaled lookup table for TOSCA
This is an interpolated lookup approach to the TOSCA resolution function, using McStas data from Adrien Perrichon
1 parent f04f721 commit e650d35

File tree

6 files changed

+157
-3
lines changed

6 files changed

+157
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ include-package-data = true
6868
where = ["src"]
6969

7070
[tool.setuptools.package-data]
71-
resolution_functions = ["instrument_data/*.yaml"]
71+
resolution_functions = ["instrument_data/*.yaml", "instrument_data/*.npz"]

src/resolution_functions/instrument.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
import yaml
1313
from typing import Iterator, Iterable, Optional, Union, TYPE_CHECKING
1414

15+
16+
INSTRUMENT_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'instrument_data')
17+
18+
1519
from .models import MODELS
1620

1721
if TYPE_CHECKING:
1822
from .models.model_base import ModelData, InstrumentModel
1923
from inspect import Signature
2024

21-
INSTRUMENT_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'instrument_data')
22-
2325
INSTRUMENT_MAP: dict[str, tuple[str, None | str]] = {
2426
'ARCS': ('arcs.yaml', None),
2527
'CNCS': ('cncs.yaml', None),

src/resolution_functions/instrument_data/tosca.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,10 @@ version:
146146
<<: *tosca_configurations_forward
147147
# TODO: Recompute below once average_secondary_flight_path for Forward has been recomputed
148148
distance_sample_analyzer: 0.2312830382624369 # m (as above)
149+
perrichon2022: "perrichon2022_v1"
150+
perrichon2022_v1:
151+
function: "scaled_tabulated"
152+
citation: ["https://doi.org/10.1016/j.nima.2022.167401"]
153+
parameters:
154+
npz: "tosca_perrichon2022_v1.npz"
155+
configurations: {}
Binary file not shown.

src/resolution_functions/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from typing import TYPE_CHECKING
1515

16+
from .lookuptables import ScaledTabulatedModel
1617
from .polynomial import PolynomialModel1D, DiscontinuousPolynomialModel1D
1718
from .panther_abins import PantherAbINSModel
1819
from .pychop import PyChopModelFermi, PyChopModelNonFermi, PyChopModelCNCS, PyChopModelLET
@@ -32,5 +33,6 @@
3233
'pychop_fit_fermi': PyChopModelFermi,
3334
'pychop_fit_cncs': PyChopModelCNCS,
3435
'pychop_fit_let': PyChopModelLET,
36+
'scaled_tabulated': ScaledTabulatedModel,
3537
}
3638
"""A dictionary mapping the unique name of a model to the corresponding class."""
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""
2+
A model based on intepolated lookup tables
3+
4+
All classes within are exposed for reference only and should not be instantiated directly. For
5+
obtaining the :term:`resolution function` of an :term:`instrument`, please use the
6+
`resolution_functions.instrument.Instrument.get_resolution_function` method.
7+
"""
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass
11+
from typing import ClassVar, TYPE_CHECKING
12+
13+
import numpy as np
14+
from numpy.polynomial.polynomial import Polynomial
15+
16+
from .model_base import InstrumentModel, ModelData
17+
from .mixins import SimpleBroaden1DMixin
18+
from ..instrument import INSTRUMENT_DATA_PATH
19+
20+
if TYPE_CHECKING:
21+
from jaxtyping import Float
22+
23+
24+
@dataclass(init=True, repr=True, frozen=True, slots=True, kw_only=True)
25+
class ScaledTabulatedModelData(ModelData):
26+
"""
27+
Data for the `ScaledTabulatedModel` :term:`model`.
28+
29+
Attributes
30+
----------
31+
function
32+
The name of the function, i.e. the alias for `ScaledTabulatedModel`.
33+
citation
34+
The citation for the model. Please use this to look up more details and cite the model.
35+
npz
36+
Relative path from Instrument yaml files to lookup table file
37+
restrictions
38+
defaults
39+
"""
40+
npz: str
41+
42+
43+
class ScaledTabulatedModel(SimpleBroaden1DMixin, InstrumentModel):
44+
"""
45+
Model using a lookup table to model a 1D :term:`instrument`.
46+
47+
This allows non-Gaussian shapes to be produced. For smooth interpolation
48+
and data efficiency, the x-axis is scaled in proportion to an approximated
49+
standard deviation. This standard deviation is fitted to a polynomial
50+
function of energy.
51+
52+
Parameters
53+
----------
54+
model_data
55+
The data associated with the model for a given version of a given instrument.
56+
57+
Attributes
58+
----------
59+
input
60+
The names of the columns in the ``omega_q`` array expected by all computation methods, i.e.
61+
the names of the independent variables ([Q, w]) that the model models.
62+
data_class
63+
Reference to the `PolynomialModelData` type.
64+
npz
65+
The .npz file containing the model data
66+
citation
67+
"""
68+
input = ('energy_transfer',)
69+
70+
data_class: ClassVar[type[ScaledTabultedModelData]] = ScaledTabulatedModelData
71+
72+
def __init__(self, model_data: ScaledTabulatedModelData, **_):
73+
from pathlib import Path
74+
75+
from numpy.polynomial import Polynomial
76+
from scipy.interpolate import RegularGridInterpolator
77+
78+
super().__init__(model_data)
79+
self.data = np.load(Path(INSTRUMENT_DATA_PATH) / model_data.npz)
80+
81+
self.polynomial = Polynomial(coef=self.data["coef"],
82+
domain=self.data["domain"],
83+
window=self.data["window"])
84+
self._interp = RegularGridInterpolator((self.data["energy_transfer"], self.data["kernel_energies"]),
85+
self.data["table"],
86+
method="linear",
87+
bounds_error=False,
88+
fill_value=0.)
89+
90+
def get_characteristics(self, omega_q: Float[np.ndarray, 'energy_transfer dimension=1']
91+
) -> dict[str, Float[np.ndarray, 'sigma']]:
92+
"""
93+
Computes the broadening width at each value of energy transfer (`omega_q`).
94+
95+
The model approximates the broadening using the Gaussian distribution, so the returned
96+
widths are in the form of the standard deviation (sigma).
97+
98+
Parameters
99+
----------
100+
omega_q
101+
The energy transfer in meV at which to compute the width in sigma of the kernel.
102+
This *must* be a ``sample`` x 1 2D array where ``sample`` is the number of energy
103+
transfers.
104+
105+
Returns
106+
-------
107+
characteristics
108+
The characteristics of the broadening function, i.e. the Gaussian width as sigma.
109+
"""
110+
return {'sigma': self.polynomial(omega_q[:, 0])}
111+
112+
def get_kernel(self,
113+
omega_q: Float[np.ndarray, 'sample dimension=1'],
114+
mesh: Float[np.ndarray, 'mesh'],
115+
) -> Float[np.ndarray, 'sample mesh']:
116+
117+
assert len(omega_q.shape) == 2 and omega_q.shape[1] == 1
118+
energy = omega_q
119+
120+
scale_factors = self.polynomial(energy)
121+
scaled_x_values = mesh / scale_factors
122+
123+
# Clip lookup energies to known maximum; width scaling should give a
124+
# reasonable extrapolation from there
125+
energy = np.minimum(energy, max(self.data["energy_transfer"]))
126+
127+
energy_expanded = np.meshgrid(energy[:, None], mesh, indexing="ij")[0]
128+
lookup_mesh = np.stack([energy_expanded, scaled_x_values], axis=-1)
129+
interp_kernels = self._interp(lookup_mesh) / scale_factors
130+
return interp_kernels
131+
132+
def get_peak(self,
133+
omega_q: Float[np.ndarray, 'sample dimension=1'],
134+
mesh: Float[np.ndarray, 'mesh'],
135+
) -> Float[np.ndarray, 'sample mesh']:
136+
shifted_meshes = [mesh - energy for energy in omega_q[:, 0]]
137+
138+
shifted_kernels = [
139+
self.get_kernel(np.array([omega_q]), shifted_mesh)
140+
for omega_q, shifted_mesh in zip(omega_q, shifted_meshes)
141+
]
142+
143+
return np.array(np.vstack(shifted_kernels))

0 commit comments

Comments
 (0)