source: sasmodels/sasmodels/mixture.py @ 0edb6c1

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 0edb6c1 was 0edb6c1, checked in by lewis, 7 years ago

Merge branch 'ticket-767' of https://github.com/lewisodriscoll/sasmodels into ticket-767

  • Property mode set to 100644
File size: 12.0 KB
Line 
1"""
2Mixture model
3-------------
4
5The product model multiplies the structure factor by the form factor,
6modulated by the effective radius of the form.  The resulting model
7has a attributes of both the model description (with parameters, etc.)
8and the module evaluator (with call, release, etc.).
9
10To use it, first load form factor P and structure factor S, then create
11*ProductModel(P, S)*.
12"""
13from __future__ import print_function
14
15from copy import copy
16import numpy as np  # type: ignore
17
18from .modelinfo import Parameter, ParameterTable, ModelInfo
19from .kernel import KernelModel, Kernel
20from .details import make_details
21
22try:
23    from typing import List
24except ImportError:
25    pass
26
27def make_mixture_info(parts, operation='+'):
28    # type: (List[ModelInfo]) -> ModelInfo
29    """
30    Create info block for mixture model.
31    """
32    # Build new parameter list
33    combined_pars = []
34
35    model_num = 0
36    all_parts = copy(parts)
37    is_flat = False
38    while not is_flat:
39        is_flat = True
40        for part in all_parts:
41            if part.composition and part.composition[0] == 'mixture' and \
42                len(part.composition[1]) > 1:
43                all_parts += part.composition[1]
44                all_parts.remove(part)
45                is_flat = False
46
47    # When creating a mixture model that is a sum of product models (ie (1*2)+(3*4))
48    # the parameters for models 1 & 2 will be prefixed with A & B respectively,
49    # but so will the parameters for models 3 & 4. We need to rename models 3 & 4
50    # so that they are prefixed with C & D to avoid overlap of parameter names.
51    used_prefixes = []
52    for part in parts:
53        i = 0
54        if part.composition and part.composition[0] == 'mixture':
55            npars_list = [info.parameters.npars for info in part.composition[1]]
56            for npars in npars_list:
57                # List of params of one of the constituent models of part
58                submodel_pars = part.parameters.kernel_parameters[i:i+npars]
59                # Prefix of the constituent model
60                prefix = submodel_pars[0].name[0]
61                if prefix not in used_prefixes: # Haven't seen this prefix so far
62                    used_prefixes.append(prefix)
63                    i += npars
64                    continue
65                while prefix in used_prefixes:
66                    # This prefix has been already used, so change it to the
67                    # next letter that hasn't been used
68                    prefix = chr(ord(prefix) + 1)
69                used_prefixes.append(prefix)
70                prefix += "_"
71                # Update the parameters of this constituent model to use the
72                # new prefix
73                for par in submodel_pars:
74                    par.id = prefix + par.id[2:]
75                    par.name = prefix + par.name[2:]
76                    if par.length_control is not None:
77                        par.length_control = prefix + par.length_control[2:]
78                i += npars
79
80    for part in parts:
81        # Parameter prefix per model, A_, B_, ...
82        # Note that prefix must also be applied to id and length_control
83        # to support vector parameters
84        prefix = ''
85        if not part.composition:
86            # Model isn't a composition model, so it's parameters don't have a
87            # a prefix. Add the next available prefix
88            prefix = chr(ord('A')+len(used_prefixes))
89            used_prefixes.append(prefix)
90            prefix += '_'
91           
92        if operation == '+':
93            # If model is a sum model, each constituent model gets its own scale parameter
94            scale_prefix = prefix
95            if prefix == '' and part.operation == '*':
96                # `part` is a composition product model. Find the prefixes of
97                # it's parameters to form a new prefix for the scale, eg:
98                # a model with A*B*C will have ABC_scale
99                sub_prefixes = []
100                for param in part.parameters.kernel_parameters:
101                    # Prefix of constituent model
102                    sub_prefix = param.id.split('_')[0]
103                    if sub_prefix not in sub_prefixes:
104                        sub_prefixes.append(sub_prefix)
105                # Concatenate sub_prefixes to form prefix for the scale
106                scale_prefix = ''.join(sub_prefixes) + '_'
107            scale =  Parameter(scale_prefix + 'scale', default=1.0,
108                            description="model intensity for " + part.name)
109            combined_pars.append(scale)
110        for p in part.parameters.kernel_parameters:
111            p = copy(p)
112            p.name = prefix + p.name
113            p.id = prefix + p.id
114            if p.length_control is not None:
115                p.length_control = prefix + p.length_control
116            combined_pars.append(p)
117    parameters = ParameterTable(combined_pars)
118    parameters.max_pd = sum(part.parameters.max_pd for part in parts)
119
120    def random():
121        combined_pars = {}
122        for k, part in enumerate(parts):
123            prefix = chr(ord('A')+k) + '_'
124            pars = part.random()
125            combined_pars.update((prefix+k, v) for k, v in pars.items())
126        return combined_pars
127
128    model_info = ModelInfo()
129    model_info.id = operation.join(part.id for part in parts)
130    model_info.operation = operation
131    model_info.name = '(' + operation.join(part.name for part in parts) + ')'
132    model_info.filename = None
133    model_info.title = 'Mixture model with ' + model_info.name
134    model_info.description = model_info.title
135    model_info.docs = model_info.title
136    model_info.category = "custom"
137    model_info.parameters = parameters
138    model_info.random = random
139    #model_info.single = any(part['single'] for part in parts)
140    model_info.structure_factor = False
141    model_info.variant_info = None
142    #model_info.tests = []
143    #model_info.source = []
144    # Iq, Iqxy, form_volume, ER, VR and sesans
145    # Remember the component info blocks so we can build the model
146    model_info.composition = ('mixture', parts)
147    return model_info
148
149
150class MixtureModel(KernelModel):
151    def __init__(self, model_info, parts):
152        # type: (ModelInfo, List[KernelModel]) -> None
153        self.info = model_info
154        self.parts = parts
155        self.dtype = parts[0].dtype
156
157    def make_kernel(self, q_vectors):
158        # type: (List[np.ndarray]) -> MixtureKernel
159        # Note: may be sending the q_vectors to the n times even though they
160        # are only needed once.  It would mess up modularity quite a bit to
161        # handle this optimally, especially since there are many cases where
162        # separate q vectors are needed (e.g., form in python and structure
163        # in opencl; or both in opencl, but one in single precision and the
164        # other in double precision).
165        kernels = [part.make_kernel(q_vectors) for part in self.parts]
166        return MixtureKernel(self.info, kernels)
167
168    def release(self):
169        # type: () -> None
170        """
171        Free resources associated with the model.
172        """
173        for part in self.parts:
174            part.release()
175
176
177class MixtureKernel(Kernel):
178    def __init__(self, model_info, kernels):
179        # type: (ModelInfo, List[Kernel]) -> None
180        self.dim = kernels[0].dim
181        self.info =  model_info
182        self.kernels = kernels
183        self.dtype = self.kernels[0].dtype
184        self.operation = model_info.operation
185        self.results = []  # type: List[np.ndarray]
186
187    def __call__(self, call_details, values, cutoff, magnetic):
188        # type: (CallDetails, np.ndarray, np.ndarry, float, bool) -> np.ndarray
189        scale, background = values[0:2]
190        total = 0.0
191        # remember the parts for plotting later
192        self.results = []  # type: List[np.ndarray]
193        parts = MixtureParts(self.info, self.kernels, call_details, values)
194        for kernel, kernel_details, kernel_values in parts:
195            #print("calling kernel", kernel.info.name)
196            result = kernel(kernel_details, kernel_values, cutoff, magnetic)
197            result = np.array(result).astype(kernel.dtype)
198            # print(kernel.info.name, result)
199            if self.operation == '+':
200                total += result
201            elif self.operation == '*':
202                if np.all(total) == 0.0:
203                    total = result
204                else:
205                    total *= result
206            self.results.append(result)
207
208        return scale*total + background
209
210    def release(self):
211        # type: () -> None
212        for k in self.kernels:
213            k.release()
214
215
216class MixtureParts(object):
217    def __init__(self, model_info, kernels, call_details, values):
218        # type: (ModelInfo, List[Kernel], CallDetails, np.ndarray) -> None
219        self.model_info = model_info
220        self.parts = model_info.composition[1]
221        self.kernels = kernels
222        self.call_details = call_details
223        self.values = values
224        self.spin_index = model_info.parameters.npars + 2
225        #call_details.show(values)
226
227    def __iter__(self):
228        # type: () -> PartIterable
229        self.part_num = 0
230        self.par_index = 2
231        self.mag_index = self.spin_index + 3
232        return self
233
234    def next(self):
235        # type: () -> Tuple[List[Callable], CallDetails, np.ndarray]
236        if self.part_num >= len(self.parts):
237            raise StopIteration()
238        info = self.parts[self.part_num]
239        kernel = self.kernels[self.part_num]
240        call_details = self._part_details(info, self.par_index)
241        values = self._part_values(info, self.par_index, self.mag_index)
242        values = values.astype(kernel.dtype)
243        #call_details.show(values)
244
245        self.part_num += 1
246        self.par_index += info.parameters.npars
247        if self.model_info.operation == '+':
248            self.par_index += 1 # Account for each constituent model's scale param
249        self.mag_index += 3 * len(info.parameters.magnetism_index)
250
251        return kernel, call_details, values
252
253    def _part_details(self, info, par_index):
254        # type: (ModelInfo, int) -> CallDetails
255        full = self.call_details
256        # par_index is index into values array of the current parameter,
257        # which includes the initial scale and background parameters.
258        # We want the index into the weight length/offset for each parameter.
259        # Exclude the initial scale and background, so subtract two. If we're
260        # building an addition model, each component has its own scale factor
261        # which we need to skip when constructing the details for the kernel, so
262        # add one, giving a net subtract one.
263        diff = 1 if self.model_info.operation == '+' else 2
264        index = slice(par_index - diff, par_index - diff + info.parameters.npars)
265        length = full.length[index]
266        offset = full.offset[index]
267        # The complete weight vector is being sent to each part so that
268        # offsets don't need to be adjusted.
269        part = make_details(info, length, offset, full.num_weights)
270        return part
271
272    def _part_values(self, info, par_index, mag_index):
273        # type: (ModelInfo, int, int) -> np.ndarray
274        # Set each constituent model's scale to 1 if this is a multiplication model
275        scale = self.values[par_index] if self.model_info.operation == '+' else 1.0
276        diff = 1 if self.model_info.operation == '+' else 0 # Skip scale if addition model
277        pars = self.values[par_index + diff:par_index + info.parameters.npars + diff]
278        nmagnetic = len(info.parameters.magnetism_index)
279        if nmagnetic:
280            spin_state = self.values[self.spin_index:self.spin_index + 3]
281            mag_index = self.values[mag_index:mag_index + 3 * nmagnetic]
282        else:
283            spin_state = []
284            mag_index = []
285        nvalues = self.model_info.parameters.nvalues
286        nweights = self.call_details.num_weights
287        weights = self.values[nvalues:nvalues+2*nweights]
288        zero = self.values.dtype.type(0.)
289        values = [[scale, zero], pars, spin_state, mag_index, weights]
290        # Pad value array to a 32 value boundary
291        spacer = (32 - sum(len(v) for v in values)%32)%32
292        values.append([zero]*spacer)
293        values = np.hstack(values).astype(self.kernels[0].dtype)
294        return values
Note: See TracBrowser for help on using the repository browser.