source: sasmodels/sasmodels/modelinfo.py @ c499331

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

progress on having compare.py recognize vector parameters

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