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 and whether a translation was counted inside a group, which the legend needs to explain the symbols they print.

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

format = fmt instance-attribute #

grouped = False instance-attribute #

namespace = namespace instance-attribute #

policies = 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.'
    ]

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]

position(dimension, at, grouping=None) #

index(dim, i) as the coordinate it names.

An upright application of the operator to the set, the same shape a lookup gets — rather than min/max, which would read the two ends and leave every other position without a notation. grouping is the lookup already applied to the row, and prints as a third argument so the row a position is counted for is visible where the position is.

Source code in src/math_spec/typeset/walk.py
def position(self, dimension: str, at: int, grouping: str | None = None) -> str:
    """``index(dim, i)`` as the coordinate it names.

    An upright application of the operator to the set, the same shape a
    lookup gets — rather than ``min``/``max``, which would read the two
    ends and leave every other position without a notation. *grouping* is
    the lookup already applied to the row, and prints as a third argument
    so the row a position is counted for is visible where the position is.
    """
    parts = [self.symbols.set[dimension], self.number(at)]
    if grouping is not None:
        parts.append(grouping)
    return self.format.apply(self.format.upright('index'), ', '.join(parts))

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