source: sasmodels/sasmodels/core.py @ 47fb816

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

Merge branch 'master' into cuda-test

  • Property mode set to 100644
File size: 14.4 KB
RevLine 
[aa4946b]1"""
2Core model handling routines.
3"""
[6d6508e]4from __future__ import print_function
5
[98f60fc]6__all__ = [
[6d6508e]7    "list_models", "load_model", "load_model_info",
[2547694]8    "build_model", "precompile_dlls",
[98f60fc]9    ]
[f734e7d]10
[7bf4757]11import os
[e65c3ba]12from os.path import basename, join as joinpath
[f734e7d]13from glob import glob
[e65c3ba]14import re
[f734e7d]15
[0db7dbd]16# Set "SAS_OPENCL=cuda" in the environment to use the CUDA rather than OpenCL
17USE_CUDA = os.environ.get("SAS_OPENCL", "") == "cuda"
18
[7ae2b7f]19import numpy as np # type: ignore
[f734e7d]20
[aa4946b]21from . import generate
[6d6508e]22from . import modelinfo
23from . import product
[72a081d]24from . import mixture
[aa4946b]25from . import kernelpy
[0db7dbd]26if USE_CUDA:
27    from . import kernelcuda
28else:
29    from . import kernelcl
[aa4946b]30from . import kerneldll
[60335cc]31from . import custom
[880a2ed]32
[2d81cfe]33# pylint: disable=unused-import
[f619de7]34try:
35    from typing import List, Union, Optional, Any
36    from .kernel import KernelModel
[dd7fc12]37    from .modelinfo import ModelInfo
[f619de7]38except ImportError:
39    pass
[2d81cfe]40# pylint: enable=unused-import
[f619de7]41
[3221de0]42CUSTOM_MODEL_PATH = os.environ.get('SAS_MODELPATH', "")
43if CUSTOM_MODEL_PATH == "":
44    CUSTOM_MODEL_PATH = joinpath(os.path.expanduser("~"), ".sasmodels", "custom_models")
[4341dd4]45    #if not os.path.isdir(CUSTOM_MODEL_PATH):
46    #    os.makedirs(CUSTOM_MODEL_PATH)
[3221de0]47
[4d76711]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
[f734e7d]59
[8407d8c]60KINDS = ("all", "py", "c", "double", "single", "opencl", "1d", "2d",
[2547694]61         "nonmagnetic", "magnetic")
[0b03001]62def list_models(kind=None):
[52e9a45]63    # type: (str) -> List[str]
[aa4946b]64    """
65    Return the list of available models on the model path.
[40a87fa]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
[8407d8c]74        * opencl: controls if OpenCL is supperessed
[40a87fa]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
[5124c969]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.
[aa4946b]83    """
[5124c969]84    if kind and any(k not in KINDS for k in kind.split('+')):
[2547694]85        raise ValueError("kind not in " + ", ".join(KINDS))
[3d9001f]86    files = sorted(glob(joinpath(generate.MODEL_PATH, "[a-zA-Z]*.py")))
[f734e7d]87    available_models = [basename(f)[:-3] for f in files]
[5124c969]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)]
[0b03001]94
95    return selected
96
97def _matches(name, kind):
[2547694]98    if kind is None or kind == "all":
[0b03001]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
[d2d6100]108    elif kind == "single" and info.single:
109        return True
[8407d8c]110    elif kind == "opencl" and info.opencl:
[407bf48]111        return True
[2547694]112    elif kind == "2d" and any(p.type == 'orientation' for p in pars):
[d2d6100]113        return True
[40a87fa]114    elif kind == "1d" and all(p.type != 'orientation' for p in pars):
[0b03001]115        return True
[2547694]116    elif kind == "magnetic" and any(p.type == 'sld' for p in pars):
[0b03001]117        return True
[2547694]118    elif kind == "nonmagnetic" and any(p.type != 'sld' for p in pars):
[d2d6100]119        return True
[0b03001]120    return False
[f734e7d]121
[f619de7]122def load_model(model_name, dtype=None, platform='ocl'):
[dd7fc12]123    # type: (str, str, str) -> KernelModel
[b8e5e21]124    """
125    Load model info and build model.
[f619de7]126
[2e66ef5]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`.
[b8e5e21]131    """
[f619de7]132    return build_model(load_model_info(model_name),
133                       dtype=dtype, platform=platform)
[aa4946b]134
[61a4bd4]135def load_model_info(model_string):
[f619de7]136    # type: (str) -> modelinfo.ModelInfo
[aa4946b]137    """
138    Load a model definition given the model name.
[1d4017a]139
[481ff64]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
[60335cc]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'.
[2e66ef5]144
[1d4017a]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.
[aa4946b]147    """
[ffc2a61]148    if "+" in model_string:
149        parts = [load_model_info(part)
150                 for part in model_string.split("+")]
151        return mixture.make_mixture_info(parts, operation='+')
152    elif "*" in model_string:
153        parts = [load_model_info(part)
154                 for part in model_string.split("*")]
155        return mixture.make_mixture_info(parts, operation='*')
[e68bae9]156    elif "@" in model_string:
157        p_info, q_info = [load_model_info(part)
158                          for part in model_string.split("@")]
159        return product.make_product_info(p_info, q_info)
[ffc2a61]160    # We are now dealing with a pure model
161    elif "custom." in model_string:
162        pattern = "custom.([A-Za-z0-9_-]+)"
163        result = re.match(pattern, model_string)
164        if result is None:
165            raise ValueError("Model name in invalid format: " + model_string)
166        model_name = result.group(1)
167        # Use ModelName to find the path to the custom model file
168        model_path = joinpath(CUSTOM_MODEL_PATH, model_name + ".py")
169        if not os.path.isfile(model_path):
170            raise ValueError("The model file {} doesn't exist".format(model_path))
171        kernel_module = custom.load_custom_kernel_module(model_path)
172        return modelinfo.make_model_info(kernel_module)
173    kernel_module = generate.load_kernel_module(model_string)
174    return modelinfo.make_model_info(kernel_module)
[d19962c]175
176
[17bbadd]177def build_model(model_info, dtype=None, platform="ocl"):
[dd7fc12]178    # type: (modelinfo.ModelInfo, str, str) -> KernelModel
[aa4946b]179    """
180    Prepare the model for the default execution platform.
181
182    This will return an OpenCL model, a DLL model or a python model depending
183    on the model and the computing platform.
184
[17bbadd]185    *model_info* is the model definition structure returned from
186    :func:`load_model_info`.
[bcd3aa3]187
[aa4946b]188    *dtype* indicates whether the model should use single or double precision
[dd7fc12]189    for the calculation.  Choices are 'single', 'double', 'quad', 'half',
190    or 'fast'.  If *dtype* ends with '!', then force the use of the DLL rather
191    than OpenCL for the calculation.
[aa4946b]192
193    *platform* should be "dll" to force the dll to be used for C models,
194    otherwise it uses the default "ocl".
195    """
[6d6508e]196    composition = model_info.composition
[72a081d]197    if composition is not None:
198        composition_type, parts = composition
199        models = [build_model(p, dtype=dtype, platform=platform) for p in parts]
200        if composition_type == 'mixture':
201            return mixture.MixtureModel(model_info, models)
202        elif composition_type == 'product':
[e79f0a5]203            P, S = models
[72a081d]204            return product.ProductModel(model_info, P, S)
205        else:
206            raise ValueError('unknown mixture type %s'%composition_type)
[aa4946b]207
[fa5fd8d]208    # If it is a python model, return it immediately
209    if callable(model_info.Iq):
210        return kernelpy.PyModel(model_info)
211
[def2c1b]212    numpy_dtype, fast, platform = parse_dtype(model_info, dtype, platform)
[7891a2a]213
[72a081d]214    source = generate.make_source(model_info)
[7891a2a]215    if platform == "dll":
[f2f67a6]216        #print("building dll", numpy_dtype)
[a4280bd]217        return kerneldll.load_dll(source['dll'], model_info, numpy_dtype)
[0db7dbd]218    elif USE_CUDA:
219        #print("building cuda", numpy_dtype)
220        return kernelcuda.GpuModel(source, model_info, numpy_dtype, fast=fast)
[aa4946b]221    else:
[f2f67a6]222        #print("building ocl", numpy_dtype)
[dd7fc12]223        return kernelcl.GpuModel(source, model_info, numpy_dtype, fast=fast)
[aa4946b]224
[7bf4757]225def precompile_dlls(path, dtype="double"):
[7891a2a]226    # type: (str, str) -> List[str]
[b8e5e21]227    """
[7bf4757]228    Precompile the dlls for all builtin models, returning a list of dll paths.
[b8e5e21]229
[7bf4757]230    *path* is the directory in which to save the dlls.  It will be created if
231    it does not already exist.
[b8e5e21]232
233    This can be used when build the windows distribution of sasmodels
[7bf4757]234    which may be missing the OpenCL driver and the dll compiler.
[b8e5e21]235    """
[7891a2a]236    numpy_dtype = np.dtype(dtype)
[7bf4757]237    if not os.path.exists(path):
238        os.makedirs(path)
239    compiled_dlls = []
240    for model_name in list_models():
241        model_info = load_model_info(model_name)
[a4280bd]242        if not callable(model_info.Iq):
243            source = generate.make_source(model_info)['dll']
[2dcd6e7]244            old_path = kerneldll.SAS_DLL_PATH
[7bf4757]245            try:
[2dcd6e7]246                kerneldll.SAS_DLL_PATH = path
[def2c1b]247                dll = kerneldll.make_dll(source, model_info, dtype=numpy_dtype)
[7bf4757]248            finally:
[2dcd6e7]249                kerneldll.SAS_DLL_PATH = old_path
[7bf4757]250            compiled_dlls.append(dll)
251    return compiled_dlls
[b8e5e21]252
[7891a2a]253def parse_dtype(model_info, dtype=None, platform=None):
254    # type: (ModelInfo, str, str) -> (np.dtype, bool, str)
[dd7fc12]255    """
256    Interpret dtype string, returning np.dtype and fast flag.
257
258    Possible types include 'half', 'single', 'double' and 'quad'.  If the
[2e66ef5]259    type is 'fast', then this is equivalent to dtype 'single' but using
[9e771a3]260    fast native functions rather than those with the precision level
261    guaranteed by the OpenCL standard.  'default' will choose the appropriate
262    default for the model and platform.
[2e66ef5]263
264    Platform preference can be specfied ("ocl" vs "dll"), with the default
265    being OpenCL if it is availabe.  If the dtype name ends with '!' then
266    platform is forced to be DLL rather than OpenCL.
267
268    This routine ignores the preferences within the model definition.  This
269    is by design.  It allows us to test models in single precision even when
270    we have flagged them as requiring double precision so we can easily check
271    the performance on different platforms without having to change the model
272    definition.
[dd7fc12]273    """
[7891a2a]274    # Assign default platform, overriding ocl with dll if OpenCL is unavailable
[8407d8c]275    # If opencl=False OpenCL is switched off
[fe77343]276
[f49675c]277    if platform is None:
[7891a2a]278        platform = "ocl"
[0db7dbd]279    if not model_info.opencl:
[7891a2a]280        platform = "dll"
[0db7dbd]281    elif USE_CUDA:
282        if not kernelcuda.use_cuda():
283            platform = "dll"
284    else:
285        if not kernelcl.use_opencl():
286            platform = "dll"
[7891a2a]287
288    # Check if type indicates dll regardless of which platform is given
289    if dtype is not None and dtype.endswith('!'):
290        platform = "dll"
[dd7fc12]291        dtype = dtype[:-1]
292
[7891a2a]293    # Convert special type names "half", "fast", and "quad"
[2547694]294    fast = (dtype == "fast")
[7891a2a]295    if fast:
296        dtype = "single"
[2547694]297    elif dtype == "quad":
[7891a2a]298        dtype = "longdouble"
[2547694]299    elif dtype == "half":
[650c6d2]300        dtype = "float16"
[7891a2a]301
302    # Convert dtype string to numpy dtype.
[bb39b4a]303    if dtype is None or dtype == "default":
[2547694]304        numpy_dtype = (generate.F32 if platform == "ocl" and model_info.single
305                       else generate.F64)
[dd7fc12]306    else:
[7891a2a]307        numpy_dtype = np.dtype(dtype)
[9890053]308
[7891a2a]309    # Make sure that the type is supported by opencl, otherwise use dll
[2547694]310    if platform == "ocl":
[0db7dbd]311        if USE_CUDA:
312            env = kernelcuda.environment()
313        else:
314            env = kernelcl.environment()
[7891a2a]315        if not env.has_type(numpy_dtype):
316            platform = "dll"
317            if dtype is None:
318                numpy_dtype = generate.F64
[dd7fc12]319
[7891a2a]320    return numpy_dtype, fast, platform
[0c24a82]321
[2547694]322def list_models_main():
[40a87fa]323    # type: () -> None
324    """
325    Run list_models as a main program.  See :func:`list_models` for the
326    kinds of models that can be requested on the command line.
327    """
[0b03001]328    import sys
329    kind = sys.argv[1] if len(sys.argv) > 1 else "all"
330    print("\n".join(list_models(kind)))
[2547694]331
[a69d8cd]332def test_composite_order():
[7a516d0]333    def test_models(fst, snd):
334        """Confirm that two models produce the same parameters"""
335        fst = load_model(fst)
336        snd = load_model(snd)
[a69d8cd]337        # Un-disambiguate parameter names so that we can check if the same
338        # parameters are in a pair of composite models. Since each parameter in
339        # the mixture model is tagged as e.g., A_sld, we ought to use a
340        # regex subsitution s/^[A-Z]+_/_/, but removing all uppercase letters
341        # is good enough.
[7a516d0]342        fst = [[x for x in p.name if x == x.lower()] for p in fst.info.parameters.kernel_parameters]
343        snd = [[x for x in p.name if x == x.lower()] for p in snd.info.parameters.kernel_parameters]
344        assert sorted(fst) == sorted(snd), "{} != {}".format(fst, snd)
345
[a69d8cd]346    def build_test(first, second):
347        test = lambda description: test_models(first, second)
348        description = first + " vs. " + second
349        return test, description
[7a516d0]350
[a69d8cd]351    yield build_test(
[7a516d0]352        "cylinder+sphere",
353        "sphere+cylinder")
[a69d8cd]354    yield build_test(
[7a516d0]355        "cylinder*sphere",
356        "sphere*cylinder")
[a69d8cd]357    yield build_test(
[7a516d0]358        "cylinder@hardsphere*sphere",
359        "sphere*cylinder@hardsphere")
[a69d8cd]360    yield build_test(
[7a516d0]361        "barbell+sphere*cylinder@hardsphere",
362        "sphere*cylinder@hardsphere+barbell")
[a69d8cd]363    yield build_test(
[7a516d0]364        "barbell+cylinder@hardsphere*sphere",
365        "cylinder@hardsphere*sphere+barbell")
[a69d8cd]366    yield build_test(
[7a516d0]367        "barbell+sphere*cylinder@hardsphere",
368        "barbell+cylinder@hardsphere*sphere")
[a69d8cd]369    yield build_test(
[7a516d0]370        "sphere*cylinder@hardsphere+barbell",
371        "cylinder@hardsphere*sphere+barbell")
[a69d8cd]372    yield build_test(
[7a516d0]373        "barbell+sphere*cylinder@hardsphere",
374        "cylinder@hardsphere*sphere+barbell")
[a69d8cd]375    yield build_test(
[7a516d0]376        "barbell+cylinder@hardsphere*sphere",
377        "sphere*cylinder@hardsphere+barbell")
378
[a69d8cd]379def test_composite():
380    # type: () -> None
381    """Check that model load works"""
[7a516d0]382    #Test the the model produces the parameters that we would expect
383    model = load_model("cylinder@hardsphere*sphere")
384    actual = [p.name for p in model.info.parameters.kernel_parameters]
385    target = ("sld sld_solvent radius length theta phi volfraction"
386              " A_sld A_sld_solvent A_radius").split()
387    assert target == actual, "%s != %s"%(target, actual)
388
[2547694]389if __name__ == "__main__":
390    list_models_main()
Note: See TracBrowser for help on using the repository browser.