yuzihan@SIM1
Deploy SRHarness Docker Space
d06fa57
Raw History Blame Contribute Delete
5.54 kB
"""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__}.")