Skip to content

math_spec.typeset.walk

The walk: resolved AST → 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.

The walk holds no opinion the lanes do not: names come from resolution, dim sets from dimensions, operator shapes from the closed BUILTINS set, and a operator it forgot is an assert_never rather than a blank.

PRIME = "'" module-attribute #

Walk(schema, namespace, symbols, fmt) #

Walks a validated schema, emitting :class:Lines in one format.

Stateful only in what it has noticed — which edge policies appeared, whether a translation was counted inside a group, which positional forms printed, and which dimensions were compared against a coordinate that is a number; every one of them something the legend has to explain once the equations print it.

Source code in src/math_spec/typeset/walk.py
def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fmt: Format) -> None:
    self.schema = schema
    self.namespace = namespace
    self.symbols = symbols
    self.format = fmt
    self.policies: set[str] = set()
    self.grouped = False
    self.positions: set[str] = set()
    self.numeric_coordinates: set[str] = set()
    #: Cased expressions met while rendering, in first-use order. Each one
    #: prints once, as a definition of its own; see :meth:`definitions`.
    self.defined: dict[str, CasesNode] = {}

defined = {} instance-attribute #

format = fmt instance-attribute #

grouped = False instance-attribute #

namespace = namespace instance-attribute #

numeric_coordinates = set() instance-attribute #

policies = set() instance-attribute #

positions = set() instance-attribute #

schema = schema instance-attribute #

symbols = symbols instance-attribute #

arithmetic(node, ctx, *, need=0) #

Source code in src/math_spec/typeset/walk.py
def arithmetic(self, node: ArithmeticNode, ctx: _Context, *, need: int = 0) -> str:
    text, precedence = self._arithmetic(node, ctx)
    return self.format.parenthesise(text) if precedence < need else text

conjoined(ctx, *nodes) #

The mask on a quantifier, as one condition.

A mask that is only True prints nothing: the language says it is the same as no where at all, so a quantifier reading : \top would put a condition on the page that reads as one and is not. Nested it still prints — \top \wedge x is what the file says, and simplifying a mask is resolution's job rather than the typesetter's.

Source code in src/math_spec/typeset/walk.py
def conjoined(self, ctx: _Context, *nodes: WhereNode | None) -> str:
    r"""The mask on a quantifier, as one condition.

    A mask that is *only* ``True`` prints nothing: the language says it is
    the same as no ``where`` at all, so a quantifier reading ``: \top``
    would put a condition on the page that reads as one and is not. Nested
    it still prints — ``\top \wedge x`` is what the file says, and
    simplifying a mask is resolution's job rather than the typesetter's.
    """
    kept = [n for n in nodes if n is not None and not (isinstance(n, BooleanLiteralNode) and n.value)]
    parts = [self.where(n, ctx, need=1) for n in kept]
    return self.format.joined(parts, self.op('and')) if parts else ''

constraints() #

Source code in src/math_spec/typeset/walk.py
def constraints(self) -> list[Line]:
    lines = []
    for name, block in self.schema.constraints.items():
        context = f"constraint '{name}'"
        node = expression_of(block.expression, self.schema, self.namespace, context)
        if not isinstance(node, ComparisonNode):
            msg = f'{context}: expected a comparison, got {type(node).__name__}'
            raise AssertionError(msg)
        ctx = self.context(ceiling=2)
        condition = self.conjoined(ctx, where_of(block.where, self.namespace, context))
        lines.append(
            Line(
                label=name,
                left=self.arithmetic(node.left, ctx),
                right=f'{self.op(_RELATIONS[node.op])} {self.arithmetic(node.right, ctx)}',
                condition=self.quantifier(list(block.foreach), condition),
            )
        )
    return lines

context(ceiling=1) #

Source code in src/math_spec/typeset/walk.py
def context(self, ceiling: int = 1) -> _Context:
    return _Context(self, ceiling=ceiling)

convention_notes() #

What the two faces mean, said once, with the model's own symbols.

A nomenclature table already splits the legend into parameters and variables, and that is the convention a paper in this field uses. It is a lookup rather than a reading, though: quote one equation on a slide and the distinction is gone. So the symbols carry it, and this is where the page says which face is which.

Only where the model has both, and only quoting symbols the derivation produced: a table is printed verbatim and is the author's to write, so a symbol it supplies is not one this note governs.

