source: sasmodels/sasmodels/kerneldll.py @ 2a55a6f

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

add support for tinycc

  • 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
52from ctypes import c_void_p, c_int, c_longdouble, c_double, c_float
53import _ctypes
54
55import numpy as np
56
57from . import generate
58from .kernelpy import PyInput, PyModel
59from .exception import annotate_exception
60
61# Compiler platform details
62if sys.platform == 'darwin':
63    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
64    COMPILE = "gcc -shared -fPIC -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
65elif os.name == 'nt':
66    # call vcvarsall.bat before compiling to set path, headers, libs, etc.
67    if "VCINSTALLDIR" in os.environ:
68        # MSVC compiler is available, so use it.  OpenMP requires a copy of
69        # vcomp90.dll on the path.  One may be found here:
70        #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
71        # Copy this to the python directory and uncomment the OpenMP COMPILE
72        # TODO: remove intermediate OBJ file created in the directory
73        # TODO: maybe don't use randomized name for the c file
74        CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG /Tp%(source)s "
75        LN = "/link /DLL /INCREMENTAL:NO /MANIFEST /OUT:%(output)s"
76        if "SAS_OPENMP" in os.environ:
77            COMPILE = " ".join((CC, "/openmp", LN))
78        else:
79            COMPILE = " ".join((CC, LN))
80    else:
81        # fPIC is unused on windows
82        # COMPILE = "gcc -shared -fPIC -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
83        COMPILE = "gcc -shared -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
84        if "SAS_OPENMP" in os.environ:
85            COMPILE = COMPILE + " -fopenmp"
86        #COMPILE = "z:/tcc/tcc -shared -rdynamic -Wall %(source)s -o %(output)s"
87else:
88    COMPILE = "cc -shared -fPIC -fopenmp -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
89
90# Assume the default location of module DLLs is within the sasmodel directory.
91DLL_PATH = os.path.join(os.path.split(os.path.realpath(__file__))[0], "models", "dll")
92
93ALLOW_SINGLE_PRECISION_DLLS = True
94
95
96def dll_path(model_info, dtype="double"):
97    """
98    Path to the compiled model defined by *model_info*.
99    """
100    from os.path import join as joinpath, split as splitpath, splitext
101    basename = splitext(splitpath(model_info['filename'])[1])[0]
102    if np.dtype(dtype) == generate.F32:
103        basename += "32"
104    elif np.dtype(dtype) == generate.F64:
105        basename += "64"
106    else:
107        basename += "128"
108    return joinpath(DLL_PATH, basename+'.so')
109
110def make_dll(source, model_info, dtype="double"):
111    """
112    Load the compiled model defined by *kernel_module*.
113
114    Recompile if any files are newer than the model file.
115
116    *dtype* is a numpy floating point precision specifier indicating whether
117    the model should be single or double precision.  The default is double
118    precision.
119
120    The DLL is not loaded until the kernel is called so models can
121    be defined without using too many resources.
122
123    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
124    The default is the system temporary directory.
125
126    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to True if single precision
127    models are allowed as DLLs.
128    """
129    if callable(model_info.get('Iq', None)):
130        return PyModel(model_info)
131   
132    dtype = np.dtype(dtype)
133    if dtype == generate.F16:
134        raise ValueError("16 bit floats not supported")
135    if dtype == generate.F32 and not ALLOW_SINGLE_PRECISION_DLLS:
136        dtype = generate.F64  # Force 64-bit dll
137
138    if dtype == generate.F32: # 32-bit dll
139        tempfile_prefix = 'sas_' + model_info['name'] + '32_'
140    elif dtype == generate.F64:
141        tempfile_prefix = 'sas_' + model_info['name'] + '64_'
142    else:
143        tempfile_prefix = 'sas_' + model_info['name'] + '128_'
144 
145    source = generate.convert_type(source, dtype)
146    source_files = generate.model_sources(model_info) + [model_info['filename']]
147    dll = dll_path(model_info, dtype)
148
149    #newest = max(os.path.getmtime(f) for f in source_files)
150    #if not os.path.exists(dll) or os.path.getmtime(dll) < newest:
151    if not os.path.exists(dll):
152        # Replace with a proper temp file
153        fid, filename = tempfile.mkstemp(suffix=".c", prefix=tempfile_prefix)
154        os.fdopen(fid, "w").write(source)
155        command = COMPILE%{"source":filename, "output":dll}
156        print("Compile command: "+command)
157        status = os.system(command)
158        if status != 0 or not os.path.exists(dll):
159            raise RuntimeError("compile failed.  File is in %r"%filename)
160        else:
161            ## comment the following to keep the generated c file
162            os.unlink(filename)
163            #print("saving compiled file in %r"%filename)
164    return dll
165
166
167def load_dll(source, model_info, dtype="double"):
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
179IQ_ARGS = [c_void_p, c_void_p, c_int]
180IQXY_ARGS = [c_void_p, c_void_p, c_void_p, c_int]
181
182class DllModel(object):
183    """
184    ctypes wrapper for a single model.
185
186    *source* and *model_info* are the model source and interface as returned
187    from :func:`gen.make`.
188
189    *dtype* is the desired model precision.  Any numpy dtype for single
190    or double precision floats will do, such as 'f', 'float32' or 'single'
191    for single and 'd', 'float64' or 'double' for double.  Double precision
192    is an optional extension which may not be available on all devices.
193
194    Call :meth:`release` when done with the kernel.
195    """
196   
197    def __init__(self, dllpath, model_info, dtype=generate.F32):
198        self.info = model_info
199        self.dllpath = dllpath
200        self.dll = None
201        self.dtype = np.dtype(dtype)
202
203    def _load_dll(self):
204        Nfixed1d = len(self.info['partype']['fixed-1d'])
205        Nfixed2d = len(self.info['partype']['fixed-2d'])
206        Npd1d = len(self.info['partype']['pd-1d'])
207        Npd2d = len(self.info['partype']['pd-2d'])
208
209        #print("dll", self.dllpath)
210        try:
211            self.dll = ct.CDLL(self.dllpath)
212        except:
213            annotate_exception("while loading "+self.dllpath)
214            raise
215
216        fp = (c_float if self.dtype == generate.F32
217              else c_double if self.dtype == generate.F64
218              else c_longdouble)
219        pd_args_1d = [c_void_p, fp] + [c_int]*Npd1d if Npd1d else []
220        pd_args_2d = [c_void_p, fp] + [c_int]*Npd2d if Npd2d else []
221        self.Iq = self.dll[generate.kernel_name(self.info, False)]
222        self.Iq.argtypes = IQ_ARGS + pd_args_1d + [fp]*Nfixed1d
223
224        self.Iqxy = self.dll[generate.kernel_name(self.info, True)]
225        self.Iqxy.argtypes = IQXY_ARGS + pd_args_2d + [fp]*Nfixed2d
226       
227        self.release()
228
229    def __getstate__(self):
230        return self.info, self.dllpath
231
232    def __setstate__(self, state):
233        self.info, self.dllpath = state
234        self.dll = None
235
236    def make_kernel(self, q_vectors):
237        q_input = PyInput(q_vectors, self.dtype)
238        if self.dll is None: self._load_dll()
239        kernel = self.Iqxy if q_input.is_2d else self.Iq
240        return DllKernel(kernel, self.info, q_input)
241
242    def release(self):
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(object):
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        self.info = model_info
280        self.q_input = q_input
281        self.kernel = kernel
282        self.res = np.empty(q_input.nq, q_input.dtype)
283        dim = '2d' if q_input.is_2d else '1d'
284        self.fixed_pars = model_info['partype']['fixed-' + dim]
285        self.pd_pars = model_info['partype']['pd-' + dim]
286
287        # In dll kernel, but not in opencl kernel
288        self.p_res = self.res.ctypes.data
289
290    def __call__(self, fixed_pars, pd_pars, cutoff):
291        real = (np.float32 if self.q_input.dtype == generate.F32
292                else np.float64 if self.q_input.dtype == generate.F64
293                else np.float128)
294
295        nq = c_int(self.q_input.nq)
296        if pd_pars:
297            cutoff = real(cutoff)
298            loops_N = [np.uint32(len(p[0])) for p in pd_pars]
299            loops = np.hstack(pd_pars)
300            loops = np.ascontiguousarray(loops.T, self.q_input.dtype).flatten()
301            p_loops = loops.ctypes.data
302            dispersed = [p_loops, cutoff] + loops_N
303        else:
304            dispersed = []
305        fixed = [real(p) for p in fixed_pars]
306        args = self.q_input.q_pointers + [self.p_res, nq] + dispersed + fixed
307        #print(pars)
308        self.kernel(*args)
309
310        return self.res
311
312    def release(self):
313        """
314        Release any resources associated with the kernel.
315        """
316        pass
Note: See TracBrowser for help on using the repository browser.