source: sasmodels/sasmodels/modelinfo.py @ f67f26c

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

restructure kernels using fixed PD loops

  • Property mode set to 100644
File size: 41.0 KB
Line 
1"""
2Model Info and Parameter Tables
3===============================
4
5Defines :class:`ModelInfo` and :class:`ParameterTable` and the routines for
6manipulating them.  In particular, :func:`make_model_info` converts a kernel
7module into the model info block as seen by the rest of the sasmodels library.
8"""
9from __future__ import print_function
10
11from copy import copy
12from os.path import abspath, basename, splitext
13import inspect
14
15import numpy as np  # type: ignore
16
17# Optional typing
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
417        self.npars = sum(p.length for p in self.kernel_parameters)
418        self.nmagnetic = sum(p.length for p in self.kernel_parameters
419                             if p.type=='sld')
420        self.nvalues = 2 + self.npars
421        if self.nmagnetic:
422            self.nvalues += 3 + 3*self.nmagnetic
423
424        self.call_parameters = self._get_call_parameters()
425        self.defaults = self._get_defaults()
426        #self._name_table= dict((p.id, p) for p in parameters)
427
428        # Set the kernel parameters.  Assumes background and scale are the
429        # first two parameters in the parameter list, but these are not sent
430        # to the underlying kernel functions.
431        self.iq_parameters = [p for p in self.kernel_parameters
432                              if p.type not in ('orientation', 'magnetic')]
433        self.iqxy_parameters = [p for p in self.kernel_parameters
434                                if p.type != 'magnetic']
435        self.form_volume_parameters = [p for p in self.kernel_parameters
436                                       if p.type == 'volume']
437
438        # Theta offset
439        offset = 0
440        for p in self.kernel_parameters:
441            if p.name == 'theta':
442                self.theta_offset = offset
443                break
444            offset += p.length
445        else:
446            self.theta_offset = -1
447
448        # number of polydisperse parameters
449        num_pd = sum(p.length for p in self.kernel_parameters if p.polydisperse)
450        # Don't use more polydisperse parameters than are available in the model
451        self.max_pd = min(num_pd, MAX_PD)
452
453        # true if has 2D parameters
454        self.has_2d = any(p.type in ('orientation', 'magnetic')
455                          for p in self.kernel_parameters)
456        self.magnetism_index = [k for k,p in enumerate(self.call_parameters)
457                                if p.id.startswith('M0:')]
458
459        self.pd_1d = set(p.name for p in self.call_parameters
460                         if p.polydisperse and p.type not in ('orientation', 'magnetic'))
461        self.pd_2d = set(p.name for p in self.call_parameters if p.polydisperse)
462
463    def _set_vector_lengths(self):
464        # type: () -> List[str]
465        """
466        Walk the list of kernel parameters, setting the length field of the
467        vector parameters from the upper limit of the reference parameter.
468
469        This needs to be done once the entire parameter table is available
470        since the reference may still be undefined when the parameter is
471        initially created.
472
473        Returns the list of control parameter names.
474
475        Note: This modifies the underlying parameter object.
476        """
477        # Sort out the length of the vector parameters such as thickness[n]
478
479        for p in self.kernel_parameters:
480            if p.length_control:
481                for ref in self.kernel_parameters:
482                    if ref.id == p.length_control:
483                        break
484                else:
485                    raise ValueError("no reference variable %r for %s"
486                                     % (p.length_control, p.name))
487                ref.is_control = True
488                ref.polydisperse = False
489                low, high = ref.limits
490                if int(low) != low or int(high) != high or low < 0 or high > 20:
491                    raise ValueError("expected limits on %s to be within [0, 20]"
492                                     % ref.name)
493                p.length = int(high)
494
495    def _get_defaults(self):
496        # type: () -> ParameterSet
497        """
498        Get a list of parameter defaults from the parameters.
499
500        Expands vector parameters into parameter id+number.
501        """
502        # Construct default values, including vector defaults
503        defaults = {}
504        for p in self.call_parameters:
505            if p.length == 1:
506                defaults[p.id] = p.default
507            else:
508                for k in range(1, p.length+1):
509                    defaults["%s%d"%(p.id, k)] = p.default
510        return defaults
511
512    def _get_call_parameters(self):
513        # type: () -> List[Parameter]
514        full_list = self.COMMON[:]
515        for p in self.kernel_parameters:
516            if p.length == 1:
517                full_list.append(p)
518            else:
519                for k in range(1, p.length+1):
520                    pk = Parameter(p.id+str(k), p.units, p.default,
521                                   p.limits, p.type, p.description)
522                    pk.polydisperse = p.polydisperse
523                    pk.relative_pd = p.relative_pd
524                    full_list.append(pk)
525
526        # Add the magnetic parameters to the end of the call parameter list.
527        if self.nmagnetic > 0:
528            full_list.extend([
529                Parameter('up:frac_i', '', 0., [0., 1.],
530                          'magnetic', 'fraction of spin up incident'),
531                Parameter('up:frac_f', '', 0., [0., 1.],
532                          'magnetic', 'fraction of spin up final'),
533                Parameter('up:angle', 'degress', 0., [0., 360.],
534                          'magnetic', 'spin up angle'),
535            ])
536            slds = [p for p in full_list if p.type == 'sld']
537            for p in slds:
538                full_list.extend([
539                    Parameter('M0:'+p.id, '1e-6/Ang^2', 0., [-np.inf, np.inf],
540                              'magnetic', 'magnetic amplitude for '+p.description),
541                    Parameter('mtheta:'+p.id, 'degrees', 0., [-90., 90.],
542                               'magnetic', 'magnetic latitude for '+p.description),
543                    Parameter('mphi:'+p.id, 'degrees', 0., [-180., 180.],
544                               'magnetic', 'magnetic longitude for '+p.description),
545                ])
546        #print("call parameters", full_list)
547        return full_list
548
549    def user_parameters(self, pars={}, is2d=True):
550        # type: (Dict[str, float], bool) -> List[Parameter]
551        """
552        Return the list of parameters for the given data type.
553
554        Vector parameters are expanded in place.  If multiple parameters
555        share the same vector length, then the parameters will be interleaved
556        in the result.  The control parameters come first.  For example,
557        if the parameter table is ordered as::
558
559            sld_core
560            sld_shell[num_shells]
561            sld_solvent
562            thickness[num_shells]
563            num_shells
564
565        and *pars[num_shells]=2* then the returned list will be::
566
567            num_shells
568            scale
569            background
570            sld_core
571            sld_shell1
572            thickness1
573            sld_shell2
574            thickness2
575            sld_solvent
576
577        Note that shell/thickness pairs are grouped together in the result
578        even though they were not grouped in the incoming table.  The control
579        parameter is always returned first since the GUI will want to set it
580        early, and rerender the table when it is changed.
581
582        Parameters marked as sld will automatically have a set of associated
583        magnetic parameters (m0:p, mtheta:p, mphi:p), as well as polarization
584        information (up:theta, up:frac_i, up:frac_f).
585        """
586        # control parameters go first
587        control = [p for p in self.kernel_parameters if p.is_control]
588
589        # Gather entries such as name[n] into groups of the same n
590        dependent = {} # type: Dict[str, List[Parameter]]
591        dependent.update((p.id, []) for p in control)
592        for p in self.kernel_parameters:
593            if p.length_control is not None:
594                dependent[p.length_control].append(p)
595
596        # Gather entries such as name[4] into groups of the same length
597        fixed_length = {}  # type: Dict[int, List[Parameter]]
598        for p in self.kernel_parameters:
599            if p.length > 1 and p.length_control is None:
600                fixed_length.setdefault(p.length, []).append(p)
601
602        # Using the call_parameters table, we already have expanded forms
603        # for each of the vector parameters; put them in a lookup table
604        expanded_pars = dict((p.name, p) for p in self.call_parameters)
605
606        def append_group(name):
607            """add the named parameter, and related magnetic parameters if any"""
608            result.append(expanded_pars[name])
609            if is2d:
610                for tag in 'M0:', 'mtheta:', 'mphi:':
611                    if tag+name in expanded_pars:
612                        result.append(expanded_pars[tag+name])
613
614        # Gather the user parameters in order
615        result = control + self.COMMON
616        for p in self.kernel_parameters:
617            if not is2d and p.type in ('orientation', 'magnetic'):
618                pass
619            elif p.is_control:
620                pass # already added
621            elif p.length_control is not None:
622                table = dependent.get(p.length_control, [])
623                if table:
624                    # look up length from incoming parameters
625                    table_length = int(pars.get(p.length_control, p.length))
626                    del dependent[p.length_control] # first entry seen
627                    for k in range(1, table_length+1):
628                        for entry in table:
629                            append_group(entry.id+str(k))
630                else:
631                    pass # already processed all entries
632            elif p.length > 1:
633                table = fixed_length.get(p.length, [])
634                if table:
635                    table_length = p.length
636                    del fixed_length[p.length]
637                    for k in range(1, table_length+1):
638                        for entry in table:
639                            append_group(entry.id+str(k))
640                else:
641                    pass # already processed all entries
642            else:
643                append_group(p.id)
644
645        if is2d and 'up:angle' in expanded_pars:
646            result.extend([
647                expanded_pars['up:frac_i'],
648                expanded_pars['up:frac_f'],
649                expanded_pars['up:angle'],
650            ])
651
652        return result
653
654def isstr(x):
655    # type: (Any) -> bool
656    """
657    Return True if the object is a string.
658    """
659    # TODO: 2-3 compatible tests for str, including unicode strings
660    return isinstance(x, str)
661
662
663def _find_source_lines(model_info, kernel_module):
664    """
665    Identify the location of the C source inside the model definition file.
666
667    This code runs through the source of the kernel module looking for
668    lines that start with 'Iq', 'Iqxy' or 'form_volume'.  Clearly there are
669    all sorts of reasons why this might not work (e.g., code commented out
670    in a triple-quoted line block, code built using string concatenation,
671    or code defined in the branch of an 'if' block), but it should work
672    properly in the 95% case, and getting the incorrect line number will
673    be harmless.
674    """
675    # Check if we need line numbers at all
676    if callable(model_info.Iq):
677        return None
678
679    if (model_info.Iq is None
680        and model_info.Iqxy is None
681        and model_info.Imagnetic is None
682        and model_info.form_volume is None):
683        return
684
685    # find the defintion lines for the different code blocks
686    try:
687        source = inspect.getsource(kernel_module)
688    except IOError:
689        return
690    for k, v in enumerate(source.split('\n')):
691        if v.startswith('Imagnetic'):
692            model_info._Imagnetic_line = k+1
693        elif v.startswith('Iqxy'):
694            model_info._Iqxy_line = k+1
695        elif v.startswith('Iq'):
696            model_info._Iq_line = k+1
697        elif v.startswith('form_volume'):
698            model_info._form_volume_line = k+1
699
700
701def make_model_info(kernel_module):
702    # type: (module) -> ModelInfo
703    """
704    Extract the model definition from the loaded kernel module.
705
706    Fill in default values for parts of the module that are not provided.
707
708    Note: vectorized Iq and Iqxy functions will be created for python
709    models when the model is first called, not when the model is loaded.
710    """
711    info = ModelInfo()
712    #print("make parameter table", kernel_module.parameters)
713    parameters = make_parameter_table(getattr(kernel_module, 'parameters', []))
714    demo = expand_pars(parameters, getattr(kernel_module, 'demo', None))
715    filename = abspath(kernel_module.__file__)
716    kernel_id = splitext(basename(filename))[0]
717    name = getattr(kernel_module, 'name', None)
718    if name is None:
719        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
720
721    info.id = kernel_id  # string used to load the kernel
722    info.filename = abspath(kernel_module.__file__)
723    info.name = name
724    info.title = getattr(kernel_module, 'title', name+" model")
725    info.description = getattr(kernel_module, 'description', 'no description')
726    info.parameters = parameters
727    info.demo = demo
728    info.composition = None
729    info.docs = kernel_module.__doc__
730    info.category = getattr(kernel_module, 'category', None)
731    info.single = getattr(kernel_module, 'single', True)
732    info.structure_factor = getattr(kernel_module, 'structure_factor', False)
733    info.profile_axes = getattr(kernel_module, 'profile_axes', ['x', 'y'])
734    info.source = getattr(kernel_module, 'source', [])
735    # TODO: check the structure of the tests
736    info.tests = getattr(kernel_module, 'tests', [])
737    info.ER = getattr(kernel_module, 'ER', None) # type: ignore
738    info.VR = getattr(kernel_module, 'VR', None) # type: ignore
739    info.form_volume = getattr(kernel_module, 'form_volume', None) # type: ignore
740    info.Iq = getattr(kernel_module, 'Iq', None) # type: ignore
741    info.Iqxy = getattr(kernel_module, 'Iqxy', None) # type: ignore
742    info.Imagnetic = getattr(kernel_module, 'Imagnetic', None) # type: ignore
743    info.profile = getattr(kernel_module, 'profile', None) # type: ignore
744    info.sesans = getattr(kernel_module, 'sesans', None) # type: ignore
745
746    # multiplicity info
747    control_pars = [p.id for p in parameters.kernel_parameters if p.is_control]
748    default_control = control_pars[0] if control_pars else None
749    info.control = getattr(kernel_module, 'control', default_control)
750    info.hidden = getattr(kernel_module, 'hidden', None) # type: ignore
751
752    _find_source_lines(info, kernel_module)
753
754    return info
755
756class ModelInfo(object):
757    """
758    Interpret the model definition file, categorizing the parameters.
759
760    The module can be loaded with a normal python import statement if you
761    know which module you need, or with __import__('sasmodels.model.'+name)
762    if the name is in a string.
763
764    The structure should be mostly static, other than the delayed definition
765    of *Iq* and *Iqxy* if they need to be defined.
766    """
767    #: Full path to the file defining the kernel, if any.
768    filename = None         # type: Optiona[str]
769    #: Id of the kernel used to load it from the filesystem.
770    id = None               # type: str
771    #: Display name of the model, which defaults to the model id but with
772    #: capitalization of the parts so for example core_shell defaults to
773    #: "Core Shell".
774    name = None             # type: str
775    #: Short description of the model.
776    title = None            # type: str
777    #: Long description of the model.
778    description = None      # type: str
779    #: Model parameter table. Parameters are defined using a list of parameter
780    #: definitions, each of which is contains parameter name, units,
781    #: default value, limits, type and description.  See :class:`Parameter`
782    #: for details on the individual parameters.  The parameters are gathered
783    #: into a :class:`ParameterTable`, which provides various views into the
784    #: parameter list.
785    parameters = None       # type: ParameterTable
786    #: Demo parameters as a *parameter:value* map used as the default values
787    #: for :mod:`compare`.  Any parameters not set in *demo* will use the
788    #: defaults from the parameter table.  That means no polydispersity, and
789    #: in the case of multiplicity models, a minimal model with no interesting
790    #: scattering.
791    demo = None             # type: Dict[str, float]
792    #: Composition is None if this is an independent model, or it is a
793    #: tuple with comoposition type ('product' or 'misture') and a list of
794    #: :class:`ModelInfo` blocks for the composed objects.  This allows us
795    #: to rebuild a complete mixture or product model from the info block.
796    #: *composition* is not given in the model definition file, but instead
797    #: arises when the model is constructed using names such as
798    #: *sphere*hardsphere* or *cylinder+sphere*.
799    composition = None      # type: Optional[Tuple[str, List[ModelInfo]]]
800    #: Name of the control parameter for a variant model such as :ref:`rpa`.
801    #: The *control* parameter should appear in the parameter table, with
802    #: limits defined as *[CASES]*, for case names such as
803    #: *CASES = ["diblock copolymer", "triblock copolymer", ...]*.
804    #: This should give *limits=[[case1, case2, ...]]*, but the
805    #: model loader translates this to *limits=[0, len(CASES)-1]*, and adds
806    #: *choices=CASES* to the :class:`Parameter` definition. Note that
807    #: models can use a list of cases as a parameter without it being a
808    #: control parameter.  Either way, the parameter is sent to the model
809    #: evaluator as *float(choice_num)*, where choices are numbered from 0.
810    #: See also :attr:`hidden`.
811    control = None          # type: str
812    #: Different variants require different parameters.  In order to show
813    #: just the parameters needed for the variant selected by :attr:`control`,
814    #: you should provide a function *hidden(control) -> set(['a', 'b', ...])*
815    #: indicating which parameters need to be hidden.  For multiplicity
816    #: models, you need to use the complete name of the parameter, including
817    #: its number.  So for example, if variant "a" uses only *sld1* and *sld2*,
818    #: then *sld3*, *sld4* and *sld5* of multiplicity parameter *sld[5]*
819    #: should be in the hidden set.
820    hidden = None           # type: Optional[Callable[[int], Set[str]]]
821    #: Doc string from the top of the model file.  This should be formatted
822    #: using ReStructuredText format, with latex markup in ".. math"
823    #: environments, or in dollar signs.  This will be automatically
824    #: extracted to a .rst file by :func:`generate.make_docs`, then
825    #: converted to HTML or PDF by Sphinx.
826    docs = None             # type: str
827    #: Location of the model description in the documentation.  This takes the
828    #: form of "section" or "section:subsection".  So for example,
829    #: :ref:`porod` uses *category="shape-independent"* so it is in the
830    #: :ref:`Shape-independent` section whereas
831    #: :ref:`capped_cylinder` uses: *category="shape:cylinder"*, which puts
832    #: it in the :ref:`shape-cylinder` section.
833    category = None         # type: Optional[str]
834    #: True if the model can be computed accurately with single precision.
835    #: This is True by default, but models such as :ref:`bcc_paracrystal` set
836    #: it to False because they require double precision calculations.
837    single = None           # type: bool
838    #: True if the model is a structure factor used to model the interaction
839    #: between form factor models.  This will default to False if it is not
840    #: provided in the file.
841    structure_factor = None # type: bool
842    #: List of C source files used to define the model.  The source files
843    #: should define the *Iq* function, and possibly *Iqxy*, though a default
844    #: *Iqxy = Iq(sqrt(qx**2+qy**2)* will be created if no *Iqxy* is provided.
845    #: Files containing the most basic functions must appear first in the list,
846    #: followed by the files that use those functions.  Form factors are
847    #: indicated by providing a :attr:`ER` function.
848    source = None           # type: List[str]
849    #: The set of tests that must pass.  The format of the tests is described
850    #: in :mod:`model_test`.
851    tests = None            # type: List[TestCondition]
852    #: Returns the effective radius of the model given its volume parameters.
853    #: The presence of *ER* indicates that the model is a form factor model
854    #: that may be used together with a structure factor to form an implicit
855    #: multiplication model.
856    #:
857    #: The parameters to the *ER* function must be marked with type *volume*.
858    #: in the parameter table.  They will appear in the same order as they
859    #: do in the table.  The values passed to *ER* will be vectors, with one
860    #: value for each polydispersity condition.  For example, if the model
861    #: is polydisperse over both length and radius, then both length and
862    #: radius will have the same number of values in the vector, with one
863    #: value for each *length X radius*.  If only *radius* is polydisperse,
864    #: then the value for *length* will be repeated once for each value of
865    #: *radius*.  The *ER* function should return one effective radius for
866    #: each parameter set.  Multiplicity parameters will be received as
867    #: arrays, with one row per polydispersity condition.
868    ER = None               # type: Optional[Callable[[np.ndarray], np.ndarray]]
869    #: Returns the occupied volume and the total volume for each parameter set.
870    #: See :attr:`ER` for details on the parameters.
871    VR = None               # type: Optional[Callable[[np.ndarray], Tuple[np.ndarray, np.ndarray]]]
872    #: Returns the form volume for python-based models.  Form volume is needed
873    #: for volume normalization in the polydispersity integral.  If no
874    #: parameters are *volume* parameters, then form volume is not needed.
875    #: For C-based models, (with :attr:`sources` defined, or with :attr:`Iq`
876    #: defined using a string containing C code), form_volume must also be
877    #: C code, either defined as a string, or in the sources.
878    form_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
879    #: Returns *I(q, a, b, ...)* for parameters *a*, *b*, etc. defined
880    #: by the parameter table.  *Iq* can be defined as a python function, or
881    #: as a C function.  If it is defined in C, then set *Iq* to the body of
882    #: the C function, including the return statement.  This function takes
883    #: values for *q* and each of the parameters as separate *double* values
884    #: (which may be converted to float or long double by sasmodels).  All
885    #: source code files listed in :attr:`sources` will be loaded before the
886    #: *Iq* function is defined.  If *Iq* is not present, then sources should
887    #: define *static double Iq(double q, double a, double b, ...)* which
888    #: will return *I(q, a, b, ...)*.  Multiplicity parameters are sent as
889    #: pointers to doubles.  Constants in floating point expressions should
890    #: include the decimal point. See :mod:`generate` for more details.
891    Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
892    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
893    Iqxy = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
894    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
895    Imagnetic = None        # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
896    #: Returns a model profile curve *x, y*.  If *profile* is defined, this
897    #: curve will appear in response to the *Show* button in SasView.  Use
898    #: :attr:`profile_axes` to set the axis labels.  Note that *y* values
899    #: will be scaled by 1e6 before plotting.
900    profile = None          # type: Optional[Callable[[np.ndarray], None]]
901    #: Axis labels for the :attr:`profile` plot.  The default is *['x', 'y']*.
902    #: Only the *x* component is used for now.
903    profile_axes = None     # type: Tuple[str, str]
904    #: Returns *sesans(z, a, b, ...)* for models which can directly compute
905    #: the SESANS correlation function.  Note: not currently implemented.
906    sesans = None           # type: Optional[Callable[[np.ndarray], np.ndarray]]
907
908    # line numbers within the python file for bits of C source, if defined
909    # NB: some compilers fail with a "#line 0" directive, so default to 1.
910    _Imagnetic_line = 1
911    _Iqxy_line = 1
912    _Iq_line = 1
913    _form_volume_line = 1
914
915
916    def __init__(self):
917        # type: () -> None
918        pass
919
920    def get_hidden_parameters(self, control):
921        """
922        Returns the set of hidden parameters for the model.  *control* is the
923        value of the control parameter.  Note that multiplicity models have
924        an implicit control parameter, which is the parameter that controls
925        the multiplicity.
926        """
927        if self.hidden is not None:
928            hidden = self.hidden(control)
929        else:
930            controls = [p for p in self.parameters.kernel_parameters
931                        if p.is_control]
932            if len(controls) != 1:
933                raise ValueError("more than one control parameter")
934            hidden = set(p.id+str(k)
935                         for p in self.parameters.kernel_parameters
936                         for k in range(control+1, p.length+1)
937                         if p.length > 1)
938        return hidden
Note: See TracBrowser for help on using the repository browser.