Source code in src/math_spec/typeset/walk.py
def convention_notes(self) -> list[str]:
    """What the two faces mean, said once, with the model's own symbols.

    A nomenclature table already splits the legend into parameters and
    variables, and that is the convention a paper in this field uses. It is
    a *lookup* rather than a reading, though: quote one equation on a slide
    and the distinction is gone. So the symbols carry it, and this is where
    the page says which face is which.

    Only where the model has both, and only quoting symbols the
    *derivation* produced: a table is printed verbatim and is the author's
    to write, so a symbol it supplies is not one this note governs.
    """
    derived = [
        next((n for n in names if n not in self.symbols.overridden), None)
        for names in (self.schema.parameters, self.schema.variables)
    ]
    if not all(derived):
        return []
    given, chosen = (self.format.math(self.symbols.name[n]) for n in derived if n is not None)
    return [
        f'Upright is what the model is given {self.format.dash} a parameter such as {given}, a coordinate '
        f'map, a label {self.format.dash} and italic is what the solver chooses, such as {chosen}. '
        f'An index is italic too, being what a quantifier chooses, and a set is script.'
    ]

definitions() #

One line per cased expression the equations used, defining it.

Inlining a cases block where its name stood is what the AST does, and it is the wrong thing to print: a three-arm block is three rows tall, so whatever follows it in the equation sits beside its middle arm and reads as part of that arm's condition. Worse, a quantity written once in the file would be written once per use on the page — the opposite of what naming it was for.

So a use prints the symbol and the block prints here, which is how a paper states a quantity defined by region. Run this after the sections that use it: what lands here is what they reached, and an arm may itself name another cased expression, so the loop runs to a fixpoint rather than over one pass.

Source code in src/math_spec/typeset/walk.py
def definitions(self) -> list[Line]:
    """One line per cased expression the equations used, defining it.

    Inlining a cases block where its name stood is what the AST does, and
    it is the wrong thing to print: a three-arm block is three rows tall,
    so whatever follows it in the equation sits beside its middle arm and
    reads as part of that arm's condition. Worse, a quantity written once
    in the file would be written once per use on the page — the opposite of
    what naming it was for.

    So a use prints the symbol and the block prints here, which is how a
    paper states a quantity defined by region. Run this **after** the
    sections that use it: what lands here is what they reached, and an arm
    may itself name another cased expression, so the loop runs to a
    fixpoint rather than over one pass.
    """
    lines: list[Line] = []
    done: set[str] = set()
    while pending := [name for name in self.defined if name not in done]:
        for name in pending:
            done.add(name)
            node = self.defined[name]
            ctx = self.context(ceiling=2)
            arms = [(self.arithmetic(arm.value, ctx), self.where(arm.when, ctx, need=1)) for arm in node.arms]
            lines.append(
                Line(
                    label=name,
                    left=ctx.indexed(self.symbols.name[name], list(node.foreach)),
                    right=f'{self.op("equal")} {self.format.cases(arms)}',
                    condition=self.quantifier(list(node.foreach), ''),
                )
            )
    return lines

glossaries() #

Source code in src/math_spec/typeset/walk.py
def glossaries(self) -> list[Glossary]:
    fmt = self.format
    sets = [
        Entry(
            symbol=self.symbols.set[d],
            name=f'index {fmt.math(self.symbols.index[d])} {fmt.dash} {fmt.mono(d)}',
            detail=self._coords(d),
            description=fmt.escape(block.description or ''),
        )
        for d, block in self.schema.dimensions.items()
    ]
    parameters = [
        Entry(
            symbol=self.symbols.name[p],
            name=fmt.mono(p),
            detail=self._over(list(block.dims)),
            description=fmt.escape(block.description or ''),
        )
        for p, block in self.schema.parameters.items()
    ]
    variables = [
        Entry(
            symbol=self.symbols.name[v],
            name=fmt.mono(v),
            detail=self._over(list(block.foreach)),
            description=fmt.escape(block.description or ''),
        )
        for v, block in self.schema.variables.items()
    ]
    groups = (Glossary('Sets', sets), Glossary('Parameters', parameters), Glossary('Variables', variables))
    return [group for group in groups if group.entries]

literal(value) #

Source code in src/math_spec/typeset/walk.py
def literal(self, value: float | str | datetime.date) -> str:
    return self.number(value) if isinstance(value, (int, float)) else self.format.prose(str(value))

membership(dim) #

Source code in src/math_spec/typeset/walk.py
def membership(self, dim: str) -> str:
    return f'{self.symbols.index[dim]} {self.op("in")} {self.symbols.set[dim]}'

