source: sasmodels/sasmodels/kerneldll.py @ 1a3559f

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

allow SAS_DLL_PATH to be set via environment variable

  • Property mode set to 100644
File size: 15.5 KB
RevLine 
[92da231]1r"""
[eafc9fa]2DLL driver for C kernels
[750ffa5]3
[3764ec1]4If the environment variable *SAS_OPENMP* is set, then sasmodels
5will attempt to compile with OpenMP flags so that the model can use all
6available kernels.  This may or may not be available on your compiler
7toolchain.  Depending on operating system and environment.
[92da231]8
[3764ec1]9Windows does not have provide a compiler with the operating system.
10Instead, we assume that TinyCC is installed and available.  This can
11be done with a simple pip command if it is not already available::
[92da231]12
[3764ec1]13    pip install tinycc
[92da231]14
[3764ec1]15If Microsoft Visual C++ is available (because VCINSTALLDIR is
16defined in the environment), then that will be used instead.
17Microsoft Visual C++ for Python is available from Microsoft:
[92da231]18
[d138d43]19    `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
[92da231]20
[3764ec1]21If neither compiler is available, sasmodels will check for *MinGW*,
22the GNU compiler toolchain. This available in packages such as Anaconda
23and PythonXY, or available stand alone. This toolchain has had
24difficulties on some systems, and may or may not work for you.
25
26You can control which compiler to use by setting SAS_COMPILER in the
27environment:
28
29  - tinycc (Windows): use the TinyCC compiler shipped with SasView
30  - msvc (Windows): use the Microsoft Visual C++ compiler
31  - mingw (Windows): use the MinGW GNU cc compiler
32  - unix (Linux): use the system cc compiler.
33  - unix (Mac): use the clang compiler. You will need XCode installed, and
34    the XCode command line tools. Mac comes with OpenCL drivers, so generally
35    this will not be needed.
36
37Both *msvc* and *mingw* require that the compiler is available on your path.
38For *msvc*, this can done by running vcvarsall.bat in a windows terminal.
39Install locations are system dependent, such as:
[92da231]40
[2d81cfe]41    C:\Program Files (x86)\Common Files\Microsoft\Visual
42    C++ for Python\9.0\vcvarsall.bat
[92da231]43
44or maybe
45
[2d81cfe]46    C:\Users\yourname\AppData\Local\Programs\Common\Microsoft\Visual
47    C++ for Python\9.0\vcvarsall.bat
[92da231]48
[3764ec1]49OpenMP for *msvc* requires the Microsoft vcomp90.dll library, which doesn't
50seem to be included with the compiler, nor does there appear to be a public
51download location.  There may be one on your machine already in a location
52such as:
[92da231]53
54    C:\Windows\winsxs\x86_microsoft.vc90.openmp*\vcomp90.dll
55
[3764ec1]56If you copy this to somewhere on your path, such as the python directory or
57the install directory for this application, then OpenMP should be supported.
58
59For full control of the compiler, define a function
60*compile_command(source,output)* which takes the name of the source file
61and the name of the output file and returns a compile command that can be
62evaluated in the shell.  For even more control, replace the entire
63*compile(source,output)* function.
64
65The global attribute *ALLOW_SINGLE_PRECISION_DLLS* should be set to *False* if
66you wish to prevent single precision floating point evaluation for the compiled
67models, otherwise set it defaults to *True*.
[14de349]68"""
[eafc9fa]69from __future__ import print_function
[750ffa5]70
[5d4777d]71import sys
72import os
[40a87fa]73from os.path import join as joinpath, splitext
[7e16db7]74import subprocess
[df4dc86]75import tempfile
[7ae2b7f]76import ctypes as ct  # type: ignore
[886fa25]77import _ctypes as _ct
[821a9c6]78import logging
[14de349]79
[7ae2b7f]80import numpy as np  # type: ignore
[14de349]81
[739aad4]82try:
83    import tinycc
84except ImportError:
85    tinycc = None
86
[cb6ecf4]87from . import generate
[f619de7]88from .kernel import KernelModel, Kernel
89from .kernelpy import PyInput
[2c801fe]90from .exception import annotate_exception
[f619de7]91from .generate import F16, F32, F64
92
[2d81cfe]93# pylint: disable=unused-import
[f619de7]94try:
95    from typing import Tuple, Callable, Any
96    from .modelinfo import ModelInfo
97    from .details import CallDetails
98except ImportError:
99    pass
[2d81cfe]100# pylint: enable=unused-import
[14de349]101
[1a3559f]102if "SAS_DLL_PATH" in os.environ:
103    SAS_DLL_PATH = os.environ["SAS_DLL_PATH"]
104else:
105    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
106    SAS_DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
107
[739aad4]108if "SAS_COMPILER" in os.environ:
[b2f1d2f]109    COMPILER = os.environ["SAS_COMPILER"]
[739aad4]110elif os.name == 'nt':
[b2f1d2f]111    if tinycc is not None:
112        COMPILER = "tinycc"
113    elif "VCINSTALLDIR" in os.environ:
114        # If vcvarsall.bat has been called, then VCINSTALLDIR is in the environment
115        # and we can use the MSVC compiler.  Otherwise, if tinycc is available
116        # the use it.  Otherwise, hope that mingw is available.
117        COMPILER = "msvc"
[fb69211]118    else:
[b2f1d2f]119        COMPILER = "mingw"
[5d4777d]120else:
[b2f1d2f]121    COMPILER = "unix"
[739aad4]122
[886fa25]123ARCH = "" if ct.sizeof(ct.c_void_p) > 4 else "x86"  # 4 byte pointers on x86
[b2f1d2f]124if COMPILER == "unix":
[5efe850]125    # Generic unix compile
126    # On mac users will need the X code command line tools installed
[7e16db7]127    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
[aa343d6]128    CC = "cc -shared -fPIC -std=c99 -O2 -Wall".split()
[5efe850]129    # add openmp support if not running on a mac
[7e16db7]130    if sys.platform != "darwin":
[33969b6]131        # OpenMP seems to be broken on gcc 5.4.0 (ubuntu 16.04.9)
132        # Shut it off for all unix until we can investigate.
133        #CC.append("-fopenmp")
134        pass
[7e16db7]135    def compile_command(source, output):
[40a87fa]136        """unix compiler command"""
[15e74ad]137        return CC + [source, "-o", output, "-lm"]
[b2f1d2f]138elif COMPILER == "msvc":
[739aad4]139    # Call vcvarsall.bat before compiling to set path, headers, libs, etc.
140    # MSVC compiler is available, so use it.  OpenMP requires a copy of
141    # vcomp90.dll on the path.  One may be found here:
142    #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
143    # Copy this to the python directory and uncomment the OpenMP COMPILE
144    # TODO: remove intermediate OBJ file created in the directory
145    # TODO: maybe don't use randomized name for the c file
146    # TODO: maybe ask distutils to find MSVC
147    CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG".split()
148    if "SAS_OPENMP" in os.environ:
149        CC.append("/openmp")
150    LN = "/link /DLL /INCREMENTAL:NO /MANIFEST".split()
151    def compile_command(source, output):
[40a87fa]152        """MSVC compiler command"""
[739aad4]153        return CC + ["/Tp%s"%source] + LN + ["/OUT:%s"%output]
[b2f1d2f]154elif COMPILER == "tinycc":
[739aad4]155    # TinyCC compiler.
156    CC = [tinycc.TCC] + "-shared -rdynamic -Wall".split()
157    def compile_command(source, output):
[40a87fa]158        """tinycc compiler command"""
[739aad4]159        return CC + [source, "-o", output]
[b2f1d2f]160elif COMPILER == "mingw":
[739aad4]161    # MinGW compiler.
162    CC = "gcc -shared -std=c99 -O2 -Wall".split()
163    if "SAS_OPENMP" in os.environ:
164        CC.append("-fopenmp")
165    def compile_command(source, output):
[40a87fa]166        """mingw compiler command"""
[739aad4]167        return CC + [source, "-o", output, "-lm"]
[df4dc86]168
[5d316e9]169ALLOW_SINGLE_PRECISION_DLLS = True
[5d4777d]170
[7e16db7]171def compile(source, output):
[40a87fa]172    # type: (str, str) -> None
173    """
174    Compile *source* producing *output*.
175
176    Raises RuntimeError if the compile failed or the output wasn't produced.
177    """
[7e16db7]178    command = compile_command(source=source, output=output)
179    command_str = " ".join('"%s"'%p if ' ' in p else p for p in command)
180    logging.info(command_str)
181    try:
[15e74ad]182        # need shell=True on windows to keep console box from popping up
183        shell = (os.name == 'nt')
[aa343d6]184        subprocess.check_output(command, shell=shell, stderr=subprocess.STDOUT)
[7e16db7]185    except subprocess.CalledProcessError as exc:
186        raise RuntimeError("compile failed.\n%s\n%s"%(command_str, exc.output))
187    if not os.path.exists(output):
188        raise RuntimeError("compile failed.  File is in %r"%source)
[750ffa5]189
[151f3bc]190def dll_name(model_info, dtype):
[f619de7]191    # type: (ModelInfo, np.dtype) ->  str
[5d4777d]192    """
[151f3bc]193    Name of the dll containing the model.  This is the base file name without
194    any path or extension, with a form such as 'sas_sphere32'.
[5d4777d]195    """
[151f3bc]196    bits = 8*dtype.itemsize
[56b2687]197    basename = "sas%d_%s"%(bits, model_info.id)
[c7118f4]198    basename += ARCH + ".so"
[01b8659]199
200    # Hack to find precompiled dlls
[c7118f4]201    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename)
[01b8659]202    if os.path.exists(path):
203        return path
204
[1a3559f]205    return joinpath(SAS_DLL_PATH, basename)
[5d4777d]206
[f619de7]207
[151f3bc]208def dll_path(model_info, dtype):
[f619de7]209    # type: (ModelInfo, np.dtype) -> str
[151f3bc]210    """
211    Complete path to the dll for the model.  Note that the dll may not
212    exist yet if it hasn't been compiled.
213    """
[1a3559f]214    return os.path.join(SAS_DLL_PATH, dll_name(model_info, dtype))
[5d4777d]215
[f619de7]216
217def make_dll(source, model_info, dtype=F64):
218    # type: (str, ModelInfo, np.dtype) -> str
[5d4777d]219    """
[f619de7]220    Returns the path to the compiled model defined by *kernel_module*.
[5d4777d]221
[f619de7]222    If the model has not been compiled, or if the source file(s) are newer
223    than the dll, then *make_dll* will compile the model before returning.
224    This routine does not load the resulting dll.
[5d4777d]225
[aa4946b]226    *dtype* is a numpy floating point precision specifier indicating whether
[f619de7]227    the model should be single, double or long double precision.  The default
228    is double precision, *np.dtype('d')*.
[5d4777d]229
[f619de7]230    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
231    models are not allowed as DLLs.
[aa4946b]232
[1a3559f]233    Set *sasmodels.kerneldll.SAS_DLL_PATH* to the compiled dll output path.
234    Alternatively, set the environment variable *SAS_DLL_PATH*.
[bf94e6e]235    The default is in ~/.sasmodels/compiled_models.
[5d4777d]236    """
[f619de7]237    if dtype == F16:
[5d316e9]238        raise ValueError("16 bit floats not supported")
[f619de7]239    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
240        dtype = F64  # Force 64-bit dll
241    # Note: dtype may be F128 for long double precision
[750ffa5]242
[17bbadd]243    dll = dll_path(model_info, dtype)
[e1454ab]244
245    if not os.path.exists(dll):
[5a91c6b]246        need_recompile = True
247    else:
248        dll_time = os.path.getmtime(dll)
[0dc34c3]249        newest_source = generate.dll_timestamp(model_info)
[5a91c6b]250        need_recompile = dll_time < newest_source
251    if need_recompile:
[bf94e6e]252        # Make sure the DLL path exists
[1a3559f]253        if not os.path.exists(SAS_DLL_PATH):
254            os.makedirs(SAS_DLL_PATH)
[40a87fa]255        basename = splitext(os.path.basename(dll))[0] + "_"
256        system_fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
[f619de7]257        source = generate.convert_type(source, dtype)
[40a87fa]258        with os.fdopen(system_fd, "w") as file_handle:
259            file_handle.write(source)
[7e16db7]260        compile(source=filename, output=dll)
261        # comment the following to keep the generated c file
262        # Note: if there is a syntax error then compile raises an error
263        # and the source file will not be deleted.
264        os.unlink(filename)
265        #print("saving compiled file in %r"%filename)
[aa4946b]266    return dll
267
268
[f619de7]269def load_dll(source, model_info, dtype=F64):
270    # type: (str, ModelInfo, np.dtype) -> "DllModel"
[aa4946b]271    """
[823e620]272    Create and load a dll corresponding to the source, info pair returned
[aa4946b]273    from :func:`sasmodels.generate.make` compiled for the target precision.
274
275    See :func:`make_dll` for details on controlling the dll path and the
276    allowed floating point precision.
277    """
[17bbadd]278    filename = make_dll(source, model_info, dtype=dtype)
279    return DllModel(filename, model_info, dtype=dtype)
[5d4777d]280
[f619de7]281
282class DllModel(KernelModel):
[14de349]283    """
284    ctypes wrapper for a single model.
285
[17bbadd]286    *source* and *model_info* are the model source and interface as returned
[14de349]287    from :func:`gen.make`.
288
289    *dtype* is the desired model precision.  Any numpy dtype for single
290    or double precision floats will do, such as 'f', 'float32' or 'single'
291    for single and 'd', 'float64' or 'double' for double.  Double precision
292    is an optional extension which may not be available on all devices.
[ff7119b]293
294    Call :meth:`release` when done with the kernel.
[14de349]295    """
[17bbadd]296    def __init__(self, dllpath, model_info, dtype=generate.F32):
[f619de7]297        # type: (str, ModelInfo, np.dtype) -> None
[17bbadd]298        self.info = model_info
[ce27e21]299        self.dllpath = dllpath
[f619de7]300        self._dll = None  # type: ct.CDLL
[40a87fa]301        self._kernels = None # type: List[Callable, Callable]
[750ffa5]302        self.dtype = np.dtype(dtype)
[14de349]303
[ce27e21]304    def _load_dll(self):
[f619de7]305        # type: () -> None
[2c801fe]306        try:
[f619de7]307            self._dll = ct.CDLL(self.dllpath)
[4d76711]308        except:
309            annotate_exception("while loading "+self.dllpath)
[2c801fe]310            raise
[14de349]311
[886fa25]312        float_type = (ct.c_float if self.dtype == generate.F32
313                      else ct.c_double if self.dtype == generate.F64
314                      else ct.c_longdouble)
[ce27e21]315
[a738209]316        # int, int, int, int*, double*, double*, double*, double*, double
[886fa25]317        argtypes = [ct.c_int32]*3 + [ct.c_void_p]*4 + [float_type]
[9eb3632]318        names = [generate.kernel_name(self.info, variant)
319                 for variant in ("Iq", "Iqxy", "Imagnetic")]
320        self._kernels = [self._dll[name] for name in names]
321        for k in self._kernels:
322            k.argtypes = argtypes
[ce27e21]323
324    def __getstate__(self):
[f619de7]325        # type: () -> Tuple[ModelInfo, str]
[eafc9fa]326        return self.info, self.dllpath
[ce27e21]327
328    def __setstate__(self, state):
[f619de7]329        # type: (Tuple[ModelInfo, str]) -> None
[eafc9fa]330        self.info, self.dllpath = state
[f619de7]331        self._dll = None
[ce27e21]332
[48fbd50]333    def make_kernel(self, q_vectors):
[f619de7]334        # type: (List[np.ndarray]) -> DllKernel
[eafc9fa]335        q_input = PyInput(q_vectors, self.dtype)
[f619de7]336        # Note: pickle not supported for DllKernel
337        if self._dll is None:
338            self._load_dll()
[9eb3632]339        is_2d = len(q_vectors) == 2
340        kernel = self._kernels[1:3] if is_2d else [self._kernels[0]]*2
[3c56da87]341        return DllKernel(kernel, self.info, q_input)
[4d76711]342
[eafc9fa]343    def release(self):
[f619de7]344        # type: () -> None
[14de349]345        """
[eafc9fa]346        Release any resources associated with the model.
[14de349]347        """
[886fa25]348        dll_handle = self._dll._handle
[6ad0e87]349        if os.name == 'nt':
[40a87fa]350            ct.windll.kernel32.FreeLibrary(dll_handle)
351        else:
[886fa25]352            _ct.dlclose(dll_handle)
353        del self._dll
354        self._dll = None
[14de349]355
[f619de7]356class DllKernel(Kernel):
[ff7119b]357    """
358    Callable SAS kernel.
359
[b3f6bc3]360    *kernel* is the c function to call.
[ff7119b]361
[17bbadd]362    *model_info* is the module information
[ff7119b]363
[3c56da87]364    *q_input* is the DllInput q vectors at which the kernel should be
[ff7119b]365    evaluated.
366
367    The resulting call method takes the *pars*, a list of values for
[823e620]368    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
[ff7119b]369    vectors for the polydisperse parameters.  *cutoff* determines the
370    integration limits: any points with combined weight less than *cutoff*
371    will not be calculated.
372
373    Call :meth:`release` when done with the kernel instance.
374    """
[17bbadd]375    def __init__(self, kernel, model_info, q_input):
[f619de7]376        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
[48fbd50]377        self.kernel = kernel
[17bbadd]378        self.info = model_info
[3c56da87]379        self.q_input = q_input
[39cc3be]380        self.dtype = q_input.dtype
[445d1c0]381        self.dim = '2d' if q_input.is_2d else '1d'
[a5b8477]382        self.result = np.empty(q_input.nq+1, q_input.dtype)
383        self.real = (np.float32 if self.q_input.dtype == generate.F32
384                     else np.float64 if self.q_input.dtype == generate.F64
385                     else np.float128)
[14de349]386
[32e3c9b]387    def __call__(self, call_details, values, cutoff, magnetic):
388        # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray
[48fbd50]389
[9eb3632]390        kernel = self.kernel[1 if magnetic else 0]
[303d8d6]391        args = [
392            self.q_input.nq, # nq
[9eb3632]393            None, # pd_start
394            None, # pd_stop pd_stride[MAX_PD]
[8d62008]395            call_details.buffer.ctypes.data, # problem
[303d8d6]396            values.ctypes.data,  #pars
[48fbd50]397            self.q_input.q.ctypes.data, #q
398            self.result.ctypes.data,   # results
[a5b8477]399            self.real(cutoff), # cutoff
[9eb3632]400        ]
[bde38b5]401        #print("Calling DLL")
402        #call_details.show(values)
[9eb3632]403        step = 100
[bde38b5]404        for start in range(0, call_details.num_eval, step):
405            stop = min(start + step, call_details.num_eval)
[9eb3632]406            args[1:3] = [start, stop]
407            kernel(*args) # type: ignore
408
409        #print("returned",self.q_input.q, self.result)
[14a15a3]410        pd_norm = self.result[self.q_input.nq]
[886fa25]411        scale = values[0]/(pd_norm if pd_norm != 0.0 else 1.0)
[9eb3632]412        background = values[1]
413        #print("scale",scale,background)
414        return scale*self.result[:self.q_input.nq] + background
[14de349]415
416    def release(self):
[f619de7]417        # type: () -> None
[eafc9fa]418        """
419        Release any resources associated with the kernel.
420        """
[f619de7]421        self.q_input.release()
Note: See TracBrowser for help on using the repository browser.