source: sasmodels/sasmodels/modelinfo.py @ 17695aa

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

Merge branch 'master' into ticket-1157

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