source: sasmodels/sasmodels/kerneldll.py @ 13b99fd

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

Merge branch 'master' into polydisp

Conflicts:

.gitignore
sasmodels/kerneldll.py

  • Property mode set to 100644
File size: 11.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, realpath, 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:  # Don't use mingw
90        # fPIC is not needed on windows
91        COMPILE = "gcc -shared -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
92        if "SAS_OPENMP" in os.environ:
93            COMPILE += " -fopenmp"
94    else:
95        from tinycc import TCC
96        COMPILE = TCC + " -shared -rdynamic -Wall %(source)s -o %(output)s"
97else:
98    COMPILE = "cc -shared -fPIC -fopenmp -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
99
100# Assume the default location of module DLLs is in top level /models dir.
101DLL_PATH = joinpath(splitpath(realpath(sys.argv[0]))[0], "models")
102
103ALLOW_SINGLE_PRECISION_DLLS = True
104
105
106def dll_name(model_info, dtype):
107    # type: (ModelInfo, np.dtype) ->  str
108    """
109    Name of the dll containing the model.  This is the base file name without
110    any path or extension, with a form such as 'sas_sphere32'.
111    """
112    bits = 8*dtype.itemsize
113    return "sas_%s%d"%(model_info.id, bits)
114
115
116def dll_path(model_info, dtype):
117    # type: (ModelInfo, np.dtype) -> str
118    """
119    Complete path to the dll for the model.  Note that the dll may not
120    exist yet if it hasn't been compiled.
121    """
122    return os.path.join(DLL_PATH, dll_name(model_info, dtype)+".so")
123
124
125def make_dll(source, model_info, dtype=F64):
126    # type: (str, ModelInfo, np.dtype) -> str
127    """
128    Returns the path to the compiled model defined by *kernel_module*.
129
130    If the model has not been compiled, or if the source file(s) are newer
131    than the dll, then *make_dll* will compile the model before returning.
132    This routine does not load the resulting dll.
133
134    *dtype* is a numpy floating point precision specifier indicating whether
135    the model should be single, double or long double precision.  The default
136    is double precision, *np.dtype('d')*.
137
138    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
139    models are not allowed as DLLs.
140
141    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
142    The default is the system temporary directory.
143    """
144    if dtype == F16:
145        raise ValueError("16 bit floats not supported")
146    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
147        dtype = F64  # Force 64-bit dll
148    # Note: dtype may be F128 for long double precision
149
150    newest = generate.timestamp(model_info)
151    dll = dll_path(model_info, dtype)
152    if not os.path.exists(dll) or os.path.getmtime(dll) < newest:
153        basename = dll_name(model_info, dtype) + "_"
154        fid, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
155        source = generate.convert_type(source, dtype)
156        os.fdopen(fid, "w").write(source)
157        command = COMPILE%{"source":filename, "output":dll}
158        status = os.system(command)
159        if status != 0 or not os.path.exists(dll):
160            raise RuntimeError("compile failed.  File is in %r"%filename)
161        else:
162            ## comment the following to keep the generated c file
163            os.unlink(filename)
164            #print("saving compiled file in %r"%filename)
165    return dll
166
167
168def load_dll(source, model_info, dtype=F64):
169    # type: (str, ModelInfo, np.dtype) -> "DllModel"
170    """
171    Create and load a dll corresponding to the source, info pair returned
172    from :func:`sasmodels.generate.make` compiled for the target precision.
173
174    See :func:`make_dll` for details on controlling the dll path and the
175    allowed floating point precision.
176    """
177    filename = make_dll(source, model_info, dtype=dtype)
178    return DllModel(filename, model_info, dtype=dtype)
179
180
181class DllModel(KernelModel):
182    """
183    ctypes wrapper for a single model.
184
185    *source* and *model_info* are the model source and interface as returned
186    from :func:`gen.make`.
187
188    *dtype* is the desired model precision.  Any numpy dtype for single
189    or double precision floats will do, such as 'f', 'float32' or 'single'
190    for single and 'd', 'float64' or 'double' for double.  Double precision
191    is an optional extension which may not be available on all devices.
192
193    Call :meth:`release` when done with the kernel.
194    """
195   
196    def __init__(self, dllpath, model_info, dtype=generate.F32):
197        # type: (str, ModelInfo, np.dtype) -> None
198        self.info = model_info
199        self.dllpath = dllpath
200        self._dll = None  # type: ct.CDLL
201        self.dtype = np.dtype(dtype)
202
203    def _load_dll(self):
204        # type: () -> None
205        #print("dll", self.dllpath)
206        try:
207            self._dll = ct.CDLL(self.dllpath)
208        except:
209            annotate_exception("while loading "+self.dllpath)
210            raise
211
212        fp = (c_float if self.dtype == generate.F32
213              else c_double if self.dtype == generate.F64
214              else c_longdouble)
215
216        # int, int, int, int*, double*, double*, double*, double*, double*, double
217        argtypes = [c_int32]*3 + [c_void_p]*5 + [fp]
218        self._Iq = self._dll[generate.kernel_name(self.info, is_2d=False)]
219        self._Iqxy = self._dll[generate.kernel_name(self.info, is_2d=True)]
220        self._Iq.argtypes = argtypes
221        self._Iqxy.argtypes = argtypes
222
223    def __getstate__(self):
224        # type: () -> Tuple[ModelInfo, str]
225        return self.info, self.dllpath
226
227    def __setstate__(self, state):
228        # type: (Tuple[ModelInfo, str]) -> None
229        self.info, self.dllpath = state
230        self._dll = None
231
232    def make_kernel(self, q_vectors):
233        # type: (List[np.ndarray]) -> DllKernel
234        q_input = PyInput(q_vectors, self.dtype)
235        # Note: pickle not supported for DllKernel
236        if self._dll is None:
237            self._load_dll()
238        kernel = self._Iqxy if q_input.is_2d else self._Iq
239        return DllKernel(kernel, self.info, q_input)
240
241    def release(self):
242        # type: () -> None
243        """
244        Release any resources associated with the model.
245        """
246        if os.name == 'nt':
247            #dll = ct.cdll.LoadLibrary(self.dllpath)
248            dll = ct.CDLL(self.dllpath)
249            libHandle = dll._handle
250            #libHandle = ct.c_void_p(dll._handle)
251            del dll, self._dll
252            self._dll = None
253            #_ctypes.FreeLibrary(libHandle)
254            ct.windll.kernel32.FreeLibrary(libHandle)
255        else:   
256            pass 
257
258
259class DllKernel(Kernel):
260    """
261    Callable SAS kernel.
262
263    *kernel* is the c function to call.
264
265    *model_info* is the module information
266
267    *q_input* is the DllInput q vectors at which the kernel should be
268    evaluated.
269
270    The resulting call method takes the *pars*, a list of values for
271    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
272    vectors for the polydisperse parameters.  *cutoff* determines the
273    integration limits: any points with combined weight less than *cutoff*
274    will not be calculated.
275
276    Call :meth:`release` when done with the kernel instance.
277    """
278    def __init__(self, kernel, model_info, q_input):
279        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
280        self.kernel = kernel
281        self.info = model_info
282        self.q_input = q_input
283        self.dtype = q_input.dtype
284        self.dim = '2d' if q_input.is_2d else '1d'
285        self.result = np.empty(q_input.nq+1, q_input.dtype)
286        self.real = (np.float32 if self.q_input.dtype == generate.F32
287                     else np.float64 if self.q_input.dtype == generate.F64
288                     else np.float128)
289
290    def __call__(self, call_details, weights, values, cutoff):
291        # type: (CallDetails, np.ndarray, np.ndarray, float) -> np.ndarray
292
293        #print("in kerneldll")
294        #print("weights", weights)
295        #print("values", values)
296        start, stop = 0, call_details.total_pd
297        args = [
298            self.q_input.nq, # nq
299            start, # pd_start
300            stop, # pd_stop pd_stride[MAX_PD]
301            call_details.buffer.ctypes.data, # problem
302            weights.ctypes.data,  # weights
303            values.ctypes.data,  #pars
304            self.q_input.q.ctypes.data, #q
305            self.result.ctypes.data,   # results
306            self.real(cutoff), # cutoff
307            ]
308        self.kernel(*args) # type: ignore
309        return self.result[:-1]
310
311    def release(self):
312        # type: () -> None
313        """
314        Release any resources associated with the kernel.
315        """
316        self.q_input.release()
Note: See TracBrowser for help on using the repository browser.