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

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

reinstate timestamp test unless running in a frozen distribution

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