source: sasmodels/sasmodels/modelinfo.py @ 4073633

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

make sure that jitter angle defaults to zero if no polydispersity

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