|
| 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