source: sasmodels/sasmodels/generate.py @ 6e7ff6d

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

reenable python models

  • Property mode set to 100644
File size: 31.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, or 1.0 if no volume normalization is required.
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
24    #define INVALID(v) (expr)  returns False if v.parameter is invalid
25    for some parameter or other (e.g., v.bell_radius < v.radius).  If
26    necessary, the expression can call a function.
27
28These functions are defined in a kernel module .py script and an associated
29set of .c files.  The model constructor will use them to create models with
30polydispersity across volume and orientation parameters, and provide
31scale and background parameters for each model.
32
33*Iq*, *Iqxy*, *Imagnetic* and *form_volume* should be stylized C-99
34functions written for OpenCL.  All functions need prototype declarations
35even if the are defined before they are used.  OpenCL does not support
36*#include* preprocessor directives, so instead the list of includes needs
37to be given as part of the metadata in the kernel module definition.
38The included files should be listed using a path relative to the kernel
39module, or if using "lib/file.c" if it is one of the standard includes
40provided with the sasmodels source.  The includes need to be listed in
41order so that functions are defined before they are used.
42
43Floating point values should be declared as *double*.  For single precision
44calculations, *double* will be replaced by *float*.  The single precision
45conversion will also tag floating point constants with "f" to make them
46single precision constants.  When using integral values in floating point
47expressions, they should be expressed as floating point values by including
48a decimal point.  This includes 0., 1. and 2.
49
50OpenCL has a *sincos* function which can improve performance when both
51the *sin* and *cos* values are needed for a particular argument.  Since
52this function does not exist in C99, all use of *sincos* should be
53replaced by the macro *SINCOS(value, sn, cn)* where *sn* and *cn* are
54previously declared *double* variables.  When compiled for systems without
55OpenCL, *SINCOS* will be replaced by *sin* and *cos* calls.   If *value* is
56an expression, it will appear twice in this case; whether or not it will be
57evaluated twice depends on the quality of the compiler.
58
59If the input parameters are invalid, the scattering calculator should
60return a negative number. Particularly with polydispersity, there are
61some sets of shape parameters which lead to nonsensical forms, such
62as a capped cylinder where the cap radius is smaller than the
63cylinder radius.  The polydispersity calculation will ignore these points,
64effectively chopping the parameter weight distributions at the boundary
65of the infeasible region.  The resulting scattering will be set to
66background.  This will work correctly even when polydispersity is off.
67
68*ER* and *VR* are python functions which operate on parameter vectors.
69The constructor code will generate the necessary vectors for computing
70them with the desired polydispersity.
71The kernel module must set variables defining the kernel meta data:
72
73    *id* is an implicit variable formed from the filename.  It will be
74    a valid python identifier, and will be used as the reference into
75    the html documentation, with '_' replaced by '-'.
76
77    *name* is the model name as displayed to the user.  If it is missing,
78    it will be constructed from the id.
79
80    *title* is a short description of the model, suitable for a tool tip,
81    or a one line model summary in a table of models.
82
83    *description* is an extended description of the model to be displayed
84    while the model parameters are being edited.
85
86    *parameters* is the list of parameters.  Parameters in the kernel
87    functions must appear in the same order as they appear in the
88    parameters list.  Two additional parameters, *scale* and *background*
89    are added to the beginning of the parameter list.  They will show up
90    in the documentation as model parameters, but they are never sent to
91    the kernel functions.  Note that *effect_radius* and *volfraction*
92    must occur first in structure factor calculations.
93
94    *category* is the default category for the model.  The category is
95    two level structure, with the form "group:section", indicating where
96    in the manual the model will be located.  Models are alphabetical
97    within their section.
98
99    *source* is the list of C-99 source files that must be joined to
100    create the OpenCL kernel functions.  The files defining the functions
101    need to be listed before the files which use the functions.
102
103    *ER* is a python function defining the effective radius.  If it is
104    not present, the effective radius is 0.
105
106    *VR* is a python function defining the volume ratio.  If it is not
107    present, the volume ratio is 1.
108
109    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
110    C source code for the body of the volume, Iq, and Iqxy functions
111    respectively.  These can also be defined in the last source file.
112
113    *Iq* and *Iqxy* also be instead be python functions defining the
114    kernel.  If they are marked as *Iq.vectorized = True* then the
115    kernel is passed the entire *q* vector at once, otherwise it is
116    passed values one *q* at a time.  The performance improvement of
117    this step is significant.
118
119    *demo* is a dictionary of parameter=value defining a set of
120    parameters to use by default when *compare* is called.  Any
121    parameter not set in *demo* gets the initial value from the
122    parameter list.  *demo* is mostly needed to set the default
123    polydispersity values for tests.
124
125An *model_info* dictionary is constructed from the kernel meta data and
126returned to the caller.
127
128The doc string at the start of the kernel module will be used to
129construct the model documentation web pages.  Embedded figures should
130appear in the subdirectory "img" beside the model definition, and tagged
131with the kernel module name to avoid collision with other models.  Some
132file systems are case-sensitive, so only use lower case characters for
133file names and extensions.
134
135
136The function :func:`make` loads the metadata from the module and returns
137the kernel source.  The function :func:`make_doc` extracts the doc string
138and adds the parameter table to the top.  The function :func:`model_sources`
139returns a list of files required by the model.
140
141Code follows the C99 standard with the following extensions and conditions::
142
143    M_PI_180 = pi/180
144    M_4PI_3 = 4pi/3
145    square(x) = x*x
146    cube(x) = x*x*x
147    sinc(x) = sin(x)/x, with sin(0)/0 -> 1
148    all double precision constants must include the decimal point
149    all double declarations may be converted to half, float, or long double
150    FLOAT_SIZE is the number of bytes in the converted variables
151"""
152from __future__ import print_function
153
154#TODO: determine which functions are useful outside of generate
155#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
156
157from os.path import abspath, dirname, join as joinpath, exists, basename, \
158    splitext, getmtime
159import re
160import string
161import warnings
162
163import numpy as np
164
165from .modelinfo import ModelInfo, Parameter, make_parameter_table, set_demo
166from .custom import load_custom_kernel_module
167
168TEMPLATE_ROOT = dirname(__file__)
169
170F16 = np.dtype('float16')
171F32 = np.dtype('float32')
172F64 = np.dtype('float64')
173try:  # CRUFT: older numpy does not support float128
174    F128 = np.dtype('float128')
175except TypeError:
176    F128 = None
177
178# Conversion from units defined in the parameter table for each model
179# to units displayed in the sphinx documentation.
180RST_UNITS = {
181    "Ang": "|Ang|",
182    "1/Ang": "|Ang^-1|",
183    "1/Ang^2": "|Ang^-2|",
184    "1e-6/Ang^2": "|1e-6Ang^-2|",
185    "degrees": "degree",
186    "1/cm": "|cm^-1|",
187    "Ang/cm": "|Ang*cm^-1|",
188    "g/cm3": "|g/cm^3|",
189    "mg/m2": "|mg/m^2|",
190    "": "None",
191    }
192
193# Headers for the parameters tables in th sphinx documentation
194PARTABLE_HEADERS = [
195    "Parameter",
196    "Description",
197    "Units",
198    "Default value",
199    ]
200
201# Minimum width for a default value (this is shorter than the column header
202# width, so will be ignored).
203PARTABLE_VALUE_WIDTH = 10
204
205# Documentation header for the module, giving the model name, its short
206# description and its parameter table.  The remainder of the doc comes
207# from the module docstring.
208DOC_HEADER = """.. _%(id)s:
209
210%(name)s
211=======================================================
212
213%(title)s
214
215%(parameters)s
216
217%(returns)s
218
219%(docs)s
220"""
221
222def format_units(units):
223    """
224    Convert units into ReStructured Text format.
225    """
226    return "string" if isinstance(units, list) else RST_UNITS.get(units, units)
227
228def make_partable(pars):
229    """
230    Generate the parameter table to include in the sphinx documentation.
231    """
232    column_widths = [
233        max(len(p.name) for p in pars),
234        max(len(p.description) for p in pars),
235        max(len(format_units(p.units)) for p in pars),
236        PARTABLE_VALUE_WIDTH,
237        ]
238    column_widths = [max(w, len(h))
239                     for w, h in zip(column_widths, PARTABLE_HEADERS)]
240
241    sep = " ".join("="*w for w in column_widths)
242    lines = [
243        sep,
244        " ".join("%-*s" % (w, h)
245                 for w, h in zip(column_widths, PARTABLE_HEADERS)),
246        sep,
247        ]
248    for p in pars:
249        lines.append(" ".join([
250            "%-*s" % (column_widths[0], p.name),
251            "%-*s" % (column_widths[1], p.description),
252            "%-*s" % (column_widths[2], format_units(p.units)),
253            "%*g" % (column_widths[3], p.default),
254            ]))
255    lines.append(sep)
256    return "\n".join(lines)
257
258def _search(search_path, filename):
259    """
260    Find *filename* in *search_path*.
261
262    Raises ValueError if file does not exist.
263    """
264    for path in search_path:
265        target = joinpath(path, filename)
266        if exists(target):
267            return target
268    raise ValueError("%r not found in %s" % (filename, search_path))
269
270
271def model_sources(model_info):
272    """
273    Return a list of the sources file paths for the module.
274    """
275    search_path = [dirname(model_info['filename']),
276                   abspath(joinpath(dirname(__file__), 'models'))]
277    return [_search(search_path, f) for f in model_info['source']]
278
279def timestamp(model_info):
280    """
281    Return a timestamp for the model corresponding to the most recently
282    changed file or dependency.
283    """
284    source_files = (model_sources(model_info)
285                    + model_templates()
286                    + [model_info['filename']])
287    newest = max(getmtime(f) for f in source_files)
288    return newest
289
290def convert_type(source, dtype):
291    """
292    Convert code from double precision to the desired type.
293
294    Floating point constants are tagged with 'f' for single precision or 'L'
295    for long double precision.
296    """
297    if dtype == F16:
298        fbytes = 2
299        source = _convert_type(source, "half", "f")
300    elif dtype == F32:
301        fbytes = 4
302        source = _convert_type(source, "float", "f")
303    elif dtype == F64:
304        fbytes = 8
305        # no need to convert if it is already double
306    elif dtype == F128:
307        fbytes = 16
308        source = _convert_type(source, "long double", "L")
309    else:
310        raise ValueError("Unexpected dtype in source conversion: %s"%dtype)
311    return ("#define FLOAT_SIZE %d\n"%fbytes)+source
312
313
314def _convert_type(source, type_name, constant_flag):
315    """
316    Replace 'double' with *type_name* in *source*, tagging floating point
317    constants with *constant_flag*.
318    """
319    # Convert double keyword to float/long double/half.
320    # Accept an 'n' # parameter for vector # values, where n is 2, 4, 8 or 16.
321    # Assume complex numbers are represented as cdouble which is typedef'd
322    # to double2.
323    source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
324                    r'\1%s\2'%type_name, source)
325    # Convert floating point constants to single by adding 'f' to the end,
326    # or long double with an 'L' suffix.  OS/X complains if you don't do this.
327    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
328                    r'\g<0>%s'%constant_flag, source)
329    return source
330
331
332def kernel_name(model_info, is_2d):
333    """
334    Name of the exported kernel symbol.
335    """
336    return model_info['name'] + "_" + ("Iqxy" if is_2d else "Iq")
337
338
339def indent(s, depth):
340    """
341    Indent a string of text with *depth* additional spaces on each line.
342    """
343    spaces = " "*depth
344    sep = "\n" + spaces
345    return spaces + sep.join(s.split("\n"))
346
347
348_template_cache = {}
349def load_template(filename):
350    path = joinpath(TEMPLATE_ROOT, filename)
351    mtime = getmtime(path)
352    if filename not in _template_cache or mtime > _template_cache[filename][0]:
353        with open(path) as fid:
354            _template_cache[filename] = (mtime, fid.read(), path)
355    return _template_cache[filename][1]
356
357def model_templates():
358    # TODO: fails DRY; templates are listed in two places.
359    # should instead have model_info contain a list of paths
360    return [joinpath(TEMPLATE_ROOT, filename)
361            for filename in ('kernel_header.c', 'kernel_iq.c')]
362
363
364_FN_TEMPLATE = """\
365double %(name)s(%(pars)s);
366double %(name)s(%(pars)s) {
367    %(body)s
368}
369
370
371"""
372
373def _gen_fn(name, pars, body):
374    """
375    Generate a function given pars and body.
376
377    Returns the following string::
378
379         double fn(double a, double b, ...);
380         double fn(double a, double b, ...) {
381             ....
382         }
383    """
384    par_decl = ', '.join(p.as_function_argument() for p in pars) if pars else 'void'
385    return _FN_TEMPLATE % {'name': name, 'body': body, 'pars': par_decl}
386
387def _call_pars(prefix, pars):
388    """
389    Return a list of *prefix.parameter* from parameter items.
390    """
391    return [p.as_call_reference(prefix) for p in pars]
392
393_IQXY_PATTERN = re.compile("^((inline|static) )? *(double )? *Iqxy *([(]|$)",
394                           flags=re.MULTILINE)
395def _have_Iqxy(sources):
396    """
397    Return true if any file defines Iqxy.
398
399    Note this is not a C parser, and so can be easily confused by
400    non-standard syntax.  Also, it will incorrectly identify the following
401    as having Iqxy::
402
403        /*
404        double Iqxy(qx, qy, ...) { ... fill this in later ... }
405        */
406
407    If you want to comment out an Iqxy function, use // on the front of the
408    line instead.
409    """
410    for code in sources:
411        if _IQXY_PATTERN.search(code):
412            return True
413    else:
414        return False
415
416def make_source(model_info):
417    """
418    Generate the OpenCL/ctypes kernel from the module info.
419
420    Uses source files found in the given search path.
421    """
422    if callable(model_info['Iq']):
423        return None
424
425    # TODO: need something other than volume to indicate dispersion parameters
426    # No volume normalization despite having a volume parameter.
427    # Thickness is labelled a volume in order to trigger polydispersity.
428    # May want a separate dispersion flag, or perhaps a separate category for
429    # disperse, but not volume.  Volume parameters also use relative values
430    # for the distribution rather than the absolute values used by angular
431    # dispersion.  Need to be careful that necessary parameters are available
432    # for computing volume even if we allow non-disperse volume parameters.
433
434    partable = model_info['parameters']
435
436    # Identify parameters for Iq, Iqxy, Iq_magnetic and form_volume.
437    # Note that scale and volume are not possible types.
438
439    # Load templates and user code
440    kernel_header = load_template('kernel_header.c')
441    kernel_code = load_template('kernel_iq.c')
442    user_code = [open(f).read() for f in model_sources(model_info)]
443
444    # Build initial sources
445    source = [kernel_header] + user_code
446
447    # Make parameters for q, qx, qy so that we can use them in declarations
448    q, qx, qy = [Parameter(name=v) for v in ('q', 'qx', 'qy')]
449    # Generate form_volume function, etc. from body only
450    if model_info['form_volume'] is not None:
451        pars = partable.form_volume_parameters
452        source.append(_gen_fn('form_volume', pars, model_info['form_volume']))
453    if model_info['Iq'] is not None:
454        pars = [q] + partable.iq_parameters
455        source.append(_gen_fn('Iq', pars, model_info['Iq']))
456    if model_info['Iqxy'] is not None:
457        pars = [qx, qy] + partable.iqxy_parameters
458        source.append(_gen_fn('Iqxy', pars, model_info['Iqxy']))
459
460    # Define the parameter table
461    source.append("#define PARAMETER_TABLE \\")
462    source.append("\\\n".join(p.as_definition()
463                              for p in partable.kernel_parameters))
464
465    # Define the function calls
466    if partable.form_volume_parameters:
467        refs = _call_pars("_v.", partable.form_volume_parameters)
468        call_volume = "#define CALL_VOLUME(_v) form_volume(%s)" % (",".join(refs))
469    else:
470        # Model doesn't have volume.  We could make the kernel run a little
471        # faster by not using/transferring the volume normalizations, but
472        # the ifdef's reduce readability more than is worthwhile.
473        call_volume = "#define CALL_VOLUME(v) 1.0"
474    source.append(call_volume)
475
476    refs = ["_q[_i]"] + _call_pars("_v.", partable.iq_parameters)
477    call_iq = "#define CALL_IQ(_q,_i,_v) Iq(%s)" % (",".join(refs))
478    if _have_Iqxy(user_code):
479        # Call 2D model
480        refs = ["q[2*i]", "q[2*i+1]"] + _call_pars("_v.", partable.iqxy_parameters)
481        call_iqxy = "#define CALL_IQ(_q,_i,_v) Iqxy(%s)" % (",".join(refs))
482    else:
483        # Call 1D model with sqrt(qx^2 + qy^2)
484        warnings.warn("Creating Iqxy = Iq(sqrt(qx^2 + qy^2))")
485        # still defined:: refs = ["q[i]"] + _call_pars("v", iq_parameters)
486        pars_sqrt = ["sqrt(_q[2*_i]*_q[2*_i]+_q[2*_i+1]*_q[2*_i+1])"] + refs[1:]
487        call_iqxy = "#define CALL_IQ(_q,_i,_v) Iq(%s)" % (",".join(pars_sqrt))
488
489    # Fill in definitions for numbers of parameters
490    source.append("#define MAX_PD %s"%partable.max_pd)
491    source.append("#define NPARS %d"%partable.npars)
492
493    # TODO: allow mixed python/opencl kernels?
494
495    # define the Iq kernel
496    source.append("#define KERNEL_NAME %s_Iq"%model_info['name'])
497    source.append(call_iq)
498    source.append(kernel_code)
499    source.append("#undef CALL_IQ")
500    source.append("#undef KERNEL_NAME")
501
502    # define the Iqxy kernel from the same source with different #defines
503    source.append("#define KERNEL_NAME %s_Iqxy"%model_info['name'])
504    source.append(call_iqxy)
505    source.append(kernel_code)
506    source.append("#undef CALL_IQ")
507    source.append("#undef KERNEL_NAME")
508
509    return '\n'.join(source)
510
511class CoordinationDetails(object):
512    def __init__(self, model_info):
513        parameters = model_info['parameters']
514        max_pd = parameters.max_pd
515        npars = parameters.npars
516        par_offset = 4*max_pd
517        self.details = np.zeros(par_offset + 3*npars + 4, 'i4')
518
519        # generate views on different parts of the array
520        self._pd_par     = self.details[0*max_pd:1*max_pd]
521        self._pd_length  = self.details[1*max_pd:2*max_pd]
522        self._pd_offset  = self.details[2*max_pd:3*max_pd]
523        self._pd_stride  = self.details[3*max_pd:4*max_pd]
524        self._par_offset = self.details[par_offset+0*npars:par_offset+1*npars]
525        self._par_coord  = self.details[par_offset+1*npars:par_offset+2*npars]
526        self._pd_coord   = self.details[par_offset+2*npars:par_offset+3*npars]
527
528        # theta_par is fixed
529        self.details[-1] = parameters.theta_offset
530
531    @property
532    def ctypes(self): return self.details.ctypes
533
534    @property
535    def pd_par(self): return self._pd_par
536
537    @property
538    def pd_length(self): return self._pd_length
539
540    @property
541    def pd_offset(self): return self._pd_offset
542
543    @property
544    def pd_stride(self): return self._pd_stride
545
546    @property
547    def pd_coord(self): return self._pd_coord
548
549    @property
550    def par_coord(self): return self._par_coord
551
552    @property
553    def par_offset(self): return self._par_offset
554
555    @property
556    def num_active(self): return self.details[-4]
557    @num_active.setter
558    def num_active(self, v): self.details[-4] = v
559
560    @property
561    def total_pd(self): return self.details[-3]
562    @total_pd.setter
563    def total_pd(self, v): self.details[-3] = v
564
565    @property
566    def num_coord(self): return self.details[-2]
567    @num_coord.setter
568    def num_coord(self, v): self.details[-2] = v
569
570    @property
571    def theta_par(self): return self.details[-1]
572
573    def show(self):
574        print("total_pd", self.total_pd)
575        print("num_active", self.num_active)
576        print("pd_par", self.pd_par)
577        print("pd_length", self.pd_length)
578        print("pd_offset", self.pd_offset)
579        print("pd_stride", self.pd_stride)
580        print("par_offsets", self.par_offset)
581        print("num_coord", self.num_coord)
582        print("par_coord", self.par_coord)
583        print("pd_coord", self.pd_coord)
584        print("theta par", self.details[-1])
585
586def mono_details(model_info):
587    details = CoordinationDetails(model_info)
588    # The zero defaults for monodisperse systems are mostly fine
589    details.par_offset[:] = np.arange(2, len(details.par_offset)+2)
590    return details
591
592def poly_details(model_info, weights):
593    #print("weights",weights)
594    weights = weights[2:] # Skip scale and background
595
596    # Decreasing list of polydispersity lengths
597    # Note: the reversing view, x[::-1], does not require a copy
598    pd_length = np.array([len(w) for w in weights])
599    num_active = np.sum(pd_length>1)
600    if num_active > model_info['parameters'].max_pd:
601        raise ValueError("Too many polydisperse parameters")
602
603    pd_offset = np.cumsum(np.hstack((0, pd_length)))
604    idx = np.argsort(pd_length)[::-1][:num_active]
605    par_length = np.array([max(len(w),1) for w in weights])
606    pd_stride = np.cumprod(np.hstack((1, par_length[idx])))
607    par_offsets = np.cumsum(np.hstack((2, par_length)))
608
609    details = CoordinationDetails(model_info)
610    details.pd_par[:num_active] = idx
611    details.pd_length[:num_active] = pd_length[idx]
612    details.pd_offset[:num_active] = pd_offset[idx]
613    details.pd_stride[:num_active] = pd_stride[:-1]
614    details.par_offset[:] = par_offsets[:-1]
615    details.total_pd = pd_stride[-1]
616    details.num_active = num_active
617    # Without constraints coordinated parameters are just the pd parameters
618    details.par_coord[:num_active] = idx
619    details.pd_coord[:num_active] = 2**np.arange(num_active)
620    details.num_coord = num_active
621    #details.show()
622    return details
623
624def constrained_poly_details(model_info, weights, constraints):
625    # Need to find the independently varying pars and sort them
626    # Need to build a coordination list for the dependent variables
627    # Need to generate a constraints function which takes values
628    # and weights, returning par blocks
629    raise NotImplementedError("Can't handle constraints yet")
630
631
632def create_vector_Iq(model_info):
633    """
634    Define Iq as a vector function if it exists.
635    """
636    Iq = model_info['Iq']
637    if callable(Iq) and not getattr(Iq, 'vectorized', False):
638        def vector_Iq(q, *args):
639            """
640            Vectorized 1D kernel.
641            """
642            return np.array([Iq(qi, *args) for qi in q])
643        model_info['Iq'] = vector_Iq
644
645def create_vector_Iqxy(model_info):
646    """
647    Define Iqxy as a vector function if it exists, or default it from Iq().
648    """
649    Iq, Iqxy = model_info['Iq'], model_info['Iqxy']
650    if callable(Iqxy) and not getattr(Iqxy, 'vectorized', False):
651        def vector_Iqxy(qx, qy, *args):
652            """
653            Vectorized 2D kernel.
654            """
655            return np.array([Iqxy(qxi, qyi, *args) for qxi, qyi in zip(qx, qy)])
656        model_info['Iqxy'] = vector_Iqxy
657    elif callable(Iq):
658        # Iq is vectorized because create_vector_Iq was already called.
659        def default_Iqxy(qx, qy, *args):
660            """
661            Default 2D kernel.
662            """
663            return Iq(np.sqrt(qx**2 + qy**2), *args)
664        model_info['Iqxy'] = default_Iqxy
665
666def create_default_functions(model_info):
667    """
668    Autogenerate missing functions, such as Iqxy from Iq.
669
670    This only works for Iqxy when Iq is written in python. :func:`make_source`
671    performs a similar role for Iq written in C.  This also vectorizes
672    any functions that are not already marked as vectorized.
673    """
674    create_vector_Iq(model_info)
675    create_vector_Iqxy(model_info)  # call create_vector_Iq() first
676
677def load_kernel_module(model_name):
678    if model_name.endswith('.py'):
679        kernel_module = load_custom_kernel_module(model_name)
680    else:
681        from sasmodels import models
682        __import__('sasmodels.models.'+model_name)
683        kernel_module = getattr(models, model_name, None)
684    return kernel_module
685
686
687def make_model_info(kernel_module):
688    """
689    Interpret the model definition file, categorizing the parameters.
690
691    The module can be loaded with a normal python import statement if you
692    know which module you need, or with __import__('sasmodels.model.'+name)
693    if the name is in a string.
694
695    The *model_info* structure contains the following fields:
696
697    * *id* is the id of the kernel
698    * *name* is the display name of the kernel
699    * *filename* is the full path to the module defining the file (if any)
700    * *title* is a short description of the kernel
701    * *description* is a long description of the kernel (this doesn't seem
702      very useful since the Help button on the model page brings you directly
703      to the documentation page)
704    * *docs* is the docstring from the module.  Use :func:`make_doc` to
705    * *category* specifies the model location in the docs
706    * *parameters* is the model parameter table
707    * *single* is True if the model allows single precision
708    * *structure_factor* is True if the model is useable in a product
709    * *variant_info* contains the information required to select between
710      model variants (e.g., the list of cases) or is None if there are no
711      model variants
712    * *par_type* categorizes the model parameters. See
713      :func:`categorize_parameters` for details.
714    * *demo* contains the *{parameter: value}* map used in compare (and maybe
715      for the demo plot, if plots aren't set up to use the default values).
716      If *demo* is not given in the file, then the default values will be used.
717    * *tests* is a set of tests that must pass
718    * *source* is the list of library files to include in the C model build
719    * *Iq*, *Iqxy*, *form_volume*, *ER*, *VR* and *sesans* are python functions
720      implementing the kernel for the module, or None if they are not
721      defined in python
722    * *composition* is None if the model is independent, otherwise it is a
723      tuple with composition type ('product' or 'mixture') and a list of
724      *model_info* blocks for the composition objects.  This allows us to
725      build complete product and mixture models from just the info.
726    """
727    # TODO: maybe turn model_info into a class ModelDefinition
728    #print("make parameter table", kernel_module.parameters)
729    parameters = make_parameter_table(kernel_module.parameters)
730    filename = abspath(kernel_module.__file__)
731    kernel_id = splitext(basename(filename))[0]
732    name = getattr(kernel_module, 'name', None)
733    if name is None:
734        name = " ".join(w.capitalize() for w in kernel_id.split('_'))
735    model_info = dict(
736        id=kernel_id,  # string used to load the kernel
737        filename=abspath(kernel_module.__file__),
738        name=name,
739        title=getattr(kernel_module, 'title', name+" model"),
740        description=getattr(kernel_module, 'description', 'no description'),
741        parameters=parameters,
742        composition=None,
743        docs=kernel_module.__doc__,
744        category=getattr(kernel_module, 'category', None),
745        single=getattr(kernel_module, 'single', True),
746        structure_factor=getattr(kernel_module, 'structure_factor', False),
747        profile_axes=getattr(kernel_module, 'profile_axes', ['x','y']),
748        variant_info=getattr(kernel_module, 'invariant_info', None),
749        demo=getattr(kernel_module, 'demo', None),
750        source=getattr(kernel_module, 'source', []),
751        tests=getattr(kernel_module, 'tests', []),
752        )
753    set_demo(model_info, getattr(kernel_module, 'demo', None))
754    # Check for optional functions
755    functions = "ER VR form_volume Iq Iqxy profile sesans".split()
756    model_info.update((k, getattr(kernel_module, k, None)) for k in functions)
757    create_default_functions(model_info)
758    # Precalculate the monodisperse parameters
759    # TODO: make this a lazy evaluator
760    # make_model_info is called for every model on sasview startup
761    model_info['mono_details'] = mono_details(model_info)
762    return model_info
763
764section_marker = re.compile(r'\A(?P<first>[%s])(?P=first)*\Z'
765                            %re.escape(string.punctuation))
766def _convert_section_titles_to_boldface(lines):
767    """
768    Do the actual work of identifying and converting section headings.
769    """
770    prior = None
771    for line in lines:
772        if prior is None:
773            prior = line
774        elif section_marker.match(line):
775            if len(line) >= len(prior):
776                yield "".join(("**", prior, "**"))
777                prior = None
778            else:
779                yield prior
780                prior = line
781        else:
782            yield prior
783            prior = line
784    if prior is not None:
785        yield prior
786
787def convert_section_titles_to_boldface(s):
788    """
789    Use explicit bold-face rather than section headings so that the table of
790    contents is not polluted with section names from the model documentation.
791
792    Sections are identified as the title line followed by a line of punctuation
793    at least as long as the title line.
794    """
795    return "\n".join(_convert_section_titles_to_boldface(s.split('\n')))
796
797def make_doc(model_info):
798    """
799    Return the documentation for the model.
800    """
801    Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
802    Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
803    docs = convert_section_titles_to_boldface(model_info['docs'])
804    subst = dict(id=model_info['id'].replace('_', '-'),
805                 name=model_info['name'],
806                 title=model_info['title'],
807                 parameters=make_partable(model_info['parameters']),
808                 returns=Sq_units if model_info['structure_factor'] else Iq_units,
809                 docs=docs)
810    return DOC_HEADER % subst
811
812
813def demo_time():
814    """
815    Show how long it takes to process a model.
816    """
817    from .models import cylinder
818    import datetime
819    tic = datetime.datetime.now()
820    make_source(make_model_info(cylinder))
821    toc = (datetime.datetime.now() - tic).total_seconds()
822    print("time: %g"%toc)
823
824def main():
825    """
826    Program which prints the source produced by the model.
827    """
828    import sys
829    if len(sys.argv) <= 1:
830        print("usage: python -m sasmodels.generate modelname")
831    else:
832        name = sys.argv[1]
833        kernel_module = load_kernel_module(name)
834        model_info = make_model_info(kernel_module)
835        source = make_source(model_info)
836        print(source)
837
838if __name__ == "__main__":
839    main()
Note: See TracBrowser for help on using the repository browser.