source: sasmodels/sasmodels/modelinfo.py @ 69aa451

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 69aa451 was 69aa451, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

refactor parameter representation

  • Property mode set to 100644
File size: 12.0 KB
Line 
1
2import numpy as np
3
4# TODO: turn ModelInfo into a proper class
5ModelInfo = dict
6
7COMMON_PARAMETERS = [
8    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
9    ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],
10]
11assert (len(COMMON_PARAMETERS) == 2
12        and COMMON_PARAMETERS[0][0]=="scale"
13        and COMMON_PARAMETERS[1][0]=="background"), "don't change common parameters"
14# assumptions about common parameters exist throughout the code, such as:
15# (1) kernel functions Iq, Iqxy, form_volume, ... don't see them
16# (2) kernel drivers assume scale is par[0] and background is par[1]
17# (3) mixture models drop the background on components and replace the scale
18#     with a scale that varies from [-inf, inf]
19# (4) product models drop the background and reassign scale
20# and maybe other places.
21# Note that scale and background cannot be coordinated parameters whose value
22# depends on the some polydisperse parameter with the current implementation
23
24def make_parameter_table(pars):
25    processed = []
26    for p in pars:
27        if not isinstance(p, list) or len(p) != 6:
28            raise ValueError("Parameter should be [name, units, default, limits, type, desc], but got %r"
29                             %str(p))
30        processed.append(parse_parameter(*p))
31    partable = ParameterTable(processed)
32    set_vector_length_from_reference(partable)
33    return partable
34
35def set_vector_length_from_reference(partable):
36    # Sort out the length of the vector parameters such as thickness[n]
37    for p in partable:
38        if p.length_control:
39           ref = partable[p.length_control]
40           low, high = ref.limits
41           if int(low) != low or int(high) != high or low<0 or high>20:
42               raise ValueError("expected limits on %s to be within [0, 20]"%ref.name)
43           p.length = low
44
45def parse_parameter(name, units='', default=None,
46                    limits=(-np.inf, np.inf), type='', description=''):
47    # Parameter is a user facing class.  Do robust type checking.
48    if not isstr(name):
49        raise ValueError("expected string for parameter name %r"%name)
50    if not isstr(units):
51        raise ValueError("expected units to be a string for %s"%name)
52    # if limits is a list of strings, then this is a choice list
53    # field, and limits are 1 to length of string list
54    if isinstance(limits, list) and all(isstr(k) for k in limits):
55        choices = limits
56        limits = [1, len(choices)]
57    else:
58        choices = None
59    # TODO: maybe allow limits of None for (-inf, inf)
60    try:
61        low, high = limits
62        if not isinstance(low, (int, float)):
63            raise TypeError("low is not numeric")
64        if not isinstance(high, (int, float)):
65            raise TypeError("high is not numeric")
66        if low >= high:
67            raise ValueError("require low < high")
68    except:
69        raise ValueError("invalid limits %s for %s"%(limits, name))
70
71    if not isinstance(default, (int, float)):
72        raise ValueError("expected default %r to be a number for %s"
73                         % (default, name))
74    if default < low or default > high:
75        raise ValueError("default value %r not in range for %s"
76                         % (default, name))
77
78    if type not in ("volume", "orientation", "sld", "magnetic", ""):
79        raise ValueError("unexpected type %r for %s" % (type, name))
80
81    if not isstr(description):
82        raise ValueError("expected description to be a string")
83
84
85    # Parameter id for name[n] does not include [n]
86    if "[" in name:
87        if not name.endswith(']'):
88            raise ValueError("Expected name[len] for vector parameter %s"%name)
89        pid, ref = name[:-1].split('[', 1)
90        ref = ref.strip()
91    else:
92        pid, ref = name, None
93
94
95    # automatically identify sld types
96    if type=='' and (pid.startswith('sld') or pid.endswith('sld')):
97        type = 'sld'
98
99    # Check if using a vector definition, name[k], as the parameter name
100    if ref:
101        if ref == '':
102            raise ValueError("Need to specify vector length for %s"%name)
103        try:
104            length = int(ref)
105            control = None
106        except:
107            length = None
108            control = ref
109    else:
110        length = 1
111        control = None
112
113    # Build the parameter
114    parameter = Parameter(name=name, units=units, default=default,
115                          limits=limits, type=type, description=description)
116
117    # TODO: need better control over whether a parameter is polydisperse
118    parameter.polydisperse = type in ('orientation', 'volume')
119    parameter.relative_pd = type in ('volume')
120    parameter.choices = choices
121    parameter.length = length
122    parameter.length_control = control
123
124    return parameter
125
126class Parameter(object):
127    """
128    The available kernel parameters are defined as a list, with each parameter
129    defined as a sublist with the following elements:
130
131    *name* is the name that will be used in the call to the kernel
132    function and the name that will be displayed to the user.  Names
133    should be lower case, with words separated by underscore.  If
134    acronyms are used, the whole acronym should be upper case.
135
136    *units* should be one of *degrees* for angles, *Ang* for lengths,
137    *1e-6/Ang^2* for SLDs.
138
139    *default value* will be the initial value for  the model when it
140    is selected, or when an initial value is not otherwise specified.
141
142    *limits = [lb, ub]* are the hard limits on the parameter value, used to
143    limit the polydispersity density function.  In the fit, the parameter limits
144    given to the fit are the limits  on the central value of the parameter.
145    If there is polydispersity, it will evaluate parameter values outside
146    the fit limits, but not outside the hard limits specified in the model.
147    If there are no limits, use +/-inf imported from numpy.
148
149    *type* indicates how the parameter will be used.  "volume" parameters
150    will be used in all functions.  "orientation" parameters will be used
151    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
152    *Imagnetic* only.  If *type* is the empty string, the parameter will
153    be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
154    can automatically be promoted to magnetic parameters, each of which
155    will have a magnitude and a direction, which may be different from
156    other sld parameters.
157
158    *description* is a short description of the parameter.  This will
159    be displayed in the parameter table and used as a tool tip for the
160    parameter value in the user interface.
161
162    Additional values can be set after the parameter is created:
163
164    *length* is the length of the field if it is a vector field
165    *length_control* is the parameter which sets the vector length
166    *polydisperse* is true if the parameter accepts a polydispersity
167    *relative_pd* is true if that polydispersity is relative
168
169    In the usual process these values are set by :func:`make_parameter_table`
170    and :func:`parse_parameter` therein.
171    """
172    def __init__(self, name, units='', default=None, limits=(-np.inf, np.inf),
173                 type='', description=''):
174        self.id = name
175        self.name = name
176        self.default = default
177        self.limits = limits
178        self.type = type
179        self.description = description
180        self.choices = None
181
182        # Length and length_control will be filled in by
183        # set_vector_length_from_reference(partable) once the complete
184        # parameter table is available.
185        self.length = 1
186        self.length_control = None
187
188        # TODO: need better control over whether a parameter is polydisperse
189        self.polydisperse = False
190        self.relative_pd = False
191
192    def as_definition(self):
193        """
194        Declare space for the variable in a parameter structure.
195
196        For example, the parameter thickness with length 3 will
197        return "double thickness[3];", with no spaces before and
198        no new line character afterward.
199        """
200        if self.length == 1:
201            return "double %s;"%self.id
202        else:
203            return "double %s[%d];"%(self.id, self.length)
204
205    def as_function_argument(self):
206        """
207        Declare the variable as a function argument.
208
209        For example, the parameter thickness with length 3 will
210        return "double *thickness", with no spaces before and
211        no comma afterward.
212        """
213        if self.length == 1:
214            return "double %s"%self.id
215        else:
216            return "double *%s"%self.id
217
218    def as_call_reference(self, prefix=""):
219        # Note: if the parameter is a struct type, then we will need to use
220        # &prefix+id.  For scalars and vectors we can just use prefix+id.
221        return prefix + self.id
222
223    def __str__(self):
224        return "<%s>"%self.name
225
226    def __repr__(self):
227        return "P<%s>"%self.name
228
229class ParameterTable(object):
230    # scale and background are implicit parameters
231    COMMON = [Parameter(*p) for p in COMMON_PARAMETERS]
232
233    def __init__(self, parameters):
234        self.parameters = self.COMMON + parameters
235        self._name_table= dict((p.name, p) for p in parameters)
236        self._categorize_parameters()
237
238    def __getitem__(self, k):
239        if isinstance(k, (int, slice)):
240            return self.parameters[k]
241        else:
242            return self._name_table[k]
243
244    def __contains__(self, key):
245        return key in self._name_table
246
247    def __iter__(self):
248        return iter(self.parameters)
249
250    def kernel_pars(self, ptype=None):
251        """
252        Return the parameters to the user kernel which match the given type.
253
254        Types include '1d' for Iq kernels, '2d' for Iqxy kernels and
255        'volume' for form_volume kernels.
256        """
257        # Assumes background and scale are the first two parameters
258        if ptype is None:
259            return self.parameters[2:]
260        else:
261            return [p for p in self.parameters[2:] if p in self.type[ptype]]
262
263    def _categorize_parameters(self):
264        """
265        Build parameter categories out of the the parameter definitions.
266
267        Returns a dictionary of categories.
268
269        Note: these categories are subject to change, depending on the needs of
270        the UI and the needs of the kernel calling function.
271
272        The categories are as follows:
273
274        * *volume* list of volume parameter names
275        * *orientation* list of orientation parameters
276        * *magnetic* list of magnetic parameters
277        * *sld* list of parameters that have no type info
278        * *other* list of parameters that have no type info
279
280        Each parameter is in one and only one category.
281        """
282        pars = self.parameters
283
284        par_type = {
285            'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], 'other': [],
286        }
287        for p in self.parameters:
288            par_type[p.type if p.type else 'other'].append(p)
289        par_type['1d'] = [p for p in pars if p.type not in ('orientation', 'magnetic')]
290        par_type['2d'] = [p for p in pars if p.type != 'magnetic']
291        par_type['magnetic'] = [p for p in pars]
292        par_type['pd'] = [p for p in pars if p.polydisperse]
293        par_type['pd_relative'] = [p for p in pars if p.relative_pd]
294        self.type = par_type
295
296        # find index of theta (or whatever variable is used for spherical
297        # normalization during polydispersity...
298        if 'theta' in par_type['2d']:
299            # TODO: may be an off-by 2 bug due to background and scale
300            # TODO: is theta always the polar coordinate?
301            self.theta_par = [k for k,p in enumerate(pars) if p.name=='theta'][0]
302        else:
303            self.theta_par = -1
304
305    @property
306    def defaults(self):
307        return dict((p.name, p.default) for p in self.parameters)
308
309    @property
310    def num_pd(self):
311        """
312        Number of distributional parameters in the model (polydispersity in
313        shape dimensions and orientational distributions).
314        """
315        return len(self.type['pd'])
316
317    @property
318    def has_2d(self):
319        return self.type['orientation'] or self.type['magnetic']
320
321
322def isstr(x):
323    # TODO: 2-3 compatible tests for str, including unicode strings
324    return isinstance(x, str)
325
Note: See TracBrowser for help on using the repository browser.