source: sasmodels/sasmodels/kerneldll.py @ f619de7

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

more type hinting

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