sasdata.model_requirements module

class sasdata.model_requirements.ComposeRequirements(fst, snd)

Bases: ModellingRequirements

Composition of two models

first: ModellingRequirements
postprocess_iq(data: ndarray, full_data: SasData) ndarray

Perform both transformations in order

preprocess_q(data: ndarray, full_data: SasData) ndarray

Perform both transformations in order

second: ModellingRequirements
class sasdata.model_requirements.ModellingRequirements

Bases: ABC

Requirements that need to be passed to any modelling step

compose(other: Self) Self
dimensionality: int
operation: Operation
abstractmethod postprocess_iq(data: ndarray, full_data: SasData) ndarray

Transform the I(Q) values after running the model

abstractmethod preprocess_q(data: ndarray, full_data: SasData) ndarray

Transform the Q values before processing in the model

class sasdata.model_requirements.NullModel

Bases: ModellingRequirements

A model that does nothing

compose(other: ModellingRequirements) ModellingRequirements
postprocess_iq(data: ndarray, _full_data: SasData) ndarray

Do nothing

preprocess_q(data: Quantity[ndarray], _full_data: SasData) ndarray

Do nothing

class sasdata.model_requirements.PinholeModel(q_width: ndarray, nsigma: (<class 'float'>, <class 'float'>)=(2.5, 3.0))

Bases: ModellingRequirements

Perform a pin hole smearing

postprocess_iq(data: ndarray, full_data: SasData) ndarray

Perform smearing transform

preprocess_q(q: ndarray, full_data: SasData) ndarray

Perform smearing transform

class sasdata.model_requirements.SesansModel

Bases: ModellingRequirements

Perform Hankel transform for SESANS

postprocess_iq(data: ndarray, full_data: SasData) ndarray

Apply the SESANS transform to the computed I(q)

preprocess_q(spin_echo_length: ndarray, full_data: SasData) ndarray

Calculate the q values needed to perform the Hankel transform

Note: this is undefined for the case when spin_echo_lengths contains exactly one element and that values is zero.

class sasdata.model_requirements.SlitModel(q_length: ndarray, q_width: ndarray, nsigma: (<class 'float'>, <class 'float'>)=(2.5, 3.0))

Bases: ModellingRequirements

Perform a slit smearing

postprocess_iq(data: ndarray, full_data: SasData) ndarray

Perform smearing transform

preprocess_q(q: ndarray, full_data: SasData) ndarray

Perform smearing transform

sasdata.model_requirements.bin_edges(x)

Determine bin edges from bin centers, assuming that edges are centered between the bins.

Note: this uses the arithmetic mean, which may not be appropriate for log-scaled data.

sasdata.model_requirements.compose(second: ModellingRequirements, first: ModellingRequirements) ModellingRequirements
sasdata.model_requirements.compose(second: NullModel, first: ModellingRequirements) ModellingRequirements
sasdata.model_requirements.compose(second: SesansModel, first: ModellingRequirements) ModellingRequirements

Compose to models together

This function uses a reverse order so that it can perform dispatch on the second term, since the classes already had a chance to dispatch on the first parameter

sasdata.model_requirements.geometric_extrapolation(q, q_min, q_max, points_per_decade=None)

Extrapolate q to [q_min, q_max] using geometric steps, with the average geometric step size in q as the step size.

if q_min is zero or less then q[0]/10 is used instead.

points_per_decade sets the ratio between consecutive steps such that there will be $n$ points used for every factor of 10 increase in q.

If points_per_decade is not given, it will be estimated as follows. Starting at $q_1$ and stepping geometrically by $Delta q$ to $q_n$ in $n$ points gives a geometric average of:

\[\log \Delta q = (\log q_n - \log q_1) / (n - 1)\]

From this we can compute the number of steps required to extend $q$ from $q_n$ to $q_text{max}$ by $Delta q$ as:

\[n_\text{extend} = (\log q_\text{max} - \log q_n) / \log \Delta q\]

Substituting:

\[n_\text{extend} = (n-1) (\log q_\text{max} - \log q_n) / (\log q_n - \log q_1)\]
sasdata.model_requirements.guess_requirements(data: SasData) ModellingRequirements

Use names of axes and units to guess what kind of processing needs to be done

sasdata.model_requirements.linear_extrapolation(q, q_min, q_max)

Extrapolate q out to [q_min, q_max] using the step size in q as a guide. Extrapolation below uses about the same size as the first interval. Extrapolation above uses about the same size as the final interval.

Note that extrapolated values may be negative.

sasdata.model_requirements.pinhole_resolution(q_calc: ndarray, q: ndarray, q_width: ndarray, nsigma=(2.5, 3.0)) ndarray

Compute the convolution matrix W for pinhole resolution 1-D data.

Each row W[i] determines the normalized weight that the corresponding points q_calc contribute to the resolution smeared point q[i]. Given W, the resolution smearing can be computed using dot(W,q).

