source: sasmodels/sasmodels/core.py @ 1e2a1ba

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

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

Conflicts:

sasmodels/core.py
sasmodels/custom/init.py
sasmodels/direct_model.py
sasmodels/generate.py
sasmodels/kernelpy.py
sasmodels/sasview_model.py

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