Skip to content

math_spec.typesetting.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.

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/typesetting/walk.py
def __init__(self, schema: _ExpandedSpec, 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()

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/typesetting/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, *masks) #

The mask on a quantifier, as one condition.

A mask every row passes arrives as None — resolution folds it, so this prints what a program carries — and a quantifier with no condition prints none.

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

    A mask every row passes arrives as ``None`` — resolution folds it,
    so this prints what a program carries — and a quantifier with no
    condition prints none.
    """
    kept = [mask.root for mask in masks if mask is not None]
    parts = [self.where(n, ctx, need=1 if len(kept) > 1 else 0) for n in kept]
    return self.format.joined(parts, self.op('and')) if parts else ''

constraints() #

Source code in src/math_spec/typesetting/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(frame=block.foreach)
        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(_PREDICATES[node.op])} {self.arithmetic(node.right, ctx)}',
                condition=self.quantifier(list(block.foreach), condition),
            )
        )
    return lines

context(frame=()) #

Source code in src/math_spec/typesetting/walk.py
def context(self, frame: Iterable[str] = ()) -> _Context:
    return _Context(self, bound=tuple(frame))

convention_notes() #

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

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

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

    Only where the model has both, and quoting only derived symbols: a
    table 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, in declaration order, defining it.

Inlining the block where its name stood is what the AST does and the wrong thing to print: three arms are three rows tall, so whatever follows sits beside the middle one. So a use prints the symbol and the block prints here, as a paper states a quantity defined by region.

Every declared one prints, used or not — the rule a variable's domain follows, and what keeps this section independent of the others having run.

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

    Inlining the block where its name stood is what the AST does and the
    wrong thing to print: three arms are three rows tall, so whatever
    follows sits beside the middle one. So a use prints the symbol and the
    block prints here, as a paper states a quantity defined by region.

    Every declared one prints, used or not — the rule a variable's domain
    follows, and what keeps this section independent of the others having
    run.
    """
    lines = []
    for name in printed_expressions(self.schema):
        node = expression_of(name, self.schema, self.namespace, f"expression '{name}'")
        assert isinstance(node, CasesNode)
        frame = self._frame(name)
        ctx = self.context(frame)
        lines.append(
            Line(
                label=name,
                left=ctx.indexed(self.symbols.name[name], frame),
                right=f'{self.op("equal")} {self.format.cases(self._arms(node, ctx))}',
                condition=self.quantifier(frame, ''),
            )
        )
    return lines

glossaries() #

Source code in src/math_spec/typesetting/walk.py
def glossaries(self) -> list[Glossary]:
    fmt = self.format
    sets = [
        self._entry(
            self.symbols.set[d],
            f'index {fmt.math(self.symbols.index[d])} {fmt.dash} {fmt.mono(d)}{self._coords(d)}',
            block.description,
        )
        for d, block in self.schema.dimensions.items()
    ]
    parameters = [
        self._entry(self.symbols.name[p], f'{fmt.mono(p)}{self._over(list(block.dims))}', block.description)
        for p, block in self.schema.parameters.items()
    ]
    variables = [
        self._entry(self.symbols.name[v], f'{fmt.mono(v)}{self._over(list(block.foreach))}', block.description)
        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/typesetting/walk.py
def literal(self, value: float | str | datetime.date) -> str:
    return self.number(value) if isinstance(value, (int, float)) else self.format.quoted(str(value))

lookup(name, index) #

A coordinate map applied to an index: bus(g).

Source code in src/math_spec/typesetting/walk.py
def lookup(self, name: str, index: str) -> str:
    """A coordinate map applied to an index: ``bus(g)``."""
    return self.format.apply(self.format.upright(name), index)

membership(dim, index=None) #

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

number(value) #

Source code in src/math_spec/typesetting/walk.py
def number(self, value: float) -> str:
    if value == float('inf'):
        return self.op('infinity')
    if value == int(value):
        return str(int(value))
    mantissa, _, exponent = repr(value).partition('e')
    if not exponent:
        return mantissa
    power = self.format.superscript('10', str(int(exponent)))
    return power if mantissa == '1' else f'{mantissa} {self.op("times")} {power}'

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/typesetting/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()))]

op(name) #

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

ordinal(dimension, at, grouping) #

The position compared against; a negative one counts back from the size of the set it is a position in — the group's where grouped.

Source code in src/math_spec/typesetting/walk.py
def ordinal(self, dimension: str, at: int, grouping: str | None) -> str:
    """The position compared against; a negative one counts back from the size of the set it is a position in — the group's where grouped."""
    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) #

position(dim) applied to the row, grouping as a subscript — as an argument it read as a second position.

Source code in src/math_spec/typesetting/walk.py
def position(self, index: str, grouping: str | None) -> str:
    """``position(dim)`` applied to the row, *grouping* as a subscript — as an argument it 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.

The first is what the page cannot go without: a reader arrives from papers where the index is the ordinal, so a page printing both pos(t) = 0 and t >= 3 has to say once which is the position.

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

    The first is what the page cannot go without: a reader arrives from
    papers where the index *is* the ordinal, so a page printing both
    ``pos(t) = 0`` and ``t >= 3`` has to say once which is the position.
    """
    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.lookup('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/typesetting/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/typesetting/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) #

The operator for one translation, its fill below and its group above.

Two subscripts is a TeX error rather than a rendering (#1165), and comma-joined in one, 0,season_of(t) said nothing about which was the fill and which the group. A named offset is always backward, since offset=-p is refused at load.

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

    Two subscripts is a TeX error rather than a rendering (#1165), and
    comma-joined in one, ``0,season_of(t)`` said nothing about which was
    the fill and which the group. A named offset is always backward, since
    ``offset=-p`` is refused at load.
    """
    backward, forward = _TRANSLATIONS[step.policy]
    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 step.within:
        return operator
    self.grouped = True
    return self.format.superscript(operator, step.within)

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/typesetting/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.lookup('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/typesetting/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(frame=block.foreach)
        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/typesetting/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