source: sasmodels/sasmodels/modelinfo.py @ 1711569

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 1711569 was 1711569, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

fix 'degress' typo

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