source: sasmodels/sasmodels/kerneldll.py @ 61f8638

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

Merge branch 'master' into polydisp

Conflicts:

sasmodels/generate.py
sasmodels/kerneldll.py

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