source: sasmodels/sasmodels/modelinfo.py @ 89dba62

ticket-1257-vesicle-product
Last change on this file since 89dba62 was 98c045a, checked in by Paul Kienzle <pkienzle@…>, 5 years ago

lint

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