Skip to content

math_spec.typesetting.walk

The walk: program → typeset lines. Written once, for every format.

Everything here is a decision about the math — where a bracket changes the reading, which dimension a reduction binds, that a mask belongs on the ∀ rather than in the equation, that a translation shows at the leaf it re-indexes. None of it is about syntax, so none is duplicated per format.

AlignedComparison = ParameterComparison | ExpressionComparison | CountComparison | DimensionComparison | DimensionPosition | RelationComparison | RelationPairComparison module-attribute #

Walk(program, symbols, fmt, *, inline_expressions=False) #

Walks a program, emitting :class:Lines in one format.

:meth:equations prints every section; what those sections use, the legend reads off the program (:func:~math_spec.typesetting.legend.notice).

Source code in src/math_spec/typesetting/walk.py
def __init__(
    self,
    program: Program,
    symbols: Symbols,
    fmt: Format,
    *,
    inline_expressions: bool = False,
) -> None:
    self.program = program
    self.symbols = symbols
    self.format = fmt
    #: Substitute each plain named expression where it is used, rather than
    #: printing its symbol there and its definition once.
    self.inline_expressions = inline_expressions

format = fmt instance-attribute #

inline_expressions = inline_expressions instance-attribute #

program = program instance-attribute #

symbols = symbols instance-attribute #

defined() #

The named expressions that print under their own symbol: every one, or only the unsubstitutable when inlining.

Inlining leaves a name standing only where substitution cannot reach it — a cases block, and an entry the objective and constraints never read, which is a quantity reported back rather than solved for.

Source code in src/math_spec/typesetting/walk.py
def defined(self) -> list[str]:
    """The named expressions that print under their own symbol: every one, or only the unsubstitutable when inlining.

    Inlining leaves a name standing only where substitution cannot reach
    it — a ``cases`` block, and an entry the objective and constraints
    never read, which is a quantity reported back rather than solved for.
    """
    entries = self.program.expressions
    if not self.inline_expressions:
        return list(entries)
    return [name for name, entry in entries.items() if isinstance(entry.expression, Cases) or not entry.in_math]

definition(name) #

The line defining one named expression, symbol = body over its frame.

Source code in src/math_spec/typesetting/walk.py
def definition(self, name: str) -> Line:
    """The line defining one named expression, ``symbol = body`` over its frame."""
    body = self.program.expressions[name].expression
    frame = self._frame_of(name)
    ctx = self._context(frame)
    rendered = self.format.cases(self._arms(body, ctx)) if isinstance(body, Cases) else self._expression(body, ctx)
    return Line(
        label=name,
        left=ctx.indexed(self.symbols.name[name], frame),
        right=f'{self._op("equal")} {rendered}',
        condition=self._quantifier(frame, ''),
    )

equations() #

Every titled section of equations.

Source code in src/math_spec/typesetting/walk.py
def equations(self) -> list[tuple[str, list[Line]]]:
    """Every titled section of equations."""
    return [
        ('Objective', self._objective()),
        ('Subject to', self._constraints()),
        ('Definitions', self._definitions()),
        ('Variable domains', self._variables()),
        ('Assumptions', self._assumptions()),
    ]

line(name) #

The one line name prints as: a named expression, a constraint, an assumption, a curve, or a variable's domain.

An assumption is looked up where the document prints it from, so a condition a curve's method states is a line a reader can ask for before the curve is written out.

RAISES DESCRIPTION
SchemaError

name is declared as none of the five, or as two — a constraint may share a variable's name, and one line prints one of them.

Source code in src/math_spec/typesetting/walk.py
def line(self, name: str) -> Line:
    """The one line *name* prints as: a named expression, a constraint, an assumption, a curve, or a variable's domain.

    An assumption is looked up where the document prints it from, so a
    condition a curve's method states is a line a reader can ask for
    before the curve is written out.

    Raises:
        SchemaError: *name* is declared as none of the five, or as two — a
            constraint may share a variable's name, and one line prints
            one of them.
    """
    program = self.program
    kinds = {
        'named expression': (program.expressions, self.definition),
        'constraint': (program.constraints, self._constraint),
        'assumption': (program.assumptions, self._assumption),
        'curve': (program.piecewise, self._piecewise),
        'variable': (program.variables, self._variable),
    }
    found = [kind for kind, (group, _) in kinds.items() if name in group]
    if not found:
        everything = {n for group, _ in kinds.values() for n in group}
        msg = (
            f"'{name}' is not a named expression, constraint, assumption, curve or variable. "
            f'{did_you_mean(name, everything)}'
        )
        raise SchemaError(msg)
    if len(found) > 1:
        msg = f"'{name}' is declared twice, as {found[0]} and as {found[1]}, and one line prints one of them — rename one."
        raise SchemaError(msg)
    return kinds[found[0]][1](name)

sides(node, ctx) #

One comparison as its two sides, the relation symbol leading the right.

Split so that a line whose whole predicate is one comparison aligns on the relation, as a constraint does.

Source code in src/math_spec/typesetting/walk.py
def sides(self, node: AlignedComparison, ctx: _Context) -> tuple[str, str]:
    """One comparison as its two sides, the relation symbol leading the right.

    Split so that a line whose whole predicate is one comparison aligns on
    the relation, as a constraint does.
    """
    if isinstance(node, ParameterComparison):
        left, right = ctx.indexed(self.symbols.name[node.name], list(node.dims)), self._literal(node.value)
    elif isinstance(node, ExpressionComparison):
        left, right = self._expression(node.left, ctx), self._expression(node.right, ctx)
    elif isinstance(node, DimensionComparison):
        left, right = ctx.subscript(node.name), self._literal(node.value)
    elif isinstance(node, DimensionPosition):
        grouping = (
            None
            if node.partition is None
            else self._tuple([self._value_read(node.partition.name, c, ctx) for c in node.partition.group])
        )
        left = self._position(ctx.subscript(node.name), grouping)
        right = self._ordinal(node.name, node.position, grouping)
    elif isinstance(node, RelationComparison):
        left, right = self._value_read(node.name, node.column, ctx), self._literal(node.value)
    elif isinstance(node, RelationPairComparison):
        left = self._value_read(node.name, node.column, ctx)
        right = self._value_read(node.other, node.other_column, ctx)
    elif isinstance(node, CountComparison):
        index, inner = ctx.reducing(node.over)
        counted = self.format.set_of(
            self._membership(node.over, index), self._predicate(node.predicate.root, inner)
        )
        left, right = self.format.cardinality(counted), self._number(node.value)
    else:
        assert_never(node)
    return left, f'{self._op(_PREDICATES[node.op])} {right}'