source: sasmodels/sasmodels/generate.py @ 001d9f5

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

remove '#line 0' problem from bundled build

  • Property mode set to 100644
File size: 34.3 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, or 1.0 if no volume normalization is required.
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, isdir
213import re
214import string
215import warnings
216from collections import namedtuple
217import inspect
218
219import numpy as np
220
221from .custom import load_custom_kernel_module
222
223PARAMETER_FIELDS = ['name', 'units', 'default', 'limits', 'type', 'description']
224Parameter = namedtuple('Parameter', PARAMETER_FIELDS)
225
226def get_data_path(external_dir, target_file):
227    path = abspath(dirname(__file__))
228    if exists(joinpath(path, target_file)):
229        return path
230
231    # check next to exe/zip file
232    exepath = dirname(sys.executable)
233    path = joinpath(exepath, external_dir)
234    if exists(joinpath(path, target_file)):
235        return path
236
237    # check in py2app Contents/Resources
238    path = joinpath(exepath, '..', 'Resources', external_dir)
239    if exists(joinpath(path, target_file)):
240        return abspath(path)
241
242    raise RuntimeError('Could not find '+joinpath(external_dir, target_file))
243
244EXTERNAL_DIR = 'sasmodels-data'
245DATA_PATH = get_data_path(EXTERNAL_DIR, 'kernel_template.c')
246MODEL_PATH = joinpath(DATA_PATH, 'models')
247C_KERNEL_TEMPLATE_FILE = joinpath(DATA_PATH, 'kernel_template.c')
248
249
250F16 = np.dtype('float16')
251F32 = np.dtype('float32')
252F64 = np.dtype('float64')
253try:  # CRUFT: older numpy does not support float128
254    F128 = np.dtype('float128')
255except TypeError:
256    F128 = None
257
258# Scale and background, which are parameters common to every form factor
259COMMON_PARAMETERS = [
260    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
261    ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],
262    ]
263
264# Conversion from units defined in the parameter table for each model
265# to units displayed in the sphinx documentation.
266RST_UNITS = {
267    "Ang": "|Ang|",
268    "1/Ang": "|Ang^-1|",
269    "1/Ang^2": "|Ang^-2|",
270    "1e-6/Ang^2": "|1e-6Ang^-2|",
271    "degrees": "degree",
272    "1/cm": "|cm^-1|",
273    "Ang/cm": "|Ang*cm^-1|",
274    "g/cm3": "|g/cm^3|",
275    "mg/m2": "|mg/m^2|",
276    "": "None",
277    }
278
279# Headers for the parameters tables in th sphinx documentation
280PARTABLE_HEADERS = [
281    "Parameter",
282    "Description",
283    "Units",
284    "Default value",
285    ]
286
287# Minimum width for a default value (this is shorter than the column header
288# width, so will be ignored).
289PARTABLE_VALUE_WIDTH = 10
290
291# Documentation header for the module, giving the model name, its short
292# description and its parameter table.  The remainder of the doc comes
293# from the module docstring.
294DOC_HEADER = """.. _%(id)s:
295
296%(name)s
297=======================================================
298
299%(title)s
300
301%(parameters)s
302
303%(returns)s
304
305%(docs)s
306"""
307
308def format_units(units):
309    """
310    Convert units into ReStructured Text format.
311    """
312    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
313
314def make_partable(pars):
315    """
316    Generate the parameter table to include in the sphinx documentation.
317    """
318    column_widths = [
319        max(len(p.name) for p in pars),
320        max(len(p.description) for p in pars),
321        max(len(format_units(p.units)) for p in pars),
322        PARTABLE_VALUE_WIDTH,
323        ]
324    column_widths = [max(w, len(h))
325                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
326
327    sep = " ".join("="*w for w in column_widths)
328    lines = [
329        sep,
330        " ".join("%-*s" % (w, h)
331                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
332        sep,
333        ]
334    for p in pars:
335        lines.append(" ".join([
336            "%-*s" % (column_widths[0], p.name),
337            "%-*s" % (column_widths[1], p.description),
338            "%-*s" % (column_widths[2], format_units(p.units)),
339            "%*g" % (column_widths[3], p.default),
340            ]))
341    lines.append(sep)
342    return "\n".join(lines)
343
344def _search(search_path, filename):
345    """
346    Find *filename* in *search_path*.
347
348    Raises ValueError if file does not exist.
349    """
350    for path in search_path:
351        target = joinpath(path, filename)
352        if exists(target):
353            return target
354    raise ValueError("%r not found in %s" % (filename, search_path))
355
356def model_sources(model_info):
357    """
358    Return a list of the sources file paths for the module.
359    """
360    search_path = [dirname(model_info['filename']), MODEL_PATH]
361    return [_search(search_path, f) for f in model_info['source']]
362
363# Pragmas for enable OpenCL features.  Be sure to protect them so that they
364# still compile even if OpenCL is not present.
365_F16_PRAGMA = """\
366#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp16)
367#  pragma OPENCL EXTENSION cl_khr_fp16: enable
368#endif
369"""
370
371_F64_PRAGMA = """\
372#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp64)
373#  pragma OPENCL EXTENSION cl_khr_fp64: enable
374#endif
375"""
376
377def convert_type(source, dtype):
378    """
379    Convert code from double precision to the desired type.
380
381    Floating point constants are tagged with 'f' for single precision or 'L'
382    for long double precision.
383    """
384    if dtype == F16:
385        fbytes = 2
386        source = _F16_PRAGMA + _convert_type(source, "half", "f")
387    elif dtype == F32:
388        fbytes = 4
389        source = _convert_type(source, "float", "f")
390    elif dtype == F64:
391        fbytes = 8
392        source = _F64_PRAGMA + source  # Source is already double
393    elif dtype == F128:
394        fbytes = 16
395        source = _convert_type(source, "long double", "L")
396    else:
397        raise ValueError("Unexpected dtype in source conversion: %s"%dtype)
398    return ("#define FLOAT_SIZE %d\n"%fbytes)+source
399
400
401def _convert_type(source, type_name, constant_flag):
402    """
403    Replace 'double' with *type_name* in *source*, tagging floating point
404    constants with *constant_flag*.
405    """
406    # Convert double keyword to float/long double/half.
407    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
408    # Assume complex numbers are represented as cdouble which is typedef'd
409    # to double2.
410    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
411                    r'\1%s\2'%type_name, source)
412    # Convert floating point constants to single by adding 'f' to the end,
413    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
414    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
415                    r'\g<0>%s'%constant_flag, source)
416    return source
417
418
419def kernel_name(model_info, is_2d):
420    """
421    Name of the exported kernel symbol.
422    """
423    return model_info['name'] + "_" + ("Iqxy" if is_2d else "Iq")
424
425
426def indent(s, depth):
427    """
428    Indent a string of text with *depth* additional spaces on each line.
429    """
430    spaces = " "*depth
431    sep = "\n" + spaces
432    return spaces + sep.join(s.split("\n"))
433
434
435LOOP_OPEN = """\
436for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
437  const double %(name)s = loops[2*(%(name)s_i%(offset)s)];
438  const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\
439"""
440def build_polydispersity_loops(pd_pars):
441    """
442    Build polydispersity loops
443
444    Returns loop opening and loop closing
445    """
446    depth = 4
447    offset = ""
448    loop_head = []
449    loop_end = []
450    for name in pd_pars:
451        subst = {'name': name, 'offset': offset}
452        loop_head.append(indent(LOOP_OPEN % subst, depth))
453        loop_end.insert(0, (" "*depth) + "}")
454        offset += '+N' + name
455        depth += 2
456    return "\n".join(loop_head), "\n".join(loop_end)
457
458C_KERNEL_TEMPLATE = None
459def make_source(model_info):
460    """
461    Generate the OpenCL/ctypes kernel from the module info.
462
463    Uses source files found in the given search path.
464    """
465    if callable(model_info['Iq']):
466        return None
467
468    # TODO: need something other than volume to indicate dispersion parameters
469    # No volume normalization despite having a volume parameter.
470    # Thickness is labelled a volume in order to trigger polydispersity.
471    # May want a separate dispersion flag, or perhaps a separate category for
472    # disperse, but not volume.  Volume parameters also use relative values
473    # for the distribution rather than the absolute values used by angular
474    # dispersion.  Need to be careful that necessary parameters are available
475    # for computing volume even if we allow non-disperse volume parameters.
476
477    # Load template
478    global C_KERNEL_TEMPLATE
479    if C_KERNEL_TEMPLATE is None:
480        with open(C_KERNEL_TEMPLATE_FILE) as fid:
481            C_KERNEL_TEMPLATE = fid.read()
482
483    # Load additional sources
484    source = [p
485              for f in model_sources(model_info)
486              # Add #line directives at the start of each file
487              for p in ('#line 1 "%s"'%f.replace('\\', '\\\\'), open(f).read())
488              ]
489    source.append('#line 133 "%s"'%C_KERNEL_TEMPLATE_FILE.replace('\\', '\\\\'))
490
491    # Prepare defines
492    defines = []
493    partype = model_info['partype']
494    pd_1d = partype['pd-1d']
495    pd_2d = partype['pd-2d']
496    fixed_1d = partype['fixed-1d']
497    fixed_2d = partype['fixed-1d']
498
499    iq_parameters = [p.name
500                     for p in model_info['parameters'][2:]  # skip scale, background
501                     if p.name in set(fixed_1d + pd_1d)]
502    iqxy_parameters = [p.name
503                       for p in model_info['parameters'][2:]  # skip scale, background
504                       if p.name in set(fixed_2d + pd_2d)]
505    volume_parameters = [p.name
506                         for p in model_info['parameters']
507                         if p.type == 'volume']
508
509    # Fill in defintions for volume parameters
510    if volume_parameters:
511        defines.append(('VOLUME_PARAMETERS',
512                        ','.join(volume_parameters)))
513        defines.append(('VOLUME_WEIGHT_PRODUCT',
514                        '*'.join(p + '_w' for p in volume_parameters)))
515
516    # Generate form_volume function from body only
517    if model_info['form_volume'] is not None:
518        if volume_parameters:
519            vol_par_decl = ', '.join('double ' + p for p in volume_parameters)
520        else:
521            vol_par_decl = 'void'
522        defines.append(('VOLUME_PARAMETER_DECLARATIONS',
523                        vol_par_decl))
524        fn = """\
525double form_volume(VOLUME_PARAMETER_DECLARATIONS);
526double form_volume(VOLUME_PARAMETER_DECLARATIONS) {
527#line %(line)d "%(file)s"
528    %(body)s
529}
530""" % {'body':model_info['form_volume'],
531       'file':model_info['filename'].replace('\\', '\\\\'),
532       'line':model_info['form_volume_line'],
533       }
534        source.append(fn)
535
536    # Fill in definitions for Iq parameters
537    defines.append(('IQ_KERNEL_NAME', model_info['name'] + '_Iq'))
538    defines.append(('IQ_PARAMETERS', ', '.join(iq_parameters)))
539    if fixed_1d:
540        defines.append(('IQ_FIXED_PARAMETER_DECLARATIONS',
541                        ', \\\n    '.join('const double %s' % p for p in fixed_1d)))
542    if pd_1d:
543        defines.append(('IQ_WEIGHT_PRODUCT',
544                        '*'.join(p + '_w' for p in pd_1d)))
545        defines.append(('IQ_DISPERSION_LENGTH_DECLARATIONS',
546                        ', \\\n    '.join('const int N%s' % p for p in pd_1d)))
547        defines.append(('IQ_DISPERSION_LENGTH_SUM',
548                        '+'.join('N' + p for p in pd_1d)))
549        open_loops, close_loops = build_polydispersity_loops(pd_1d)
550        defines.append(('IQ_OPEN_LOOPS',
551                        open_loops.replace('\n', ' \\\n')))
552        defines.append(('IQ_CLOSE_LOOPS',
553                        close_loops.replace('\n', ' \\\n')))
554    if model_info['Iq'] is not None:
555        defines.append(('IQ_PARAMETER_DECLARATIONS',
556                        ', '.join('double ' + p for p in iq_parameters)))
557        fn = """\
558double Iq(double q, IQ_PARAMETER_DECLARATIONS);
559double Iq(double q, IQ_PARAMETER_DECLARATIONS) {
560#line %(line)d "%(file)s"
561    %(body)s
562}
563""" % {'body':model_info['Iq'],
564       'file':model_info['filename'].replace('\\', '\\\\'),
565       'line':model_info['Iq_line'],
566       }
567        source.append(fn)
568
569    # Fill in definitions for Iqxy parameters
570    defines.append(('IQXY_KERNEL_NAME', model_info['name'] + '_Iqxy'))
571    defines.append(('IQXY_PARAMETERS', ', '.join(iqxy_parameters)))
572    if fixed_2d:
573        defines.append(('IQXY_FIXED_PARAMETER_DECLARATIONS',
574                        ', \\\n    '.join('const double %s' % p for p in fixed_2d)))
575    if pd_2d:
576        defines.append(('IQXY_WEIGHT_PRODUCT',
577                        '*'.join(p + '_w' for p in pd_2d)))
578        defines.append(('IQXY_DISPERSION_LENGTH_DECLARATIONS',
579                        ', \\\n    '.join('const int N%s' % p for p in pd_2d)))
580        defines.append(('IQXY_DISPERSION_LENGTH_SUM',
581                        '+'.join('N' + p for p in pd_2d)))
582        open_loops, close_loops = build_polydispersity_loops(pd_2d)
583        defines.append(('IQXY_OPEN_LOOPS',
584                        open_loops.replace('\n', ' \\\n')))
585        defines.append(('IQXY_CLOSE_LOOPS',
586                        close_loops.replace('\n', ' \\\n')))
587    if model_info['Iqxy'] is not None:
588        defines.append(('IQXY_PARAMETER_DECLARATIONS',
589                        ', '.join('double ' + p for p in iqxy_parameters)))
590        fn = """\
591double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS);
592double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS) {
593#line %(line)d "%(file)s"
594    %(body)s
595}
596""" % {'body':model_info['Iqxy'],
597       'file':model_info['filename'].replace('\\', '\\\\'),
598       'line':model_info['Iqxy_line'],
599       }
600        source.append(fn)
601
602    # Need to know if we have a theta parameter for Iqxy; it is not there
603    # for the magnetic sphere model, for example, which has a magnetic
604    # orientation but no shape orientation.
605    if 'theta' in pd_2d:
606        defines.append(('IQXY_HAS_THETA', '1'))
607
608    #for d in defines: print(d)
609    defines = '\n'.join('#define %s %s' % (k, v) for k, v in defines)
610    sources = '\n\n'.join(source)
611    return C_KERNEL_TEMPLATE % {
612        'DEFINES': defines,
613        'SOURCES': sources,
614        }
615
616def categorize_parameters(pars):
617    """
618    Build parameter categories out of the the parameter definitions.
619
620    Returns a dictionary of categories.
621
622    Note: these categories are subject to change, depending on the needs of
623    the UI and the needs of the kernel calling function.
624
625    The categories are as follows:
626
627    * *volume* list of volume parameter names
628    * *orientation* list of orientation parameters
629    * *magnetic* list of magnetic parameters
630    * *<empty string>* list of parameters that have no type info
631
632    Each parameter is in one and only one category.
633
634    The following derived categories are created:
635
636    * *fixed-1d* list of non-polydisperse parameters for 1D models
637    * *pd-1d* list of polydisperse parameters for 1D models
638    * *fixed-2d* list of non-polydisperse parameters for 2D models
639    * *pd-d2* list of polydisperse parameters for 2D models
640    """
641    partype = {
642        'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], '': [],
643        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
644        'pd-rel': set(),
645    }
646
647    for p in pars:
648        if p.type == 'volume':
649            partype['pd-1d'].append(p.name)
650            partype['pd-2d'].append(p.name)
651            partype['pd-rel'].add(p.name)
652        elif p.type == 'magnetic':
653            partype['fixed-2d'].append(p.name)
654        elif p.type == 'orientation':
655            partype['pd-2d'].append(p.name)
656        elif p.type in ('', 'sld'):
657            partype['fixed-1d'].append(p.name)
658            partype['fixed-2d'].append(p.name)
659        else:
660            raise ValueError("unknown parameter type %r" % p.type)
661        partype[p.type].append(p.name)
662
663    return partype
664
665def process_parameters(model_info):
666    """
667    Process parameter block, precalculating parameter details.
668    """
669    # convert parameters into named tuples
670    for p in model_info['parameters']:
671        if p[4] == '' and (p[0].startswith('sld') or p[0].endswith('sld')):
672            p[4] = 'sld'
673            # TODO: make sure all models explicitly label their sld parameters
674            #raise ValueError("%s.%s needs to be explicitly set to type 'sld'" %(model_info['id'], p[0]))
675
676    pars = [Parameter(*p) for p in model_info['parameters']]
677    # Fill in the derived attributes
678    model_info['parameters'] = pars
679    partype = categorize_parameters(pars)
680    model_info['limits'] = dict((p.name, p.limits) for p in pars)
681    model_info['partype'] = partype
682    model_info['defaults'] = dict((p.name, p.default) for p in pars)
683    if model_info.get('demo', None) is None:
684        model_info['demo'] = model_info['defaults']
685    model_info['has_2d'] = partype['orientation'] or partype['magnetic']
686
687
688def load_kernel_module(model_name):
689    if model_name.endswith('.py'):
690        kernel_module = load_custom_kernel_module(model_name)
691    else:
692        from sasmodels import models
693        __import__('sasmodels.models.'+model_name)
694        kernel_module = getattr(models, model_name, None)
695    return kernel_module
696
697def find_source_lines(model_info, kernel_module):
698    """
699    Identify the location of the C source inside the model definition file.
700
701    This code runs through the source of the kernel module looking for
702    lines that start with 'Iq', 'Iqxy' or 'form_volume'.  Clearly there are
703    all sorts of reasons why this might not work (e.g., code commented out
704    in a triple-quoted line block, code built using string concatenation,
705    or code defined in the branch of an 'if' block), but it should work
706    properly in the 95% case, and getting the incorrect line number will
707    be harmless.
708    """
709    # Check if we need line numbers at all
710    if callable(model_info['Iq']):
711        return None
712
713    if (model_info['Iq'] is None
714        and model_info['Iqxy'] is None
715        and model_info['form_volume'] is None):
716        return
717
718    # Make sure we have harmless default values
719    # NB: 0 is not harmless---some compilers break with a "#line 0" directive
720    model_info['Iqxy_line'] = 1
721    model_info['Iq_line'] = 1
722    model_info['form_volume_line'] = 1
723
724    # find the defintion lines for the different code blocks
725    try:
726        source = inspect.getsource(kernel_module)
727    except IOError:
728        return
729    for k, v in enumerate(source.split('\n')):
730        if v.startswith('Iqxy'):
731            model_info['Iqxy_line'] = k+1
732        elif v.startswith('Iq'):
733            model_info['Iq_line'] = k+1
734        elif v.startswith('form_volume'):
735            model_info['form_volume_line'] = k+1
736
737
738def make_model_info(kernel_module):
739    """
740    Interpret the model definition file, categorizing the parameters.
741
742    The module can be loaded with a normal python import statement if you
743    know which module you need, or with __import__('sasmodels.model.'+name)
744    if the name is in a string.
745
746    The *model_info* structure contains the following fields:
747
748    * *id* is the id of the kernel
749    * *name* is the display name of the kernel
750    * *filename* is the full path to the module defining the file (if any)
751    * *title* is a short description of the kernel
752    * *description* is a long description of the kernel (this doesn't seem
753      very useful since the Help button on the model page brings you directly
754      to the documentation page)
755    * *docs* is the docstring from the module.  Use :func:`make_doc` to
756    * *category* specifies the model location in the docs
757    * *parameters* is the model parameter table
758    * *single* is True if the model allows single precision
759    * *structure_factor* is True if the model is useable in a product
760    * *defaults* is the *{parameter: value}* table built from the parameter
761      description table.
762    * *limits* is the *{parameter: [min, max]}* table built from the
763      parameter description table.
764    * *partypes* categorizes the model parameters. See
765      :func:`categorize_parameters` for details.
766    * *demo* contains the *{parameter: value}* map used in compare (and maybe
767      for the demo plot, if plots aren't set up to use the default values).
768      If *demo* is not given in the file, then the default values will be used.
769    * *tests* is a set of tests that must pass
770    * *source* is the list of library files to include in the C model build
771    * *Iq*, *Iqxy*, *form_volume*, *ER*, *VR* and *sesans* are python functions
772      implementing the kernel for the module, or None if they are not
773      defined in python
774    * *composition* is None if the model is independent, otherwise it is a
775      tuple with composition type ('product' or 'mixture') and a list of
776      *model_info* blocks for the composition objects.  This allows us to
777      build complete product and mixture models from just the info.
778
779    """
780    parameters = COMMON_PARAMETERS + kernel_module.parameters
781    filename = abspath(kernel_module.__file__)
782    kernel_id = splitext(basename(filename))[0]
783    name = getattr(kernel_module, 'name', None)
784    if name is None:
785        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
786    model_info = dict(
787        id=kernel_id,  # string used to load the kernel
788        filename=abspath(kernel_module.__file__.rstrip("cd")),
789        name=name,
790        title=kernel_module.title,
791        description=kernel_module.description,
792        parameters=parameters,
793        composition=None,
794        docs=kernel_module.__doc__,
795        category=getattr(kernel_module, 'category', None),
796        single=getattr(kernel_module, 'single', True),
797        structure_factor=getattr(kernel_module, 'structure_factor', False),
798        control=getattr(kernel_module, 'control', None),
799        demo=getattr(kernel_module, 'demo', None),
800        source=getattr(kernel_module, 'source', []),
801        tests=getattr(kernel_module, 'tests', []),
802        )
803    process_parameters(model_info)
804    # Check for optional functions
805    functions = "ER VR form_volume Iq Iqxy shape sesans".split()
806    model_info.update((k, getattr(kernel_module, k, None)) for k in functions)
807    find_source_lines(model_info, kernel_module)
808    return model_info
809
810section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
811                            %re.escape(string.punctuation))
812def _convert_section_titles_to_boldface(lines):
813    """
814    Do the actual work of identifying and converting section headings.
815    """
816    prior = None
817    for line in lines:
818        if prior is None:
819            prior = line
820        elif section_marker.match(line):
821            if len(line) >= len(prior):
822                yield "".join(("**", prior, "**"))
823                prior = None
824            else:
825                yield prior
826                prior = line
827        else:
828            yield prior
829            prior = line
830    if prior is not None:
831        yield prior
832
833def convert_section_titles_to_boldface(s):
834    """
835    Use explicit bold-face rather than section headings so that the table of
836    contents is not polluted with section names from the model documentation.
837
838    Sections are identified as the title line followed by a line of punctuation
839    at least as long as the title line.
840    """
841    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
842
843def make_doc(model_info):
844    """
845    Return the documentation for the model.
846    """
847    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
848    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
849    docs = convert_section_titles_to_boldface(model_info['docs'])
850    subst = dict(id=model_info['id'].replace('_', '-'),
851                 name=model_info['name'],
852                 title=model_info['title'],
853                 parameters=make_partable(model_info['parameters']),
854                 returns=Sq_units if model_info['structure_factor'] else Iq_units,
855                 docs=docs)
856    return DOC_HEADER % subst
857
858
859def make_html(model_info):
860    """
861    Convert model docs directly to html.
862    """
863    from . import rst2html
864    return rst2html.convert(make_doc(model_info), title=model_info['name'])
865
866def demo_time():
867    """
868    Show how long it takes to process a model.
869    """
870    from .models import cylinder
871    import datetime
872    tic = datetime.datetime.now()
873    make_source(make_model_info(cylinder))
874    toc = (datetime.datetime.now() - tic).total_seconds()
875    print("time: %g"%toc)
876
877def main():
878    """
879    Program which prints the source produced by the model.
880    """
881    if len(sys.argv) <= 1:
882        print("usage: python -m sasmodels.generate modelname")
883    else:
884        name = sys.argv[1]
885        kernel_module = load_kernel_module(name)
886        model_info = make_model_info(kernel_module)
887        source = make_source(model_info)
888        print(source)
889
890if __name__ == "__main__":
891    main()
Note: See TracBrowser for help on using the repository browser.