source: sasmodels/sasmodels/generate.py @ a22d104

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since a22d104 was a22d104, checked in by krzywon, 6 years ago

If cannot find built-in model, try plugin path.

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