source: sasmodels/sasmodels/gen.py @ 32c160a

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

support ER/VR python kernels; move metadata to python

  • Property mode set to 100644
File size: 13.6 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 _search(search_path, filename):
319    """
320    Find *filename* in *search_path*.
321
322    Raises ValueError if file does not exist.
323    """
324    for path in search_path:
325        target = os.path.join(path, filename)
326        if os.path.exists(target):
327            return target
328    raise ValueError("%r not found in %s"%(filename, search_path))
329
330def make_model(search_path, info):
331    kernel_Iq = make_kernel(info, is_2D=False)
332    kernel_Iqxy = make_kernel(info, is_2D=True)
333    source = [open(_search(search_path, f)).read() for f in info['source']]
334    kernel = "\n\n".join([KERNEL_HEADER]+source+[kernel_Iq, kernel_Iqxy])
335    return kernel
336
337def categorize_parameters(pars):
338    """
339    Build parameter categories out of the the parameter definitions.
340
341    Returns a dictionary of categories.
342
343    The function call sequence consists of q inputs and the return vector,
344    followed by the loop value/weight vector, followed by the values for
345    the non-polydisperse parameters, followed by the lengths of the
346    polydispersity loops.  To construct the call for 1D models, the
347    categories *fixed-1d* and *pd-1d* list the names of the parameters
348    of the non-polydisperse and the polydisperse parameters respectively.
349    Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
350    The *pd-rel* category is a set of those parameters which give
351    polydispersitiy as a portion of the value (so a 10% length dispersity
352    would use a polydispersity value of 0.1) rather than absolute
353    dispersity such as an angle plus or minus 15 degrees.
354
355    The *volume* category lists the volume parameters in order for calls
356    to volume within the kernel (used for volume normalization) and for
357    calls to ER and VR for effective radius and volume ratio respectively.
358
359    The *orientation* and *magnetic* categories list the orientation and
360    magnetic parameters.  These are used by the sasview interface.  The
361    blank category is for parameters such as scale which don't have any
362    other marking.
363    """
364    partype = {
365        'volume': [], 'orientation': [], 'magnetic': [], '': [],
366        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
367        'pd-rel': set(),
368    }
369
370    for p in pars:
371        name,ptype = p[0],p[4]
372        if ptype == 'volume':
373            partype['pd-1d'].append(name)
374            partype['pd-2d'].append(name)
375            partype['pd-rel'].add(name)
376        elif ptype == 'magnetic':
377            partype['fixed-2d'].append(name)
378        elif ptype == 'orientation':
379            partype['pd-2d'].append(name)
380        elif ptype == '':
381            partype['fixed-1d'].append(name)
382            partype['fixed-2d'].append(name)
383        else:
384            raise ValueError("unknown parameter type %r"%ptype)
385        partype[ptype].append(name)
386
387    return partype
388
389def make(kernel_module):
390    """
391    Build an OpenCL function from the source in *kernelfile*.
392
393    The kernel file needs to define metadata about the parameters.  This
394    will be a JSON definition containing
395    """
396    # TODO: allow Iq and Iqxy to be defined in python
397    from os.path import abspath, dirname, join as joinpath
398    #print kernelfile
399    info = dict(
400        filename = abspath(kernel_module.__file__),
401        name = kernel_module.name,
402        title = kernel_module.title,
403        source = kernel_module.source,
404        description = kernel_module.description,
405        parameters = COMMON_PARAMETERS + kernel_module.parameters,
406        ER = getattr(kernel_module, 'ER', None),
407        VR = getattr(kernel_module, 'VR', None),
408        )
409    info['limits'] = dict((p[0],p[3]) for p in info['parameters'])
410    info['partype'] = categorize_parameters(info['parameters'])
411
412    search_path = [ dirname(info['filename']),
413                    abspath(joinpath(dirname(__file__),'models')) ]
414    source = make_model(search_path, info)
415
416    return source, info
417
418
419def demo_time():
420    import datetime
421    tic = datetime.datetime.now()
422    toc = lambda: (datetime.datetime.now()-tic).total_seconds()
423    path = os.path.dirname("__file__")
424    doc, c = make_model(os.path.join(path, "models", "cylinder.c"))
425    print "time:",toc()
426
427def demo():
428    from os.path import join as joinpath, dirname
429    c, info, doc = make_model(joinpath(dirname(__file__), "models", "cylinder.c"))
430    #print doc
431    #print c
432
433if __name__ == "__main__":
434    demo()
Note: See TracBrowser for help on using the repository browser.