source: sasmodels/sasmodels/kerneldll.py @ 32e3c9b

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

dll version of magnetic sld

  • 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))
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 = os.path.splitext(os.path.basename(dll))[0] + "_"
209        fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
210        source = generate.convert_type(source, dtype)
211        with os.fdopen(fd, "w") as file:
212            file.write(source)
213        compile(source=filename, output=dll)
214        # comment the following to keep the generated c file
215        # Note: if there is a syntax error then compile raises an error
216        # and the source file will not be deleted.
217        os.unlink(filename)
218        #print("saving compiled file in %r"%filename)
219    return dll
220
221
222def load_dll(source, model_info, dtype=F64):
223    # type: (str, ModelInfo, np.dtype) -> "DllModel"
224    """
225    Create and load a dll corresponding to the source, info pair returned
226    from :func:`sasmodels.generate.make` compiled for the target precision.
227
228    See :func:`make_dll` for details on controlling the dll path and the
229    allowed floating point precision.
230    """
231    filename = make_dll(source, model_info, dtype=dtype)
232    return DllModel(filename, model_info, dtype=dtype)
233
234
235class DllModel(KernelModel):
236    """
237    ctypes wrapper for a single model.
238
239    *source* and *model_info* are the model source and interface as returned
240    from :func:`gen.make`.
241
242    *dtype* is the desired model precision.  Any numpy dtype for single
243    or double precision floats will do, such as 'f', 'float32' or 'single'
244    for single and 'd', 'float64' or 'double' for double.  Double precision
245    is an optional extension which may not be available on all devices.
246
247    Call :meth:`release` when done with the kernel.
248    """
249   
250    def __init__(self, dllpath, model_info, dtype=generate.F32):
251        # type: (str, ModelInfo, np.dtype) -> None
252        self.info = model_info
253        self.dllpath = dllpath
254        self._dll = None  # type: ct.CDLL
255        self.dtype = np.dtype(dtype)
256
257    def _load_dll(self):
258        # type: () -> None
259        #print("dll", self.dllpath)
260        try:
261            self._dll = ct.CDLL(self.dllpath)
262        except:
263            annotate_exception("while loading "+self.dllpath)
264            raise
265
266        fp = (c_float if self.dtype == generate.F32
267              else c_double if self.dtype == generate.F64
268              else c_longdouble)
269
270        # int, int, int, int*, double*, double*, double*, double*, double
271        argtypes = [c_int32]*3 + [c_void_p]*4 + [fp]
272        self._Iq = self._dll[generate.kernel_name(self.info, "Iq")]
273        self._Iqxy = self._dll[generate.kernel_name(self.info, "Iqxy")]
274        self._Imagnetic = self._dll[generate.kernel_name(self.info, "Imagnetic")]
275        self._Iq.argtypes = argtypes
276        self._Iqxy.argtypes = argtypes
277        self._Imagnetic.argtypes = argtypes
278
279    def __getstate__(self):
280        # type: () -> Tuple[ModelInfo, str]
281        return self.info, self.dllpath
282
283    def __setstate__(self, state):
284        # type: (Tuple[ModelInfo, str]) -> None
285        self.info, self.dllpath = state
286        self._dll = None
287
288    def make_kernel(self, q_vectors, magnetic=False):
289        # type: (List[np.ndarray]) -> DllKernel
290        q_input = PyInput(q_vectors, self.dtype)
291        # Note: pickle not supported for DllKernel
292        if self._dll is None:
293            self._load_dll()
294        kernel = [self._Iqxy, self._Imagnetic] if q_input.is_2d else self._Iq
295        return DllKernel(kernel, self.info, q_input)
296
297    def release(self):
298        # type: () -> None
299        """
300        Release any resources associated with the model.
301        """
302        if os.name == 'nt':
303            #dll = ct.cdll.LoadLibrary(self.dllpath)
304            dll = ct.CDLL(self.dllpath)
305            libHandle = dll._handle
306            #libHandle = ct.c_void_p(dll._handle)
307            del dll, self._dll
308            self._dll = None
309            ct.windll.kernel32.FreeLibrary(libHandle)
310        else:   
311            pass 
312
313
314class DllKernel(Kernel):
315    """
316    Callable SAS kernel.
317
318    *kernel* is the c function to call.
319
320    *model_info* is the module information
321
322    *q_input* is the DllInput q vectors at which the kernel should be
323    evaluated.
324
325    The resulting call method takes the *pars*, a list of values for
326    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
327    vectors for the polydisperse parameters.  *cutoff* determines the
328    integration limits: any points with combined weight less than *cutoff*
329    will not be calculated.
330
331    Call :meth:`release` when done with the kernel instance.
332    """
333    def __init__(self, kernel, model_info, q_input):
334        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
335        self.kernel = kernel
336        self.info = model_info
337        self.q_input = q_input
338        self.dtype = q_input.dtype
339        self.dim = '2d' if q_input.is_2d else '1d'
340        self.result = np.empty(q_input.nq+1, q_input.dtype)
341        self.real = (np.float32 if self.q_input.dtype == generate.F32
342                     else np.float64 if self.q_input.dtype == generate.F64
343                     else np.float128)
344
345    def __call__(self, call_details, values, cutoff, magnetic):
346        # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray
347
348        #print("in kerneldll")
349        #print("values", values)
350        start, stop = 0, call_details.pd_prod
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            values.ctypes.data,  #pars
357            self.q_input.q.ctypes.data, #q
358            self.result.ctypes.data,   # results
359            self.real(cutoff), # cutoff
360            ]
361        #print("calling DLL")
362        self.kernel[1 if magnetic else 0](*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.