source: sasmodels/sasmodels/kerneldll.py @ 56b2687

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

Merge branch 'master' into polydisp

Conflicts:

README.rst
sasmodels/core.py
sasmodels/data.py
sasmodels/generate.py
sasmodels/kernelcl.py
sasmodels/kerneldll.py
sasmodels/sasview_model.py

  • Property mode set to 100644
File size: 13.7 KB
Line 
1r"""
2DLL driver for C kernels
3
4The global attribute *ALLOW_SINGLE_PRECISION_DLLS* should be set to *True* if
5you wish to allow single precision floating point evaluation for the compiled
6models, otherwise it defaults to *False*.
7
8The compiler command line is stored in the attribute *COMPILE*, with string
9substitutions for %(source)s and %(output)s indicating what to compile and
10where to store it.  The actual command is system dependent.
11
12On windows systems, you have a choice of compilers.  *MinGW* is the GNU
13compiler toolchain, available in packages such as anaconda and PythonXY,
14or available stand alone. This toolchain has had difficulties on some
15systems, and may or may not work for you.  In order to build DLLs, *gcc*
16must be on your path.  If the environment variable *SAS_OPENMP* is given
17then -fopenmp is added to the compiler flags.  This requires a version
18of MinGW compiled with OpenMP support.
19
20An alternative toolchain uses the Microsoft Visual C++ compiler, available
21free from microsoft:
22
23    `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
24
25Again, this requires that the compiler is available on your path.  This is
26done by running vcvarsall.bat in a windows terminal.  Install locations are
27system dependent, such as:
28
29    C:\Program Files (x86)\Common Files\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat
30
31or maybe
32
33    C:\Users\yourname\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat
34
35And again, the environment variable *SAS_OPENMP* controls whether OpenMP is
36used to compile the C code.  This requires the Microsoft vcomp90.dll library,
37which doesn't seem to be included with the compiler, nor does there appear
38to be a public download location.  There may be one on your machine already
39in a location such as:
40
41    C:\Windows\winsxs\x86_microsoft.vc90.openmp*\vcomp90.dll
42
43If you copy this onto your path, such as the python directory or the install
44directory for this application, then OpenMP should be supported.
45"""
46from __future__ import print_function
47
48import sys
49import os
50from os.path import join as joinpath, split as splitpath, splitext
51import subprocess
52import tempfile
53import ctypes as ct  # type: ignore
54from ctypes import c_void_p, c_int32, c_longdouble, c_double, c_float  # type: ignore
55import logging
56
57import numpy as np  # type: ignore
58
59from . import generate
60from .kernel import KernelModel, Kernel
61from .kernelpy import PyInput
62from .exception import annotate_exception
63from .generate import F16, F32, F64
64
65try:
66    from typing import Tuple, Callable, Any
67    from .modelinfo import ModelInfo
68    from .details import CallDetails
69except ImportError:
70    pass
71
72if os.name == 'nt':
73    ARCH = "" if sys.maxint > 2**32 else "x86"  # maxint=2**31-1 on 32 bit
74    # Windows compiler; check if TinyCC is available
75    try:
76        import tinycc
77    except ImportError:
78        tinycc = None
79    # call vcvarsall.bat before compiling to set path, headers, libs, etc.
80    if "VCINSTALLDIR" in os.environ:
81        # MSVC compiler is available, so use it.  OpenMP requires a copy of
82        # vcomp90.dll on the path.  One may be found here:
83        #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
84        # Copy this to the python directory and uncomment the OpenMP COMPILE
85        # TODO: remove intermediate OBJ file created in the directory
86        # TODO: maybe don't use randomized name for the c file
87        # TODO: maybe ask distutils to find MSVC
88        CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG".split()
89        if "SAS_OPENMP" in os.environ:
90            CC.append("/openmp")
91        LN = "/link /DLL /INCREMENTAL:NO /MANIFEST".split()
92        def compile_command(source, output):
93            return CC + ["/Tp%s"%source] + LN + ["/OUT:%s"%output]
94    elif tinycc:
95        # TinyCC compiler.
96        CC = [tinycc.TCC] + "-shared -rdynamic -Wall".split()
97        def compile_command(source, output):
98            return CC + [source, "-o", output]
99    else:
100        # MinGW compiler.
101        CC = "gcc -shared -std=c99 -O2 -Wall".split()
102        if "SAS_OPENMP" in os.environ:
103            CC.append("-fopenmp")
104        def compile_command(source, output):
105            return CC + [source, "-o", output, "-lm"]
106else:
107    ARCH = ""
108    # Generic unix compile
109    # On mac users will need the X code command line tools installed
110    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
111    CC = "cc -shared -fPIC -std=c99 -O2 -Wall".split()
112    # add openmp support if not running on a mac
113    if sys.platform != "darwin":
114        CC.append("-fopenmp")
115    def compile_command(source, output):
116        return CC + [source, "-o", output, "-lm"]
117
118# Windows-specific solution
119if os.name == 'nt':
120    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
121    DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
122    if not os.path.exists(DLL_PATH):
123        os.makedirs(DLL_PATH)
124else:
125    # Set up the default path for compiled modules.
126    DLL_PATH = tempfile.gettempdir()
127
128ALLOW_SINGLE_PRECISION_DLLS = True
129
130def compile(source, output):
131    command = compile_command(source=source, output=output)
132    command_str = " ".join('"%s"'%p if ' ' in p else p for p in command)
133    logging.info(command_str)
134    try:
135        # need shell=True on windows to keep console box from popping up
136        shell = (os.name == 'nt')
137        subprocess.check_output(command, shell=shell, stderr=subprocess.STDOUT)
138    except subprocess.CalledProcessError as exc:
139        raise RuntimeError("compile failed.\n%s\n%s"%(command_str, exc.output))
140    if not os.path.exists(output):
141        raise RuntimeError("compile failed.  File is in %r"%source)
142
143def dll_name(model_info, dtype):
144    # type: (ModelInfo, np.dtype) ->  str
145    """
146    Name of the dll containing the model.  This is the base file name without
147    any path or extension, with a form such as 'sas_sphere32'.
148    """
149    bits = 8*dtype.itemsize
150    basename = "sas%d_%s"%(bits, model_info.id)
151    basename += ARCH + ".so"
152
153    # Hack to find precompiled dlls
154    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename)
155    if os.path.exists(path):
156        return path
157
158    return joinpath(DLL_PATH, basename)
159
160
161def dll_path(model_info, dtype):
162    # type: (ModelInfo, np.dtype) -> str
163    """
164    Complete path to the dll for the model.  Note that the dll may not
165    exist yet if it hasn't been compiled.
166    """
167    return os.path.join(DLL_PATH, dll_name(model_info, dtype)+".so")
168
169
170def make_dll(source, model_info, dtype=F64):
171    # type: (str, ModelInfo, np.dtype) -> str
172    """
173    Returns the path to the compiled model defined by *kernel_module*.
174
175    If the model has not been compiled, or if the source file(s) are newer
176    than the dll, then *make_dll* will compile the model before returning.
177    This routine does not load the resulting dll.
178
179    *dtype* is a numpy floating point precision specifier indicating whether
180    the model should be single, double or long double precision.  The default
181    is double precision, *np.dtype('d')*.
182
183    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
184    models are not allowed as DLLs.
185
186    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
187    The default is the system temporary directory.
188    """
189    if dtype == F16:
190        raise ValueError("16 bit floats not supported")
191    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
192        dtype = F64  # Force 64-bit dll
193    # Note: dtype may be F128 for long double precision
194
195    dll = dll_path(model_info, dtype)
196
197    if not os.path.exists(dll):
198        need_recompile = True
199    elif getattr(sys, 'frozen', None) is not None:
200        # TODO: don't suppress time stamp
201        # Currently suppressing recompile when running in a frozen environment
202        need_recompile = False
203    else:
204        dll_time = os.path.getmtime(dll)
205        newest_source = generate.timestamp(model_info)
206        need_recompile = dll_time < newest_source
207    if need_recompile:
208        basename = dll_name(model_info, dtype) + "_"
209        fid, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
210        source = generate.convert_type(source, dtype)
211        fd, filename = tempfile.mkstemp(suffix=".c", prefix=tempfile_prefix)
212        with os.fdopen(fd, "w") as file:
213            file.write(source)
214        compile(source=filename, output=dll)
215        # comment the following to keep the generated c file
216        # Note: if there is a syntax error then compile raises an error
217        # and the source file will not be deleted.
218        os.unlink(filename)
219        #print("saving compiled file in %r"%filename)
220    return dll
221
222
223def load_dll(source, model_info, dtype=F64):
224    # type: (str, ModelInfo, np.dtype) -> "DllModel"
225    """
226    Create and load a dll corresponding to the source, info pair returned
227    from :func:`sasmodels.generate.make` compiled for the target precision.
228
229    See :func:`make_dll` for details on controlling the dll path and the
230    allowed floating point precision.
231    """
232    filename = make_dll(source, model_info, dtype=dtype)
233    return DllModel(filename, model_info, dtype=dtype)
234
235
236class DllModel(KernelModel):
237    """
238    ctypes wrapper for a single model.
239
240    *source* and *model_info* are the model source and interface as returned
241    from :func:`gen.make`.
242
243    *dtype* is the desired model precision.  Any numpy dtype for single
244    or double precision floats will do, such as 'f', 'float32' or 'single'
245    for single and 'd', 'float64' or 'double' for double.  Double precision
246    is an optional extension which may not be available on all devices.
247
248    Call :meth:`release` when done with the kernel.
249    """
250   
251    def __init__(self, dllpath, model_info, dtype=generate.F32):
252        # type: (str, ModelInfo, np.dtype) -> None
253        self.info = model_info
254        self.dllpath = dllpath
255        self._dll = None  # type: ct.CDLL
256        self.dtype = np.dtype(dtype)
257
258    def _load_dll(self):
259        # type: () -> None
260        #print("dll", self.dllpath)
261        try:
262            self._dll = ct.CDLL(self.dllpath)
263        except:
264            annotate_exception("while loading "+self.dllpath)
265            raise
266
267        fp = (c_float if self.dtype == generate.F32
268              else c_double if self.dtype == generate.F64
269              else c_longdouble)
270
271        # int, int, int, int*, double*, double*, double*, double*, double*, double
272        argtypes = [c_int32]*3 + [c_void_p]*5 + [fp]
273        self._Iq = self._dll[generate.kernel_name(self.info, is_2d=False)]
274        self._Iqxy = self._dll[generate.kernel_name(self.info, is_2d=True)]
275        self._Iq.argtypes = argtypes
276        self._Iqxy.argtypes = argtypes
277
278    def __getstate__(self):
279        # type: () -> Tuple[ModelInfo, str]
280        return self.info, self.dllpath
281
282    def __setstate__(self, state):
283        # type: (Tuple[ModelInfo, str]) -> None
284        self.info, self.dllpath = state
285        self._dll = None
286
287    def make_kernel(self, q_vectors):
288        # type: (List[np.ndarray]) -> DllKernel
289        q_input = PyInput(q_vectors, self.dtype)
290        # Note: pickle not supported for DllKernel
291        if self._dll is None:
292            self._load_dll()
293        kernel = self._Iqxy if q_input.is_2d else self._Iq
294        return DllKernel(kernel, self.info, q_input)
295
296    def release(self):
297        # type: () -> None
298        """
299        Release any resources associated with the model.
300        """
301        if os.name == 'nt':
302            #dll = ct.cdll.LoadLibrary(self.dllpath)
303            dll = ct.CDLL(self.dllpath)
304            libHandle = dll._handle
305            #libHandle = ct.c_void_p(dll._handle)
306            del dll, self._dll
307            self._dll = None
308            ct.windll.kernel32.FreeLibrary(libHandle)
309        else:   
310            pass 
311
312
313class DllKernel(Kernel):
314    """
315    Callable SAS kernel.
316
317    *kernel* is the c function to call.
318
319    *model_info* is the module information
320
321    *q_input* is the DllInput q vectors at which the kernel should be
322    evaluated.
323
324    The resulting call method takes the *pars*, a list of values for
325    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
326    vectors for the polydisperse parameters.  *cutoff* determines the
327    integration limits: any points with combined weight less than *cutoff*
328    will not be calculated.
329
330    Call :meth:`release` when done with the kernel instance.
331    """
332    def __init__(self, kernel, model_info, q_input):
333        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
334        self.kernel = kernel
335        self.info = model_info
336        self.q_input = q_input
337        self.dtype = q_input.dtype
338        self.dim = '2d' if q_input.is_2d else '1d'
339        self.result = np.empty(q_input.nq+1, q_input.dtype)
340        self.real = (np.float32 if self.q_input.dtype == generate.F32
341                     else np.float64 if self.q_input.dtype == generate.F64
342                     else np.float128)
343
344    def __call__(self, call_details, weights, values, cutoff):
345        # type: (CallDetails, np.ndarray, np.ndarray, float) -> np.ndarray
346
347        #print("in kerneldll")
348        #print("weights", weights)
349        #print("values", values)
350        start, stop = 0, call_details.total_pd
351        args = [
352            self.q_input.nq, # nq
353            start, # pd_start
354            stop, # pd_stop pd_stride[MAX_PD]
355            call_details.buffer.ctypes.data, # problem
356            weights.ctypes.data,  # weights
357            values.ctypes.data,  #pars
358            self.q_input.q.ctypes.data, #q
359            self.result.ctypes.data,   # results
360            self.real(cutoff), # cutoff
361            ]
362        self.kernel(*args) # type: ignore
363        return self.result[:-1]
364
365    def release(self):
366        # type: () -> None
367        """
368        Release any resources associated with the kernel.
369        """
370        self.q_input.release()
Note: See TracBrowser for help on using the repository browser.