source: sasmodels/sasmodels/modelinfo.py @ 8d62008

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

remove circular dependency between details/modelinfo; fix compare Calculator type hint

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