source: sasmodels/sasmodels/gen.py @ ce27e21

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

first pass for sasview wrapper around opencl models

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