number(value) #

Source code in src/math_spec/typeset/walk.py
def number(self, value: float) -> str:
    if value == float('inf'):
        return self.op('infinity')
    if value == float('-inf'):
        return self.op('minus_infinity')
    return str(int(value)) if value == int(value) else repr(value)

objective() #

The objective's line.

The expression is scalar — every reduction in it is one the file wrote — so it renders like any other, and the line carries no label: the block has no name, and the section heading already says what it is.

Source code in src/math_spec/typeset/walk.py
def objective(self) -> list[Line]:
    """The objective's line.

    The expression is scalar — every reduction in it is one the file wrote
    — so it renders like any other, and the line carries no label: the
    block has no name, and the section heading already says what it is.
    """
    block = self.schema.objective
    if block is None:
        return []
    sense = self.op('minimize' if block.sense == 'minimize' else 'maximize')
    node = expression_of(block.expression, self.schema, self.namespace, 'the objective')
    assert not isinstance(node, ComparisonNode)
    return [Line(label='', left=sense, right=self.arithmetic(node, self.context(ceiling=2)))]

op(name) #

Source code in src/math_spec/typeset/walk.py
def op(self, name: str) -> str:
    return self.format.operators[name]

ordinal(dimension, at, grouping=None) #

The position compared against, counted from the end where it is negative.

A position is a place in the order, and the legend runs that order from 0 to the size less one — so -1 printed as itself asserts a position the page has just said cannot exist. What it counts back from is that size, and the group's size where the count is grouped, which is the set those positions are positions in.

0 prints as 0: the file is 0-based and so is the page, so a clause can be read off one and written into the other.

Source code in src/math_spec/typeset/walk.py
def ordinal(self, dimension: str, at: int, grouping: str | None = None) -> str:
    """The position compared against, counted from the end where it is negative.

    A position is a place in the order, and the legend runs that order
    from ``0`` to the size less one — so ``-1`` printed as itself asserts a
    position the page has just said cannot exist. What it counts back from
    is that size, and the *group's* size where the count is grouped, which
    is the set those positions are positions in.

    ``0`` prints as ``0``: the file is 0-based and so is the page, so a
    clause can be read off one and written into the other.
    """
    if at >= 0:
        return self.number(at)
    self.positions.add('from_end')
    size = self.symbols.set[dimension]
    if grouping is not None:
        size = self.format.subscript(size, [grouping])
    return f'{self.format.cardinality(size)} {self.op("minus")} {self.number(-at)}'

position(index, grouping=None) #

position(dim) as the row's place along the dimension.

Applied to the row rather than to the set, because that is what it converts: a coordinate to where that coordinate sits. grouping is the lookup already applied to the row, and rides as a subscript rather than as a second argument — a modifier saying which order is being counted, the way an edge fill rides its translation. As an argument it sat where the first one's integer sits, and read as a second position.

Source code in src/math_spec/typeset/walk.py
def position(self, index: str, grouping: str | None = None) -> str:
    """``position(dim)`` as the row's place along the dimension.

    Applied to the *row* rather than to the set, because that is what it
    converts: a coordinate to where that coordinate sits. *grouping* is the
    lookup already applied to the row, and rides as a **subscript** rather
    than as a second argument — a modifier saying which order is being
    counted, the way an edge fill rides its translation. As an argument it
    sat where the first one's integer sits, and read as a second position.
    """
    self.positions.add('grouped' if grouping is not None else 'plain')
    symbol = self.op('position')
    if grouping is not None:
        symbol = self.format.subscript(symbol, [grouping])
    return self.format.apply(symbol, index)

position_notes() #

A sentence for each positional symbol the model actually printed.

Gated the way the translation notes are, and the first of them is what the page cannot go without. A reader arrives from papers where the index is the ordinal — sets are written as {1, …, T} there, so nothing is marked because nothing needs to be — and this language indexes by coordinates instead. A page printing both pos(t) = 0 and t >= 3 therefore has to say once which of the two is the position, or the reader recovers a different model from the one the file holds.

