Skip to content

printing

Printing utilities for expressions and equations of motion.

print_eom(eqns) — display the EOM dict as LaTeX equations print_tree(expr) — display the expression tree structure for debugging render_eom(eqns) — compile EOM to PDF via pdflatex

display_eom(eqns)

Display equations of motion as rendered LaTeX in a Jupyter notebook.

Source code in geomech/utils/printing.py
def display_eom(eqns):
    """Display equations of motion as rendered LaTeX in a Jupyter notebook."""
    from IPython.display import Math, display

    for key, (var_vec, eqn) in eqns.items():
        latex_str = _strip_outer_parens(to_latex(eqn)) + " = 0"
        display(Math(latex_str))

display_latex(expr)

Display an expression as rendered LaTeX in a Jupyter notebook.

Source code in geomech/utils/printing.py
def display_latex(expr):
    """Display an expression as rendered LaTeX in a Jupyter notebook."""
    from IPython.display import Math, display

    display(Math(to_latex(expr)))

display_standard_form(sf)

Display standard form as Mddq + Gu + f = 0 in a Jupyter notebook.

Source code in geomech/utils/printing.py
def display_standard_form(sf):
    """Display standard form as M*ddq + G*u + f = 0 in a Jupyter notebook."""
    from IPython.display import Math, display

    for key, eq in sf.items():
        parts = []
        # M * accel terms
        for accel, coeff in eq.M.items():
            coeff_str = to_latex(coeff)
            accel_str = _latex_str_key(accel)
            parts.append(f"{coeff_str} {accel_str}")
        # G * input terms
        for inp, coeff in eq.G.items():
            g_str = to_latex(coeff)
            if g_str in ("I", "1"):
                parts.append(inp)
            else:
                parts.append(f"{g_str} {inp}")
        # f term
        f_str = to_latex(eq.f)
        if f_str not in ("0v", "0"):
            parts.append(f_str)
        # Join with sign-aware concatenation
        if not parts:
            equation = "0 = 0"
        else:
            result = parts[0]
            for p in parts[1:]:
                if p.startswith("-"):
                    result += f" {p}"
                else:
                    result += f" + {p}"
            equation = result + " = 0"
        display(Math(f"{_latex_str_key(key)}: \\quad {equation}"))

eom_to_latex(eqns)

Return a complete LaTeX document string for the EOM dict.

Source code in geomech/utils/printing.py
def eom_to_latex(eqns):
    """Return a complete LaTeX document string for the EOM dict."""
    lines = [
        r"\documentclass[12pt]{article}",
        r"\usepackage{amsmath,amssymb}",
        r"\usepackage[margin=1in]{geometry}",
        r"\begin{document}",
        r"\section*{Equations of Motion}",
    ]
    for key, (var_vec, eqn) in eqns.items():
        lhs = str(var_vec)
        rhs = str(eqn)
        lines.append(r"\begin{equation}")
        lines.append(r"\int " + lhs + r" \cdot \Big(" + rhs + r"\Big)\, dt = 0")
        lines.append(r"\end{equation}")
    lines.append(r"\end{document}")
    return "\n".join(lines)

print_eom(eqns)

Print equations of motion as LaTeX integral equations.

eqns is the dict returned by compute_eom: {str(variation_vector): (variation_vector, equation)}

Source code in geomech/utils/printing.py
def print_eom(eqns):
    """Print equations of motion as LaTeX integral equations.

    *eqns* is the dict returned by ``compute_eom``:
        {str(variation_vector): (variation_vector, equation)}
    """
    for key, (var_vec, eqn) in eqns.items():
        lhs = str(var_vec)
        rhs = str(eqn)
        line = "\\int{" + lhs + " \\cdot " + "\\Big(" + rhs + "\\Big)}dt=0"
        print(line)

print_tree(expr, indent=0, label='', style='topdown')

Print the expression tree structure for debugging.

Each node shows its class name, type, and key properties. Children are indented below their parent.

Parameters

style : str 'topdown' (default) — graphical top-down tree with / and \ branches. 'indent' — original indented list format.

Source code in geomech/utils/printing.py
def print_tree(expr, indent=0, label="", style="topdown"):
    """Print the expression tree structure for debugging.

    Each node shows its class name, type, and key properties.
    Children are indented below their parent.

    Parameters
    ----------
    style : str
        'topdown' (default) — graphical top-down tree with / and \\ branches.
        'indent' — original indented list format.
    """
    if style == "topdown":
        lines, _, _ = _render_topdown(expr)
        print("\n".join(lines))
    else:
        lines = []
        _build_tree(expr, lines, indent, label)
        print("\n".join(lines))
    print()
    print(f"expr: {repr(expr)}")

