source: sasmodels/sasmodels/gen.py @ 14de349

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

add autogenerated polydispersity loops

  • Property mode set to 100644
File size: 13.4 KB
Line 
1__all__ = ["make_opencl"]
2
3import os.path
4
5import numpy as np
6
7from .jsonutil import relaxed_loads
8
9F64 = np.dtype('float64')
10F32 = np.dtype('float32')
11
12# Conversion from units defined in the parameter table for each model
13# to units displayed in the sphinx documentation.
14RST_UNITS = {
15    "Ang": "|Ang|",
16    "1/Ang^2": "|Ang^-2|",
17    "1e-6/Ang^2": "|1e-6Ang^-2|",
18    "degrees": "degree",
19    "1/cm": "|cm^-1|",
20    "": "None",
21    }
22
23# Headers for the parameters tables in th sphinx documentation
24PARTABLE_HEADERS = [
25    "Parameter name",
26    "Units",
27    "Default value",
28    ]
29
30PARTABLE_VALUE_WIDTH = 10
31
32# Scale and background, which are parameters common to every form factor
33COMMON_PARAMETERS = [
34    [ "scale", "", 0, [0, np.inf], "", "Source intensity" ],
35    [ "background", "1/cm", 0, [0, np.inf], "", "Source background" ],
36    ]
37
38
39# Header included before every kernel.
40# This makes sure that the appropriate math constants are defined, and
41KERNEL_HEADER = """\
42// GENERATED CODE --- DO NOT EDIT ---
43// Code is produced by sasmodels.gen from sasmodels/models/MODEL.c
44
45#ifdef __OPENCL_VERSION__
46# define USE_OPENCL
47#endif
48
49// If opencl is not available, then we are compiling a C function
50// Note: if using a C++ compiler, then define kernel as extern "C"
51#ifndef USE_OPENCL
52#  include <math.h>
53#  define REAL(x) (x)
54#  ifndef real
55#      define real double
56#  endif
57#  define global
58#  define local
59#  define constant const
60#  define kernel
61#  define SINCOS(angle,svar,cvar) do {svar=sin(angle);cvar=cos(angle);} while (0)
62#else
63#  ifdef USE_SINCOS
64#    define SINCOS(angle,svar,cvar) svar=sincos(angle,&cvar)
65#  else
66#    define SINCOS(angle,svar,cvar) do {svar=sin(angle);cvar=cos(angle);} while (0)
67#  endif
68#endif
69
70// Standard mathematical constants, prefixed with M_:
71//   E, LOG2E, LOG10E, LN2, LN10, PI, PI_2, PI_4, 1_PI, 2_PI,
72//   2_SQRTPI, SQRT2, SQRT1_2
73// OpenCL defines M_constant_F for float constants, and nothing if double
74// is not enabled on the card, which is why these constants may be missing
75#ifndef M_PI
76#  define M_PI REAL(3.141592653589793)
77#endif
78#ifndef M_PI_2
79#  define M_PI_2 REAL(1.570796326794897)
80#endif
81#ifndef M_PI_4
82#  define M_PI_4 REAL(0.7853981633974483)
83#endif
84
85// Non-standard pi/180, used for converting between degrees and radians
86#ifndef M_PI_180
87#  define M_PI_180 REAL(0.017453292519943295)
88#endif
89"""
90
91
92# The I(q) kernel and the I(qx, qy) kernel have one and two q parameters
93# respectively, so the template builder will need to do extra work to
94# declare, initialize and pass the q parameters.
95IQ_KERNEL = {
96    'fn': "Iq",
97    'q_par_decl': "global const real *q,",
98    'qinit': "const real qi = q[i];",
99    'qcall': "qi",
100    }
101
102IQXY_KERNEL = {
103    'fn': "Iqxy",
104    'q_par_decl': "global const real *qx,\n    global const real *qy,",
105    'qinit': "const real qxi = qx[i];\n    const real qyi = qy[i];",
106    'qcall': "qxi, qyi",
107    }
108
109# Generic kernel template for opencl/openmp.
110# This defines the opencl kernel that is available to the host.  The same
111# structure is used for Iq and Iqxy kernels, so extra flexibility is needed
112# for q parameters.  The polydispersity loop is built elsewhere and
113# substituted into this template.
114KERNEL_TEMPLATE = """\
115kernel void %(name)s(
116    %(q_par_decl)s
117    global real *result,
118#ifdef USE_OPENCL
119    global real *loops_g,
120#else
121    const int Nq,
122#endif
123    local real *loops,
124    const real cutoff,
125    %(par_decl)s
126    )
127{
128#ifdef USE_OPENCL
129  // copy loops info to local memory
130  event_t e = async_work_group_copy(loops, loops_g, (%(pd_length)s)*2, 0);
131  wait_group_events(1, &e);
132
133  int i = get_global_id(0);
134  int Nq = get_global_size(0);
135#endif
136
137#ifdef USE_OPENCL
138  if (i < Nq)
139#else
140  #pragma omp parallel for
141  for (int i=0; i < Nq; i++)
142#endif
143  {
144    %(qinit)s
145    real ret=REAL(0.0), norm=REAL(0.0);
146    real vol=REAL(0.0), norm_vol=REAL(0.0);
147%(loops)s
148    if (vol*norm_vol != REAL(0.0)) {
149      ret *= norm_vol/vol;
150    }
151    result[i] = scale*ret/norm+background;
152  }
153}
154"""
155
156# Polydispersity loop level.
157# This pulls the parameter value and weight from the looping vector in order
158# in preperation for a nested loop.
159LOOP_OPEN="""\
160for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
161  const real %(name)s = loops[2*(%(name)s_i%(offset)s)];
162  const real %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];"""
163
164# Polydispersity loop body.
165# This computes the weight, and if it is sufficient, calls the scattering
166# function and adds it to the total.  If there is a volume normalization,
167# it will also be added here.
168LOOP_BODY="""\
169const real weight = %(weight_product)s;
170if (weight > cutoff) {
171  ret += weight*%(fn)s(%(qcall)s, %(pcall)s);
172  norm += weight;
173  %(volume_norm)s
174}"""
175
176# Volume normalization.
177# If there are "volume" polydispersity parameters, then these will be used
178# to call the volume function from the user supplied kernel, and accumulate
179# a normalized weight.
180VOLUME_NORM="""const real vol_weight = %(weight)s;
181  vol += vol_weight*volume(%(pars)s);
182  norm_vol += vol_weight;"""
183
184def indent(s, depth):
185    """
186    Indent a string of text with *depth* additional spaces on each line.
187    """
188    spaces = " "*depth
189    sep = "\n"+spaces
190    return spaces + sep.join(s.split("\n"))
191
192
193def make_kernel(meta, form):
194    """
195    Build a kernel call from metadata supplied by the user.
196
197    *meta* is the json object defined in the kernel file.
198
199    *form* is either "Iq" or "Iqxy".
200
201    This does not create a complete OpenCL kernel source, only the top
202    level kernel call with polydispersity and a call to the appropriate
203    Iq or Iqxy function.
204    """
205
206    # If we are building the Iqxy kernel, we need to propagate qx,qy
207    # parameters, otherwise we can
208    if form == "Iqxy":
209        qpars = IQXY_KERNEL
210    else:
211        qpars = IQ_KERNEL
212
213    depth = 4
214    offset = ""
215    loop_head = []
216    loop_end = []
217    vol_pars = []
218    fixed_pars = []
219    pd_pars = []
220    fn_pars = []
221    for i,p in enumerate(meta['parameters']):
222        name = p[0]
223        ptype = p[4]
224        if ptype == "volume":
225            vol_pars.append(name)
226        elif ptype == "orientation":
227            if form != "Iqxy": continue  # no orientation for 1D kernels
228        elif ptype == "magnetic":
229            raise NotImplementedError("no magnetic parameters yet")
230        if name not in ['scale','background']: fn_pars.append(name)
231        if ptype == "":
232            fixed_pars.append(name)
233            continue
234        else:
235            pd_pars.append(name)
236        subst = { 'name': name, 'offset': offset }
237        loop_head.append(indent(LOOP_OPEN%subst, depth))
238        loop_end.insert(0, (" "*depth) + "}")
239        offset += '+N'+name
240        depth += 2
241
242    # The volume parameters in the inner loop are used to call the volume()
243    # function in the kernel, with the parameters defined in vol_pars and the
244    # weight product defined in weight.  If there are no volume parameters,
245    # then there will be no volume normalization.
246    if vol_pars:
247        subst = {
248            'weight': "*".join(p+"_w" for p in vol_pars),
249            'pars': ", ".join(vol_pars),
250            }
251        volume_norm = VOLUME_NORM%subst
252    else:
253        volume_norm = ""
254
255    # Define the inner loop function call
256    subst = {
257        'weight_product': "*".join(p+"_w" for p in pd_pars),
258        'volume_norm': volume_norm,
259        'fn': qpars['fn'],
260        'qcall': qpars['qcall'],
261        'pcall': ", ".join(fn_pars),
262        }
263    loop_body = [indent(LOOP_BODY%subst, depth)]
264    loops = "\n".join(loop_head+loop_body+loop_end)
265
266    # declarations for non-pd followed by pd pars
267    # e.g.,
268    #     const real sld,
269    #     const int Nradius
270    fixed_par_decl = ",\n    ".join("const real %s"%p for p in fixed_pars)
271    pd_par_decl = ",\n    ".join("const int N%s"%p for p in pd_pars)
272    if fixed_par_decl and pd_par_decl:
273        par_decl = ",\n    ".join((fixed_par_decl, pd_par_decl))
274    elif fixed_par_decl:
275        par_decl = fixed_par_decl
276    else:
277        par_decl = pd_par_decl
278
279    # Finally, put the pieces together in the kernel.
280    subst = {
281        # kernel name is, e.g., cylinder_Iq
282        'name': "_".join((meta['name'], qpars['fn'])),
283        # to declare, e.g., global real q[],
284        'q_par_decl': qpars['q_par_decl'],
285        # to declare, e.g., real sld, int Nradius, int Nlength
286        'par_decl': par_decl,
287        # to copy global to local pd pars we need, e.g., Nradius+Nlength
288        'pd_length': "+".join('N'+p for p in pd_pars),
289        # the q initializers, e.g., real qi = q[i];
290        'qinit': qpars['qinit'],
291        # the actual polydispersity loop
292        'loops': loops,
293        }
294    kernel = KERNEL_TEMPLATE%subst
295    return kernel
296
297def make_partable(meta):
298    pars = meta['parameters']
299    column_widths = [
300        max(len(p[0]) for p in pars),
301        max(len(RST_UNITS[p[1]]) for p in pars),
302        PARTABLE_VALUE_WIDTH,
303        ]
304    column_widths = [max(w, len(h))
305                     for w,h in zip(column_widths, PARTABLE_HEADERS)]
306
307    sep = " ".join("="*w for w in column_widths)
308    lines = [
309        sep,
310        " ".join("%-*s"%(w,h) for w,h in zip(column_widths, PARTABLE_HEADERS)),
311        sep,
312        ]
313    for p in pars:
314        lines.append(" ".join([
315            "%-*s"%(column_widths[0],p[0]),
316            "%-*s"%(column_widths[1],RST_UNITS[p[1]]),
317            "%*g"%(column_widths[2],p[2]),
318            ]))
319    lines.append(sep)
320    return "\n".join(lines)
321
322def make_doc(kernelfile, meta, doc):
323    doc = doc%{'parameters': make_partable(meta)}
324    return doc
325
326def make_model(kernelfile, meta, source):
327    kernel_Iq = make_kernel(meta, "Iq")
328    kernel_Iqxy = make_kernel(meta, "Iqxy")
329    path = os.path.dirname(kernelfile)
330    extra = [open("%s/%s"%(path,f)).read() for f in meta['include']]
331    kernel = "\n\n".join([KERNEL_HEADER]+extra+[source, kernel_Iq, kernel_Iqxy])
332    return kernel
333
334def parse_file(kernelfile):
335    source = open(kernelfile).read()
336
337    # select parameters out of the source file
338    parts = source.split("PARAMETERS")
339    if len(parts) != 3:
340        raise ValueError("PARAMETERS block missing from %r"%kernelfile)
341    meta_source = parts[1].strip()
342    try:
343        meta = relaxed_loads(meta_source)
344    except:
345        print "in json text:"
346        print "\n".join("%2d: %s"%(i+1,s)
347                        for i,s in enumerate(meta_source.split('\n')))
348        raise
349        #raise ValueError("PARAMETERS block could not be parsed from %r"%kernelfile)
350    meta['parameters'] = COMMON_PARAMETERS + meta['parameters']
351    meta['filename'] = kernelfile
352
353    # select documentation out of the source file
354    parts = source.split("DOCUMENTATION")
355    if len(parts) == 3:
356        doc = make_doc(kernelfile, meta, parts[1].strip())
357    elif len(parts) == 1:
358        raise ValueError("DOCUMENTATION block is missing from %r"%kernelfile)
359    else:
360        raise ValueError("DOCUMENTATION block incorrect from %r"%kernelfile)
361
362    return source, meta, doc
363
364def make(kernelfile):
365    """
366    Build an OpenCL function from the source in *kernelfile*.
367
368    The kernel file needs to define metadata about the parameters.  This
369    will be a JSON definition containing
370    """
371    #print kernelfile
372    source, meta, doc = parse_file(kernelfile)
373    doc = make_doc(kernelfile, meta, doc)
374    model = make_model(kernelfile, meta, source)
375    return model, meta, doc
376
377
378
379# Convert from python float to C float or double, depending on dtype
380FLOAT_CONVERTER = {
381    F32: np.float32,
382    F64: np.float64,
383    }
384
385def kernel_name(meta, is_2D):
386    return meta['name'] + "_" + ("Iqxy" if is_2D else "Iq")
387
388
389def kernel_pars(pars, par_info, is_2D, dtype=F32):
390    """
391    Convert parameter dictionary into arguments for the kernel.
392
393    *pars* is a dictionary of *{ name: value }*, with *name_pd* for the
394    polydispersity width, *name_pd_n* for the number of pd steps, and
395    *name_pd_nsigma* for the polydispersity limits.
396
397    *par_info* is the parameter info structure from the kernel metadata.
398
399    *is_2D* is True if the dataset represents 2D data, with the corresponding
400    orientation parameters.
401
402    *dtype* is F32 or F64, the numpy single and double precision floating
403    point dtypes.  These should not be the strings.
404    """
405    from .weights import GaussianDispersion
406    real = np.float32 if dtype == F32 else np.float64
407    fixed = []
408    parts = []
409    for p in par_info['parameters']:
410        name, ptype = p[0],p[4]
411        value = pars[name]
412        if ptype == "":
413            fixed.append(real(value))
414        elif is_2D or ptype != "orientation":
415            limits, width = p[3], pars[name+'_pd']
416            n, nsigma = pars[name+'_pd_n'], pars[name+'_pd_nsigma']
417            relative = (ptype != "orientation")
418            dist = GaussianDispersion(int(n), width, nsigma)
419            # Make sure that weights are normalized to peaks at 1 so that
420            # the tolerance term can be used properly on truncated distributions
421            v,w = dist.get_weights(value, limits[0], limits[1], relative)
422            parts.append((v, w/w.max()))
423    loops = np.hstack(parts)
424    loops = np.ascontiguousarray(loops.T, dtype).flatten()
425    loopsN = [np.uint32(len(p[0])) for p in parts]
426    return fixed, loops, loopsN
427
428
429def demo_time():
430    import datetime
431    tic = datetime.datetime.now()
432    toc = lambda: (datetime.datetime.now()-tic).total_seconds()
433    path = os.path.dirname("__file__")
434    doc, c = make_model(os.path.join(path, "models", "cylinder.c"))
435    print "time:",toc()
436
437def demo():
438    from os.path import join as joinpath, dirname
439    c, meta, doc = make_model(joinpath(dirname(__file__), "models", "cylinder.c"))
440    #print doc
441    #print c
442
443if __name__ == "__main__":
444    demo()
Note: See TracBrowser for help on using the repository browser.