source: sasmodels/sasmodels/generate.py @ 9ef9dd9

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 9ef9dd9 was 9ef9dd9, checked in by jhbakker, 8 years ago

don't try to be clever with fp64 pragma

  • Property mode set to 100644
File size: 31.8 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
156    *oldname* is the name of the model in sasview before sasmodels
157    was split into its own package, and *oldpars* is a dictionary
158    of *parameter: old_parameter* pairs defining the new names for
159    the parameters.  This is used by *compare* to check the values
160    of the new model against the values of the old model before
161    you are ready to add the new model to sasmodels.
162
163
164An *model_info* dictionary is constructed from the kernel meta data and
165returned to the caller.
166
167The model evaluator, function call sequence consists of q inputs and the return vector,
168followed by the loop value/weight vector, followed by the values for
169the non-polydisperse parameters, followed by the lengths of the
170polydispersity loops.  To construct the call for 1D models, the
171categories *fixed-1d* and *pd-1d* list the names of the parameters
172of the non-polydisperse and the polydisperse parameters respectively.
173Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
174The *pd-rel* category is a set of those parameters which give
175polydispersitiy as a portion of the value (so a 10% length dispersity
176would use a polydispersity value of 0.1) rather than absolute
177dispersity such as an angle plus or minus 15 degrees.
178
179The *volume* category lists the volume parameters in order for calls
180to volume within the kernel (used for volume normalization) and for
181calls to ER and VR for effective radius and volume ratio respectively.
182
183The *orientation* and *magnetic* categories list the orientation and
184magnetic parameters.  These are used by the sasview interface.  The
185blank category is for parameters such as scale which don't have any
186other marking.
187
188The doc string at the start of the kernel module will be used to
189construct the model documentation web pages.  Embedded figures should
190appear in the subdirectory "img" beside the model definition, and tagged
191with the kernel module name to avoid collision with other models.  Some
192file systems are case-sensitive, so only use lower case characters for
193file names and extensions.
194
195
196The function :func:`make` loads the metadata from the module and returns
197the kernel source.  The function :func:`make_doc` extracts the doc string
198and adds the parameter table to the top.  The function :func:`model_sources`
199returns a list of files required by the model.
200
201Code follows the C99 standard with the following extensions and conditions::
202
203    M_PI_180 = pi/180
204    M_4PI_3 = 4pi/3
205    square(x) = x*x
206    cube(x) = x*x*x
207    sinc(x) = sin(x)/x, with sin(0)/0 -> 1
208    all double precision constants must include the decimal point
209    all double declarations may be converted to half, float, or long double
210    FLOAT_SIZE is the number of bytes in the converted variables
211"""
212from __future__ import print_function
213
214# TODO: identify model files which have changed since loading and reload them.
215
216import sys
217from os.path import abspath, dirname, join as joinpath, exists, basename, \
218    splitext
219import re
220import string
221import warnings
222from collections import namedtuple
223
224import numpy as np
225
226PARAMETER_FIELDS = ['name', 'units', 'default', 'limits', 'type', 'description']
227Parameter = namedtuple('Parameter', PARAMETER_FIELDS)
228
229#TODO: determine which functions are useful outside of generate
230#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
231
232C_KERNEL_TEMPLATE_PATH = joinpath(dirname(__file__), 'kernel_template.c')
233
234F16 = np.dtype('float16')
235F32 = np.dtype('float32')
236F64 = np.dtype('float64')
237try:  # CRUFT: older numpy does not support float128
238    F128 = np.dtype('float128')
239except TypeError:
240    F128 = None
241
242# Scale and background, which are parameters common to every form factor
243COMMON_PARAMETERS = [
244    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
245    ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],
246    ]
247
248# Conversion from units defined in the parameter table for each model
249# to units displayed in the sphinx documentation.
250RST_UNITS = {
251    "Ang": "|Ang|",
252    "1/Ang": "|Ang^-1|",
253    "1/Ang^2": "|Ang^-2|",
254    "1e-6/Ang^2": "|1e-6Ang^-2|",
255    "degrees": "degree",
256    "1/cm": "|cm^-1|",
257    "Ang/cm": "|Ang*cm^-1|",
258    "g/cm3": "|g/cm^3|",
259    "mg/m2": "|mg/m^2|",
260    "": "None",
261    }
262
263# Headers for the parameters tables in th sphinx documentation
264PARTABLE_HEADERS = [
265    "Parameter",
266    "Description",
267    "Units",
268    "Default value",
269    ]
270
271# Minimum width for a default value (this is shorter than the column header
272# width, so will be ignored).
273PARTABLE_VALUE_WIDTH = 10
274
275# Documentation header for the module, giving the model name, its short
276# description and its parameter table.  The remainder of the doc comes
277# from the module docstring.
278DOC_HEADER = """.. _%(id)s:
279
280%(name)s
281=======================================================
282
283%(title)s
284
285%(parameters)s
286
287%(returns)s
288
289%(docs)s
290"""
291
292def format_units(units):
293    """
294    Convert units into ReStructured Text format.
295    """
296    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
297
298def make_partable(pars):
299    """
300    Generate the parameter table to include in the sphinx documentation.
301    """
302    column_widths = [
303        max(len(p.name) for p in pars),
304        max(len(p.description) for p in pars),
305        max(len(format_units(p.units)) for p in pars),
306        PARTABLE_VALUE_WIDTH,
307        ]
308    column_widths = [max(w, len(h))
309                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
310
311    sep = " ".join("="*w for w in column_widths)
312    lines = [
313        sep,
314        " ".join("%-*s" % (w, h)
315                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
316        sep,
317        ]
318    for p in pars:
319        lines.append(" ".join([
320            "%-*s" % (column_widths[0], p.name),
321            "%-*s" % (column_widths[1], p.description),
322            "%-*s" % (column_widths[2], format_units(p.units)),
323            "%*g" % (column_widths[3], p.default),
324            ]))
325    lines.append(sep)
326    return "\n".join(lines)
327
328def _search(search_path, filename):
329    """
330    Find *filename* in *search_path*.
331
332    Raises ValueError if file does not exist.
333    """
334    for path in search_path:
335        target = joinpath(path, filename)
336        if exists(target):
337            return target
338    raise ValueError("%r not found in %s" % (filename, search_path))
339
340def model_sources(model_info):
341    """
342    Return a list of the sources file paths for the module.
343    """
344    search_path = [dirname(model_info['filename']),
345                   abspath(joinpath(dirname(__file__), 'models'))]
346    return [_search(search_path, f) for f in model_info['source']]
347
348# Pragmas for enable OpenCL features.  Be sure to protect them so that they
349# still compile even if OpenCL is not present.
350_F16_PRAGMA = """\
351#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp16)
352#  pragma OPENCL EXTENSION cl_khr_fp16: enable
353#endif
354"""
355
356_F64_PRAGMA = """\
357#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp64)
358#  pragma OPENCL EXTENSION cl_khr_fp64: enable
359#endif
360"""
361
362def convert_type(source, dtype):
363    """
364    Convert code from double precision to the desired type.
365
366    Floating point constants are tagged with 'f' for single precision or 'L'
367    for long double precision.
368    """
369    if dtype == F16:
370        fbytes = 2
371        source = _F16_PRAGMA + _convert_type(source, "half", "f")
372    elif dtype == F32:
373        fbytes = 4
374        source = _convert_type(source, "float", "f")
375    elif dtype == F64:
376        fbytes = 8
377        source = _F64_PRAGMA + source  # Source is already double
378    elif dtype == F128:
379        fbytes = 16
380        source = _convert_type(source, "long double", "L")
381    else:
382        raise ValueError("Unexpected dtype in source conversion: %s"%dtype)
383    return ("#define FLOAT_SIZE %d\n"%fbytes)+source
384
385
386def _convert_type(source, type_name, constant_flag):
387    """
388    Replace 'double' with *type_name* in *source*, tagging floating point
389    constants with *constant_flag*.
390    """
391    # Convert double keyword to float/long double/half.
392    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
393    # Assume complex numbers are represented as cdouble which is typedef'd
394    # to double2.
395    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
396                    r'\1%s\2'%type_name, source)
397    # Convert floating point constants to single by adding 'f' to the end,
398    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
399    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
400                    r'\g<0>%s'%constant_flag, source)
401    return source
402
403
404def kernel_name(model_info, is_2d):
405    """
406    Name of the exported kernel symbol.
407    """
408    return model_info['name'] + "_" + ("Iqxy" if is_2d else "Iq")
409
410
411def indent(s, depth):
412    """
413    Indent a string of text with *depth* additional spaces on each line.
414    """
415    spaces = " "*depth
416    sep = "\n" + spaces
417    return spaces + sep.join(s.split("\n"))
418
419
420LOOP_OPEN = """\
421for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
422  const double %(name)s = loops[2*(%(name)s_i%(offset)s)];
423  const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\
424"""
425def build_polydispersity_loops(pd_pars):
426    """
427    Build polydispersity loops
428
429    Returns loop opening and loop closing
430    """
431    depth = 4
432    offset = ""
433    loop_head = []
434    loop_end = []
435    for name in pd_pars:
436        subst = {'name': name, 'offset': offset}
437        loop_head.append(indent(LOOP_OPEN % subst, depth))
438        loop_end.insert(0, (" "*depth) + "}")
439        offset += '+N' + name
440        depth += 2
441    return "\n".join(loop_head), "\n".join(loop_end)
442
443C_KERNEL_TEMPLATE = None
444def make_source(model_info):
445    """
446    Generate the OpenCL/ctypes kernel from the module info.
447
448    Uses source files found in the given search path.
449    """
450    if callable(model_info['Iq']):
451        return None
452
453    # TODO: need something other than volume to indicate dispersion parameters
454    # No volume normalization despite having a volume parameter.
455    # Thickness is labelled a volume in order to trigger polydispersity.
456    # May want a separate dispersion flag, or perhaps a separate category for
457    # disperse, but not volume.  Volume parameters also use relative values
458    # for the distribution rather than the absolute values used by angular
459    # dispersion.  Need to be careful that necessary parameters are available
460    # for computing volume even if we allow non-disperse volume parameters.
461
462    # Load template
463    global C_KERNEL_TEMPLATE
464    if C_KERNEL_TEMPLATE is None:
465        with open(C_KERNEL_TEMPLATE_PATH) as fid:
466            C_KERNEL_TEMPLATE = fid.read()
467
468    # Load additional sources
469    source = [open(f).read() for f in model_sources(model_info)]
470
471    # Prepare defines
472    defines = []
473    partype = model_info['partype']
474    pd_1d = partype['pd-1d']
475    pd_2d = partype['pd-2d']
476    fixed_1d = partype['fixed-1d']
477    fixed_2d = partype['fixed-1d']
478
479    iq_parameters = [p.name
480                     for p in model_info['parameters'][2:]  # skip scale, background
481                     if p.name in set(fixed_1d + pd_1d)]
482    iqxy_parameters = [p.name
483                       for p in model_info['parameters'][2:]  # skip scale, background
484                       if p.name in set(fixed_2d + pd_2d)]
485    volume_parameters = [p.name
486                         for p in model_info['parameters']
487                         if p.type == 'volume']
488
489    # Fill in defintions for volume parameters
490    if volume_parameters:
491        defines.append(('VOLUME_PARAMETERS',
492                        ','.join(volume_parameters)))
493        defines.append(('VOLUME_WEIGHT_PRODUCT',
494                        '*'.join(p + '_w' for p in volume_parameters)))
495
496    # Generate form_volume function from body only
497    if model_info['form_volume'] is not None:
498        if volume_parameters:
499            vol_par_decl = ', '.join('double ' + p for p in volume_parameters)
500        else:
501            vol_par_decl = 'void'
502        defines.append(('VOLUME_PARAMETER_DECLARATIONS',
503                        vol_par_decl))
504        fn = """\
505double form_volume(VOLUME_PARAMETER_DECLARATIONS);
506double form_volume(VOLUME_PARAMETER_DECLARATIONS) {
507    %(body)s
508}
509""" % {'body':model_info['form_volume']}
510        source.append(fn)
511
512    # Fill in definitions for Iq parameters
513    defines.append(('IQ_KERNEL_NAME', model_info['name'] + '_Iq'))
514    defines.append(('IQ_PARAMETERS', ', '.join(iq_parameters)))
515    if fixed_1d:
516        defines.append(('IQ_FIXED_PARAMETER_DECLARATIONS',
517                        ', \\\n    '.join('const double %s' % p for p in fixed_1d)))
518    if pd_1d:
519        defines.append(('IQ_WEIGHT_PRODUCT',
520                        '*'.join(p + '_w' for p in pd_1d)))
521        defines.append(('IQ_DISPERSION_LENGTH_DECLARATIONS',
522                        ', \\\n    '.join('const int N%s' % p for p in pd_1d)))
523        defines.append(('IQ_DISPERSION_LENGTH_SUM',
524                        '+'.join('N' + p for p in pd_1d)))
525        open_loops, close_loops = build_polydispersity_loops(pd_1d)
526        defines.append(('IQ_OPEN_LOOPS',
527                        open_loops.replace('\n', ' \\\n')))
528        defines.append(('IQ_CLOSE_LOOPS',
529                        close_loops.replace('\n', ' \\\n')))
530    if model_info['Iq'] is not None:
531        defines.append(('IQ_PARAMETER_DECLARATIONS',
532                        ', '.join('double ' + p for p in iq_parameters)))
533        fn = """\
534double Iq(double q, IQ_PARAMETER_DECLARATIONS);
535double Iq(double q, IQ_PARAMETER_DECLARATIONS) {
536    %(body)s
537}
538""" % {'body':model_info['Iq']}
539        source.append(fn)
540
541    # Fill in definitions for Iqxy parameters
542    defines.append(('IQXY_KERNEL_NAME', model_info['name'] + '_Iqxy'))
543    defines.append(('IQXY_PARAMETERS', ', '.join(iqxy_parameters)))
544    if fixed_2d:
545        defines.append(('IQXY_FIXED_PARAMETER_DECLARATIONS',
546                        ', \\\n    '.join('const double %s' % p for p in fixed_2d)))
547    if pd_2d:
548        defines.append(('IQXY_WEIGHT_PRODUCT',
549                        '*'.join(p + '_w' for p in pd_2d)))
550        defines.append(('IQXY_DISPERSION_LENGTH_DECLARATIONS',
551                        ', \\\n    '.join('const int N%s' % p for p in pd_2d)))
552        defines.append(('IQXY_DISPERSION_LENGTH_SUM',
553                        '+'.join('N' + p for p in pd_2d)))
554        open_loops, close_loops = build_polydispersity_loops(pd_2d)
555        defines.append(('IQXY_OPEN_LOOPS',
556                        open_loops.replace('\n', ' \\\n')))
557        defines.append(('IQXY_CLOSE_LOOPS',
558                        close_loops.replace('\n', ' \\\n')))
559    if model_info['Iqxy'] is not None:
560        defines.append(('IQXY_PARAMETER_DECLARATIONS',
561                        ', '.join('double ' + p for p in iqxy_parameters)))
562        fn = """\
563double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS);
564double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS) {
565    %(body)s
566}
567""" % {'body':model_info['Iqxy']}
568        source.append(fn)
569
570    # Need to know if we have a theta parameter for Iqxy; it is not there
571    # for the magnetic sphere model, for example, which has a magnetic
572    # orientation but no shape orientation.
573    if 'theta' in pd_2d:
574        defines.append(('IQXY_HAS_THETA', '1'))
575
576    #for d in defines: print(d)
577    defines = '\n'.join('#define %s %s' % (k, v) for k, v in defines)
578    sources = '\n\n'.join(source)
579    return C_KERNEL_TEMPLATE % {
580        'DEFINES': defines,
581        'SOURCES': sources,
582        }
583
584def categorize_parameters(pars):
585    """
586    Build parameter categories out of the the parameter definitions.
587
588    Returns a dictionary of categories.
589
590    Note: these categories are subject to change, depending on the needs of
591    the UI and the needs of the kernel calling function.
592
593    The categories are as follows:
594
595    * *volume* list of volume parameter names
596    * *orientation* list of orientation parameters
597    * *magnetic* list of magnetic parameters
598    * *<empty string>* list of parameters that have no type info
599
600    Each parameter is in one and only one category.
601
602    The following derived categories are created:
603
604    * *fixed-1d* list of non-polydisperse parameters for 1D models
605    * *pd-1d* list of polydisperse parameters for 1D models
606    * *fixed-2d* list of non-polydisperse parameters for 2D models
607    * *pd-d2* list of polydisperse parameters for 2D models
608    """
609    partype = {
610        'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], '': [],
611        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
612        'pd-rel': set(),
613    }
614
615    for p in pars:
616        if p.type == 'volume':
617            partype['pd-1d'].append(p.name)
618            partype['pd-2d'].append(p.name)
619            partype['pd-rel'].add(p.name)
620        elif p.type == 'magnetic':
621            partype['fixed-2d'].append(p.name)
622        elif p.type == 'orientation':
623            partype['pd-2d'].append(p.name)
624        elif p.type in ('', 'sld'):
625            partype['fixed-1d'].append(p.name)
626            partype['fixed-2d'].append(p.name)
627        else:
628            raise ValueError("unknown parameter type %r" % p.type)
629        partype[p.type].append(p.name)
630
631    return partype
632
633def process_parameters(model_info):
634    """
635    Process parameter block, precalculating parameter details.
636    """
637    # convert parameters into named tuples
638    for p in model_info['parameters']:
639        if p[4] == '' and (p[0].startswith('sld') or p[0].endswith('sld')):
640            p[4] = 'sld'
641            # TODO: make sure all models explicitly label their sld parameters
642            #raise ValueError("%s.%s needs to be explicitly set to type 'sld'" %(model_info['id'], p[0]))
643
644    pars = [Parameter(*p) for p in model_info['parameters']]
645    # Fill in the derived attributes
646    model_info['parameters'] = pars
647    partype = categorize_parameters(pars)
648    model_info['limits'] = dict((p.name, p.limits) for p in pars)
649    model_info['partype'] = partype
650    model_info['defaults'] = dict((p.name, p.default) for p in pars)
651    if model_info.get('demo', None) is None:
652        model_info['demo'] = model_info['defaults']
653    model_info['has_2d'] = partype['orientation'] or partype['magnetic']
654
655def make_model_info(kernel_module):
656    """
657    Interpret the model definition file, categorizing the parameters.
658
659    The module can be loaded with a normal python import statement if you
660    know which module you need, or with __import__('sasmodels.model.'+name)
661    if the name is in a string.
662
663    The *model_info* structure contains the following fields:
664
665    * *id* is the id of the kernel
666    * *name* is the display name of the kernel
667    * *filename* is the full path to the module defining the file (if any)
668    * *title* is a short description of the kernel
669    * *description* is a long description of the kernel (this doesn't seem
670      very useful since the Help button on the model page brings you directly
671      to the documentation page)
672    * *docs* is the docstring from the module.  Use :func:`make_doc` to
673    * *category* specifies the model location in the docs
674    * *parameters* is the model parameter table
675    * *single* is True if the model allows single precision
676    * *structure_factor* is True if the model is useable in a product
677    * *variant_info* contains the information required to select between
678      model variants (e.g., the list of cases) or is None if there are no
679      model variants
680    * *defaults* is the *{parameter: value}* table built from the parameter
681      description table.
682    * *limits* is the *{parameter: [min, max]}* table built from the
683      parameter description table.
684    * *partypes* categorizes the model parameters. See
685      :func:`categorize_parameters` for details.
686    * *demo* contains the *{parameter: value}* map used in compare (and maybe
687      for the demo plot, if plots aren't set up to use the default values).
688      If *demo* is not given in the file, then the default values will be used.
689    * *tests* is a set of tests that must pass
690    * *source* is the list of library files to include in the C model build
691    * *Iq*, *Iqxy*, *form_volume*, *ER*, *VR* and *sesans* are python functions
692      implementing the kernel for the module, or None if they are not
693      defined in python
694    * *oldname* is the model name in pre-4.0 Sasview
695    * *oldpars* is the *{new: old}* parameter translation table
696      from pre-4.0 Sasview
697    * *composition* is None if the model is independent, otherwise it is a
698      tuple with composition type ('product' or 'mixture') and a list of
699      *model_info* blocks for the composition objects.  This allows us to
700      build complete product and mixture models from just the info.
701
702    """
703    # TODO: maybe turn model_info into a class ModelDefinition
704    parameters = COMMON_PARAMETERS + kernel_module.parameters
705    filename = abspath(kernel_module.__file__)
706    kernel_id = splitext(basename(filename))[0]
707    name = getattr(kernel_module, 'name', None)
708    if name is None:
709        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
710    model_info = dict(
711        id=kernel_id,  # string used to load the kernel
712        filename=abspath(kernel_module.__file__),
713        name=name,
714        title=kernel_module.title,
715        description=kernel_module.description,
716        parameters=parameters,
717        composition=None,
718        docs=kernel_module.__doc__,
719        category=getattr(kernel_module, 'category', None),
720        single=getattr(kernel_module, 'single', True),
721        structure_factor=getattr(kernel_module, 'structure_factor', False),
722        variant_info=getattr(kernel_module, 'invariant_info', None),
723        demo=getattr(kernel_module, 'demo', None),
724        source=getattr(kernel_module, 'source', []),
725        oldname=getattr(kernel_module, 'oldname', None),
726        oldpars=getattr(kernel_module, 'oldpars', {}),
727        tests=getattr(kernel_module, 'tests', []),
728        )
729    process_parameters(model_info)
730    # Check for optional functions
731    functions = "ER VR form_volume Iq Iqxy shape sesans".split()
732    model_info.update((k, getattr(kernel_module, k, None)) for k in functions)
733    return model_info
734
735section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
736                            %re.escape(string.punctuation))
737def _convert_section_titles_to_boldface(lines):
738    """
739    Do the actual work of identifying and converting section headings.
740    """
741    prior = None
742    for line in lines:
743        if prior is None:
744            prior = line
745        elif section_marker.match(line):
746            if len(line) >= len(prior):
747                yield "".join(("**", prior, "**"))
748                prior = None
749            else:
750                yield prior
751                prior = line
752        else:
753            yield prior
754            prior = line
755    if prior is not None:
756        yield prior
757
758def convert_section_titles_to_boldface(s):
759    """
760    Use explicit bold-face rather than section headings so that the table of
761    contents is not polluted with section names from the model documentation.
762
763    Sections are identified as the title line followed by a line of punctuation
764    at least as long as the title line.
765    """
766    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
767
768def make_doc(model_info):
769    """
770    Return the documentation for the model.
771    """
772    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
773    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
774    docs = convert_section_titles_to_boldface(model_info['docs'])
775    subst = dict(id=model_info['id'].replace('_', '-'),
776                 name=model_info['name'],
777                 title=model_info['title'],
778                 parameters=make_partable(model_info['parameters']),
779                 returns=Sq_units if model_info['structure_factor'] else Iq_units,
780                 docs=docs)
781    return DOC_HEADER % subst
782
783
784
785def demo_time():
786    """
787    Show how long it takes to process a model.
788    """
789    from .models import cylinder
790    import datetime
791    tic = datetime.datetime.now()
792    make_source(make_model_info(cylinder))
793    toc = (datetime.datetime.now() - tic).total_seconds()
794    print("time: %g"%toc)
795
796def main():
797    """
798    Program which prints the source produced by the model.
799    """
800    if len(sys.argv) <= 1:
801        print("usage: python -m sasmodels.generate modelname")
802    else:
803        name = sys.argv[1]
804        import sasmodels.models
805        __import__('sasmodels.models.' + name)
806        model = getattr(sasmodels.models, name)
807        model_info = make_model_info(model)
808        source = make_source(model_info)
809        print(source)
810
811if __name__ == "__main__":
812    main()
Note: See TracBrowser for help on using the repository browser.