diff --git a/doc/mappers.rst b/doc/mappers.rst index ce56ea91269546489e57d5a664885c499aa0a25a..a3c60b6f4c143a9278f5704c641db36305abc170 100644 --- a/doc/mappers.rst +++ b/doc/mappers.rst @@ -12,15 +12,6 @@ Converting to strings and code .. automodule:: pymbolic.mapper.stringifier -Mappers -******* - -.. autoclass:: StringifyMapper - - .. automethod:: __call__ - -.. autoclass:: CSESplittingStringifyMapperMixin - .. automodule:: pymbolic.mapper.c_code .. autoclass:: CCodeMapper diff --git a/pymbolic/mapper/stringifier.py b/pymbolic/mapper/stringifier.py index 6e1519eff9e7dfe558384ef9bb165a0e2ab0b006..26c2932342a6ae9d28b1f7d4f88ea8ab4729c789 100644 --- a/pymbolic/mapper/stringifier.py +++ b/pymbolic/mapper/stringifier.py @@ -43,6 +43,17 @@ Precedence constants .. data:: PREC_LOGICAL_AND .. data:: PREC_LOGICAL_OR .. data:: PREC_NONE + +Mappers +******* + +.. autoclass:: StringifyMapper + + .. automethod:: __call__ + +.. autoclass:: CSESplittingStringifyMapperMixin + +.. autoclass:: LaTeXMapper """ @@ -225,16 +236,20 @@ class StringifyMapper(pymbolic.mapper.Mapper): def map_left_shift(self, expr, enclosing_prec, *args, **kwargs): return self.parenthesize_if_needed( + # +1 to address + # https://gitlab.tiker.net/inducer/pymbolic/issues/6 self.format("%s << %s", - self.rec(expr.shiftee, PREC_SHIFT, *args, **kwargs), - self.rec(expr.shift, PREC_SHIFT, *args, **kwargs)), + self.rec(expr.shiftee, PREC_SHIFT+1, *args, **kwargs), + self.rec(expr.shift, PREC_SHIFT+1, *args, **kwargs)), enclosing_prec, PREC_SHIFT) def map_right_shift(self, expr, enclosing_prec, *args, **kwargs): return self.parenthesize_if_needed( + # +1 to address + # https://gitlab.tiker.net/inducer/pymbolic/issues/6 self.format("%s >> %s", - self.rec(expr.shiftee, PREC_SHIFT, *args, **kwargs), - self.rec(expr.shift, PREC_SHIFT, *args, **kwargs)), + self.rec(expr.shiftee, PREC_SHIFT+1, *args, **kwargs), + self.rec(expr.shift, PREC_SHIFT+1, *args, **kwargs)), enclosing_prec, PREC_SHIFT) def map_bitwise_not(self, expr, enclosing_prec, *args, **kwargs): @@ -558,4 +573,126 @@ class SimplifyingSortingStringifyMapper(StringifyMapper): # }}} + +# {{{ latex stringifier + +class LaTeXMapper(StringifyMapper): + + COMPARISON_OP_TO_LATEX = { + "==": r"=", + "!=": r"\ne", + "<=": r"\le", + ">=": r"\ge", + "<": r"<", + ">": r">", + } + + def map_remainder(self, expr, enclosing_prec, *args, **kwargs): + return self.format(r"(%s \bmod %s)", + self.rec(expr.numerator, PREC_PRODUCT, *args, **kwargs), + self.rec(expr.denominator, PREC_POWER, *args, **kwargs)), + + def map_left_shift(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.format(r"%s \ll %s", + self.rec(expr.shiftee, PREC_SHIFT+1, *args, **kwargs), + self.rec(expr.shift, PREC_SHIFT+1, *args, **kwargs)), + enclosing_prec, PREC_SHIFT) + + def map_right_shift(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.format(r"%s \gg %s", + self.rec(expr.shiftee, PREC_SHIFT+1, *args, **kwargs), + self.rec(expr.shift, PREC_SHIFT+1, *args, **kwargs)), + enclosing_prec, PREC_SHIFT) + + def map_bitwise_xor(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.join_rec( + r" \wedge ", expr.children, PREC_BITWISE_XOR, *args, **kwargs), + enclosing_prec, PREC_BITWISE_XOR) + + def map_product(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.join_rec(" ", expr.children, PREC_PRODUCT, *args, **kwargs), + enclosing_prec, PREC_PRODUCT) + + def map_power(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.format("{%s}^{%s}", + self.rec(expr.base, PREC_NONE, *args, **kwargs), + self.rec(expr.exponent, PREC_NONE, *args, **kwargs)), + enclosing_prec, PREC_NONE) + + def map_min(self, expr, enclosing_prec, *args, **kwargs): + from pytools import is_single_valued + if is_single_valued(expr.children): + return self.rec(expr.children[0], enclosing_prec) + + what = type(expr).__name__.lower() + return self.format(r"\%s(%s)", + what, self.join_rec(", ", expr.children, PREC_NONE, *args, **kwargs)) + + def map_max(self, expr, enclosing_prec): + return self.map_min(expr, enclosing_prec) + + def map_floor_div(self, expr, enclosing_prec, *args, **kwargs): + return self.format(r"\lfloor {%s} / {%s} \rfloor", + self.rec(expr.numerator, PREC_NONE, *args, **kwargs), + self.rec(expr.denominator, PREC_NONE, *args, **kwargs)) + + def map_subscript(self, expr, enclosing_prec, *args, **kwargs): + if isinstance(expr.index, tuple): + index_str = self.join_rec(", ", expr.index, PREC_NONE, *args, **kwargs) + else: + index_str = self.rec(expr.index, PREC_NONE, *args, **kwargs) + + return self.format("{%s}_{%s}", + self.rec(expr.aggregate, PREC_CALL, *args, **kwargs), + index_str) + + def map_logical_not(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + r"\neg " + self.rec(expr.child, PREC_UNARY, *args, **kwargs), + enclosing_prec, PREC_UNARY) + + def map_logical_or(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.join_rec( + r" \vee ", expr.children, PREC_LOGICAL_OR, *args, **kwargs), + enclosing_prec, PREC_LOGICAL_OR) + + def map_logical_and(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.join_rec( + r" \wedge ", expr.children, PREC_LOGICAL_AND, *args, **kwargs), + enclosing_prec, PREC_LOGICAL_AND) + + def map_comparison(self, expr, enclosing_prec, *args, **kwargs): + return self.parenthesize_if_needed( + self.format("%s %s %s", + self.rec(expr.left, PREC_COMPARISON, *args, **kwargs), + self.COMPARISON_OP_TO_LATEX[expr.operator], + self.rec(expr.right, PREC_COMPARISON, *args, **kwargs)), + enclosing_prec, PREC_COMPARISON) + + def map_substitution(self, expr, enclosing_prec, *args, **kwargs): + substs = ", ".join( + "%s=%s" % (name, self.rec(val, PREC_NONE, *args, **kwargs)) + for name, val in zip(expr.variables, expr.values)) + + return self.format("[%s]\{%s\}", + self.rec(expr.child, PREC_NONE, *args, **kwargs), + substs) + + def map_derivative(self, expr, enclosing_prec, *args, **kwargs): + derivs = " ".join( + r"\frac{\partial}{\partial %s}" % v + for v in expr.variables) + + return self.format("%s %s", + derivs, self.rec(expr.child, PREC_PRODUCT, *args, **kwargs)) + +# }}} + # vim: fdm=marker diff --git a/test/test_maxima.py b/test/test_maxima.py index a61f0f258c512ba28f40ddbfa4f0227376cf648f..ddb5c764085cb20e618c20e34c56fef5789e43eb 100644 --- a/test/test_maxima.py +++ b/test/test_maxima.py @@ -26,9 +26,29 @@ import pytest from pymbolic.interop.maxima import MaximaKernel -def test_kernel(): - pytest.importorskip("pexpect") +# {{{ check for maxima + +def _check_maxima(): + global MAXIMA_UNAVAILABLE + + import os + executable = os.environ.get("PYMBOLIC_MAXIMA_EXECUTABLE", "maxima") + + try: + knl = MaximaKernel(executable=executable) + MAXIMA_UNAVAILABLE = False + knl.shutdown() + except (ImportError, RuntimeError): + MAXIMA_UNAVAILABLE = True + + +_check_maxima() +# }}} + + +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") +def test_kernel(): knl = MaximaKernel() knl.exec_str("k:1/(sqrt((x0-(a+t*b))^2+(y0-(c+t*d))^2+(z0-(e+t*f))^2))") knl.eval_str("sum(diff(k, t,deg)*t^deg,deg,0,6)") @@ -38,21 +58,19 @@ def test_kernel(): @pytest.fixture def knl(request): - pytest.importorskip("pexpect") - knl = MaximaKernel() request.addfinalizer(knl.shutdown) return knl +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_setup(knl): - pytest.importorskip("pexpect") - knl.clean_eval_str_with_setup( ["k:1/(sqrt((x0-(a+t*b))^2+(y0-(c+t*d))^2+(z0-(e+t*f))^2))"], "sum(diff(k, t,deg)*t^deg,deg,0,6)") +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_error(knl): from pymbolic.interop.maxima import MaximaError try: @@ -66,6 +84,7 @@ def test_error(knl): pass +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_strict_round_trip(knl): from pymbolic import parse from pymbolic.primitives import Quotient @@ -90,6 +109,7 @@ def test_strict_round_trip(knl): assert round_trips_correctly +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_lax_round_trip(knl): from pymbolic.interop.maxima import MaximaParser k_setup = [ @@ -104,6 +124,7 @@ def test_lax_round_trip(knl): "ratsimp(result-result2)") == 0 +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_parse_matrix(knl): z = knl.clean_eval_str_with_setup([ "A:matrix([1,2+0.3*dt], [3,4])", @@ -115,23 +136,22 @@ def test_parse_matrix(knl): print(MaximaParser()(z)) +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_diff(): - pytest.importorskip("pexpect") - from pymbolic.interop.maxima import diff from pymbolic import parse diff(parse("sqrt(x**2+y**2)"), parse("x")) +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_long_command(knl): from pymbolic.interop.maxima import set_debug set_debug(4) knl.eval_str("+".join(["1"]*16384)) +@pytest.mark.skipif(MAXIMA_UNAVAILABLE, reason="maxima cannot be launched") def test_restart(knl): - pytest.importorskip("pexpect") - knl = MaximaKernel() knl.restart() knl.eval_str("1") diff --git a/test/test_pymbolic.py b/test/test_pymbolic.py index 44cd8cd4371d0d038293428387a52b796418d03a..bb92ac841a8c2be2cce9e4a704561408fd4e2b09 100644 --- a/test/test_pymbolic.py +++ b/test/test_pymbolic.py @@ -22,9 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import pymbolic import pymbolic.primitives as prim import pytest +from pymbolic import parse from pymbolic.mapper import IdentityMapper @@ -35,9 +35,6 @@ except NameError: from functools import reduce -pymbolic.disable_subscript_by_getitem() - - def test_integer_power(): from pymbolic.algorithm import integer_power @@ -487,6 +484,82 @@ def test_long_sympy_mapping(): SympyToPymbolicMapper()(sp.sympify(int(10))) +def test_stringifier_preserve_shift_order(): + for expr in [ + parse("(a << b) >> 2"), + parse("a << (b >> 2)") + ]: + assert parse(str(expr)) == expr + + +LATEX_TEMPLATE = r"""\documentclass{article} +\usepackage{amsmath} + +\begin{document} +%s +\end{document}""" + + +def test_latex_mapper(): + from pymbolic import parse + from pymbolic.mapper.stringifier import LaTeXMapper, StringifyMapper + + tm = LaTeXMapper() + sm = StringifyMapper() + + equations = [] + + def add(expr): + # Add an equation to the list of tests. + equations.append("\[%s\] %% from: %s" % (tm(expr), sm(expr))) + + add(parse("a * b + c")) + add(parse("f(a,b,c)")) + add(parse("a ** b ** c")) + add(parse("(a | b) ^ ~c")) + add(parse("a << b")) + add(parse("a >> b")) + add(parse("a[i,j,k]")) + add(parse("a[1:3]")) + add(parse("a // b")) + add(parse("not (a or b) and c")) + add(parse("(a % b) % c")) + add(parse("(a >= b) or (b <= c)")) + add(prim.Min((1,)) + prim.Max((1, 2))) + add(prim.Substitution(prim.Variable("x") ** 2, ("x",), (2,))) + add(prim.Derivative(parse("x**2"), ("x",))) + + # Run LaTeX and ensure the file compiles. + import os + import tempfile + import subprocess + import shutil + + latex_dir = tempfile.mkdtemp("pymbolic") + + try: + tex_file_path = os.path.join(latex_dir, "input.tex") + + with open(tex_file_path, "w") as tex_file: + contents = LATEX_TEMPLATE % "\n".join(equations) + tex_file.write(contents) + + try: + subprocess.check_output( + ["latex", + "-interaction=nonstopmode", + "-output-directory=%s" % latex_dir, + tex_file_path], + universal_newlines=True) + except FileNotFoundError: + pytest.skip("latex command not found") + except subprocess.CalledProcessError as err: + assert False, str(err.output) + + finally: + shutil.rmtree(latex_dir) + + if __name__ == "__main__": import sys if len(sys.argv) > 1: