source: sasmodels/sasmodels/core.py @ 7bf4757

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

move precompile all models functionality into core; default dll models to double precision

  • Property mode set to 100644
File size: 10.3 KB
Line 
1"""
2Core model handling routines.
3"""
4__all__ = [
5    "list_models", "load_model_info", "precompile_dll",
6    "build_model", "make_kernel", "call_kernel", "call_ER_VR",
7    ]
8
9import os
10from os.path import basename, dirname, join as joinpath, splitext
11from glob import glob
12
13import numpy as np
14
15from . import models
16from . import weights
17from . import generate
18# TODO: remove circular references between product and core
19# product uses call_ER/call_VR, core uses make_product_info/ProductModel
20#from . import product
21from . import mixture
22from . import kernelpy
23from . import kerneldll
24try:
25    from . import kernelcl
26    HAVE_OPENCL = True
27except:
28    HAVE_OPENCL = False
29
30try:
31    np.meshgrid([])
32    meshgrid = np.meshgrid
33except ValueError:
34    # CRUFT: np.meshgrid requires multiple vectors
35    def meshgrid(*args):
36        if len(args) > 1:
37            return np.meshgrid(*args)
38        else:
39            return [np.asarray(v) for v in args]
40
41# TODO: refactor composite model support
42# The current load_model_info/build_model does not reuse existing model
43# definitions when loading a composite model, instead reloading and
44# rebuilding the kernel for each component model in the expression.  This
45# is fine in a scripting environment where the model is built when the script
46# starts and is thrown away when the script ends, but may not be the best
47# solution in a long-lived application.  This affects the following functions:
48#
49#    load_model
50#    load_model_info
51#    build_model
52
53def list_models():
54    """
55    Return the list of available models on the model path.
56    """
57    root = dirname(__file__)
58    files = sorted(glob(joinpath(root, 'models', "[a-zA-Z]*.py")))
59    available_models = [basename(f)[:-3] for f in files]
60    return available_models
61
62def isstr(s):
63    """
64    Return True if *s* is a string-like object.
65    """
66    try: s + ''
67    except: return False
68    return True
69
70def load_model(model_name, **kw):
71    """
72    Load model info and build model.
73    """
74    return build_model(load_model_info(model_name), **kw)
75
76
77def load_model_info(model_name):
78    """
79    Load a model definition given the model name.
80
81    This returns a handle to the module defining the model.  This can be
82    used with functions in generate to build the docs or extract model info.
83    """
84    parts = model_name.split('+')
85    if len(parts) > 1:
86        model_info_list = [load_model_info(p) for p in parts]
87        return mixture.make_mixture_info(model_info_list)
88
89    parts = model_name.split('*')
90    if len(parts) > 1:
91        from . import product
92        # Note: currently have circular reference
93        if len(parts) > 2:
94            raise ValueError("use P*S to apply structure factor S to model P")
95        P_info, Q_info = [load_model_info(p) for p in parts]
96        return product.make_product_info(P_info, Q_info)
97
98    kernel_module = generate.load_kernel_module(model_name)
99    return generate.make_model_info(kernel_module)
100
101
102def build_model(model_info, dtype=None, platform="ocl"):
103    """
104    Prepare the model for the default execution platform.
105
106    This will return an OpenCL model, a DLL model or a python model depending
107    on the model and the computing platform.
108
109    *model_info* is the model definition structure returned from
110    :func:`load_model_info`.
111
112    *dtype* indicates whether the model should use single or double precision
113    for the calculation. Any valid numpy single or double precision identifier
114    is valid, such as 'single', 'f', 'f32', or np.float32 for single, or
115    'double', 'd', 'f64'  and np.float64 for double.  If *None*, then use
116    'single' unless the model defines single=False.
117
118    *platform* should be "dll" to force the dll to be used for C models,
119    otherwise it uses the default "ocl".
120    """
121    composition = model_info.get('composition', None)
122    if composition is not None:
123        composition_type, parts = composition
124        models = [build_model(p, dtype=dtype, platform=platform) for p in parts]
125        if composition_type == 'mixture':
126            return mixture.MixtureModel(model_info, models)
127        elif composition_type == 'product':
128            from . import product
129            P, S = models
130            return product.ProductModel(model_info, P, S)
131        else:
132            raise ValueError('unknown mixture type %s'%composition_type)
133
134    ## for debugging:
135    ##  1. uncomment open().write so that the source will be saved next time
136    ##  2. run "python -m sasmodels.direct_model $MODELNAME" to save the source
137    ##  3. recomment the open.write() and uncomment open().read()
138    ##  4. rerun "python -m sasmodels.direct_model $MODELNAME"
139    ##  5. uncomment open().read() so that source will be regenerated from model
140    # open(model_info['name']+'.c','w').write(source)
141    # source = open(model_info['name']+'.cl','r').read()
142    if callable(model_info.get('Iq', None)):
143        return kernelpy.PyModel(model_info)
144    source = generate.make_source(model_info)
145    default_dtype = 'single' if model_info['single'] else 'double'
146    ocl_dtype = default_dtype if dtype is None else dtype
147    dll_dtype = 'double' if dtype is None else dtype
148    if (platform == "dll"
149            or not HAVE_OPENCL
150            or not kernelcl.environment().has_type(ocl_dtype)):
151        return kerneldll.load_dll(source, model_info, dll_dtype)
152    else:
153        return kernelcl.GpuModel(source, model_info, ocl_dtype)
154
155def precompile_dlls(path, dtype="double"):
156    """
157    Precompile the dlls for all builtin models, returning a list of dll paths.
158
159    *path* is the directory in which to save the dlls.  It will be created if
160    it does not already exist.
161
162    This can be used when build the windows distribution of sasmodels
163    which may be missing the OpenCL driver and the dll compiler.
164    """
165    if not os.path.exists(path):
166        os.makedirs(path)
167    compiled_dlls = []
168    for model_name in list_models():
169        model_info = load_model_info(model_name)
170        source = generate.make_source(model_info)
171        if source:
172            old_path = kerneldll.DLL_PATH
173            try:
174                kerneldll.DLL_PATH = path
175                dll = kerneldll.make_dll(source, model_info, dtype=dtype)
176            finally:
177                kerneldll.DLL_PATH = old_path
178            compiled_dlls.append(dll)
179    return compiled_dlls
180
181def get_weights(model_info, pars, name):
182    """
183    Generate the distribution for parameter *name* given the parameter values
184    in *pars*.
185
186    Uses "name", "name_pd", "name_pd_type", "name_pd_n", "name_pd_sigma"
187    from the *pars* dictionary for parameter value and parameter dispersion.
188    """
189    relative = name in model_info['partype']['pd-rel']
190    limits = model_info['limits'][name]
191    disperser = pars.get(name+'_pd_type', 'gaussian')
192    value = pars.get(name, model_info['defaults'][name])
193    npts = pars.get(name+'_pd_n', 0)
194    width = pars.get(name+'_pd', 0.0)
195    nsigma = pars.get(name+'_pd_nsigma', 3.0)
196    value, weight = weights.get_weights(
197        disperser, npts, width, nsigma, value, limits, relative)
198    return value, weight / np.sum(weight)
199
200def dispersion_mesh(pars):
201    """
202    Create a mesh grid of dispersion parameters and weights.
203
204    Returns [p1,p2,...],w where pj is a vector of values for parameter j
205    and w is a vector containing the products for weights for each
206    parameter set in the vector.
207    """
208    value, weight = zip(*pars)
209    value = [v.flatten() for v in meshgrid(*value)]
210    weight = np.vstack([v.flatten() for v in meshgrid(*weight)])
211    weight = np.prod(weight, axis=0)
212    return value, weight
213
214def call_kernel(kernel, pars, cutoff=0, mono=False):
215    """
216    Call *kernel* returned from *model.make_kernel* with parameters *pars*.
217
218    *cutoff* is the limiting value for the product of dispersion weights used
219    to perform the multidimensional dispersion calculation more quickly at a
220    slight cost to accuracy. The default value of *cutoff=0* integrates over
221    the entire dispersion cube.  Using *cutoff=1e-5* can be 50% faster, but
222    with an error of about 1%, which is usually less than the measurement
223    uncertainty.
224
225    *mono* is True if polydispersity should be set to none on all parameters.
226    """
227    fixed_pars = [pars.get(name, kernel.info['defaults'][name])
228                  for name in kernel.fixed_pars]
229    if mono:
230        pd_pars = [( np.array([pars[name]]), np.array([1.0]) )
231                   for name in kernel.pd_pars]
232    else:
233        pd_pars = [get_weights(kernel.info, pars, name) for name in kernel.pd_pars]
234    return kernel(fixed_pars, pd_pars, cutoff=cutoff)
235
236def call_ER_VR(model_info, vol_pars):
237    """
238    Return effect radius and volume ratio for the model.
239
240    *info* is either *kernel.info* for *kernel=make_kernel(model,q)*
241    or *model.info*.
242
243    *pars* are the parameters as expected by :func:`call_kernel`.
244    """
245    ER = model_info.get('ER', None)
246    VR = model_info.get('VR', None)
247    value, weight = dispersion_mesh(vol_pars)
248
249    individual_radii = ER(*value) if ER else 1.0
250    whole, part = VR(*value) if VR else (1.0, 1.0)
251
252    effect_radius = np.sum(weight*individual_radii) / np.sum(weight)
253    volume_ratio = np.sum(weight*part)/np.sum(weight*whole)
254    return effect_radius, volume_ratio
255
256
257def call_ER(model_info, values):
258    """
259    Call the model ER function using *values*. *model_info* is either
260    *model.info* if you have a loaded model, or *kernel.info* if you
261    have a model kernel prepared for evaluation.
262    """
263    ER = model_info.get('ER', None)
264    if ER is None:
265        return 1.0
266    else:
267        vol_pars = [get_weights(model_info, values, name)
268                    for name in model_info['partype']['volume']]
269        value, weight = dispersion_mesh(vol_pars)
270        individual_radii = ER(*value)
271        #print(values[0].shape, weights.shape, fv.shape)
272        return np.sum(weight*individual_radii) / np.sum(weight)
273
274def call_VR(model_info, values):
275    """
276    Call the model VR function using *pars*.
277    *info* is either *model.info* if you have a loaded model, or *kernel.info*
278    if you have a model kernel prepared for evaluation.
279    """
280    VR = model_info.get('VR', None)
281    if VR is None:
282        return 1.0
283    else:
284        vol_pars = [get_weights(model_info, values, name)
285                    for name in model_info['partype']['volume']]
286        value, weight = dispersion_mesh(vol_pars)
287        whole, part = VR(*value)
288        return np.sum(weight*part)/np.sum(weight*whole)
289
290# TODO: remove call_ER, call_VR
291
Note: See TracBrowser for help on using the repository browser.