| """Module for compiling codegen output, and wrap the binary for use in |
| python. |
| |
| .. note:: To use the autowrap module it must first be imported |
| |
| >>> from sympy.utilities.autowrap import autowrap |
| |
| This module provides a common interface for different external backends, such |
| as f2py, fwrap, Cython, SWIG(?) etc. (Currently only f2py and Cython are |
| implemented) The goal is to provide access to compiled binaries of acceptable |
| performance with a one-button user interface, e.g., |
| |
| >>> from sympy.abc import x,y |
| >>> expr = (x - y)**25 |
| >>> flat = expr.expand() |
| >>> binary_callable = autowrap(flat) |
| >>> binary_callable(2, 3) |
| -1.0 |
| |
| Although a SymPy user might primarily be interested in working with |
| mathematical expressions and not in the details of wrapping tools |
| needed to evaluate such expressions efficiently in numerical form, |
| the user cannot do so without some understanding of the |
| limits in the target language. For example, the expanded expression |
| contains large coefficients which result in loss of precision when |
| computing the expression: |
| |
| >>> binary_callable(3, 2) |
| 0.0 |
| >>> binary_callable(4, 5), binary_callable(5, 4) |
| (-22925376.0, 25165824.0) |
| |
| Wrapping the unexpanded expression gives the expected behavior: |
| |
| >>> e = autowrap(expr) |
| >>> e(4, 5), e(5, 4) |
| (-1.0, 1.0) |
| |
| The callable returned from autowrap() is a binary Python function, not a |
| SymPy object. If it is desired to use the compiled function in symbolic |
| expressions, it is better to use binary_function() which returns a SymPy |
| Function object. The binary callable is attached as the _imp_ attribute and |
| invoked when a numerical evaluation is requested with evalf(), or with |
| lambdify(). |
| |
| >>> from sympy.utilities.autowrap import binary_function |
| >>> f = binary_function('f', expr) |
| >>> 2*f(x, y) + y |
| y + 2*f(x, y) |
| >>> (2*f(x, y) + y).evalf(2, subs={x: 1, y:2}) |
| 0.e-110 |
| |
| When is this useful? |
| |
| 1) For computations on large arrays, Python iterations may be too slow, |
| and depending on the mathematical expression, it may be difficult to |
| exploit the advanced index operations provided by NumPy. |
| |
| 2) For *really* long expressions that will be called repeatedly, the |
| compiled binary should be significantly faster than SymPy's .evalf() |
| |
| 3) If you are generating code with the codegen utility in order to use |
| it in another project, the automatic Python wrappers let you test the |
| binaries immediately from within SymPy. |
| |
| 4) To create customized ufuncs for use with numpy arrays. |
| See *ufuncify*. |
| |
| When is this module NOT the best approach? |
| |
| 1) If you are really concerned about speed or memory optimizations, |
| you will probably get better results by working directly with the |
| wrapper tools and the low level code. However, the files generated |
| by this utility may provide a useful starting point and reference |
| code. Temporary files will be left intact if you supply the keyword |
| tempdir="path/to/files/". |
| |
| 2) If the array computation can be handled easily by numpy, and you |
| do not need the binaries for another project. |
| |
| """ |
|
|
| import sys |
| import os |
| import shutil |
| import tempfile |
| from pathlib import Path |
| from subprocess import STDOUT, CalledProcessError, check_output |
| from string import Template |
| from warnings import warn |
|
|
| from sympy.core.cache import cacheit |
| from sympy.core.function import Lambda |
| from sympy.core.relational import Eq |
| from sympy.core.symbol import Dummy, Symbol |
| from sympy.tensor.indexed import Idx, IndexedBase |
| from sympy.utilities.codegen import (make_routine, get_code_generator, |
| OutputArgument, InOutArgument, |
| InputArgument, CodeGenArgumentListError, |
| Result, ResultBase, C99CodeGen) |
| from sympy.utilities.iterables import iterable |
| from sympy.utilities.lambdify import implemented_function |
| from sympy.utilities.decorator import doctest_depends_on |
|
|
| _doctest_depends_on = {'exe': ('f2py', 'gfortran', 'gcc'), |
| 'modules': ('numpy',)} |
|
|
|
|
| class CodeWrapError(Exception): |
| pass |
|
|
|
|
| class CodeWrapper: |
| """Base Class for code wrappers""" |
| _filename = "wrapped_code" |
| _module_basename = "wrapper_module" |
| _module_counter = 0 |
|
|
| @property |
| def filename(self): |
| return "%s_%s" % (self._filename, CodeWrapper._module_counter) |
|
|
| @property |
| def module_name(self): |
| return "%s_%s" % (self._module_basename, CodeWrapper._module_counter) |
|
|
| def __init__(self, generator, filepath=None, flags=[], verbose=False): |
| """ |
| generator -- the code generator to use |
| """ |
| self.generator = generator |
| self.filepath = filepath |
| self.flags = flags |
| self.quiet = not verbose |
|
|
| @property |
| def include_header(self): |
| return bool(self.filepath) |
|
|
| @property |
| def include_empty(self): |
| return bool(self.filepath) |
|
|
| def _generate_code(self, main_routine, routines): |
| routines.append(main_routine) |
| self.generator.write( |
| routines, self.filename, True, self.include_header, |
| self.include_empty) |
|
|
| def wrap_code(self, routine, helpers=None): |
| helpers = helpers or [] |
| if self.filepath: |
| workdir = os.path.abspath(self.filepath) |
| else: |
| workdir = tempfile.mkdtemp("_sympy_compile") |
| if not os.access(workdir, os.F_OK): |
| os.mkdir(workdir) |
| oldwork = os.getcwd() |
| os.chdir(workdir) |
| try: |
| sys.path.append(workdir) |
| self._generate_code(routine, helpers) |
| self._prepare_files(routine) |
| self._process_files(routine) |
| mod = __import__(self.module_name) |
| finally: |
| sys.path.remove(workdir) |
| CodeWrapper._module_counter += 1 |
| os.chdir(oldwork) |
| if not self.filepath: |
| try: |
| shutil.rmtree(workdir) |
| except OSError: |
| |
| pass |
|
|
| return self._get_wrapped_function(mod, routine.name) |
|
|
| def _process_files(self, routine): |
| command = self.command |
| command.extend(self.flags) |
| try: |
| retoutput = check_output(command, stderr=STDOUT) |
| except CalledProcessError as e: |
| raise CodeWrapError( |
| "Error while executing command: %s. Command output is:\n%s" % ( |
| " ".join(command), e.output.decode('utf-8'))) |
| if not self.quiet: |
| print(retoutput) |
|
|
|
|
| class DummyWrapper(CodeWrapper): |
| """Class used for testing independent of backends """ |
|
|
| template = """# dummy module for testing of SymPy |
| def %(name)s(): |
| return "%(expr)s" |
| %(name)s.args = "%(args)s" |
| %(name)s.returns = "%(retvals)s" |
| """ |
|
|
| def _prepare_files(self, routine): |
| return |
|
|
| def _generate_code(self, routine, helpers): |
| with open('%s.py' % self.module_name, 'w') as f: |
| printed = ", ".join( |
| [str(res.expr) for res in routine.result_variables]) |
| |
| args = filter(lambda x: not isinstance( |
| x, OutputArgument), routine.arguments) |
| retvals = [] |
| for val in routine.result_variables: |
| if isinstance(val, Result): |
| retvals.append('nameless') |
| else: |
| retvals.append(val.result_var) |
|
|
| print(DummyWrapper.template % { |
| 'name': routine.name, |
| 'expr': printed, |
| 'args': ", ".join([str(a.name) for a in args]), |
| 'retvals': ", ".join([str(val) for val in retvals]) |
| }, end="", file=f) |
|
|
| def _process_files(self, routine): |
| return |
|
|
| @classmethod |
| def _get_wrapped_function(cls, mod, name): |
| return getattr(mod, name) |
|
|
|
|
| class CythonCodeWrapper(CodeWrapper): |
| """Wrapper that uses Cython""" |
|
|
| setup_template = """\ |
| from setuptools import setup |
| from setuptools import Extension |
| from Cython.Build import cythonize |
| cy_opts = {cythonize_options} |
| {np_import} |
| ext_mods = [Extension( |
| {ext_args}, |
| include_dirs={include_dirs}, |
| library_dirs={library_dirs}, |
| libraries={libraries}, |
| extra_compile_args={extra_compile_args}, |
| extra_link_args={extra_link_args} |
| )] |
| setup(ext_modules=cythonize(ext_mods, **cy_opts)) |
| """ |
|
|
| _cythonize_options = {'compiler_directives':{'language_level' : "3"}} |
|
|
| pyx_imports = ( |
| "import numpy as np\n" |
| "cimport numpy as np\n\n") |
|
|
| pyx_header = ( |
| "cdef extern from '{header_file}.h':\n" |
| " {prototype}\n\n") |
|
|
| pyx_func = ( |
| "def {name}_c({arg_string}):\n" |
| "\n" |
| "{declarations}" |
| "{body}") |
|
|
| std_compile_flag = '-std=c99' |
|
|
| def __init__(self, *args, **kwargs): |
| """Instantiates a Cython code wrapper. |
| |
| The following optional parameters get passed to ``setuptools.Extension`` |
| for building the Python extension module. Read its documentation to |
| learn more. |
| |
| Parameters |
| ========== |
| include_dirs : [list of strings] |
| A list of directories to search for C/C++ header files (in Unix |
| form for portability). |
| library_dirs : [list of strings] |
| A list of directories to search for C/C++ libraries at link time. |
| libraries : [list of strings] |
| A list of library names (not filenames or paths) to link against. |
| extra_compile_args : [list of strings] |
| Any extra platform- and compiler-specific information to use when |
| compiling the source files in 'sources'. For platforms and |
| compilers where "command line" makes sense, this is typically a |
| list of command-line arguments, but for other platforms it could be |
| anything. Note that the attribute ``std_compile_flag`` will be |
| appended to this list. |
| extra_link_args : [list of strings] |
| Any extra platform- and compiler-specific information to use when |
| linking object files together to create the extension (or to create |
| a new static Python interpreter). Similar interpretation as for |
| 'extra_compile_args'. |
| cythonize_options : [dictionary] |
| Keyword arguments passed on to cythonize. |
| |
| """ |
|
|
| self._include_dirs = kwargs.pop('include_dirs', []) |
| self._library_dirs = kwargs.pop('library_dirs', []) |
| self._libraries = kwargs.pop('libraries', []) |
| self._extra_compile_args = kwargs.pop('extra_compile_args', []) |
| self._extra_compile_args.append(self.std_compile_flag) |
| self._extra_link_args = kwargs.pop('extra_link_args', []) |
| self._cythonize_options = kwargs.pop('cythonize_options', self._cythonize_options) |
|
|
| self._need_numpy = False |
|
|
| super().__init__(*args, **kwargs) |
|
|
| @property |
| def command(self): |
| command = [sys.executable, "setup.py", "build_ext", "--inplace"] |
| return command |
|
|
| def _prepare_files(self, routine, build_dir=os.curdir): |
| |
| pyxfilename = self.module_name + '.pyx' |
| codefilename = "%s.%s" % (self.filename, self.generator.code_extension) |
|
|
| |
| with open(os.path.join(build_dir, pyxfilename), 'w') as f: |
| self.dump_pyx([routine], f, self.filename) |
|
|
| |
| ext_args = [repr(self.module_name), repr([pyxfilename, codefilename])] |
| if self._need_numpy: |
| np_import = 'import numpy as np\n' |
| self._include_dirs.append('np.get_include()') |
| else: |
| np_import = '' |
|
|
| includes = str(self._include_dirs).replace("'np.get_include()'", |
| 'np.get_include()') |
| code = self.setup_template.format( |
| ext_args=", ".join(ext_args), |
| np_import=np_import, |
| include_dirs=includes, |
| library_dirs=self._library_dirs, |
| libraries=self._libraries, |
| extra_compile_args=self._extra_compile_args, |
| extra_link_args=self._extra_link_args, |
| cythonize_options=self._cythonize_options) |
| Path(os.path.join(build_dir, 'setup.py')).write_text(code) |
|
|
| @classmethod |
| def _get_wrapped_function(cls, mod, name): |
| return getattr(mod, name + '_c') |
|
|
| def dump_pyx(self, routines, f, prefix): |
| """Write a Cython file with Python wrappers |
| |
| This file contains all the definitions of the routines in c code and |
| refers to the header file. |
| |
| Arguments |
| --------- |
| routines |
| List of Routine instances |
| f |
| File-like object to write the file to |
| prefix |
| The filename prefix, used to refer to the proper header file. |
| Only the basename of the prefix is used. |
| """ |
| headers = [] |
| functions = [] |
| for routine in routines: |
| prototype = self.generator.get_prototype(routine) |
|
|
| |
| headers.append(self.pyx_header.format(header_file=prefix, |
| prototype=prototype)) |
|
|
| |
| py_rets, py_args, py_loc, py_inf = self._partition_args(routine.arguments) |
|
|
| |
| name = routine.name |
| arg_string = ", ".join(self._prototype_arg(arg) for arg in py_args) |
|
|
| |
| local_decs = [] |
| for arg, val in py_inf.items(): |
| proto = self._prototype_arg(arg) |
| mat, ind = [self._string_var(v) for v in val] |
| local_decs.append(" cdef {} = {}.shape[{}]".format(proto, mat, ind)) |
| local_decs.extend([" cdef {}".format(self._declare_arg(a)) for a in py_loc]) |
| declarations = "\n".join(local_decs) |
| if declarations: |
| declarations = declarations + "\n" |
|
|
| |
| args_c = ", ".join([self._call_arg(a) for a in routine.arguments]) |
| rets = ", ".join([self._string_var(r.name) for r in py_rets]) |
| if routine.results: |
| body = ' return %s(%s)' % (routine.name, args_c) |
| if rets: |
| body = body + ', ' + rets |
| else: |
| body = ' %s(%s)\n' % (routine.name, args_c) |
| body = body + ' return ' + rets |
|
|
| functions.append(self.pyx_func.format(name=name, arg_string=arg_string, |
| declarations=declarations, body=body)) |
|
|
| |
| if self._need_numpy: |
| |
| f.write(self.pyx_imports) |
| f.write('\n'.join(headers)) |
| f.write('\n'.join(functions)) |
|
|
| def _partition_args(self, args): |
| """Group function arguments into categories.""" |
| py_args = [] |
| py_returns = [] |
| py_locals = [] |
| py_inferred = {} |
| for arg in args: |
| if isinstance(arg, OutputArgument): |
| py_returns.append(arg) |
| py_locals.append(arg) |
| elif isinstance(arg, InOutArgument): |
| py_returns.append(arg) |
| py_args.append(arg) |
| else: |
| py_args.append(arg) |
| |
| |
| if isinstance(arg, (InputArgument, InOutArgument)) and arg.dimensions: |
| dims = [d[1] + 1 for d in arg.dimensions] |
| sym_dims = [(i, d) for (i, d) in enumerate(dims) if |
| isinstance(d, Symbol)] |
| for (i, d) in sym_dims: |
| py_inferred[d] = (arg.name, i) |
| for arg in args: |
| if arg.name in py_inferred: |
| py_inferred[arg] = py_inferred.pop(arg.name) |
| |
| py_args = [a for a in py_args if a not in py_inferred] |
| return py_returns, py_args, py_locals, py_inferred |
|
|
| def _prototype_arg(self, arg): |
| mat_dec = "np.ndarray[{mtype}, ndim={ndim}] {name}" |
| np_types = {'double': 'np.double_t', |
| 'int': 'np.int_t'} |
| t = arg.get_datatype('c') |
| if arg.dimensions: |
| self._need_numpy = True |
| ndim = len(arg.dimensions) |
| mtype = np_types[t] |
| return mat_dec.format(mtype=mtype, ndim=ndim, name=self._string_var(arg.name)) |
| else: |
| return "%s %s" % (t, self._string_var(arg.name)) |
|
|
| def _declare_arg(self, arg): |
| proto = self._prototype_arg(arg) |
| if arg.dimensions: |
| shape = '(' + ','.join(self._string_var(i[1] + 1) for i in arg.dimensions) + ')' |
| return proto + " = np.empty({shape})".format(shape=shape) |
| else: |
| return proto + " = 0" |
|
|
| def _call_arg(self, arg): |
| if arg.dimensions: |
| t = arg.get_datatype('c') |
| return "<{}*> {}.data".format(t, self._string_var(arg.name)) |
| elif isinstance(arg, ResultBase): |
| return "&{}".format(self._string_var(arg.name)) |
| else: |
| return self._string_var(arg.name) |
|
|
| def _string_var(self, var): |
| printer = self.generator.printer.doprint |
| return printer(var) |
|
|
|
|
| class F2PyCodeWrapper(CodeWrapper): |
| """Wrapper that uses f2py""" |
|
|
| def __init__(self, *args, **kwargs): |
|
|
| ext_keys = ['include_dirs', 'library_dirs', 'libraries', |
| 'extra_compile_args', 'extra_link_args'] |
| msg = ('The compilation option kwarg {} is not supported with the f2py ' |
| 'backend.') |
|
|
| for k in ext_keys: |
| if k in kwargs.keys(): |
| warn(msg.format(k)) |
| kwargs.pop(k, None) |
|
|
| super().__init__(*args, **kwargs) |
|
|
| @property |
| def command(self): |
| filename = self.filename + '.' + self.generator.code_extension |
| args = ['-c', '-m', self.module_name, filename] |
| command = [sys.executable, "-c", "import numpy.f2py as f2py2e;f2py2e.main()"]+args |
| return command |
|
|
| def _prepare_files(self, routine): |
| pass |
|
|
| @classmethod |
| def _get_wrapped_function(cls, mod, name): |
| return getattr(mod, name) |
|
|
|
|
| |
| |
| |
| _lang_lookup = {'CYTHON': ('C99', 'C89', 'C'), |
| 'F2PY': ('F95',), |
| 'NUMPY': ('C99', 'C89', 'C'), |
| 'DUMMY': ('F95',)} |
|
|
|
|
| def _infer_language(backend): |
| """For a given backend, return the top choice of language""" |
| langs = _lang_lookup.get(backend.upper(), False) |
| if not langs: |
| raise ValueError("Unrecognized backend: " + backend) |
| return langs[0] |
|
|
|
|
| def _validate_backend_language(backend, language): |
| """Throws error if backend and language are incompatible""" |
| langs = _lang_lookup.get(backend.upper(), False) |
| if not langs: |
| raise ValueError("Unrecognized backend: " + backend) |
| if language.upper() not in langs: |
| raise ValueError(("Backend {} and language {} are " |
| "incompatible").format(backend, language)) |
|
|
|
|
| @cacheit |
| @doctest_depends_on(exe=('f2py', 'gfortran'), modules=('numpy',)) |
| def autowrap(expr, language=None, backend='f2py', tempdir=None, args=None, |
| flags=None, verbose=False, helpers=None, code_gen=None, **kwargs): |
| """Generates Python callable binaries based on the math expression. |
| |
| Parameters |
| ========== |
| |
| expr |
| The SymPy expression that should be wrapped as a binary routine. |
| language : string, optional |
| If supplied, (options: 'C' or 'F95'), specifies the language of the |
| generated code. If ``None`` [default], the language is inferred based |
| upon the specified backend. |
| backend : string, optional |
| Backend used to wrap the generated code. Either 'f2py' [default], |
| or 'cython'. |
| tempdir : string, optional |
| Path to directory for temporary files. If this argument is supplied, |
| the generated code and the wrapper input files are left intact in the |
| specified path. |
| args : iterable, optional |
| An ordered iterable of symbols. Specifies the argument sequence for the |
| function. |
| flags : iterable, optional |
| Additional option flags that will be passed to the backend. |
| verbose : bool, optional |
| If True, autowrap will not mute the command line backends. This can be |
| helpful for debugging. |
| helpers : 3-tuple or iterable of 3-tuples, optional |
| Used to define auxiliary functions needed for the main expression. |
| Each tuple should be of the form (name, expr, args) where: |
| |
| - name : str, the function name |
| - expr : sympy expression, the function |
| - args : iterable, the function arguments (can be any iterable of symbols) |
| |
| code_gen : CodeGen instance |
| An instance of a CodeGen subclass. Overrides ``language``. |
| include_dirs : [string] |
| A list of directories to search for C/C++ header files (in Unix form |
| for portability). |
| library_dirs : [string] |
| A list of directories to search for C/C++ libraries at link time. |
| libraries : [string] |
| A list of library names (not filenames or paths) to link against. |
| extra_compile_args : [string] |
| Any extra platform- and compiler-specific information to use when |
| compiling the source files in 'sources'. For platforms and compilers |
| where "command line" makes sense, this is typically a list of |
| command-line arguments, but for other platforms it could be anything. |
| extra_link_args : [string] |
| Any extra platform- and compiler-specific information to use when |
| linking object files together to create the extension (or to create a |
| new static Python interpreter). Similar interpretation as for |
| 'extra_compile_args'. |
| |
| Examples |
| ======== |
| |
| Basic usage: |
| |
| >>> from sympy.abc import x, y, z |
| >>> from sympy.utilities.autowrap import autowrap |
| >>> expr = ((x - y + z)**(13)).expand() |
| >>> binary_func = autowrap(expr) |
| >>> binary_func(1, 4, 2) |
| -1.0 |
| |
| Using helper functions: |
| |
| >>> from sympy.abc import x, t |
| >>> from sympy import Function |
| >>> helper_func = Function('helper_func') # Define symbolic function |
| >>> expr = 3*x + helper_func(t) # Main expression using helper function |
| >>> # Define helper_func(x) = 4*x using f2py backend |
| >>> binary_func = autowrap(expr, args=[x, t], |
| ... helpers=('helper_func', 4*x, [x])) |
| >>> binary_func(2, 5) # 3*2 + helper_func(5) = 6 + 20 |
| 26.0 |
| >>> # Same example using cython backend |
| >>> binary_func = autowrap(expr, args=[x, t], backend='cython', |
| ... helpers=[('helper_func', 4*x, [x])]) |
| >>> binary_func(2, 5) # 3*2 + helper_func(5) = 6 + 20 |
| 26.0 |
| |
| Type handling example: |
| |
| >>> import numpy as np |
| >>> expr = x + y |
| >>> f_cython = autowrap(expr, backend='cython') |
| >>> f_cython(1, 2) # doctest: +ELLIPSIS |
| Traceback (most recent call last): |
| ... |
| TypeError: Argument '_x' has incorrect type (expected numpy.ndarray, got int) |
| >>> f_cython(np.array([1.0]), np.array([2.0])) |
| array([ 3.]) |
| |
| """ |
| if language: |
| if not isinstance(language, type): |
| _validate_backend_language(backend, language) |
| else: |
| language = _infer_language(backend) |
|
|
| |
| |
| if iterable(helpers) and len(helpers) != 0 and iterable(helpers[0]): |
| helpers = helpers if helpers else () |
| else: |
| helpers = [helpers] if helpers else () |
| args = list(args) if iterable(args, exclude=set) else args |
|
|
| if code_gen is None: |
| code_gen = get_code_generator(language, "autowrap") |
|
|
| CodeWrapperClass = { |
| 'F2PY': F2PyCodeWrapper, |
| 'CYTHON': CythonCodeWrapper, |
| 'DUMMY': DummyWrapper |
| }[backend.upper()] |
| code_wrapper = CodeWrapperClass(code_gen, tempdir, flags if flags else (), |
| verbose, **kwargs) |
|
|
| helps = [] |
| for name_h, expr_h, args_h in helpers: |
| helps.append(code_gen.routine(name_h, expr_h, args_h)) |
|
|
| for name_h, expr_h, args_h in helpers: |
| if expr.has(expr_h): |
| name_h = binary_function(name_h, expr_h, backend='dummy') |
| expr = expr.subs(expr_h, name_h(*args_h)) |
| try: |
| routine = code_gen.routine('autofunc', expr, args) |
| except CodeGenArgumentListError as e: |
| |
| |
| |
| new_args = [] |
| for missing in e.missing_args: |
| if not isinstance(missing, OutputArgument): |
| raise |
| new_args.append(missing.name) |
| routine = code_gen.routine('autofunc', expr, args + new_args) |
|
|
| return code_wrapper.wrap_code(routine, helpers=helps) |
|
|
|
|
| @doctest_depends_on(exe=('f2py', 'gfortran'), modules=('numpy',)) |
| def binary_function(symfunc, expr, **kwargs): |
| """Returns a SymPy function with expr as binary implementation |
| |
| This is a convenience function that automates the steps needed to |
| autowrap the SymPy expression and attaching it to a Function object |
| with implemented_function(). |
| |
| Parameters |
| ========== |
| |
| symfunc : SymPy Function |
| The function to bind the callable to. |
| expr : SymPy Expression |
| The expression used to generate the function. |
| kwargs : dict |
| Any kwargs accepted by autowrap. |
| |
| Examples |
| ======== |
| |
| >>> from sympy.abc import x, y |
| >>> from sympy.utilities.autowrap import binary_function |
| >>> expr = ((x - y)**(25)).expand() |
| >>> f = binary_function('f', expr) |
| >>> type(f) |
| <class 'sympy.core.function.UndefinedFunction'> |
| >>> 2*f(x, y) |
| 2*f(x, y) |
| >>> f(x, y).evalf(2, subs={x: 1, y: 2}) |
| -1.0 |
| |
| """ |
| binary = autowrap(expr, **kwargs) |
| return implemented_function(symfunc, binary) |
|
|
| |
| |
| |
|
|
| _ufunc_top = Template("""\ |
| #include "Python.h" |
| #include "math.h" |
| #include "numpy/ndarraytypes.h" |
| #include "numpy/ufuncobject.h" |
| #include "numpy/halffloat.h" |
| #include ${include_file} |
| |
| static PyMethodDef ${module}Methods[] = { |
| {NULL, NULL, 0, NULL} |
| };""") |
|
|
| _ufunc_outcalls = Template("*((double *)out${outnum}) = ${funcname}(${call_args});") |
|
|
| _ufunc_body = Template("""\ |
| #ifdef NPY_1_19_API_VERSION |
| static void ${funcname}_ufunc(char **args, const npy_intp *dimensions, const npy_intp* steps, void* data) |
| #else |
| static void ${funcname}_ufunc(char **args, npy_intp *dimensions, npy_intp* steps, void* data) |
| #endif |
| { |
| npy_intp i; |
| npy_intp n = dimensions[0]; |
| ${declare_args} |
| ${declare_steps} |
| for (i = 0; i < n; i++) { |
| ${outcalls} |
| ${step_increments} |
| } |
| } |
| PyUFuncGenericFunction ${funcname}_funcs[1] = {&${funcname}_ufunc}; |
| static char ${funcname}_types[${n_types}] = ${types} |
| static void *${funcname}_data[1] = {NULL};""") |
|
|
| _ufunc_bottom = Template("""\ |
| #if PY_VERSION_HEX >= 0x03000000 |
| static struct PyModuleDef moduledef = { |
| PyModuleDef_HEAD_INIT, |
| "${module}", |
| NULL, |
| -1, |
| ${module}Methods, |
| NULL, |
| NULL, |
| NULL, |
| NULL |
| }; |
| |
| PyMODINIT_FUNC PyInit_${module}(void) |
| { |
| PyObject *m, *d; |
| ${function_creation} |
| m = PyModule_Create(&moduledef); |
| if (!m) { |
| return NULL; |
| } |
| import_array(); |
| import_umath(); |
| d = PyModule_GetDict(m); |
| ${ufunc_init} |
| return m; |
| } |
| #else |
| PyMODINIT_FUNC init${module}(void) |
| { |
| PyObject *m, *d; |
| ${function_creation} |
| m = Py_InitModule("${module}", ${module}Methods); |
| if (m == NULL) { |
| return; |
| } |
| import_array(); |
| import_umath(); |
| d = PyModule_GetDict(m); |
| ${ufunc_init} |
| } |
| #endif\ |
| """) |
|
|
| _ufunc_init_form = Template("""\ |
| ufunc${ind} = PyUFunc_FromFuncAndData(${funcname}_funcs, ${funcname}_data, ${funcname}_types, 1, ${n_in}, ${n_out}, |
| PyUFunc_None, "${module}", ${docstring}, 0); |
| PyDict_SetItemString(d, "${funcname}", ufunc${ind}); |
| Py_DECREF(ufunc${ind});""") |
|
|
| _ufunc_setup = Template("""\ |
| from setuptools.extension import Extension |
| from setuptools import setup |
| |
| from numpy import get_include |
| |
| if __name__ == "__main__": |
| setup(ext_modules=[ |
| Extension('${module}', |
| sources=['${module}.c', '${filename}.c'], |
| include_dirs=[get_include()])]) |
| """) |
|
|
|
|
| class UfuncifyCodeWrapper(CodeWrapper): |
| """Wrapper for Ufuncify""" |
|
|
| def __init__(self, *args, **kwargs): |
|
|
| ext_keys = ['include_dirs', 'library_dirs', 'libraries', |
| 'extra_compile_args', 'extra_link_args'] |
| msg = ('The compilation option kwarg {} is not supported with the numpy' |
| ' backend.') |
|
|
| for k in ext_keys: |
| if k in kwargs.keys(): |
| warn(msg.format(k)) |
| kwargs.pop(k, None) |
|
|
| super().__init__(*args, **kwargs) |
|
|
| @property |
| def command(self): |
| command = [sys.executable, "setup.py", "build_ext", "--inplace"] |
| return command |
|
|
| def wrap_code(self, routines, helpers=None): |
| |
| |
| |
| |
| helpers = helpers if helpers is not None else [] |
| |
| funcname = 'wrapped_' + str(id(routines) + id(helpers)) |
|
|
| workdir = self.filepath or tempfile.mkdtemp("_sympy_compile") |
| if not os.access(workdir, os.F_OK): |
| os.mkdir(workdir) |
| oldwork = os.getcwd() |
| os.chdir(workdir) |
| try: |
| sys.path.append(workdir) |
| self._generate_code(routines, helpers) |
| self._prepare_files(routines, funcname) |
| self._process_files(routines) |
| mod = __import__(self.module_name) |
| finally: |
| sys.path.remove(workdir) |
| CodeWrapper._module_counter += 1 |
| os.chdir(oldwork) |
| if not self.filepath: |
| try: |
| shutil.rmtree(workdir) |
| except OSError: |
| |
| pass |
|
|
| return self._get_wrapped_function(mod, funcname) |
|
|
| def _generate_code(self, main_routines, helper_routines): |
| all_routines = main_routines + helper_routines |
| self.generator.write( |
| all_routines, self.filename, True, self.include_header, |
| self.include_empty) |
|
|
| def _prepare_files(self, routines, funcname): |
|
|
| |
| codefilename = self.module_name + '.c' |
| with open(codefilename, 'w') as f: |
| self.dump_c(routines, f, self.filename, funcname=funcname) |
|
|
| |
| with open('setup.py', 'w') as f: |
| self.dump_setup(f) |
|
|
| @classmethod |
| def _get_wrapped_function(cls, mod, name): |
| return getattr(mod, name) |
|
|
| def dump_setup(self, f): |
| setup = _ufunc_setup.substitute(module=self.module_name, |
| filename=self.filename) |
| f.write(setup) |
|
|
| def dump_c(self, routines, f, prefix, funcname=None): |
| """Write a C file with Python wrappers |
| |
| This file contains all the definitions of the routines in c code. |
| |
| Arguments |
| --------- |
| routines |
| List of Routine instances |
| f |
| File-like object to write the file to |
| prefix |
| The filename prefix, used to name the imported module. |
| funcname |
| Name of the main function to be returned. |
| """ |
| if funcname is None: |
| if len(routines) == 1: |
| funcname = routines[0].name |
| else: |
| msg = 'funcname must be specified for multiple output routines' |
| raise ValueError(msg) |
| functions = [] |
| function_creation = [] |
| ufunc_init = [] |
| module = self.module_name |
| include_file = "\"{}.h\"".format(prefix) |
| top = _ufunc_top.substitute(include_file=include_file, module=module) |
|
|
| name = funcname |
|
|
| |
| |
| r_index = 0 |
| py_in, _ = self._partition_args(routines[0].arguments) |
| n_in = len(py_in) |
| n_out = len(routines) |
|
|
| |
| form = "char *{0}{1} = args[{2}];" |
| arg_decs = [form.format('in', i, i) for i in range(n_in)] |
| arg_decs.extend([form.format('out', i, i+n_in) for i in range(n_out)]) |
| declare_args = '\n '.join(arg_decs) |
|
|
| |
| form = "npy_intp {0}{1}_step = steps[{2}];" |
| step_decs = [form.format('in', i, i) for i in range(n_in)] |
| step_decs.extend([form.format('out', i, i+n_in) for i in range(n_out)]) |
| declare_steps = '\n '.join(step_decs) |
|
|
| |
| form = "*(double *)in{0}" |
| call_args = ', '.join([form.format(a) for a in range(n_in)]) |
|
|
| |
| form = "{0}{1} += {0}{1}_step;" |
| step_incs = [form.format('in', i) for i in range(n_in)] |
| step_incs.extend([form.format('out', i, i) for i in range(n_out)]) |
| step_increments = '\n '.join(step_incs) |
|
|
| |
| n_types = n_in + n_out |
| types = "{" + ', '.join(["NPY_DOUBLE"]*n_types) + "};" |
|
|
| |
| docstring = '"Created in SymPy with Ufuncify"' |
|
|
| |
| function_creation.append("PyObject *ufunc{};".format(r_index)) |
|
|
| |
| init_form = _ufunc_init_form.substitute(module=module, |
| funcname=name, |
| docstring=docstring, |
| n_in=n_in, n_out=n_out, |
| ind=r_index) |
| ufunc_init.append(init_form) |
|
|
| outcalls = [_ufunc_outcalls.substitute( |
| outnum=i, call_args=call_args, funcname=routines[i].name) for i in |
| range(n_out)] |
|
|
| body = _ufunc_body.substitute(module=module, funcname=name, |
| declare_args=declare_args, |
| declare_steps=declare_steps, |
| call_args=call_args, |
| step_increments=step_increments, |
| n_types=n_types, types=types, |
| outcalls='\n '.join(outcalls)) |
| functions.append(body) |
|
|
| body = '\n\n'.join(functions) |
| ufunc_init = '\n '.join(ufunc_init) |
| function_creation = '\n '.join(function_creation) |
| bottom = _ufunc_bottom.substitute(module=module, |
| ufunc_init=ufunc_init, |
| function_creation=function_creation) |
| text = [top, body, bottom] |
| f.write('\n\n'.join(text)) |
|
|
| def _partition_args(self, args): |
| """Group function arguments into categories.""" |
| py_in = [] |
| py_out = [] |
| for arg in args: |
| if isinstance(arg, OutputArgument): |
| py_out.append(arg) |
| elif isinstance(arg, InOutArgument): |
| raise ValueError("Ufuncify doesn't support InOutArguments") |
| else: |
| py_in.append(arg) |
| return py_in, py_out |
|
|
|
|
| @cacheit |
| @doctest_depends_on(exe=('f2py', 'gfortran', 'gcc'), modules=('numpy',)) |
| def ufuncify(args, expr, language=None, backend='numpy', tempdir=None, |
| flags=None, verbose=False, helpers=None, **kwargs): |
| """Generates a binary function that supports broadcasting on numpy arrays. |
| |
| Parameters |
| ========== |
| |
| args : iterable |
| Either a Symbol or an iterable of symbols. Specifies the argument |
| sequence for the function. |
| expr |
| A SymPy expression that defines the element wise operation. |
| language : string, optional |
| If supplied, (options: 'C' or 'F95'), specifies the language of the |
| generated code. If ``None`` [default], the language is inferred based |
| upon the specified backend. |
| backend : string, optional |
| Backend used to wrap the generated code. Either 'numpy' [default], |
| 'cython', or 'f2py'. |
| tempdir : string, optional |
| Path to directory for temporary files. If this argument is supplied, |
| the generated code and the wrapper input files are left intact in |
| the specified path. |
| flags : iterable, optional |
| Additional option flags that will be passed to the backend. |
| verbose : bool, optional |
| If True, autowrap will not mute the command line backends. This can |
| be helpful for debugging. |
| helpers : 3-tuple or iterable of 3-tuples, optional |
| Used to define auxiliary functions needed for the main expression. |
| Each tuple should be of the form (name, expr, args) where: |
| |
| - name : str, the function name |
| - expr : sympy expression, the function |
| - args : iterable, the function arguments (can be any iterable of symbols) |
| |
| kwargs : dict |
| These kwargs will be passed to autowrap if the `f2py` or `cython` |
| backend is used and ignored if the `numpy` backend is used. |
| |
| Notes |
| ===== |
| |
| The default backend ('numpy') will create actual instances of |
| ``numpy.ufunc``. These support ndimensional broadcasting, and implicit type |
| conversion. Use of the other backends will result in a "ufunc-like" |
| function, which requires equal length 1-dimensional arrays for all |
| arguments, and will not perform any type conversions. |
| |
| References |
| ========== |
| |
| .. [1] https://numpy.org/doc/stable/reference/ufuncs.html |
| |
| Examples |
| ======== |
| |
| Basic usage: |
| |
| >>> from sympy.utilities.autowrap import ufuncify |
| >>> from sympy.abc import x, y |
| >>> import numpy as np |
| >>> f = ufuncify((x, y), y + x**2) |
| >>> type(f) |
| <class 'numpy.ufunc'> |
| >>> f([1, 2, 3], 2) |
| array([ 3., 6., 11.]) |
| >>> f(np.arange(5), 3) |
| array([ 3., 4., 7., 12., 19.]) |
| |
| Using helper functions: |
| |
| >>> from sympy import Function |
| >>> helper_func = Function('helper_func') # Define symbolic function |
| >>> expr = x**2 + y*helper_func(x) # Main expression using helper function |
| >>> # Define helper_func(x) = x**3 |
| >>> f = ufuncify((x, y), expr, helpers=[('helper_func', x**3, [x])]) |
| >>> f([1, 2], [3, 4]) |
| array([ 4., 36.]) |
| |
| Type handling with different backends: |
| |
| For the 'f2py' and 'cython' backends, inputs are required to be equal length |
| 1-dimensional arrays. The 'f2py' backend will perform type conversion, but |
| the Cython backend will error if the inputs are not of the expected type. |
| |
| >>> f_fortran = ufuncify((x, y), y + x**2, backend='f2py') |
| >>> f_fortran(1, 2) |
| array([ 3.]) |
| >>> f_fortran(np.array([1, 2, 3]), np.array([1.0, 2.0, 3.0])) |
| array([ 2., 6., 12.]) |
| >>> f_cython = ufuncify((x, y), y + x**2, backend='Cython') |
| >>> f_cython(1, 2) # doctest: +ELLIPSIS |
| Traceback (most recent call last): |
| ... |
| TypeError: Argument '_x' has incorrect type (expected numpy.ndarray, got int) |
| >>> f_cython(np.array([1.0]), np.array([2.0])) |
| array([ 3.]) |
| |
| """ |
|
|
| if isinstance(args, Symbol): |
| args = (args,) |
| else: |
| args = tuple(args) |
|
|
| if language: |
| _validate_backend_language(backend, language) |
| else: |
| language = _infer_language(backend) |
|
|
| helpers = helpers if helpers else () |
| flags = flags if flags else () |
|
|
| if backend.upper() == 'NUMPY': |
| |
| |
| |
| maxargs = 32 |
| helps = [] |
| for name, expr, args in helpers: |
| helps.append(make_routine(name, expr, args)) |
| code_wrapper = UfuncifyCodeWrapper(C99CodeGen("ufuncify"), tempdir, |
| flags, verbose) |
| if not isinstance(expr, (list, tuple)): |
| expr = [expr] |
| if len(expr) == 0: |
| raise ValueError('Expression iterable has zero length') |
| if len(expr) + len(args) > maxargs: |
| msg = ('Cannot create ufunc with more than {0} total arguments: ' |
| 'got {1} in, {2} out') |
| raise ValueError(msg.format(maxargs, len(args), len(expr))) |
| routines = [make_routine('autofunc{}'.format(idx), exprx, args) for |
| idx, exprx in enumerate(expr)] |
| return code_wrapper.wrap_code(routines, helpers=helps) |
| else: |
| |
| |
| y = IndexedBase(Dummy('y')) |
| m = Dummy('m', integer=True) |
| i = Idx(Dummy('i', integer=True), m) |
| f_dummy = Dummy('f') |
| f = implemented_function('%s_%d' % (f_dummy.name, f_dummy.dummy_index), Lambda(args, expr)) |
| |
| indexed_args = [IndexedBase(Dummy(str(a))) for a in args] |
| |
| args = [y] + indexed_args + [m] |
| args_with_indices = [a[i] for a in indexed_args] |
| return autowrap(Eq(y[i], f(*args_with_indices)), language, backend, |
| tempdir, args, flags, verbose, helpers, **kwargs) |
|
|