source: sasmodels/sasmodels/gen.py @ a7684e5

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

docu updates

  • Property mode set to 100644
File size: 20.2 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.  Floating point values should be
31declared as *real*.  Depending on how the function is called, a macro
32will replace *real* with *float* or *double*.  Unfortunately, MacOSX
33is picky about floating point constants, which should be defined with
34value + 'f' if they are of type *float* or just a bare value if they
35are of type *double*.  The solution is a macro *REAL(value)* which
36adds the 'f' if compiling for single precision floating point.  This
37does make the code ugly, and may someday be replaced by a clever
38regular expression which does the same job.  OpenCL has a *sincos*
39function which can improve performance when both the *sin* and *cos*
40values are needed for a particular argument.  Since this function
41does not exist in C-99, all use of *sincos* should be replaced by the
42macro *SINCOS(value,sn,cn)* where *sn* and *cn* are previously declared
43*real* values.  *value* may be an expression.  When compiled for systems
44without OpenCL, *SINCOS* will be replaced by *sin* and *cos* calls.  All
45functions need prototype declarations even if the are defined before they
46are used -- another present from MacOSX.  OpenCL does not support
47*#include* preprocessor directives; instead the includes must be listed
48in the kernel metadata, with functions defined before they are used.
49The included files should be listed using relative path to the kernel
50source file, or if using one of the standard models, relative to the
51sasmodels source files.
52
53*ER* and *VR* are python functions which operate on parameter vectors.
54The constructor code will generate the necessary vectors for computing
55them with the desired polydispersity.
56
57The kernel module must set variables defining the kernel meta data:
58
59    *name* is the model name
60
61    *title* is a short description of the model, suitable for a tool tip,
62    or a one line model summary in a table of models.
63
64    *description* is an extended description of the model to be displayed
65    while the model parameters are being edited.
66
67    *parameters* is a list of parameters.  Each parameter is itself a
68    list containing *name*, *units*, *default value*,
69    [*lower bound*, *upper bound*], *type* and *description*.
70
71    *source* is the list of C-99 source files that must be joined to
72    create the OpenCL kernel functions.  The files defining the functions
73    need to be listed before the files which use the functions.
74
75    *ER* is a python function defining the effective radius.  If it is
76    not present, the effective radius is 0.
77
78    *VR* is a python function defining the volume ratio.  If it is not
79    present, the volume ratio is 1.
80
81The doc string at the start of the kernel module will be used to
82construct the model documentation web pages.  Embedded figures should
83appear in the subdirectory "img" beside the model definition, and tagged
84with the kernel module name to avoid collision with other models.  Some
85file systems are case-sensitive, so only use lower case characters for
86file names and extensions.
87
88Parameters are defined as follows:
89
90    *name* is the name that will be used in the call to the kernel
91    function and the name that will be displayed to the user.  Names
92    should be lower case, with words separated by underscore.  If
93    acronyms are used, the whole acronym should be upper case.
94
95    *units* should be one of *degrees* for angles, *Ang* for lengths,
96    *1e-6/Ang^2* for SLDs.
97
98    *default value* will be the initial value for  the model when it
99    is selected, or when an initial value is not otherwise specified.
100
101    *limits* are the valid bounds of the parameter, used to limit the
102    polydispersity density function.   In the fit, the parameter limits
103    given to the fit are the limits  on the central value of the parameter.
104    If there is polydispersity, it will evaluate parameter values outside
105    the fit limits, but not outside the hard limits specified in the model.
106    If there are no limits, use +/-inf imported from numpy.
107
108    *type* indicates how the parameter will be used.  "volume" parameters
109    will be used in all functions.  "orientation" parameters will be used
110    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
111    *Imagnetic* only.  If *type* is none, the parameter will be used in
112    all of *Iq*, *Iqxy* and *Imagnetic*.  This will probably be a
113    is the empty string if the parameter is used in all model calculations,
114    it is "volu
115
116    *description* is a short description of the parameter.  This will
117    be displayed in the parameter table and used as a tool tip for the
118    parameter value in the user interface.
119
120The function :func:`make` loads the metadata from the module and returns
121the kernel source.  The function :func:`doc` extracts
122"""
123
124# TODO: identify model files which have changed since loading and reload them.
125
126__all__ = ["make, doc"]
127
128import os.path
129
130import numpy as np
131
132F64 = np.dtype('float64')
133F32 = np.dtype('float32')
134
135# Scale and background, which are parameters common to every form factor
136COMMON_PARAMETERS = [
137    [ "scale", "", 1, [0, np.inf], "", "Source intensity" ],
138    [ "background", "1/cm", 0, [0, np.inf], "", "Source background" ],
139    ]
140
141
142# Conversion from units defined in the parameter table for each model
143# to units displayed in the sphinx documentation.
144RST_UNITS = {
145    "Ang": "|Ang|",
146    "1/Ang^2": "|Ang^-2|",
147    "1e-6/Ang^2": "|1e-6Ang^-2|",
148    "degrees": "degree",
149    "1/cm": "|cm^-1|",
150    "": "None",
151    }
152
153# Headers for the parameters tables in th sphinx documentation
154PARTABLE_HEADERS = [
155    "Parameter name",
156    "Units",
157    "Default value",
158    ]
159
160PARTABLE_VALUE_WIDTH = 10
161
162# Header included before every kernel.
163# This makes sure that the appropriate math constants are defined, and
164KERNEL_HEADER = """\
165// GENERATED CODE --- DO NOT EDIT ---
166// Code is produced by sasmodels.gen from sasmodels/models/MODEL.c
167
168#ifdef __OPENCL_VERSION__
169# define USE_OPENCL
170#endif
171
172// If opencl is not available, then we are compiling a C function
173// Note: if using a C++ compiler, then define kernel as extern "C"
174#ifndef USE_OPENCL
175#  include <math.h>
176#  define REAL(x) (x)
177#  ifndef real
178#      define real double
179#  endif
180#  define global
181#  define local
182#  define constant const
183#  define kernel
184#  define SINCOS(angle,svar,cvar) do {svar=sin(angle);cvar=cos(angle);} while (0)
185#  define powr(a,b) pow(a,b)
186#else
187#  ifdef USE_SINCOS
188#    define SINCOS(angle,svar,cvar) svar=sincos(angle,&cvar)
189#  else
190#    define SINCOS(angle,svar,cvar) do {svar=sin(angle);cvar=cos(angle);} while (0)
191#  endif
192#endif
193
194// Standard mathematical constants, prefixed with M_:
195//   E, LOG2E, LOG10E, LN2, LN10, PI, PI_2, PI_4, 1_PI, 2_PI,
196//   2_SQRTPI, SQRT2, SQRT1_2
197// OpenCL defines M_constant_F for float constants, and nothing if double
198// is not enabled on the card, which is why these constants may be missing
199#ifndef M_PI
200#  define M_PI REAL(3.141592653589793)
201#endif
202#ifndef M_PI_2
203#  define M_PI_2 REAL(1.570796326794897)
204#endif
205#ifndef M_PI_4
206#  define M_PI_4 REAL(0.7853981633974483)
207#endif
208
209// Non-standard pi/180, used for converting between degrees and radians
210#ifndef M_PI_180
211#  define M_PI_180 REAL(0.017453292519943295)
212#endif
213"""
214
215
216# The I(q) kernel and the I(qx, qy) kernel have one and two q parameters
217# respectively, so the template builder will need to do extra work to
218# declare, initialize and pass the q parameters.
219KERNEL_1D = {
220    'fn': "Iq",
221    'q_par_decl': "global const real *q,",
222    'qinit': "const real qi = q[i];",
223    'qcall': "qi",
224    }
225
226KERNEL_2D = {
227    'fn': "Iqxy",
228    'q_par_decl': "global const real *qx,\n    global const real *qy,",
229    'qinit': "const real qxi = qx[i];\n    const real qyi = qy[i];",
230    'qcall': "qxi, qyi",
231    }
232
233# Generic kernel template for opencl/openmp.
234# This defines the opencl kernel that is available to the host.  The same
235# structure is used for Iq and Iqxy kernels, so extra flexibility is needed
236# for q parameters.  The polydispersity loop is built elsewhere and
237# substituted into this template.
238KERNEL_TEMPLATE = """\
239kernel void %(name)s(
240    %(q_par_decl)s
241    global real *result,
242#ifdef USE_OPENCL
243    global real *loops_g,
244#else
245    const int Nq,
246#endif
247    local real *loops,
248    const real cutoff,
249    %(par_decl)s
250    )
251{
252#ifdef USE_OPENCL
253  // copy loops info to local memory
254  event_t e = async_work_group_copy(loops, loops_g, (%(pd_length)s)*2, 0);
255  wait_group_events(1, &e);
256
257  int i = get_global_id(0);
258  int Nq = get_global_size(0);
259#endif
260
261#ifdef USE_OPENCL
262  if (i < Nq)
263#else
264  #pragma omp parallel for
265  for (int i=0; i < Nq; i++)
266#endif
267  {
268    %(qinit)s
269    real ret=REAL(0.0), norm=REAL(0.0);
270    real vol=REAL(0.0), norm_vol=REAL(0.0);
271%(loops)s
272    if (vol*norm_vol != REAL(0.0)) {
273      ret *= norm_vol/vol;
274    }
275    result[i] = scale*ret/norm+background;
276  }
277}
278"""
279
280# Polydispersity loop level.
281# This pulls the parameter value and weight from the looping vector in order
282# in preperation for a nested loop.
283LOOP_OPEN="""\
284for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
285  const real %(name)s = loops[2*(%(name)s_i%(offset)s)];
286  const real %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];"""
287
288# Polydispersity loop body.
289# This computes the weight, and if it is sufficient, calls the scattering
290# function and adds it to the total.  If there is a volume normalization,
291# it will also be added here.
292LOOP_BODY="""\
293const real weight = %(weight_product)s;
294if (weight > cutoff) {
295  ret += weight*%(fn)s(%(qcall)s, %(pcall)s);
296  norm += weight;
297  %(volume_norm)s
298}"""
299
300# Volume normalization.
301# If there are "volume" polydispersity parameters, then these will be used
302# to call the form_volume function from the user supplied kernel, and accumulate
303# a normalized weight.
304VOLUME_NORM="""const real vol_weight = %(weight)s;
305  vol += vol_weight*form_volume(%(pars)s);
306  norm_vol += vol_weight;"""
307
308# Documentation header for the module, giving the model name, its short
309# description and its parameter table.  The remainder of the doc comes
310# from the module docstring.
311DOC_HEADER=""".. _%(name)s:
312
313%(name)s
314=======================================================
315
316%(title)s
317
318%(parameters)s
319
320The returned value is scaled to units of |cm^-1|.
321
322%(docs)s
323"""
324
325def indent(s, depth):
326    """
327    Indent a string of text with *depth* additional spaces on each line.
328    """
329    spaces = " "*depth
330    sep = "\n"+spaces
331    return spaces + sep.join(s.split("\n"))
332
333
334def kernel_name(info, is_2D):
335    return info['name'] + "_" + ("Iqxy" if is_2D else "Iq")
336
337
338def make_kernel(info, is_2D):
339    """
340    Build a kernel call from metadata supplied by the user.
341
342    *info* is the json object defined in the kernel file.
343
344    *form* is either "Iq" or "Iqxy".
345
346    This does not create a complete OpenCL kernel source, only the top
347    level kernel call with polydispersity and a call to the appropriate
348    Iq or Iqxy function.
349    """
350
351    # If we are building the Iqxy kernel, we need to propagate qx,qy
352    # parameters, otherwise we can
353    dim = "2d" if is_2D else "1d"
354    fixed_pars = info['partype']['fixed-'+dim]
355    pd_pars = info['partype']['pd-'+dim]
356    vol_pars = info['partype']['volume']
357    q_pars = KERNEL_2D if is_2D else KERNEL_1D
358
359    # Build polydispersity loops
360    depth = 4
361    offset = ""
362    loop_head = []
363    loop_end = []
364    for name in pd_pars:
365        subst = { 'name': name, 'offset': offset }
366        loop_head.append(indent(LOOP_OPEN%subst, depth))
367        loop_end.insert(0, (" "*depth) + "}")
368        offset += '+N'+name
369        depth += 2
370
371    # The volume parameters in the inner loop are used to call the volume()
372    # function in the kernel, with the parameters defined in vol_pars and the
373    # weight product defined in weight.  If there are no volume parameters,
374    # then there will be no volume normalization.
375    if vol_pars:
376        subst = {
377            'weight': "*".join(p+"_w" for p in vol_pars),
378            'pars': ", ".join(vol_pars),
379            }
380        volume_norm = VOLUME_NORM%subst
381    else:
382        volume_norm = ""
383
384    # Define the inner loop function call
385    # The parameters to the f(q,p1,p2...) call should occur in the same
386    # order as given in the parameter info structure.  This may be different
387    # from the parameter order in the call to the kernel since the kernel
388    # call places all fixed parameters before all polydisperse parameters.
389    fq_pars = [p[0] for p in info['parameters'][len(COMMON_PARAMETERS):]
390               if p[0] in set(fixed_pars+pd_pars)]
391    subst = {
392        'weight_product': "*".join(p+"_w" for p in pd_pars),
393        'volume_norm': volume_norm,
394        'fn': q_pars['fn'],
395        'qcall': q_pars['qcall'],
396        'pcall': ", ".join(fq_pars), # skip scale and background
397        }
398    loop_body = [indent(LOOP_BODY%subst, depth)]
399    loops = "\n".join(loop_head+loop_body+loop_end)
400
401    # declarations for non-pd followed by pd pars
402    # e.g.,
403    #     const real sld,
404    #     const int Nradius
405    fixed_par_decl = ",\n    ".join("const real %s"%p for p in fixed_pars)
406    pd_par_decl = ",\n    ".join("const int N%s"%p for p in pd_pars)
407    if fixed_par_decl and pd_par_decl:
408        par_decl = ",\n    ".join((fixed_par_decl, pd_par_decl))
409    elif fixed_par_decl:
410        par_decl = fixed_par_decl
411    else:
412        par_decl = pd_par_decl
413
414    # Finally, put the pieces together in the kernel.
415    subst = {
416        # kernel name is, e.g., cylinder_Iq
417        'name': kernel_name(info, is_2D),
418        # to declare, e.g., global real q[],
419        'q_par_decl': q_pars['q_par_decl'],
420        # to declare, e.g., real sld, int Nradius, int Nlength
421        'par_decl': par_decl,
422        # to copy global to local pd pars we need, e.g., Nradius+Nlength
423        'pd_length': "+".join('N'+p for p in pd_pars),
424        # the q initializers, e.g., real qi = q[i];
425        'qinit': q_pars['qinit'],
426        # the actual polydispersity loop
427        'loops': loops,
428        }
429    kernel = KERNEL_TEMPLATE%subst
430    return kernel
431
432def make_partable(info):
433    pars = info['parameters']
434    column_widths = [
435        max(len(p[0]) for p in pars),
436        max(len(RST_UNITS[p[1]]) for p in pars),
437        PARTABLE_VALUE_WIDTH,
438        ]
439    column_widths = [max(w, len(h))
440                     for w,h in zip(column_widths, PARTABLE_HEADERS)]
441
442    sep = " ".join("="*w for w in column_widths)
443    lines = [
444        sep,
445        " ".join("%-*s"%(w,h) for w,h in zip(column_widths, PARTABLE_HEADERS)),
446        sep,
447        ]
448    for p in pars:
449        lines.append(" ".join([
450            "%-*s"%(column_widths[0],p[0]),
451            "%-*s"%(column_widths[1],RST_UNITS[p[1]]),
452            "%*g"%(column_widths[2],p[2]),
453            ]))
454    lines.append(sep)
455    return "\n".join(lines)
456
457def _search(search_path, filename):
458    """
459    Find *filename* in *search_path*.
460
461    Raises ValueError if file does not exist.
462    """
463    for path in search_path:
464        target = os.path.join(path, filename)
465        if os.path.exists(target):
466            return target
467    raise ValueError("%r not found in %s"%(filename, search_path))
468
469def make_model(search_path, info):
470    kernel_Iq = make_kernel(info, is_2D=False)
471    kernel_Iqxy = make_kernel(info, is_2D=True)
472    source = [open(_search(search_path, f)).read() for f in info['source']]
473    kernel = "\n\n".join([KERNEL_HEADER]+source+[kernel_Iq, kernel_Iqxy])
474    return kernel
475
476def categorize_parameters(pars):
477    """
478    Build parameter categories out of the the parameter definitions.
479
480    Returns a dictionary of categories.
481
482    The function call sequence consists of q inputs and the return vector,
483    followed by the loop value/weight vector, followed by the values for
484    the non-polydisperse parameters, followed by the lengths of the
485    polydispersity loops.  To construct the call for 1D models, the
486    categories *fixed-1d* and *pd-1d* list the names of the parameters
487    of the non-polydisperse and the polydisperse parameters respectively.
488    Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
489    The *pd-rel* category is a set of those parameters which give
490    polydispersitiy as a portion of the value (so a 10% length dispersity
491    would use a polydispersity value of 0.1) rather than absolute
492    dispersity such as an angle plus or minus 15 degrees.
493
494    The *volume* category lists the volume parameters in order for calls
495    to volume within the kernel (used for volume normalization) and for
496    calls to ER and VR for effective radius and volume ratio respectively.
497
498    The *orientation* and *magnetic* categories list the orientation and
499    magnetic parameters.  These are used by the sasview interface.  The
500    blank category is for parameters such as scale which don't have any
501    other marking.
502    """
503    partype = {
504        'volume': [], 'orientation': [], 'magnetic': [], '': [],
505        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
506        'pd-rel': set(),
507    }
508
509    for p in pars:
510        name,ptype = p[0],p[4]
511        if ptype == 'volume':
512            partype['pd-1d'].append(name)
513            partype['pd-2d'].append(name)
514            partype['pd-rel'].add(name)
515        elif ptype == 'magnetic':
516            partype['fixed-2d'].append(name)
517        elif ptype == 'orientation':
518            partype['pd-2d'].append(name)
519        elif ptype == '':
520            partype['fixed-1d'].append(name)
521            partype['fixed-2d'].append(name)
522        else:
523            raise ValueError("unknown parameter type %r"%ptype)
524        partype[ptype].append(name)
525
526    return partype
527
528def make(kernel_module):
529    """
530    Build an OpenCL/ctypes function from the definition in *kernel_module*.
531
532    The module can be loaded with a normal python import statement if you
533    know which module you need, or with __import__('sasmodels.model.'+name)
534    if the name is in a string.
535    """
536    # TODO: allow Iq and Iqxy to be defined in python
537    from os.path import abspath, dirname, join as joinpath
538    #print kernelfile
539    info = dict(
540        filename = abspath(kernel_module.__file__),
541        name = kernel_module.name,
542        title = kernel_module.title,
543        source = kernel_module.source,
544        description = kernel_module.description,
545        parameters = COMMON_PARAMETERS + kernel_module.parameters,
546        ER = getattr(kernel_module, 'ER', None),
547        VR = getattr(kernel_module, 'VR', None),
548        )
549    info['limits'] = dict((p[0],p[3]) for p in info['parameters'])
550    info['partype'] = categorize_parameters(info['parameters'])
551
552    search_path = [ dirname(info['filename']),
553                    abspath(joinpath(dirname(__file__),'models')) ]
554    source = make_model(search_path, info)
555
556    return source, info
557
558def doc(kernel_module):
559    """
560    Return the documentation for the model.
561    """
562    subst = dict(name=kernel_module.name,
563                 title=kernel_module.title,
564                 parameters=make_partable(kernel_module.parameters),
565                 doc=kernel_module.__doc__)
566    return DOC_HEADER%subst
567
568
569def demo_time():
570    import datetime
571    tic = datetime.datetime.now()
572    toc = lambda: (datetime.datetime.now()-tic).total_seconds()
573    path = os.path.dirname("__file__")
574    doc, c = make_model(os.path.join(path, "models", "cylinder.c"))
575    print "time:",toc()
576
577def demo():
578    from os.path import join as joinpath, dirname
579    c, info, doc = make_model(joinpath(dirname(__file__), "models", "cylinder.c"))
580    #print doc
581    #print c
582
583if __name__ == "__main__":
584    demo()
Note: See TracBrowser for help on using the repository browser.