Skip to content

math_spec.typesetting.symbols

Which symbol each declared name prints as, and the sidecar that overrides it.

This module decides which symbol a name gets; a :class:~math_spec.typesetting.format.Format decides how it is written.

SymbolTable(notation, indices=dict(), sets=dict(), names=dict()) dataclass #

How a reader wants the model to print — notation only, kept out of the model.

Every entry is a spelling, printed verbatim. notation: says which language they are written in, and a render in the other one refuses::

notation: latex
dimensions:
  snapshot: {index: t, set: "\\mathcal{T}"}
  plant:    {index: n}
names:
  marginal_cost: "c^{\\mathrm{marg}}"

An entry naming nothing in the model is an error naming the near miss.

ATTRIBUTE DESCRIPTION
notation

The language the entries are written in; :meth:load lower-cases it.

TYPE: Notation

indices = field(default_factory=dict) class-attribute instance-attribute #

names = field(default_factory=dict) class-attribute instance-attribute #

notation instance-attribute #

sets = field(default_factory=dict) class-attribute instance-attribute #

checked_against(program) #

Reject entries naming nothing in program or in what its formulations state, with the near miss.

A name a piecewise: or sos: block emits counts as declared, so one table spells both readings of a model: the blocks as the file states them, and the rows :meth:~math_spec.model.Spec.expand writes out.

Source code in src/math_spec/typesetting/symbols.py
def checked_against(self, program: Program) -> SymbolTable:
    """Reject entries naming nothing in *program* or in what its formulations state, with the near miss.

    A name a ``piecewise:`` or ``sos:`` block emits counts as declared, so
    one table spells both readings of a model: the blocks as the file states
    them, and the rows :meth:`~math_spec.model.Spec.expand` writes out.
    """
    dims = set(program.dimensions)
    everything = dims | _declared(program) | _emitted(program)
    errors = [
        *(_unknown_entry(d, 'dimensions', dims) for d in {*self.indices, *self.sets} - dims),
        *(_unknown_entry(n, 'names', everything - dims) for n in set(self.names) - everything),
    ]
    if errors:
        raise SchemaError('\n'.join(sorted(errors)))
    return self

load(source) classmethod #

A table from a YAML path or the mapping it parses to.

RAISES DESCRIPTION
SchemaError

An unknown section, a section or a dimension that is not a mapping, or a notation: that is missing or not latex/typst.

Source code in src/math_spec/typesetting/symbols.py
@classmethod
def load(cls, source: str | Path | Mapping[str, object]) -> SymbolTable:
    """A table from a YAML path or the mapping it parses to.

    Raises:
        SchemaError: An unknown section, a section or a dimension that is
            not a mapping, or a ``notation:`` that is missing or not
            ``latex``/``typst``.
    """
    raw = dict(source) if isinstance(source, Mapping) else read_yaml(Path(source))
    unknown = set(raw) - {'notation', 'dimensions', 'names'}
    if unknown:
        msg = f'symbol table: unknown section(s) {sorted(unknown)}. Valid sections: notation, dimensions, names.'
        raise SchemaError(msg)
    if 'notation' not in raw:
        msg = "symbol table: 'notation:' is required — latex or typst, the language the entries are written in."
        raise SchemaError(msg)
    notation = str(raw['notation']).lower()
    if notation not in NOTATIONS:
        msg = f'symbol table: unknown notation {raw["notation"]!r}. Valid notations: latex, typst.'
        raise SchemaError(msg)

    indices: dict[str, str] = {}
    sets: dict[str, str] = {}
    for dim, spec in _section(raw, 'dimensions').items():
        if not isinstance(spec, Mapping):
            msg = f"symbol table: dimension '{dim}' must be a mapping like {{index: t, set: '\\\\mathcal{{T}}'}}"
            raise SchemaError(msg)
        extra = set(spec) - {'index', 'set'}
        if extra:
            msg = f"symbol table: dimension '{dim}' has unknown key(s) {sorted(extra)}. Valid keys: index, set."
            raise SchemaError(msg)
        if 'index' in spec:
            indices[dim] = str(spec['index'])
        if 'set' in spec:
            sets[dim] = str(spec['set'])

    return cls(
        notation=cast('Notation', notation),
        indices=indices,
        sets=sets,
        names={k: str(v) for k, v in _section(raw, 'names').items()},
    )

