source: sasmodels/sasmodels/kerneldll.py @ 5ff1b03

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

working kerneldll

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