source: sasmodels/sasmodels/modelinfo.py @ 32e3c9b

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

dll version of magnetic sld

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