source: sasmodels/sasmodels/generate.py @ 4d76711

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

adjust interface to sasview

  • Property mode set to 100644
File size: 31.5 KB
Line 
1"""
2SAS model constructor.
3
4Small angle scattering models are defined by a set of kernel functions:
5
6    *Iq(q, p1, p2, ...)* returns the scattering at q for a form with
7    particular dimensions averaged over all orientations.
8
9    *Iqxy(qx, qy, p1, p2, ...)* returns the scattering at qx, qy for a form
10    with particular dimensions for a single orientation.
11
12    *Imagnetic(qx, qy, result[], p1, p2, ...)* returns the scattering for the
13    polarized neutron spin states (up-up, up-down, down-up, down-down) for
14    a form with particular dimensions for a single orientation.
15
16    *form_volume(p1, p2, ...)* returns the volume of the form with particular
17    dimension.
18
19    *ER(p1, p2, ...)* returns the effective radius of the form with
20    particular dimensions.
21
22    *VR(p1, p2, ...)* returns the volume ratio for core-shell style forms.
23
24These functions are defined in a kernel module .py script and an associated
25set of .c files.  The model constructor will use them to create models with
26polydispersity across volume and orientation parameters, and provide
27scale and background parameters for each model.
28
29*Iq*, *Iqxy*, *Imagnetic* and *form_volume* should be stylized C-99
30functions written for OpenCL.  All functions need prototype declarations
31even if the are defined before they are used.  OpenCL does not support
32*#include* preprocessor directives, so instead the list of includes needs
33to be given as part of the metadata in the kernel module definition.
34The included files should be listed using a path relative to the kernel
35module, or if using "lib/file.c" if it is one of the standard includes
36provided with the sasmodels source.  The includes need to be listed in
37order so that functions are defined before they are used.
38
39Floating point values should be declared as *double*.  For single precision
40calculations, *double* will be replaced by *float*.  The single precision
41conversion will also tag floating point constants with "f" to make them
42single precision constants.  When using integral values in floating point
43expressions, they should be expressed as floating point values by including
44a decimal point.  This includes 0., 1. and 2.
45
46OpenCL has a *sincos* function which can improve performance when both
47the *sin* and *cos* values are needed for a particular argument.  Since
48this function does not exist in C99, all use of *sincos* should be
49replaced by the macro *SINCOS(value, sn, cn)* where *sn* and *cn* are
50previously declared *double* variables.  When compiled for systems without
51OpenCL, *SINCOS* will be replaced by *sin* and *cos* calls.   If *value* is
52an expression, it will appear twice in this case; whether or not it will be
53evaluated twice depends on the quality of the compiler.
54
55If the input parameters are invalid, the scattering calculator should
56return a negative number. Particularly with polydispersity, there are
57some sets of shape parameters which lead to nonsensical forms, such
58as a capped cylinder where the cap radius is smaller than the
59cylinder radius.  The polydispersity calculation will ignore these points,
60effectively chopping the parameter weight distributions at the boundary
61of the infeasible region.  The resulting scattering will be set to
62background.  This will work correctly even when polydispersity is off.
63
64*ER* and *VR* are python functions which operate on parameter vectors.
65The constructor code will generate the necessary vectors for computing
66them with the desired polydispersity.
67
68The available kernel parameters are defined as a list, with each parameter
69defined as a sublist with the following elements:
70
71    *name* is the name that will be used in the call to the kernel
72    function and the name that will be displayed to the user.  Names
73    should be lower case, with words separated by underscore.  If
74    acronyms are used, the whole acronym should be upper case.
75
76    *units* should be one of *degrees* for angles, *Ang* for lengths,
77    *1e-6/Ang^2* for SLDs.
78
79    *default value* will be the initial value for  the model when it
80    is selected, or when an initial value is not otherwise specified.
81
82    *limits = [lb, ub]* are the hard limits on the parameter value, used to
83    limit the polydispersity density function.  In the fit, the parameter limits
84    given to the fit are the limits  on the central value of the parameter.
85    If there is polydispersity, it will evaluate parameter values outside
86    the fit limits, but not outside the hard limits specified in the model.
87    If there are no limits, use +/-inf imported from numpy.
88
89    *type* indicates how the parameter will be used.  "volume" parameters
90    will be used in all functions.  "orientation" parameters will be used
91    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
92    *Imagnetic* only.  If *type* is the empty string, the parameter will
93    be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
94    can automatically be promoted to magnetic parameters, each of which
95    will have a magnitude and a direction, which may be different from
96    other sld parameters.
97
98    *description* is a short description of the parameter.  This will
99    be displayed in the parameter table and used as a tool tip for the
100    parameter value in the user interface.
101
102The kernel module must set variables defining the kernel meta data:
103
104    *id* is an implicit variable formed from the filename.  It will be
105    a valid python identifier, and will be used as the reference into
106    the html documentation, with '_' replaced by '-'.
107
108    *name* is the model name as displayed to the user.  If it is missing,
109    it will be constructed from the id.
110
111    *title* is a short description of the model, suitable for a tool tip,
112    or a one line model summary in a table of models.
113
114    *description* is an extended description of the model to be displayed
115    while the model parameters are being edited.
116
117    *parameters* is the list of parameters.  Parameters in the kernel
118    functions must appear in the same order as they appear in the
119    parameters list.  Two additional parameters, *scale* and *background*
120    are added to the beginning of the parameter list.  They will show up
121    in the documentation as model parameters, but they are never sent to
122    the kernel functions.  Note that *effect_radius* and *volfraction*
123    must occur first in structure factor calculations.
124
125    *category* is the default category for the model.  The category is
126    two level structure, with the form "group:section", indicating where
127    in the manual the model will be located.  Models are alphabetical
128    within their section.
129
130    *source* is the list of C-99 source files that must be joined to
131    create the OpenCL kernel functions.  The files defining the functions
132    need to be listed before the files which use the functions.
133
134    *ER* is a python function defining the effective radius.  If it is
135    not present, the effective radius is 0.
136
137    *VR* is a python function defining the volume ratio.  If it is not
138    present, the volume ratio is 1.
139
140    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
141    C source code for the body of the volume, Iq, and Iqxy functions
142    respectively.  These can also be defined in the last source file.
143
144    *Iq* and *Iqxy* also be instead be python functions defining the
145    kernel.  If they are marked as *Iq.vectorized = True* then the
146    kernel is passed the entire *q* vector at once, otherwise it is
147    passed values one *q* at a time.  The performance improvement of
148    this step is significant.
149
150    *demo* is a dictionary of parameter=value defining a set of
151    parameters to use by default when *compare* is called.  Any
152    parameter not set in *demo* gets the initial value from the
153    parameter list.  *demo* is mostly needed to set the default
154    polydispersity values for tests.
155
156An *model_info* dictionary is constructed from the kernel meta data and
157returned to the caller.
158
159The model evaluator, function call sequence consists of q inputs and the return vector,
160followed by the loop value/weight vector, followed by the values for
161the non-polydisperse parameters, followed by the lengths of the
162polydispersity loops.  To construct the call for 1D models, the
163categories *fixed-1d* and *pd-1d* list the names of the parameters
164of the non-polydisperse and the polydisperse parameters respectively.
165Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
166The *pd-rel* category is a set of those parameters which give
167polydispersitiy as a portion of the value (so a 10% length dispersity
168would use a polydispersity value of 0.1) rather than absolute
169dispersity such as an angle plus or minus 15 degrees.
170
171The *volume* category lists the volume parameters in order for calls
172to volume within the kernel (used for volume normalization) and for
173calls to ER and VR for effective radius and volume ratio respectively.
174
175The *orientation* and *magnetic* categories list the orientation and
176magnetic parameters.  These are used by the sasview interface.  The
177blank category is for parameters such as scale which don't have any
178other marking.
179
180The doc string at the start of the kernel module will be used to
181construct the model documentation web pages.  Embedded figures should
182appear in the subdirectory "img" beside the model definition, and tagged
183with the kernel module name to avoid collision with other models.  Some
184file systems are case-sensitive, so only use lower case characters for
185file names and extensions.
186
187
188The function :func:`make` loads the metadata from the module and returns
189the kernel source.  The function :func:`make_doc` extracts the doc string
190and adds the parameter table to the top.  The function :func:`model_sources`
191returns a list of files required by the model.
192
193Code follows the C99 standard with the following extensions and conditions::
194
195    M_PI_180 = pi/180
196    M_4PI_3 = 4pi/3
197    square(x) = x*x
198    cube(x) = x*x*x
199    sinc(x) = sin(x)/x, with sin(0)/0 -> 1
200    all double precision constants must include the decimal point
201    all double declarations may be converted to half, float, or long double
202    FLOAT_SIZE is the number of bytes in the converted variables
203"""
204from __future__ import print_function
205
206#TODO: identify model files which have changed since loading and reload them.
207#TODO: determine which functions are useful outside of generate
208#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
209
210import sys
211from os.path import abspath, dirname, join as joinpath, exists, basename, \
212    splitext
213import re
214import string
215import warnings
216from collections import namedtuple
217
218import numpy as np
219
220from .custom import load_custom_kernel_module
221
222PARAMETER_FIELDS = ['name', 'units', 'default', 'limits', 'type', 'description']
223Parameter = namedtuple('Parameter', PARAMETER_FIELDS)
224
225C_KERNEL_TEMPLATE_PATH = joinpath(dirname(__file__), 'kernel_template.c')
226
227F16 = np.dtype('float16')
228F32 = np.dtype('float32')
229F64 = np.dtype('float64')
230try:  # CRUFT: older numpy does not support float128
231    F128 = np.dtype('float128')
232except TypeError:
233    F128 = None
234
235# Scale and background, which are parameters common to every form factor
236COMMON_PARAMETERS = [
237    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
238    ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],
239    ]
240
241# Conversion from units defined in the parameter table for each model
242# to units displayed in the sphinx documentation.
243RST_UNITS = {
244    "Ang": "|Ang|",
245    "1/Ang": "|Ang^-1|",
246    "1/Ang^2": "|Ang^-2|",
247    "1e-6/Ang^2": "|1e-6Ang^-2|",
248    "degrees": "degree",
249    "1/cm": "|cm^-1|",
250    "Ang/cm": "|Ang*cm^-1|",
251    "g/cm3": "|g/cm^3|",
252    "mg/m2": "|mg/m^2|",
253    "": "None",
254    }
255
256# Headers for the parameters tables in th sphinx documentation
257PARTABLE_HEADERS = [
258    "Parameter",
259    "Description",
260    "Units",
261    "Default value",
262    ]
263
264# Minimum width for a default value (this is shorter than the column header
265# width, so will be ignored).
266PARTABLE_VALUE_WIDTH = 10
267
268# Documentation header for the module, giving the model name, its short
269# description and its parameter table.  The remainder of the doc comes
270# from the module docstring.
271DOC_HEADER = """.. _%(id)s:
272
273%(name)s
274=======================================================
275
276%(title)s
277
278%(parameters)s
279
280%(returns)s
281
282%(docs)s
283"""
284
285def format_units(units):
286    """
287    Convert units into ReStructured Text format.
288    """
289    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
290
291def make_partable(pars):
292    """
293    Generate the parameter table to include in the sphinx documentation.
294    """
295    column_widths = [
296        max(len(p.name) for p in pars),
297        max(len(p.description) for p in pars),
298        max(len(format_units(p.units)) for p in pars),
299        PARTABLE_VALUE_WIDTH,
300        ]
301    column_widths = [max(w, len(h))
302                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
303
304    sep = " ".join("="*w for w in column_widths)
305    lines = [
306        sep,
307        " ".join("%-*s" % (w, h)
308                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
309        sep,
310        ]
311    for p in pars:
312        lines.append(" ".join([
313            "%-*s" % (column_widths[0], p.name),
314            "%-*s" % (column_widths[1], p.description),
315            "%-*s" % (column_widths[2], format_units(p.units)),
316            "%*g" % (column_widths[3], p.default),
317            ]))
318    lines.append(sep)
319    return "\n".join(lines)
320
321def _search(search_path, filename):
322    """
323    Find *filename* in *search_path*.
324
325    Raises ValueError if file does not exist.
326    """
327    for path in search_path:
328        target = joinpath(path, filename)
329        if exists(target):
330            return target
331    raise ValueError("%r not found in %s" % (filename, search_path))
332
333def model_sources(model_info):
334    """
335    Return a list of the sources file paths for the module.
336    """
337    search_path = [dirname(model_info['filename']),
338                   abspath(joinpath(dirname(__file__), 'models'))]
339    return [_search(search_path, f) for f in model_info['source']]
340
341# Pragmas for enable OpenCL features.  Be sure to protect them so that they
342# still compile even if OpenCL is not present.
343_F16_PRAGMA = """\
344#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp16)
345#  pragma OPENCL EXTENSION cl_khr_fp16: enable
346#endif
347"""
348
349_F64_PRAGMA = """\
350#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp64)
351#  pragma OPENCL EXTENSION cl_khr_fp64: enable
352#endif
353"""
354
355def convert_type(source, dtype):
356    """
357    Convert code from double precision to the desired type.
358
359    Floating point constants are tagged with 'f' for single precision or 'L'
360    for long double precision.
361    """
362    if dtype == F16:
363        fbytes = 2
364        source = _F16_PRAGMA + _convert_type(source, "half", "f")
365    elif dtype == F32:
366        fbytes = 4
367        source = _convert_type(source, "float", "f")
368    elif dtype == F64:
369        fbytes = 8
370        source = _F64_PRAGMA + source  # Source is already double
371    elif dtype == F128:
372        fbytes = 16
373        source = _convert_type(source, "long double", "L")
374    else:
375        raise ValueError("Unexpected dtype in source conversion: %s"%dtype)
376    return ("#define FLOAT_SIZE %d\n"%fbytes)+source
377
378
379def _convert_type(source, type_name, constant_flag):
380    """
381    Replace 'double' with *type_name* in *source*, tagging floating point
382    constants with *constant_flag*.
383    """
384    # Convert double keyword to float/long double/half.
385    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
386    # Assume complex numbers are represented as cdouble which is typedef'd
387    # to double2.
388    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
389                    r'\1%s\2'%type_name, source)
390    # Convert floating point constants to single by adding 'f' to the end,
391    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
392    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
393                    r'\g<0>%s'%constant_flag, source)
394    return source
395
396
397def kernel_name(model_info, is_2d):
398    """
399    Name of the exported kernel symbol.
400    """
401    return model_info['name'] + "_" + ("Iqxy" if is_2d else "Iq")
402
403
404def indent(s, depth):
405    """
406    Indent a string of text with *depth* additional spaces on each line.
407    """
408    spaces = " "*depth
409    sep = "\n" + spaces
410    return spaces + sep.join(s.split("\n"))
411
412
413LOOP_OPEN = """\
414for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
415  const double %(name)s = loops[2*(%(name)s_i%(offset)s)];
416  const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\
417"""
418def build_polydispersity_loops(pd_pars):
419    """
420    Build polydispersity loops
421
422    Returns loop opening and loop closing
423    """
424    depth = 4
425    offset = ""
426    loop_head = []
427    loop_end = []
428    for name in pd_pars:
429        subst = {'name': name, 'offset': offset}
430        loop_head.append(indent(LOOP_OPEN % subst, depth))
431        loop_end.insert(0, (" "*depth) + "}")
432        offset += '+N' + name
433        depth += 2
434    return "\n".join(loop_head), "\n".join(loop_end)
435
436C_KERNEL_TEMPLATE = None
437def make_source(model_info):
438    """
439    Generate the OpenCL/ctypes kernel from the module info.
440
441    Uses source files found in the given search path.
442    """
443    if callable(model_info['Iq']):
444        return None
445
446    # TODO: need something other than volume to indicate dispersion parameters
447    # No volume normalization despite having a volume parameter.
448    # Thickness is labelled a volume in order to trigger polydispersity.
449    # May want a separate dispersion flag, or perhaps a separate category for
450    # disperse, but not volume.  Volume parameters also use relative values
451    # for the distribution rather than the absolute values used by angular
452    # dispersion.  Need to be careful that necessary parameters are available
453    # for computing volume even if we allow non-disperse volume parameters.
454
455    # Load template
456    global C_KERNEL_TEMPLATE
457    if C_KERNEL_TEMPLATE is None:
458        with open(C_KERNEL_TEMPLATE_PATH) as fid:
459            C_KERNEL_TEMPLATE = fid.read()
460
461    # Load additional sources
462    source = [open(f).read() for f in model_sources(model_info)]
463
464    # Prepare defines
465    defines = []
466    partype = model_info['partype']
467    pd_1d = partype['pd-1d']
468    pd_2d = partype['pd-2d']
469    fixed_1d = partype['fixed-1d']
470    fixed_2d = partype['fixed-1d']
471
472    iq_parameters = [p.name
473                     for p in model_info['parameters'][2:]  # skip scale, background
474                     if p.name in set(fixed_1d + pd_1d)]
475    iqxy_parameters = [p.name
476                       for p in model_info['parameters'][2:]  # skip scale, background
477                       if p.name in set(fixed_2d + pd_2d)]
478    volume_parameters = [p.name
479                         for p in model_info['parameters']
480                         if p.type == 'volume']
481
482    # Fill in defintions for volume parameters
483    if volume_parameters:
484        defines.append(('VOLUME_PARAMETERS',
485                        ','.join(volume_parameters)))
486        defines.append(('VOLUME_WEIGHT_PRODUCT',
487                        '*'.join(p + '_w' for p in volume_parameters)))
488
489    # Generate form_volume function from body only
490    if model_info['form_volume'] is not None:
491        if volume_parameters:
492            vol_par_decl = ', '.join('double ' + p for p in volume_parameters)
493        else:
494            vol_par_decl = 'void'
495        defines.append(('VOLUME_PARAMETER_DECLARATIONS',
496                        vol_par_decl))
497        fn = """\
498double form_volume(VOLUME_PARAMETER_DECLARATIONS);
499double form_volume(VOLUME_PARAMETER_DECLARATIONS) {
500    %(body)s
501}
502""" % {'body':model_info['form_volume']}
503        source.append(fn)
504
505    # Fill in definitions for Iq parameters
506    defines.append(('IQ_KERNEL_NAME', model_info['name'] + '_Iq'))
507    defines.append(('IQ_PARAMETERS', ', '.join(iq_parameters)))
508    if fixed_1d:
509        defines.append(('IQ_FIXED_PARAMETER_DECLARATIONS',
510                        ', \\\n    '.join('const double %s' % p for p in fixed_1d)))
511    if pd_1d:
512        defines.append(('IQ_WEIGHT_PRODUCT',
513                        '*'.join(p + '_w' for p in pd_1d)))
514        defines.append(('IQ_DISPERSION_LENGTH_DECLARATIONS',
515                        ', \\\n    '.join('const int N%s' % p for p in pd_1d)))
516        defines.append(('IQ_DISPERSION_LENGTH_SUM',
517                        '+'.join('N' + p for p in pd_1d)))
518        open_loops, close_loops = build_polydispersity_loops(pd_1d)
519        defines.append(('IQ_OPEN_LOOPS',
520                        open_loops.replace('\n', ' \\\n')))
521        defines.append(('IQ_CLOSE_LOOPS',
522                        close_loops.replace('\n', ' \\\n')))
523    if model_info['Iq'] is not None:
524        defines.append(('IQ_PARAMETER_DECLARATIONS',
525                        ', '.join('double ' + p for p in iq_parameters)))
526        fn = """\
527double Iq(double q, IQ_PARAMETER_DECLARATIONS);
528double Iq(double q, IQ_PARAMETER_DECLARATIONS) {
529    %(body)s
530}
531""" % {'body':model_info['Iq']}
532        source.append(fn)
533
534    # Fill in definitions for Iqxy parameters
535    defines.append(('IQXY_KERNEL_NAME', model_info['name'] + '_Iqxy'))
536    defines.append(('IQXY_PARAMETERS', ', '.join(iqxy_parameters)))
537    if fixed_2d:
538        defines.append(('IQXY_FIXED_PARAMETER_DECLARATIONS',
539                        ', \\\n    '.join('const double %s' % p for p in fixed_2d)))
540    if pd_2d:
541        defines.append(('IQXY_WEIGHT_PRODUCT',
542                        '*'.join(p + '_w' for p in pd_2d)))
543        defines.append(('IQXY_DISPERSION_LENGTH_DECLARATIONS',
544                        ', \\\n    '.join('const int N%s' % p for p in pd_2d)))
545        defines.append(('IQXY_DISPERSION_LENGTH_SUM',
546                        '+'.join('N' + p for p in pd_2d)))
547        open_loops, close_loops = build_polydispersity_loops(pd_2d)
548        defines.append(('IQXY_OPEN_LOOPS',
549                        open_loops.replace('\n', ' \\\n')))
550        defines.append(('IQXY_CLOSE_LOOPS',
551                        close_loops.replace('\n', ' \\\n')))
552    if model_info['Iqxy'] is not None:
553        defines.append(('IQXY_PARAMETER_DECLARATIONS',
554                        ', '.join('double ' + p for p in iqxy_parameters)))
555        fn = """\
556double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS);
557double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS) {
558    %(body)s
559}
560""" % {'body':model_info['Iqxy']}
561        source.append(fn)
562
563    # Need to know if we have a theta parameter for Iqxy; it is not there
564    # for the magnetic sphere model, for example, which has a magnetic
565    # orientation but no shape orientation.
566    if 'theta' in pd_2d:
567        defines.append(('IQXY_HAS_THETA', '1'))
568
569    #for d in defines: print(d)
570    defines = '\n'.join('#define %s %s' % (k, v) for k, v in defines)
571    sources = '\n\n'.join(source)
572    return C_KERNEL_TEMPLATE % {
573        'DEFINES': defines,
574        'SOURCES': sources,
575        }
576
577def categorize_parameters(pars):
578    """
579    Build parameter categories out of the the parameter definitions.
580
581    Returns a dictionary of categories.
582
583    Note: these categories are subject to change, depending on the needs of
584    the UI and the needs of the kernel calling function.
585
586    The categories are as follows:
587
588    * *volume* list of volume parameter names
589    * *orientation* list of orientation parameters
590    * *magnetic* list of magnetic parameters
591    * *<empty string>* list of parameters that have no type info
592
593    Each parameter is in one and only one category.
594
595    The following derived categories are created:
596
597    * *fixed-1d* list of non-polydisperse parameters for 1D models
598    * *pd-1d* list of polydisperse parameters for 1D models
599    * *fixed-2d* list of non-polydisperse parameters for 2D models
600    * *pd-d2* list of polydisperse parameters for 2D models
601    """
602    partype = {
603        'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], '': [],
604        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
605        'pd-rel': set(),
606    }
607
608    for p in pars:
609        if p.type == 'volume':
610            partype['pd-1d'].append(p.name)
611            partype['pd-2d'].append(p.name)
612            partype['pd-rel'].add(p.name)
613        elif p.type == 'magnetic':
614            partype['fixed-2d'].append(p.name)
615        elif p.type == 'orientation':
616            partype['pd-2d'].append(p.name)
617        elif p.type in ('', 'sld'):
618            partype['fixed-1d'].append(p.name)
619            partype['fixed-2d'].append(p.name)
620        else:
621            raise ValueError("unknown parameter type %r" % p.type)
622        partype[p.type].append(p.name)
623
624    return partype
625
626def process_parameters(model_info):
627    """
628    Process parameter block, precalculating parameter details.
629    """
630    # convert parameters into named tuples
631    for p in model_info['parameters']:
632        if p[4] == '' and (p[0].startswith('sld') or p[0].endswith('sld')):
633            p[4] = 'sld'
634            # TODO: make sure all models explicitly label their sld parameters
635            #raise ValueError("%s.%s needs to be explicitly set to type 'sld'" %(model_info['id'], p[0]))
636
637    pars = [Parameter(*p) for p in model_info['parameters']]
638    # Fill in the derived attributes
639    model_info['parameters'] = pars
640    partype = categorize_parameters(pars)
641    model_info['limits'] = dict((p.name, p.limits) for p in pars)
642    model_info['partype'] = partype
643    model_info['defaults'] = dict((p.name, p.default) for p in pars)
644    if model_info.get('demo', None) is None:
645        model_info['demo'] = model_info['defaults']
646    model_info['has_2d'] = partype['orientation'] or partype['magnetic']
647
648
649def load_kernel_module(model_name):
650    if model_name.endswith('.py'):
651        kernel_module = load_custom_kernel_module(model_name)
652    else:
653        from sasmodels import models
654        __import__('sasmodels.models.'+model_name)
655        kernel_module = getattr(models, model_name, None)
656    return kernel_module
657
658
659def make_model_info(kernel_module):
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 *model_info* structure contains the following fields:
668
669    * *id* is the id of the kernel
670    * *name* is the display name of the kernel
671    * *filename* is the full path to the module defining the file (if any)
672    * *title* is a short description of the kernel
673    * *description* is a long description of the kernel (this doesn't seem
674      very useful since the Help button on the model page brings you directly
675      to the documentation page)
676    * *docs* is the docstring from the module.  Use :func:`make_doc` to
677    * *category* specifies the model location in the docs
678    * *parameters* is the model parameter table
679    * *single* is True if the model allows single precision
680    * *structure_factor* is True if the model is useable in a product
681    * *variant_info* contains the information required to select between
682      model variants (e.g., the list of cases) or is None if there are no
683      model variants
684    * *defaults* is the *{parameter: value}* table built from the parameter
685      description table.
686    * *limits* is the *{parameter: [min, max]}* table built from the
687      parameter description table.
688    * *partypes* categorizes the model parameters. See
689      :func:`categorize_parameters` for details.
690    * *demo* contains the *{parameter: value}* map used in compare (and maybe
691      for the demo plot, if plots aren't set up to use the default values).
692      If *demo* is not given in the file, then the default values will be used.
693    * *tests* is a set of tests that must pass
694    * *source* is the list of library files to include in the C model build
695    * *Iq*, *Iqxy*, *form_volume*, *ER*, *VR* and *sesans* are python functions
696      implementing the kernel for the module, or None if they are not
697      defined in python
698    * *composition* is None if the model is independent, otherwise it is a
699      tuple with composition type ('product' or 'mixture') and a list of
700      *model_info* blocks for the composition objects.  This allows us to
701      build complete product and mixture models from just the info.
702
703    """
704    # TODO: maybe turn model_info into a class ModelDefinition
705    parameters = COMMON_PARAMETERS + kernel_module.parameters
706    filename = abspath(kernel_module.__file__)
707    kernel_id = splitext(basename(filename))[0]
708    name = getattr(kernel_module, 'name', None)
709    if name is None:
710        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
711    model_info = dict(
712        id=kernel_id,  # string used to load the kernel
713        filename=abspath(kernel_module.__file__),
714        name=name,
715        title=kernel_module.title,
716        description=kernel_module.description,
717        parameters=parameters,
718        composition=None,
719        docs=kernel_module.__doc__,
720        category=getattr(kernel_module, 'category', None),
721        single=getattr(kernel_module, 'single', True),
722        structure_factor=getattr(kernel_module, 'structure_factor', False),
723        variant_info=getattr(kernel_module, 'invariant_info', None),
724        demo=getattr(kernel_module, 'demo', None),
725        source=getattr(kernel_module, 'source', []),
726        tests=getattr(kernel_module, 'tests', []),
727        )
728    process_parameters(model_info)
729    # Check for optional functions
730    functions = "ER VR form_volume Iq Iqxy shape sesans".split()
731    model_info.update((k, getattr(kernel_module, k, None)) for k in functions)
732    return model_info
733
734section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
735                            %re.escape(string.punctuation))
736def _convert_section_titles_to_boldface(lines):
737    """
738    Do the actual work of identifying and converting section headings.
739    """
740    prior = None
741    for line in lines:
742        if prior is None:
743            prior = line
744        elif section_marker.match(line):
745            if len(line) >= len(prior):
746                yield "".join(("**", prior, "**"))
747                prior = None
748            else:
749                yield prior
750                prior = line
751        else:
752            yield prior
753            prior = line
754    if prior is not None:
755        yield prior
756
757def convert_section_titles_to_boldface(s):
758    """
759    Use explicit bold-face rather than section headings so that the table of
760    contents is not polluted with section names from the model documentation.
761
762    Sections are identified as the title line followed by a line of punctuation
763    at least as long as the title line.
764    """
765    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
766
767def make_doc(model_info):
768    """
769    Return the documentation for the model.
770    """
771    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
772    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
773    docs = convert_section_titles_to_boldface(model_info['docs'])
774    subst = dict(id=model_info['id'].replace('_', '-'),
775                 name=model_info['name'],
776                 title=model_info['title'],
777                 parameters=make_partable(model_info['parameters']),
778                 returns=Sq_units if model_info['structure_factor'] else Iq_units,
779                 docs=docs)
780    return DOC_HEADER % subst
781
782
783def demo_time():
784    """
785    Show how long it takes to process a model.
786    """
787    from .models import cylinder
788    import datetime
789    tic = datetime.datetime.now()
790    make_source(make_model_info(cylinder))
791    toc = (datetime.datetime.now() - tic).total_seconds()
792    print("time: %g"%toc)
793
794def main():
795    """
796    Program which prints the source produced by the model.
797    """
798    if len(sys.argv) <= 1:
799        print("usage: python -m sasmodels.generate modelname")
800    else:
801        name = sys.argv[1]
802        kernel_module = load_kernel_module(name)
803        model_info = make_model_info(kernel_module)
804        source = make_source(model_info)
805        print(source)
806
807if __name__ == "__main__":
808    main()
Note: See TracBrowser for help on using the repository browser.