File size: 5,536 Bytes
d06fa57 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """Canonical string renderer for symbolic expressions."""
from __future__ import annotations
from .expression import (
Aggregate,
Binary,
Expression,
Function,
Gather,
GroupedParameter,
Indexed,
Number,
Parameter,
Reduction,
RelationLift,
Symbol,
Unary,
)
PRECEDENCE = {"+": 10, "-": 10, "*": 20, "/": 20, "**": 30}
def render(
expression: Expression,
parent_precedence: int = 0,
right: bool = False,
*,
latex: bool = False,
number_format: str = "",
) -> str:
"""Render an expression as canonical text.
Args:
expression: Symbolic expression to process.
parent_precedence: Precedence required by the enclosing expression.
right: Whether the expression is the right operand of its parent.
latex: Whether to render LaTeX notation.
number_format: Format specification for numeric literals.
Returns:
The rendered expression.
"""
if isinstance(expression, Number):
return format(expression.value, number_format) if number_format else repr(expression.value)
if isinstance(expression, Symbol):
return expression.name
if isinstance(expression, Parameter):
suffix = "" if expression.value is None else f", value={expression.value!r}"
return f"param({expression.name!r}{suffix})"
if isinstance(expression, GroupedParameter):
arguments = [render(expression.by)]
if expression.name is not None:
arguments.append(f"name={expression.name!r}")
if expression.value is not None:
arguments.append(f"value={dict(expression.value)!r}")
if expression.default is not None:
arguments.append(f"default={expression.default!r}")
return f"grouped_param({', '.join(arguments)})"
if isinstance(expression, Indexed):
indices = ", ".join(str(index) for index in expression.indices)
base = render(expression.base, 100, latex=latex, number_format=number_format)
return f"{base}[{indices}]"
if isinstance(expression, Unary):
operand = render(expression.operand, 40, latex=latex, number_format=number_format)
text = f"{expression.operator}{operand}"
return f"({text})" if parent_precedence > 40 else text
if isinstance(expression, Binary):
precedence = PRECEDENCE[expression.operator]
left = render(expression.left, precedence, latex=latex, number_format=number_format)
right_precedence = precedence + (0 if expression.operator == "**" else 1)
right_text = render(
expression.right,
right_precedence,
right=True,
latex=latex,
number_format=number_format,
)
if latex and expression.operator == "/":
text = f"\\frac{{{left}}}{{{right_text}}}"
elif latex and expression.operator == "**":
text = f"{left}^{{{right_text}}}"
else:
operator = (
r" \times "
if latex and expression.operator == "*"
else f" {expression.operator} "
)
text = f"{left}{operator}{right_text}"
return f"({text})" if precedence < parent_precedence else text
if isinstance(expression, Function):
arguments = ", ".join(
render(arg, latex=latex, number_format=number_format)
for arg in expression.arguments
)
if latex and expression.name == "sqrt":
return f"\\sqrt{{{arguments}}}"
if latex and expression.name == "abs":
return f"\\left|{arguments}\\right|"
if latex:
function_name = {
"arccos": r"\arccos",
"arcsin": r"\arcsin",
"arctan": r"\arctan",
"log": r"\ln",
"log10": r"\log_{10}",
"log2": r"\log_{2}",
}.get(expression.name, rf"\operatorname{{{expression.name}}}")
return f"{function_name}\\left({arguments}\\right)"
return f"{expression.name}({arguments})"
if isinstance(expression, Reduction):
indices = ", ".join(str(index) for index in expression.indices)
operand = render(expression.operand, latex=latex, number_format=number_format)
if expression.relation is None:
return f"\\sum_{{{indices}}} {operand}" if latex else f"sum[{indices}]({operand})"
relation = render(expression.relation, latex=latex, number_format=number_format)
if latex:
return f"\\sum_{{{indices}; {relation}}} {operand}"
return f"sum[{indices}]({relation}, {operand})"
if isinstance(expression, Gather):
relation = render(expression.relation, latex=latex, number_format=number_format)
operand = render(expression.operand, latex=latex, number_format=number_format)
if latex:
return f"\\operatorname{{gather}}\\left({relation}, {operand}\\right)"
return f"gather({relation}, {operand})"
if isinstance(expression, Aggregate):
from .desugar import desugar
return render(
desugar(expression),
parent_precedence,
right,
latex=latex,
number_format=number_format,
)
if isinstance(expression, RelationLift):
name = "targ" if expression.role == "target" else "sour"
raise ValueError(f"{name}(...) must appear inside aggr(...).")
raise TypeError(f"Cannot render {type(expression).__name__}.")
|