source: sasmodels/sasmodels/core.py @ c499331

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

progress on having compare.py recognize vector parameters

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