source: sasmodels/sasmodels/kerneldll.py @ 739aad4

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

change method for selecting a particular compiler

  • Property mode set to 100644
File size: 14.3 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
59try:
60    import tinycc
61except ImportError:
62    tinycc = None
63
64from . import generate
65from .kernel import KernelModel, Kernel
66from .kernelpy import PyInput
67from .exception import annotate_exception
68from .generate import F16, F32, F64
69
70try:
71    from typing import Tuple, Callable, Any
72    from .modelinfo import ModelInfo
73    from .details import CallDetails
74except ImportError:
75    pass
76
77if "SAS_COMPILER" in os.environ:
78    compiler = os.environ["SAS_COMPILER"]
79elif os.name == 'nt':
80    # If vcvarsall.bat has been called, then VCINSTALLDIR is in the environment
81    # and we can use the MSVC compiler.  Otherwise, if tinycc is available
82    # the use it.  Otherwise, hope that mingw is available.
83    if "VCINSTALLDIR" in os.environ:
84        compiler = "msvc"
85    elif tinycc:
86        compiler = "tinycc"
87    else:
88        compiler = "mingw"
89else:
90    compiler = "unix"
91
92ARCH = "" if sys.maxint > 2**32 else "x86"  # maxint=2**31-1 on 32 bit
93if compiler == "unix":
94    # Generic unix compile
95    # On mac users will need the X code command line tools installed
96    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
97    CC = "cc -shared -fPIC -std=c99 -O2 -Wall".split()
98    # add openmp support if not running on a mac
99    if sys.platform != "darwin":
100        CC.append("-fopenmp")
101    def compile_command(source, output):
102        return CC + [source, "-o", output, "-lm"]
103elif compiler == "msvc":
104    # Call vcvarsall.bat before compiling to set path, headers, libs, etc.
105    # MSVC compiler is available, so use it.  OpenMP requires a copy of
106    # vcomp90.dll on the path.  One may be found here:
107    #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
108    # Copy this to the python directory and uncomment the OpenMP COMPILE
109    # TODO: remove intermediate OBJ file created in the directory
110    # TODO: maybe don't use randomized name for the c file
111    # TODO: maybe ask distutils to find MSVC
112    CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG".split()
113    if "SAS_OPENMP" in os.environ:
114        CC.append("/openmp")
115    LN = "/link /DLL /INCREMENTAL:NO /MANIFEST".split()
116    def compile_command(source, output):
117        return CC + ["/Tp%s"%source] + LN + ["/OUT:%s"%output]
118elif compiler == "tinycc":
119    # TinyCC compiler.
120    CC = [tinycc.TCC] + "-shared -rdynamic -Wall".split()
121    def compile_command(source, output):
122        return CC + [source, "-o", output]
123elif compiler == "mingw":
124    # MinGW compiler.
125    CC = "gcc -shared -std=c99 -O2 -Wall".split()
126    if "SAS_OPENMP" in os.environ:
127        CC.append("-fopenmp")
128    def compile_command(source, output):
129        return CC + [source, "-o", output, "-lm"]
130
131# Windows-specific solution
132if os.name == 'nt':
133    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
134    DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
135    if not os.path.exists(DLL_PATH):
136        os.makedirs(DLL_PATH)
137else:
138    # Set up the default path for compiled modules.
139    DLL_PATH = tempfile.gettempdir()
140
141ALLOW_SINGLE_PRECISION_DLLS = True
142
143def compile(source, output):
144    command = compile_command(source=source, output=output)
145    command_str = " ".join('"%s"'%p if ' ' in p else p for p in command)
146    logging.info(command_str)
147    try:
148        # need shell=True on windows to keep console box from popping up
149        shell = (os.name == 'nt')
150        subprocess.check_output(command, shell=shell, stderr=subprocess.STDOUT)
151    except subprocess.CalledProcessError as exc:
152        raise RuntimeError("compile failed.\n%s\n%s"%(command_str, exc.output))
153    if not os.path.exists(output):
154        raise RuntimeError("compile failed.  File is in %r"%source)
155
156def dll_name(model_info, dtype):
157    # type: (ModelInfo, np.dtype) ->  str
158    """
159    Name of the dll containing the model.  This is the base file name without
160    any path or extension, with a form such as 'sas_sphere32'.
161    """
162    bits = 8*dtype.itemsize
163    basename = "sas%d_%s"%(bits, model_info.id)
164    basename += ARCH + ".so"
165
166    # Hack to find precompiled dlls
167    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename)
168    if os.path.exists(path):
169        return path
170
171    return joinpath(DLL_PATH, basename)
172
173
174def dll_path(model_info, dtype):
175    # type: (ModelInfo, np.dtype) -> str
176    """
177    Complete path to the dll for the model.  Note that the dll may not
178    exist yet if it hasn't been compiled.
179    """
180    return os.path.join(DLL_PATH, dll_name(model_info, dtype))
181
182
183def make_dll(source, model_info, dtype=F64):
184    # type: (str, ModelInfo, np.dtype) -> str
185    """
186    Returns the path to the compiled model defined by *kernel_module*.
187
188    If the model has not been compiled, or if the source file(s) are newer
189    than the dll, then *make_dll* will compile the model before returning.
190    This routine does not load the resulting dll.
191
192    *dtype* is a numpy floating point precision specifier indicating whether
193    the model should be single, double or long double precision.  The default
194    is double precision, *np.dtype('d')*.
195
196    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
197    models are not allowed as DLLs.
198
199    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
200    The default is the system temporary directory.
201    """
202    if dtype == F16:
203        raise ValueError("16 bit floats not supported")
204    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
205        dtype = F64  # Force 64-bit dll
206    # Note: dtype may be F128 for long double precision
207
208    dll = dll_path(model_info, dtype)
209
210    if not os.path.exists(dll):
211        need_recompile = True
212    elif getattr(sys, 'frozen', None) is not None:
213        # TODO: don't suppress time stamp
214        # Currently suppressing recompile when running in a frozen environment
215        need_recompile = False
216    else:
217        dll_time = os.path.getmtime(dll)
218        newest_source = generate.timestamp(model_info)
219        need_recompile = dll_time < newest_source
220    if need_recompile:
221        basename = os.path.splitext(os.path.basename(dll))[0] + "_"
222        fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
223        source = generate.convert_type(source, dtype)
224        with os.fdopen(fd, "w") as file:
225            file.write(source)
226        compile(source=filename, output=dll)
227        # comment the following to keep the generated c file
228        # Note: if there is a syntax error then compile raises an error
229        # and the source file will not be deleted.
230        os.unlink(filename)
231        #print("saving compiled file in %r"%filename)
232    return dll
233
234
235def load_dll(source, model_info, dtype=F64):
236    # type: (str, ModelInfo, np.dtype) -> "DllModel"
237    """
238    Create and load a dll corresponding to the source, info pair returned
239    from :func:`sasmodels.generate.make` compiled for the target precision.
240
241    See :func:`make_dll` for details on controlling the dll path and the
242    allowed floating point precision.
243    """
244    filename = make_dll(source, model_info, dtype=dtype)
245    return DllModel(filename, model_info, dtype=dtype)
246
247
248class DllModel(KernelModel):
249    """
250    ctypes wrapper for a single model.
251
252    *source* and *model_info* are the model source and interface as returned
253    from :func:`gen.make`.
254
255    *dtype* is the desired model precision.  Any numpy dtype for single
256    or double precision floats will do, such as 'f', 'float32' or 'single'
257    for single and 'd', 'float64' or 'double' for double.  Double precision
258    is an optional extension which may not be available on all devices.
259
260    Call :meth:`release` when done with the kernel.
261    """
262   
263    def __init__(self, dllpath, model_info, dtype=generate.F32):
264        # type: (str, ModelInfo, np.dtype) -> None
265        self.info = model_info
266        self.dllpath = dllpath
267        self._dll = None  # type: ct.CDLL
268        self.dtype = np.dtype(dtype)
269
270    def _load_dll(self):
271        # type: () -> None
272        #print("dll", self.dllpath)
273        try:
274            self._dll = ct.CDLL(self.dllpath)
275        except:
276            annotate_exception("while loading "+self.dllpath)
277            raise
278
279        fp = (c_float if self.dtype == generate.F32
280              else c_double if self.dtype == generate.F64
281              else c_longdouble)
282
283        # int, int, int, int*, double*, double*, double*, double*, double
284        argtypes = [c_int32]*3 + [c_void_p]*4 + [fp]
285        names = [generate.kernel_name(self.info, variant)
286                 for variant in ("Iq", "Iqxy", "Imagnetic")]
287        self._kernels = [self._dll[name] for name in names]
288        for k in self._kernels:
289            k.argtypes = argtypes
290
291    def __getstate__(self):
292        # type: () -> Tuple[ModelInfo, str]
293        return self.info, self.dllpath
294
295    def __setstate__(self, state):
296        # type: (Tuple[ModelInfo, str]) -> None
297        self.info, self.dllpath = state
298        self._dll = None
299
300    def make_kernel(self, q_vectors):
301        # type: (List[np.ndarray]) -> DllKernel
302        q_input = PyInput(q_vectors, self.dtype)
303        # Note: pickle not supported for DllKernel
304        if self._dll is None:
305            self._load_dll()
306        is_2d = len(q_vectors) == 2
307        kernel = self._kernels[1:3] if is_2d else [self._kernels[0]]*2
308        return DllKernel(kernel, self.info, q_input)
309
310    def release(self):
311        # type: () -> None
312        """
313        Release any resources associated with the model.
314        """
315        if os.name == 'nt':
316            #dll = ct.cdll.LoadLibrary(self.dllpath)
317            dll = ct.CDLL(self.dllpath)
318            libHandle = dll._handle
319            #libHandle = ct.c_void_p(dll._handle)
320            del dll, self._dll
321            self._dll = None
322            ct.windll.kernel32.FreeLibrary(libHandle)
323        else:   
324            pass 
325
326
327class DllKernel(Kernel):
328    """
329    Callable SAS kernel.
330
331    *kernel* is the c function to call.
332
333    *model_info* is the module information
334
335    *q_input* is the DllInput q vectors at which the kernel should be
336    evaluated.
337
338    The resulting call method takes the *pars*, a list of values for
339    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
340    vectors for the polydisperse parameters.  *cutoff* determines the
341    integration limits: any points with combined weight less than *cutoff*
342    will not be calculated.
343
344    Call :meth:`release` when done with the kernel instance.
345    """
346    def __init__(self, kernel, model_info, q_input):
347        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
348        self.kernel = kernel
349        self.info = model_info
350        self.q_input = q_input
351        self.dtype = q_input.dtype
352        self.dim = '2d' if q_input.is_2d else '1d'
353        self.result = np.empty(q_input.nq+1, q_input.dtype)
354        self.real = (np.float32 if self.q_input.dtype == generate.F32
355                     else np.float64 if self.q_input.dtype == generate.F64
356                     else np.float128)
357
358    def __call__(self, call_details, values, cutoff, magnetic):
359        # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray
360
361        kernel = self.kernel[1 if magnetic else 0]
362        args = [
363            self.q_input.nq, # nq
364            None, # pd_start
365            None, # pd_stop pd_stride[MAX_PD]
366            call_details.buffer.ctypes.data, # problem
367            values.ctypes.data,  #pars
368            self.q_input.q.ctypes.data, #q
369            self.result.ctypes.data,   # results
370            self.real(cutoff), # cutoff
371        ]
372        #print("kerneldll values", values)
373        step = 100
374        for start in range(0, call_details.pd_prod, step):
375            stop = min(start+step, call_details.pd_prod)
376            args[1:3] = [start, stop]
377            #print("calling DLL")
378            kernel(*args) # type: ignore
379
380        #print("returned",self.q_input.q, self.result)
381        scale = values[0]/self.result[self.q_input.nq]
382        background = values[1]
383        #print("scale",scale,background)
384        return scale*self.result[:self.q_input.nq] + background
385
386    def release(self):
387        # type: () -> None
388        """
389        Release any resources associated with the kernel.
390        """
391        self.q_input.release()
Note: See TracBrowser for help on using the repository browser.