source: sasmodels/sasmodels/core.py @ e65c3ba

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since e65c3ba was e65c3ba, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

lint

  • Property mode set to 100644
File size: 12.4 KB
Line 
1"""
2Core model handling routines.
3"""
4from __future__ import print_function
5
6__all__ = [
7    "list_models", "load_model", "load_model_info",
8    "build_model", "precompile_dlls",
9    ]
10
11import os
12from os.path import basename, join as joinpath
13from glob import glob
14import re
15
16import numpy as np # type: ignore
17
18from . import generate
19from . import modelinfo
20from . import product
21from . import mixture
22from . import kernelpy
23from . import kerneldll
24from . import custom
25
26if os.environ.get("SAS_OPENCL", "").lower() == "none":
27    HAVE_OPENCL = False
28else:
29    try:
30        from . import kernelcl
31        HAVE_OPENCL = True
32    except Exception:
33        HAVE_OPENCL = False
34
35CUSTOM_MODEL_PATH = os.environ.get('SAS_MODELPATH', "")
36if CUSTOM_MODEL_PATH == "":
37    CUSTOM_MODEL_PATH = joinpath(os.path.expanduser("~"), ".sasmodels", "custom_models")
38    if not os.path.isdir(CUSTOM_MODEL_PATH):
39        os.makedirs(CUSTOM_MODEL_PATH)
40
41try:
42    from typing import List, Union, Optional, Any
43    from .kernel import KernelModel
44    from .modelinfo import ModelInfo
45except ImportError:
46    pass
47
48# TODO: refactor composite model support
49# The current load_model_info/build_model does not reuse existing model
50# definitions when loading a composite model, instead reloading and
51# rebuilding the kernel for each component model in the expression.  This
52# is fine in a scripting environment where the model is built when the script
53# starts and is thrown away when the script ends, but may not be the best
54# solution in a long-lived application.  This affects the following functions:
55#
56#    load_model
57#    load_model_info
58#    build_model
59
60KINDS = ("all", "py", "c", "double", "single", "opencl", "1d", "2d",
61         "nonmagnetic", "magnetic")
62def list_models(kind=None):
63    # type: (str) -> List[str]
64    """
65    Return the list of available models on the model path.
66
67    *kind* can be one of the following:
68
69        * all: all models
70        * py: python models only
71        * c: compiled models only
72        * single: models which support single precision
73        * double: models which require double precision
74        * opencl: controls if OpenCL is supperessed
75        * 1d: models which are 1D only, or 2D using abs(q)
76        * 2d: models which can be 2D
77        * magnetic: models with an sld
78        * nommagnetic: models without an sld
79
80    For multiple conditions, combine with plus.  For example, *c+single+2d*
81    would return all oriented models implemented in C which can be computed
82    accurately with single precision arithmetic.
83    """
84    if kind and any(k not in KINDS for k in kind.split('+')):
85        raise ValueError("kind not in " + ", ".join(KINDS))
86    files = sorted(glob(joinpath(generate.MODEL_PATH, "[a-zA-Z]*.py")))
87    available_models = [basename(f)[:-3] for f in files]
88    if kind and '+' in kind:
89        all_kinds = kind.split('+')
90        condition = lambda name: all(_matches(name, k) for k in all_kinds)
91    else:
92        condition = lambda name: _matches(name, kind)
93    selected = [name for name in available_models if condition(name)]
94
95    return selected
96
97def _matches(name, kind):
98    if kind is None or kind == "all":
99        return True
100    info = load_model_info(name)
101    pars = info.parameters.kernel_parameters
102    if kind == "py" and callable(info.Iq):
103        return True
104    elif kind == "c" and not callable(info.Iq):
105        return True
106    elif kind == "double" and not info.single:
107        return True
108    elif kind == "single" and info.single:
109        return True
110    elif kind == "opencl" and info.opencl:
111        return True
112    elif kind == "2d" and any(p.type == 'orientation' for p in pars):
113        return True
114    elif kind == "1d" and all(p.type != 'orientation' for p in pars):
115        return True
116    elif kind == "magnetic" and any(p.type == 'sld' for p in pars):
117        return True
118    elif kind == "nonmagnetic" and any(p.type != 'sld' for p in pars):
119        return True
120    return False
121
122def load_model(model_name, dtype=None, platform='ocl'):
123    # type: (str, str, str) -> KernelModel
124    """
125    Load model info and build model.
126
127    *model_name* is the name of the model, or perhaps a model expression
128    such as sphere*hardsphere or sphere+cylinder.
129
130    *dtype* and *platform* are given by :func:`build_model`.
131    """
132    return build_model(load_model_info(model_name),
133                       dtype=dtype, platform=platform)
134
135def load_model_info(model_string):
136    # type: (str) -> modelinfo.ModelInfo
137    """
138    Load a model definition given the model name.
139
140    *model_string* is the name of the model, or perhaps a model expression
141    such as sphere*cylinder or sphere+cylinder. Use '@' for a structure
142    factor product, e.g. sphere@hardsphere. Custom models can be specified by
143    prefixing the model name with 'custom.', e.g. 'custom.MyModel+sphere'.
144
145    This returns a handle to the module defining the model.  This can be
146    used with functions in generate to build the docs or extract model info.
147    """
148    if '@' in model_string:
149        parts = model_string.split('@')
150        if len(parts) != 2:
151            raise ValueError("Use P@S to apply a structure factor S to model P")
152        P_info, Q_info = [load_model_info(part) for part in parts]
153        return product.make_product_info(P_info, Q_info)
154
155    product_parts = []
156    addition_parts = []
157
158    addition_parts_names = model_string.split('+')
159    if len(addition_parts_names) >= 2:
160        addition_parts = [load_model_info(part) for part in addition_parts_names]
161    elif len(addition_parts_names) == 1:
162        product_parts_names = model_string.split('*')
163        if len(product_parts_names) >= 2:
164            product_parts = [load_model_info(part) for part in product_parts_names]
165        elif len(product_parts_names) == 1:
166            if "custom." in product_parts_names[0]:
167                # Extract ModelName from "custom.ModelName"
168                pattern = "custom.([A-Za-z0-9_-]+)"
169                result = re.match(pattern, product_parts_names[0])
170                if result is None:
171                    raise ValueError("Model name in invalid format: " + product_parts_names[0])
172                model_name = result.group(1)
173                # Use ModelName to find the path to the custom model file
174                model_path = joinpath(CUSTOM_MODEL_PATH, model_name + ".py")
175                if not os.path.isfile(model_path):
176                    raise ValueError("The model file {} doesn't exist".format(model_path))
177                kernel_module = custom.load_custom_kernel_module(model_path)
178                return modelinfo.make_model_info(kernel_module)
179            # Model is a core model
180            kernel_module = generate.load_kernel_module(product_parts_names[0])
181            return modelinfo.make_model_info(kernel_module)
182
183    model = None
184    if len(product_parts) > 1:
185        model = mixture.make_mixture_info(product_parts, operation='*')
186    if len(addition_parts) > 1:
187        if model is not None:
188            addition_parts.append(model)
189        model = mixture.make_mixture_info(addition_parts, operation='+')
190    return model
191
192
193def build_model(model_info, dtype=None, platform="ocl"):
194    # type: (modelinfo.ModelInfo, str, str) -> KernelModel
195    """
196    Prepare the model for the default execution platform.
197
198    This will return an OpenCL model, a DLL model or a python model depending
199    on the model and the computing platform.
200
201    *model_info* is the model definition structure returned from
202    :func:`load_model_info`.
203
204    *dtype* indicates whether the model should use single or double precision
205    for the calculation.  Choices are 'single', 'double', 'quad', 'half',
206    or 'fast'.  If *dtype* ends with '!', then force the use of the DLL rather
207    than OpenCL for the calculation.
208
209    *platform* should be "dll" to force the dll to be used for C models,
210    otherwise it uses the default "ocl".
211    """
212    composition = model_info.composition
213    if composition is not None:
214        composition_type, parts = composition
215        models = [build_model(p, dtype=dtype, platform=platform) for p in parts]
216        if composition_type == 'mixture':
217            return mixture.MixtureModel(model_info, models)
218        elif composition_type == 'product':
219            P, S = models
220            return product.ProductModel(model_info, P, S)
221        else:
222            raise ValueError('unknown mixture type %s'%composition_type)
223
224    # If it is a python model, return it immediately
225    if callable(model_info.Iq):
226        return kernelpy.PyModel(model_info)
227
228    numpy_dtype, fast, platform = parse_dtype(model_info, dtype, platform)
229
230    source = generate.make_source(model_info)
231    if platform == "dll":
232        #print("building dll", numpy_dtype)
233        return kerneldll.load_dll(source['dll'], model_info, numpy_dtype)
234    else:
235        #print("building ocl", numpy_dtype)
236        return kernelcl.GpuModel(source, model_info, numpy_dtype, fast=fast)
237
238def precompile_dlls(path, dtype="double"):
239    # type: (str, str) -> List[str]
240    """
241    Precompile the dlls for all builtin models, returning a list of dll paths.
242
243    *path* is the directory in which to save the dlls.  It will be created if
244    it does not already exist.
245
246    This can be used when build the windows distribution of sasmodels
247    which may be missing the OpenCL driver and the dll compiler.
248    """
249    numpy_dtype = np.dtype(dtype)
250    if not os.path.exists(path):
251        os.makedirs(path)
252    compiled_dlls = []
253    for model_name in list_models():
254        model_info = load_model_info(model_name)
255        if not callable(model_info.Iq):
256            source = generate.make_source(model_info)['dll']
257            old_path = kerneldll.DLL_PATH
258            try:
259                kerneldll.DLL_PATH = path
260                dll = kerneldll.make_dll(source, model_info, dtype=numpy_dtype)
261            finally:
262                kerneldll.DLL_PATH = old_path
263            compiled_dlls.append(dll)
264    return compiled_dlls
265
266def parse_dtype(model_info, dtype=None, platform=None):
267    # type: (ModelInfo, str, str) -> (np.dtype, bool, str)
268    """
269    Interpret dtype string, returning np.dtype and fast flag.
270
271    Possible types include 'half', 'single', 'double' and 'quad'.  If the
272    type is 'fast', then this is equivalent to dtype 'single' but using
273    fast native functions rather than those with the precision level
274    guaranteed by the OpenCL standard.  'default' will choose the appropriate
275    default for the model and platform.
276
277    Platform preference can be specfied ("ocl" vs "dll"), with the default
278    being OpenCL if it is availabe.  If the dtype name ends with '!' then
279    platform is forced to be DLL rather than OpenCL.
280
281    This routine ignores the preferences within the model definition.  This
282    is by design.  It allows us to test models in single precision even when
283    we have flagged them as requiring double precision so we can easily check
284    the performance on different platforms without having to change the model
285    definition.
286    """
287    # Assign default platform, overriding ocl with dll if OpenCL is unavailable
288    # If opencl=False OpenCL is switched off
289
290    if platform is None:
291        platform = "ocl"
292    if platform == "ocl" and not HAVE_OPENCL or not model_info.opencl:
293        platform = "dll"
294
295    # Check if type indicates dll regardless of which platform is given
296    if dtype is not None and dtype.endswith('!'):
297        platform = "dll"
298        dtype = dtype[:-1]
299
300    # Convert special type names "half", "fast", and "quad"
301    fast = (dtype == "fast")
302    if fast:
303        dtype = "single"
304    elif dtype == "quad":
305        dtype = "longdouble"
306    elif dtype == "half":
307        dtype = "float16"
308
309    # Convert dtype string to numpy dtype.
310    if dtype is None or dtype == "default":
311        numpy_dtype = (generate.F32 if platform == "ocl" and model_info.single
312                       else generate.F64)
313    else:
314        numpy_dtype = np.dtype(dtype)
315
316    # Make sure that the type is supported by opencl, otherwise use dll
317    if platform == "ocl":
318        env = kernelcl.environment()
319        if not env.has_type(numpy_dtype):
320            platform = "dll"
321            if dtype is None:
322                numpy_dtype = generate.F64
323
324    return numpy_dtype, fast, platform
325
326def list_models_main():
327    # type: () -> None
328    """
329    Run list_models as a main program.  See :func:`list_models` for the
330    kinds of models that can be requested on the command line.
331    """
332    import sys
333    kind = sys.argv[1] if len(sys.argv) > 1 else "all"
334    print("\n".join(list_models(kind)))
335
336if __name__ == "__main__":
337    list_models_main()
Note: See TracBrowser for help on using the repository browser.