From 13c091928ef96735fa5dd45979f0ee1f789122fc Mon Sep 17 00:00:00 2001 From: Aritra Kar Date: Mon, 16 Oct 2023 22:25:50 -0400 Subject: [PATCH 1/4] Added support for single expressions involving the following functions: numpy.linalg.{matrix_power, qr, svd, det, matrix_rank, inv, pinv}. --- src/latexify/codegen/expression_codegen.py | 129 ++++++++++++++++ .../codegen/expression_codegen_test.py | 138 ++++++++++++++++++ 2 files changed, 267 insertions(+) diff --git a/src/latexify/codegen/expression_codegen.py b/src/latexify/codegen/expression_codegen.py index 3c72219..8a48070 100644 --- a/src/latexify/codegen/expression_codegen.py +++ b/src/latexify/codegen/expression_codegen.py @@ -240,6 +240,125 @@ def _generate_transpose(self, node: ast.Call) -> str | None: else: return None + def _generate_determinant(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.det. + Args: + node: ast.Call node containing the appropriate method invocation. + Returns: + Generated LaTeX, or None if the node has unsupported syntax. + Raises: + LatexifyError: Unsupported argument type given. + """ + name = ast_utils.extract_function_name_or_none(node) + assert name == "det" + + if len(node.args) != 1: + return None + + func_arg = node.args[0] + if isinstance(func_arg, ast.Name): + return rf"\det \left( \mathbf{{{func_arg.id}}} \right)" + elif isinstance(func_arg, ast.List): + return rf"\det \left( {self._generate_matrix(node)} \right)" + + return None + + def _generate_matrix_rank(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.matrix_rank. + Args: + node: ast.Call node containing the appropriate method invocation. + Returns: + Generated LaTeX, or None if the node has unsupported syntax. + Raises: + LatexifyError: Unsupported argument type given. + """ + name = ast_utils.extract_function_name_or_none(node) + assert name == "matrix_rank" + + if len(node.args) != 1: + return None + + func_arg = node.args[0] + if isinstance(func_arg, ast.Name): + return rf"\mathrm{{rank}} \left( \mathbf{{{func_arg.id}}} \right)" + elif isinstance(func_arg, ast.List): + return rf"\mathrm{{rank}} \left( {self._generate_matrix(node)} \right)" + + return None + + def _generate_matrix_power(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.matrix_power. + Args: + node: ast.Call node containing the appropriate method invocation. + Returns: + Generated LaTeX, or None if the node has unsupported syntax. + Raises: + LatexifyError: Unsupported argument type given. + """ + name = ast_utils.extract_function_name_or_none(node) + assert name == "matrix_power" + + if len(node.args) != 2: + return None + + func_arg = node.args[0] + power_arg = node.args[1] + if isinstance(power_arg, ast.Num): + if isinstance(func_arg, ast.Name): + return rf"\mathbf{{{func_arg.id}}}^{{{power_arg.n}}}" + elif isinstance(func_arg, ast.List): + return self._generate_matrix(node) + rf"^{{{power_arg.n}}}" + return None + + def _generate_qr_and_svd(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.qr and numpy.linalg.svd. + Args: + node: ast.Call node containing the appropriate method invocation. + Returns: + Generated LaTeX, or None if the node has unsupported syntax. + Raises: + LatexifyError: Unsupported argument type given. + """ + name = ast_utils.extract_function_name_or_none(node) + assert name == "QR" or name == "SVD" + + if len(node.args) != 1: + return None + + func_arg = node.args[0] + if isinstance(func_arg, ast.Name): + return rf"\mathrm{{{name.upper()}}} \left( \mathbf{{{func_arg.id}}} \right)" + elif isinstance(func_arg, ast.List): + return rf"\mathrm{{{name.upper()}}} \left( {self._generate_matrix(node)} \right)" + + return None + + def _generate_inverses(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.inv. + Args: + node: ast.Call node containing the appropriate method invocation. + Returns: + Generated LaTeX, or None if the node has unsupported syntax. + Raises: + LatexifyError: Unsupported argument type given. + """ + name = ast_utils.extract_function_name_or_none(node) + assert name == "inv" or name == "pinv" + + if len(node.args) != 1: + return None + + func_arg = node.args[0] + if isinstance(func_arg, ast.Name): + if (name == "inv"): + return rf"\mathbf{{{func_arg.id}}}^{{-1}}" + return rf"\mathbf{{{func_arg.id}}}^{{+}}" + elif isinstance(func_arg, ast.List): + if (name == "inv"): + return rf"{self._generate_matrix(node)}^{{-1}}" + return rf"{self._generate_matrix(node)}^{{+}}" + return None + def visit_Call(self, node: ast.Call) -> str: """Visit a Call node.""" func_name = ast_utils.extract_function_name_or_none(node) @@ -256,6 +375,16 @@ def visit_Call(self, node: ast.Call) -> str: special_latex = self._generate_identity(node) elif func_name == "transpose": special_latex = self._generate_transpose(node) + elif func_name == "det": + special_latex = self._generate_determinant(node) + elif func_name == "matrix_rank": + special_latex = self._generate_matrix_rank(node) + elif func_name == "matrix_power": + special_latex = self._generate_matrix_power(node) + elif func_name in ("QR", "SVD"): + special_latex = self._generate_qr_and_svd(node) + elif func_name in ("inv", "pinv"): + special_latex = self._generate_inverses(node) else: special_latex = None diff --git a/src/latexify/codegen/expression_codegen_test.py b/src/latexify/codegen/expression_codegen_test.py index 5eb999b..82c08be 100644 --- a/src/latexify/codegen/expression_codegen_test.py +++ b/src/latexify/codegen/expression_codegen_test.py @@ -992,6 +992,144 @@ def test_transpose(code: str, latex: str) -> None: assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex +@pytest.mark.parametrize( + "code,latex", + [ + ("det(A)", r"\det \left( \mathbf{A} \right)"), + ("det(b)", r"\det \left( \mathbf{b} \right)"), + ("det([[1, 2], [3, 4]])", r"\det \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), + ("det([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\det \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + # Unsupported + ("det()", r"\mathrm{det} \mathopen{}\left( \mathclose{}\right)"), + ("det(2)", r"\mathrm{det} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "det(a, (1, 0))", + r"\mathrm{det} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + ] +) +def test_determinant(code: str, latex: str) -> None: + tree = ast_utils.parse_expr(code) + assert isinstance(tree, ast.Call) + assert expression_codegen.ExpressionCodegen().visit(tree) == latex + +@pytest.mark.parametrize( + "code,latex", + [ + ("matrix_rank(A)", r"\mathrm{rank} \left( \mathbf{A} \right)"), + ("matrix_rank(b)", r"\mathrm{rank} \left( \mathbf{b} \right)"), + ("matrix_rank([[1, 2], [3, 4]])", r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), + ("matrix_rank([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + # Unsupported + ("matrix_rank()", r"\mathrm{matrix\_rank} \mathopen{}\left( \mathclose{}\right)"), + ("matrix_rank(2)", r"\mathrm{matrix\_rank} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "matrix_rank(a, (1, 0))", + r"\mathrm{matrix\_rank} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + ] +) +def test_matrix_rank(code: str, latex: str) -> None: + tree = ast_utils.parse_expr(code) + assert isinstance(tree, ast.Call) + assert expression_codegen.ExpressionCodegen().visit(tree) == latex + +@pytest.mark.parametrize( + "code,latex", + [ + ("matrix_power(A, 2)", r"\mathbf{A}^{2}"), + ("matrix_power(b, 2)", r"\mathbf{b}^{2}"), + ("matrix_power([[1, 2], [3, 4]], 2)", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{2}"), + ("matrix_power([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 42)", r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{42}"), + # Unsupported + ("matrix_power()", r"\mathrm{matrix\_power} \mathopen{}\left( \mathclose{}\right)"), + ("matrix_power(2)", r"\mathrm{matrix\_power} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "matrix_power(a, (1, 0))", + r"\mathrm{matrix\_power} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + ] +) +def test_matrix_power(code: str, latex: str) -> None: + tree = ast_utils.parse_expr(code) + assert isinstance(tree, ast.Call) + assert expression_codegen.ExpressionCodegen().visit(tree) == latex + +@pytest.mark.parametrize( + "code,latex", + [ + # Test QR + ("QR(A)", r"\mathrm{QR} \left( \mathbf{A} \right)"), + ("QR(b)", r"\mathrm{QR} \left( \mathbf{b} \right)"), + ("QR([[1, 2], [3, 4]])", r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), + ("QR([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + # Unsupported + ("QR()", r"\mathrm{QR} \mathopen{}\left( \mathclose{}\right)"), + ("QR(2)", r"\mathrm{QR} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "QR(a, (1, 0))", + r"\mathrm{QR} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + # Test SVD + ("SVD(A)", r"\mathrm{SVD} \left( \mathbf{A} \right)"), + ("SVD(b)", r"\mathrm{SVD} \left( \mathbf{b} \right)"), + ("SVD([[1, 2], [3, 4]])", r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), + ("SVD([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + # Unsupported + ("SVD()", r"\mathrm{SVD} \mathopen{}\left( \mathclose{}\right)"), + ("SVD(2)", r"\mathrm{SVD} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "SVD(a, (1, 0))", + r"\mathrm{SVD} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + ] +) +def test_qr_and_svd(code: str, latex: str) -> None: + tree = ast_utils.parse_expr(code) + assert isinstance(tree, ast.Call) + assert expression_codegen.ExpressionCodegen().visit(tree) == latex + +# tests for inv and pinv +@pytest.mark.parametrize( + "code,latex", + [ + # Test inv + ("inv(A)", r"\mathbf{A}^{-1}"), + ("inv(b)", r"\mathbf{b}^{-1}"), + ("inv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{-1}"), + ("inv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{-1}"), + # Unsupported + ("inv()", r"\mathrm{inv} \mathopen{}\left( \mathclose{}\right)"), + ("inv(2)", r"\mathrm{inv} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "inv(a, (1, 0))", + r"\mathrm{inv} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + # Test pinv + ("pinv(A)", r"\mathbf{A}^{+}"), + ("pinv(b)", r"\mathbf{b}^{+}"), + ("pinv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{+}"), + ("pinv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{+}"), + # Unsupported + ("pinv()", r"\mathrm{pinv} \mathopen{}\left( \mathclose{}\right)"), + ("pinv(2)", r"\mathrm{pinv} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "pinv(a, (1, 0))", + r"\mathrm{pinv} \mathopen{}\left( a, " + r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", + ), + ] +) +def test_inv_and_pinv(code: str, latex: str) -> None: + tree = ast_utils.parse_expr(code) + assert isinstance(tree, ast.Call) + assert expression_codegen.ExpressionCodegen().visit(tree) == latex # Check list for #89. # https://github.com/google/latexify_py/issues/89#issuecomment-1344967636 From 45031ca6e7288287a59c3c3a17851f9dd35a9bc9 Mon Sep 17 00:00:00 2001 From: Aritra Kar Date: Tue, 14 Nov 2023 00:01:58 -0500 Subject: [PATCH 2/4] Fixes for CI tests. --- src/latexify/codegen/expression_codegen.py | 24 +++-- .../codegen/expression_codegen_test.py | 96 +++++++++++++++---- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/latexify/codegen/expression_codegen.py b/src/latexify/codegen/expression_codegen.py index 8a48070..e9057f0 100644 --- a/src/latexify/codegen/expression_codegen.py +++ b/src/latexify/codegen/expression_codegen.py @@ -6,7 +6,11 @@ import re from latexify import analyzers, ast_utils, exceptions -from latexify.codegen import codegen_utils, expression_rules, identifier_converter +from latexify.codegen import ( + codegen_utils, + expression_rules, + identifier_converter, +) class ExpressionCodegen(ast.NodeVisitor): @@ -23,8 +27,8 @@ def __init__( """Initializer. Args: - use_math_symbols: Whether to convert identifiers with a math symbol surface - (e.g., "alpha") to the LaTeX symbol (e.g., "\\alpha"). + use_math_symbols: Whether to convert identifiers with a math symbol + surface (e.g., "alpha") to the LaTeX symbol (e.g., "\\alpha"). use_set_symbols: Whether to use set symbols or not. """ self._identifier_converter = identifier_converter.IdentifierConverter( @@ -260,7 +264,7 @@ def _generate_determinant(self, node: ast.Call) -> str | None: return rf"\det \left( \mathbf{{{func_arg.id}}} \right)" elif isinstance(func_arg, ast.List): return rf"\det \left( {self._generate_matrix(node)} \right)" - + return None def _generate_matrix_rank(self, node: ast.Call) -> str | None: @@ -283,7 +287,7 @@ def _generate_matrix_rank(self, node: ast.Call) -> str | None: return rf"\mathrm{{rank}} \left( \mathbf{{{func_arg.id}}} \right)" elif isinstance(func_arg, ast.List): return rf"\mathrm{{rank}} \left( {self._generate_matrix(node)} \right)" - + return None def _generate_matrix_power(self, node: ast.Call) -> str | None: @@ -307,7 +311,9 @@ def _generate_matrix_power(self, node: ast.Call) -> str | None: if isinstance(func_arg, ast.Name): return rf"\mathbf{{{func_arg.id}}}^{{{power_arg.n}}}" elif isinstance(func_arg, ast.List): - return self._generate_matrix(node) + rf"^{{{power_arg.n}}}" + matrix = self._generate_matrix(node) + if matrix is not None: + return rf"{matrix}^{{{power_arg.n}}}" return None def _generate_qr_and_svd(self, node: ast.Call) -> str | None: @@ -332,7 +338,7 @@ def _generate_qr_and_svd(self, node: ast.Call) -> str | None: return rf"\mathrm{{{name.upper()}}} \left( {self._generate_matrix(node)} \right)" return None - + def _generate_inverses(self, node: ast.Call) -> str | None: """Generates LaTeX for numpy.linalg.inv. Args: @@ -350,11 +356,11 @@ def _generate_inverses(self, node: ast.Call) -> str | None: func_arg = node.args[0] if isinstance(func_arg, ast.Name): - if (name == "inv"): + if name == "inv": return rf"\mathbf{{{func_arg.id}}}^{{-1}}" return rf"\mathbf{{{func_arg.id}}}^{{+}}" elif isinstance(func_arg, ast.List): - if (name == "inv"): + if name == "inv": return rf"{self._generate_matrix(node)}^{{-1}}" return rf"{self._generate_matrix(node)}^{{+}}" return None diff --git a/src/latexify/codegen/expression_codegen_test.py b/src/latexify/codegen/expression_codegen_test.py index 82c08be..eaa088f 100644 --- a/src/latexify/codegen/expression_codegen_test.py +++ b/src/latexify/codegen/expression_codegen_test.py @@ -992,13 +992,20 @@ def test_transpose(code: str, latex: str) -> None: assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex + @pytest.mark.parametrize( "code,latex", [ ("det(A)", r"\det \left( \mathbf{A} \right)"), ("det(b)", r"\det \left( \mathbf{b} \right)"), - ("det([[1, 2], [3, 4]])", r"\det \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), - ("det([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\det \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + ( + "det([[1, 2], [3, 4]])", + r"\det \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", + ), + ( + "det([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\det \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + ), # Unsupported ("det()", r"\mathrm{det} \mathopen{}\left( \mathclose{}\right)"), ("det(2)", r"\mathrm{det} \mathopen{}\left( 2 \mathclose{}\right)"), @@ -1007,65 +1014,98 @@ def test_transpose(code: str, latex: str) -> None: r"\mathrm{det} \mathopen{}\left( a, " r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", ), - ] + ], ) def test_determinant(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex + @pytest.mark.parametrize( "code,latex", [ ("matrix_rank(A)", r"\mathrm{rank} \left( \mathbf{A} \right)"), ("matrix_rank(b)", r"\mathrm{rank} \left( \mathbf{b} \right)"), - ("matrix_rank([[1, 2], [3, 4]])", r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), - ("matrix_rank([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + ( + "matrix_rank([[1, 2], [3, 4]])", + r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", + ), + ( + "matrix_rank([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + ), # Unsupported - ("matrix_rank()", r"\mathrm{matrix\_rank} \mathopen{}\left( \mathclose{}\right)"), - ("matrix_rank(2)", r"\mathrm{matrix\_rank} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "matrix_rank()", + r"\mathrm{matrix\_rank} \mathopen{}\left( \mathclose{}\right)", + ), + ( + "matrix_rank(2)", + r"\mathrm{matrix\_rank} \mathopen{}\left( 2 \mathclose{}\right)", + ), ( "matrix_rank(a, (1, 0))", r"\mathrm{matrix\_rank} \mathopen{}\left( a, " r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", ), - ] + ], ) def test_matrix_rank(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex + @pytest.mark.parametrize( "code,latex", [ ("matrix_power(A, 2)", r"\mathbf{A}^{2}"), ("matrix_power(b, 2)", r"\mathbf{b}^{2}"), - ("matrix_power([[1, 2], [3, 4]], 2)", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{2}"), - ("matrix_power([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 42)", r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{42}"), + ( + "matrix_power([[1, 2], [3, 4]], 2)", + r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{2}", + ), + ( + "matrix_power([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 42)", + r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{42}", + ), # Unsupported - ("matrix_power()", r"\mathrm{matrix\_power} \mathopen{}\left( \mathclose{}\right)"), - ("matrix_power(2)", r"\mathrm{matrix\_power} \mathopen{}\left( 2 \mathclose{}\right)"), + ( + "matrix_power()", + r"\mathrm{matrix\_power} \mathopen{}\left( \mathclose{}\right)", + ), + ( + "matrix_power(2)", + r"\mathrm{matrix\_power} \mathopen{}\left( 2 \mathclose{}\right)", + ), ( "matrix_power(a, (1, 0))", r"\mathrm{matrix\_power} \mathopen{}\left( a, " r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", ), - ] + ], ) def test_matrix_power(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex + @pytest.mark.parametrize( "code,latex", [ # Test QR ("QR(A)", r"\mathrm{QR} \left( \mathbf{A} \right)"), ("QR(b)", r"\mathrm{QR} \left( \mathbf{b} \right)"), - ("QR([[1, 2], [3, 4]])", r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), - ("QR([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + ( + "QR([[1, 2], [3, 4]])", + r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", + ), + ( + "QR([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + ), # Unsupported ("QR()", r"\mathrm{QR} \mathopen{}\left( \mathclose{}\right)"), ("QR(2)", r"\mathrm{QR} \mathopen{}\left( 2 \mathclose{}\right)"), @@ -1077,8 +1117,14 @@ def test_matrix_power(code: str, latex: str) -> None: # Test SVD ("SVD(A)", r"\mathrm{SVD} \left( \mathbf{A} \right)"), ("SVD(b)", r"\mathrm{SVD} \left( \mathbf{b} \right)"), - ("SVD([[1, 2], [3, 4]])", r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)"), - ("SVD([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)"), + ( + "SVD([[1, 2], [3, 4]])", + r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", + ), + ( + "SVD([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + ), # Unsupported ("SVD()", r"\mathrm{SVD} \mathopen{}\left( \mathclose{}\right)"), ("SVD(2)", r"\mathrm{SVD} \mathopen{}\left( 2 \mathclose{}\right)"), @@ -1087,13 +1133,14 @@ def test_matrix_power(code: str, latex: str) -> None: r"\mathrm{SVD} \mathopen{}\left( a, " r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", ), - ] + ], ) def test_qr_and_svd(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex + # tests for inv and pinv @pytest.mark.parametrize( "code,latex", @@ -1102,7 +1149,10 @@ def test_qr_and_svd(code: str, latex: str) -> None: ("inv(A)", r"\mathbf{A}^{-1}"), ("inv(b)", r"\mathbf{b}^{-1}"), ("inv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{-1}"), - ("inv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{-1}"), + ( + "inv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{-1}", + ), # Unsupported ("inv()", r"\mathrm{inv} \mathopen{}\left( \mathclose{}\right)"), ("inv(2)", r"\mathrm{inv} \mathopen{}\left( 2 \mathclose{}\right)"), @@ -1115,7 +1165,10 @@ def test_qr_and_svd(code: str, latex: str) -> None: ("pinv(A)", r"\mathbf{A}^{+}"), ("pinv(b)", r"\mathbf{b}^{+}"), ("pinv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{+}"), - ("pinv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{+}"), + ( + "pinv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{+}", + ), # Unsupported ("pinv()", r"\mathrm{pinv} \mathopen{}\left( \mathclose{}\right)"), ("pinv(2)", r"\mathrm{pinv} \mathopen{}\left( 2 \mathclose{}\right)"), @@ -1124,13 +1177,14 @@ def test_qr_and_svd(code: str, latex: str) -> None: r"\mathrm{pinv} \mathopen{}\left( a, " r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", ), - ] + ], ) def test_inv_and_pinv(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex + # Check list for #89. # https://github.com/google/latexify_py/issues/89#issuecomment-1344967636 @pytest.mark.parametrize( From f9c379cda0446d154015610f639acd08e92bafb5 Mon Sep 17 00:00:00 2001 From: Aritra Kar Date: Tue, 14 Nov 2023 23:35:39 -0500 Subject: [PATCH 3/4] Fixed issues with line lengths and import order. --- src/latexify/codegen/expression_codegen.py | 14 ++++++-------- src/latexify/codegen/expression_codegen_test.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/latexify/codegen/expression_codegen.py b/src/latexify/codegen/expression_codegen.py index e9057f0..963fc7f 100644 --- a/src/latexify/codegen/expression_codegen.py +++ b/src/latexify/codegen/expression_codegen.py @@ -6,11 +6,7 @@ import re from latexify import analyzers, ast_utils, exceptions -from latexify.codegen import ( - codegen_utils, - expression_rules, - identifier_converter, -) +from latexify.codegen import codegen_utils, expression_rules, identifier_converter class ExpressionCodegen(ast.NodeVisitor): @@ -333,10 +329,12 @@ def _generate_qr_and_svd(self, node: ast.Call) -> str | None: func_arg = node.args[0] if isinstance(func_arg, ast.Name): - return rf"\mathrm{{{name.upper()}}} \left( \mathbf{{{func_arg.id}}} \right)" - elif isinstance(func_arg, ast.List): - return rf"\mathrm{{{name.upper()}}} \left( {self._generate_matrix(node)} \right)" + func_arg_str = rf"\mathbf{{{func_arg.id}}}" + return rf"\mathrm{{{name.upper()}}} \left( {func_arg_str} \right)" + elif isinstance(func_arg, ast.List): + matrix_str = self._generate_matrix(node) + return rf"\mathrm{{{name.upper()}}} \left( {matrix_str} \right)" return None def _generate_inverses(self, node: ast.Call) -> str | None: diff --git a/src/latexify/codegen/expression_codegen_test.py b/src/latexify/codegen/expression_codegen_test.py index eaa088f..afcf454 100644 --- a/src/latexify/codegen/expression_codegen_test.py +++ b/src/latexify/codegen/expression_codegen_test.py @@ -1004,7 +1004,8 @@ def test_transpose(code: str, latex: str) -> None: ), ( "det([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\det \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + r"\det \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" + r" 7 & 8 & 9 \end{bmatrix} \right)", ), # Unsupported ("det()", r"\mathrm{det} \mathopen{}\left( \mathclose{}\right)"), @@ -1029,11 +1030,13 @@ def test_determinant(code: str, latex: str) -> None: ("matrix_rank(b)", r"\mathrm{rank} \left( \mathbf{b} \right)"), ( "matrix_rank([[1, 2], [3, 4]])", - r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", + r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 \\" + r" 3 & 4 \end{bmatrix} \right)", ), ( "matrix_rank([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" + r" 7 & 8 & 9 \end{bmatrix} \right)", ), # Unsupported ( @@ -1104,7 +1107,8 @@ def test_matrix_power(code: str, latex: str) -> None: ), ( "QR([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" + r" 7 & 8 & 9 \end{bmatrix} \right)", ), # Unsupported ("QR()", r"\mathrm{QR} \mathopen{}\left( \mathclose{}\right)"), @@ -1123,7 +1127,8 @@ def test_matrix_power(code: str, latex: str) -> None: ), ( "SVD([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \right)", + r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" + r" 7 & 8 & 9 \end{bmatrix} \right)", ), # Unsupported ("SVD()", r"\mathrm{SVD} \mathopen{}\left( \mathclose{}\right)"), From 9334feec7e2dba4bcdb15b98d129d484fe47e202 Mon Sep 17 00:00:00 2001 From: Aritra Kar Date: Thu, 16 Nov 2023 23:29:27 -0500 Subject: [PATCH 4/4] Refactored code. --- src/latexify/codegen/expression_codegen.py | 47 +++++----- .../codegen/expression_codegen_test.py | 94 ++++++------------- 2 files changed, 53 insertions(+), 88 deletions(-) diff --git a/src/latexify/codegen/expression_codegen.py b/src/latexify/codegen/expression_codegen.py index 963fc7f..65263e7 100644 --- a/src/latexify/codegen/expression_codegen.py +++ b/src/latexify/codegen/expression_codegen.py @@ -257,9 +257,11 @@ def _generate_determinant(self, node: ast.Call) -> str | None: func_arg = node.args[0] if isinstance(func_arg, ast.Name): - return rf"\det \left( \mathbf{{{func_arg.id}}} \right)" + arg_id = rf"\mathbf{{{func_arg.id}}}" + return rf"\det \mathopen{{}}\left( {arg_id} \mathclose{{}}\right)" elif isinstance(func_arg, ast.List): - return rf"\det \left( {self._generate_matrix(node)} \right)" + matrix = self._generate_matrix(node) + return rf"\det \mathopen{{}}\left( {matrix} \mathclose{{}}\right)" return None @@ -280,9 +282,15 @@ def _generate_matrix_rank(self, node: ast.Call) -> str | None: func_arg = node.args[0] if isinstance(func_arg, ast.Name): - return rf"\mathrm{{rank}} \left( \mathbf{{{func_arg.id}}} \right)" + arg_id = rf"\mathbf{{{func_arg.id}}}" + return ( + rf"\mathrm{{rank}} \mathopen{{}}\left( {arg_id} \mathclose{{}}\right)" + ) elif isinstance(func_arg, ast.List): - return rf"\mathrm{{rank}} \left( {self._generate_matrix(node)} \right)" + matrix = self._generate_matrix(node) + return ( + rf"\mathrm{{rank}} \mathopen{{}}\left( {matrix} \mathclose{{}}\right)" + ) return None @@ -312,8 +320,8 @@ def _generate_matrix_power(self, node: ast.Call) -> str | None: return rf"{matrix}^{{{power_arg.n}}}" return None - def _generate_qr_and_svd(self, node: ast.Call) -> str | None: - """Generates LaTeX for numpy.linalg.qr and numpy.linalg.svd. + def _generate_inv(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.inv. Args: node: ast.Call node containing the appropriate method invocation. Returns: @@ -322,23 +330,20 @@ def _generate_qr_and_svd(self, node: ast.Call) -> str | None: LatexifyError: Unsupported argument type given. """ name = ast_utils.extract_function_name_or_none(node) - assert name == "QR" or name == "SVD" + assert name == "inv" if len(node.args) != 1: return None func_arg = node.args[0] if isinstance(func_arg, ast.Name): - func_arg_str = rf"\mathbf{{{func_arg.id}}}" - return rf"\mathrm{{{name.upper()}}} \left( {func_arg_str} \right)" - + return rf"\mathbf{{{func_arg.id}}}^{{-1}}" elif isinstance(func_arg, ast.List): - matrix_str = self._generate_matrix(node) - return rf"\mathrm{{{name.upper()}}} \left( {matrix_str} \right)" + return rf"{self._generate_matrix(node)}^{{-1}}" return None - def _generate_inverses(self, node: ast.Call) -> str | None: - """Generates LaTeX for numpy.linalg.inv. + def _generate_pinv(self, node: ast.Call) -> str | None: + """Generates LaTeX for numpy.linalg.pinv. Args: node: ast.Call node containing the appropriate method invocation. Returns: @@ -347,19 +352,15 @@ def _generate_inverses(self, node: ast.Call) -> str | None: LatexifyError: Unsupported argument type given. """ name = ast_utils.extract_function_name_or_none(node) - assert name == "inv" or name == "pinv" + assert name == "pinv" if len(node.args) != 1: return None func_arg = node.args[0] if isinstance(func_arg, ast.Name): - if name == "inv": - return rf"\mathbf{{{func_arg.id}}}^{{-1}}" return rf"\mathbf{{{func_arg.id}}}^{{+}}" elif isinstance(func_arg, ast.List): - if name == "inv": - return rf"{self._generate_matrix(node)}^{{-1}}" return rf"{self._generate_matrix(node)}^{{+}}" return None @@ -385,10 +386,10 @@ def visit_Call(self, node: ast.Call) -> str: special_latex = self._generate_matrix_rank(node) elif func_name == "matrix_power": special_latex = self._generate_matrix_power(node) - elif func_name in ("QR", "SVD"): - special_latex = self._generate_qr_and_svd(node) - elif func_name in ("inv", "pinv"): - special_latex = self._generate_inverses(node) + elif func_name == "inv": + special_latex = self._generate_inv(node) + elif func_name == "pinv": + special_latex = self._generate_pinv(node) else: special_latex = None diff --git a/src/latexify/codegen/expression_codegen_test.py b/src/latexify/codegen/expression_codegen_test.py index afcf454..e2b9453 100644 --- a/src/latexify/codegen/expression_codegen_test.py +++ b/src/latexify/codegen/expression_codegen_test.py @@ -996,16 +996,17 @@ def test_transpose(code: str, latex: str) -> None: @pytest.mark.parametrize( "code,latex", [ - ("det(A)", r"\det \left( \mathbf{A} \right)"), - ("det(b)", r"\det \left( \mathbf{b} \right)"), + ("det(A)", r"\det \mathopen{}\left( \mathbf{A} \mathclose{}\right)"), + ("det(b)", r"\det \mathopen{}\left( \mathbf{b} \mathclose{}\right)"), ( "det([[1, 2], [3, 4]])", - r"\det \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", + r"\det \mathopen{}\left( \begin{bmatrix} 1 & 2 \\" + r" 3 & 4 \end{bmatrix} \mathclose{}\right)", ), ( "det([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\det \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" - r" 7 & 8 & 9 \end{bmatrix} \right)", + r"\det \mathopen{}\left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" + r" 7 & 8 & 9 \end{bmatrix} \mathclose{}\right)", ), # Unsupported ("det()", r"\mathrm{det} \mathopen{}\left( \mathclose{}\right)"), @@ -1026,17 +1027,23 @@ def test_determinant(code: str, latex: str) -> None: @pytest.mark.parametrize( "code,latex", [ - ("matrix_rank(A)", r"\mathrm{rank} \left( \mathbf{A} \right)"), - ("matrix_rank(b)", r"\mathrm{rank} \left( \mathbf{b} \right)"), + ( + "matrix_rank(A)", + r"\mathrm{rank} \mathopen{}\left( \mathbf{A} \mathclose{}\right)", + ), + ( + "matrix_rank(b)", + r"\mathrm{rank} \mathopen{}\left( \mathbf{b} \mathclose{}\right)", + ), ( "matrix_rank([[1, 2], [3, 4]])", - r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 \\" - r" 3 & 4 \end{bmatrix} \right)", + r"\mathrm{rank} \mathopen{}\left( \begin{bmatrix} 1 & 2 \\" + r" 3 & 4 \end{bmatrix} \mathclose{}\right)", ), ( "matrix_rank([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\mathrm{rank} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" - r" 7 & 8 & 9 \end{bmatrix} \right)", + r"\mathrm{rank} \mathopen{}\left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" + r" 7 & 8 & 9 \end{bmatrix} \mathclose{}\right)", ), # Unsupported ( @@ -1098,75 +1105,32 @@ def test_matrix_power(code: str, latex: str) -> None: @pytest.mark.parametrize( "code,latex", [ - # Test QR - ("QR(A)", r"\mathrm{QR} \left( \mathbf{A} \right)"), - ("QR(b)", r"\mathrm{QR} \left( \mathbf{b} \right)"), - ( - "QR([[1, 2], [3, 4]])", - r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", - ), - ( - "QR([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\mathrm{QR} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" - r" 7 & 8 & 9 \end{bmatrix} \right)", - ), - # Unsupported - ("QR()", r"\mathrm{QR} \mathopen{}\left( \mathclose{}\right)"), - ("QR(2)", r"\mathrm{QR} \mathopen{}\left( 2 \mathclose{}\right)"), - ( - "QR(a, (1, 0))", - r"\mathrm{QR} \mathopen{}\left( a, " - r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", - ), - # Test SVD - ("SVD(A)", r"\mathrm{SVD} \left( \mathbf{A} \right)"), - ("SVD(b)", r"\mathrm{SVD} \left( \mathbf{b} \right)"), - ( - "SVD([[1, 2], [3, 4]])", - r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \right)", - ), + ("inv(A)", r"\mathbf{A}^{-1}"), + ("inv(b)", r"\mathbf{b}^{-1}"), + ("inv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{-1}"), ( - "SVD([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\mathrm{SVD} \left( \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\" - r" 7 & 8 & 9 \end{bmatrix} \right)", + "inv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", + r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{-1}", ), # Unsupported - ("SVD()", r"\mathrm{SVD} \mathopen{}\left( \mathclose{}\right)"), - ("SVD(2)", r"\mathrm{SVD} \mathopen{}\left( 2 \mathclose{}\right)"), + ("inv()", r"\mathrm{inv} \mathopen{}\left( \mathclose{}\right)"), + ("inv(2)", r"\mathrm{inv} \mathopen{}\left( 2 \mathclose{}\right)"), ( - "SVD(a, (1, 0))", - r"\mathrm{SVD} \mathopen{}\left( a, " + "inv(a, (1, 0))", + r"\mathrm{inv} \mathopen{}\left( a, " r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", ), ], ) -def test_qr_and_svd(code: str, latex: str) -> None: +def test_inv(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex -# tests for inv and pinv @pytest.mark.parametrize( "code,latex", [ - # Test inv - ("inv(A)", r"\mathbf{A}^{-1}"), - ("inv(b)", r"\mathbf{b}^{-1}"), - ("inv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{-1}"), - ( - "inv([[1, 2, 3], [4, 5, 6], [7, 8, 9]])", - r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^{-1}", - ), - # Unsupported - ("inv()", r"\mathrm{inv} \mathopen{}\left( \mathclose{}\right)"), - ("inv(2)", r"\mathrm{inv} \mathopen{}\left( 2 \mathclose{}\right)"), - ( - "inv(a, (1, 0))", - r"\mathrm{inv} \mathopen{}\left( a, " - r"\mathopen{}\left( 1, 0 \mathclose{}\right) \mathclose{}\right)", - ), - # Test pinv ("pinv(A)", r"\mathbf{A}^{+}"), ("pinv(b)", r"\mathbf{b}^{+}"), ("pinv([[1, 2], [3, 4]])", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}^{+}"), @@ -1184,7 +1148,7 @@ def test_qr_and_svd(code: str, latex: str) -> None: ), ], ) -def test_inv_and_pinv(code: str, latex: str) -> None: +def test_pinv(code: str, latex: str) -> None: tree = ast_utils.parse_expr(code) assert isinstance(tree, ast.Call) assert expression_codegen.ExpressionCodegen().visit(tree) == latex