source: sasmodels/sasmodels/generate.py @ 462a115

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 462a115 was 462a115, checked in by butler, 8 years ago

update generate.py to fix units conversion to ReST in some models

  • Property mode set to 100644
File size: 32.6 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
24    #define INVALID(v) (expr)  returns False if v.parameter is invalid
25    for some parameter or other (e.g., v.bell_radius < v.radius).  If
26    necessary, the expression can call a function.
27
28These functions are defined in a kernel module .py script and an associated
29set of .c files.  The model constructor will use them to create models with
30polydispersity across volume and orientation parameters, and provide
31scale and background parameters for each model.
32
33*Iq*, *Iqxy*, *Imagnetic* and *form_volume* should be stylized C-99
34functions written for OpenCL.  All functions need prototype declarations
35even if the are defined before they are used.  OpenCL does not support
36*#include* preprocessor directives, so instead the list of includes needs
37to be given as part of the metadata in the kernel module definition.
38The included files should be listed using a path relative to the kernel
39module, or if using "lib/file.c" if it is one of the standard includes
40provided with the sasmodels source.  The includes need to be listed in
41order so that functions are defined before they are used.
42
43Floating point values should be declared as *double*.  For single precision
44calculations, *double* will be replaced by *float*.  The single precision
45conversion will also tag floating point constants with "f" to make them
46single precision constants.  When using integral values in floating point
47expressions, they should be expressed as floating point values by including
48a decimal point.  This includes 0., 1. and 2.
49
50OpenCL has a *sincos* function which can improve performance when both
51the *sin* and *cos* values are needed for a particular argument.  Since
52this function does not exist in C99, all use of *sincos* should be
53replaced by the macro *SINCOS(value, sn, cn)* where *sn* and *cn* are
54previously declared *double* variables.  When compiled for systems without
55OpenCL, *SINCOS* will be replaced by *sin* and *cos* calls.   If *value* is
56an expression, it will appear twice in this case; whether or not it will be
57evaluated twice depends on the quality of the compiler.
58
59If the input parameters are invalid, the scattering calculator should
60return a negative number. Particularly with polydispersity, there are
61some sets of shape parameters which lead to nonsensical forms, such
62as a capped cylinder where the cap radius is smaller than the
63cylinder radius.  The polydispersity calculation will ignore these points,
64effectively chopping the parameter weight distributions at the boundary
65of the infeasible region.  The resulting scattering will be set to
66background.  This will work correctly even when polydispersity is off.
67
68*ER* and *VR* are python functions which operate on parameter vectors.
69The constructor code will generate the necessary vectors for computing
70them with the desired polydispersity.
71The kernel module must set variables defining the kernel meta data:
72
73    *id* is an implicit variable formed from the filename.  It will be
74    a valid python identifier, and will be used as the reference into
75    the html documentation, with '_' replaced by '-'.
76
77    *name* is the model name as displayed to the user.  If it is missing,
78    it will be constructed from the id.
79
80    *title* is a short description of the model, suitable for a tool tip,
81    or a one line model summary in a table of models.
82
83    *description* is an extended description of the model to be displayed
84    while the model parameters are being edited.
85
86    *parameters* is the list of parameters.  Parameters in the kernel
87    functions must appear in the same order as they appear in the
88    parameters list.  Two additional parameters, *scale* and *background*
89    are added to the beginning of the parameter list.  They will show up
90    in the documentation as model parameters, but they are never sent to
91    the kernel functions.  Note that *effect_radius* and *volfraction*
92    must occur first in structure factor calculations.
93
94    *category* is the default category for the model.  The category is
95    two level structure, with the form "group:section", indicating where
96    in the manual the model will be located.  Models are alphabetical
97    within their section.
98
99    *source* is the list of C-99 source files that must be joined to
100    create the OpenCL kernel functions.  The files defining the functions
101    need to be listed before the files which use the functions.
102
103    *ER* is a python function defining the effective radius.  If it is
104    not present, the effective radius is 0.
105
106    *VR* is a python function defining the volume ratio.  If it is not
107    present, the volume ratio is 1.
108
109    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
110    C source code for the body of the volume, Iq, and Iqxy functions
111    respectively.  These can also be defined in the last source file.
112
113    *Iq* and *Iqxy* also be instead be python functions defining the
114    kernel.  If they are marked as *Iq.vectorized = True* then the
115    kernel is passed the entire *q* vector at once, otherwise it is
116    passed values one *q* at a time.  The performance improvement of
117    this step is significant.
118
119    *demo* is a dictionary of parameter=value defining a set of
120    parameters to use by default when *compare* is called.  Any
121    parameter not set in *demo* gets the initial value from the
122    parameter list.  *demo* is mostly needed to set the default
123    polydispersity values for tests.
124
125A :class:`modelinfo.ModelInfo` structure is constructed from the kernel meta
126data and returned to the caller.
127
128The doc string at the start of the kernel module will be used to
129construct the model documentation web pages.  Embedded figures should
130appear in the subdirectory "img" beside the model definition, and tagged
131with the kernel module name to avoid collision with other models.  Some
132file systems are case-sensitive, so only use lower case characters for
133file names and extensions.
134
135Code follows the C99 standard with the following extensions and conditions::
136
137    M_PI_180 = pi/180
138    M_4PI_3 = 4pi/3
139    square(x) = x*x
140    cube(x) = x*x*x
141    sinc(x) = sin(x)/x, with sin(0)/0 -> 1
142    all double precision constants must include the decimal point
143    all double declarations may be converted to half, float, or long double
144    FLOAT_SIZE is the number of bytes in the converted variables
145
146:func:`load_kernel_module` loads the model definition file and
147:func:`modelinfo.make_model_info` parses it. :func:`make_source`
148converts C-based model definitions to C source code, including the
149polydispersity integral.  :func:`model_sources` returns the list of
150source files the model depends on, and :func:`timestamp` returns
151the latest time stamp amongst the source files (so you can check if
152the model needs to be rebuilt).
153
154The function :func:`make_doc` extracts the doc string and adds the
155parameter table to the top.  *make_figure* in *sasmodels/doc/genmodel*
156creates the default figure for the model.  [These two sets of code
157should mignrate into docs.py so docs can be updated in one place].
158"""
159from __future__ import print_function
160
161# TODO: determine which functions are useful outside of generate
162#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
163
164import sys
165from os.path import abspath, dirname, join as joinpath, exists, isdir, getmtime
166import re
167import string
168
169import numpy as np  # type: ignore
170
171from .modelinfo import Parameter
172from .custom import load_custom_kernel_module
173
174try:
175    from typing import Tuple, Sequence, Iterator, Dict
176    from .modelinfo import ModelInfo
177except ImportError:
178    pass
179
180def get_data_path(external_dir, target_file):
181    path = abspath(dirname(__file__))
182    if exists(joinpath(path, target_file)):
183        return path
184
185    # check next to exe/zip file
186    exepath = dirname(sys.executable)
187    path = joinpath(exepath, external_dir)
188    if exists(joinpath(path, target_file)):
189        return path
190
191    # check in py2app Contents/Resources
192    path = joinpath(exepath, '..', 'Resources', external_dir)
193    if exists(joinpath(path, target_file)):
194        return abspath(path)
195
196    raise RuntimeError('Could not find '+joinpath(external_dir, target_file))
197
198EXTERNAL_DIR = 'sasmodels-data'
199DATA_PATH = get_data_path(EXTERNAL_DIR, 'kernel_template.c')
200MODEL_PATH = joinpath(DATA_PATH, 'models')
201
202F16 = np.dtype('float16')
203F32 = np.dtype('float32')
204F64 = np.dtype('float64')
205try:  # CRUFT: older numpy does not support float128
206    F128 = np.dtype('float128')
207except TypeError:
208    F128 = None
209
210# Conversion from units defined in the parameter table for each model
211# to units displayed in the sphinx documentation.
212RST_UNITS = {
213    "Ang": "|Ang|",
214    "1/Ang": "|Ang^-1|",
215    "1/Ang^2": "|Ang^-2|",
216    "Ang^3": "|Ang^3|",
217    "1e15/cm^3": "|1e15cm^3|",
218    "Ang^3/mol": "|Ang^3|/mol",
219    "1e-6/Ang^2": "|1e-6Ang^-2|",
220    "degrees": "degree",
221    "1/cm": "|cm^-1|",
222    "Ang/cm": "|Ang*cm^-1|",
223    "g/cm^3": "|g/cm^3|",
224    "mg/m^2": "|mg/m^2|",
225    "": "None",
226    }
227
228# Headers for the parameters tables in th sphinx documentation
229PARTABLE_HEADERS = [
230    "Parameter",
231    "Description",
232    "Units",
233    "Default value",
234    ]
235
236# Minimum width for a default value (this is shorter than the column header
237# width, so will be ignored).
238PARTABLE_VALUE_WIDTH = 10
239
240# Documentation header for the module, giving the model name, its short
241# description and its parameter table.  The remainder of the doc comes
242# from the module docstring.
243DOC_HEADER = """.. _%(id)s:
244
245%(name)s
246=======================================================
247
248%(title)s
249
250%(parameters)s
251
252%(returns)s
253
254%(docs)s
255"""
256
257
258def format_units(units):
259    # type: (str) -> str
260    """
261    Convert units into ReStructured Text format.
262    """
263    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
264
265
266def make_partable(pars):
267    # type: (List[Parameter]) -> str
268    """
269    Generate the parameter table to include in the sphinx documentation.
270    """
271    column_widths = [
272        max(len(p.name) for p in pars),
273        max(len(p.description) for p in pars),
274        max(len(format_units(p.units)) for p in pars),
275        PARTABLE_VALUE_WIDTH,
276        ]
277    column_widths = [max(w, len(h))
278                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
279
280    sep = " ".join("="*w for w in column_widths)
281    lines = [
282        sep,
283        " ".join("%-*s" % (w, h)
284                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
285        sep,
286        ]
287    for p in pars:
288        lines.append(" ".join([
289            "%-*s" % (column_widths[0], p.name),
290            "%-*s" % (column_widths[1], p.description),
291            "%-*s" % (column_widths[2], format_units(p.units)),
292            "%*g" % (column_widths[3], p.default),
293            ]))
294    lines.append(sep)
295    return "\n".join(lines)
296
297
298def _search(search_path, filename):
299    # type: (List[str], str) -> str
300    """
301    Find *filename* in *search_path*.
302
303    Raises ValueError if file does not exist.
304    """
305    for path in search_path:
306        target = joinpath(path, filename)
307        if exists(target):
308            return target
309    raise ValueError("%r not found in %s" % (filename, search_path))
310
311
312def model_sources(model_info):
313    # type: (ModelInfo) -> List[str]
314    """
315    Return a list of the sources file paths for the module.
316    """
317    search_path = [dirname(model_info.filename), MODEL_PATH]
318    return [_search(search_path, f) for f in model_info.source]
319
320
321def dll_timestamp(model_info):
322    # type: (ModelInfo) -> int
323    """
324    Return a timestamp for the model corresponding to the most recently
325    changed file or dependency.
326    """
327    # TODO: fails DRY; templates appear two places.
328    model_templates = [joinpath(DATA_PATH, filename)
329                       for filename in ('kernel_header.c', 'kernel_iq.c')]
330    source_files = (model_sources(model_info)
331                    + model_templates
332                    + [model_info.filename])
333    # Note: file may not exist when it is a standard model from library.zip
334    times = [getmtime(f) for f in source_files if exists(f)]
335    newest = max(times) if times else 0
336    return newest
337
338def ocl_timestamp(model_info):
339    # type: (ModelInfo) -> int
340    """
341    Return a timestamp for the model corresponding to the most recently
342    changed file or dependency.
343
344    Note that this does not look at the time stamps for the OpenCL header
345    information since that need not trigger a recompile of the DLL.
346    """
347    # TODO: fails DRY; templates appear two places.
348    model_templates = [joinpath(DATA_PATH, filename)
349                       for filename in ('kernel_header.c', 'kernel_iq.cl')]
350    source_files = (model_sources(model_info)
351                    + model_templates
352                    + [model_info.filename])
353    # Note: file may not exist when it is a standard model from library.zip
354    times = [getmtime(f) for f in source_files if exists(f)]
355    newest = max(times) if times else 0
356    return newest
357
358
359def convert_type(source, dtype):
360    # type: (str, np.dtype) -> str
361    """
362    Convert code from double precision to the desired type.
363
364    Floating point constants are tagged with 'f' for single precision or 'L'
365    for long double precision.
366    """
367    source = _fix_tgmath_int(source)
368    if dtype == F16:
369        fbytes = 2
370        source = _convert_type(source, "half", "f")
371    elif dtype == F32:
372        fbytes = 4
373        source = _convert_type(source, "float", "f")
374    elif dtype == F64:
375        fbytes = 8
376        # no need to convert if it is already double
377    elif dtype == F128:
378        fbytes = 16
379        source = _convert_type(source, "long double", "L")
380    else:
381        raise ValueError("Unexpected dtype in source conversion: %s" % dtype)
382    return ("#define FLOAT_SIZE %d\n" % fbytes)+source
383
384
385def _convert_type(source, type_name, constant_flag):
386    # type: (str, str, str) -> str
387    """
388    Replace 'double' with *type_name* in *source*, tagging floating point
389    constants with *constant_flag*.
390    """
391    # Convert double keyword to float/long double/half.
392    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
393    # Assume complex numbers are represented as cdouble which is typedef'd
394    # to double2.
395    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
396                    r'\1%s\2'%type_name, source)
397    source = _tag_float(source, constant_flag)
398    return source
399
400TGMATH_INT_RE = re.compile(r"""
401(?: # Non-capturing match; not lookbehind since pattern length is variable
402  \b              # word boundary
403   # various math functions
404  (a?(sin|cos|tan)h? | atan2
405   | erfc? | tgamma
406   | exp(2|10|m1)? | log(2|10|1p)? | pow[nr]? | sqrt | rsqrt | rootn
407   | fabs | fmax | fmin
408   )
409  \s*[(]\s*       # open parenthesis
410)
411[+-]?(0|[1-9]\d*) # integer
412(?=               # lookahead match: don't want to move from end of int
413  \s*[,)]         # comma or close parenthesis for end of argument
414)                 # end lookahead
415""", re.VERBOSE)
416def _fix_tgmath_int(source):
417    # type: (str) -> str
418    """
419    Replace f(integer) with f(integer.) for sin, cos, pow, etc.
420
421    OS X OpenCL complains that it can't resolve the type generic calls to
422    the standard math functions when they are called with integer constants,
423    but this does not happen with the Windows Intel driver for example.
424    To avoid confusion on the matrix marketplace, automatically promote
425    integers to floats if we recognize them in the source.
426
427    The specific functions we look for are:
428
429        trigonometric: sin, asin, sinh, asinh, etc., and atan2
430        exponential:   exp, exp2, exp10, expm1, log, log2, log10, logp1
431        power:         pow, pown, powr, sqrt, rsqrt, rootn
432        special:       erf, erfc, tgamma
433        float:         fabs, fmin, fmax
434
435    Note that we don't convert the second argument of dual argument
436    functions: atan2, fmax, fmin, pow, powr.  This could potentially
437    be a problem for pow(x, 2), but that case seems to work without change.
438    """
439    out = TGMATH_INT_RE.sub(r'\g<0>.', source)
440    return out
441
442
443# Floating point regular expression
444#
445# Define parts:
446#
447#    E = [eE][+-]?\d+    : Exponent
448#    P = [.]             : Decimal separator
449#    N = [1-9]\d*        : Natural number, no leading zeros
450#    Z = 0               : Zero
451#    F = \d+             : Fractional number, maybe leading zeros
452#    F? = \d*            : Optional fractional number
453#
454# We want to reject bare natural numbers and bare decimal points, so we
455# need to tediously outline the cases where we have either a fraction or
456# an exponent:
457#
458#   ( ZP | ZPF | ZE | ZPE | ZPFE | NP | NPF | NE | NPE | NPFE | PF | PFE )
459#
460#
461# We can then join cases by making parts optional.  The following are
462# some ways to do this:
463#
464#   ( (Z|N)(P|PF|E|PE|PFE) | PFE? )                   # Split on lead
465#     => ( (Z|N)(PF?|(PF?)?E) | PFE? )
466#   ( ((Z|N)PF?|PF)E? | (Z|N)E)                       # Split on point
467#   ( (ZP|ZPF|NP|NPF|PF) | (Z|ZP|ZPF|N|NP|NPF|PF)E )  # Split on E
468#     => ( ((Z|N)PF?|PF) | ((Z|N)(PF?)? | PF) E )
469FLOAT_RE = re.compile(r"""
470    (?<!\w)  # use negative lookbehind since '.' confuses \b test
471    # use split on lead to match float ( (Z|N)(PF?|(PF?)?E) | PFE? )
472    ( ( 0 | [1-9]\d* )                     # ( ( Z | N )
473      ([.]\d* | ([.]\d*)? [eE][+-]?\d+ )   #   (PF? | (PF?)? E )
474    | [.]\d+ ([eE][+-]?\d+)?               # | PF (E)?
475    )                                      # )
476    (?!\w)  # use negative lookahead since '.' confuses \b test
477    """, re.VERBOSE)
478def _tag_float(source, constant_flag):
479    # Convert floating point constants to single by adding 'f' to the end,
480    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
481    out = FLOAT_RE.sub(r'\g<0>%s'%constant_flag, source)
482    #print("in",repr(source),"out",repr(out), constant_flag)
483    return out
484
485def test_tag_float():
486
487    cases="""
488ZP  : 0.
489ZPF : 0.0,0.01,0.1
490Z  E: 0e+001
491ZP E: 0.E0
492ZPFE: 0.13e-031
493NP  : 1., 12.
494NPF : 1.0001, 1.1, 1.0
495N  E: 1e0, 37E-080
496NP E: 1.e0, 37.E-080
497NPFE: 845.017e+22
498 PF : .1, .0, .0100
499 PFE: .6e+9, .82E-004
500# isolated cases
5010.
5021e0
5030.13e-013
504# untouched
505struct3.e3, 03.05.67, 37
506# expressions
5073.75+-1.6e-7-27+13.2
508a3.e2 - 0.
5094*atan(1)
5104.*atan(1.)
511"""
512
513    output="""
514ZP  : 0.f
515ZPF : 0.0f,0.01f,0.1f
516Z  E: 0e+001f
517ZP E: 0.E0f
518ZPFE: 0.13e-031f
519NP  : 1.f, 12.f
520NPF : 1.0001f, 1.1f, 1.0f
521N  E: 1e0f, 37E-080f
522NP E: 1.e0f, 37.E-080f
523NPFE: 845.017e+22f
524 PF : .1f, .0f, .0100f
525 PFE: .6e+9f, .82E-004f
526# isolated cases
5270.f
5281e0f
5290.13e-013f
530# untouched
531struct3.e3, 03.05.67, 37
532# expressions
5333.75f+-1.6e-7f-27+13.2f
534a3.e2 - 0.f
5354*atan(1)
5364.f*atan(1.f)
537"""
538
539    for case_in, case_out in zip(cases.split('\n'), output.split('\n')):
540        out = _tag_float(case_in, 'f')
541        assert case_out == out, "%r => %r"%(case_in, out)
542
543
544def kernel_name(model_info, variant):
545    # type: (ModelInfo, str) -> str
546    """
547    Name of the exported kernel symbol.
548
549    *variant* is "Iq", "Iqxy" or "Imagnetic".
550    """
551    return model_info.name + "_" + variant
552
553
554def indent(s, depth):
555    # type: (str, int) -> str
556    """
557    Indent a string of text with *depth* additional spaces on each line.
558    """
559    spaces = " "*depth
560    sep = "\n" + spaces
561    return spaces + sep.join(s.split("\n"))
562
563
564_template_cache = {}  # type: Dict[str, Tuple[int, str, str]]
565def load_template(filename):
566    # type: (str) -> str
567    path = joinpath(DATA_PATH, filename)
568    mtime = getmtime(path)
569    if filename not in _template_cache or mtime > _template_cache[filename][0]:
570        with open(path) as fid:
571            _template_cache[filename] = (mtime, fid.read(), path)
572    return _template_cache[filename][1], path
573
574
575_FN_TEMPLATE = """\
576double %(name)s(%(pars)s);
577double %(name)s(%(pars)s) {
578#line %(line)d "%(filename)s"
579    %(body)s
580}
581
582"""
583def _gen_fn(name, pars, body, filename, line):
584    # type: (str, List[Parameter], str, str, int) -> str
585    """
586    Generate a function given pars and body.
587
588    Returns the following string::
589
590         double fn(double a, double b, ...);
591         double fn(double a, double b, ...) {
592             ....
593         }
594    """
595    par_decl = ', '.join(p.as_function_argument() for p in pars) if pars else 'void'
596    return _FN_TEMPLATE % {
597        'name': name, 'pars': par_decl, 'body': body,
598        'filename': filename.replace('\\', '\\\\'), 'line': line,
599    }
600
601
602def _call_pars(prefix, pars):
603    # type: (str, List[Parameter]) -> List[str]
604    """
605    Return a list of *prefix.parameter* from parameter items.
606    """
607    return [p.as_call_reference(prefix) for p in pars]
608
609
610# type in IQXY pattern could be single, float, double, long double, ...
611_IQXY_PATTERN = re.compile("^((inline|static) )? *([a-z ]+ )? *Iqxy *([(]|$)",
612                           flags=re.MULTILINE)
613def _have_Iqxy(sources):
614    # type: (List[str]) -> bool
615    """
616    Return true if any file defines Iqxy.
617
618    Note this is not a C parser, and so can be easily confused by
619    non-standard syntax.  Also, it will incorrectly identify the following
620    as having Iqxy::
621
622        /*
623        double Iqxy(qx, qy, ...) { ... fill this in later ... }
624        */
625
626    If you want to comment out an Iqxy function, use // on the front of the
627    line instead.
628    """
629    for path, code in sources:
630        if _IQXY_PATTERN.search(code):
631            return True
632    else:
633        return False
634
635
636def _add_source(source, code, path):
637    """
638    Add a file to the list of source code chunks, tagged with path and line.
639    """
640    path = path.replace('\\', '\\\\')
641    source.append('#line 1 "%s"' % path)
642    source.append(code)
643
644def make_source(model_info):
645    # type: (ModelInfo) -> Dict[str, str]
646    """
647    Generate the OpenCL/ctypes kernel from the module info.
648
649    Uses source files found in the given search path.  Returns None if this
650    is a pure python model, with no C source components.
651    """
652    if callable(model_info.Iq):
653        raise ValueError("can't compile python model")
654        #return None
655
656    # TODO: need something other than volume to indicate dispersion parameters
657    # No volume normalization despite having a volume parameter.
658    # Thickness is labelled a volume in order to trigger polydispersity.
659    # May want a separate dispersion flag, or perhaps a separate category for
660    # disperse, but not volume.  Volume parameters also use relative values
661    # for the distribution rather than the absolute values used by angular
662    # dispersion.  Need to be careful that necessary parameters are available
663    # for computing volume even if we allow non-disperse volume parameters.
664
665    partable = model_info.parameters
666
667    # Load templates and user code
668    kernel_header = load_template('kernel_header.c')
669    dll_code = load_template('kernel_iq.c')
670    ocl_code = load_template('kernel_iq.cl')
671    #ocl_code = load_template('kernel_iq_local.cl')
672    user_code = [(f, open(f).read()) for f in model_sources(model_info)]
673
674    # Build initial sources
675    source = []
676    _add_source(source, *kernel_header)
677    for path, code in user_code:
678        _add_source(source, code, path)
679
680    # Make parameters for q, qx, qy so that we can use them in declarations
681    q, qx, qy = [Parameter(name=v) for v in ('q', 'qx', 'qy')]
682    # Generate form_volume function, etc. from body only
683    if isinstance(model_info.form_volume, str):
684        pars = partable.form_volume_parameters
685        source.append(_gen_fn('form_volume', pars, model_info.form_volume,
686                              model_info.filename, model_info._form_volume_line))
687    if isinstance(model_info.Iq, str):
688        pars = [q] + partable.iq_parameters
689        source.append(_gen_fn('Iq', pars, model_info.Iq,
690                              model_info.filename, model_info._Iq_line))
691    if isinstance(model_info.Iqxy, str):
692        pars = [qx, qy] + partable.iqxy_parameters
693        source.append(_gen_fn('Iqxy', pars, model_info.Iqxy,
694                              model_info.filename, model_info._Iqxy_line))
695
696    # Define the parameter table
697    # TODO: plug in current line number
698    source.append('#line 542 "sasmodels/generate.py"')
699    source.append("#define PARAMETER_TABLE \\")
700    source.append("\\\n".join(p.as_definition()
701                              for p in partable.kernel_parameters))
702
703    # Define the function calls
704    if partable.form_volume_parameters:
705        refs = _call_pars("_v.", partable.form_volume_parameters)
706        call_volume = "#define CALL_VOLUME(_v) form_volume(%s)"%(",".join(refs))
707    else:
708        # Model doesn't have volume.  We could make the kernel run a little
709        # faster by not using/transferring the volume normalizations, but
710        # the ifdef's reduce readability more than is worthwhile.
711        call_volume = "#define CALL_VOLUME(v) 1.0"
712    source.append(call_volume)
713
714    refs = ["_q[_i]"] + _call_pars("_v.", partable.iq_parameters)
715    call_iq = "#define CALL_IQ(_q,_i,_v) Iq(%s)" % (",".join(refs))
716    if _have_Iqxy(user_code) or isinstance(model_info.Iqxy, str):
717        # Call 2D model
718        refs = ["_q[2*_i]", "_q[2*_i+1]"] + _call_pars("_v.", partable.iqxy_parameters)
719        call_iqxy = "#define CALL_IQ(_q,_i,_v) Iqxy(%s)" % (",".join(refs))
720    else:
721        # Call 1D model with sqrt(qx^2 + qy^2)
722        #warnings.warn("Creating Iqxy = Iq(sqrt(qx^2 + qy^2))")
723        # still defined:: refs = ["q[i]"] + _call_pars("v", iq_parameters)
724        pars_sqrt = ["sqrt(_q[2*_i]*_q[2*_i]+_q[2*_i+1]*_q[2*_i+1])"] + refs[1:]
725        call_iqxy = "#define CALL_IQ(_q,_i,_v) Iq(%s)" % (",".join(pars_sqrt))
726
727    magpars = [k-2 for k,p in enumerate(partable.call_parameters)
728               if p.type == 'sld']
729
730    # Fill in definitions for numbers of parameters
731    source.append("#define MAX_PD %s"%partable.max_pd)
732    source.append("#define NUM_PARS %d"%partable.npars)
733    source.append("#define NUM_VALUES %d" % partable.nvalues)
734    source.append("#define NUM_MAGNETIC %d" % partable.nmagnetic)
735    source.append("#define MAGNETIC_PARS %s"%",".join(str(k) for k in magpars))
736    for k,v in enumerate(magpars[:3]):
737        source.append("#define MAGNETIC_PAR%d %d"%(k+1, v))
738
739    # TODO: allow mixed python/opencl kernels?
740
741    ocl = kernels(ocl_code, call_iq, call_iqxy, model_info.name)
742    dll = kernels(dll_code, call_iq, call_iqxy, model_info.name)
743    result = {
744        'dll': '\n'.join(source+dll[0]+dll[1]+dll[2]),
745        'opencl': '\n'.join(source+ocl[0]+ocl[1]+ocl[2]),
746    }
747
748    return result
749
750
751def kernels(kernel, call_iq, call_iqxy, name):
752    # type: ([str,str], str, str, str) -> List[str]
753    code = kernel[0]
754    path = kernel[1].replace('\\', '\\\\')
755    iq = [
756        # define the Iq kernel
757        "#define KERNEL_NAME %s_Iq" % name,
758        call_iq,
759        '#line 1 "%s Iq"' % path,
760        code,
761        "#undef CALL_IQ",
762        "#undef KERNEL_NAME",
763        ]
764
765    iqxy = [
766        # define the Iqxy kernel from the same source with different #defines
767        "#define KERNEL_NAME %s_Iqxy" % name,
768        call_iqxy,
769        '#line 1 "%s Iqxy"' % path,
770        code,
771        "#undef CALL_IQ",
772        "#undef KERNEL_NAME",
773         ]
774
775    imagnetic = [
776        # define the Imagnetic kernel
777        "#define KERNEL_NAME %s_Imagnetic" % name,
778        "#define MAGNETIC 1",
779        call_iqxy,
780        '#line 1 "%s Imagnetic"' % path,
781        code,
782        "#undef MAGNETIC",
783        "#undef CALL_IQ",
784        "#undef KERNEL_NAME",
785    ]
786
787    return iq, iqxy, imagnetic
788
789
790def load_kernel_module(model_name):
791    # type: (str) -> module
792    """
793    Return the kernel module named in *model_name*.
794
795    If the name ends in *.py* then load it as a custom model using
796    :func:`sasmodels.custom.load_custom_kernel_module`, otherwise
797    load it from :mod:`sasmodels.models`.
798    """
799    if model_name.endswith('.py'):
800        kernel_module = load_custom_kernel_module(model_name)
801    else:
802        from sasmodels import models
803        __import__('sasmodels.models.'+model_name)
804        kernel_module = getattr(models, model_name, None)
805    return kernel_module
806
807
808section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
809                            % re.escape(string.punctuation))
810def _convert_section_titles_to_boldface(lines):
811    # type: (Sequence[str]) -> Iterator[str]
812    """
813    Do the actual work of identifying and converting section headings.
814    """
815    prior = None
816    for line in lines:
817        if prior is None:
818            prior = line
819        elif section_marker.match(line):
820            if len(line) >= len(prior):
821                yield "".join(("**", prior, "**"))
822                prior = None
823            else:
824                yield prior
825                prior = line
826        else:
827            yield prior
828            prior = line
829    if prior is not None:
830        yield prior
831
832
833def convert_section_titles_to_boldface(s):
834    # type: (str) -> str
835    """
836    Use explicit bold-face rather than section headings so that the table of
837    contents is not polluted with section names from the model documentation.
838
839    Sections are identified as the title line followed by a line of punctuation
840    at least as long as the title line.
841    """
842    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
843
844
845def make_doc(model_info):
846    # type: (ModelInfo) -> str
847    """
848    Return the documentation for the model.
849    """
850    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
851    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
852    docs = model_info.docs if model_info.docs is not None else ""
853    docs = convert_section_titles_to_boldface(docs)
854    pars = make_partable(model_info.parameters.COMMON
855                         + model_info.parameters.kernel_parameters)
856    subst = dict(id=model_info.id.replace('_', '-'),
857                 name=model_info.name,
858                 title=model_info.title,
859                 parameters=pars,
860                 returns=Sq_units if model_info.structure_factor else Iq_units,
861                 docs=docs)
862    return DOC_HEADER % subst
863
864
865# TODO: need a single source for rst_prolog; it is also in doc/rst_prolog
866RST_PROLOG = """\
867.. |Ang| unicode:: U+212B
868.. |Ang^-1| replace:: |Ang|\ :sup:`-1`
869.. |Ang^2| replace:: |Ang|\ :sup:`2`
870.. |Ang^-2| replace:: |Ang|\ :sup:`-2`
871.. |1e-6Ang^-2| replace:: 10\ :sup:`-6`\ |Ang|\ :sup:`-2`
872.. |Ang^3| replace:: |Ang|\ :sup:`3`
873.. |Ang^-3| replace:: |Ang|\ :sup:`-3`
874.. |Ang^-4| replace:: |Ang|\ :sup:`-4`
875.. |cm^-1| replace:: cm\ :sup:`-1`
876.. |cm^2| replace:: cm\ :sup:`2`
877.. |cm^-2| replace:: cm\ :sup:`-2`
878.. |cm^3| replace:: cm\ :sup:`3`
879.. |1e15cm^3| replace:: 10\ :sup:`15`\ cm\ :sup:`3`
880.. |cm^-3| replace:: cm\ :sup:`-3`
881.. |sr^-1| replace:: sr\ :sup:`-1`
882.. |P0| replace:: P\ :sub:`0`\
883
884.. |equiv| unicode:: U+2261
885.. |noteql| unicode:: U+2260
886.. |TM| unicode:: U+2122
887
888.. |cdot| unicode:: U+00B7
889.. |deg| unicode:: U+00B0
890.. |g/cm^3| replace:: g\ |cdot|\ cm\ :sup:`-3`
891.. |mg/m^2| replace:: mg\ |cdot|\ m\ :sup:`-2`
892.. |fm^2| replace:: fm\ :sup:`2`
893.. |Ang*cm^-1| replace:: |Ang|\ |cdot|\ cm\ :sup:`-1`
894"""
895
896# TODO: make a better fake reference role
897RST_ROLES = """\
898.. role:: ref
899
900.. role:: numref
901
902"""
903
904def make_html(model_info):
905    """
906    Convert model docs directly to html.
907    """
908    from . import rst2html
909
910    rst = make_doc(model_info)
911    return rst2html.rst2html("".join((RST_ROLES, RST_PROLOG, rst)))
912
913def view_html(model_name):
914    from . import rst2html
915    from . import modelinfo
916    kernel_module = load_kernel_module(model_name)
917    info = modelinfo.make_model_info(kernel_module)
918    url = "file://"+dirname(info.filename)+"/"
919    rst2html.wxview(make_html(info), url=url)
920
921def demo_time():
922    # type: () -> None
923    """
924    Show how long it takes to process a model.
925    """
926    import datetime
927    from .modelinfo import make_model_info
928    from .models import cylinder
929
930    tic = datetime.datetime.now()
931    make_source(make_model_info(cylinder))
932    toc = (datetime.datetime.now() - tic).total_seconds()
933    print("time: %g"%toc)
934
935
936def main():
937    # type: () -> None
938    """
939    Program which prints the source produced by the model.
940    """
941    import sys
942    from .modelinfo import make_model_info
943
944    if len(sys.argv) <= 1:
945        print("usage: python -m sasmodels.generate modelname")
946    else:
947        name = sys.argv[1]
948        kernel_module = load_kernel_module(name)
949        model_info = make_model_info(kernel_module)
950        source = make_source(model_info)
951        print(source['dll'])
952
953
954if __name__ == "__main__":
955    main()
Note: See TracBrowser for help on using the repository browser.