source: sasmodels/sasmodels/core.py @ 445d1c0

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

restrict polydispersity checks in bumps to 1d parameters for 1d data

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