Note that resolution is limited to $pm 2.5 sigma$.[1] The true resolution function is a broadened triangle, and does not extend over the entire range $(-infty, +infty)$. It is important to impose this limitation since some models fall so steeply that the weighted value in gaussian tails would otherwise dominate the integral.

q_calc must be increasing. q_width must be greater than zero.

[1] Barker, J. G., and J. S. Pedersen. 1995. Instrumental Smearing Effects in Radially Symmetric Small-Angle Neutron Scattering by Numerical and Analytical Methods. Journal of Applied Crystallography 28 (2): 105–14. https://doi.org/10.1107/S0021889894010095.

sasdata.model_requirements.slit_extend_q(q, width, length)

Given q, width and length, find a set of sampling points q_calc so that each point I(q) has sufficient support from the underlying function.

sasdata.model_requirements.slit_resolution(q_calc, q, width, length, n_length=30)

Build a weight matrix to compute I_s(q) from I(q_calc), given $q_perp$ = width (in the high-resolution axis) and $q_parallel$ = length (in the low resolution axis). n_length is the number of steps to use in the integration over $q_parallel$ when both $q_perp$ and $q_parallel$ are non-zero.

Each $q$ can have an independent width and length value even though current instruments use the same slit setting for all measured points.

If slit length is large relative to width, use:

\[I_s(q_i) = \frac{1}{\Delta q_\perp} \int_0^{\Delta q_\perp} I\left(\sqrt{q_i^2 + q_\perp^2}\right) \,dq_\perp\]

If slit width is large relative to length, use:

\[I_s(q_i) = \frac{1}{2 \Delta q_\parallel} \int_{-\Delta q_\parallel}^{\Delta q_\parallel} I\left(|q_i + q_\parallel|\right) \,dq_\parallel\]

For a mixture of slit width and length use:

\[I_s(q_i) = \frac{1}{2 \Delta q_\parallel \Delta q_\perp} \int_{-\Delta q_\parallel}^{\Delta q_\parallel} \int_0^{\Delta q_\perp} I\left(\sqrt{(q_i + q_\parallel)^2 + q_\perp^2}\right) \,dq_\perp dq_\parallel\]

Definition

We are using the mid-point integration rule to assign weights to each element of a weight matrix $W$ so that

\[I_s(q) = W\,I(q_\text{calc})\]

If q_calc is at the mid-point, we can infer the bin edges from the pairwise averages of q_calc, adding the missing edges before q_calc[0] and after q_calc[-1].

For $q_parallel = 0$, the smeared value can be computed numerically using the $u$ substitution

\[u_j = \sqrt{q_j^2 - q^2}\]

This gives

\[I_s(q) \approx \sum_j I(u_j) \Delta u_j\]

where $I(u_j)$ is the value at the mid-point, and $Delta u_j$ is the difference between consecutive edges which have been first converted to $u$. Only $u_j in [0, Delta q_perp]$ are used, which corresponds to $q_j in left[q, sqrt{q^2 + Delta q_perp}right]$, so

\[W_{ij} = \frac{1}{\Delta q_\perp} \Delta u_j = \frac{1}{\Delta q_\perp} \left( \sqrt{q_{j+1}^2 - q_i^2} - \sqrt{q_j^2 - q_i^2} \right) \ \text{if}\ q_j \in \left[q_i, \sqrt{q_i^2 + q_\perp^2}\right]\]

where $I_s(q_i)$ is the theory function being computed and $q_j$ are the mid-points between the calculated values in q_calc. We tweak the edges of the initial and final intervals so that they lie on integration limits.

(To be precise, the transformed midpoint $u(q_j)$ is not necessarily the midpoint of the edges $u((q_{j-1}+q_j)/2)$ and $u((q_j + q_{j+1})/2)$, but it is at least in the interval, so the approximation is going to be a little better than the left or right Riemann sum, and should be good enough for our purposes.)

For $q_perp = 0$, the $u$ substitution is simpler:

\[u_j = \left|q_j - q\right|\]

so

\[W_{ij} = \frac{1}{2 \Delta q_\parallel} \Delta u_j = \frac{1}{2 \Delta q_\parallel} (q_{j+1} - q_j) \ \text{if}\ q_j \in \left[q-\Delta q_\parallel, q+\Delta q_\parallel\right]\]

However, we need to support cases were $u_j < 0$, which means using $2 (q_{j+1} - q_j)$ when $q_j in left[0, q_parallel-q_iright]$. This is not an issue for $q_i > q_parallel$.

For both $q_perp > 0$ and $q_parallel > 0$ we perform a 2 dimensional integration with

\[u_{jk} = \sqrt{q_j^2 - (q + (k\Delta q_\parallel/L))^2} \ \text{for}\ k = -L \ldots L\]

for $L$ = n_length. This gives

\[W_{ij} = \frac{1}{2 \Delta q_\perp q_\parallel} \sum_{k=-L}^L \Delta u_{jk} \left(\frac{\Delta q_\parallel}{2 L + 1}\right)\]