source: sasmodels/sasmodels/kerneldll.py @ 8357f66

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 8357f66 was 8357f66, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Allow for compiler choice

  • Property mode set to 100644
File size: 13.6 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        if "DONT_USE_TINYCC" in os.environ:
78            tinycc = None
79    except ImportError:
80        tinycc = None
81    # call vcvarsall.bat before compiling to set path, headers, libs, etc.
82    if "VCINSTALLDIR" in os.environ:
83        # MSVC compiler is available, so use it.  OpenMP requires a copy of
84        # vcomp90.dll on the path.  One may be found here:
85        #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
86        # Copy this to the python directory and uncomment the OpenMP COMPILE
87        # TODO: remove intermediate OBJ file created in the directory
88        # TODO: maybe don't use randomized name for the c file
89        # TODO: maybe ask distutils to find MSVC
90        CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG".split()
91        if "SAS_OPENMP" in os.environ:
92            CC.append("/openmp")
93        LN = "/link /DLL /INCREMENTAL:NO /MANIFEST".split()
94        def compile_command(source, output):
95            return CC + ["/Tp%s"%source] + LN + ["/OUT:%s"%output]
96    elif tinycc:
97        # TinyCC compiler.
98        CC = [tinycc.TCC] + "-shared -rdynamic -Wall".split()
99        def compile_command(source, output):
100            return CC + [source, "-o", output]
101    else:
102        # MinGW compiler.
103        CC = "gcc -shared -std=c99 -O2 -Wall".split()
104        if "SAS_OPENMP" in os.environ:
105            CC.append("-fopenmp")
106        def compile_command(source, output):
107            return CC + [source, "-o", output, "-lm"]
108else:
109    ARCH = ""
110    # Generic unix compile
111    # On mac users will need the X code command line tools installed
112    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
113    CC = "cc -shared -fPIC -std=c99 -O2 -Wall".split()
114    # add openmp support if not running on a mac
115    if sys.platform != "darwin":
116        CC.append("-fopenmp")
117    def compile_command(source, output):
118        return CC + [source, "-o", output, "-lm"]
119
120# Windows-specific solution
121if os.name == 'nt':
122    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
123    DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
124    if not os.path.exists(DLL_PATH):
125        os.makedirs(DLL_PATH)
126else:
127    # Set up the default path for compiled modules.
128    DLL_PATH = tempfile.gettempdir()
129
130ALLOW_SINGLE_PRECISION_DLLS = True
131
132def compile(source, output):
133    command = compile_command(source=source, output=output)
134    command_str = " ".join('"%s"'%p if ' ' in p else p for p in command)
135    logging.info(command_str)
136    try:
137        # need shell=True on windows to keep console box from popping up
138        shell = (os.name == 'nt')
139        subprocess.check_output(command, shell=shell, stderr=subprocess.STDOUT)
140    except subprocess.CalledProcessError as exc:
141        raise RuntimeError("compile failed.\n%s\n%s"%(command_str, exc.output))
142    if not os.path.exists(output):
143        raise RuntimeError("compile failed.  File is in %r"%source)
144
145def dll_name(model_info, dtype):
146    # type: (ModelInfo, np.dtype) ->  str
147    """
148    Name of the dll containing the model.  This is the base file name without
149    any path or extension, with a form such as 'sas_sphere32'.
150    """
151    bits = 8*dtype.itemsize
152    basename = "sas%d_%s"%(bits, model_info.id)
153    basename += ARCH + ".so"
154
155    # Hack to find precompiled dlls
156    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename)
157    if os.path.exists(path):
158        return path
159
160    return joinpath(DLL_PATH, basename)
161
162
163def dll_path(model_info, dtype):
164    # type: (ModelInfo, np.dtype) -> str
165    """
166    Complete path to the dll for the model.  Note that the dll may not
167    exist yet if it hasn't been compiled.
168    """
169    return os.path.join(DLL_PATH, dll_name(model_info, dtype))
170
171
172def make_dll(source, model_info, dtype=F64):
173    # type: (str, ModelInfo, np.dtype) -> str
174    """
175    Returns the path to the compiled model defined by *kernel_module*.
176
177    If the model has not been compiled, or if the source file(s) are newer
178    than the dll, then *make_dll* will compile the model before returning.
179    This routine does not load the resulting dll.
180
181    *dtype* is a numpy floating point precision specifier indicating whether
182    the model should be single, double or long double precision.  The default
183    is double precision, *np.dtype('d')*.
184
185    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
186    models are not allowed as DLLs.
187
188    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
189    The default is the system temporary directory.
190    """
191    if dtype == F16:
192        raise ValueError("16 bit floats not supported")
193    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
194        dtype = F64  # Force 64-bit dll
195    # Note: dtype may be F128 for long double precision
196
197    dll = dll_path(model_info, dtype)
198
199    if not os.path.exists(dll):
200        need_recompile = True
201    elif getattr(sys, 'frozen', None) is not None:
202        # TODO: don't suppress time stamp
203        # Currently suppressing recompile when running in a frozen environment
204        need_recompile = False
205    else:
206        dll_time = os.path.getmtime(dll)
207        newest_source = generate.timestamp(model_info)
208        need_recompile = dll_time < newest_source
209    if need_recompile:
210        basename = os.path.splitext(os.path.basename(dll))[0] + "_"
211        fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
212        source = generate.convert_type(source, dtype)
213        with os.fdopen(fd, "w") as file:
214            file.write(source)
215        compile(source=filename, output=dll)
216        # comment the following to keep the generated c file
217        # Note: if there is a syntax error then compile raises an error
218        # and the source file will not be deleted.
219        os.unlink(filename)
220        #print("saving compiled file in %r"%filename)
221    return dll
222
223
224def load_dll(source, model_info, dtype=F64):
225    # type: (str, ModelInfo, np.dtype) -> "DllModel"
226    """
227    Create and load a dll corresponding to the source, info pair returned
228    from :func:`sasmodels.generate.make` compiled for the target precision.
229
230    See :func:`make_dll` for details on controlling the dll path and the
231    allowed floating point precision.
232    """
233    filename = make_dll(source, model_info, dtype=dtype)
234    return DllModel(filename, model_info, dtype=dtype)
235
236
237class DllModel(KernelModel):
238    """
239    ctypes wrapper for a single model.
240
241    *source* and *model_info* are the model source and interface as returned
242    from :func:`gen.make`.
243
244    *dtype* is the desired model precision.  Any numpy dtype for single
245    or double precision floats will do, such as 'f', 'float32' or 'single'
246    for single and 'd', 'float64' or 'double' for double.  Double precision
247    is an optional extension which may not be available on all devices.
248
249    Call :meth:`release` when done with the kernel.
250    """
251   
252    def __init__(self, dllpath, model_info, dtype=generate.F32):
253        # type: (str, ModelInfo, np.dtype) -> None
254        self.info = model_info
255        self.dllpath = dllpath
256        self._dll = None  # type: ct.CDLL
257        self.dtype = np.dtype(dtype)
258
259    def _load_dll(self):
260        # type: () -> None
261        #print("dll", self.dllpath)
262        try:
263            self._dll = ct.CDLL(self.dllpath)
264        except:
265            annotate_exception("while loading "+self.dllpath)
266            raise
267
268        fp = (c_float if self.dtype == generate.F32
269              else c_double if self.dtype == generate.F64
270              else c_longdouble)
271
272        # int, int, int, int*, double*, double*, double*, double*, double
273        argtypes = [c_int32]*3 + [c_void_p]*4 + [fp]
274        self._Iq = self._dll[generate.kernel_name(self.info, is_2d=False)]
275        self._Iqxy = self._dll[generate.kernel_name(self.info, is_2d=True)]
276        self._Iq.argtypes = argtypes
277        self._Iqxy.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):
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 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):
346        # type: (CallDetails, np.ndarray, np.ndarray, float) -> 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(*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.