Skip to content

math_spec.typesetting.symbols

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

Derivation aims at unambiguous, not beautiful, so a model prints with no setup; :class:SymbolTable is where a reader makes it conventional, in a file of its own, since presentation is not language. What a declaration is stays description: on the declaration. 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(schema) #

Reject entries naming nothing in schema, with the near miss.

Source code in src/math_spec/typesetting/symbols.py
def checked_against(self, schema: _ExpandedSpec) -> SymbolTable:
    """Reject entries naming nothing in *schema*, with the near miss."""
    dims = set(schema.dimensions)
    everything = dims | set(schema.parameters) | set(schema.variables) | set(printed_expressions(schema))
    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 malformed dimension, 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, Any]) -> SymbolTable:
    """A table from a YAML path or the mapping it parses to.

    Raises:
        SchemaError: An unknown section, a malformed dimension, 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 (raw.get('dimensions') or {}).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 (raw.get('names') or {}).items()},
    )

Symbols(schema, fmt, table) #

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

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.

RAISES DESCRIPTION
SchemaError

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

Source code in src/math_spec/typesetting/symbols.py
def __init__(self, schema: _ExpandedSpec, fmt: Format, table: SymbolTable) -> None:
    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)
    printed = printed_expressions(schema)
    chosen = frozenset(schema.variables) | chosen_expressions(schema)
    names = (*schema.parameters, *schema.variables, *printed)
    declared = frozenset(names)

    #: Names whose symbol came from the table rather than the derivation;
    #: the convention note quotes only the others, a table being free to
    #: map a parameter to an italic symbol.
    self.overridden = frozenset(table.names) & declared
    self.name: dict[str, str] = {
        name: table.names[name]
        if name in table.names
        else _derive_name_symbol(name, declared, fmt, given=name not in chosen)
        for name in names
    }
    spoken_for = {s for s in self.name.values() if len(s) == 1}

    self.index: dict[str, str] = {}
    self.set: dict[str, str] = {}
    taken_index, taken_set = set(spoken_for), set()
    for dim in schema.dimensions:
        overridden = dim in table.indices
        letter = table.indices[dim] if overridden else _first_free(_index_candidates(dim), taken_index)
        taken_index.add(letter)
        self.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)
        self.set[dim] = table.sets[dim] if dim in table.sets else fmt.script(upper)

index = {} instance-attribute #

name = {name: table.names[name] if name in table.names else _derive_name_symbol(name, declared, fmt, given=name not in chosen) for name in names} instance-attribute #

overridden = frozenset(table.names) & declared instance-attribute #

set = {} instance-attribute #

chosen_expressions(schema) #

The cased 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 a second cased expression's arms too, since :func:~math_spec.expression_of expands those where the name stood.

Source code in src/math_spec/typesetting/symbols.py
def chosen_expressions(schema: _ExpandedSpec) -> frozenset[str]:
    """The cased 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 a second cased expression's arms too, since
    :func:`~math_spec.expression_of` expands those where the name stood.
    """
    namespace = Namespace.of(schema)
    return frozenset(
        name
        for name in printed_expressions(schema)
        if any(
            carries_variable(expression_of(text, schema, namespace, f"expression '{name}', {where}"))
            for text, where in _values_of(schema.expressions[name])
        )
    )

printed_expressions(schema) #

The named expressions that print under their own name, in declaration order.

A named expression is substituted where it is used, so it normally prints nothing a symbol could stand for. A cased one is the exception: it prints as a definition of its own, which the equations using it name. The order is the file's, because the definitions print in it.

Source code in src/math_spec/typesetting/symbols.py
def printed_expressions(schema: _ExpandedSpec) -> tuple[str, ...]:
    """The named expressions that print under their own name, in declaration order.

    A named expression is substituted where it is used, so it normally prints
    nothing a symbol could stand for. A **cased** one is the exception: it
    prints as a definition of its own, which the equations using it name. The
    order is the file's, because the definitions print in it.
    """
    return tuple(name for name, block in schema.expressions.items() if block.cases)