source: sasmodels/sasmodels/kerneldll.py @ 74667d3

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

don't print path to generated c file when it is being deleted after build

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