Symbols(overridden, name, constraint, index, set) dataclass #

How every declared name prints: overrides first, derivation for the rest.

Built by :func:symbols_for. Name symbols settle before dimension indices, so an index is kept off a single letter a variable owns — a dimension plant beside a variable p would otherwise render p_{t,p}. A parameter is upright, so \mathrm{p} beside an index p is not a collision.

ATTRIBUTE DESCRIPTION
overridden

Names the table spelled; the convention note quotes only derived symbols.

TYPE: frozenset[str]

name

Each parameter's, variable's and expression's symbol.

TYPE: Mapping[str, str]

constraint

Each constraint's symbol, the subscript dual(c) prints λ against. Off the flat namespace, like the constraints themselves — a model may name a constraint after a variable, so this is its own map rather than an entry in :attr:name. Given structure, so upright unless a table overrides it.

TYPE: Mapping[str, str]

index

Each dimension's index letter.

TYPE: Mapping[str, str]

set

Each dimension's set symbol.

TYPE: Mapping[str, str]

constraint instance-attribute #

index instance-attribute #

name instance-attribute #

overridden instance-attribute #

set instance-attribute #

chosen_expressions(program) #

The named expressions the solver decides, rather than is handed.

A when does not move one: a variable there asks whether the variable exists, which the model settles when it is built. Only a value reaching a variable does — through another named expression too, since a use of one stands where the name was written. A dual moves one for the same reason a variable does: the solve settles it, and no data hands it over.

Source code in src/math_spec/typesetting/symbols.py
def chosen_expressions(program: Program) -> frozenset[str]:
    """The named expressions the solver decides, rather than is handed.

    A ``when`` does not move one: a variable there asks whether the variable
    *exists*, which the model settles when it is built. Only a value reaching a
    variable does — through another named expression too, since a use of one
    stands where the name was written.
    A ``dual`` moves one for the same reason a variable does: the solve settles
    it, and no data hands it over.
    """
    return frozenset(
        name
        for name, entry in program.expressions.items()
        if any(isinstance(node, Variable | Dual) for node in walk(entry.expression))
    )

symbols_for(program, fmt, table) #

The :class:Symbols program prints with in fmt, table overriding the derivation.

RAISES DESCRIPTION
SchemaError

If table is written in a notation fmt does not read.

Source code in src/math_spec/typesetting/symbols.py
def symbols_for(program: Program, fmt: Format, table: SymbolTable) -> Symbols:
    """The :class:`Symbols` *program* prints with in *fmt*, *table* overriding the derivation.

    Raises:
        SchemaError: If *table* is written in a notation *fmt* does not read.
    """
    if table.notation != fmt.notation:
        msg = (
            f'symbol table: written in {table.notation}, but this is a {fmt.notation} render '
            f'and nothing translates between notations — write a {fmt.notation} table.'
        )
        raise SchemaError(msg)
    chosen = frozenset(program.variables) | chosen_expressions(program)
    names = (*program.parameters, *program.variables, *program.expressions)
    declared = frozenset(names)

    name = {
        n: table.names[n] if n in table.names else _derive_name_symbol(n, declared, fmt, given=n not in chosen)
        for n in names
    }
    spoken_for = {s for s in name.values() if len(s) == 1}
    constraint = {
        n: table.names[n] if n in table.names else _derive_name_symbol(n, declared, fmt, given=True)
        for n in program.constraints
    }

    index: dict[str, str] = {}
    sets: dict[str, str] = {}
    taken_index, taken_set = set(spoken_for), set()
    for dim in program.dimensions:
        overridden = dim in table.indices
        letter = table.indices[dim] if overridden else _first_free(_index_candidates(dim), taken_index)
        taken_index.add(letter)
        index[dim] = letter if len(letter) <= 1 or overridden else fmt.upright(letter)
        upper = _first_free(_set_candidates(dim, letter), taken_set)
        taken_set.add(upper)
        sets[dim] = table.sets[dim] if dim in table.sets else fmt.script(upper)
    return Symbols(frozenset(table.names) & declared, name, constraint, index, sets)