source: sasmodels/sasmodels/generate.py @ ea05c87

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

Merge remote-tracking branch 'origin/master' into polydisp

Conflicts:

sasmodels/models/spherical_sld.py

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