source: sasmodels/sasmodels/modelinfo.py @ 3b6567f

Last change on this file since 3b6567f was 3b6567f, checked in by Paul Kienzle <pkienzle@…>, 8 months ago

Merge remote-tracking branch 'upstream/beta_approx'

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