source: sasmodels/sasmodels/generate.py @ f1ecfa92

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since f1ecfa92 was f1ecfa92, checked in by ajj, 9 years ago

towards fixing the 'models with no polydispersity' problem

  • Property mode set to 100644
File size: 27.7 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.  All functions need prototype declarations
31even if the are defined before they are used.  OpenCL does not support
32*#include* preprocessor directives, so instead the list of includes needs
33to be given as part of the metadata in the kernel module definition.
34The included files should be listed using a path relative to the kernel
35module, or if using "lib/file.c" if it is one of the standard includes
36provided with the sasmodels source.  The includes need to be listed in
37order so that functions are defined before they are used.
38
39Floating point values should be declared as *double*.  For single precision
40calculations, *double* will be replaced by *float*.  The single precision
41conversion will also tag floating point constants with "f" to make them
42single precision constants.  When using integral values in floating point
43expressions, they should be expressed as floating point values by including
44a decimal point.  This includes 0., 1. and 2.
45
46OpenCL has a *sincos* function which can improve performance when both
47the *sin* and *cos* values are needed for a particular argument.  Since
48this function does not exist in C99, all use of *sincos* should be
49replaced by the macro *SINCOS(value,sn,cn)* where *sn* and *cn* are
50previously declared *double* variables.  When compiled for systems without
51OpenCL, *SINCOS* will be replaced by *sin* and *cos* calls.   If *value* is
52an expression, it will appear twice in this case; whether or not it will be
53evaluated twice depends on the quality of the compiler.
54
55If the input parameters are invalid, the scattering calculator should
56return a negative number. Particularly with polydispersity, there are
57some sets of shape parameters which lead to nonsensical forms, such
58as a capped cylinder where the cap radius is smaller than the
59cylinder radius.  The polydispersity calculation will ignore these points,
60effectively chopping the parameter weight distributions at the boundary
61of the infeasible region.  The resulting scattering will be set to
62background.  This will work correctly even when polydispersity is off.
63
64*ER* and *VR* are python functions which operate on parameter vectors.
65The constructor code will generate the necessary vectors for computing
66them with the desired polydispersity.
67
68The available kernel parameters are defined as a list, with each parameter
69defined as a sublist with the following elements:
70
71    *name* is the name that will be used in the call to the kernel
72    function and the name that will be displayed to the user.  Names
73    should be lower case, with words separated by underscore.  If
74    acronyms are used, the whole acronym should be upper case.
75
76    *units* should be one of *degrees* for angles, *Ang* for lengths,
77    *1e-6/Ang^2* for SLDs.
78
79    *default value* will be the initial value for  the model when it
80    is selected, or when an initial value is not otherwise specified.
81
82    [*lb*, *ub*] are the hard limits on the parameter value, used to limit
83    the polydispersity density function.  In the fit, the parameter limits
84    given to the fit are the limits  on the central value of the parameter.
85    If there is polydispersity, it will evaluate parameter values outside
86    the fit limits, but not outside the hard limits specified in the model.
87    If there are no limits, use +/-inf imported from numpy.
88
89    *type* indicates how the parameter will be used.  "volume" parameters
90    will be used in all functions.  "orientation" parameters will be used
91    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
92    *Imagnetic* only.  If *type* is the empty string, the parameter will
93    be used in all of *Iq*, *Iqxy* and *Imagnetic*.
94
95    *description* is a short description of the parameter.  This will
96    be displayed in the parameter table and used as a tool tip for the
97    parameter value in the user interface.
98
99The kernel module must set variables defining the kernel meta data:
100
101    *name* is the model name
102
103    *title* is a short description of the model, suitable for a tool tip,
104    or a one line model summary in a table of models.
105
106    *description* is an extended description of the model to be displayed
107    while the model parameters are being edited.
108
109    *parameters* is the list of parameters.  Parameters in the kernel
110    functions must appear in the same order as they appear in the
111    parameters list.  Two additional parameters, *scale* and *background*
112    are added to the beginning of the parameter list.  They will show up
113    in the documentation as model parameters, but they are never sent to
114    the kernel functions.
115
116    *source* is the list of C-99 source files that must be joined to
117    create the OpenCL kernel functions.  The files defining the functions
118    need to be listed before the files which use the functions.
119
120    *ER* is a python function defining the effective radius.  If it is
121    not present, the effective radius is 0.
122
123    *VR* is a python function defining the volume ratio.  If it is not
124    present, the volume ratio is 1.
125
126    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
127    C source code for the body of the volume, Iq, and Iqxy functions
128    respectively.  These can also be defined in the last source file.
129
130    *Iq* and *Iqxy* also be instead be python functions defining the
131    kernel.  If they are marked as *Iq.vectorized = True* then the
132    kernel is passed the entire *q* vector at once, otherwise it is
133    passed values one *q* at a time.  The performance improvement of
134    this step is significant.
135
136An *info* dictionary is constructed from the kernel meta data and
137returned to the caller.
138
139Additional fields can be defined in the kernel definition file that
140are not needed for sas modelling.
141
142    *demo* is a dictionary of parameter=value defining a set of
143    parameters to use by default when *compare* is called.
144
145    *oldname* is the name of the model in sasview before sasmodels
146    was split into its own package, and *oldpars* is a dictionary
147    of *parameter: old_parameter* pairs defining the new names for
148    the parameters.  This is used by *compare* to check the values
149    of the new model against the values of the old model before
150    you are ready to add the new model to sasmodels.
151
152The model evaluator, function call sequence consists of q inputs and the return vector,
153followed by the loop value/weight vector, followed by the values for
154the non-polydisperse parameters, followed by the lengths of the
155polydispersity loops.  To construct the call for 1D models, the
156categories *fixed-1d* and *pd-1d* list the names of the parameters
157of the non-polydisperse and the polydisperse parameters respectively.
158Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
159The *pd-rel* category is a set of those parameters which give
160polydispersitiy as a portion of the value (so a 10% length dispersity
161would use a polydispersity value of 0.1) rather than absolute
162dispersity such as an angle plus or minus 15 degrees.
163
164The *volume* category lists the volume parameters in order for calls
165to volume within the kernel (used for volume normalization) and for
166calls to ER and VR for effective radius and volume ratio respectively.
167
168The *orientation* and *magnetic* categories list the orientation and
169magnetic parameters.  These are used by the sasview interface.  The
170blank category is for parameters such as scale which don't have any
171other marking.
172
173The doc string at the start of the kernel module will be used to
174construct the model documentation web pages.  Embedded figures should
175appear in the subdirectory "img" beside the model definition, and tagged
176with the kernel module name to avoid collision with other models.  Some
177file systems are case-sensitive, so only use lower case characters for
178file names and extensions.
179
180
181The function :func:`make` loads the metadata from the module and returns
182the kernel source.  The function :func:`doc` extracts the doc string
183and adds the parameter table to the top.  The function :func:`sources`
184returns a list of files required by the model.
185"""
186
187# TODO: identify model files which have changed since loading and reload them.
188
189__all__ = ["make", "doc", "sources", "use_single"]
190
191import sys
192import os
193import os.path
194import re
195
196import numpy as np
197
198F64 = np.dtype('float64')
199F32 = np.dtype('float32')
200
201# Scale and background, which are parameters common to every form factor
202COMMON_PARAMETERS = [
203    [ "scale", "", 1, [0, np.inf], "", "Source intensity" ],
204    [ "background", "1/cm", 0, [0, np.inf], "", "Source background" ],
205    ]
206
207
208# Conversion from units defined in the parameter table for each model
209# to units displayed in the sphinx documentation.
210RST_UNITS = {
211    "Ang": "|Ang|",
212    "1/Ang^2": "|Ang^-2|",
213    "1e-6/Ang^2": "|1e-6Ang^-2|",
214    "degrees": "degree",
215    "1/cm": "|cm^-1|",
216    "": "None",
217    }
218
219# Headers for the parameters tables in th sphinx documentation
220PARTABLE_HEADERS = [
221    "Parameter",
222    "Description",
223    "Units",
224    "Default value",
225    ]
226
227# Minimum width for a default value (this is shorter than the column header
228# width, so will be ignored).
229PARTABLE_VALUE_WIDTH = 10
230
231# Header included before every kernel.
232# This makes sure that the appropriate math constants are defined, and does
233# whatever is required to make the kernel compile as pure C rather than
234# as an OpenCL kernel.
235KERNEL_HEADER = """\
236// GENERATED CODE --- DO NOT EDIT ---
237// Code is produced by sasmodels.gen from sasmodels/models/MODEL.c
238
239#ifdef __OPENCL_VERSION__
240# define USE_OPENCL
241#endif
242
243// If opencl is not available, then we are compiling a C function
244// Note: if using a C++ compiler, then define kernel as extern "C"
245#ifndef USE_OPENCL
246#  ifdef __cplusplus
247     #include <cmath>
248     #if defined(_MSC_VER)
249     #define kernel extern "C" __declspec( dllexport )
250     #else
251     #define kernel extern "C"
252     #endif
253     using namespace std;
254     inline void SINCOS(double angle, double &svar, double &cvar)
255       { svar=sin(angle); cvar=cos(angle); }
256#  else
257     #include <math.h>
258     #if defined(_MSC_VER)
259     #define kernel __declspec( dllexport )
260     #else
261     #define kernel
262     #endif
263     #define SINCOS(angle,svar,cvar) do {svar=sin(angle);cvar=cos(angle);} while (0)
264#  endif
265#  define global
266#  define local
267#  define constant const
268#  define powr(a,b) pow(a,b)
269#  define pown(a,b) pow(a,b)
270#else
271#  ifdef USE_SINCOS
272#    define SINCOS(angle,svar,cvar) svar=sincos(angle,&cvar)
273#  else
274#    define SINCOS(angle,svar,cvar) do {svar=sin(angle);cvar=cos(angle);} while (0)
275#  endif
276#endif
277
278// Standard mathematical constants:
279//   M_E, M_LOG2E, M_LOG10E, M_LN2, M_LN10, M_PI, M_PI_2=pi/2, M_PI_4=pi/4,
280//   M_1_PI=1/pi, M_2_PI=2/pi, M_2_SQRTPI=2/sqrt(pi), SQRT2, SQRT1_2=sqrt(1/2)
281// OpenCL defines M_constant_F for float constants, and nothing if double
282// is not enabled on the card, which is why these constants may be missing
283#ifndef M_PI
284#  define M_PI 3.141592653589793
285#endif
286#ifndef M_PI_2
287#  define M_PI_2 1.570796326794897
288#endif
289#ifndef M_PI_4
290#  define M_PI_4 0.7853981633974483
291#endif
292
293// Non-standard pi/180, used for converting between degrees and radians
294#ifndef M_PI_180
295#  define M_PI_180 0.017453292519943295
296#endif
297"""
298
299
300# The I(q) kernel and the I(qx, qy) kernel have one and two q parameters
301# respectively, so the template builder will need to do extra work to
302# declare, initialize and pass the q parameters.
303KERNEL_1D = {
304    'fn': "Iq",
305    'q_par_decl': "global const double *q,",
306    'qinit': "const double qi = q[i];",
307    'qcall': "qi",
308    'qwork': ["q"],
309    }
310
311KERNEL_2D = {
312    'fn': "Iqxy",
313    'q_par_decl': "global const double *qx,\n    global const double *qy,",
314    'qinit': "const double qxi = qx[i];\n    const double qyi = qy[i];",
315    'qcall': "qxi, qyi",
316    'qwork': ["qx", "qy"],
317    }
318
319# Generic kernel template for the polydispersity loop.
320# This defines the opencl kernel that is available to the host.  The same
321# structure is used for Iq and Iqxy kernels, so extra flexibility is needed
322# for q parameters.  The polydispersity loop is built elsewhere and
323# substituted into this template.
324KERNEL_TEMPLATE = """\
325kernel void %(name)s(
326    %(q_par_decl)s
327    global double *result,
328#ifdef USE_OPENCL
329    global double *loops_g,
330#else
331    const int Nq,
332#endif
333    local double *loops,
334    const double cutoff,
335    %(par_decl)s
336    )
337{
338#ifdef USE_OPENCL
339  // copy loops info to local memory
340  event_t e = async_work_group_copy(loops, loops_g, (%(pd_length)s)*2, 0);
341  wait_group_events(1, &e);
342
343  int i = get_global_id(0);
344  int Nq = get_global_size(0);
345#endif
346
347#ifdef USE_OPENCL
348  if (i < Nq)
349#else
350  #pragma omp parallel for
351  for (int i=0; i < Nq; i++)
352#endif
353  {
354    %(qinit)s
355    double ret=0.0, norm=0.0;
356    double vol=0.0, norm_vol=0.0;
357%(loops)s
358    if (vol*norm_vol != 0.0) {
359      ret *= norm_vol/vol;
360    }
361    result[i] = scale*ret/norm+background;
362  }
363}
364"""
365
366# Polydispersity loop level.
367# This pulls the parameter value and weight from the looping vector in order
368# in preperation for a nested loop.
369LOOP_OPEN="""\
370for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) {
371  const double %(name)s = loops[2*(%(name)s_i%(offset)s)];
372  const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\
373"""
374
375
376
377##########################################################
378#                                                        #
379#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
380#   !!                                              !!   #
381#   !!  KEEP THIS CODE CONSISTENT WITH PYKERNEL.PY  !!   #
382#   !!                                              !!   #
383#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
384#                                                        #
385##########################################################
386
387
388# Polydispersity loop body.
389# This computes the weight, and if it is sufficient, calls the scattering
390# function and adds it to the total.  If there is a volume normalization,
391# it will also be added here.
392LOOP_BODY="""\
393const double weight = %(weight_product)s;
394if (weight > cutoff) {
395  const double I = %(fn)s(%(qcall)s, %(pcall)s);
396  if (I>=0.0) { // scattering cannot be negative
397    ret += weight*I%(sasview_spherical)s;
398    norm += weight;
399    %(volume_norm)s
400  }
401  //else { printf("exclude qx,qy,I:%%g,%%g,%%g\\n",%(qcall)s,I); }
402}
403//else { printf("exclude weight:%%g\\n",weight); }\
404"""
405
406# Use this when integrating over orientation
407SPHERICAL_CORRECTION="""\
408// Correction factor for spherical integration p(theta) I(q) sin(theta) dtheta
409double spherical_correction = (Ntheta>1 ? fabs(sin(M_PI_180*theta)) : 1.0);\
410"""
411# Use this to reproduce sasview behaviour
412SASVIEW_SPHERICAL_CORRECTION="""\
413// Correction factor for spherical integration p(theta) I(q) sin(theta) dtheta
414double spherical_correction = (Ntheta>1 ? fabs(cos(M_PI_180*theta))*M_PI_2 : 1.0);\
415"""
416
417# Volume normalization.
418# If there are "volume" polydispersity parameters, then these will be used
419# to call the form_volume function from the user supplied kernel, and accumulate
420# a normalized weight.
421VOLUME_NORM="""const double vol_weight = %(vol_weight)s;
422    vol += vol_weight*form_volume(%(vol_pars)s);
423    norm_vol += vol_weight;\
424"""
425
426# functions defined as strings in the .py module
427WORK_FUNCTION="""\
428double %(name)s(%(pars)s);
429double %(name)s(%(pars)s)
430{
431%(body)s
432}\
433"""
434
435# Documentation header for the module, giving the model name, its short
436# description and its parameter table.  The remainder of the doc comes
437# from the module docstring.
438DOC_HEADER=""".. _%(name)s:
439
440%(label)s
441=======================================================
442
443%(title)s
444
445%(parameters)s
446
447The returned value is scaled to units of |cm^-1|.
448
449%(docs)s
450"""
451
452def indent(s, depth):
453    """
454    Indent a string of text with *depth* additional spaces on each line.
455    """
456    spaces = " "*depth
457    sep = "\n"+spaces
458    return spaces + sep.join(s.split("\n"))
459
460
461def kernel_name(info, is_2D):
462    """
463    Name of the exported kernel symbol.
464    """
465    return info['name'] + "_" + ("Iqxy" if is_2D else "Iq")
466
467
468def use_single(source):
469    """
470    Convert code from double precision to single precision.
471    """
472    # Convert double keyword to float.  Accept an 'n' parameter for vector
473    # values, where n is 2, 4, 8 or 16. Assume complex numbers are represented
474    # as cdouble which is typedef'd to double2.
475    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
476                    r'\1float\2', source)
477    # Convert floating point constants to single by adding 'f' to the end.
478    # OS/X driver complains if you don't do this.
479    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
480                    r'\g<0>f', source)
481    return source
482
483
484def make_kernel(info, is_2D):
485    """
486    Build a kernel call from metadata supplied by the user.
487
488    *info* is the json object defined in the kernel file.
489
490    *form* is either "Iq" or "Iqxy".
491
492    This does not create a complete OpenCL kernel source, only the top
493    level kernel call with polydispersity and a call to the appropriate
494    Iq or Iqxy function.
495    """
496
497    # If we are building the Iqxy kernel, we need to propagate qx,qy
498    # parameters, otherwise we can
499    dim = "2d" if is_2D else "1d"
500    fixed_pars = info['partype']['fixed-'+dim]
501    pd_pars = info['partype']['pd-'+dim]
502    vol_pars = info['partype']['volume']
503    q_pars = KERNEL_2D if is_2D else KERNEL_1D
504    fn = q_pars['fn']
505
506
507    # Build polydispersity loops
508    depth = 4
509    offset = ""
510    loop_head = []
511    loop_end = []
512    for name in pd_pars:
513        subst = { 'name': name, 'offset': offset }
514        loop_head.append(indent(LOOP_OPEN%subst, depth))
515        loop_end.insert(0, (" "*depth) + "}")
516        offset += '+N'+name
517        depth += 2
518
519    # The volume parameters in the inner loop are used to call the volume()
520    # function in the kernel, with the parameters defined in vol_pars and the
521    # weight product defined in weight.  If there are no volume parameters,
522    # then there will be no volume normalization.
523    if vol_pars:
524        subst = {
525            'vol_weight': "*".join(p+"_w" for p in vol_pars),
526            'vol_pars': ", ".join(vol_pars),
527            }
528        volume_norm = VOLUME_NORM%subst
529    else:
530        volume_norm = ""
531
532    # Define the inner loop function call
533    # The parameters to the f(q,p1,p2...) call should occur in the same
534    # order as given in the parameter info structure.  This may be different
535    # from the parameter order in the call to the kernel since the kernel
536    # call places all fixed parameters before all polydisperse parameters.
537    fq_pars = [p[0] for p in info['parameters'][len(COMMON_PARAMETERS):]
538               if p[0] in set(fixed_pars+pd_pars)]
539    if False and "theta" in pd_pars:
540        spherical_correction = [indent(SPHERICAL_CORRECTION, depth)]
541        weights = [p+"_w" for p in pd_pars]+['spherical_correction']
542        sasview_spherical = ""
543    elif True and "theta" in pd_pars:
544        spherical_correction = [indent(SASVIEW_SPHERICAL_CORRECTION,depth)]
545        weights = [p+"_w" for p in pd_pars]
546        sasview_spherical = "*spherical_correction"
547    else:
548        spherical_correction = []
549        weights = [p+"_w" for p in pd_pars]
550        sasview_spherical = ""
551    weight_product = "*".join(weights) if len(weights) > 1 else "1.0"
552    subst = {
553        'weight_product': weight_product,
554        'volume_norm': volume_norm,
555        'fn': fn,
556        'qcall': q_pars['qcall'],
557        'pcall': ", ".join(fq_pars), # skip scale and background
558        'sasview_spherical': sasview_spherical,
559        }
560    loop_body = [indent(LOOP_BODY%subst, depth)]
561    loops = "\n".join(loop_head+spherical_correction+loop_body+loop_end)
562
563    # declarations for non-pd followed by pd pars
564    # e.g.,
565    #     const double sld,
566    #     const int Nradius
567    fixed_par_decl = ",\n    ".join("const double %s"%p for p in fixed_pars)
568    pd_par_decl = ",\n    ".join("const int N%s"%p for p in pd_pars)
569    if fixed_par_decl and pd_par_decl:
570        par_decl = ",\n    ".join((fixed_par_decl, pd_par_decl))
571    elif fixed_par_decl:
572        par_decl = fixed_par_decl
573    else:
574        par_decl = pd_par_decl
575
576    # Finally, put the pieces together in the kernel.
577    pd_length = "+".join('N'+p for p in pd_pars) if len(pd_pars) > 0 else "0"
578    subst = {
579        # kernel name is, e.g., cylinder_Iq
580        'name': kernel_name(info, is_2D),
581        # to declare, e.g., global double q[],
582        'q_par_decl': q_pars['q_par_decl'],
583        # to declare, e.g., double sld, int Nradius, int Nlength
584        'par_decl': par_decl,
585        # to copy global to local pd pars we need, e.g., Nradius+Nlength
586        'pd_length': pd_length,
587        # the q initializers, e.g., double qi = q[i];
588        'qinit': q_pars['qinit'],
589        # the actual polydispersity loop
590        'loops': loops,
591        }
592    kernel = KERNEL_TEMPLATE%subst
593
594    # If the working function is defined in the kernel metadata as a
595    # string, translate the string to an actual function definition
596    # and put it before the kernel.
597    if info[fn]:
598        subst = {
599            'name': fn,
600            'pars': ", ".join("double "+p for p in q_pars['qwork']+fq_pars),
601            'body': info[fn],
602            }
603        kernel = "\n".join((WORK_FUNCTION%subst, kernel))
604    return kernel
605
606def make_partable(pars):
607    """
608    Generate the parameter table to include in the sphinx documentation.
609    """
610    pars = COMMON_PARAMETERS + pars
611    column_widths = [
612        max(len(p[0]) for p in pars),
613        max(len(p[-1]) for p in pars),
614        max(len(RST_UNITS[p[1]]) for p in pars),
615        PARTABLE_VALUE_WIDTH,
616        ]
617    column_widths = [max(w, len(h))
618                     for w,h in zip(column_widths, PARTABLE_HEADERS)]
619
620    sep = " ".join("="*w for w in column_widths)
621    lines = [
622        sep,
623        " ".join("%-*s"%(w,h) for w,h in zip(column_widths, PARTABLE_HEADERS)),
624        sep,
625        ]
626    for p in pars:
627        lines.append(" ".join([
628            "%-*s"%(column_widths[0],p[0]),
629            "%-*s"%(column_widths[1],p[-1]),
630            "%-*s"%(column_widths[2],RST_UNITS[p[1]]),
631            "%*g"%(column_widths[3],p[2]),
632            ]))
633    lines.append(sep)
634    return "\n".join(lines)
635
636def _search(search_path, filename):
637    """
638    Find *filename* in *search_path*.
639
640    Raises ValueError if file does not exist.
641    """
642    for path in search_path:
643        target = os.path.join(path, filename)
644        if os.path.exists(target):
645            return target
646    raise ValueError("%r not found in %s"%(filename, search_path))
647
648def sources(info):
649    """
650    Return a list of the sources file paths for the module.
651    """
652    from os.path import abspath, dirname, join as joinpath
653    search_path = [ dirname(info['filename']),
654                    abspath(joinpath(dirname(__file__),'models')) ]
655    return [_search(search_path, f) for f in info['source']]
656
657def make_model(info):
658    """
659    Generate the code for the kernel defined by info, using source files
660    found in the given search path.
661    """
662    source = [open(f).read() for f in sources(info)]
663    # If the form volume is defined as a string, then wrap it in a
664    # function definition and place it after the external sources but
665    # before the kernel functions.  If the kernel functions are strings,
666    # they will be translated in the make_kernel call.
667    if info['form_volume']:
668        subst = {
669            'name': "form_volume",
670            'pars': ", ".join("double "+p for p in info['partype']['volume']),
671            'body': info['form_volume'],
672            }
673        source.append(WORK_FUNCTION%subst)
674    kernel_Iq = make_kernel(info, is_2D=False) if not callable(info['Iq']) else ""
675    kernel_Iqxy = make_kernel(info, is_2D=True) if not callable(info['Iqxy']) else ""
676    kernel = "\n\n".join([KERNEL_HEADER]+source+[kernel_Iq, kernel_Iqxy])
677    return kernel
678
679def categorize_parameters(pars):
680    """
681    Build parameter categories out of the the parameter definitions.
682
683    Returns a dictionary of categories.
684    """
685    partype = {
686        'volume': [], 'orientation': [], 'magnetic': [], '': [],
687        'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [],
688        'pd-rel': set(),
689    }
690
691    for p in pars:
692        name,ptype = p[0],p[4]
693        if ptype == 'volume':
694            partype['pd-1d'].append(name)
695            partype['pd-2d'].append(name)
696            partype['pd-rel'].add(name)
697        elif ptype == 'magnetic':
698            partype['fixed-2d'].append(name)
699        elif ptype == 'orientation':
700            partype['pd-2d'].append(name)
701        elif ptype == '':
702            partype['fixed-1d'].append(name)
703            partype['fixed-2d'].append(name)
704        else:
705            raise ValueError("unknown parameter type %r"%ptype)
706        partype[ptype].append(name)
707
708    return partype
709
710def make(kernel_module):
711    """
712    Build an OpenCL/ctypes function from the definition in *kernel_module*.
713
714    The module can be loaded with a normal python import statement if you
715    know which module you need, or with __import__('sasmodels.model.'+name)
716    if the name is in a string.
717    """
718    # TODO: allow Iq and Iqxy to be defined in python
719    from os.path import abspath
720    #print kernelfile
721    info = dict(
722        filename = abspath(kernel_module.__file__),
723        name = kernel_module.name,
724        title = kernel_module.title,
725        description = kernel_module.description,
726        parameters = COMMON_PARAMETERS + kernel_module.parameters,
727        source = getattr(kernel_module, 'source', []),
728        oldname = kernel_module.oldname,
729        oldpars = kernel_module.oldpars,
730        )
731    # Fill in attributes which default to None
732    info.update((k,getattr(kernel_module, k, None))
733                for k in ('ER', 'VR', 'form_volume', 'Iq', 'Iqxy'))
734    # Fill in the derived attributes
735    info['limits'] = dict((p[0],p[3]) for p in info['parameters'])
736    info['partype'] = categorize_parameters(info['parameters'])
737    info['defaults'] = dict((p[0],p[2]) for p in info['parameters'])
738
739    source = make_model(info)
740
741    return source, info
742
743def doc(kernel_module):
744    """
745    Return the documentation for the model.
746    """
747    subst = dict(name=kernel_module.name.replace('_','-'),
748                 label=" ".join(kernel_module.name.split('_')).capitalize(),
749                 title=kernel_module.title,
750                 parameters=make_partable(kernel_module.parameters),
751                 docs=kernel_module.__doc__)
752    return DOC_HEADER%subst
753
754
755
756def demo_time():
757    import datetime
758    from .models import cylinder
759    toc = lambda: (datetime.datetime.now()-tic).total_seconds()
760    tic = datetime.datetime.now()
761    source, info = make(cylinder)
762    print "time:",toc()
763
764def main():
765    if len(sys.argv) <= 1:
766        print "usage: python -m sasmodels.generate modelname"
767    else:
768        name = sys.argv[1]
769        import sasmodels.models
770        __import__('sasmodels.models.'+name)
771        model = getattr(sasmodels.models, name)
772        source, info = make(model); print source
773        #print doc(model)
774
775if __name__ == "__main__":
776    main()
Note: See TracBrowser for help on using the repository browser.