source: sasmodels/sasmodels/generate.py @ c2c51a2

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

allow sasmodels to look in sasmodels-data for builtin model c source

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