1 | """ |
---|
2 | Execution kernel interface |
---|
3 | ========================== |
---|
4 | |
---|
5 | :class:`KernelModel` defines the interface to all kernel models. |
---|
6 | In particular, each model should provide a :meth:`KernelModel.make_kernel` |
---|
7 | call which returns an executable kernel, :class:`Kernel`, that operates |
---|
8 | on the given set of *q_vector* inputs. On completion of the computation, |
---|
9 | the kernel should be released, which also releases the inputs. |
---|
10 | """ |
---|
11 | |
---|
12 | from __future__ import division, print_function |
---|
13 | |
---|
14 | # pylint: disable=unused-import |
---|
15 | try: |
---|
16 | from typing import List |
---|
17 | except ImportError: |
---|
18 | pass |
---|
19 | else: |
---|
20 | import numpy as np |
---|
21 | from .details import CallDetails |
---|
22 | from .modelinfo import ModelInfo |
---|
23 | # pylint: enable=unused-import |
---|
24 | |
---|
25 | class KernelModel(object): |
---|
26 | info = None # type: ModelInfo |
---|
27 | dtype = None # type: np.dtype |
---|
28 | def make_kernel(self, q_vectors): |
---|
29 | # type: (List[np.ndarray]) -> "Kernel" |
---|
30 | raise NotImplementedError("need to implement make_kernel") |
---|
31 | |
---|
32 | def release(self): |
---|
33 | # type: () -> None |
---|
34 | pass |
---|
35 | |
---|
36 | class Kernel(object): |
---|
37 | #: kernel dimension, either "1d" or "2d" |
---|
38 | dim = None # type: str |
---|
39 | info = None # type: ModelInfo |
---|
40 | results = None # type: List[np.ndarray] |
---|
41 | dtype = None # type: np.dtype |
---|
42 | |
---|
43 | def Iq(self, call_details, values, cutoff, magnetic): |
---|
44 | # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray |
---|
45 | r""" |
---|
46 | Returns I(q) from the polydisperse average scattering. |
---|
47 | |
---|
48 | .. math:: |
---|
49 | |
---|
50 | I(q) = \text{scale} \cdot P(q) + \text{background} |
---|
51 | |
---|
52 | With the correct choice of model and contrast, setting *scale* to |
---|
53 | the volume fraction $V_f$ of particles should match the measured |
---|
54 | absolute scattering. Some models (e.g., vesicle) have volume fraction |
---|
55 | built into the model, and do not need an additional scale. |
---|
56 | """ |
---|
57 | _, F2, _, shell_volume, _ = self.Fq(call_details, values, cutoff, magnetic, |
---|
58 | effective_radius_type=0) |
---|
59 | combined_scale = values[0]/shell_volume |
---|
60 | background = values[1] |
---|
61 | return combined_scale*F2 + background |
---|
62 | __call__ = Iq |
---|
63 | |
---|
64 | def Fq(self, call_details, values, cutoff, magnetic, effective_radius_type=0): |
---|
65 | # type: (CallDetails, np.ndarray, np.ndarray, float, bool, int) -> np.ndarray |
---|
66 | r""" |
---|
67 | Returns <F(q)>, <F(q)^2>, effective radius, shell volume and |
---|
68 | form:shell volume ratio. The <F(q)> term may be None if the |
---|
69 | form factor does not support direct computation of $F(q)$ |
---|
70 | |
---|
71 | $P(q) = <F^2(q)>/<V>$ is used for structure factor calculations, |
---|
72 | |
---|
73 | .. math:: |
---|
74 | |
---|
75 | I(q) = \text{scale} \cdot P(q) \cdot S(q) + \text{background} |
---|
76 | |
---|
77 | For the beta approximation, this becomes |
---|
78 | |
---|
79 | .. math:: |
---|
80 | |
---|
81 | I(q) = \text{scale} * P (1 + <F>^2/<F^2> (S - 1)) + \text{background} |
---|
82 | = \text{scale}/<V> (<F^2> + <F>^2 (S - 1)) + \text{background} |
---|
83 | |
---|
84 | $<F(q)>$ and $<F^2(q)>$ are averaged by polydispersity in shape |
---|
85 | and orientation, with each configuration $x_k$ having form factor |
---|
86 | $F(q, x_k)$, weight $w_k$ and volume $V_k$. The result is: |
---|
87 | |
---|
88 | .. math:: |
---|
89 | |
---|
90 | P(q) = \frac{\sum w_k F^2(q, x_k) / \sum w_k}{\sum w_k V_k / \sum w_k} |
---|
91 | |
---|
92 | The form factor itself is scaled by volume and contrast to compute the |
---|
93 | total scattering. This is then squared, and the volume weighted |
---|
94 | F^2 is then normalized by volume F. For a given density, the number |
---|
95 | of scattering centers is assumed to scale linearly with volume. Later |
---|
96 | scaling the resulting $P(q)$ by the volume fraction of particles |
---|
97 | gives the total scattering on an absolute scale. Most models |
---|
98 | incorporate the volume fraction into the overall scale parameter. An |
---|
99 | exception is vesicle, which includes the volume fraction parameter in |
---|
100 | the model itself, scaling $F$ by $\surd V_f$ so that the math for |
---|
101 | the beta approximation works out. |
---|
102 | |
---|
103 | By scaling $P(q)$ by total weight $\sum w_k$, there is no need to make |
---|
104 | sure that the polydisperisity distributions normalize to one. In |
---|
105 | particular, any distibution values $x_k$ outside the valid domain |
---|
106 | of $F$ will not be included, and the distribution will be implicitly |
---|
107 | truncated. This is controlled by the parameter limits defined in the |
---|
108 | model (which truncate the distribution before calling the kernel) as |
---|
109 | well as any region excluded using the *INVALID* macro defined within |
---|
110 | the model itself. |
---|
111 | |
---|
112 | The volume used in the polydispersity calculation is the form volume |
---|
113 | for solid objects or the shell volume for hollow objects. Shell |
---|
114 | volume should be used within $F$ so that the normalizing scale |
---|
115 | represents the volume fraction of the shell rather than the entire |
---|
116 | form. This corresponds to the volume fraction of shell-forming |
---|
117 | material added to the solvent. |
---|
118 | |
---|
119 | The calculation of $S$ requires the effective radius and the |
---|
120 | volume fraction of the particles. The model can have several |
---|
121 | different ways to compute effective radius, with the |
---|
122 | *effective_radius_type* parameter used to select amongst them. The |
---|
123 | volume fraction of particles should be determined from the total |
---|
124 | volume fraction of the form, not just the shell volume fraction. |
---|
125 | This makes a difference for hollow shapes, which need to scale |
---|
126 | the volume fraction by the returned volume ratio when computing $S$. |
---|
127 | For solid objects, the shell volume is set to the form volume so |
---|
128 | this scale factor evaluates to one and so can be used for both |
---|
129 | hollow and solid shapes. |
---|
130 | """ |
---|
131 | self._call_kernel(call_details, values, cutoff, magnetic, effective_radius_type) |
---|
132 | #print("returned",self.q_input.q, self.result) |
---|
133 | nout = 2 if self.info.have_Fq and self.dim == '1d' else 1 |
---|
134 | total_weight = self.result[nout*self.q_input.nq + 0] |
---|
135 | if total_weight == 0.: |
---|
136 | total_weight = 1. |
---|
137 | # Note: shell_volume == form_volume for solid objects |
---|
138 | form_volume = self.result[nout*self.q_input.nq + 1]/total_weight |
---|
139 | shell_volume = self.result[nout*self.q_input.nq + 2]/total_weight |
---|
140 | effective_radius = self.result[nout*self.q_input.nq + 3]/total_weight |
---|
141 | if shell_volume == 0.: |
---|
142 | shell_volume = 1. |
---|
143 | F1 = self.result[1:nout*self.q_input.nq:nout]/total_weight if nout == 2 else None |
---|
144 | F2 = self.result[0:nout*self.q_input.nq:nout]/total_weight |
---|
145 | return F1, F2, effective_radius, shell_volume, form_volume/shell_volume |
---|
146 | |
---|
147 | def release(self): |
---|
148 | # type: () -> None |
---|
149 | pass |
---|
150 | |
---|
151 | def _call_kernel(self, call_details, values, cutoff, magnetic, effective_radius_type): |
---|
152 | # type: (CallDetails, np.ndarray, np.ndarray, float, bool, int) -> np.ndarray |
---|
153 | """ |
---|
154 | Call the kernel. Subclasses defining kernels for particular execution |
---|
155 | engines need to provide an implementation for this. |
---|
156 | """ |
---|
157 | raise NotImplementedError() |
---|