source: sasmodels/sasmodels/kerneldll.py @ 639c4e3

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

Merge branch 'master' into polydisp

Conflicts:

sasmodels/kerneldll.py
sasmodels/models/rpa.c

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