source: sasmodels/sasmodels/modelinfo.py @ 4f4d3e3

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

re-enable control parameter for rpa model. Refs #1022

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