source: sasmodels/sasmodels/modelinfo.py @ c036ddb

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since c036ddb was c036ddb, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

refactor so Iq is not needed if Fq is defined

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