source: sasmodels/sasmodels/generate.py @ fcd7bbd

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

use named tuple for parameter information

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