source: sasmodels/sasmodels/kerneldll.py @ 8d62008

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

remove circular dependency between details/modelinfo; fix compare Calculator type hint

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