Source code in src/math_spec/typeset/walk.py
def position_notes(self) -> list[str]:
    """A sentence for each positional symbol the model actually printed.

    Gated the way the translation notes are, and the first of them is what
    the page cannot go without. A reader arrives from papers where the
    index *is* the ordinal — sets are written as ``{1, …, T}`` there, so
    nothing is marked because nothing needs to be — and this language
    indexes by coordinates instead. A page printing both
    ``pos(t) = 0`` and ``t >= 3`` therefore has to say once which of the
    two is the position, or the reader recovers a different model from the
    one the file holds.
    """
    notes = []
    if self.positions:
        index = self.format.math('t')
        place = self.format.math(self.format.apply(self.op('position'), 't'))
        dash = self.format.dash
        notes.append(
            f"{place} denotes where index {index} sits along its dimension's own order {dash} the order "
            f'{self.format.mono("shift")} walks, not the order labels sort in {dash} counted from '
            f'{self.format.math("0")}. The index itself stays the coordinate, so {index} compares against '
            f'labels and {place} against positions.'
        )
    if 'grouped' in self.positions:
        applied = self.format.apply(self.format.upright('lookup'), 't')
        grouped = self.format.math(self.format.apply(self.format.subscript(self.op('position'), [applied]), 't'))
        group = self.format.math(self.format.subscript(self.format.script('T'), [applied]))
        notes.append(
            f'{grouped} counts within the group a lookup puts {self.format.math("t")} in: the subscript names '
            f'the map, {group} is the group it lands in, and that group has a first position of its own.'
        )
    if 'from_end' in self.positions:
        size = self.format.cardinality(self.format.script('T'))
        last = self.format.math(f'{size} {self.op("minus")} {self.number(1)}')
        notes.append(
            f'{self.format.math(size)} denotes the size of the set being counted along, and a position '
            f'counted from the end prints against it {self.format.dash} {last} is the last position, one '
            f'less than the size because the first is {self.format.math("0")}.'
        )
    return notes

quantifier(dims, condition) #

Source code in src/math_spec/typeset/walk.py
def quantifier(self, dims: list[str], condition: str) -> str:
    if not dims and not condition:
        return ''
    over = self.format.joined([self.membership(d) for d in dims], '')
    if not condition:
        return f'{self.op("forall")} {over}'
    if not over:
        return f'{self.format.prose("where ")} {condition}'
    return f'{self.op("forall")} {over} {self.op("such_that")} {condition}'

reduction_body(node, ctx) #

What sits to the right of a sum, bracketed only where it must be.

A sum binds everything up to the next + or - at its own level, so an additive body needs the bracket and nothing else does — including a nested reduction, which is unambiguous. The precedence rule would bracket that too, and a renderer that brackets everything is one nobody trusts to bracket the thing that matters.

Source code in src/math_spec/typeset/walk.py
def reduction_body(self, node: ArithmeticNode, ctx: _Context) -> str:
    """What sits to the right of a sum, bracketed only where it must be.

    A sum binds everything up to the next ``+`` or ``-`` at its own level,
    so an additive body needs the bracket and nothing else does — including
    a nested reduction, which is unambiguous. The precedence rule would
    bracket that too, and a renderer that brackets everything is one nobody
    trusts to bracket the thing that matters.
    """
    additive = isinstance(node, UnaryOperatorNode) or (
        isinstance(node, BinaryOperatorNode) and node.op in ('+', '-')
    )
    return self.arithmetic(node, ctx, need=2 if additive else 0)

translation(step, group='') #

The operator for one translation, carrying its fill and its group.

