source: sasmodels/sasmodels/modelinfo.py @ e65c3ba

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since e65c3ba was e65c3ba, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

lint

  • Property mode set to 100644
File size: 43.9 KB
Line 
1"""
2Model Info and Parameter Tables
3===============================
4
5Defines :class:`ModelInfo` and :class:`ParameterTable` and the routines for
6manipulating them.  In particular, :func:`make_model_info` converts a kernel
7module into the model info block as seen by the rest of the sasmodels library.
8"""
9from __future__ import print_function
10
11from copy import copy
12from os.path import abspath, basename, splitext
13import inspect
14
15import numpy as np  # type: ignore
16
17# Optional typing
18try:
19    from typing import Tuple, List, Union, Dict, Optional, Any, Callable, Sequence, Set
20    from types import ModuleType
21except ImportError:
22    pass
23else:
24    Limits = Tuple[float, float]
25    #LimitsOrChoice = Union[Limits, Tuple[Sequence[str]]]
26    ParameterDef = Tuple[str, str, float, Limits, str, str]
27    ParameterSetUser = Dict[str, Union[float, List[float]]]
28    ParameterSet = Dict[str, float]
29    TestInput = Union[str, float, List[float], Tuple[float, float], List[Tuple[float, float]]]
30    TestValue = Union[float, List[float]]
31    TestCondition = Tuple[ParameterSetUser, TestInput, TestValue]
32
33# If MAX_PD changes, need to change the loop macros in kernel_iq.c
34MAX_PD = 5 #: Maximum number of simultaneously polydisperse parameters
35
36# assumptions about common parameters exist throughout the code, such as:
37# (1) kernel functions Iq, Iqxy, form_volume, ... don't see them
38# (2) kernel drivers assume scale is par[0] and background is par[1]
39# (3) mixture models drop the background on components and replace the scale
40#     with a scale that varies from [-inf, inf]
41# (4) product models drop the background and reassign scale
42# and maybe other places.
43# Note that scale and background cannot be coordinated parameters whose value
44# depends on the some polydisperse parameter with the current implementation
45COMMON_PARAMETERS = [
46    ("scale", "", 1, (0.0, np.inf), "", "Source intensity"),
47    ("background", "1/cm", 1e-3, (-np.inf, np.inf), "", "Source background"),
48]
49assert (len(COMMON_PARAMETERS) == 2
50        and COMMON_PARAMETERS[0][0] == "scale"
51        and COMMON_PARAMETERS[1][0] == "background"), "don't change common parameters"
52
53
54def make_parameter_table(pars):
55    # type: (List[ParameterDef]) -> ParameterTable
56    """
57    Construct a parameter table from a list of parameter definitions.
58
59    This is used by the module processor to convert the parameter block into
60    the parameter table seen in the :class:`ModelInfo` for the module.
61    """
62    processed = []
63    for p in pars:
64        if not isinstance(p, (list, tuple)) or len(p) != 6:
65            raise ValueError("Parameter should be [name, units, default, limits, type, desc], but got %r"
66                             %str(p))
67        processed.append(parse_parameter(*p))
68    partable = ParameterTable(processed)
69    return partable
70
71def parse_parameter(name, units='', default=np.NaN,
72                    user_limits=None, ptype='', description=''):
73    # type: (str, str, float, Sequence[Any], str, str) -> Parameter
74    """
75    Parse an individual parameter from the parameter definition block.
76
77    This does type and value checking on the definition, leading
78    to early failure in the model loading process and easier debugging.
79    """
80    # Parameter is a user facing class.  Do robust type checking.
81    if not isstr(name):
82        raise ValueError("expected string for parameter name %r"%name)
83    if not isstr(units):
84        raise ValueError("expected units to be a string for %s"%name)
85
86    # Process limits as [float, float] or [[str, str, ...]]
87    choices = []  # type: List[str]
88    if user_limits is None:
89        limits = (-np.inf, np.inf)
90    elif not isinstance(user_limits, (tuple, list)):
91        raise ValueError("invalid limits for %s"%name)
92    else:
93        # if limits is [[str,...]], then this is a choice list field,
94        # and limits are 1 to length of string list
95        if isinstance(user_limits[0], (tuple, list)):
96            choices = user_limits[0]
97            limits = (0., len(choices)-1.)
98            if not all(isstr(k) for k in choices):
99                raise ValueError("choices must be strings for %s"%name)
100        else:
101            try:
102                low, high = user_limits
103                limits = (float(low), float(high))
104            except Exception:
105                raise ValueError("invalid limits for %s: %r"%(name, user_limits))
106            if low >= high:
107                raise ValueError("require lower limit < upper limit")
108
109    # Process default value as float, making sure it is in range
110    if not isinstance(default, (int, float)):
111        raise ValueError("expected default %r to be a number for %s"
112                         % (default, name))
113    if default < limits[0] or default > limits[1]:
114        raise ValueError("default value %r not in range for %s"
115                         % (default, name))
116
117    # Check for valid parameter type
118    if ptype not in ("volume", "orientation", "sld", "magnetic", ""):
119        raise ValueError("unexpected type %r for %s" % (ptype, name))
120
121    # Check for valid parameter description
122    if not isstr(description):
123        raise ValueError("expected description to be a string")
124
125    # Parameter id for name[n] does not include [n]
126    if "[" in name:
127        if not name.endswith(']'):
128            raise ValueError("Expected name[len] for vector parameter %s"%name)
129        pid, ref = name[:-1].split('[', 1)
130        ref = ref.strip()
131    else:
132        pid, ref = name, None
133
134    # automatically identify sld types
135    if ptype == '' and (pid.startswith('sld') or pid.endswith('sld')):
136        ptype = 'sld'
137
138    # Check if using a vector definition, name[k], as the parameter name
139    if ref:
140        if ref == '':
141            raise ValueError("Need to specify vector length for %s"%name)
142        try:
143            length = int(ref)
144            control = None
145        except ValueError:
146            length = None
147            control = ref
148    else:
149        length = 1
150        control = None
151
152    # Build the parameter
153    parameter = Parameter(name=name, units=units, default=default,
154                          limits=limits, ptype=ptype, description=description)
155
156    # TODO: need better control over whether a parameter is polydisperse
157    parameter.polydisperse = ptype in ('orientation', 'volume')
158    parameter.relative_pd = ptype == 'volume'
159    parameter.choices = choices
160    parameter.length = length
161    parameter.length_control = control
162
163    return parameter
164
165
166def expand_pars(partable, pars):
167    # type: (ParameterTable, ParameterSetUser) ->  ParameterSet
168    """
169    Create demo parameter set from key-value pairs.
170
171    *pars* are the key-value pairs to use for the parameters.  Any
172    parameters not specified in *pars* are set from the *partable* defaults.
173
174    If *pars* references vector fields, such as thickness[n], then support
175    different ways of assigning the demo values, including assigning a
176    specific value (e.g., thickness3=50.0), assigning a new value to all
177    (e.g., thickness=50.0) or assigning values using list notation.
178    """
179    if pars is None:
180        result = partable.defaults
181    else:
182        lookup = dict((p.id, p) for p in partable.kernel_parameters)
183        result = partable.defaults.copy()
184        scalars = dict((name, value) for name, value in pars.items()
185                       if name not in lookup or lookup[name].length == 1)
186        vectors = dict((name, value) for name, value in pars.items()
187                       if name in lookup and lookup[name].length > 1)
188        #print("lookup", lookup)
189        #print("scalars", scalars)
190        #print("vectors", vectors)
191        if vectors:
192            for name, value in vectors.items():
193                if np.isscalar(value):
194                    # support for the form
195                    #    dict(thickness=0, thickness2=50)
196                    for k in range(1, lookup[name].length+1):
197                        key = name+str(k)
198                        if key not in scalars:
199                            scalars[key] = value
200                else:
201                    # supoprt for the form
202                    #    dict(thickness=[20,10,3])
203                    for (k, v) in enumerate(value):
204                        scalars[name+str(k+1)] = v
205        result.update(scalars)
206        #print("expanded", result)
207
208    return result
209
210def prefix_parameter(par, prefix):
211    # type: (Parameter, str) -> Parameter
212    """
213    Return a copy of the parameter with its name prefixed.
214    """
215    new_par = copy(par)
216    new_par.name = prefix + par.name
217    new_par.id = prefix + par.id
218
219def suffix_parameter(par, suffix):
220    # type: (Parameter, str) -> Parameter
221    """
222    Return a copy of the parameter with its name prefixed.
223    """
224    new_par = copy(par)
225    # If name has the form x[n], replace with x_suffix[n]
226    new_par.name = par.id + suffix + par.name[len(par.id):]
227    new_par.id = par.id + suffix
228
229class Parameter(object):
230    """
231    The available kernel parameters are defined as a list, with each parameter
232    defined as a sublist with the following elements:
233
234    *name* is the name that will be displayed to the user.  Names
235    should be lower case, with words separated by underscore.  If
236    acronyms are used, the whole acronym should be upper case. For vector
237    parameters, the name will be followed by *[len]* where *len* is an
238    integer length of the vector, or the name of the parameter which
239    controls the length.  The attribute *id* will be created from name
240    without the length.
241
242    *units* should be one of *degrees* for angles, *Ang* for lengths,
243    *1e-6/Ang^2* for SLDs.
244
245    *default value* will be the initial value for  the model when it
246    is selected, or when an initial value is not otherwise specified.
247
248    *limits = [lb, ub]* are the hard limits on the parameter value, used to
249    limit the polydispersity density function.  In the fit, the parameter limits
250    given to the fit are the limits  on the central value of the parameter.
251    If there is polydispersity, it will evaluate parameter values outside
252    the fit limits, but not outside the hard limits specified in the model.
253    If there are no limits, use +/-inf imported from numpy.
254
255    *type* indicates how the parameter will be used.  "volume" parameters
256    will be used in all functions.  "orientation" parameters will be used
257    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
258    *Imagnetic* only.  If *type* is the empty string, the parameter will
259    be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
260    can automatically be promoted to magnetic parameters, each of which
261    will have a magnitude and a direction, which may be different from
262    other sld parameters. The volume parameters are used for calls
263    to form_volume within the kernel (required for volume normalization)
264    and for calls to ER and VR for effective radius and volume ratio
265    respectively.
266
267    *description* is a short description of the parameter.  This will
268    be displayed in the parameter table and used as a tool tip for the
269    parameter value in the user interface.
270
271    Additional values can be set after the parameter is created:
272
273    * *length* is the length of the field if it is a vector field
274
275    * *length_control* is the parameter which sets the vector length
276
277    * *is_control* is True if the parameter is a control parameter for a vector
278
279    * *polydisperse* is true if the parameter accepts a polydispersity
280
281    * *relative_pd* is true if that polydispersity is a portion of the
282      value (so a 10% length dipsersity would use a polydispersity value
283      of 0.1) rather than absolute dispersisity (such as an angle plus or
284      minus 15 degrees).
285
286    *choices* is the option names for a drop down list of options, as for
287    example, might be used to set the value of a shape parameter.
288
289    These values are set by :func:`make_parameter_table` and
290    :func:`parse_parameter` therein.
291    """
292    def __init__(self, name, units='', default=None, limits=(-np.inf, np.inf),
293                 ptype='', description=''):
294        # type: (str, str, float, Limits, str, str) -> None
295        self.id = name.split('[')[0].strip() # type: str
296        self.name = name                     # type: str
297        self.units = units                   # type: str
298        self.default = default               # type: float
299        self.limits = limits                 # type: Limits
300        self.type = ptype                    # type: str
301        self.description = description       # type: str
302
303        # Length and length_control will be filled in once the complete
304        # parameter table is available.
305        self.length = 1                      # type: int
306        self.length_control = None           # type: Optional[str]
307        self.is_control = False              # type: bool
308
309        # TODO: need better control over whether a parameter is polydisperse
310        self.polydisperse = False            # type: bool
311        self.relative_pd = False             # type: bool
312
313        # choices are also set externally.
314        self.choices = []                    # type: List[str]
315
316    def as_definition(self):
317        # type: () -> str
318        """
319        Declare space for the variable in a parameter structure.
320
321        For example, the parameter thickness with length 3 will
322        return "double thickness[3];", with no spaces before and
323        no new line character afterward.
324        """
325        if self.length == 1:
326            return "double %s;"%self.id
327        else:
328            return "double %s[%d];"%(self.id, self.length)
329
330    def as_function_argument(self):
331        # type: () -> str
332        r"""
333        Declare the variable as a function argument.
334
335        For example, the parameter thickness with length 3 will
336        return "double \*thickness", with no spaces before and
337        no comma afterward.
338        """
339        if self.length == 1:
340            return "double %s"%self.id
341        else:
342            return "double *%s"%self.id
343
344    def as_call_reference(self, prefix=""):
345        # type: (str) -> str
346        """
347        Return *prefix* + parameter name.  For struct references, use "v."
348        as the prefix.
349        """
350        # Note: if the parameter is a struct type, then we will need to use
351        # &prefix+id.  For scalars and vectors we can just use prefix+id.
352        return prefix + self.id
353
354    def __str__(self):
355        # type: () -> str
356        return "<%s>"%self.name
357
358    def __repr__(self):
359        # type: () -> str
360        return "P<%s>"%self.name
361
362
363class ParameterTable(object):
364    """
365    ParameterTable manages the list of available parameters.
366
367    There are a couple of complications which mean that the list of parameters
368    for the kernel differs from the list of parameters that the user sees.
369
370    (1) Common parameters.  Scale and background are implicit to every model,
371    but are not passed to the kernel.
372
373    (2) Vector parameters.  Vector parameters are passed to the kernel as a
374    pointer to an array, e.g., thick[], but they are seen by the user as n
375    separate parameters thick1, thick2, ...
376
377    Therefore, the parameter table is organized by how it is expected to be
378    used. The following information is needed to set up the kernel functions:
379
380    * *kernel_parameters* is the list of parameters in the kernel parameter
381      table, with vector parameter p declared as p[].
382
383    * *iq_parameters* is the list of parameters to the Iq(q, ...) function,
384      with vector parameter p sent as p[].
385
386    * [removed] *iqxy_parameters* is the list of parameters to the Iqxy(qx, qy, ...)
387      function, with vector parameter p sent as p[].
388
389    * *form_volume_parameters* is the list of parameters to the form_volume(...)
390      function, with vector parameter p sent as p[].
391
392    Problem details, which sets up the polydispersity loops, requires the
393    following:
394
395    * *theta_offset* is the offset of the theta parameter in the kernel parameter
396      table, with vector parameters counted as n individual parameters
397      p1, p2, ..., or offset is -1 if there is no theta parameter.
398
399    * *max_pd* is the maximum number of polydisperse parameters, with vector
400      parameters counted as n individual parameters p1, p2, ...  Note that
401      this number is limited to sasmodels.modelinfo.MAX_PD.
402
403    * *npars* is the total number of parameters to the kernel, with vector
404      parameters counted as n individual parameters p1, p2, ...
405
406    * *call_parameters* is the complete list of parameters to the kernel,
407      including scale and background, with vector parameters recorded as
408      individual parameters p1, p2, ...
409
410    * *active_1d* is the set of names that may be polydisperse for 1d data
411
412    * *active_2d* is the set of names that may be polydisperse for 2d data
413
414    User parameters are the set of parameters visible to the user, including
415    the scale and background parameters that the kernel does not see.  User
416    parameters don't use vector notation, and instead use p1, p2, ...
417    """
418    # scale and background are implicit parameters
419    COMMON = [Parameter(*p) for p in COMMON_PARAMETERS]
420
421    def __init__(self, parameters):
422        # type: (List[Parameter]) -> None
423        self.kernel_parameters = parameters
424        self._check_angles()
425        self._set_vector_lengths()
426
427        self.npars = sum(p.length for p in self.kernel_parameters)
428        self.nmagnetic = sum(p.length for p in self.kernel_parameters
429                             if p.type == 'sld')
430        self.nvalues = 2 + self.npars
431        if self.nmagnetic:
432            self.nvalues += 3 + 3*self.nmagnetic
433
434        self.call_parameters = self._get_call_parameters()
435        self.defaults = self._get_defaults()
436        #self._name_table= dict((p.id, p) for p in parameters)
437
438        # Set the kernel parameters.  Assumes background and scale are the
439        # first two parameters in the parameter list, but these are not sent
440        # to the underlying kernel functions.
441        self.iq_parameters = [p for p in self.kernel_parameters
442                              if p.type not in ('orientation', 'magnetic')]
443        # note: orientation no longer sent to Iqxy, so its the same as
444        #self.iqxy_parameters = [p for p in self.kernel_parameters
445        #                        if p.type != 'magnetic']
446        self.form_volume_parameters = [p for p in self.kernel_parameters
447                                       if p.type == 'volume']
448
449        # Theta offset
450        offset = 0
451        for p in self.kernel_parameters:
452            if p.name == 'theta':
453                self.theta_offset = offset
454                break
455            offset += p.length
456        else:
457            self.theta_offset = -1
458
459        # number of polydisperse parameters
460        num_pd = sum(p.length for p in self.kernel_parameters if p.polydisperse)
461        # Don't use more polydisperse parameters than are available in the model
462        self.max_pd = min(num_pd, MAX_PD)
463
464        # true if has 2D parameters
465        self.has_2d = any(p.type in ('orientation', 'magnetic')
466                          for p in self.kernel_parameters)
467        self.is_asymmetric = any(p.name == 'psi' for p in self.kernel_parameters)
468        self.magnetism_index = [k for k, p in enumerate(self.call_parameters)
469                                if p.id.startswith('M0:')]
470
471        self.pd_1d = set(p.name for p in self.call_parameters
472                         if p.polydisperse and p.type not in ('orientation', 'magnetic'))
473        self.pd_2d = set(p.name for p in self.call_parameters if p.polydisperse)
474
475    def _check_angles(self):
476        theta = phi = psi = -1
477        for k, p in enumerate(self.kernel_parameters):
478            if p.name == 'theta':
479                theta = k
480                if p.type != 'orientation':
481                    raise TypeError("theta must be an orientation parameter")
482            elif p.name == 'phi':
483                phi = k
484                if p.type != 'orientation':
485                    raise TypeError("phi must be an orientation parameter")
486            elif p.name == 'psi':
487                psi = k
488                if p.type != 'orientation':
489                    raise TypeError("psi must be an orientation parameter")
490        if theta >= 0 and phi >= 0:
491            if phi != theta+1:
492                raise TypeError("phi must follow theta")
493            if psi >= 0 and psi != phi+1:
494                raise TypeError("psi must follow phi")
495        elif theta >= 0 or phi >= 0 or psi >= 0:
496            raise TypeError("oriented shapes must have both theta and phi and maybe psi")
497
498    def __getitem__(self, key):
499        # Find the parameter definition
500        for par in self.call_parameters:
501            if par.name == key:
502                return par
503        raise KeyError("unknown parameter %r"%key)
504
505    def __contains__(self, key):
506        for par in self.call_parameters:
507            if par.name == key:
508                return True
509        return False
510
511    def _set_vector_lengths(self):
512        # type: () -> List[str]
513        """
514        Walk the list of kernel parameters, setting the length field of the
515        vector parameters from the upper limit of the reference parameter.
516
517        This needs to be done once the entire parameter table is available
518        since the reference may still be undefined when the parameter is
519        initially created.
520
521        Returns the list of control parameter names.
522
523        Note: This modifies the underlying parameter object.
524        """
525        # Sort out the length of the vector parameters such as thickness[n]
526        for p in self.kernel_parameters:
527            if p.length_control:
528                ref = self._get_ref(p)
529                ref.is_control = True
530                ref.polydisperse = False
531                low, high = ref.limits
532                if int(low) != low or int(high) != high or low < 0 or high > 20:
533                    raise ValueError("expected limits on %s to be within [0, 20]"
534                                     % ref.name)
535                p.length = int(high)
536
537    def _get_ref(self, p):
538        # type: (Parameter) -> Parameter
539        for ref in self.kernel_parameters:
540            if ref.id == p.length_control:
541                return ref
542        raise ValueError("no reference variable %r for %s"
543                         % (p.length_control, p.name))
544
545    def _get_defaults(self):
546        # type: () -> ParameterSet
547        """
548        Get a list of parameter defaults from the parameters.
549
550        Expands vector parameters into parameter id+number.
551        """
552        # Construct default values, including vector defaults
553        defaults = {}
554        for p in self.call_parameters:
555            if p.length == 1:
556                defaults[p.id] = p.default
557            else:
558                for k in range(1, p.length+1):
559                    defaults["%s%d"%(p.id, k)] = p.default
560        return defaults
561
562    def _get_call_parameters(self):
563        # type: () -> List[Parameter]
564        full_list = self.COMMON[:]
565        for p in self.kernel_parameters:
566            if p.length == 1:
567                full_list.append(p)
568            else:
569                for k in range(1, p.length+1):
570                    pk = Parameter(p.id+str(k), p.units, p.default,
571                                   p.limits, p.type, p.description)
572                    pk.polydisperse = p.polydisperse
573                    pk.relative_pd = p.relative_pd
574                    pk.choices = p.choices
575                    full_list.append(pk)
576
577        # Add the magnetic parameters to the end of the call parameter list.
578        if self.nmagnetic > 0:
579            full_list.extend([
580                Parameter('up:frac_i', '', 0., [0., 1.],
581                          'magnetic', 'fraction of spin up incident'),
582                Parameter('up:frac_f', '', 0., [0., 1.],
583                          'magnetic', 'fraction of spin up final'),
584                Parameter('up:angle', 'degress', 0., [0., 360.],
585                          'magnetic', 'spin up angle'),
586            ])
587            slds = [p for p in full_list if p.type == 'sld']
588            for p in slds:
589                full_list.extend([
590                    Parameter('M0:'+p.id, '1e-6/Ang^2', 0., [-np.inf, np.inf],
591                              'magnetic', 'magnetic amplitude for '+p.description),
592                    Parameter('mtheta:'+p.id, 'degrees', 0., [-90., 90.],
593                              'magnetic', 'magnetic latitude for '+p.description),
594                    Parameter('mphi:'+p.id, 'degrees', 0., [-180., 180.],
595                              'magnetic', 'magnetic longitude for '+p.description),
596                ])
597        #print("call parameters", full_list)
598        return full_list
599
600    def user_parameters(self, pars, is2d=True):
601        # type: (Dict[str, float], bool) -> List[Parameter]
602        """
603        Return the list of parameters for the given data type.
604
605        Vector parameters are expanded in place.  If multiple parameters
606        share the same vector length, then the parameters will be interleaved
607        in the result.  The control parameters come first.  For example,
608        if the parameter table is ordered as::
609
610            sld_core
611            sld_shell[num_shells]
612            sld_solvent
613            thickness[num_shells]
614            num_shells
615
616        and *pars[num_shells]=2* then the returned list will be::
617
618            num_shells
619            scale
620            background
621            sld_core
622            sld_shell1
623            thickness1
624            sld_shell2
625            thickness2
626            sld_solvent
627
628        Note that shell/thickness pairs are grouped together in the result
629        even though they were not grouped in the incoming table.  The control
630        parameter is always returned first since the GUI will want to set it
631        early, and rerender the table when it is changed.
632
633        Parameters marked as sld will automatically have a set of associated
634        magnetic parameters (m0:p, mtheta:p, mphi:p), as well as polarization
635        information (up:theta, up:frac_i, up:frac_f).
636        """
637        # control parameters go first
638        control = [p for p in self.kernel_parameters if p.is_control]
639
640        # Gather entries such as name[n] into groups of the same n
641        dependent = {} # type: Dict[str, List[Parameter]]
642        dependent.update((p.id, []) for p in control)
643        for p in self.kernel_parameters:
644            if p.length_control is not None:
645                dependent[p.length_control].append(p)
646
647        # Gather entries such as name[4] into groups of the same length
648        fixed_length = {}  # type: Dict[int, List[Parameter]]
649        for p in self.kernel_parameters:
650            if p.length > 1 and p.length_control is None:
651                fixed_length.setdefault(p.length, []).append(p)
652
653        # Using the call_parameters table, we already have expanded forms
654        # for each of the vector parameters; put them in a lookup table
655        # Note: p.id and p.name are currently identical for the call parameters
656        expanded_pars = dict((p.id, p) for p in self.call_parameters)
657
658        def append_group(name):
659            """add the named parameter, and related magnetic parameters if any"""
660            result.append(expanded_pars[name])
661            if is2d:
662                for tag in 'M0:', 'mtheta:', 'mphi:':
663                    if tag+name in expanded_pars:
664                        result.append(expanded_pars[tag+name])
665
666        # Gather the user parameters in order
667        result = control + self.COMMON
668        for p in self.kernel_parameters:
669            if not is2d and p.type in ('orientation', 'magnetic'):
670                pass
671            elif p.is_control:
672                pass # already added
673            elif p.length_control is not None:
674                table = dependent.get(p.length_control, [])
675                if table:
676                    # look up length from incoming parameters
677                    table_length = int(pars.get(p.length_control, p.length))
678                    del dependent[p.length_control] # first entry seen
679                    for k in range(1, table_length+1):
680                        for entry in table:
681                            append_group(entry.id+str(k))
682                else:
683                    pass # already processed all entries
684            elif p.length > 1:
685                table = fixed_length.get(p.length, [])
686                if table:
687                    table_length = p.length
688                    del fixed_length[p.length]
689                    for k in range(1, table_length+1):
690                        for entry in table:
691                            append_group(entry.id+str(k))
692                else:
693                    pass # already processed all entries
694            else:
695                append_group(p.id)
696
697        if is2d and 'up:angle' in expanded_pars:
698            result.extend([
699                expanded_pars['up:frac_i'],
700                expanded_pars['up:frac_f'],
701                expanded_pars['up:angle'],
702            ])
703
704        return result
705
706def isstr(x):
707    # type: (Any) -> bool
708    """
709    Return True if the object is a string.
710    """
711    # TODO: 2-3 compatible tests for str, including unicode strings
712    return isinstance(x, str)
713
714
715def _find_source_lines(model_info, kernel_module):
716    # type: (ModelInfo, ModuleType) -> None
717    """
718    Identify the location of the C source inside the model definition file.
719
720    This code runs through the source of the kernel module looking for
721    lines that start with 'Iq', 'Iqxy' or 'form_volume'.  Clearly there are
722    all sorts of reasons why this might not work (e.g., code commented out
723    in a triple-quoted line block, code built using string concatenation,
724    or code defined in the branch of an 'if' block), but it should work
725    properly in the 95% case, and getting the incorrect line number will
726    be harmless.
727    """
728    # Check if we need line numbers at all
729    if callable(model_info.Iq):
730        return None
731
732    if (model_info.Iq is None
733            and model_info.Iqxy is None
734            and model_info.Imagnetic is None
735            and model_info.form_volume is None):
736        return
737
738    # find the defintion lines for the different code blocks
739    try:
740        source = inspect.getsource(kernel_module)
741    except IOError:
742        return
743    for k, v in enumerate(source.split('\n')):
744        if v.startswith('Imagnetic'):
745            model_info._Imagnetic_line = k+1
746        elif v.startswith('Iqxy'):
747            model_info._Iqxy_line = k+1
748        elif v.startswith('Iq'):
749            model_info._Iq_line = k+1
750        elif v.startswith('form_volume'):
751            model_info._form_volume_line = k+1
752
753
754def make_model_info(kernel_module):
755    # type: (module) -> ModelInfo
756    """
757    Extract the model definition from the loaded kernel module.
758
759    Fill in default values for parts of the module that are not provided.
760
761    Note: vectorized Iq and Iqxy functions will be created for python
762    models when the model is first called, not when the model is loaded.
763    """
764    if hasattr(kernel_module, "model_info"):
765        # Custom sum/multi models
766        return kernel_module.model_info
767    info = ModelInfo()
768    #print("make parameter table", kernel_module.parameters)
769    parameters = make_parameter_table(getattr(kernel_module, 'parameters', []))
770    demo = expand_pars(parameters, getattr(kernel_module, 'demo', None))
771    filename = abspath(kernel_module.__file__).replace('.pyc', '.py')
772    kernel_id = splitext(basename(filename))[0]
773    name = getattr(kernel_module, 'name', None)
774    if name is None:
775        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
776
777    info.id = kernel_id  # string used to load the kernel
778    info.filename = filename
779    info.name = name
780    info.title = getattr(kernel_module, 'title', name+" model")
781    info.description = getattr(kernel_module, 'description', 'no description')
782    info.parameters = parameters
783    info.demo = demo
784    info.composition = None
785    info.docs = kernel_module.__doc__
786    info.category = getattr(kernel_module, 'category', None)
787    info.structure_factor = getattr(kernel_module, 'structure_factor', False)
788    info.profile_axes = getattr(kernel_module, 'profile_axes', ['x', 'y'])
789    info.source = getattr(kernel_module, 'source', [])
790    # TODO: check the structure of the tests
791    info.tests = getattr(kernel_module, 'tests', [])
792    info.ER = getattr(kernel_module, 'ER', None) # type: ignore
793    info.VR = getattr(kernel_module, 'VR', None) # type: ignore
794    info.form_volume = getattr(kernel_module, 'form_volume', None) # type: ignore
795    info.Iq = getattr(kernel_module, 'Iq', None) # type: ignore
796    info.Iqxy = getattr(kernel_module, 'Iqxy', None) # type: ignore
797    info.Imagnetic = getattr(kernel_module, 'Imagnetic', None) # type: ignore
798    info.profile = getattr(kernel_module, 'profile', None) # type: ignore
799    info.sesans = getattr(kernel_module, 'sesans', None) # type: ignore
800    # Default single and opencl to True for C models.  Python models have callable Iq.
801    info.opencl = getattr(kernel_module, 'opencl', not callable(info.Iq))
802    info.single = getattr(kernel_module, 'single', not callable(info.Iq))
803    info.random = getattr(kernel_module, 'random', None)
804
805    # multiplicity info
806    control_pars = [p.id for p in parameters.kernel_parameters if p.is_control]
807    default_control = control_pars[0] if control_pars else None
808    info.control = getattr(kernel_module, 'control', default_control)
809    info.hidden = getattr(kernel_module, 'hidden', None) # type: ignore
810
811    _find_source_lines(info, kernel_module)
812
813    return info
814
815class ModelInfo(object):
816    """
817    Interpret the model definition file, categorizing the parameters.
818
819    The module can be loaded with a normal python import statement if you
820    know which module you need, or with __import__('sasmodels.model.'+name)
821    if the name is in a string.
822
823    The structure should be mostly static, other than the delayed definition
824    of *Iq* and *Iqxy* if they need to be defined.
825    """
826    #: Full path to the file defining the kernel, if any.
827    filename = None         # type: Optional[str]
828    #: Id of the kernel used to load it from the filesystem.
829    id = None               # type: str
830    #: Display name of the model, which defaults to the model id but with
831    #: capitalization of the parts so for example core_shell defaults to
832    #: "Core Shell".
833    name = None             # type: str
834    #: Short description of the model.
835    title = None            # type: str
836    #: Long description of the model.
837    description = None      # type: str
838    #: Model parameter table. Parameters are defined using a list of parameter
839    #: definitions, each of which is contains parameter name, units,
840    #: default value, limits, type and description.  See :class:`Parameter`
841    #: for details on the individual parameters.  The parameters are gathered
842    #: into a :class:`ParameterTable`, which provides various views into the
843    #: parameter list.
844    parameters = None       # type: ParameterTable
845    #: Demo parameters as a *parameter:value* map used as the default values
846    #: for :mod:`compare`.  Any parameters not set in *demo* will use the
847    #: defaults from the parameter table.  That means no polydispersity, and
848    #: in the case of multiplicity models, a minimal model with no interesting
849    #: scattering.
850    demo = None             # type: Dict[str, float]
851    #: Composition is None if this is an independent model, or it is a
852    #: tuple with comoposition type ('product' or 'misture') and a list of
853    #: :class:`ModelInfo` blocks for the composed objects.  This allows us
854    #: to rebuild a complete mixture or product model from the info block.
855    #: *composition* is not given in the model definition file, but instead
856    #: arises when the model is constructed using names such as
857    #: *sphere*hardsphere* or *cylinder+sphere*.
858    composition = None      # type: Optional[Tuple[str, List[ModelInfo]]]
859    #: Name of the control parameter for a variant model such as :ref:`rpa`.
860    #: The *control* parameter should appear in the parameter table, with
861    #: limits defined as *[CASES]*, for case names such as
862    #: *CASES = ["diblock copolymer", "triblock copolymer", ...]*.
863    #: This should give *limits=[[case1, case2, ...]]*, but the
864    #: model loader translates this to *limits=[0, len(CASES)-1]*, and adds
865    #: *choices=CASES* to the :class:`Parameter` definition. Note that
866    #: models can use a list of cases as a parameter without it being a
867    #: control parameter.  Either way, the parameter is sent to the model
868    #: evaluator as *float(choice_num)*, where choices are numbered from 0.
869    #: See also :attr:`hidden`.
870    control = None          # type: str
871    #: Different variants require different parameters.  In order to show
872    #: just the parameters needed for the variant selected by :attr:`control`,
873    #: you should provide a function *hidden(control) -> set(['a', 'b', ...])*
874    #: indicating which parameters need to be hidden.  For multiplicity
875    #: models, you need to use the complete name of the parameter, including
876    #: its number.  So for example, if variant "a" uses only *sld1* and *sld2*,
877    #: then *sld3*, *sld4* and *sld5* of multiplicity parameter *sld[5]*
878    #: should be in the hidden set.
879    hidden = None           # type: Optional[Callable[[int], Set[str]]]
880    #: Doc string from the top of the model file.  This should be formatted
881    #: using ReStructuredText format, with latex markup in ".. math"
882    #: environments, or in dollar signs.  This will be automatically
883    #: extracted to a .rst file by :func:`generate.make_docs`, then
884    #: converted to HTML or PDF by Sphinx.
885    docs = None             # type: str
886    #: Location of the model description in the documentation.  This takes the
887    #: form of "section" or "section:subsection".  So for example,
888    #: :ref:`porod` uses *category="shape-independent"* so it is in the
889    #: :ref:`shape-independent` section whereas
890    #: :ref:`capped-cylinder` uses: *category="shape:cylinder"*, which puts
891    #: it in the :ref:`shape-cylinder` section.
892    category = None         # type: Optional[str]
893    #: True if the model can be computed accurately with single precision.
894    #: This is True by default, but models such as :ref:`bcc-paracrystal` set
895    #: it to False because they require double precision calculations.
896    single = None           # type: bool
897    #: True if the model can be run as an opencl model.  If for some reason
898    #: the model cannot be run in opencl (e.g., because the model passes
899    #: functions by reference), then set this to false.
900    opencl = None           # type: bool
901    #: True if the model is a structure factor used to model the interaction
902    #: between form factor models.  This will default to False if it is not
903    #: provided in the file.
904    structure_factor = None # type: bool
905    #: List of C source files used to define the model.  The source files
906    #: should define the *Iq* function, and possibly *Iqxy*, though a default
907    #: *Iqxy = Iq(sqrt(qx**2+qy**2)* will be created if no *Iqxy* is provided.
908    #: Files containing the most basic functions must appear first in the list,
909    #: followed by the files that use those functions.  Form factors are
910    #: indicated by providing a :attr:`ER` function.
911    source = None           # type: List[str]
912    #: The set of tests that must pass.  The format of the tests is described
913    #: in :mod:`model_test`.
914    tests = None            # type: List[TestCondition]
915    #: Returns the effective radius of the model given its volume parameters.
916    #: The presence of *ER* indicates that the model is a form factor model
917    #: that may be used together with a structure factor to form an implicit
918    #: multiplication model.
919    #:
920    #: The parameters to the *ER* function must be marked with type *volume*.
921    #: in the parameter table.  They will appear in the same order as they
922    #: do in the table.  The values passed to *ER* will be vectors, with one
923    #: value for each polydispersity condition.  For example, if the model
924    #: is polydisperse over both length and radius, then both length and
925    #: radius will have the same number of values in the vector, with one
926    #: value for each *length X radius*.  If only *radius* is polydisperse,
927    #: then the value for *length* will be repeated once for each value of
928    #: *radius*.  The *ER* function should return one effective radius for
929    #: each parameter set.  Multiplicity parameters will be received as
930    #: arrays, with one row per polydispersity condition.
931    ER = None               # type: Optional[Callable[[np.ndarray], np.ndarray]]
932    #: Returns the occupied volume and the total volume for each parameter set.
933    #: See :attr:`ER` for details on the parameters.
934    VR = None               # type: Optional[Callable[[np.ndarray], Tuple[np.ndarray, np.ndarray]]]
935    #: Returns the form volume for python-based models.  Form volume is needed
936    #: for volume normalization in the polydispersity integral.  If no
937    #: parameters are *volume* parameters, then form volume is not needed.
938    #: For C-based models, (with :attr:`sources` defined, or with :attr:`Iq`
939    #: defined using a string containing C code), form_volume must also be
940    #: C code, either defined as a string, or in the sources.
941    form_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
942    #: Returns *I(q, a, b, ...)* for parameters *a*, *b*, etc. defined
943    #: by the parameter table.  *Iq* can be defined as a python function, or
944    #: as a C function.  If it is defined in C, then set *Iq* to the body of
945    #: the C function, including the return statement.  This function takes
946    #: values for *q* and each of the parameters as separate *double* values
947    #: (which may be converted to float or long double by sasmodels).  All
948    #: source code files listed in :attr:`sources` will be loaded before the
949    #: *Iq* function is defined.  If *Iq* is not present, then sources should
950    #: define *static double Iq(double q, double a, double b, ...)* which
951    #: will return *I(q, a, b, ...)*.  Multiplicity parameters are sent as
952    #: pointers to doubles.  Constants in floating point expressions should
953    #: include the decimal point. See :mod:`generate` for more details.
954    Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
955    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
956    Iqxy = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
957    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
958    Imagnetic = None        # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
959    #: Returns a model profile curve *x, y*.  If *profile* is defined, this
960    #: curve will appear in response to the *Show* button in SasView.  Use
961    #: :attr:`profile_axes` to set the axis labels.  Note that *y* values
962    #: will be scaled by 1e6 before plotting.
963    profile = None          # type: Optional[Callable[[np.ndarray], None]]
964    #: Axis labels for the :attr:`profile` plot.  The default is *['x', 'y']*.
965    #: Only the *x* component is used for now.
966    profile_axes = None     # type: Tuple[str, str]
967    #: Returns *sesans(z, a, b, ...)* for models which can directly compute
968    #: the SESANS correlation function.  Note: not currently implemented.
969    sesans = None           # type: Optional[Callable[[np.ndarray], np.ndarray]]
970    #: Returns a random parameter set for the model
971    random = None           # type: Optional[Callable[[], Dict[str, float]]]
972
973    # line numbers within the python file for bits of C source, if defined
974    # NB: some compilers fail with a "#line 0" directive, so default to 1.
975    _Imagnetic_line = 1
976    _Iqxy_line = 1
977    _Iq_line = 1
978    _form_volume_line = 1
979
980
981    def __init__(self):
982        # type: () -> None
983        pass
984
985    def get_hidden_parameters(self, control):
986        """
987        Returns the set of hidden parameters for the model.  *control* is the
988        value of the control parameter.  Note that multiplicity models have
989        an implicit control parameter, which is the parameter that controls
990        the multiplicity.
991        """
992        if self.hidden is not None:
993            hidden = self.hidden(control)
994        else:
995            controls = [p for p in self.parameters.kernel_parameters
996                        if p.is_control]
997            if len(controls) != 1:
998                raise ValueError("more than one control parameter")
999            hidden = set(p.id+str(k)
1000                         for p in self.parameters.kernel_parameters
1001                         for k in range(control+1, p.length+1)
1002                         if p.length > 1)
1003        return hidden
Note: See TracBrowser for help on using the repository browser.