source: sasmodels/sasmodels/modelinfo.py @ 1662ebe

Last change on this file since 1662ebe was 1662ebe, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

Merge remote-tracking branch 'upstream/master'

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