source: sasmodels/sasmodels/kerneldll.py @ 3ecf034

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 3ecf034 was 3ecf034, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Don't use potentially inaccessible directory, instead create user-specific .sasmodels

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