# 27. Product model evaluator

## `sasmodels.product`

### Product model

The product model multiplies the structure factor by the form factor, modulated by the effective radius of the form. The resulting model has a attributes of both the model description (with parameters, etc.) and the module evaluator (with call, release, etc.).

To use it, first load form factor P and structure factor S, then create make_product_info(P, S).

The P@S models is somewhat complicated because there are many special parameters that need to be handled in particular ways. Much of the code is used to figure out what special parameters we have, where to find them in the P@S model inputs and how to distribute them to the underlying P and S model calculators.

The parameter packet received by the P@S is a `details.CallDetails` structure along with a data vector. The CallDetails structure indicates which parameters are polydisperse, the length of the distribution, and where to find it in the data vector. The distributions are ordered from longest to shortest, with length 1 distributions filling out the distribution set. That way the kernel calculator doesn’t have to check if it needs another nesting level since it is always there. The data vector consists of a list of target values for the parameters, followed by a concatenation of the distribution values, and then followed by a concatenation of the distribution weights. Given the combined details and data for P@S, we must decompose them in to details for P and details for S separately, which unfortunately requires intimate knowledge of the data structures and tricky code.

The special parameters are:

- scale and background:

    First two parameters of the value list in each of P, S and P@S. When decomposing P@S parameters, ignore scale and background, instead using 1 and 0 for the first two slots of both P and S. After calling P and S individually, the results are combined as `volfraction*scale*P*S + background`. The scale and background do not show up in the polydispersity structure so they are easy to handle.

- volfraction:

    Always the first parameter of S, but it may also be in P. If it is in P, then P.volfraction is used in the combined P@S list, and S.volfraction is elided, otherwise S.volfraction is used. If we are using volfraction from P we can treat it like all the other P parameters when calling P, but when calling S we need to insert the P.volfraction into data vector for S and assign a slot of length 1 in the distribution. Because we are using the original layout of the distribution vectors from P@S, but copying it into private data vectors for S and P, we are free to “borrow” a P slots to store the missing S.volfraction distribution. We use the P.volfraction slot itself but any slot will work.

    For hollow shapes, volfraction represents the volume fraction of material but S needs the volume fraction enclosed by the shape. The answer is to scale the user specified volume fraction by the form:shell ratio computed from the average form volume and average shell volume returned from P. Use the original volfraction divided by shell_volume to compute the number density, and scale P@S by that to get absolute scaling on the final I(q). The scale for P@S should therefore usually be one.

- radius_effective:

    Always the second parameter of S and always part of P@S, but never in P. The value may be calculated using P.radius_effective() or it may be set to the radius_effective value in P@S, depending on radius_effective_mode. If part of S, the value may be polydisperse. If calculated by P, then it will be the weighted average of effective radii computed for the polydisperse shape parameters.

- structure_factor_mode

    If P@S supports beta approximation (i.e., if it has the Fq function that returns <FF*> and <F><F*>), then structure_factor_mode will be added to the P@S parameters right after the S parameters. This mode may be 0 for the monodisperse approximation or 1 for the beta approximation. We will add more values here as we implemented more complicated operations, but for now P and S must be computed separately. If beta, then we return I = scale volfrac/volume ( <FF> + <F>^2 (S-1)) + background. If not beta then return I = scale/volume P S + background . In both cases, return the appropriate immediate values.

- radius_effective_mode

    If P defines the radius_effective function (and therefore P.info.radius_effective_modes is a list of effective radius modes), then radius_effective_mode will be the final parameter in P@S. Mode will be zero if radius_effective is defined by the user using the S parameter; any other value and the radius_effective parameter will be filled in from the value computed in P. In the latter case, the polydispersity information for S.radius_effective will need to be suppressed, with pd length set to 1, the first value set to the effective radius and the first weight set to 1. Do this after composing the S data vector so the inputs are left untouched.

- regular parameters

    The regular P parameters form a block of length P.info.npars at the start of the data vector (after scale and background). These will be followed by S.effective_radius, and S.volfraction (if P.volfraction is absent), and then the regular S parameters. The P and S blocks can be copied as a group into the respective P and S data vectors. We can copy the distribution value and weight vectors untouched to both the P and S data vectors since they are referenced by offset and length. We can update the radius_effective slots in the P data vector with P.radius_effective() if needed.

- magnetic parameters

    For each P parameter that is an SLD there will be a set of three magnetic parameters tacked on to P@S after the regular P and S and after the special structure_factor_mode and radius_effective_mode. These can be copied as a group after the regular P parameters. There won’t be any magnetic S parameters.

