source: sasmodels/sasmodels/modelinfo.py @ ef07e95

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

lint

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