render_eom(eqns, output='eom.pdf', open_pdf=True)

Compile EOM to PDF via pdflatex.

Parameters

eqns : dict The dict returned by compute_eom. output : str Output PDF path (default: eom.pdf in current directory). open_pdf : bool If True, open the PDF after compilation (macOS open).

Source code in geomech/utils/printing.py
def render_eom(eqns, output="eom.pdf", open_pdf=True):
    """Compile EOM to PDF via pdflatex.

    Parameters
    ----------
    eqns : dict
        The dict returned by ``compute_eom``.
    output : str
        Output PDF path (default: ``eom.pdf`` in current directory).
    open_pdf : bool
        If True, open the PDF after compilation (macOS ``open``).
    """
    tex_src = eom_to_latex(eqns)
    output = os.path.abspath(output)
    pdf_name = os.path.splitext(os.path.basename(output))[0]

    with tempfile.TemporaryDirectory() as tmpdir:
        tex_path = os.path.join(tmpdir, pdf_name + ".tex")
        with open(tex_path, "w") as f:
            f.write(tex_src)

        result = subprocess.run(
            ["pdflatex", "-interaction=nonstopmode", "-output-directory", tmpdir, tex_path],
            capture_output=True,
            text=True,
        )
        pdf_tmp = os.path.join(tmpdir, pdf_name + ".pdf")
        if result.returncode != 0 or not os.path.exists(pdf_tmp):
            print("pdflatex failed:")
            print(result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout)
            return None

        import shutil

        shutil.copy2(pdf_tmp, output)

    print(f"PDF written to {output}")
    if open_pdf:
        subprocess.run(["open", output])
    return output

repr_str(expr)

Return a human-readable string for the expression (used by repr).

Source code in geomech/utils/printing.py
def repr_str(expr):
    """Return a human-readable string for the expression (used by __repr__)."""
    from geomech.core.operations.addition import Add, MAdd, VAdd
    from geomech.core.operations.calculus import TimeDerivative, TimeIntegral, Variation
    from geomech.core.operations.geometry import Cross, Dot, Hat, Transpose, Vee
    from geomech.core.operations.multiplication import MMMul, Mul, MVMul, SMMul, SVMul, VVMul

    nodes = getattr(expr, "nodes", None)

    # Leaf
    if nodes is None or len(nodes) == 0:
        name = getattr(expr, "name", None)
        if name is not None:
            return name
        value = getattr(expr, "value", None)
        if value is not None:
            return str(value)
        return "?"

    # N-ary addition
    if isinstance(expr, (Add, VAdd, MAdd)):
        parts = []
        for i, child in enumerate(nodes):
            s = repr_str(child)
            if i > 0 and not s.startswith("-"):
                parts.append(" + ")
            elif i > 0:
                parts.append(" ")
            parts.append(s)
        return "(" + "".join(parts) + ")"

    # Binary multiplication — check for negative scalar factor
    if isinstance(expr, (Mul, SVMul, SMMul, MVMul, MMMul, VVMul)):
        left = repr_str(expr.left)
        right = repr_str(expr.right)
        # Detect multiplying by -1
        lval = getattr(expr.left, "value", None)
        rval = getattr(expr.right, "value", None)
        if isinstance(lval, (int, float)) and lval == -1:
            return f"-{right}"
        if isinstance(rval, (int, float)) and rval == -1:
            return f"-{left}"
        return f"{left}*{right}"

    # Dot, Cross
    if isinstance(expr, Dot):
        return f"<{repr_str(expr.left)}, {repr_str(expr.right)}>"
    if isinstance(expr, Cross):
        return f"cross({repr_str(expr.left)}, {repr_str(expr.right)})"

    # Hat, Vee, Transpose
    if isinstance(expr, Hat):
        return f"hat({repr_str(expr.expr)})"
    if isinstance(expr, Vee):
        return f"vee({repr_str(expr.expr)})"
    if isinstance(expr, Transpose):
        return f"{repr_str(expr.expr)}'"

    # Calculus
    if isinstance(expr, Variation):
        return f"δ({repr_str(expr.expr)})"
    if isinstance(expr, TimeDerivative):
        inner = repr_str(expr.expr)
        # For simple names, put combining dot above: x → ẋ
        if inner.isalnum() and len(inner) <= 10:
            return inner + "\u0307"
        return f"d/dt({inner})"
    if isinstance(expr, TimeIntegral):
        return f"∫({repr_str(expr.expr)})dt"

    # Fallback
    return str(expr)

to_latex(expr)

Convert an expression tree to a LaTeX string.

Returns a string suitable for use with IPython.display.Math() or inside a LaTeX equation environment.

