source: sasmodels/sasmodels/modelinfo.py @ ce176ca

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since ce176ca was ce176ca, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

fix parameter show/hide for multiplicity models

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