source: sasmodels/sasmodels/generate.py @ 17bbadd

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

refactor so all model defintion queries use model_info; better documentation of model_info structure; initial implementation of product model (broken)

  • Property mode set to 100644
File size: 30.8 KB
Line 
1"""
2SAS model constructor.
3
4Small angle scattering models are defined by a set of kernel functions:
5
6    *Iq(q, p1, p2, ...)* returns the scattering at q for a form with
7    particular dimensions averaged over all orientations.
8
9    *Iqxy(qx, qy, p1, p2, ...)* returns the scattering at qx, qy for a form
10    with particular dimensions for a single orientation.
11
12    *Imagnetic(qx, qy, result[], p1, p2, ...)* returns the scattering for the
13    polarized neutron spin states (up-up, up-down, down-up, down-down) for
14    a form with particular dimensions for a single orientation.
15
16    *form_volume(p1, p2, ...)* returns the volume of the form with particular
17    dimension.
18
19    *ER(p1, p2, ...)* returns the effective radius of the form with
20    particular dimensions.
21
22    *VR(p1, p2, ...)* returns the volume ratio for core-shell style forms.
23
24These functions are defined in a kernel module .py script and an associated
25set of .c files.  The model constructor will use them to create models with
26polydispersity across volume and orientation parameters, and provide
27scale and background parameters for each model.
28
29*Iq*, *Iqxy*, *Imagnetic* and *form_volume* should be stylized C-99
30functions written for OpenCL.  All functions need prototype declarations
31even if the are defined before they are used.  OpenCL does not support
32*#include* preprocessor directives, so instead the list of includes needs
33to be given as part of the metadata in the kernel module definition.
34The included files should be listed using a path relative to the kernel
35module, or if using "lib/file.c" if it is one of the standard includes
36provided with the sasmodels source.  The includes need to be listed in
37order so that functions are defined before they are used.
38
39Floating point values should be declared as *double*.  For single precision
40calculations, *double* will be replaced by *float*.  The single precision
41conversion will also tag floating point constants with "f" to make them
42single precision constants.  When using integral values in floating point
43expressions, they should be expressed as floating point values by including
44a decimal point.  This includes 0., 1. and 2.
45
46OpenCL has a *sincos* function which can improve performance when both
47the *sin* and *cos* values are needed for a particular argument.  Since
48this function does not exist in C99, all use of *sincos* should be
49replaced by the macro *SINCOS(value, sn, cn)* where *sn* and *cn* are
50previously declared *double* variables.  When compiled for systems without
51OpenCL, *SINCOS* will be replaced by *sin* and *cos* calls.   If *value* is
52an expression, it will appear twice in this case; whether or not it will be
53evaluated twice depends on the quality of the compiler.
54
55If the input parameters are invalid, the scattering calculator should
56return a negative number. Particularly with polydispersity, there are
57some sets of shape parameters which lead to nonsensical forms, such
58as a capped cylinder where the cap radius is smaller than the
59cylinder radius.  The polydispersity calculation will ignore these points,
60effectively chopping the parameter weight distributions at the boundary
61of the infeasible region.  The resulting scattering will be set to
62background.  This will work correctly even when polydispersity is off.
63
64*ER* and *VR* are python functions which operate on parameter vectors.
65The constructor code will generate the necessary vectors for computing
66them with the desired polydispersity.
67
68The available kernel parameters are defined as a list, with each parameter
69defined as a sublist with the following elements:
70
71    *name* is the name that will be used in the call to the kernel
72    function and the name that will be displayed to the user.  Names
73    should be lower case, with words separated by underscore.  If
74    acronyms are used, the whole acronym should be upper case.
75
76    *units* should be one of *degrees* for angles, *Ang* for lengths,
77    *1e-6/Ang^2* for SLDs.
78
79    *default value* will be the initial value for  the model when it
80    is selected, or when an initial value is not otherwise specified.
81
82    [*lb*, *ub*] are the hard limits on the parameter value, used to limit
83    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
218
219import numpy as np
220
221#TODO: determine which functions are useful outside of generate
222#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
223
224C_KERNEL_TEMPLATE_PATH = joinpath(dirname(__file__), 'kernel_template.c')
225
226F16 = np.dtype('float16')
227F32 = np.dtype('float32')
228F64 = np.dtype('float64')
229try:  # CRUFT: older numpy does not support float128
230    F128 = np.dtype('float128')
231except TypeError:
232    F128 = None
233
234# Scale and background, which are parameters common to every form factor
235COMMON_PARAMETERS = [
236    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
237    ["background", "1/cm", 0, [0, np.inf], "", "Source background"],
238    ]
239
240# Conversion from units defined in the parameter table for each model
241# to units displayed in the sphinx documentation.
242RST_UNITS = {
243    "Ang": "|Ang|",
244    "1/Ang": "|Ang^-1|",
245    "1/Ang^2": "|Ang^-2|",
246    "1e-6/Ang^2": "|1e-6Ang^-2|",
247    "degrees": "degree",
248    "1/cm": "|cm^-1|",
249    "Ang/cm": "|Ang*cm^-1|",
250    "g/cm3": "|g/cm^3|",
251    "mg/m2": "|mg/m^2|",
252    "": "None",
253    }
254
255# Headers for the parameters tables in th sphinx documentation
256PARTABLE_HEADERS = [
257    "Parameter",
258    "Description",
259    "Units",
260    "Default value",
261    ]
262
263# Minimum width for a default value (this is shorter than the column header
264# width, so will be ignored).
265PARTABLE_VALUE_WIDTH = 10
266
267# Documentation header for the module, giving the model name, its short
268# description and its parameter table.  The remainder of the doc comes
269# from the module docstring.
270DOC_HEADER = """.. _%(id)s:
271
272%(name)s
273=======================================================
274
275%(title)s
276
277%(parameters)s
278
279%(returns)s
280
281%(docs)s
282"""
283
284def format_units(units):
285    """
286    Convert units into ReStructured Text format.
287    """
288    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
289
290def make_partable(pars):
291    """
292    Generate the parameter table to include in the sphinx documentation.
293    """
294    column_widths = [
295        max(len(p[0]) for p in pars),
296        max(len(p[-1]) for p in pars),
297        max(len(format_units(p[1])) for p in pars),
298        PARTABLE_VALUE_WIDTH,
299        ]
300    column_widths = [max(w, len(h))
301                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
302
303    sep = " ".join("="*w for w in column_widths)
304    lines = [
305        sep,
306        " ".join("%-*s" % (w, h)
307                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
308        sep,
309        ]
310    for p in pars:
311        lines.append(" ".join([
312            "%-*s" % (column_widths[0], p[0]),
313            "%-*s" % (column_widths[1], p[-1]),
314            "%-*s" % (column_widths[2], format_units(p[1])),
315            "%*g" % (column_widths[3], p[2]),
316            ]))
317    lines.append(sep)
318    return "\n".join(lines)
319
320def _search(search_path, filename):
321    """
322    Find *filename* in *search_path*.
323
324    Raises ValueError if file does not exist.
325    """
326    for path in search_path:
327        target = joinpath(path, filename)
328        if exists(target):
329            return target
330    raise ValueError("%r not found in %s" % (filename, search_path))
331
332def model_sources(model_info):
333    """
334    Return a list of the sources file paths for the module.
335    """
336    search_path = [dirname(model_info['filename']),
337                   abspath(joinpath(dirname(__file__), 'models'))]
338    return [_search(search_path, f) for f in model_info['source']]
339
340# Pragmas for enable OpenCL features.  Be sure to protect them so that they
341# still compile even if OpenCL is not present.
342_F16_PRAGMA = """\
343#if defined(__OPENCL_VERSION__) && !defined(cl_khr_fp16)
344#  pragma OPENCL EXTENSION cl_khr_fp16: enable
345#endif
346"""
347
348_F64_PRAGMA = """\
349#if defined(__OPENCL_VERSION__) && !defined(cl_khr_fp64)
350#  pragma OPENCL EXTENSION cl_khr_fp64: enable
351#endif
352"""
353
354def convert_type(source, dtype):
355    """
356    Convert code from double precision to the desired type.
357
358    Floating point constants are tagged with 'f' for single precision or 'L'
359    for long double precision.
360    """
361    if dtype == F16:
362        fbytes = 2
363        source = _F16_PRAGMA + _convert_type(source, "half", "f")
364    elif dtype == F32:
365        fbytes = 4
366        source = _convert_type(source, "float", "f")
367    elif dtype == F64:
368        fbytes = 8
369        source = _F64_PRAGMA + source  # Source is already double
370    elif dtype == F128:
371        fbytes = 16
372        source = _convert_type(source, "long double", "L")
373    else:
374        raise ValueError("Unexpected dtype in source conversion: %s"%dtype)
375    return ("#define FLOAT_SIZE %d\n"%fbytes)+source
376
377
378def _convert_type(source, type_name, constant_flag):
379    """
380    Replace 'double' with *type_name* in *source*, tagging floating point
381    constants with *constant_flag*.
382    """
383    # Convert double keyword to float/long double/half.
384    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
385    # Assume complex numbers are represented as cdouble which is typedef'd
386    # to double2.
387    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
388                    r'\1%s\2'%type_name, source)
389    # Convert floating point constants to single by adding 'f' to the end,
390    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
391    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
392                    r'\g<0>%s'%constant_flag, source)
393    return source
394
395
396def kernel_name(model_info, is_2d):
397    """
398    Name of the exported kernel symbol.
399    """
400    return model_info['name'] + "_" + ("Iqxy" if is_2d else "Iq")
401
402
403def indent(s, depth):
404    """
405    Indent a string of text with *depth* additional spaces on each line.
406    """
407    spaces = " "*depth
408    sep = "\n" + spaces
409    return spaces + sep.join(s.split("\n"))
410
411
412LOOP_OPEN = """\
413for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
414  const double %(name)s = loops[2*(%(name)s_i%(offset)s)];
415  const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\
416"""
417def build_polydispersity_loops(pd_pars):
418    """
419    Build polydispersity loops
420
421    Returns loop opening and loop closing
422    """
423    depth = 4
424    offset = ""
425    loop_head = []
426    loop_end = []
427    for name in pd_pars:
428        subst = {'name': name, 'offset': offset}
429        loop_head.append(indent(LOOP_OPEN % subst, depth))
430        loop_end.insert(0, (" "*depth) + "}")
431        offset += '+N' + name
432        depth += 2
433    return "\n".join(loop_head), "\n".join(loop_end)
434
435C_KERNEL_TEMPLATE = None
436def make_source(model_info):
437    """
438    Generate the OpenCL/ctypes kernel from the module info.
439
440    Uses source files found in the given search path.
441    """
442    if callable(model_info['Iq']):
443        return None
444
445    # TODO: need something other than volume to indicate dispersion parameters
446    # No volume normalization despite having a volume parameter.
447    # Thickness is labelled a volume in order to trigger polydispersity.
448    # May want a separate dispersion flag, or perhaps a separate category for
449    # disperse, but not volume.  Volume parameters also use relative values
450    # for the distribution rather than the absolute values used by angular
451    # dispersion.  Need to be careful that necessary parameters are available
452    # for computing volume even if we allow non-disperse volume parameters.
453
454    # Load template
455    global C_KERNEL_TEMPLATE
456    if C_KERNEL_TEMPLATE is None:
457        with open(C_KERNEL_TEMPLATE_PATH) as fid:
458            C_KERNEL_TEMPLATE = fid.read()
459
460    # Load additional sources
461    source = [open(f).read() for f in model_sources(model_info)]
462
463    # Prepare defines
464    defines = []
465    partype = model_info['partype']
466    pd_1d = partype['pd-1d']
467    pd_2d = partype['pd-2d']
468    fixed_1d = partype['fixed-1d']
469    fixed_2d = partype['fixed-1d']
470
471    iq_parameters = [p[0]
472                     for p in model_info['parameters'][2:]  # skip scale, background
473                     if p[0] in set(fixed_1d + pd_1d)]
474    iqxy_parameters = [p[0]
475                       for p in model_info['parameters'][2:]  # skip scale, background
476                       if p[0] in set(fixed_2d + pd_2d)]
477    volume_parameters = [p[0]
478                         for p in model_info['parameters']
479                         if p[4] == 'volume']
480
481    # Fill in defintions for volume parameters
482    if volume_parameters:
483        defines.append(('VOLUME_PARAMETERS',
484                        ','.join(volume_parameters)))
485        defines.append(('VOLUME_WEIGHT_PRODUCT',
486                        '*'.join(p + '_w' for p in volume_parameters)))
487
488    # Generate form_volume function from body only
489    if model_info['form_volume'] is not None:
490        if volume_parameters:
491            vol_par_decl = ', '.join('double ' + p for p in volume_parameters)
492        else:
493            vol_par_decl = 'void'
494        defines.append(('VOLUME_PARAMETER_DECLARATIONS',
495                        vol_par_decl))
496        fn = """\
497double form_volume(VOLUME_PARAMETER_DECLARATIONS);
498double form_volume(VOLUME_PARAMETER_DECLARATIONS) {
499    %(body)s
500}
501""" % {'body':model_info['form_volume']}
502        source.append(fn)
503
504    # Fill in definitions for Iq parameters
505    defines.append(('IQ_KERNEL_NAME', model_info['name'] + '_Iq'))
506    defines.append(('IQ_PARAMETERS', ', '.join(iq_parameters)))
507    if fixed_1d:
508        defines.append(('IQ_FIXED_PARAMETER_DECLARATIONS',
509                        ', \\\n    '.join('const double %s' % p for p in fixed_1d)))
510    if pd_1d:
511        defines.append(('IQ_WEIGHT_PRODUCT',
512                        '*'.join(p + '_w' for p in pd_1d)))
513        defines.append(('IQ_DISPERSION_LENGTH_DECLARATIONS',
514                        ', \\\n    '.join('const int N%s' % p for p in pd_1d)))
515        defines.append(('IQ_DISPERSION_LENGTH_SUM',
516                        '+'.join('N' + p for p in pd_1d)))
517        open_loops, close_loops = build_polydispersity_loops(pd_1d)
518        defines.append(('IQ_OPEN_LOOPS',
519                        open_loops.replace('\n', ' \\\n')))
520        defines.append(('IQ_CLOSE_LOOPS',
521                        close_loops.replace('\n', ' \\\n')))
522    if model_info['Iq'] is not None:
523        defines.append(('IQ_PARAMETER_DECLARATIONS',
524                        ', '.join('double ' + p for p in iq_parameters)))
525        fn = """\
526double Iq(double q, IQ_PARAMETER_DECLARATIONS);
527double Iq(double q, IQ_PARAMETER_DECLARATIONS) {
528    %(body)s
529}
530""" % {'body':model_info['Iq']}
531        source.append(fn)
532
533    # Fill in definitions for Iqxy parameters
534    defines.append(('IQXY_KERNEL_NAME', model_info['name'] + '_Iqxy'))
535    defines.append(('IQXY_PARAMETERS', ', '.join(iqxy_parameters)))
536    if fixed_2d:
537        defines.append(('IQXY_FIXED_PARAMETER_DECLARATIONS',
538                        ', \\\n    '.join('const double %s' % p for p in fixed_2d)))
539    if pd_2d:
540        defines.append(('IQXY_WEIGHT_PRODUCT',
541                        '*'.join(p + '_w' for p in pd_2d)))
542        defines.append(('IQXY_DISPERSION_LENGTH_DECLARATIONS',
543                        ', \\\n    '.join('const int N%s' % p for p in pd_2d)))
544        defines.append(('IQXY_DISPERSION_LENGTH_SUM',
545                        '+'.join('N' + p for p in pd_2d)))
546        open_loops, close_loops = build_polydispersity_loops(pd_2d)
547        defines.append(('IQXY_OPEN_LOOPS',
548                        open_loops.replace('\n', ' \\\n')))
549        defines.append(('IQXY_CLOSE_LOOPS',
550                        close_loops.replace('\n', ' \\\n')))
551    if model_info['Iqxy'] is not None:
552        defines.append(('IQXY_PARAMETER_DECLARATIONS',
553                        ', '.join('double ' + p for p in iqxy_parameters)))
554        fn = """\
555double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS);
556double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS) {
557    %(body)s
558}
559""" % {'body':model_info['Iqxy']}
560        source.append(fn)
561
562    # Need to know if we have a theta parameter for Iqxy; it is not there
563    # for the magnetic sphere model, for example, which has a magnetic
564    # orientation but no shape orientation.
565    if 'theta' in pd_2d:
566        defines.append(('IQXY_HAS_THETA', '1'))
567
568    #for d in defines: print(d)
569    defines = '\n'.join('#define %s %s' % (k, v) for k, v in defines)
570    sources = '\n\n'.join(source)
571    return C_KERNEL_TEMPLATE % {
572        'DEFINES': defines,
573        'SOURCES': sources,
574        }
575
576def categorize_parameters(pars):
577    """
578    Build parameter categories out of the the parameter definitions.
579
580    Returns a dictionary of categories.
581
582    Note: these categories are subject to change, depending on the needs of
583    the UI and the needs of the kernel calling function.
584
585    The categories are as follows:
586
587    * *volume* list of volume parameter names
588    * *orientation* list of orientation parameters
589    * *magnetic* list of magnetic parameters
590    * *<empty string>* list of parameters that have no type info
591
592    Each parameter is in one and only one category.
593
594    The following derived categories are created:
595
596    * *fixed-1d* list of non-polydisperse parameters for 1D models
597    * *pd-1d* list of polydisperse parameters for 1D models
598    * *fixed-2d* list of non-polydisperse parameters for 2D models
599    * *pd-d2* list of polydisperse parameters for 2D models
600    """
601    partype = {
602        'volume': [], 'orientation': [], 'magnetic': [], '': [],
603        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
604        'pd-rel': set(),
605    }
606
607    for p in pars:
608        name, ptype = p[0], p[4]
609        if ptype == 'volume':
610            partype['pd-1d'].append(name)
611            partype['pd-2d'].append(name)
612            partype['pd-rel'].add(name)
613        elif ptype == 'magnetic':
614            partype['fixed-2d'].append(name)
615        elif ptype == 'orientation':
616            partype['pd-2d'].append(name)
617        elif ptype == '':
618            partype['fixed-1d'].append(name)
619            partype['fixed-2d'].append(name)
620        else:
621            raise ValueError("unknown parameter type %r" % ptype)
622        partype[ptype].append(name)
623
624    return partype
625
626def process_parameters(model_info):
627    """
628    Process parameter block, precalculating parameter details.
629    """
630    # Fill in the derived attributes
631    model_info['limits'] = dict((p[0], p[3]) for p in model_info['parameters'])
632    model_info['partype'] = categorize_parameters(model_info['parameters'])
633    model_info['defaults'] = dict((p[0], p[2]) for p in model_info['parameters'])
634    if model_info.get('demo', None) is None:
635        model_info['demo'] = model_info['defaults']
636
637def make_model_info(kernel_module):
638    """
639    Interpret the model definition file, categorizing the parameters.
640
641    The module can be loaded with a normal python import statement if you
642    know which module you need, or with __import__('sasmodels.model.'+name)
643    if the name is in a string.
644
645    The *model_info* structure contains the following fields:
646
647    * *id* is the id of the kernel
648    * *name* is the display name of the kernel
649    * *title* is a short description of the kernel
650    * *description* is a long description of the kernel (this doesn't seem
651      very useful since the Help button on the model page brings you directly
652      to the documentation page)
653    * *docs* is the docstring from the module.  Use :func:`make_doc` to
654    * *category* specifies the model location in the docs
655    * *parameters* is the model parameter table
656    * *single* is True if the model allows single precision
657    * *defaults* is the *{parameter: value}* table built from the parameter
658      description table.
659    * *limits* is the *{parameter: [min, max]}* table built from the
660      parameter description table.
661    * *partypes* categorizes the model parameters. See
662      :func:`categorize_parameters` for details.
663    * *demo* contains the *{parameter: value}* map used in compare (and maybe
664      for the demo plot, if plots aren't set up to use the default values).
665      If *demo* is not given in the file, then the default values will be used.
666    * *tests* is a set of tests that must pass
667    * *source* is the list of library files to include in the C model build
668    * *Iq*, *Iqxy*, *form_volume*, *ER*, and *VR* are python functions
669      implementing the kernel for the module, or None if they are not
670      defined in python
671    * *oldname* is the model name in pre-4.0 Sasview
672    * *oldpars* is the *{new: old}* parameter translation table
673      from pre-4.0 Sasview
674    * *composition* is None if the model is independent, otherwise it is a
675      tuple with composition type ('product' or 'mixture') and a list of
676      *model_info* blocks for the composition objects.  This allows us to
677      build complete product and mixture models from just the info.
678    """
679    # TODO: maybe turn model_info into a class ModelDefinition
680    #print(kernelfile)
681    category = getattr(kernel_module, 'category', None)
682    parameters = COMMON_PARAMETERS + kernel_module.parameters
683    # Default the demo parameters to the starting values for the individual
684    # parameters if an explicit demo parameter set has not been specified.
685    demo_parameters = getattr(kernel_module, 'demo', None)
686    filename = abspath(kernel_module.__file__)
687    kernel_id = splitext(basename(filename))[0]
688    name = getattr(kernel_module, 'name', None)
689    single = getattr(kernel_module, 'single', True)
690    if name is None:
691        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
692    model_info = dict(
693        id=kernel_id,  # string used to load the kernel
694        filename=abspath(kernel_module.__file__),
695        name=name,
696        title=kernel_module.title,
697        description=kernel_module.description,
698        docs=kernel_module.__doc__,
699        category=category,
700        parameters=parameters,
701        composition=None,
702        single=single,
703        demo=demo_parameters,
704        source=getattr(kernel_module, 'source', []),
705        oldname=getattr(kernel_module, 'oldname', None),
706        oldpars=getattr(kernel_module, 'oldpars', {}),
707        tests=getattr(kernel_module, 'tests', []),
708        )
709    process_parameters(model_info)
710    # Fill in attributes which default to None
711    model_info.update((k, getattr(kernel_module, k, None))
712                      for k in ('ER', 'VR', 'form_volume', 'Iq', 'Iqxy'))
713    return model_info
714
715section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
716                            %re.escape(string.punctuation))
717def _convert_section_titles_to_boldface(lines):
718    """
719    Do the actual work of identifying and converting section headings.
720    """
721    prior = None
722    for line in lines:
723        if prior is None:
724            prior = line
725        elif section_marker.match(line):
726            if len(line) >= len(prior):
727                yield "".join(("**", prior, "**"))
728                prior = None
729            else:
730                yield prior
731                prior = line
732        else:
733            yield prior
734            prior = line
735    if prior is not None:
736        yield prior
737
738def convert_section_titles_to_boldface(s):
739    """
740    Use explicit bold-face rather than section headings so that the table of
741    contents is not polluted with section names from the model documentation.
742
743    Sections are identified as the title line followed by a line of punctuation
744    at least as long as the title line.
745    """
746    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
747
748def make_doc(model_info):
749    """
750    Return the documentation for the model.
751    """
752    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
753    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
754    is_Sq = ("structure-factor" in model_info['category'])
755    #docs = kernel_module.__doc__
756    docs = convert_section_titles_to_boldface(model_info['docs'])
757    subst = dict(id=model_info['id'].replace('_', '-'),
758                 name=model_info['name'],
759                 title=model_info['title'],
760                 parameters=make_partable(model_info['parameters']),
761                 returns=Sq_units if is_Sq else Iq_units,
762                 docs=docs)
763    return DOC_HEADER % subst
764
765
766
767def demo_time():
768    """
769    Show how long it takes to process a model.
770    """
771    from .models import cylinder
772    import datetime
773    tic = datetime.datetime.now()
774    make_source(make_model_info(cylinder))
775    toc = (datetime.datetime.now() - tic).total_seconds()
776    print("time: %g"%toc)
777
778def main():
779    """
780    Program which prints the source produced by the model.
781    """
782    if len(sys.argv) <= 1:
783        print("usage: python -m sasmodels.generate modelname")
784    else:
785        name = sys.argv[1]
786        import sasmodels.models
787        __import__('sasmodels.models.' + name)
788        model = getattr(sasmodels.models, name)
789        model_info = make_model_info(model)
790        source = make_source(model_info)
791        print(source)
792
793if __name__ == "__main__":
794    main()
Note: See TracBrowser for help on using the repository browser.