Source code in geomech/utils/printing.py
def to_latex(expr):
    """Convert an expression tree to a LaTeX string.

    Returns a string suitable for use with IPython.display.Math() or
    inside a LaTeX equation environment.
    """
    from geomech.core.operations.addition import Add, MAdd, VAdd
    from geomech.core.operations.calculus import TimeDerivative, TimeIntegral, Variation
    from geomech.core.operations.geometry import Cross, Dot, Hat, Transpose, Vee
    from geomech.core.operations.multiplication import MMMul, Mul, MVMul, SMMul, SVMul, VVMul

    nodes = getattr(expr, "nodes", None)

    # Leaf
    if nodes is None or len(nodes) == 0:
        name = getattr(expr, "name", None)
        if name is not None:
            val = getattr(expr, "value", None)
            if val is not None and isinstance(val, (int, float)):
                if val == -1 or val == -1.0:
                    return "-1"
                if val == 0.5:
                    return r"\frac{1}{2}"
                if val == -0.5:
                    return r"-\frac{1}{2}"
                if val == 1 or val == 1.0:
                    return "1"
                return str(val)
            return name
        return "?"

    # N-ary addition
    if isinstance(expr, (Add, VAdd, MAdd)):
        parts = []
        for i, child in enumerate(nodes):
            s = to_latex(child)
            if i > 0 and not s.startswith("-"):
                parts.append(" + ")
            elif i > 0:
                parts.append(" ")
            parts.append(s)
        return "\\left(" + "".join(parts) + "\\right)"

    # Scalar * Scalar — collect chain
    if isinstance(expr, Mul):
        neg, factors = _collect_scalar_factors(expr)
        result = " ".join(factors) if factors else "1"
        return f"-{result}" if neg else result

    # SVMul: vector * scalar → collect scalar, render as coeff * vector
    if isinstance(expr, SVMul):
        vec_str = to_latex(expr.left)
        neg, factors = _collect_scalar_factors(expr.right)
        if factors:
            coeff = " ".join(factors)
            result = f"{coeff} {vec_str}"
        else:
            result = vec_str
        return f"-{result}" if neg else result

    # SMMul: matrix * scalar
    if isinstance(expr, SMMul):
        mat_str = to_latex(expr.left)
        neg, factors = _collect_scalar_factors(expr.right)
        if factors:
            coeff = " ".join(factors)
            result = f"{coeff} {mat_str}"
        else:
            result = mat_str
        return f"-{result}" if neg else result

    # MVMul: matrix * vector
    if isinstance(expr, MVMul):
        return f"{to_latex(expr.left)} {to_latex(expr.right)}"

    # MMMul: matrix * matrix
    if isinstance(expr, MMMul):
        return f"{to_latex(expr.left)} {to_latex(expr.right)}"

    # VVMul: vector * vector^T
    if isinstance(expr, VVMul):
        return f"{to_latex(expr.left)} {to_latex(expr.right)}"

    # Dot product
    if isinstance(expr, Dot):
        return f"{to_latex(expr.left)} \\cdot {to_latex(expr.right)}"

    # Cross product
    if isinstance(expr, Cross):
        return f"{to_latex(expr.left)} \\times {to_latex(expr.right)}"

    # Hat (skew-symmetric)
    if isinstance(expr, Hat):
        return f"\\widehat{{{to_latex(expr.expr)}}}"

    # Vee
    if isinstance(expr, Vee):
        return f"\\left({to_latex(expr.expr)}\\right)^\\vee"

    # Transpose
    if isinstance(expr, Transpose):
        return f"{to_latex(expr.expr)}^T"

    # Time derivative — detect double derivative for ddot
    if isinstance(expr, TimeDerivative):
        if isinstance(expr.expr, TimeDerivative):
            return f"\\ddot{{{to_latex(expr.expr.expr)}}}"
        return f"\\dot{{{to_latex(expr.expr)}}}"

    # Variation
    if isinstance(expr, Variation):
        return f"\\delta {to_latex(expr.expr)}"

    # Time integral
    if isinstance(expr, TimeIntegral):
        return f"\\int {to_latex(expr.expr)} \\, dt"

    # Fallback
    return str(expr)

tree_str(expr, indent=0, label='', style='topdown')

Return the expression tree as a string (without printing).

Source code in geomech/utils/printing.py
def tree_str(expr, indent=0, label="", style="topdown"):
    """Return the expression tree as a string (without printing)."""
    if style == "topdown":
        lines, _, _ = _render_topdown(expr)
        return "\n".join(lines)
    else:
        lines = []
        _build_tree(expr, lines, indent, label)
        return "\n".join(lines)