source: sasmodels/sasmodels/modelinfo.py @ 765d025

Last change on this file since 765d025 was 765d025, checked in by Paul Kienzle <pkienzle@…>, 5 years ago

Merge remote-tracking branch 'upstream/beta_approx'

  • Property mode set to 100644
File size: 45.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
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
52DEFAULT_BACKGROUND = 1e-3
53COMMON_PARAMETERS = [
54    ("scale", "", 1, (0.0, np.inf), "", "Source intensity"),
55    ("background", "1/cm", DEFAULT_BACKGROUND, (-np.inf, np.inf), "", "Source background"),
56]
57assert (len(COMMON_PARAMETERS) == 2
58        and COMMON_PARAMETERS[0][0] == "scale"
59        and COMMON_PARAMETERS[1][0] == "background"), "don't change common parameters"
60
61
62def make_parameter_table(pars):
63    # type: (List[ParameterDef]) -> ParameterTable
64    """
65    Construct a parameter table from a list of parameter definitions.
66
67    This is used by the module processor to convert the parameter block into
68    the parameter table seen in the :class:`ModelInfo` for the module.
69    """
70    processed = []
71    for p in pars:
72        if not isinstance(p, (list, tuple)) or len(p) != 6:
73            raise ValueError("Parameter should be [name, units, default, limits, type, desc], but got %r"
74                             %str(p))
75        processed.append(parse_parameter(*p))
76    partable = ParameterTable(processed)
77    partable.check_angles()
78    return partable
79
80def parse_parameter(name, units='', default=np.NaN,
81                    user_limits=None, ptype='', description=''):
82    # type: (str, str, float, Sequence[Any], str, str) -> Parameter
83    """
84    Parse an individual parameter from the parameter definition block.
85
86    This does type and value checking on the definition, leading
87    to early failure in the model loading process and easier debugging.
88    """
89    # Parameter is a user facing class.  Do robust type checking.
90    if not isstr(name):
91        raise ValueError("expected string for parameter name %r"%name)
92    if not isstr(units):
93        raise ValueError("expected units to be a string for %s"%name)
94
95    # Process limits as [float, float] or [[str, str, ...]]
96    choices = []  # type: List[str]
97    if user_limits is None:
98        limits = (-np.inf, np.inf)
99    elif not isinstance(user_limits, (tuple, list)):
100        raise ValueError("invalid limits for %s"%name)
101    else:
102        # if limits is [[str,...]], then this is a choice list field,
103        # and limits are 1 to length of string list
104        if isinstance(user_limits[0], (tuple, list)):
105            choices = user_limits[0]
106            limits = (0., len(choices)-1.)
107            if not all(isstr(k) for k in choices):
108                raise ValueError("choices must be strings for %s"%name)
109        else:
110            try:
111                low, high = user_limits
112                limits = (float(low), float(high))
113            except Exception:
114                raise ValueError("invalid limits for %s: %r"%(name, user_limits))
115            if low >= high:
116                raise ValueError("require lower limit < upper limit")
117
118    # Process default value as float, making sure it is in range
119    if not isinstance(default, (int, float)):
120        raise ValueError("expected default %r to be a number for %s"
121                         % (default, name))
122    if default < limits[0] or default > limits[1]:
123        raise ValueError("default value %r not in range for %s"
124                         % (default, name))
125
126    # Check for valid parameter type
127    if ptype not in ("volume", "orientation", "sld", "magnetic", ""):
128        raise ValueError("unexpected type %r for %s" % (ptype, name))
129
130    # Check for valid parameter description
131    if not isstr(description):
132        raise ValueError("expected description to be a string")
133
134    # Parameter id for name[n] does not include [n]
135    if "[" in name:
136        if not name.endswith(']'):
137            raise ValueError("Expected name[len] for vector parameter %s"%name)
138        pid, ref = name[:-1].split('[', 1)
139        ref = ref.strip()
140    else:
141        pid, ref = name, None
142
143    # automatically identify sld types
144    if ptype == '' and (pid.startswith('sld') or pid.endswith('sld')):
145        ptype = 'sld'
146
147    # Check if using a vector definition, name[k], as the parameter name
148    if ref:
149        if ref == '':
150            raise ValueError("Need to specify vector length for %s"%name)
151        try:
152            length = int(ref)
153            control = None
154        except ValueError:
155            length = None
156            control = ref
157    else:
158        length = 1
159        control = None
160
161    # Build the parameter
162    parameter = Parameter(name=name, units=units, default=default,
163                          limits=limits, ptype=ptype, description=description)
164
165    # TODO: need better control over whether a parameter is polydisperse
166    parameter.polydisperse = ptype in ('orientation', 'volume')
167    parameter.relative_pd = ptype == 'volume'
168    parameter.choices = choices
169    parameter.length = length
170    parameter.length_control = control
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    to shell_volume (for hollow shapes), and to effective_radius (for
273    structure factor interactions) 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        self.npars = sum(p.length for p in self.kernel_parameters)
431        self.nmagnetic = sum(p.length for p in self.kernel_parameters
432                             if p.type == 'sld')
433        self.nvalues = 2 + self.npars
434        if self.nmagnetic:
435            self.nvalues += 3 + 3*self.nmagnetic
436        self.call_parameters = self._get_call_parameters()
437        self.defaults = self._get_defaults()
438        #self._name_table= dict((p.id, p) for p in parameters)
439
440        # Set the kernel parameters.  Assumes background and scale are the
441        # first two parameters in the parameter list, but these are not sent
442        # to the underlying kernel functions.
443        self.iq_parameters = [p for p in self.kernel_parameters
444                              if p.type not in ('orientation', 'magnetic')]
445        self.orientation_parameters = [p for p in self.kernel_parameters
446                                       if p.type == 'orientation']
447        self.form_volume_parameters = [p for p in self.kernel_parameters
448                                       if p.type == 'volume']
449
450        # Theta offset
451        offset = 0
452        for p in self.kernel_parameters:
453            if p.name == 'theta':
454                self.theta_offset = offset
455                break
456            offset += p.length
457        else:
458            self.theta_offset = -1
459
460        # number of polydisperse parameters
461        num_pd = sum(p.length for p in self.kernel_parameters if p.polydisperse)
462        # Don't use more polydisperse parameters than are available in the model
463        self.max_pd = min(num_pd, MAX_PD)
464
465        # true if has 2D parameters
466        self.has_2d = any(p.type in ('orientation', 'magnetic')
467                          for p in self.kernel_parameters)
468        self.is_asymmetric = any(p.name == 'psi' for p in self.kernel_parameters)
469        self.magnetism_index = [k for k, p in enumerate(self.call_parameters)
470                                if p.id.endswith('_M0')]
471
472        self.pd_1d = set(p.name for p in self.call_parameters
473                         if p.polydisperse and p.type not in ('orientation', 'magnetic'))
474        self.pd_2d = set(p.name for p in self.call_parameters if p.polydisperse)
475
476    def check_angles(self):
477        """
478        Check that orientation angles are theta, phi and possibly psi.
479        """
480        theta = phi = psi = -1
481        for k, p in enumerate(self.kernel_parameters):
482            if p.name == 'theta':
483                theta = k
484                if p.type != 'orientation':
485                    raise TypeError("theta must be an orientation parameter")
486            elif p.name == 'phi':
487                phi = k
488                if p.type != 'orientation':
489                    raise TypeError("phi must be an orientation parameter")
490            elif p.name == 'psi':
491                psi = k
492                if p.type != 'orientation':
493                    raise TypeError("psi must be an orientation parameter")
494            elif p.type == 'orientation':
495                raise TypeError("only theta, phi and psi can be orientation parameters")
496        if theta >= 0 and phi >= 0:
497            last_par = len(self.kernel_parameters) - 1
498            if phi != theta+1:
499                raise TypeError("phi must follow theta")
500            if psi >= 0 and psi != phi+1:
501                raise TypeError("psi must follow phi")
502            if (psi >= 0 and psi != last_par) or (psi < 0 and phi != last_par):
503                raise TypeError("orientation parameters must appear at the "
504                                "end of the parameter table")
505        elif theta >= 0 or phi >= 0 or psi >= 0:
506            raise TypeError("oriented shapes must have both theta and phi and maybe psi")
507
508    def __getitem__(self, key):
509        # Find the parameter definition
510        for par in self.call_parameters:
511            if par.name == key:
512                return par
513        raise KeyError("unknown parameter %r"%key)
514
515    def __contains__(self, key):
516        for par in self.call_parameters:
517            if par.name == key:
518                return True
519        return False
520
521    def _set_vector_lengths(self):
522        # type: () -> List[str]
523        """
524        Walk the list of kernel parameters, setting the length field of the
525        vector parameters from the upper limit of the reference parameter.
526
527        This needs to be done once the entire parameter table is available
528        since the reference may still be undefined when the parameter is
529        initially created.
530
531        Returns the list of control parameter names.
532
533        Note: This modifies the underlying parameter object.
534        """
535        # Sort out the length of the vector parameters such as thickness[n]
536        for p in self.kernel_parameters:
537            if p.length_control:
538                ref = self._get_ref(p)
539                ref.is_control = True
540                ref.polydisperse = False
541                low, high = ref.limits
542                if int(low) != low or int(high) != high or low < 0 or high > 20:
543                    raise ValueError("expected limits on %s to be within [0, 20]"
544                                     % ref.name)
545                p.length = int(high)
546
547    def _get_ref(self, p):
548        # type: (Parameter) -> Parameter
549        for ref in self.kernel_parameters:
550            if ref.id == p.length_control:
551                return ref
552        raise ValueError("no reference variable %r for %s"
553                         % (p.length_control, p.name))
554
555    def _get_defaults(self):
556        # type: () -> ParameterSet
557        """
558        Get a list of parameter defaults from the parameters.
559
560        Expands vector parameters into parameter id+number.
561        """
562        # Construct default values, including vector defaults
563        defaults = {}
564        for p in self.call_parameters:
565            if p.length == 1:
566                defaults[p.id] = p.default
567            else:
568                for k in range(1, p.length+1):
569                    defaults["%s%d"%(p.id, k)] = p.default
570        return defaults
571
572    def _get_call_parameters(self):
573        # type: () -> List[Parameter]
574        full_list = self.COMMON[:]
575        for p in self.kernel_parameters:
576            if p.length == 1:
577                full_list.append(p)
578            else:
579                for k in range(1, p.length+1):
580                    pk = Parameter(p.id+str(k), p.units, p.default,
581                                   p.limits, p.type, p.description)
582                    pk.polydisperse = p.polydisperse
583                    pk.relative_pd = p.relative_pd
584                    pk.choices = p.choices
585                    full_list.append(pk)
586
587        # Add the magnetic parameters to the end of the call parameter list.
588        if self.nmagnetic > 0:
589            full_list.extend([
590                Parameter('up_frac_i', '', 0., [0., 1.],
591                          'magnetic', 'fraction of spin up incident'),
592                Parameter('up_frac_f', '', 0., [0., 1.],
593                          'magnetic', 'fraction of spin up final'),
594                Parameter('up_angle', 'degrees', 0., [0., 360.],
595                          'magnetic', 'spin up angle'),
596            ])
597            slds = [p for p in full_list if p.type == 'sld']
598            for p in slds:
599                full_list.extend([
600                    Parameter(p.id+'_M0', '1e-6/Ang^2', 0., [-np.inf, np.inf],
601                              'magnetic', 'magnetic amplitude for '+p.description),
602                    Parameter(p.id+'_mtheta', 'degrees', 0., [-90., 90.],
603                              'magnetic', 'magnetic latitude for '+p.description),
604                    Parameter(p.id+'_mphi', 'degrees', 0., [-180., 180.],
605                              'magnetic', 'magnetic longitude for '+p.description),
606                ])
607        #print("call parameters", full_list)
608        return full_list
609
610    def user_parameters(self, pars, is2d=True):
611        # type: (Dict[str, float], bool) -> List[Parameter]
612        """
613        Return the list of parameters for the given data type.
614
615        Vector parameters are expanded in place.  If multiple parameters
616        share the same vector length, then the parameters will be interleaved
617        in the result.  The control parameters come first.  For example,
618        if the parameter table is ordered as::
619
620            sld_core
621            sld_shell[num_shells]
622            sld_solvent
623            thickness[num_shells]
624            num_shells
625
626        and *pars[num_shells]=2* then the returned list will be::
627
628            num_shells
629            scale
630            background
631            sld_core
632            sld_shell1
633            thickness1
634            sld_shell2
635            thickness2
636            sld_solvent
637
638        Note that shell/thickness pairs are grouped together in the result
639        even though they were not grouped in the incoming table.  The control
640        parameter is always returned first since the GUI will want to set it
641        early, and rerender the table when it is changed.
642
643        Parameters marked as sld will automatically have a set of associated
644        magnetic parameters (p_M0, p_mtheta, p_mphi), as well as polarization
645        information (up_theta, up_frac_i, up_frac_f).
646        """
647        # control parameters go first
648        control = [p for p in self.kernel_parameters if p.is_control]
649
650        # Gather entries such as name[n] into groups of the same n
651        dependent = {} # type: Dict[str, List[Parameter]]
652        dependent.update((p.id, []) for p in control)
653        for p in self.kernel_parameters:
654            if p.length_control is not None:
655                dependent[p.length_control].append(p)
656
657        # Gather entries such as name[4] into groups of the same length
658        fixed_length = {}  # type: Dict[int, List[Parameter]]
659        for p in self.kernel_parameters:
660            if p.length > 1 and p.length_control is None:
661                fixed_length.setdefault(p.length, []).append(p)
662
663        # Using the call_parameters table, we already have expanded forms
664        # for each of the vector parameters; put them in a lookup table
665        # Note: p.id and p.name are currently identical for the call parameters
666        expanded_pars = dict((p.id, p) for p in self.call_parameters)
667
668        def append_group(name):
669            """add the named parameter, and related magnetic parameters if any"""
670            result.append(expanded_pars[name])
671            if is2d:
672                for tag in '_M0', '_mtheta', '_mphi':
673                    if name+tag in expanded_pars:
674                        result.append(expanded_pars[name+tag])
675
676        # Gather the user parameters in order
677        result = control + self.COMMON
678        for p in self.kernel_parameters:
679            if not is2d and p.type in ('orientation', 'magnetic'):
680                pass
681            elif p.is_control:
682                pass # already added
683            elif p.length_control is not None:
684                table = dependent.get(p.length_control, [])
685                if table:
686                    # look up length from incoming parameters
687                    table_length = int(pars.get(p.length_control, p.length))
688                    del dependent[p.length_control] # first entry seen
689                    for k in range(1, table_length+1):
690                        for entry in table:
691                            append_group(entry.id+str(k))
692                else:
693                    pass # already processed all entries
694            elif p.length > 1:
695                table = fixed_length.get(p.length, [])
696                if table:
697                    table_length = p.length
698                    del fixed_length[p.length]
699                    for k in range(1, table_length+1):
700                        for entry in table:
701                            append_group(entry.id+str(k))
702                else:
703                    pass # already processed all entries
704            else:
705                append_group(p.id)
706
707        if is2d and 'up_angle' in expanded_pars:
708            result.extend([
709                expanded_pars['up_frac_i'],
710                expanded_pars['up_frac_f'],
711                expanded_pars['up_angle'],
712            ])
713
714        return result
715
716def isstr(x):
717    # type: (Any) -> bool
718    """
719    Return True if the object is a string.
720    """
721    # TODO: 2-3 compatible tests for str, including unicode strings
722    return isinstance(x, str)
723
724
725#: Set of variables defined in the model that might contain C code
726C_SYMBOLS = ['Imagnetic', 'Iq', 'Iqxy', 'Iqac', 'Iqabc', 'form_volume', 'shell_volume', 'c_code']
727
728def _find_source_lines(model_info, kernel_module):
729    # type: (ModelInfo, ModuleType) -> None
730    """
731    Identify the location of the C source inside the model definition file.
732
733    This code runs through the source of the kernel module looking for lines
734    that contain C code (because it is a c function definition).  Clearly
735    there are all sorts of reasons why this might not work (e.g., code
736    commented out in a triple-quoted line block, code built using string
737    concatenation, code defined in the branch of an 'if' block, code imported
738    from another file), but it should work properly in the 95% case, and for
739    the remainder, getting the incorrect line number will merely be
740    inconvenient.
741    """
742    # Only need line numbers if we are creating a C module and the C symbols
743    # are defined.
744    if (callable(model_info.Iq)
745            or not any(hasattr(model_info, s) for s in C_SYMBOLS)):
746        return
747
748    # load the module source if we can
749    try:
750        source = inspect.getsource(kernel_module)
751    except IOError:
752        return
753
754    # look for symbol at the start of the line
755    for lineno, line in enumerate(source.split('\n')):
756        for name in C_SYMBOLS:
757            if line.startswith(name):
758                # Add 1 since some compilers complain about "#line 0"
759                model_info.lineno[name] = lineno + 1
760                break
761
762def make_model_info(kernel_module):
763    # type: (module) -> ModelInfo
764    """
765    Extract the model definition from the loaded kernel module.
766
767    Fill in default values for parts of the module that are not provided.
768
769    Note: vectorized Iq and Iqac/Iqabc functions will be created for python
770    models when the model is first called, not when the model is loaded.
771    """
772    if hasattr(kernel_module, "model_info"):
773        # Custom sum/multi models
774        return kernel_module.model_info
775
776    info = ModelInfo()
777    #print("make parameter table", kernel_module.parameters)
778    parameters = make_parameter_table(getattr(kernel_module, 'parameters', []))
779    demo = expand_pars(parameters, getattr(kernel_module, 'demo', None))
780    filename = abspath(kernel_module.__file__).replace('.pyc', '.py')
781    kernel_id = splitext(basename(filename))[0]
782    name = getattr(kernel_module, 'name', None)
783    if name is None:
784        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
785
786    info.id = kernel_id  # string used to load the kernel
787    info.filename = filename
788    info.name = name
789    info.title = getattr(kernel_module, 'title', name+" model")
790    info.description = getattr(kernel_module, 'description', 'no description')
791    info.parameters = parameters
792    info.demo = demo
793    info.composition = None
794    info.docs = kernel_module.__doc__
795    info.category = getattr(kernel_module, 'category', None)
796    info.structure_factor = getattr(kernel_module, 'structure_factor', False)
797    # TODO: find Fq by inspection
798    info.effective_radius_type = getattr(kernel_module, 'effective_radius_type', None)
799    info.have_Fq = getattr(kernel_module, 'have_Fq', False)
800    info.profile_axes = getattr(kernel_module, 'profile_axes', ['x', 'y'])
801    # Note: custom.load_custom_kernel_module assumes the C sources are defined
802    # by this attribute.
803    info.source = getattr(kernel_module, 'source', [])
804    info.c_code = getattr(kernel_module, 'c_code', None)
805    info.effective_radius = getattr(kernel_module, 'effective_radius', None)
806    # TODO: check the structure of the tests
807    info.tests = getattr(kernel_module, 'tests', [])
808    info.form_volume = getattr(kernel_module, 'form_volume', None) # type: ignore
809    info.shell_volume = getattr(kernel_module, 'shell_volume', None) # type: ignore
810    info.Iq = getattr(kernel_module, 'Iq', None) # type: ignore
811    info.Iqxy = getattr(kernel_module, 'Iqxy', None) # type: ignore
812    info.Iqac = getattr(kernel_module, 'Iqac', None) # type: ignore
813    info.Iqabc = getattr(kernel_module, 'Iqabc', None) # type: ignore
814    info.Imagnetic = getattr(kernel_module, 'Imagnetic', None) # type: ignore
815    info.profile = getattr(kernel_module, 'profile', None) # type: ignore
816    info.sesans = getattr(kernel_module, 'sesans', None) # type: ignore
817    info.random = getattr(kernel_module, 'random', None)
818
819    # multiplicity info
820    control_pars = [p.id for p in parameters.kernel_parameters if p.is_control]
821    default_control = control_pars[0] if control_pars else None
822    info.control = getattr(kernel_module, 'control', default_control)
823    info.hidden = getattr(kernel_module, 'hidden', None) # type: ignore
824
825    info.lineno = {}
826    _find_source_lines(info, kernel_module)
827    if getattr(kernel_module, 'py2c', False):
828        try:
829            warnings = autoc.convert(info, kernel_module)
830        except Exception as exc:
831            warnings = [str(exc)]
832        if warnings:
833            warnings.append("while converting %s from C to python"%name)
834            if len(warnings) > 2:
835                warnings = "\n".join(warnings)
836            else:
837                warnings = " ".join(warnings)
838            logger.warn(warnings)
839
840    # Default single and opencl to True for C models.  Python models have callable Iq.
841    # Needs to come after autoc.convert since the Iq symbol may have been
842    # converted from python to C
843    info.opencl = getattr(kernel_module, 'opencl', not callable(info.Iq))
844    info.single = getattr(kernel_module, 'single', not callable(info.Iq))
845
846    if callable(info.Iq) and parameters.has_2d:
847        raise ValueError("oriented python models not supported")
848
849    return info
850
851class ModelInfo(object):
852    """
853    Interpret the model definition file, categorizing the parameters.
854
855    The module can be loaded with a normal python import statement if you
856    know which module you need, or with __import__('sasmodels.model.'+name)
857    if the name is in a string.
858
859    The structure should be mostly static, other than the delayed definition
860    of *Iq*, *Iqac* and *Iqabc* if they need to be defined.
861    """
862    #: Full path to the file defining the kernel, if any.
863    filename = None         # type: Optional[str]
864    #: Id of the kernel used to load it from the filesystem.
865    id = None               # type: str
866    #: Display name of the model, which defaults to the model id but with
867    #: capitalization of the parts so for example core_shell defaults to
868    #: "Core Shell".
869    name = None             # type: str
870    #: Short description of the model.
871    title = None            # type: str
872    #: Long description of the model.
873    description = None      # type: str
874    #: Model parameter table. Parameters are defined using a list of parameter
875    #: definitions, each of which is contains parameter name, units,
876    #: default value, limits, type and description.  See :class:`Parameter`
877    #: for details on the individual parameters.  The parameters are gathered
878    #: into a :class:`ParameterTable`, which provides various views into the
879    #: parameter list.
880    parameters = None       # type: ParameterTable
881    #: Demo parameters as a *parameter:value* map used as the default values
882    #: for :mod:`compare`.  Any parameters not set in *demo* will use the
883    #: defaults from the parameter table.  That means no polydispersity, and
884    #: in the case of multiplicity models, a minimal model with no interesting
885    #: scattering.
886    demo = None             # type: Dict[str, float]
887    #: Composition is None if this is an independent model, or it is a
888    #: tuple with comoposition type ('product' or 'misture') and a list of
889    #: :class:`ModelInfo` blocks for the composed objects.  This allows us
890    #: to rebuild a complete mixture or product model from the info block.
891    #: *composition* is not given in the model definition file, but instead
892    #: arises when the model is constructed using names such as
893    #: *sphere*hardsphere* or *cylinder+sphere*.
894    composition = None      # type: Optional[Tuple[str, List[ModelInfo]]]
895    #: Name of the control parameter for a variant model such as :ref:`rpa`.
896    #: The *control* parameter should appear in the parameter table, with
897    #: limits defined as *[CASES]*, for case names such as
898    #: *CASES = ["diblock copolymer", "triblock copolymer", ...]*.
899    #: This should give *limits=[[case1, case2, ...]]*, but the
900    #: model loader translates this to *limits=[0, len(CASES)-1]*, and adds
901    #: *choices=CASES* to the :class:`Parameter` definition. Note that
902    #: models can use a list of cases as a parameter without it being a
903    #: control parameter.  Either way, the parameter is sent to the model
904    #: evaluator as *float(choice_num)*, where choices are numbered from 0.
905    #: See also :attr:`hidden`.
906    control = None          # type: str
907    #: Different variants require different parameters.  In order to show
908    #: just the parameters needed for the variant selected by :attr:`control`,
909    #: you should provide a function *hidden(control) -> set(['a', 'b', ...])*
910    #: indicating which parameters need to be hidden.  For multiplicity
911    #: models, you need to use the complete name of the parameter, including
912    #: its number.  So for example, if variant "a" uses only *sld1* and *sld2*,
913    #: then *sld3*, *sld4* and *sld5* of multiplicity parameter *sld[5]*
914    #: should be in the hidden set.
915    hidden = None           # type: Optional[Callable[[int], Set[str]]]
916    #: Doc string from the top of the model file.  This should be formatted
917    #: using ReStructuredText format, with latex markup in ".. math"
918    #: environments, or in dollar signs.  This will be automatically
919    #: extracted to a .rst file by :func:`generate.make_docs`, then
920    #: converted to HTML or PDF by Sphinx.
921    docs = None             # type: str
922    #: Location of the model description in the documentation.  This takes the
923    #: form of "section" or "section:subsection".  So for example,
924    #: :ref:`porod` uses *category="shape-independent"* so it is in the
925    #: :ref:`shape-independent` section whereas
926    #: :ref:`capped-cylinder` uses: *category="shape:cylinder"*, which puts
927    #: it in the :ref:`shape-cylinder` section.
928    category = None         # type: Optional[str]
929    #: True if the model can be computed accurately with single precision.
930    #: This is True by default, but models such as :ref:`bcc-paracrystal` set
931    #: it to False because they require double precision calculations.
932    single = None           # type: bool
933    #: True if the model can be run as an opencl model.  If for some reason
934    #: the model cannot be run in opencl (e.g., because the model passes
935    #: functions by reference), then set this to false.
936    opencl = None           # type: bool
937    #: True if the model is a structure factor used to model the interaction
938    #: between form factor models.  This will default to False if it is not
939    #: provided in the file.
940    structure_factor = None # type: bool
941    #: True if the model defines an Fq function with signature
942    #: void Fq(double q, double *F1, double *F2, ...)
943    have_Fq = False
944    #: List of options for computing the effective radius of the shape,
945    #: or None if the model is not usable as a form factor model.
946    effective_radius_type = None   # type: List[str]
947    #: List of C source files used to define the model.  The source files
948    #: should define the *Iq* function, and possibly *Iqac* or *Iqabc* if the
949    #: model defines orientation parameters. Files containing the most basic
950    #: functions must appear first in the list, followed by the files that
951    #: use those functions.
952    source = None           # type: List[str]
953    #: inline source code, added after all elements of source
954    c_code = None           # type: Optional[str]
955    #: Returns the form volume for python-based models.  Form volume is needed
956    #: for volume normalization in the polydispersity integral.  If no
957    #: parameters are *volume* parameters, then form volume is not needed.
958    #: For C-based models, (with :attr:`sources` defined, or with :attr:`Iq`
959    #: defined using a string containing C code), form_volume must also be
960    #: C code, either defined as a string, or in the sources.
961    form_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
962    #: Returns the shell volume for python-based models.  Form volume and
963    #: shell volume are needed for volume normalization in the polydispersity
964    #: integral and structure interactions for hollow shapes.  If no
965    #: parameters are *volume* parameters, then shell volume is not needed.
966    #: For C-based models, (with :attr:`sources` defined, or with :attr:`Iq`
967    #: defined using a string containing C code), shell_volume must also be
968    #: C code, either defined as a string, or in the sources.
969    shell_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
970    #: Returns *I(q, a, b, ...)* for parameters *a*, *b*, etc. defined
971    #: by the parameter table.  *Iq* can be defined as a python function, or
972    #: as a C function.  If it is defined in C, then set *Iq* to the body of
973    #: the C function, including the return statement.  This function takes
974    #: values for *q* and each of the parameters as separate *double* values
975    #: (which may be converted to float or long double by sasmodels).  All
976    #: source code files listed in :attr:`sources` will be loaded before the
977    #: *Iq* function is defined.  If *Iq* is not present, then sources should
978    #: define *static double Iq(double q, double a, double b, ...)* which
979    #: will return *I(q, a, b, ...)*.  Multiplicity parameters are sent as
980    #: pointers to doubles.  Constants in floating point expressions should
981    #: include the decimal point. See :mod:`generate` for more details.
982    Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
983    #: Returns *I(qab, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
984    Iqac = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
985    #: Returns *I(qa, qb, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
986    Iqabc = None            # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
987    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
988    Imagnetic = None        # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
989    #: Returns a model profile curve *x, y*.  If *profile* is defined, this
990    #: curve will appear in response to the *Show* button in SasView.  Use
991    #: :attr:`profile_axes` to set the axis labels.  Note that *y* values
992    #: will be scaled by 1e6 before plotting.
993    profile = None          # type: Optional[Callable[[np.ndarray], None]]
994    #: Axis labels for the :attr:`profile` plot.  The default is *['x', 'y']*.
995    #: Only the *x* component is used for now.
996    profile_axes = None     # type: Tuple[str, str]
997    #: Returns *sesans(z, a, b, ...)* for models which can directly compute
998    #: the SESANS correlation function.  Note: not currently implemented.
999    sesans = None           # type: Optional[Callable[[np.ndarray], np.ndarray]]
1000    #: Returns a random parameter set for the model
1001    random = None           # type: Optional[Callable[[], Dict[str, float]]]
1002    #: Line numbers for symbols defining C code
1003    lineno = None           # type: Dict[str, int]
1004    #: The set of tests that must pass.  The format of the tests is described
1005    #: in :mod:`model_test`.
1006    tests = None            # type: List[TestCondition]
1007
1008    def __init__(self):
1009        # type: () -> None
1010        pass
1011
1012    def get_hidden_parameters(self, control):
1013        """
1014        Returns the set of hidden parameters for the model.  *control* is the
1015        value of the control parameter.  Note that multiplicity models have
1016        an implicit control parameter, which is the parameter that controls
1017        the multiplicity.
1018        """
1019        if self.hidden is not None:
1020            hidden = self.hidden(control)
1021        else:
1022            controls = [p for p in self.parameters.kernel_parameters
1023                        if p.is_control]
1024            if len(controls) != 1:
1025                raise ValueError("more than one control parameter")
1026            hidden = set(p.id+str(k)
1027                         for p in self.parameters.kernel_parameters
1028                         for k in range(control+1, p.length+1)
1029                         if p.length > 1)
1030            for p in self.parameters.kernel_parameters:
1031                if p.length > 1 and p.type == "sld":
1032                    for k in range(control+1, p.length+1):
1033                        base = p.id+str(k)
1034                        hidden.update((base+"_M0", base+"_mtheta", base+"_mphi"))
1035        return hidden
Note: See TracBrowser for help on using the repository browser.