diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea10c572e03cae4daeb1087517576b0fbedd0139..af88cbb30257a23b209e67f8fbd96bef6ab5c8d5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,14 @@ Python 2.7: except: - tags +Python 2.7 Conda: + script: + - CONDA_ENVIRONMENT=.test-py2.yml + - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project-within-miniconda.sh + - ". ./build-and-test-py-project-within-miniconda.sh" + except: + - tags + Python 3.5: script: - py_version=3.5 @@ -22,6 +30,14 @@ Python 3.5: except: - tags +Python 3.5 Conda: + script: + - CONDA_ENVIRONMENT=.test-py3.yml + - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project-within-miniconda.sh + - ". ./build-and-test-py-project-within-miniconda.sh" + except: + - tags + Python 2.6: script: - py_version=2.6 diff --git a/.test-py2.yml b/.test-py2.yml new file mode 100644 index 0000000000000000000000000000000000000000..f21c307c51bdfdc92e0192cd5f60a887a1d3454d --- /dev/null +++ b/.test-py2.yml @@ -0,0 +1,12 @@ +name: py2 +channels: + - symengine/label/dev + - conda-forge + - defaults +dependencies: + - pexpect + - pytools + - conda-forge::numpy + - conda-forge::sympy + - python=2.7 + - symengine/label/dev::python-symengine=0.2.0.53.g83912b7=py27_1 diff --git a/.test-py3.yml b/.test-py3.yml new file mode 100644 index 0000000000000000000000000000000000000000..d80032887699f58f8709e50a40110298c9ccd5c6 --- /dev/null +++ b/.test-py3.yml @@ -0,0 +1,11 @@ +name: py3 +channels: + - symengine/label/dev + - conda-forge + - defaults +dependencies: + - pexpect + - conda-forge::numpy + - conda-forge::sympy + - python=3.5 + - symengine/label/dev::python-symengine=0.2.0.53.g83912b7=py35_1 diff --git a/pymbolic/interop/common.py b/pymbolic/interop/common.py new file mode 100644 index 0000000000000000000000000000000000000000..256dda7c34565002a99ce3d3235a07e2a78a9b52 --- /dev/null +++ b/pymbolic/interop/common.py @@ -0,0 +1,164 @@ +from __future__ import division, absolute_import + +__copyright__ = "Copyright (C) 2009-2013 Andreas Kloeckner" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import pymbolic.primitives as prim +from pymbolic.mapper.evaluator import EvaluationMapper + + +class SympyLikeMapperBase(object): + + def __call__(self, expr, *args, **kwargs): + return self.rec(expr, *args, **kwargs) + + def rec(self, expr, *args, **kwargs): + mro = list(type(expr).__mro__) + dispatch_class = kwargs.pop("dispatch_class", type(self)) + + while mro: + method_name = "map_"+mro.pop(0).__name__ + + try: + method = getattr(dispatch_class, method_name) + except AttributeError: + pass + else: + return method(self, expr, *args, **kwargs) + + return self.not_supported(expr) + + def not_supported(self, expr): + print(expr, expr.__class__.__mro__) + raise NotImplementedError( + "%s does not know how to map type '%s'" + % (type(self).__name__, + type(expr).__name__)) + + +# {{{ sympy like -> pymbolic + +class SympyLikeToPymbolicMapper(SympyLikeMapperBase): + + # {{{ utils + + def to_float(self, expr): + return float(expr) + + def function_name(self, expr): + # Given a symbolic function application f(x), return the name of f as a + # string + raise NotImplementedError() + + # }}} + + def map_Symbol(self, expr): # noqa + return prim.Variable(str(expr.name)) + + def map_Rational(self, expr): # noqa + p, q = expr.p, expr.q + + num = self.rec(p) + denom = self.rec(q) + + if prim.is_zero(denom-1): + return num + return prim.Quotient(num, denom) + + def map_Integer(self, expr): # noqa + return int(expr) + + def map_Add(self, expr): # noqa + return prim.Sum(tuple(self.rec(arg) for arg in expr.args)) + + def map_Mul(self, expr): # noqa + return prim.Product(tuple(self.rec(arg) for arg in expr.args)) + + def map_Pow(self, expr): # noqa + base, exp = expr.args + return prim.Power(self.rec(base), self.rec(exp)) + + def map_Subs(self, expr): # noqa + return prim.Substitution(self.rec(expr.expr), + tuple(v.name for v in expr.variables), + tuple(self.rec(v) for v in expr.point), + ) + + def map_Derivative(self, expr): # noqa + return prim.Derivative(self.rec(expr.expr), + tuple(v.name for v in expr.variables)) + + def map_CSE(self, expr): # noqa + return prim.CommonSubexpression( + self.rec(expr.args[0]), expr.prefix) + + def not_supported(self, expr): + if isinstance(expr, int): + return expr + elif getattr(expr, "is_Function", False): + return prim.Variable(self.function_name(expr))( + *tuple(self.rec(arg) for arg in expr.args)) + else: + return SympyLikeMapperBase.not_supported(self, expr) + +# }}} + + +# {{{ pymbolic -> sympy + +class PymbolicToSympyLikeMapper(EvaluationMapper): + + def map_variable(self, expr): + return self.sym.Symbol(expr.name) + + def map_constant(self, expr): + return self.sym.sympify(expr) + + def map_call(self, expr): + if isinstance(expr.function, prim.Variable): + func_name = expr.function.name + try: + func = getattr(self.sym.functions, func_name) + except AttributeError: + func = self.sym.Function(func_name) + return func(*[self.rec(par) for par in expr.parameters]) + else: + self.raise_conversion_error(expr) + + def map_subscript(self, expr): + if isinstance(expr.aggregate, prim.Variable) and isinstance(expr.index, int): + return self.sym.Symbol("%s_%d" % (expr.aggregate.name, expr.index)) + else: + self.raise_conversion_error(expr) + + def map_substitution(self, expr): + return self.sym.Subs(self.rec(expr.child), + tuple(self.sym.Symbol(v) for v in expr.variables), + tuple(self.rec(v) for v in expr.values), + ) + + def map_derivative(self, expr): + raise NotImplementedError() + +# }}} + +# vim: fdm=marker diff --git a/pymbolic/interop/symengine.py b/pymbolic/interop/symengine.py new file mode 100644 index 0000000000000000000000000000000000000000..ab68e7fde1596e390ad59e55b40827589d9c6f80 --- /dev/null +++ b/pymbolic/interop/symengine.py @@ -0,0 +1,99 @@ +from __future__ import division, absolute_import + +__copyright__ = """ +Copyright (C) 2017 Matt Wala +""" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from pymbolic.interop.common import ( + SympyLikeToPymbolicMapper, PymbolicToSympyLikeMapper) + +import pymbolic.primitives as prim +import symengine.sympy_compat + + +__doc__ = """ +.. class:: SymEngineToPymbolicMapper + + .. method:: __call__(expr) + +.. class:: PymbolicToSymEngineMapper + + .. method:: __call__(expr) +""" + + +# {{{ symengine -> pymbolic + +class SymEngineToPymbolicMapper(SympyLikeToPymbolicMapper): + + def map_Pow(self, expr): # noqa + # SymEngine likes to use as e**a to express exp(a); we undo that here. + base, exp = expr.args + if base == symengine.E: + return prim.Variable("exp")(self.rec(exp)) + else: + return prim.Power(self.rec(base), self.rec(exp)) + + def map_Constant(self, expr): # noqa + return self.rec(expr.n()) + + map_Complex = map_Constant + + def map_ComplexDouble(self, expr): # noqa + r = self.rec(expr.real_part()) + i = self.rec(expr.imaginary_part()) + if prim.is_zero(i): + return r + else: + return r + 1j * i + + map_RealDouble = SympyLikeToPymbolicMapper.to_float + + def function_name(self, expr): + try: + # For FunctionSymbol instances + return expr.get_name() + except AttributeError: + # For builtin functions + return type(expr).__name__ + +# }}} + + +# {{{ pymbolic -> symengine + +class PymbolicToSymEngineMapper(PymbolicToSympyLikeMapper): + + sym = symengine.sympy_compat + + def raise_conversion_error(self, expr): + raise RuntimeError( + "do not know how to translate '%s' to symengine" % expr) + + def map_derivative(self, expr): + return self.sym.Derivative(self.rec(expr.child), + [self.sym.Symbol(v) for v in expr.variables]) + +# }}} + +# vim: fdm=marker diff --git a/pymbolic/interop/sympy.py b/pymbolic/interop/sympy.py index 034fb9c5aea314998ff33fb9e17c219c4b11591e..7e1f4399a6ce0bf21c5931f084e1b1aef6d7c3fc 100644 --- a/pymbolic/interop/sympy.py +++ b/pymbolic/interop/sympy.py @@ -1,6 +1,9 @@ from __future__ import division, absolute_import -__copyright__ = "Copyright (C) 2009-2013 Andreas Kloeckner" +__copyright__ = """ +Copyright (C) 2017 Matt Wala +Copyright (C) 2009-2013 Andreas Kloeckner +""" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,9 +25,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import pymbolic.primitives as prim -from pymbolic.mapper.evaluator import EvaluationMapper -import sympy as sp +from pymbolic.interop.common import ( + SympyLikeToPymbolicMapper, PymbolicToSympyLikeMapper) + +import sympy + __doc__ = """ .. class:: SympyToPymbolicMapper @@ -37,145 +42,53 @@ __doc__ = """ """ -class SympyMapper(object): - def __call__(self, expr, *args, **kwargs): - return self.rec(expr, *args, **kwargs) - - def rec(self, expr, *args, **kwargs): - mro = list(type(expr).__mro__) - dispatch_class = kwargs.pop("dispatch_class", type(self)) - - while mro: - method_name = "map_"+mro.pop(0).__name__ +# {{{ symengine -> pymbolic - try: - method = getattr(dispatch_class, method_name) - except AttributeError: - pass - else: - return method(self, expr, *args, **kwargs) - - return self.not_supported(expr) - - def not_supported(self, expr): - raise NotImplementedError( - "%s does not know how to map type '%s'" - % (type(self).__name__, - type(expr).__name__)) - - -class CSE(sp.Function): - """A function to translate to a Pymbolic CSE.""" - - nargs = 1 - - -def make_cse(arg, prefix=None): - result = CSE(arg) - result.prefix = prefix - return result +class SympyToPymbolicMapper(SympyLikeToPymbolicMapper): + def map_ImaginaryUnit(self, expr): # noqa + return 1j -# {{{ sympy -> pymbolic + map_Float = SympyLikeToPymbolicMapper.to_float -class SympyToPymbolicMapper(SympyMapper): - def map_Symbol(self, expr): # noqa - return prim.Variable(expr.name) + map_NumberSymbol = SympyLikeToPymbolicMapper.to_float - def map_ImaginaryUnit(self, expr): # noqa - return 1j + def function_name(self, expr): + return type(expr).__name__ # only called for Py2 def map_long(self, expr): return long(expr) # noqa - def map_Float(self, expr): # noqa - return float(expr) - - def map_Pi(self, expr): # noqa - return float(expr) - - def map_Add(self, expr): # noqa - return prim.Sum(tuple(self.rec(arg) for arg in expr.args)) - - def map_Mul(self, expr): # noqa - return prim.Product(tuple(self.rec(arg) for arg in expr.args)) - - def map_Rational(self, expr): # noqa - num = self.rec(expr.p) - denom = self.rec(expr.q) +# }}} - if prim.is_zero(denom-1): - return num - return prim.Quotient(num, denom) - def map_Pow(self, expr): # noqa - return prim.Power(self.rec(expr.base), self.rec(expr.exp)) +# {{{ pymbolic -> symengine - def map_Subs(self, expr): # noqa - return prim.Substitution(self.rec(expr.expr), - tuple(v.name for v in expr.variables), - tuple(self.rec(v) for v in expr.point), - ) +class PymbolicToSympyMapper(PymbolicToSympyLikeMapper): - def map_Derivative(self, expr): # noqa - return prim.Derivative(self.rec(expr.expr), - tuple(v.name for v in expr.variables)) + sym = sympy - def map_CSE(self, expr): # noqa - return prim.CommonSubexpression( - self.rec(expr.args[0]), expr.prefix) + def raise_conversion_error(self, expr): + raise RuntimeError( + "do not know how to translate '%s' to sympy" % expr) - def not_supported(self, expr): - if isinstance(expr, int): - return expr - elif getattr(expr, "is_Function", False): - return prim.Variable(type(expr).__name__)( - *tuple(self.rec(arg) for arg in expr.args)) - else: - return SympyMapper.not_supported(self, expr) + def map_derivative(self, expr): + return self.sym.Derivative(self.rec(expr.child), + *[self.sym.Symbol(v) for v in expr.variables]) # }}} -# {{{ pymbolic -> sympy - -class PymbolicToSympyMapper(EvaluationMapper): - def map_variable(self, expr): - return sp.Symbol(expr.name) - - def map_constant(self, expr): - return sp.sympify(expr) - - def map_call(self, expr): - if isinstance(expr.function, prim.Variable): - func_name = expr.function.name - try: - func = getattr(sp.functions, func_name) - except AttributeError: - func = sp.Function(func_name) - return func(*[self.rec(par) for par in expr.parameters]) - else: - raise RuntimeError("do not know how to translate '%s' to sympy" - % expr) - - def map_subscript(self, expr): - if isinstance(expr.aggregate, prim.Variable) and isinstance(expr.index, int): - return sp.Symbol("%s_%d" % (expr.aggregate.name, expr.index)) - else: - raise RuntimeError("do not know how to translate '%s' to sympy" - % expr) - - def map_substitution(self, expr): - return sp.Subs(self.rec(expr.child), - tuple(sp.Symbol(v) for v in expr.variables), - tuple(self.rec(v) for v in expr.values), - ) +class CSE(sympy.Function): + """A function to translate to a Pymbolic CSE.""" + nargs = 1 - def map_derivative(self, expr): - return sp.Derivative(self.rec(expr.child), - *[sp.Symbol(v) for v in expr.variables]) -# }}} +def make_cse(arg, prefix=None): + result = CSE(arg) + result.prefix = prefix + return result + # vim: fdm=marker diff --git a/test/test_pymbolic.py b/test/test_pymbolic.py index 95182c741f8479884d7b9af9b5d70bf9bd6783be..fe7c8a395b1ad433395d9e0771f6b833ca74884f 100644 --- a/test/test_pymbolic.py +++ b/test/test_pymbolic.py @@ -481,8 +481,8 @@ def test_unifier(): def test_long_sympy_mapping(): + sp = pytest.importorskip("sympy") from pymbolic.interop.sympy import SympyToPymbolicMapper - import sympy as sp SympyToPymbolicMapper()(sp.sympify(int(10**20))) SympyToPymbolicMapper()(sp.sympify(int(10))) diff --git a/test/test_sympy.py b/test/test_sympy.py new file mode 100644 index 0000000000000000000000000000000000000000..6f6ff69249ab2d7ad71620b59c166eac73dfe5ce --- /dev/null +++ b/test/test_sympy.py @@ -0,0 +1,119 @@ +from __future__ import division + +__copyright__ = "Copyright (C) 2017 Matt Wala" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import pytest +import pymbolic.primitives as prim + +x_, y_ = (prim.Variable(s) for s in "x y".split()) + + +# {{{ to pymbolic test + +def _test_to_pymbolic(mapper, sym, use_symengine): + x, y = sym.symbols("x,y") + + assert mapper(sym.Rational(3, 4)) == prim.Quotient(3, 4) + assert mapper(sym.Integer(6)) == 6 + + assert mapper(sym.Subs(x**2, (x,), (y,))) == \ + prim.Substitution(x_**2, ("x",), (y_,)) + # FIXME in symengine + deriv = sym.Derivative(x**2, (x,)) if use_symengine else sym.Derivative(x**2, x) + assert mapper(deriv) == prim.Derivative(x_**2, ("x",)) + + # functions + assert mapper(sym.Function("f")(x)) == prim.Variable("f")(x_) + assert mapper(sym.exp(x)) == prim.Variable("exp")(x_) + + # constants + import math + # FIXME: Why isn't this exact? + assert abs(mapper(sym.pi) - math.pi) < 1e-14 + assert abs(mapper(sym.E) - math.e) < 1e-14 + assert mapper(sym.I) == 1j + +# }}} + + +def test_symengine_to_pymbolic(): + sym = pytest.importorskip("symengine.sympy_compat") + from pymbolic.interop.symengine import SymEngineToPymbolicMapper + mapper = SymEngineToPymbolicMapper() + + _test_to_pymbolic(mapper, sym, True) + + +def test_sympy_to_pymbolic(): + sym = pytest.importorskip("sympy") + from pymbolic.interop.sympy import SympyToPymbolicMapper + mapper = SympyToPymbolicMapper() + + _test_to_pymbolic(mapper, sym, False) + + +# {{{ from pymbolic test + +def _test_from_pymbolic(mapper, sym, use_symengine): + x, y = sym.symbols("x,y") + + assert mapper(x_ + y_) == x + y + assert mapper(x_ * y_) == x * y + assert mapper(x_ ** 2) == x ** 2 + + assert mapper(prim.Substitution(x_**2, ("x",), (y_,))) == \ + sym.Subs(x**2, (x,), (y,)) + # FIXME in symengine + deriv = sym.Derivative(x**2, (x,)) if use_symengine else sym.Derivative(x**2, x) + assert mapper(prim.Derivative(x_**2, ("x",))) == deriv + + assert mapper(x_[0]) == sym.Symbol("x_0") + + assert mapper(prim.Variable("f")(x_)) == sym.Function("f")(x) + +# }}} + + +def test_pymbolic_to_symengine(): + sym = pytest.importorskip("symengine.sympy_compat") + from pymbolic.interop.symengine import PymbolicToSymEngineMapper + mapper = PymbolicToSymEngineMapper() + + _test_from_pymbolic(mapper, sym, True) + + +def test_pymbolic_to_sympy(): + sym = pytest.importorskip("sympy") + from pymbolic.interop.sympy import PymbolicToSympyMapper + mapper = PymbolicToSympyMapper() + + _test_from_pymbolic(mapper, sym, False) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + exec(sys.argv[1]) + else: + from py.test.cmdline import main + main([__file__])