diff --git a/src/resolution_functions/instrument_data/arcs.yaml b/src/resolution_functions/instrument_data/arcs.yaml index d16ce0c..a80800d 100644 --- a/src/resolution_functions/instrument_data/arcs.yaml +++ b/src/resolution_functions/instrument_data/arcs.yaml @@ -8,10 +8,8 @@ version: d_sample_detector: 3.0 # Distance (x2) from sample to detector (m) aperture_width: 0.1751 # Width of aperture at moderator face (m) theta: -13.75 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 500 # meV allowed_e_init: [ 20, 1500 ] # meV - max_wavenumber: 2000 # meV default_chopper_frequency: 300 allowed_chopper_frequencies: [60, 601, 60] frequency_matrix: [[1]] diff --git a/src/resolution_functions/instrument_data/cncs.yaml b/src/resolution_functions/instrument_data/cncs.yaml index aa14376..db2d506 100644 --- a/src/resolution_functions/instrument_data/cncs.yaml +++ b/src/resolution_functions/instrument_data/cncs.yaml @@ -49,10 +49,8 @@ version: d_sample_detector: 3.5 # Distance (x2) from sample to detector (m) aperture_width: 0. # Width of aperture at moderator face (m) theta: 32.0 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 20 # meV allowed_e_init: [ 0.5, 80 ] # meV - max_wavenumber: 2000 # meV default_chopper_frequency: [ 300, 60 ] allowed_chopper_frequencies: [[60, 301, 60], [60, 301, 60]] constant_frequencies: [0, 60, 60, 0] @@ -67,7 +65,6 @@ version: moderator: type: 3 scaling_function: null - scaling_parameters: null measured_wavelength: [28.60141458, 26.65479018, 24.84065387, 23.14998844, 21.57439041, 20.10602826, 18.73760346, 17.46231422, 16.27382172, 15.16621852, 14.13399926, 13.17203328, 12.27553911, 11.44006072, 10.66144534, diff --git a/src/resolution_functions/instrument_data/hyspec.yaml b/src/resolution_functions/instrument_data/hyspec.yaml index e7a241d..ef646b3 100644 --- a/src/resolution_functions/instrument_data/hyspec.yaml +++ b/src/resolution_functions/instrument_data/hyspec.yaml @@ -8,10 +8,8 @@ version: d_sample_detector: 4.5 # Distance (x2) from sample to detector (m) aperture_width: 0.0 # Width of aperture at moderator face (m) theta: 32.0 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 30 # meV allowed_e_init: [ 3.6, 61.0 ] # meV - max_wavenumber: 2000 # meV default_chopper_frequency: 180 allowed_chopper_frequencies: [60, 421, 60] frequency_matrix: @@ -21,7 +19,6 @@ version: moderator: type: 3 scaling_function: null - scaling_parameters: null measured_wavelength: [28.60141458, 26.65479018, 24.84065387, 23.14998844, 21.57439041, 20.10602826, 18.73760346, 17.46231422, 16.27382172, 15.16621852, 14.13399926, 13.17203328, 12.27553911, 11.44006072, 10.66144534, diff --git a/src/resolution_functions/instrument_data/let.yaml b/src/resolution_functions/instrument_data/let.yaml index 670cbdf..413c553 100644 --- a/src/resolution_functions/instrument_data/let.yaml +++ b/src/resolution_functions/instrument_data/let.yaml @@ -58,10 +58,8 @@ version: d_sample_detector: 3.5 # Distance (x2) from sample to detector (m) aperture_width: 0. # Width of aperture at moderator face (m) theta: 32.0 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 20 # meV allowed_e_init: [ 0., 30. ] # meV - max_wavenumber: 2000 # meV default_chopper_frequency: [ 240, 120 ] allowed_chopper_frequencies: [[10, 301, 10], [10, 301, 10]] constant_frequencies: [0, 10, 0, 0, 0] @@ -71,7 +69,6 @@ version: moderator: type: 3 scaling_function: null - scaling_parameters: null measured_wavelength: [3.8063, 2.1961, 6.2121, 5.3820, 1.4371, 1.7010, 2.6920, 1.9013] measured_width: [90.4, 40.8, 154.4, 131.2, 22.4, 25.6, 52.4, 32.4] parameters: [0.535, 49.28, -3.143] # Parameters for time profile diff --git a/src/resolution_functions/instrument_data/maps.yaml b/src/resolution_functions/instrument_data/maps.yaml index 9693bcb..13d3c86 100644 --- a/src/resolution_functions/instrument_data/maps.yaml +++ b/src/resolution_functions/instrument_data/maps.yaml @@ -8,10 +8,8 @@ version: d_sample_detector: 6.0 # Distance (x2) from sample to detector (m) aperture_width: 0.094 # Width of aperture at moderator face (m) theta: 32.0 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 500 # meV allowed_e_init: [ 0, 2000 ] # meV - max_wavenumber: 2000 # meV default_chopper_frequency: 400 allowed_chopper_frequencies: [50, 601, 50] frequency_matrix: diff --git a/src/resolution_functions/instrument_data/mari.yaml b/src/resolution_functions/instrument_data/mari.yaml index 1da25b1..e414d50 100644 --- a/src/resolution_functions/instrument_data/mari.yaml +++ b/src/resolution_functions/instrument_data/mari.yaml @@ -8,9 +8,7 @@ version: d_sample_detector: 4.022 # Distance (x2) from sample to detector (m) aperture_width: 0.06667 # Width of aperture at moderator face (m) theta: 13.0 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 500 # meV - max_wavenumber: 2000 # meV default_chopper_frequency: 400 allowed_chopper_frequencies: [50, 601, 50] frequency_matrix: @@ -19,7 +17,6 @@ version: moderator: type: 2 scaling_function: null - scaling_parameters: null measured_wavelength: [4.0240, 5.6898, 2.3250, 2.8480, 1.5224, 3.4331, 1.8009, 1.1167] measured_width: [53.2, 62, 39.2, 44.8, 18.8, 48.8, 27.2, 12.4] parameters: [ 38.6, 0.5226 ] # Parameters for time profile diff --git a/src/resolution_functions/instrument_data/merlin.yaml b/src/resolution_functions/instrument_data/merlin.yaml index 1a3a199..3fd2a43 100644 --- a/src/resolution_functions/instrument_data/merlin.yaml +++ b/src/resolution_functions/instrument_data/merlin.yaml @@ -8,9 +8,7 @@ version: d_sample_detector: 2.5 # Distance (x2) from sample to detector (m) aperture_width: 0.06667 # Width of aperture at moderator face (m) theta: 26.7 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 400 # meV - max_wavenumber: 2000 # meV default_chopper_frequency: 400 allowed_chopper_frequencies: [50, 601, 50] frequency_matrix: @@ -19,7 +17,6 @@ version: moderator: type: 2 scaling_function: null - scaling_parameters: null measured_wavelength: [3.81238, 5.38791, 2.20214, 2.69636, 3.25068, 1.70664, 1.9078, 1.4425, 3.11379, 2.41294, 2.47528, 1.27219, 2.07682, 1.05882, 1.55818] measured_width: [49, 56, 34, 37, 42, 29, 30, 25, 40, 34, 35, 21, 31, 18, 26] parameters: [80.0, 0.5226] # Parameters for time profile diff --git a/src/resolution_functions/instrument_data/sequoia.yaml b/src/resolution_functions/instrument_data/sequoia.yaml index 34a0daf..fc48e82 100644 --- a/src/resolution_functions/instrument_data/sequoia.yaml +++ b/src/resolution_functions/instrument_data/sequoia.yaml @@ -8,19 +8,15 @@ version: d_sample_detector: 5.5 # Distance (x2) from sample to detector (m) aperture_width: 0.050 # Width of aperture at moderator face (m) theta: -13.75 # Angle beamline makes with moderator face (degrees) - q_size: 100 default_e_init: 500 # meV allowed_e_init: [ 4, 6000 ] # meV - max_wavenumber: 2000 # meV default_chopper_frequency: 300 allowed_chopper_frequencies: [60, 601, 60] frequency_matrix: [[1]] moderator: type: 1 scaling_function: null - scaling_parameters: null measured_wavelength: null - measured_width: null parameters: [119.63, 33.618, .037, .17, 172.42] # Parameters for time profile detector: type: 2 diff --git a/src/resolution_functions/models/panther_abins.py b/src/resolution_functions/models/panther_abins.py index 6b9f0c8..c66ea8b 100644 --- a/src/resolution_functions/models/panther_abins.py +++ b/src/resolution_functions/models/panther_abins.py @@ -1,4 +1,10 @@ -"""The AbINS model of the PANTHER instrument.""" +""" +The AbINS model of the PANTHER instrument. + +All classes within are exposed for reference only and should not be instantiated directly. For +obtaining the resolution function of an instrument, please use the +`Instrument.get_resolution_function` method. +""" from __future__ import annotations from dataclasses import dataclass diff --git a/src/resolution_functions/models/pychop.py b/src/resolution_functions/models/pychop.py index b7f419a..cf1d13e 100644 --- a/src/resolution_functions/models/pychop.py +++ b/src/resolution_functions/models/pychop.py @@ -1,10 +1,32 @@ +""" +The [PyChop]_ model, a 1D model for direct-geometry 2D instruments. + +[PyChop]_, originating from [Mantid]_ as a single model, is here organised as a collection of +models. These can be split into two types: the `PyChopModelFermi` model, used for all instruments +with a Fermi chopper (which is the sole determinant of the chopper contribution to the resolution), +and models such as `PyChopModelLET` and `PyChopModelCNCS`, which are models of instruments without +a Fermi chopper, in which the first and last choppers determine the chopper contribution to the +resolution. + +All classes here are exposed for reference only and should not be instantiated directly. For +obtaining the resolution function of an instrument, please use the +`Instrument.get_resolution_function` method. + +.. [PyChop] https://github.com/mducle/pychop/tree/main +.. [Mantid] https://mantidproject.org/ +""" from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from copy import deepcopy from math import erf -from typing import Optional, TypedDict, TYPE_CHECKING, Union +from typing import Optional, TYPE_CHECKING, Union + +try: + from typing import NotRequired, TypedDict +except ImportError: + from typing_extensions import NotRequired, TypedDict import numpy as np @@ -25,14 +47,87 @@ @dataclass(init=True, repr=True, frozen=True, slots=True) class PyChopModelData(ModelData): + """ + Base class for PyChop model data. + + Corresponds to the abstract base class `PyChopModel` and so should not be used directly. + Concrete implementations of the PyChop model should have their own model data subclasses of this + class. + + Parameters + ---------- + function + The name of the function, i.e. the alias for `PantherAbINSModel`. + citation + The citation for a particular model. + d_chopper_sample + Distance from the final chopper to sample in meters (m). + d_sample_detector + Distance from sample to detector in meters (m). + aperture_width + Width of aperture at moderator face in meters (m) + theta + The angle that the beamline makes with the moderator face in degrees. + default_e_init + The default value for the initial energy (``e_init``) in meV. + allowed_e_init + The limits (lower and upper bound) for the allowed initial energies (``e_init``). This + should correspond to the physical limitations of the INS instrument. + frequency_matrix + A matrix mapping the relationship between the user-provided parameter ``chopper_frequency`` + or its equivalent (depending on model) to the frequency of each chopper in the instrument. + choppers + Data for each chopper in the instrument. See `FermiChopper` and `DiskChopper` for more info. + moderator + Data for the moderator. See `Moderator` for more info. + detector + Data for the detector. See `Detector` for more info. + sample + Data for the sample. See `Sample` for more info. + tjit + The jitter time in microseconds (us). + + Attributes + ---------- + function + The name of the function, i.e. the alias for `PantherAbINSModel`. + citation + The citation for the model. Please use this to look up more details and cite the model. + d_chopper_sample + Distance from the final chopper to sample in meters (m). + d_sample_detector + Distance from sample to detector in meters (m). + aperture_width + Width of aperture at moderator face in meters (m) + theta + The angle that the beamline makes with the moderator face in degrees. + default_e_init + The default value for the initial energy (``e_init``) in meV. + allowed_e_init + The limits (lower and upper bound) for the allowed initial energies (``e_init``). This + should correspond to the physical limitations of the INS instrument. + frequency_matrix + A matrix mapping the relationship between the user-provided parameter ``chopper_frequency`` + or its equivalent (depending on model) to the frequency of each chopper in the instrument. + choppers + Data for each chopper in the instrument. See `FermiChopper` and `DiskChopper` for more info. + moderator + Data for the moderator. See `Moderator` for more info. + detector + Data for the detector. See `Detector` for more info. + sample + Data for the sample. See `Sample` for more info. + tjit + The jitter time in microseconds (us). + restrictions + defaults + """ d_chopper_sample: float d_sample_detector: float aperture_width: float theta: float - q_size: float default_e_init: float allowed_e_init: list[float] - max_wavenumber: float frequency_matrix: list[list[float]] choppers: dict[str, FermiChopper | DiskChopper] moderator: Moderator @@ -43,6 +138,101 @@ class PyChopModelData(ModelData): @dataclass(init=True, repr=True, frozen=True, slots=True) class PyChopModelDataFermi(PyChopModelData): + """ + Data for the `PyChopModelFermi` model. + + Parameters + ---------- + function + The name of the function, i.e. the alias for `PantherAbINSModel`. + citation + The citation for a particular model. + d_chopper_sample + Distance from the final chopper to sample in meters (m). + d_sample_detector + Distance from sample to detector in meters (m). + aperture_width + Width of aperture at moderator face in meters (m) + theta + The angle that the beamline makes with the moderator face in degrees. + default_e_init + The default value for the initial energy (``e_init``) in meV. + allowed_e_init + The limits (lower and upper bound) for the allowed initial energies (``e_init``). This + should correspond to the physical limitations of the INS instrument. + frequency_matrix + A matrix mapping the relationship between the user-provided parameter ``chopper_frequency`` + to the frequency of each chopper in the instrument. + choppers + Data for each chopper in the instrument. See `FermiChopper` and `DiskChopper` for more info. + moderator + Data for the moderator. See `Moderator` for more info. + detector + Data for the detector. See `Detector` for more info. + sample + Data for the sample. See `Sample` for more info. + tjit + The jitter time in microseconds (us). + default_chopper_frequency + The default value for the Fermi chopper frequency (``chopper_frequency``) in Hz. + allowed_chopper_frequencies + The allowed values for the Fermi chopper frequency (``chopper_frequency``) in Hz. + `allowed_chopper_frequencies` defines the (start, stop, step), which is converted into a + list at runtime. + pslit + Width of the neutron-transparent slit in meters (m). + radius + Radius of the chopper package in meters (m). + rho + Curvature of the chopper package in meters (m). + + Attributes + ---------- + function + The name of the function, i.e. the alias for `PantherAbINSModel`. + citation + The citation for the model. Please use this to look up more details and cite the model. + d_chopper_sample + Distance from the final chopper to sample in meters (m). + d_sample_detector + Distance from sample to detector in meters (m). + aperture_width + Width of aperture at moderator face in meters (m) + theta + The angle that the beamline makes with the moderator face in degrees. + default_e_init + The default value for the initial energy (``e_init``) in meV. + allowed_e_init + The limits (lower and upper bound) for the allowed initial energies (``e_init``). This + should correspond to the physical limitations of the INS instrument. + frequency_matrix + A matrix mapping the relationship between the user-provided parameter ``chopper_frequency`` + to the frequency of each chopper in the instrument. + choppers + Data for each chopper in the instrument. See `FermiChopper` and `DiskChopper` for more info. + moderator + Data for the moderator. See `Moderator` for more info. + detector + Data for the detector. See `Detector` for more info. + sample + Data for the sample. See `Sample` for more info. + tjit + The jitter time in microseconds (us). + default_chopper_frequency + The default value for the Fermi chopper frequency (``chopper_frequency``) in Hz. + allowed_chopper_frequencies + The allowed values for the Fermi chopper frequency (``chopper_frequency``) in Hz. + `allowed_chopper_frequencies` defines the (start, stop, step), which is converted into a + list at runtime. + pslit + Width of the neutron-transparent slit in meters (m). + radius + Radius of the chopper package in meters (m). + rho + Curvature of the chopper package in meters (m). + restrictions + defaults + """ default_chopper_frequency: int allowed_chopper_frequencies: list[int] pslit: float @@ -60,6 +250,104 @@ def defaults(self) -> dict: @dataclass(init=True, repr=True, frozen=True, slots=True) class PyChopModelDataNonFermi(PyChopModelData): + """ + Data for the PyChop models that do not have a Fermi chopper: + + - `PyChopModelCNCS` + - `PyChopModelLET` + + Parameters + ---------- + function + The name of the function, i.e. the alias for `PantherAbINSModel`. + citation + The citation for a particular model. + d_chopper_sample + Distance from the final chopper to sample in meters (m). + d_sample_detector + Distance from sample to detector in meters (m). + aperture_width + Width of aperture at moderator face in meters (m) + theta + The angle that the beamline makes with the moderator face in degrees. + default_e_init + The default value for the initial energy (``e_init``) in meV. + allowed_e_init + The limits (lower and upper bound) for the allowed initial energies (``e_init``). This + should correspond to the physical limitations of the INS instrument. + frequency_matrix + A matrix mapping the relationship between all user-provided chopper frequency parameters + (i.e. the choppers with user control) to the frequency of each chopper in the instrument. + choppers + Data for each chopper in the instrument. See `DiskChopper` for more info. + moderator + Data for the moderator. See `Moderator` for more info. + detector + Data for the detector. See `Detector` for more info. + sample + Data for the sample. See `Sample` for more info. + tjit + The jitter time in microseconds (us). + default_chopper_frequency + The default value for the chopper frequency of each user-controlled chopper in Hz. + allowed_chopper_frequencies + The allowed values for the chopper frequency of each user-controlled chopper, in Hz. Each + value defines the (start, stop, step), which is converted into a list at runtime. + constant_frequencies + The frequency of each chopper in Hz, with those run at a constant frequency having + non-zero values. + source_frequency + The frequency of the neutron source in Hz. + n_frame + Number of frames to calculate time-distance diagram for. + + Attributes + ---------- + function + The name of the function, i.e. the alias for `PantherAbINSModel`. + citation + The citation for the model. Please use this to look up more details and cite the model. + d_chopper_sample + Distance from the final chopper to sample in meters (m). + d_sample_detector + Distance from sample to detector in meters (m). + aperture_width + Width of aperture at moderator face in meters (m) + theta + The angle that the beamline makes with the moderator face in degrees. + default_e_init + The default value for the initial energy (``e_init``) in meV. + allowed_e_init + The limits (lower and upper bound) for the allowed initial energies (``e_init``). This + should correspond to the physical limitations of the INS instrument. + frequency_matrix + A matrix mapping the relationship between all user-provided chopper frequency parameters + (i.e. the choppers with user control) to the frequency of each chopper in the instrument. + choppers + Data for each chopper in the instrument. See `DiskChopper` for more info. + moderator + Data for the moderator. See `Moderator` for more info. + detector + Data for the detector. See `Detector` for more info. + sample + Data for the sample. See `Sample` for more info. + tjit + The jitter time in microseconds (us). + default_chopper_frequency + The default value for the chopper frequency of each user-controlled chopper in Hz. + allowed_chopper_frequencies + The allowed values for the chopper frequency of each user-controlled chopper, in Hz. Each + value defines the (start, stop, step), which is converted into a list at runtime. + constant_frequencies + The frequency of each chopper in Hz, with those run at a constant frequency having + non-zero values. + source_frequency + The frequency of the neutron source in Hz. + n_frame + Number of frames to calculate time-distance diagram for. + restrictions + defaults + """ default_chopper_frequency: list[int] allowed_chopper_frequencies: list[list[int]] constant_frequencies: list[int] @@ -76,11 +364,48 @@ def defaults(self) -> dict: class FermiChopper(TypedDict): + """ + Data for a Fermi chopper. + + Attributes + ---------- + distance + Distance from moderator to this Fermi chopper in meters (m). + aperture_distance + Distance from aperture (moderator face) to this Fermi chopper in meters (m). + """ distance: float aperture_distance: float class DiskChopper(TypedDict): + """ + Data for a disk chopper. + + Attributes + ---------- + distance + Distance from moderator to this disk chopper in meters (m). + nslot + Number of slots in the chopper. + slot_width + Width of all slots (assumed to be equal) in millimeters (mm). + slot_ang_pos + Angular position of each slot in the chopper, in degrees. Must be monotonically increasing. + If None, equal spacing is assumed. + guide_width + Width of the guide after the chopper in millimeters (mm). + radius + Disk radius in millimeters (mm)? + num_disk + Number of disks making up this disk chopper. + is_phase_independent + Whether this disk is supposed to be phased independently. + default_phase + Default phase for this disk chopper. If an ``int`` is provided, it is assumed to be time in + microseconds (us), and if a ``str`` is provided, it is assumed to be a slot index for the + desired rep to go through. + """ distance: float nslot: int slot_width: float @@ -93,6 +418,22 @@ class DiskChopper(TypedDict): class Sample(TypedDict): + """ + Data for the sample. + + Attributes + ---------- + type + Sample type: 0==flat plate, 1==ellipse, 2==annulus, 3==sphere, 4==solid cylinder. + thickness + Sample thickness in meters (m). + width + Sample width in meters (m). + height + Sample height in meters (m). + gamma + Angle of x-axis to ki in degrees. + """ type: int thickness: float width: float @@ -101,27 +442,106 @@ class Sample(TypedDict): class Detector(TypedDict): + """ + Data for the detector. + + Attributes + ---------- + type + Detector type: 1==Li Davidson scintillator, 2==Helium tube binned together, 3==Helium tube. + phi + Detector scattering angle in degrees. + depth + Detector depth (diameter for tube) in meters (m). + """ type: int phi: float depth: float class Moderator(TypedDict): + """ + Data for the moderator. + + Attributes + ---------- + type + Moderator time profile type: 0==chi squared, 1==Ikeda-Carpenter, 2==modified chi squared, + 3==polynomial. + parameters + Time profile parameters. + scaling_function + The name of the scaling function to use. If None, no scaling will be applied. + scaling_parameters + The parameters to use for the `scaling_function`. Does not have to be provided if + `scaling_function` is None. + measured_wavelength + The wavelength at which the moderator contribution to the resolution was measured + experimentally. If None, interpolation will not be used and instead the moderator + contribution to the resolution will be computed analytically. + measured_width + The experimentally measured moderator contribution to the resolution (width of the Gaussian) + at the `measured_wavelength`. Does not have to be provided if `measured_wavelength` is None. + """ type: int parameters: list[float] scaling_function: None | str - scaling_parameters: list[float] - measured_wavelength: list[float] - measured_width: list[float] + scaling_parameters: NotRequired[list[float]] + measured_wavelength: None | list[float] + measured_width: NotRequired[list[float]] class PyChopModel(InstrumentModel, ABC): + """ + Abstract base class for all PyChop models. + + This class provides the concrete implementation for most of PyChop algorithm, only the chopper + contribution to the resolution is missing. The following methods must be implemented: + + - ``__init__`` which must take the ``model_data`` and ``e_init`` parameters as well as some form + of chopper frequency parameters. It should fit a polynomial to the data obtained from the + `_precompute_resolution` method. It must also use the provided `_validate_e_init` method. + - `polynomial` - this property should be implemented in such a way that it exposes the + polynomial computed in ``__init__``. + - `_get_chopper_width_squared` which calculates the chopper contribution. + + Parameters + ---------- + model_data + The data associated with the model for a given version of a given instrument. + + Attributes + ---------- + input + The input that the ``__call__`` method expects. + output + The output of the ``__call__`` method. + data_class + Reference to the `PyChopModelData` type. + citation + polynomial + """ input = 1 output = 1 data_class = PyChopModelData - def __call__(self, frequencies: Float[np.ndarray, 'frequencies'], *args, **kwargs) -> Float[np.ndarray, 'sigma']: + def __call__(self, frequencies: Float[np.ndarray, 'frequencies'], *args, **kwargs + ) -> Float[np.ndarray, 'sigma']: + """ + Evaluates the model at given energy transfer values (`frequencies`), returning the + corresponding Gaussian widths (sigma). + + Parameters + ---------- + frequencies + Energy transfer in meV. The frequencies at which to return widths. + + Returns + ------- + sigma + The Gaussian widths at `frequencies` as predicted by this model. + """ return self.polynomial(frequencies) @property @@ -174,7 +594,36 @@ def _precompute_resolution(cls, model_data: PyChopModelData, e_init: float, chopper_frequencies: list[int] - ) -> tuple[Float[np.ndarray, 'frequency'], Float[np.ndarray, 'resolution']]: + ) -> tuple[Float[np.ndarray, 'frequency'], + Float[np.ndarray, 'resolution']]: + """ + Calculates the resolution on a coarse grid. + + The grid consists of 40 equally spaced values between 0 meV and `e_init` meV. + + Parameters + ---------- + model_data + The data for a particular version of a particular instrument. + e_init + The initial energy, as selected by the user. + chopper_frequencies + The frequency of each user-controlled chopper. + + Returns + ------- + fake_frequencies + The grid of energy transfer (``frequencies``) on which the resolution was evaluated. + resolution + The resolution at `fake_frequencies` in sigma (Gaussian width). + + Raises + ------ + NoTransmissionError + If the instrument contains a Fermi chopper, and the combination of `e_init` and + `chopper_frequency` would result in the Fermi chopper blocking the neutron beam, + resulting in no signal. + """ fake_frequencies = np.linspace(0, e_init, 40, endpoint=False) vsq_van = cls._precompute_van_var(model_data, e_init, chopper_frequencies, fake_frequencies) e_final = e_init - fake_frequencies @@ -189,6 +638,32 @@ def _precompute_van_var(cls, chopper_frequencies: list[int], fake_frequencies: Float[np.ndarray, 'frequency'], ) -> Float[np.ndarray, 'resolution']: + """ + Calculates the time squared FWHM in s^2 at the sample for all components. + + Parameters + ---------- + model_data + The data for a particular version of a particular instrument. + e_init + The initial energy, as selected by the user. + chopper_frequencies + The frequency of each user-controlled chopper. + fake_frequencies + The coarse grid of energy transfer at which to evaluate the contributions to resolution. + + Returns + ------- + vsq_van + The time squared FWHM in s^2 at the sample. + + Raises + ------ + NoTransmissionError + If the instrument contains a Fermi chopper, and the combination of `e_init` and + `chopper_frequency` would result in the Fermi chopper blocking the neutron beam, + resulting in no signal. + """ tsq_jit = model_data.tjit ** 2 x0, xa, xm = cls._get_distances(model_data.choppers) x1, x2 = model_data.d_chopper_sample, model_data.d_sample_detector @@ -255,6 +730,34 @@ def _precompute_van_var(cls, def _get_moderator_width_squared(cls, moderator_data: Moderator, e_init: float, ): + """ + Calculates the moderator contribution to the resolution squared, in FWHM. + + If the moderator data contains the experimentally measured widths, and the `e_init` lies + within the measured range of wavelengths, the result is obtained via interpolation of the + measured widths. Otherwise, the result is computed analytically. + + Parameters + ---------- + moderator_data + The data for the moderator. + e_init + The initial energy, as selected by the user. + + Returns + ------- + moderator_width_squared + The moderator contribution to the resolution squared, in FWHM. + + Raises + ------ + NotImplementedError + If analytical and the moderator type is not one of the defined values. + + See Also + -------- + _get_moderator_width_analytical : Analytical computation of the moderator contribution. + """ wavelengths = moderator_data['measured_wavelength'] if wavelengths is not None: # TODO: Sort the data in the yaml file and remove sorting below @@ -273,15 +776,43 @@ def _get_moderator_width_squared(cls, return cls._get_moderator_width_analytical(moderator_data['type'], moderator_data['parameters'], moderator_data['scaling_function'], - moderator_data['scaling_parameters'], + moderator_data.get('scaling_parameters', None), e_init) @staticmethod def _get_moderator_width_analytical(imod: int, mod_pars: list[float], scaling_function: str | None, - scaling_parameters: list[float], + scaling_parameters: None | list[float], e_init: float) -> float: + """ + Analytically calculates the moderator contribution to the resolution squared, in FWHM. + + Parameters + ---------- + imod + The moderator type. See `Moderator.type`. + mod_pars + Moderator parameters. See `Moderator.parameters`. + scaling_function + The name of the scaling function to use. If None, no scaling will be applied. Only + applicable for type 2 moderators (modified chi squared). + scaling_parameters + The parameters for the `scaling_function`. Only required when `scaling_function` is + provided. + e_init + The initial energy, as selected by the user. + + Returns + ------- + moderator_width_squared + The moderator contribution to the resolution squared, in FWHM. + + Raises + ------ + NotImplementedError + If the moderator type is not one of the defined values. + """ # TODO: Look into composition if imod == 0: return np.array(mod_pars) * 1e-3 / 1.95 / (437.392 * np.sqrt(e_init)) ** 2 * SIGMA2FWHMSQ @@ -302,7 +833,30 @@ def _get_moderator_width_analytical(imod: int, raise NotImplementedError() @staticmethod - def _get_moderator_width_ikeda_carpenter(s1: float, s2: float, b1: float, b2: float, e_mod: float, e_init: float): + def _get_moderator_width_ikeda_carpenter(s1: float, + s2: float, + b1: float, + b2: float, + e_mod: float, + e_init: float) -> float: + """ + Calculates the moderator time width based on the Ikeda-Carpenter distribution. + + Parameters + ---------- + s1 + s2 + b1 + b2 + e_mod + e_init + The initial energy, as selected by the user. + + Returns + ------- + moderator_width_squared + The moderator contribution to the resolution squared, in FWHM. + """ sig = np.sqrt(s1 ** 2 + s2 ** 2 * 81.8048 / e_init) a = 4.37392e-4 * sig * np.sqrt(e_init) b = b2 if e_init > 130. else b1 @@ -318,7 +872,28 @@ def _get_chopper_width_squared(cls, raise NotImplementedError() @staticmethod - def _get_distances(choppers: dict[str, FermiChopper | DiskChopper]) -> tuple[float, float, float]: + def _get_distances(choppers: dict[str, FermiChopper | DiskChopper] + ) -> tuple[float, float, float]: + """ + Determines various distances in the instrument. + + The choppers in `PyChopModelData` must be present in the order of increasing distance from + the moderator. + + Parameters + ---------- + choppers + Data for all choppers in the instrument. + + Returns + ------- + d_moderator_last_chopper + Distance between the moderator and the last chopper. + d_aperture_last_chopper + Distance between the aperture and the last chopper. + d_moderator_first_chopper + Distance between the moderator and the first chopper. + """ choppers: list[FermiChopper | DiskChopper] = list(choppers.values()) mod_chop = choppers[-1]['distance'] try: @@ -329,9 +904,36 @@ def _get_distances(choppers: dict[str, FermiChopper | DiskChopper]) -> tuple[flo return mod_chop, ap_chop, choppers[0]['distance'] @classmethod - def _get_detector_width_squared(cls, detector_data: Detector, + def _get_detector_width_squared(cls, + detector_data: Detector, fake_frequencies: Float[np.ndarray, 'frequency'], - e_init: float): + e_init: float) -> Float[np.ndarray, 'detector_width_squared']: + """ + Calculates the detector contribution to the resolution squared, in FWHM. + + Parameters + ---------- + detector_data + The data for the detector. + fake_frequencies + The coarse grid of energy transfer at which to evaluate the contribution to resolution. + e_init + The initial energy, as selected by the user. + + Returns + ------- + detector_width_squared + The detector contribution to the resolution squared, in FWHM. + + Raises + ------ + NotImplementedError + If the detector type is 1 (Lithium detector). + + See Also + -------- + _get_he_detector_width_squared : Computes the detector contribution for Helium detectors + """ wfs = np.sqrt(E2K * (e_init - fake_frequencies)) t2rad = 0.063 atms = 10. @@ -350,7 +952,31 @@ def _get_detector_width_squared(cls, detector_data: Detector, return cls._get_he_detector_width_squared(alf) * reff ** 2 * SIGMA2FWHMSQ @staticmethod - def _get_he_detector_width_squared(alf: Float[np.ndarray, 'ALF']) -> Float[np.ndarray, 'ALF']: + def _get_he_detector_width_squared(alf: Float[np.ndarray, 'ALF']) -> Float[np.ndarray, 'VX']: + """ + Calculates the helium detector contribution to the resolution squared, in FWHM. + + T.G.Perring 6/4/90 + + This method approximates the contribution using Chebyshev polynomial expansions over the + ranges 0 <= `alf` <= 9 and 10 <= `alf`. For the intervening interval, 9 <= `alf` <= 10, + a linear approximation of the two approximations is taken. + + The original Chebyshev coefficients were obtained using the ``CHEBFT`` routine from + "numerical recipes", but the numpy equivalent for the ``CHEBEV``, + `numpy.polynomial.chebyshev.Chebyshev`, uses a slightly different formalism, so the first + coefficient is halved compared to the source implementation of this method. + + Parameters + ---------- + alf + ALF = radius in m.f.p. + + Returns + ------- + vx + Variance of depth absorption. + """ out = np.zeros(np.shape(alf)) coefficients_low = [0.613452291529095, -0.3621914072547197, 6.0117947617747081e-02, 1.8037337764424607e-02, -1.4439005957980123e-02, 3.8147446724517908e-03, 1.3679160269450818e-05, @@ -390,6 +1016,19 @@ def _get_he_detector_width_squared(alf: Float[np.ndarray, 'ALF']) -> Float[np.nd @staticmethod def _get_sample_width_squared(sample_data: Sample) -> float: + """ + Calculates the sample contribution to the resolution squared, in FWHM. + + Parameters + ---------- + sample_data + The data for the sample. + + Returns + ------- + sample_width_squared + The sample contribution to the resolution squared, in FWHM. + """ scaling_factor = 0.125 if sample_data['type'] == 2 else 1 / 12 return sample_data['width'] ** 2 * scaling_factor * SIGMA2FWHMSQ @@ -418,6 +1057,51 @@ def _get_sample_width_squared(sample_data: Sample) -> float: class PyChopModelFermi(PyChopModel): + """ + PyChop model of 2D direct-geometry INS instruments that use a Fermi chopper. + + Models the resolution as a function of energy transfer (frequencies) only, with the output model + being a Gaussian. This is done by computing the contribution of each part of the instrument to + the resolution function. However, this model calculates the resolution on a coarse grid, and + then fits a polynomial to the results - the resolution at the user-provided ``frequencies`` is + obtained by evaluating the polynomial. + + Parameters + ---------- + model_data + The data associated with the model for a given version of a given instrument. + e_init + The initial energy in meV used in the INS experiment. If not provided, the default value for + the particular version of an instrument will be used + (see `PyChopModelDataFermi.defaults`). Please note that the `e_init` value must be + within the range allowed for the instrument (see `PyChopModelDataFermi.restrictions`). + chopper_frequency + The frequency of the Fermi chopper in Hz used in the INS experiment. If not provided, the + default value for the particular version of an instrument will be used + (see `PyChopModelDataFermi.defaults`). Please note that the `chopper_frequency` value must + be within the range allowed for the instrument (see `PyChopModelDataFermi.restrictions`). + fitting_order + The order of the polynomial to use when fitting to the coarse grid. + + Raises + ------ + InvalidInputError + If either the `e_init` or the `chopper_frequency` is not allowed. + NoTransmissionError + If the combination of `e_init` and `chopper_frequency` would result in the Fermi chopper + blocking the neutron beam, resulting in no signal. + + Attributes + ---------- + input + The input that the ``__call__`` method expects. + output + The output of the ``__call__`` method. + data_class + Reference to the `PyChopModelDataFermi` type. + citation + polynomial + """ data_class = PyChopModelDataFermi def __init__(self, @@ -431,8 +1115,8 @@ def __init__(self, if chopper_frequency is None: chopper_frequency = model_data.default_chopper_frequency elif chopper_frequency not in range(*model_data.allowed_chopper_frequencies): - raise InvalidInputError(f'The provided chopper frequency ({chopper_frequency}) is not allowed; only the ' - f'following frequencies are possible: ' + raise InvalidInputError(f'The provided chopper frequency ({chopper_frequency}) is not ' + f'allowed; only the following frequencies are possible: ' f'{list(range(*model_data.allowed_chopper_frequencies))}') e_init = self._validate_e_init(e_init, model_data) @@ -441,7 +1125,7 @@ def __init__(self, self._polynomial = Polynomial.fit(fake_frequencies, resolution, fitting_order) @property - def polynomial(self): + def polynomial(self) -> Polynomial: return self._polynomial @classmethod @@ -449,13 +1133,39 @@ def _get_chopper_width_squared(cls, model_data: PyChopModelDataFermi, e_init: float, chopper_frequency: list[int]) -> tuple[float, None]: + """ + Calculates the Fermi chopper contribution to the resolution squared, in FWHM. + + Parameters + ---------- + model_data + The data for a particular version of a particular instrument. + e_init + The initial energy, as selected by the user. + chopper_frequency + The frequency of the Fermi chopper, as selected by the user. + + Returns + ------- + chopper_width_squared + The chopper contribution to the resolution squared, in FWHM. + none + For compatibility with `PyChopModelNonFermi`. + + Raises + ------ + NoTransmissionError + If the combination of `e_init` and `chopper_frequency` would result in the Fermi chopper + blocking the neutron beam, resulting in no signal. + """ frequency = 2 * np.pi * chopper_frequency[0] gamm = (2.00 * model_data.radius ** 2 / model_data.pslit) * \ abs(1.00 / model_data.rho - 2.00 * frequency / (437.392 * np.sqrt(e_init))) if gamm >= 4.: - raise NoTransmissionError(f'The combination of e_init={e_init} and chopper_frequency={chopper_frequency} ' - f'is not valid because the Fermi chopper has no transmission at these values.') + raise NoTransmissionError(f'The combination of e_init={e_init} and chopper_frequency=' + f'{chopper_frequency} is not valid because the Fermi chopper ' + f'has no transmission at these values.') elif gamm <= 1.: gsqr = (1.00 - (gamm ** 2) ** 2 / 10.00) / (1.00 - (gamm ** 2) / 6.00) else: @@ -525,19 +1235,61 @@ def _validate_chopper_frequency(chopper_frequencies: list[int | None], return chopper_frequencies @staticmethod - def get_long_frequency(frequency: list[int], + def get_long_frequency(frequencies: list[int], model_data: PyChopModelDataNonFermi ) -> Float[np.ndarray, 'chopper_frequencies']: - frequency += model_data.default_chopper_frequency[len(frequency):] + """ + Calculates the frequency of each chopper making up this instrument. + + Different instruments are set up differently; usually, at least one of the choppers is + controllable by the user, and others are run at constant frequencies. However, in some + instruments, some of the choppers are run at a particular fraction of another chopper's + frequency (see `PyChopModelDataNonFermi.frequency_matrix`). This function takes everything + into account and computes the frequency of each chopper. + + Parameters + ---------- + frequencies + The frequency of each user-controllable chopper (in Hz), in the same order as in the + ``__init__``. + model_data + The data for a particular INS instrument. + + Returns + ------- + all_frequencies + The frequency of each chopper, in the order of increasing distance from the moderator. + """ + frequencies += model_data.default_chopper_frequency[len(frequencies):] frequency_matrix = np.array(model_data.frequency_matrix) - return np.dot(frequency_matrix, frequency) + model_data.constant_frequencies + return np.dot(frequency_matrix, frequencies) + model_data.constant_frequencies @classmethod def _get_chop_times(cls, model_data: PyChopModelDataNonFermi, e_init: float, chopper_frequency: list[int]) -> list[list[Float[np.ndarray, 'times']]]: + """ + Calculates the chop times of the first and last choppers. + + This information can be used to compute the chopper contribution to the resolution. If the + instrument contains only one chopper, a fake chopper is prepended. + + Parameters + ---------- + model_data + The data for a particular INS instrument. + e_init + The initial energy, as selected by the user. + chopper_frequency + The frequency of all user-controlled choppers. + + Returns + ------- + chop_times + The chop times of the first and last chopper. + """ frequencies = cls.get_long_frequency(chopper_frequency, model_data) choppers = model_data.choppers @@ -632,6 +1384,25 @@ def _get_chopper_width_squared(cls, model_data: PyChopModelDataNonFermi, e_init: float, chopper_frequency: list[int]) -> tuple[float, float]: + """ + Calculates the chopper contribution to the resolution squared, in FWHM. + + Parameters + ---------- + model_data + The data for a particular INS instrument. + e_init + The initial energy, as selected by the user. + chopper_frequency + The frequency of all user-controlled choppers. + + Returns + ------- + last_chopper_width_squared + The last chopper's contribution to the resolution squared, in FWHM. + first_chopper_width_squared + The first chopper's contribution to the resolution squared, in FWHM. + """ chop_times = cls._get_chop_times(model_data, e_init, chopper_frequency) wd0 = (chop_times[-1][0][1] - chop_times[-1][0][0]) * 0.5e-6 @@ -652,16 +1423,37 @@ class PyChopModelCNCS(PyChopModelNonFermi): model_data The data for the PyChopModel of the CNCS instrument. e_init - The incident energy used in the INS experiment. + The incident energy used in the INS experiment. If not provided, the default value for + the particular version of the CNCS instrument will be used + (see `PyChopModelDataNonFermi.defaults`). Please note that the `e_init` value must be + within the range allowed for the instrument (see `PyChopModelDataNonFermi.restrictions`). resolution_disk_frequency - The frequency of the resolution disk chopper (chopper 4) + The frequency of the resolution disk chopper (chopper 4). If not provided, the + default value for the particular version of the CNCS instrument will be used (see + `PyChopModelDataNonFermi.defaults`). Please note that the `resolution_disk_frequency` value must + be within the range allowed for the instrument (see `PyChopModelDataNonFermi.restrictions`). fermi_frequency - The frequency of the Fermi chopper (chopper 1) + The frequency of the Fermi chopper (chopper 1). If not provided, the + default value for the particular version of the CNCS instrument will be used + (see `PyChopModelDataNonFermi.defaults`). Please note that the `fermi_frequency` value must be + within the range allowed for the instrument (see `PyChopModelDataNonFermi.restrictions`). fitting_order The order of the polynomial used for fitting against the resolution. + Raises + ------ + InvalidInputError + If any of `e_init`, `resolution_disk_frequency`, or `fermi_frequency` is not allowed. + Attributes ---------- + input + The input that the ``__call__`` method expects. + output + The output of the ``__call__`` method. + data_class + Reference to the `PyChopModelDataNonFermi` type. + citation polynomial """ def __init__(self, @@ -702,19 +1494,41 @@ class PyChopModelLET(PyChopModelNonFermi): Parameters ---------- model_data - The data for the PyChopModel of the CNCS instrument. + The data for the PyChopModel of the LET instrument. e_init - The incident energy used in the INS experiment. + The incident energy used in the INS experiment. If not provided, the default value for + the particular version of the LET instrument will be used + (see `PyChopModelDataNonFermi.defaults`). Please note that the `e_init` value must be + within the range allowed for the instrument (see `PyChopModelDataNonFermi.restrictions`). resolution_frequency The frequency of the resolution chopper (i.e. the second resolution disk chopper, or chopper - 5). + 5). If not provided, the default value for the particular version of the LET instrument will + be used (see `PyChopModelDataNonFermi.defaults`). Please note that the + `resolution_frequency` value must be within the range allowed for the instrument + (see `PyChopModelDataNonFermi.restrictions`). pulse_remover_frequency - The frequency of the pulse remover disk chopper (chopper 3). + The frequency of the pulse remover disk chopper (chopper 3). If not provided, the + default value for the particular version of the LET instrument will be used (see + `PyChopModelDataNonFermi.defaults`). Please note that the `pulse_remover_frequency` value + must be within the range allowed for the instrument (see + `PyChopModelDataNonFermi.restrictions`). fitting_order The order of the polynomial used for fitting against the resolution. + Raises + ------ + InvalidInputError + If any of `e_init`, `resolution_frequency`, or `pulse_remover_frequency` is not allowed. + Attributes ---------- + input + The input that the ``__call__`` method expects. + output + The output of the ``__call__`` method. + data_class + Reference to the `PyChopModelDataNonFermi` type. + citation polynomial """ def __init__(self, @@ -739,15 +1553,29 @@ def polynomial(self): return self._polynomial -def soft_hat(x, p): +def soft_hat(x: float, p: list[float]): """ - ! Soft hat function, from Herbert subroutine library. - ! For rescaling t-mod at low energy to account for broader moderator term + Soft hat function, from Herbert subroutine library. + + Used for some versions of some instruments for rescaling t-mod at low energy to account for + broader moderator term. + + Parameters + ---------- + x + The inital energy in meV, as provided by the user. + p + A list of parameters to use for the scaling. + + Returns + ------- + y + The scaling factor """ x = np.array(x) sig2fwhh = np.sqrt(8 * np.log(2)) height, grad, x1, x2 = tuple(p[:4]) - sig1, sig2 = tuple(np.abs(p[4:6] / sig2fwhh)) + sig1, sig2 = abs(p[4] / sig2fwhh), abs(p[5] / sig2fwhh) # linearly interpolate sig for x1