source: sasmodels/sasmodels/modelinfo.py @ 98ba1fc

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

complete merge from master to polydisp

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