class sasmodels.product.ProductKernel(model_info: ModelInfo, p_kernel: Kernel, s_kernel: Kernel, q: tuple[ndarray])

  Bases: `Kernel`

  Instantiated kernel for product model.

  Fq(call_details: CallDetails, values: np.ndarray, cutoff: np.ndarray, magnetic: float, radius_effective_mode: bool = 0) → np.ndarray

    Returns <F(q)>, <F(q)^2>, effective radius, shell volume and form:shell volume ratio. The <F(q)> term may be None if the form factor does not support direct computation of \(F(q)\)

    \(P(q) = <F^2(q)>/<V>\) is used for structure factor calculations,

    \[I(q) = \text{scale} \cdot P(q) \cdot S(q) + \text{background}\]

    For the beta approximation, this becomes

    \[I(q) = \text{scale} P (1 + <F>^2/<F^2> (S - 1)) + \text{background} = \text{scale}/<V> (<F^2> + <F>^2 (S - 1)) + \text{background}\]

    \(<F(q)>\) and \(<F^2(q)>\) are averaged by polydispersity in shape and orientation, with each configuration \(x_k\) having form factor \(F(q, x_k)\), weight \(w_k\) and volume \(V_k\). The result is:

    \[P(q)=\frac{\sum w_k F^2(q, x_k) / \sum w_k}{\sum w_k V_k / \sum w_k}\]

    The form factor itself is scaled by volume and contrast to compute the total scattering. This is then squared, and the volume weighted F^2 is then normalized by volume F. For a given density, the number of scattering centers is assumed to scale linearly with volume. Later scaling the resulting \(P(q)\) by the volume fraction of particles gives the total scattering on an absolute scale. Most models incorporate the volume fraction into the overall scale parameter. An exception is vesicle, which includes the volume fraction parameter in the model itself, scaling \(F\) by \(\surd V_f\) so that the math for the beta approximation works out.

    By scaling \(P(q)\) by total weight \(\sum w_k\), there is no need to make sure that the polydisperisity distributions normalize to one. In particular, any distibution values \(x_k\) outside the valid domain of \(F\) will not be included, and the distribution will be implicitly truncated. This is controlled by the parameter limits defined in the model (which truncate the distribution before calling the kernel) as well as any region excluded using the INVALID macro defined within the model itself.

    The volume used in the polydispersity calculation is the form volume for solid objects or the shell volume for hollow objects. Shell volume should be used within \(F\) so that the normalizing scale represents the volume fraction of the shell rather than the entire form. This corresponds to the volume fraction of shell-forming material added to the solvent.

    The calculation of \(S\) requires the effective radius and the volume fraction of the particles. The model can have several different ways to compute effective radius, with the radius_effective_mode parameter used to select amongst them. The volume fraction of particles should be determined from the total volume fraction of the form, not just the shell volume fraction. This makes a difference for hollow shapes, which need to scale the volume fraction by the returned volume ratio when computing \(S\). For solid objects, the shell volume is set to the form volume so this scale factor evaluates to one and so can be used for both hollow and solid shapes.

  Iq(call_details: CallDetails, values: ndarray, cutoff: float, magnetic: bool) → ndarray

    Returns I(q) from the polydisperse average scattering.

    \[I(q) = \text{scale} \cdot P(q) + \text{background}\]

    With the correct choice of model and contrast, setting scale to the volume fraction \(V_f\) of particles should match the measured absolute scattering. Some models (e.g., vesicle) have volume fraction built into the model, and do not need an additional scale.

  dim: str = None

    Kernel dimension, either “1d” or “2d”.

  dtype: np.dtype = None

    Numerical precision for the computation.

  info: ModelInfo = None

    Model info.

  q_input: Any = None

    Q values at which the kernel is to be evaluated.

  release() → None

    Free resources associated with the kernel.

  result: np.ndarray = None

    Place to hold result of _call_kernel() for subclass.

class sasmodels.product.ProductModel(model_info: ModelInfo, P: KernelModel, S: KernelModel)

  Bases: `KernelModel`

  Model definition for product model.

  P: KernelModel = None

    Form factor modelling individual particles.

  S: KernelModel = None

    Structure factor modelling interaction between particles.

  dtype: np.dtype = None

    Model precision. This is not really relevant, since it is the individual P and S models that control the effective dtype, converting the q-vectors to the correct type when the kernels for each are created. Ideally this should be set to the more precise type to avoid loss of precision, but precision in q is not critical (single is good enough for our purposes), so it just uses the precision of the form factor.

  info: ModelInfo = None

    Combined info plock for the product model

  make_kernel(q_vectors: list[ndarray]) → Kernel

    Instantiate a kernel for evaluating the model at q_vectors.

  release() → None

    Free resources associated with the model.

sasmodels.product.make_extra_pars(p_info: ModelInfo) → list[Parameter]

  Create parameters for structure factor and effective radius modes.

sasmodels.product.make_product_info(p_info: ModelInfo, s_info: ModelInfo) → ModelInfo

  Create info block for product model.