Both ride the operator, and they take different slots: the fill below, the partition above. Sharing one subscript is what a single symbol allows — two subscripts is a TeX error rather than a rendering (#1165) — but comma-joined there, 0,season_of(t) said nothing about which of the two was the value standing at the boundary and which was the group the translation stays inside.

Source code in src/math_spec/typeset/walk.py
def translation(self, step: _Step, group: str = '') -> str:
    """The operator for one translation, carrying its fill and its group.

    Both ride the operator, and they take **different slots**: the fill
    below, the partition above. Sharing one subscript is what a single
    symbol allows — two subscripts is a TeX error rather than a rendering
    (#1165) — but comma-joined there, ``0,season_of(t)`` said nothing about
    which of the two was the value standing at the boundary and which was
    the group the translation stays inside.
    """
    backward, forward = _TRANSLATIONS[step.policy]
    # a named offset is always backward: `by=-p` is refused, so the sign is
    # in the data and the operator cannot read it off the call
    operator = self.op(backward if isinstance(step.by, str) or step.by > 0 else forward)
    if step.fill:
        operator = self.format.subscript(operator, [step.fill])
    if not group:
        return operator
    self.grouped = True
    return self.format.superscript(operator, group)

translation_notes() #

A sentence for each translation symbol the model actually printed.

Only those: a legend explaining a symbol that is nowhere on the page is a dead end, and plain t-k needs no note until something else stands beside it.

Source code in src/math_spec/typeset/walk.py
def translation_notes(self) -> list[str]:
    """A sentence for each translation symbol the model actually printed.

    Only those: a legend explaining a symbol that is nowhere on the page is
    a dead end, and plain ``t-k`` needs no note until something else stands
    beside it.
    """
    notes = []
    if 'wrap' in self.policies:
        cyclic = self.format.math(f't {self.op("cyclic_minus")} k')
        notes.append(
            f'{cyclic} denotes cyclic translation: index {self.format.math("t-k")} taken modulo the size of '
            f'the dimension ({self.format.mono("roll")}). Plain {self.format.math("t-k")} '
            f'({self.format.mono("shift")}) has no wraparound {self.format.dash} terms translated past '
            f'the edge are simply absent.'
        )
    if 'edge' in self.policies:
        filled = self.format.math(f't {self.format.subscript(self.op("edge_minus"), ["v"])} k')
        notes.append(
            f'{filled} denotes translation with {self.format.math("v")} standing where index '
            f'{self.format.math("t-k")} leaves the dimension ({self.format.mono("shift(edge=v)")}), so the row '
            f'at that boundary is built and carries {self.format.math("v")} rather than being dropped.'
        )
    if self.grouped:
        applied = self.format.apply(self.format.upright('lookup'), 't')
        counted = self.format.math(f't {self.format.superscript(self.op("cyclic_minus"), applied)} k')
        note = (
            f'{counted} denotes a translation counted inside the group a lookup puts {self.format.math("t")} '
            f'in ({self.format.mono("shift(by=lookup)")}), so a term never crosses out of its own group.'
        )
        if 'edge' in self.policies:
            both = self.format.superscript(self.format.subscript(self.op('edge_minus'), ['v']), applied)
            note += (
                f' The two modifiers take different slots {self.format.dash} the group above, the fill '
                f'below {self.format.dash} so {self.format.math(f"t {both} k")} is both at once.'
            )
        notes.append(note)
    return notes

variables() #

One line per variable, and one more for a set the variable carries.

A sos: block restricts the domain — which members of a family may be nonzero at once — so it prints under this heading, beside the variable it is a property of, rather than among the constraints, where it would read as a row a solver holds.

Source code in src/math_spec/typeset/walk.py
def variables(self) -> list[Line]:
    """One line per variable, and one more for a set the variable carries.

    A ``sos:`` block restricts the *domain* — which members of a family may
    be nonzero at once — so it prints under this heading, beside the
    variable it is a property of, rather than among the constraints, where
    it would read as a row a solver holds.
    """
    sets = {block.variable: block for block in self.schema.sos.values()}
    lines = []
    for name, block in self.schema.variables.items():
        ctx = self.context()
        symbol = ctx.indexed(self.symbols.name[name], list(block.foreach))
        where = where_of(block.where, self.namespace, f"variable '{name}'", self_variable=name)
        condition = self.quantifier(list(block.foreach), self.conjoined(ctx, where))
        lower, upper = block.bounds.lower, block.bounds.upper

        if block.domain == 'binary':
            left, right = symbol, f'{self.op("in")} {self.op("binary_set")}'
        else:
            below, above = lower == float('-inf'), upper == float('inf')
            if below and above:
                domain = self.op('integers' if block.domain == 'integer' else 'reals')
                left, right = symbol, f'{self.op("in")} {domain}'
            elif below:
                left, right = symbol, f'{self.op("le")} {self._bound(ctx, upper)}'
            elif above:
                left, right = symbol, f'{self.op("ge")} {self._bound(ctx, lower)}'
            else:
                left = f'{self._bound(ctx, lower)} {self.op("le")} {symbol}'
                right = f'{self.op("le")} {self._bound(ctx, upper)}'
            if block.domain == 'integer' and not (below and above):
                right = f'{right}, {symbol} {self.op("in")} {self.op("integers")}'
        lines.append(Line(label=name, left=left, right=right, condition=condition))
        if name in sets:
            lines.append(self._sos(name, sets[name], ctx))
    return lines

where(node, ctx, *, need=0) #

Source code in src/math_spec/typeset/walk.py
def where(self, node: WhereNode, ctx: _Context, *, need: int = 0) -> str:
    text, precedence = self._where(node, ctx)
    return self.format.parenthesise(text) if precedence < need else text