source: sasmodels/sasmodels/modelinfo.py @ c01ed3e

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

code cleanup for py2c converter

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