source: sasmodels/sasmodels/kerneldll.py @ 15e74ad

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

code cleanup

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