source: sasmodels/sasmodels/generate.py @ b15849c

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

Improve accuracy of sph_j1c to 7 digits single, 14 digits double

  • Property mode set to 100644
File size: 27.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    [*lb*, *ub*] are the hard limits on the parameter value, used to limit
83    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*.
94
95    *description* is a short description of the parameter.  This will
96    be displayed in the parameter table and used as a tool tip for the
97    parameter value in the user interface.
98
99The kernel module must set variables defining the kernel meta data:
100
101    *id* is an implicit variable formed from the filename.  It will be
102    a valid python identifier, and will be used as the reference into
103    the html documentation, with '_' replaced by '-'.
104
105    *name* is the model name as displayed to the user.  If it is missing,
106    it will be constructed from the id.
107
108    *title* is a short description of the model, suitable for a tool tip,
109    or a one line model summary in a table of models.
110
111    *description* is an extended description of the model to be displayed
112    while the model parameters are being edited.
113
114    *parameters* is the list of parameters.  Parameters in the kernel
115    functions must appear in the same order as they appear in the
116    parameters list.  Two additional parameters, *scale* and *background*
117    are added to the beginning of the parameter list.  They will show up
118    in the documentation as model parameters, but they are never sent to
119    the kernel functions.
120
121    *category* is the default category for the model.  Models in the
122    *structure-factor* category do not have *scale* and *background*
123    added.
124
125    *source* is the list of C-99 source files that must be joined to
126    create the OpenCL kernel functions.  The files defining the functions
127    need to be listed before the files which use the functions.
128
129    *ER* is a python function defining the effective radius.  If it is
130    not present, the effective radius is 0.
131
132    *VR* is a python function defining the volume ratio.  If it is not
133    present, the volume ratio is 1.
134
135    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
136    C source code for the body of the volume, Iq, and Iqxy functions
137    respectively.  These can also be defined in the last source file.
138
139    *Iq* and *Iqxy* also be instead be python functions defining the
140    kernel.  If they are marked as *Iq.vectorized = True* then the
141    kernel is passed the entire *q* vector at once, otherwise it is
142    passed values one *q* at a time.  The performance improvement of
143    this step is significant.
144
145    *demo* is a dictionary of parameter=value defining a set of
146    parameters to use by default when *compare* is called.  Any
147    parameter not set in *demo* gets the initial value from the
148    parameter list.  *demo* is mostly needed to set the default
149    polydispersity values for tests.
150
151    *oldname* is the name of the model in sasview before sasmodels
152    was split into its own package, and *oldpars* is a dictionary
153    of *parameter: old_parameter* pairs defining the new names for
154    the parameters.  This is used by *compare* to check the values
155    of the new model against the values of the old model before
156    you are ready to add the new model to sasmodels.
157
158
159An *info* dictionary is constructed from the kernel meta data and
160returned to the caller.
161
162The model evaluator, function call sequence consists of q inputs and the return vector,
163followed by the loop value/weight vector, followed by the values for
164the non-polydisperse parameters, followed by the lengths of the
165polydispersity loops.  To construct the call for 1D models, the
166categories *fixed-1d* and *pd-1d* list the names of the parameters
167of the non-polydisperse and the polydisperse parameters respectively.
168Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
169The *pd-rel* category is a set of those parameters which give
170polydispersitiy as a portion of the value (so a 10% length dispersity
171would use a polydispersity value of 0.1) rather than absolute
172dispersity such as an angle plus or minus 15 degrees.
173
174The *volume* category lists the volume parameters in order for calls
175to volume within the kernel (used for volume normalization) and for
176calls to ER and VR for effective radius and volume ratio respectively.
177
178The *orientation* and *magnetic* categories list the orientation and
179magnetic parameters.  These are used by the sasview interface.  The
180blank category is for parameters such as scale which don't have any
181other marking.
182
183The doc string at the start of the kernel module will be used to
184construct the model documentation web pages.  Embedded figures should
185appear in the subdirectory "img" beside the model definition, and tagged
186with the kernel module name to avoid collision with other models.  Some
187file systems are case-sensitive, so only use lower case characters for
188file names and extensions.
189
190
191The function :func:`make` loads the metadata from the module and returns
192the kernel source.  The function :func:`doc` extracts the doc string
193and adds the parameter table to the top.  The function :func:`model_sources`
194returns a list of files required by the model.
195
196Code follows the C99 standard with the following extensions and conditions::
197
198    M_PI_180 = pi/180
199    M_4PI_3 = 4pi/3
200    square(x) = x*x
201    cube(x) = x*x*x
202    sinc(x) = sin(x)/x, with sin(0)/0 -> 1
203    all double precision constants must include the decimal point
204    all double declarations may be converted to half, float, or long double
205    FLOAT_SIZE is the number of bytes in the converted variables
206"""
207from __future__ import print_function
208
209# TODO: identify model files which have changed since loading and reload them.
210
211import sys
212from os.path import abspath, dirname, join as joinpath, exists, basename, \
213    splitext
214import re
215import string
216
217import numpy as np
218
219#__all__ = ["make", "doc", "model_sources", "convert_type"]
220
221C_KERNEL_TEMPLATE_PATH = joinpath(dirname(__file__), 'kernel_template.c')
222
223F16 = np.dtype('float16')
224F32 = np.dtype('float32')
225F64 = np.dtype('float64')
226try:  # CRUFT: older numpy does not support float128
227    F128 = np.dtype('float128')
228except TypeError:
229    F128 = None
230
231# Scale and background, which are parameters common to every form factor
232COMMON_PARAMETERS = [
233    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
234    ["background", "1/cm", 0, [0, np.inf], "", "Source background"],
235    ]
236
237# Conversion from units defined in the parameter table for each model
238# to units displayed in the sphinx documentation.
239RST_UNITS = {
240    "Ang": "|Ang|",
241    "1/Ang": "|Ang^-1|",
242    "1/Ang^2": "|Ang^-2|",
243    "1e-6/Ang^2": "|1e-6Ang^-2|",
244    "degrees": "degree",
245    "1/cm": "|cm^-1|",
246    "Ang/cm": "|Ang*cm^-1|",
247    "": "None",
248    }
249
250# Headers for the parameters tables in th sphinx documentation
251PARTABLE_HEADERS = [
252    "Parameter",
253    "Description",
254    "Units",
255    "Default value",
256    ]
257
258# Minimum width for a default value (this is shorter than the column header
259# width, so will be ignored).
260PARTABLE_VALUE_WIDTH = 10
261
262# Documentation header for the module, giving the model name, its short
263# description and its parameter table.  The remainder of the doc comes
264# from the module docstring.
265DOC_HEADER = """.. _%(id)s:
266
267%(name)s
268=======================================================
269
270%(title)s
271
272%(parameters)s
273
274%(returns)s
275
276%(docs)s
277"""
278
279def format_units(units):
280    """
281    Convert units into ReStructured Text format.
282    """
283    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
284
285def make_partable(pars):
286    """
287    Generate the parameter table to include in the sphinx documentation.
288    """
289    column_widths = [
290        max(len(p[0]) for p in pars),
291        max(len(p[-1]) for p in pars),
292        max(len(format_units(p[1])) for p in pars),
293        PARTABLE_VALUE_WIDTH,
294        ]
295    column_widths = [max(w, len(h))
296                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
297
298    sep = " ".join("="*w for w in column_widths)
299    lines = [
300        sep,
301        " ".join("%-*s" % (w, h)
302                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
303        sep,
304        ]
305    for p in pars:
306        lines.append(" ".join([
307            "%-*s" % (column_widths[0], p[0]),
308            "%-*s" % (column_widths[1], p[-1]),
309            "%-*s" % (column_widths[2], format_units(p[1])),
310            "%*g" % (column_widths[3], p[2]),
311            ]))
312    lines.append(sep)
313    return "\n".join(lines)
314
315def _search(search_path, filename):
316    """
317    Find *filename* in *search_path*.
318
319    Raises ValueError if file does not exist.
320    """
321    for path in search_path:
322        target = joinpath(path, filename)
323        if exists(target):
324            return target
325    raise ValueError("%r not found in %s" % (filename, search_path))
326
327def model_sources(info):
328    """
329    Return a list of the sources file paths for the module.
330    """
331    search_path = [dirname(info['filename']),
332                   abspath(joinpath(dirname(__file__), 'models'))]
333    return [_search(search_path, f) for f in info['source']]
334
335# Pragmas for enable OpenCL features.  Be sure to protect them so that they
336# still compile even if OpenCL is not present.
337_F16_PRAGMA = """\
338#if defined(__OPENCL_VERSION__) && !defined(cl_khr_fp16)
339#  pragma OPENCL EXTENSION cl_khr_fp16: enable
340#endif
341"""
342
343_F64_PRAGMA = """\
344#if defined(__OPENCL_VERSION__) && !defined(cl_khr_fp64)
345#  pragma OPENCL EXTENSION cl_khr_fp64: enable
346#endif
347"""
348
349def convert_type(source, dtype):
350    """
351    Convert code from double precision to the desired type.
352
353    Floating point constants are tagged with 'f' for single precision or 'L'
354    for long double precision.
355    """
356    if dtype == F16:
357        fbytes = 2
358        source = _F16_PRAGMA + _convert_type(source, "half", "f")
359    elif dtype == F32:
360        fbytes = 4
361        source = _convert_type(source, "float", "f")
362    elif dtype == F64:
363        fbytes = 8
364        source = _F64_PRAGMA + source  # Source is already double
365    elif dtype == F128:
366        fbytes = 16
367        source = _convert_type(source, "long double", "L")
368    else:
369        raise ValueError("Unexpected dtype in source conversion: %s"%dtype)
370    return ("#define FLOAT_SIZE %d\n"%fbytes)+source
371
372
373def _convert_type(source, type_name, constant_flag):
374    """
375    Replace 'double' with *type_name* in *source*, tagging floating point
376    constants with *constant_flag*.
377    """
378    # Convert double keyword to float/long double/half.
379    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
380    # Assume complex numbers are represented as cdouble which is typedef'd
381    # to double2.
382    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
383                    r'\1%s\2'%type_name, source)
384    # Convert floating point constants to single by adding 'f' to the end,
385    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
386    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
387                    r'\g<0>%s'%constant_flag, source)
388    return source
389
390
391def kernel_name(info, is_2d):
392    """
393    Name of the exported kernel symbol.
394    """
395    return info['name'] + "_" + ("Iqxy" if is_2d else "Iq")
396
397
398def categorize_parameters(pars):
399    """
400    Build parameter categories out of the the parameter definitions.
401
402    Returns a dictionary of categories.
403    """
404    partype = {
405        'volume': [], 'orientation': [], 'magnetic': [], '': [],
406        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
407        'pd-rel': set(),
408    }
409
410    for p in pars:
411        name, ptype = p[0], p[4]
412        if ptype == 'volume':
413            partype['pd-1d'].append(name)
414            partype['pd-2d'].append(name)
415            partype['pd-rel'].add(name)
416        elif ptype == 'magnetic':
417            partype['fixed-2d'].append(name)
418        elif ptype == 'orientation':
419            partype['pd-2d'].append(name)
420        elif ptype == '':
421            partype['fixed-1d'].append(name)
422            partype['fixed-2d'].append(name)
423        else:
424            raise ValueError("unknown parameter type %r" % ptype)
425        partype[ptype].append(name)
426
427    return partype
428
429def indent(s, depth):
430    """
431    Indent a string of text with *depth* additional spaces on each line.
432    """
433    spaces = " "*depth
434    sep = "\n" + spaces
435    return spaces + sep.join(s.split("\n"))
436
437
438LOOP_OPEN = """\
439for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
440  const double %(name)s = loops[2*(%(name)s_i%(offset)s)];
441  const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\
442"""
443def build_polydispersity_loops(pd_pars):
444    """
445    Build polydispersity loops
446
447    Returns loop opening and loop closing
448    """
449    depth = 4
450    offset = ""
451    loop_head = []
452    loop_end = []
453    for name in pd_pars:
454        subst = {'name': name, 'offset': offset}
455        loop_head.append(indent(LOOP_OPEN % subst, depth))
456        loop_end.insert(0, (" "*depth) + "}")
457        offset += '+N' + name
458        depth += 2
459    return "\n".join(loop_head), "\n".join(loop_end)
460
461C_KERNEL_TEMPLATE = None
462def make_model(info):
463    """
464    Generate the code for the kernel defined by info, using source files
465    found in the given search path.
466    """
467    # TODO: need something other than volume to indicate dispersion parameters
468    # No volume normalization despite having a volume parameter.
469    # Thickness is labelled a volume in order to trigger polydispersity.
470    # May want a separate dispersion flag, or perhaps a separate category for
471    # disperse, but not volume.  Volume parameters also use relative values
472    # for the distribution rather than the absolute values used by angular
473    # dispersion.  Need to be careful that necessary parameters are available
474    # for computing volume even if we allow non-disperse volume parameters.
475
476    # Load template
477    global C_KERNEL_TEMPLATE
478    if C_KERNEL_TEMPLATE is None:
479        with open(C_KERNEL_TEMPLATE_PATH) as fid:
480            C_KERNEL_TEMPLATE = fid.read()
481
482    # Load additional sources
483    source = [open(f).read() for f in model_sources(info)]
484
485    # Prepare defines
486    defines = []
487    partype = info['partype']
488    pd_1d = partype['pd-1d']
489    pd_2d = partype['pd-2d']
490    fixed_1d = partype['fixed-1d']
491    fixed_2d = partype['fixed-1d']
492
493    iq_parameters = [p[0]
494                     for p in info['parameters'][2:] # skip scale, background
495                     if p[0] in set(fixed_1d + pd_1d)]
496    iqxy_parameters = [p[0]
497                       for p in info['parameters'][2:] # skip scale, background
498                       if p[0] in set(fixed_2d + pd_2d)]
499    volume_parameters = [p[0]
500                         for p in info['parameters']
501                         if p[4] == 'volume']
502
503    # Fill in defintions for volume parameters
504    if volume_parameters:
505        defines.append(('VOLUME_PARAMETERS',
506                        ','.join(volume_parameters)))
507        defines.append(('VOLUME_WEIGHT_PRODUCT',
508                        '*'.join(p + '_w' for p in volume_parameters)))
509
510    # Generate form_volume function from body only
511    if info['form_volume'] is not None:
512        if volume_parameters:
513            vol_par_decl = ', '.join('double ' + p for p in volume_parameters)
514        else:
515            vol_par_decl = 'void'
516        defines.append(('VOLUME_PARAMETER_DECLARATIONS',
517                        vol_par_decl))
518        fn = """\
519double form_volume(VOLUME_PARAMETER_DECLARATIONS);
520double form_volume(VOLUME_PARAMETER_DECLARATIONS) {
521    %(body)s
522}
523""" % {'body':info['form_volume']}
524        source.append(fn)
525
526    # Fill in definitions for Iq parameters
527    defines.append(('IQ_KERNEL_NAME', info['name'] + '_Iq'))
528    defines.append(('IQ_PARAMETERS', ', '.join(iq_parameters)))
529    if fixed_1d:
530        defines.append(('IQ_FIXED_PARAMETER_DECLARATIONS',
531                        ', \\\n    '.join('const double %s' % p for p in fixed_1d)))
532    if pd_1d:
533        defines.append(('IQ_WEIGHT_PRODUCT',
534                        '*'.join(p + '_w' for p in pd_1d)))
535        defines.append(('IQ_DISPERSION_LENGTH_DECLARATIONS',
536                        ', \\\n    '.join('const int N%s' % p for p in pd_1d)))
537        defines.append(('IQ_DISPERSION_LENGTH_SUM',
538                        '+'.join('N' + p for p in pd_1d)))
539        open_loops, close_loops = build_polydispersity_loops(pd_1d)
540        defines.append(('IQ_OPEN_LOOPS',
541                        open_loops.replace('\n', ' \\\n')))
542        defines.append(('IQ_CLOSE_LOOPS',
543                        close_loops.replace('\n', ' \\\n')))
544    if info['Iq'] is not None:
545        defines.append(('IQ_PARAMETER_DECLARATIONS',
546                        ', '.join('double ' + p for p in iq_parameters)))
547        fn = """\
548double Iq(double q, IQ_PARAMETER_DECLARATIONS);
549double Iq(double q, IQ_PARAMETER_DECLARATIONS) {
550    %(body)s
551}
552""" % {'body':info['Iq']}
553        source.append(fn)
554
555    # Fill in definitions for Iqxy parameters
556    defines.append(('IQXY_KERNEL_NAME', info['name'] + '_Iqxy'))
557    defines.append(('IQXY_PARAMETERS', ', '.join(iqxy_parameters)))
558    if fixed_2d:
559        defines.append(('IQXY_FIXED_PARAMETER_DECLARATIONS',
560                        ', \\\n    '.join('const double %s' % p for p in fixed_2d)))
561    if pd_2d:
562        defines.append(('IQXY_WEIGHT_PRODUCT',
563                        '*'.join(p + '_w' for p in pd_2d)))
564        defines.append(('IQXY_DISPERSION_LENGTH_DECLARATIONS',
565                        ', \\\n    '.join('const int N%s' % p for p in pd_2d)))
566        defines.append(('IQXY_DISPERSION_LENGTH_SUM',
567                        '+'.join('N' + p for p in pd_2d)))
568        open_loops, close_loops = build_polydispersity_loops(pd_2d)
569        defines.append(('IQXY_OPEN_LOOPS',
570                        open_loops.replace('\n', ' \\\n')))
571        defines.append(('IQXY_CLOSE_LOOPS',
572                        close_loops.replace('\n', ' \\\n')))
573    if info['Iqxy'] is not None:
574        defines.append(('IQXY_PARAMETER_DECLARATIONS',
575                        ', '.join('double ' + p for p in iqxy_parameters)))
576        fn = """\
577double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS);
578double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS) {
579    %(body)s
580}
581""" % {'body':info['Iqxy']}
582        source.append(fn)
583
584    # Need to know if we have a theta parameter for Iqxy; it is not there
585    # for the magnetic sphere model, for example, which has a magnetic
586    # orientation but no shape orientation.
587    if 'theta' in pd_2d:
588        defines.append(('IQXY_HAS_THETA', '1'))
589
590    #for d in defines: print(d)
591    defines = '\n'.join('#define %s %s' % (k, v) for k, v in defines)
592    sources = '\n\n'.join(source)
593    return C_KERNEL_TEMPLATE % {
594        'DEFINES': defines,
595        'SOURCES': sources,
596        }
597
598def make_info(kernel_module):
599    """
600    Interpret the model definition file, categorizing the parameters.
601    """
602    #print(kernelfile)
603    category = getattr(kernel_module, 'category', None)
604    parameters = COMMON_PARAMETERS + kernel_module.parameters
605    # Default the demo parameters to the starting values for the individual
606    # parameters if an explicit demo parameter set has not been specified.
607    demo_parameters = getattr(kernel_module, 'demo', None)
608    if demo_parameters is None:
609        demo_parameters = dict((p[0], p[2]) for p in parameters)
610    filename = abspath(kernel_module.__file__)
611    kernel_id = splitext(basename(filename))[0]
612    name = getattr(kernel_module, 'name', None)
613    if name is None:
614        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
615    info = dict(
616        id=kernel_id,  # string used to load the kernel
617        filename=abspath(kernel_module.__file__),
618        name=name,
619        title=kernel_module.title,
620        description=kernel_module.description,
621        category=category,
622        parameters=parameters,
623        demo=demo_parameters,
624        source=getattr(kernel_module, 'source', []),
625        oldname=kernel_module.oldname,
626        oldpars=kernel_module.oldpars,
627        )
628    # Fill in attributes which default to None
629    info.update((k, getattr(kernel_module, k, None))
630                for k in ('ER', 'VR', 'form_volume', 'Iq', 'Iqxy'))
631    # Fill in the derived attributes
632    info['limits'] = dict((p[0], p[3]) for p in info['parameters'])
633    info['partype'] = categorize_parameters(info['parameters'])
634    info['defaults'] = dict((p[0], p[2]) for p in info['parameters'])
635    return info
636
637def make(kernel_module):
638    """
639    Build an OpenCL/ctypes function from the definition in *kernel_module*.
640
641    The module can be loaded with a normal python import statement if you
642    know which module you need, or with __import__('sasmodels.model.'+name)
643    if the name is in a string.
644    """
645    info = make_info(kernel_module)
646    # Assume if one part of the kernel is python then all parts are.
647    source = make_model(info) if not callable(info['Iq']) else None
648    return source, info
649
650section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
651                            %re.escape(string.punctuation))
652def _convert_section_titles_to_boldface(lines):
653    """
654    Do the actual work of identifying and converting section headings.
655    """
656    prior = None
657    for line in lines:
658        if prior is None:
659            prior = line
660        elif section_marker.match(line):
661            if len(line) >= len(prior):
662                yield "".join(("**", prior, "**"))
663                prior = None
664            else:
665                yield prior
666                prior = line
667        else:
668            yield prior
669            prior = line
670    if prior is not None:
671        yield prior
672
673def convert_section_titles_to_boldface(s):
674    """
675    Use explicit bold-face rather than section headings so that the table of
676    contents is not polluted with section names from the model documentation.
677
678    Sections are identified as the title line followed by a line of punctuation
679    at least as long as the title line.
680    """
681    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
682
683def doc(kernel_module):
684    """
685    Return the documentation for the model.
686    """
687    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
688    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
689    info = make_info(kernel_module)
690    is_Sq = ("structure-factor" in info['category'])
691    #docs = kernel_module.__doc__
692    docs = convert_section_titles_to_boldface(kernel_module.__doc__)
693    subst = dict(id=info['id'].replace('_', '-'),
694                 name=info['name'],
695                 title=info['title'],
696                 parameters=make_partable(info['parameters']),
697                 returns=Sq_units if is_Sq else Iq_units,
698                 docs=docs)
699    return DOC_HEADER % subst
700
701
702
703def demo_time():
704    """
705    Show how long it takes to process a model.
706    """
707    from .models import cylinder
708    import datetime
709    tic = datetime.datetime.now()
710    make(cylinder)
711    toc = (datetime.datetime.now() - tic).total_seconds()
712    print("time: %g"%toc)
713
714def main():
715    """
716    Program which prints the source produced by the model.
717    """
718    if len(sys.argv) <= 1:
719        print("usage: python -m sasmodels.generate modelname")
720    else:
721        name = sys.argv[1]
722        import sasmodels.models
723        __import__('sasmodels.models.' + name)
724        model = getattr(sasmodels.models, name)
725        source, _ = make(model)
726        print(source)
727
728if __name__ == "__main__":
729    main()
Note: See TracBrowser for help on using the repository browser.