source: sasmodels/sasmodels/generate.py @ c4c426b

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

line numbers need to be off by one since some versions of msvc don't accept #line 0

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