source: sasmodels/sasmodels/modelinfo.py @ 3199b17

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

Remove references to ER and VR from sasmodels. Refs #1202.

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