Skip to content

Python API#

This page documents every name that import mathspec exports, grouped by task.

Loading#

The module mathspec.program holds the classes a Program is made of. The Program API documents them.

mathspec.to_spec(spec) #

Load and validate a spec definition — the language's front door.

Everything decidable without data is decided here: schema shape, every rule one declaration is held to against the others, every expression and where string, and every macro template.

PARAMETER DESCRIPTION
spec

A YAML path — a Path, or a str with no newline in it — the YAML text itself as a str with one, a mapping, or a loaded Spec.

TYPE: str | Path | Mapping[str, object] | Spec

RETURNS DESCRIPTION
Spec

The schema as the file declares it, piecewise: intact.

RAISES DESCRIPTION
LanguageError

Anything the language does not accept, a text that is not a mapping of sections included.

FileNotFoundError

A str with no newline that names no file.

Source code in src/mathspec/validation.py
def to_spec(spec: str | Path | Mapping[str, object] | Spec) -> Spec:
    """Load and validate a spec definition — the language's front door.

    Everything decidable without data is decided here: schema shape, every
    rule one declaration is held to against the others, every expression and
    where string, and every macro template.

    Args:
        spec: A YAML path — a [`Path`][], or a ``str`` with no
            newline in it — the YAML text itself as a ``str`` with one, a
            mapping, or a loaded [`Spec`][].

    Returns:
        The schema *as the file declares it*, ``piecewise:`` intact.

    Raises:
        LanguageError: Anything the language does not accept, a text that is
            not a mapping of sections included.
        FileNotFoundError: A ``str`` with no newline that names no file.
    """
    if isinstance(spec, (list, tuple)):
        msg = 'a spec is one file, one dict or one Spec, never a list of them; merge the declarations into one dict.'
        raise SchemaError(msg)
    if isinstance(spec, Spec):
        return spec
    return Spec.model_validate(spec if isinstance(spec, Mapping) else read_spec(spec))

mathspec.Spec #

Bases: _StrictBlock

The declared math — one YAML file, or one dict, validated. Nothing here has seen data.

A Spec that exists has passed the whole language: constructing one by any route — to_spec, model_validate, the constructor — runs every load-time check, expression pass included, and raises LanguageError on a spec the language refuses. Holding one is the proof, so nothing downstream checks it again.

The API is the twelve declaration sections plus version and description, three ways back out — to_dict for the spec as data, to_yaml for the file a reviewer reads, expand for the spec with its formulations written out as plain rows — and program, the spec typed, which every reader after load walks. Everything else on this class is pydantic's, not a contract this package keeps.

assumptions = {} class-attribute instance-attribute #

constraints = {} class-attribute instance-attribute #

description = None class-attribute instance-attribute #

dimensions = {} class-attribute instance-attribute #

expressions = {} class-attribute instance-attribute #

given = GivenBlock() class-attribute instance-attribute #

macros = {} class-attribute instance-attribute #

objective = None class-attribute instance-attribute #

parameters = {} class-attribute instance-attribute #

piecewise = {} class-attribute instance-attribute #

program cached property #

This spec typed, section for section — what every reader after load walks.

Computing it is the expression pass, so a spec the language refuses raises here; loading forces it, so every ask on a spec in hand is the one object. It mirrors the spec: a piecewise: block still in it is a curve under program.piecewise and a sos: block a set under program.sos, and expand is what writes either out as rows, so a consumer building rows reads spec.expand(...).program and refuses a block it does not take.

relations = {} class-attribute instance-attribute #

sos = {} class-attribute instance-attribute #

variables = {} class-attribute instance-attribute #

version = 0 class-attribute instance-attribute #

expand(*kinds) #

This spec with its formulations written out as plain variables and constraints.

A formulation states rows rather than being one — piecewise: states a curve, sos: states which members of a family may be nonzero. Expanding one writes those rows under names prefixed with the block's own, and drops the block. The result is a different spec: it declares more variables and constraints, so it does not compare equal to this one. It declares the same dimensions and parameters, so the same data attaches to both. Nothing is cached, so a second call builds the expansion again.

PARAMETER DESCRIPTION
kinds

Which formulations to write out — 'piecewise', 'sos', or none of them for every one. They go in that order whatever order they are asked in, because a method: sos2 curve emits a set and no set emits a curve.

TYPE: Formulation DEFAULT: ()

RETURNS DESCRIPTION
Spec

The spec with those blocks written out, or this same object where

Spec

it declares none of them, so an expansion asked for the same kinds

Spec

again returns itself. It is a spec like any other: to_yaml

Spec

writes it, and program holds its rows.

RAISES DESCRIPTION
ValueError

kinds names something that is not a formulation.

Source code in src/mathspec/spec.py
def expand(self, *kinds: Formulation) -> Spec:
    """This spec with its formulations written out as plain variables and constraints.

    A formulation states rows rather than being one — ``piecewise:`` states
    a curve, ``sos:`` states which members of a family may be nonzero.
    Expanding one writes those rows under names prefixed with the block's
    own, and drops the block. The result is a different spec: it declares
    more variables and constraints, so it does not compare equal to this
    one. It declares the same dimensions and parameters, so the same data
    attaches to both. Nothing is cached, so a second call builds the
    expansion again.

    Args:
        kinds: Which formulations to write out — ``'piecewise'``,
            ``'sos'``, or none of them for every one. They go in that
            order whatever order they are asked in, because a
            ``method: sos2`` curve emits a set and no set emits a curve.

    Returns:
        The spec with those blocks written out, or this same object where
        it declares none of them, so an expansion asked for the same kinds
        again returns itself. It is a spec like any other: [`to_yaml`][]
        writes it, and [`program`][] holds its rows.

    Raises:
        ValueError: *kinds* names something that is not a formulation.
    """
    wanted = _formulations(kinds)
    from mathspec.piecewise import expand_piecewise
    from mathspec.sos import expand_sets

    expanded = expand_piecewise(self) if 'piecewise' in wanted else self
    if 'sos' in wanted and expanded.sos:
        expanded = expand_sets(expanded)
    return expanded

model_validate(obj, *, strict=None, extra=None, from_attributes=None, context=None, by_alias=None, by_name=None) classmethod #

Validate a mapping, raising this package's exception tree rather than pydantic's.

__init__ is not wrapped the same way, because defining one makes pydantic run every after-validator twice.

Source code in src/mathspec/spec.py
@classmethod
@override
def model_validate(
    cls,
    obj: object,
    *,
    strict: bool | None = None,
    extra: ExtraValues | None = None,
    from_attributes: bool | None = None,
    context: object = None,
    by_alias: bool | None = None,
    by_name: bool | None = None,
) -> Self:
    """Validate a mapping, raising this package's exception tree rather than pydantic's.

    ``__init__`` is not wrapped the same way, because defining one makes
    pydantic run every after-validator twice.
    """
    try:
        return super().model_validate(
            obj,
            strict=strict,
            extra=extra,
            from_attributes=from_attributes,
            context=context,
            by_alias=by_alias,
            by_name=by_name,
        )
    except ValidationError as exc:
        raise schema_error(exc) from None

to_dict() #

The spec as plain data. to_spec(m.to_dict()) reproduces it.

Source code in src/mathspec/spec.py
def to_dict(self) -> dict[str, object]:
    """The spec as plain data. ``to_spec(m.to_dict())`` reproduces it."""
    return self.model_dump()

to_yaml(*, canonical=False) #

The file a reviewer reads — including for a spec that never had one.

PARAMETER DESCRIPTION
canonical

Write the normal form instead: declarations sorted by name, every expression printed from its parsed tree, one term of a sum per line. Two files that state the same spec write the same text, so what a diff shows is a difference in the spec. The normal form loads to the same spec and not to an equal Spec, a reprinted expression being a different string.

TYPE: bool DEFAULT: False

Source code in src/mathspec/spec.py
def to_yaml(self, *, canonical: bool = False) -> str:
    """The file a reviewer reads — including for a spec that never had one.

    Args:
        canonical: Write the normal form instead: declarations sorted by
            name, every expression printed from its parsed tree, one term
            of a sum per line. Two files that state the same spec write
            the same text, so what a diff shows is a difference in the
            spec. The normal form loads to the same spec and not to an
            equal [`Spec`][mathspec.spec.Spec], a reprinted expression
            being a different string.
    """
    import yaml

    if canonical:
        from mathspec.canonical import canonical_yaml

        return canonical_yaml(self)
    return yaml.safe_dump(self.to_dict(), sort_keys=False, allow_unicode=True)

Composing#

Compose a spec from several files shows both in use.

mathspec.merge(fragments, description=None) #

fragments composed as peers, each owning the math it declares.

PARAMETER DESCRIPTION
fragments

Each fragment as a YAML path, YAML text, a mapping, or a loaded Spec. A sum and the objective write their terms in the order of the list.

TYPE: Sequence[Source]

description

What the composed spec is. A fragment's own description is about the fragment, and is not carried.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Spec

The composed spec, loaded. A given declaration a sibling introduces is

Spec

folded away; one nothing introduces stays under given:.

RAISES DESCRIPTION
LanguageError

A fragment does not load on its own; two fragments declare one name; two fragments say different things about one dimension, relation or given declaration; a fragment reads a name as something other than what its sibling introduces, as another kind of thing, or over fewer dimensions than its body carries; a fragment adds a term to a variable, a parameter, a constraint or a definition written as cases:; a term reads its own sum through another fragment; a name no fragment defines is read by nothing but the fragments that add a term to it, or by one fragment alone; the readers of such a name write its dims in different orders; two fragments are written against different language versions; their objectives run opposite ways; or the composed spec does not load.

FileNotFoundError

A str with no newline that names no file.

TypeError

fragments is one path rather than a list.

Source code in src/mathspec/composition.py
def merge(fragments: Sequence[Source], description: str | None = None) -> Spec:
    """*fragments* composed as peers, each owning the math it declares.

    Args:
        fragments: Each fragment as a YAML path, YAML text, a mapping, or a
            loaded [`Spec`][mathspec.spec.Spec]. A sum and the objective write
            their terms in the order of the list.
        description: What the composed spec is. A fragment's own
            ``description`` is about the fragment, and is not carried.

    Returns:
        The composed spec, loaded. A given declaration a sibling introduces is
        folded away; one nothing introduces stays under ``given:``.

    Raises:
        LanguageError: A fragment does not load on its own; two fragments
            declare one name; two fragments say different things about one
            dimension, relation or given declaration; a fragment reads a name as
            something other than what its sibling introduces, as another kind
            of thing, or over fewer dimensions than its body carries; a fragment
            adds a term to a variable, a parameter, a constraint or a
            definition written as ``cases:``; a term reads its own sum through
            another fragment; a name no fragment defines is read by nothing
            but the fragments that add a term to it, or by one fragment alone;
            the readers of such a name write its dims in different orders; two
            fragments are written against different language versions; their
            objectives run opposite ways; or the composed spec does not load.
        FileNotFoundError: A ``str`` with no newline that names no file.
        TypeError: *fragments* is one path rather than a list.
    """
    loaded = {name: _fragment(name, fragment) for name, fragment in _labelled(fragments, 'fragments').items()}
    read = {name: spec.to_dict() for name, spec in loaded.items()}
    merged: dict[str, object] = {'version': _one_version(read)}
    if description is not None:
        merged['description'] = description
    for section in SHARED_SECTIONS:
        if agreed := _agreed(read, section, _singular(section), 'give one of them a name of its own'):
            merged[section] = agreed
    asked = {name: spec.given.model_dump(exclude_unset=True) for name, spec in loaded.items()}
    readings = {
        kind: _agreed(asked, kind, label, 'read it over one frame', claims=_reading_claims)
        for kind, label in GIVEN_KINDS.items()
    }
    for section in OWNED_SECTIONS:
        if claimed := _claimed(read, section):
            merged[section] = claimed
    summed = _summed(loaded, merged, readings['expressions'], read)
    if expressions := {**_mapping(merged.get('expressions')), **summed}:
        merged['expressions'] = {key: _without(block, 'adds_to') for key, block in expressions.items()}
    if given := _folded(read, merged, loaded, readings):
        merged['given'] = given
    if (objective := _summed_objective(read)) is not None:
        merged['objective'] = objective
    return to_spec(merged)

mathspec.override(base, patches) #

base with each patch laid over it in turn.

PARAMETER DESCRIPTION
base

The spec being extended: a YAML path, YAML text, a mapping, or a loaded Spec.

TYPE: Source

patches

Each patch as a YAML path, YAML text, a mapping, or a loaded Spec. Each is laid on the base with every earlier patch laid on it, so a later patch wins a field an earlier one writes.

TYPE: Sequence[Source]

RETURNS DESCRIPTION
Spec

The patched spec, loaded.

RAISES DESCRIPTION
LanguageError

The base does not load; the patched spec does not load; a patch edits or removes a declaration its base does not declare; a patch creates one that is not whole; a patch redeclares or removes a dimension or a relation; or a patch sets a whole section to null.

FileNotFoundError

A str with no newline that names no file.

TypeError

patches is one path rather than a list.

Source code in src/mathspec/composition.py
def override(base: Source, patches: Sequence[Source]) -> Spec:
    """*base* with each patch laid over it in turn.

    Args:
        base: The spec being extended: a YAML path, YAML text, a mapping, or a
            loaded [`Spec`][mathspec.spec.Spec].
        patches: Each patch as a YAML path, YAML text, a mapping, or a loaded
            [`Spec`][mathspec.spec.Spec]. Each is laid on the base with every
            earlier patch laid on it, so a later patch wins a field an earlier
            one writes.

    Returns:
        The patched spec, loaded.

    Raises:
        LanguageError: The base does not load; the patched spec does not
            load; a patch edits or removes a declaration its base does not
            declare; a patch creates one that is not whole; a patch redeclares
            or removes a dimension or a relation; or a patch sets a whole
            section to ``null``.
        FileNotFoundError: A ``str`` with no newline that names no file.
        TypeError: *patches* is one path rather than a list.
    """
    read = {name: _declarations(patch) for name, patch in _labelled(patches, 'patches').items()}
    result = to_spec(base).to_dict()
    for name, patch in read.items():
        result = _lay_over(result, deepcopy(patch), name)
    return to_spec(result)

Typesetting#

mathspec.to_latex(spec, **options) #

Render spec as LaTeX (amsmath align). See typeset.

Source code in src/mathspec/typesetting/__init__.py
def to_latex(spec: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
    """Render *spec* as LaTeX (amsmath ``align``). See [`typeset`][]."""
    return typeset(spec, 'latex', **options)

mathspec.to_typst(spec, **options) #

Render spec as Typst. See typeset.

Source code in src/mathspec/typesetting/__init__.py
def to_typst(spec: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
    """Render *spec* as Typst. See [`typeset`][]."""
    return typeset(spec, 'typst', **options)

mathspec.to_markdown(spec, **options) #

Render spec as GitHub-flavoured Markdown. See typeset.

Source code in src/mathspec/typesetting/__init__.py
def to_markdown(spec: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
    """Render *spec* as GitHub-flavoured Markdown. See [`typeset`][]."""
    return typeset(spec, 'markdown', **options)

mathspec.typeset(spec, fmt, *, symbols=None, standalone=False, legend=True, numbered=True, inline_expressions=False) #

Render spec's math in fmt.

PARAMETER DESCRIPTION
spec

Anything mathspec.to_spec accepts, or a Program. A Spec or a Program is rendered as it stands, so printing one spec in several formats reads and checks the file once rather than once per format, and a curve prints as the curve it states. Pass spec.expand() for the rows a solver holds instead.

TYPE: str | Path | Mapping[str, object] | Spec | Program

fmt

What spells the math — a key of FORMATS.

TYPE: FormatName

symbols

How names print, as a SymbolTable, a path or a mapping. Names it does not carry are derived, and it must be written in fmt's notation.

TYPE: str | Path | Mapping[str, object] | SymbolTable | None DEFAULT: None

standalone

Emit a compilable document rather than a fragment.

TYPE: bool DEFAULT: False

legend

Prepend the sets/parameters/variables table. The spec's own description: opens the document either way — it is what the file says it is, not a symbol table.

TYPE: bool DEFAULT: True

numbered

Number the equations.

TYPE: bool DEFAULT: True

inline_expressions

Substitute each plain named expression into the equations that use it, rather than printing its symbol there and its definition once. A cased expression is a definition either way: its block is taller than the line it would sit in.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
str

The rendered text.

RAISES DESCRIPTION
ValueError

fmt names no format.

LanguageError

A spec that does not compile; it does not print.

SchemaError

A symbol table entry naming nothing in the spec, or a table written in a notation fmt does not read.

Source code in src/mathspec/typesetting/__init__.py
def typeset(
    spec: str | Path | Mapping[str, object] | Spec | Program,
    fmt: FormatName,
    *,
    symbols: str | Path | Mapping[str, object] | SymbolTable | None = None,
    standalone: bool = False,
    legend: bool = True,
    numbered: bool = True,
    inline_expressions: bool = False,
) -> str:
    """Render *spec*'s math in *fmt*.

    Args:
        spec: Anything [`mathspec.to_spec`][] accepts, or a
            [`Program`][]. A ``Spec`` or a ``Program``
            is rendered as it stands, so printing one spec in several formats
            reads and checks the file once rather than once per format, and a
            curve prints as the curve it states. Pass ``spec.expand()`` for the rows a solver holds
            instead.
        fmt: What spells the math — a key of [`FORMATS`][].
        symbols: How names print, as a [`SymbolTable`][], a path or a
            mapping. Names it does not carry are derived, and it must be
            written in *fmt*'s notation.
        standalone: Emit a compilable document rather than a fragment.
        legend: Prepend the sets/parameters/variables table. The spec's own
            ``description:`` opens the document either way — it is what the
            file says it is, not a symbol table.
        numbered: Number the equations.
        inline_expressions: Substitute each plain named expression into the equations that
            use it, rather than printing its symbol there and its definition
            once. A cased expression is a definition either way: its block is
            taller than the line it would sit in.

    Returns:
        The rendered text.

    Raises:
        ValueError: *fmt* names no format.
        LanguageError: A spec that does not compile; it does not print.
        SchemaError: A symbol table entry naming nothing in the spec, or a
            table written in a notation *fmt* does not read.
    """
    walk = _walk(spec, fmt, symbols, inline_expressions=inline_expressions)
    program, format_ = walk.program, walk.format

    rendered = [
        format_.section(title, format_.equations(lines, numbered=numbered))
        for title, lines in walk.equations()
        if lines
    ]

    blocks = [format_.note(format_.escape(program.description))] if program.description else []
    if legend:
        explained, noticed = Legend(program, walk.symbols, format_), notice(program)
        blocks += [
            format_.section(title, format_.glossary(entries))
            for title, entries in explained.glossaries(noticed, walk.defined())
        ]
        blocks += [format_.note(text) for text in explained.convention_notes()]
        blocks += [format_.note(text) for text in explained.translation_notes(noticed)]
        blocks += [format_.note(text) for text in explained.position_notes(noticed)]
    return format_.document([*blocks, *rendered], standalone=standalone)

mathspec.typeset_declaration(spec, name, fmt, *, symbols=None, inline_expressions=True) #

Render one declaration as the bare line the document prints for it.

The line the whole-spec render prints for it — a named expression's definition, a constraint, an assumption, a piecewise: curve, or a variable's domain, quantifier included — with no document, label, equation number or math delimiters around it, for a math context the caller lays out: a docstring, a table cell. A line on its own has no Definitions section beside it, so the plain named expressions it uses are substituted unless inline_expressions says otherwise; a cased one prints by symbol, and a second call with its name prints its block.

PARAMETER DESCRIPTION
spec

Anything mathspec.to_spec accepts, or a Program.

TYPE: str | Path | Mapping[str, object] | Spec | Program

name

A named expression, constraint, assumption, piecewise: block or variable the spec declares.

TYPE: str

fmt

What spells the math — a key of FORMATS.

TYPE: FormatName

symbols

How names print; see typeset.

TYPE: str | Path | Mapping[str, object] | SymbolTable | None DEFAULT: None

inline_expressions

Substitute the plain named expressions the line uses, so it stands on its own; False prints their symbols, as the document does. A plain expression asked for by name prints its definition either way.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str

The line, math only.

RAISES DESCRIPTION
ValueError

fmt names no format.

LanguageError

A spec that does not compile; it does not print.

SchemaError

name is declared as none of the five, as two — a constraint may share a variable's name — or under given:, which prints in the legend rather than as a line; or a symbol table entry names nothing in the spec.

Source code in src/mathspec/typesetting/__init__.py
def typeset_declaration(
    spec: str | Path | Mapping[str, object] | Spec | Program,
    name: str,
    fmt: FormatName,
    *,
    symbols: str | Path | Mapping[str, object] | SymbolTable | None = None,
    inline_expressions: bool = True,
) -> str:
    """Render one declaration as the bare line the document prints for it.

    The line the whole-spec render prints for it — a named expression's
    definition, a constraint, an assumption, a ``piecewise:`` curve, or a
    variable's domain, quantifier included —
    with no document, label, equation number or math delimiters around it, for
    a math context the caller lays out: a docstring, a table cell. A line on
    its own has no Definitions section beside it, so the plain named
    expressions it uses are substituted unless *inline_expressions* says otherwise; a cased
    one prints by symbol, and a second call with its name prints its block.

    Args:
        spec: Anything [`mathspec.to_spec`][] accepts, or a [`Program`][].
        name: A named expression, constraint, assumption, ``piecewise:``
            block or variable the spec declares.
        fmt: What spells the math — a key of [`FORMATS`][].
        symbols: How names print; see [`typeset`][].
        inline_expressions: Substitute the plain named expressions the line uses, so it
            stands on its own; ``False`` prints their symbols, as the document
            does. A plain expression asked for by name prints its definition
            either way.

    Returns:
        The line, math only.

    Raises:
        ValueError: *fmt* names no format.
        LanguageError: A spec that does not compile; it does not print.
        SchemaError: *name* is declared as none of the five, as two — a
            constraint may share a variable's name — or under ``given:``, which
            prints in the legend rather than as a line; or a symbol table entry
            names nothing in the spec.
    """
    walk = _walk(spec, fmt, symbols, inline_expressions=inline_expressions)
    given = walk.program.given
    givens = {
        'parameter': given.parameters,
        'variable': given.variables,
        'expression': given.expressions,
        'constraint': given.constraints,
    }
    given_kind = next((kind for kind, group in givens.items() if name in group), None)
    if given_kind is not None:
        msg = (
            f"'{name}' is a given {given_kind}, and a given declaration prints no line of its own — "
            f"this file reads it and does not build it. It prints in the legend, under 'Given', "
            f'so call typeset() for the whole spec.'
        )
        raise SchemaError(msg)
    return walk.format.equation(walk.line(name))

mathspec.FORMATS = {'latex': LatexFormat(), 'markdown': MarkdownFormat(), 'typst': TypstFormat()} module-attribute #

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

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

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 spec is an error naming the near miss.

ATTRIBUTE DESCRIPTION
notation

The language the entries are written in; 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 spec: the blocks as the file states them, and the rows expand writes out.

Source code in src/mathspec/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 spec: the blocks as the file states
    them, and the rows [`expand`][mathspec.spec.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/mathspec/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()},
    )

Advice#

mathspec.advice(spec) #

Everything the language advises about spec, decided without data.

Advice is a note, not a refusal: a file with advice still loads.

PARAMETER DESCRIPTION
spec

Anything to_spec accepts, or a Program, read as it arrived. A piecewise: or sos: block is read as the rows it states, so the answer is the one its expansion gets, with nothing expanded.

TYPE: str | Path | Mapping[str, object] | Spec | Program

RETURNS DESCRIPTION
Advice

The never-an-axis advice in declaration order, then one note per

...

declaration the program reads and does not build, then the

tuple[Advice, ...]

unboundedness advice; str() of each is its sentence.

RAISES DESCRIPTION
LanguageError

spec does not load; to_spec says why.

FileNotFoundError

A str with no newline that names no file.

Source code in src/mathspec/advising.py
def advice(spec: str | Path | Mapping[str, object] | Spec | Program) -> tuple[Advice, ...]:
    """Everything the language advises about *spec*, decided without data.

    Advice is a note, not a refusal: a file with advice still loads.

    Args:
        spec: Anything [`to_spec`][] accepts, or a [`Program`][], read as
            it arrived. A ``piecewise:`` or ``sos:`` block is read as the rows
            it states, so the answer is the one its expansion gets, with
            nothing expanded.

    Returns:
        The never-an-axis advice in declaration order, then one note per
        declaration the program reads and does not build, then the
        unboundedness advice; ``str()`` of each is its sentence.

    Raises:
        LanguageError: *spec* does not load; [`to_spec`][] says why.
        FileNotFoundError: A ``str`` with no newline that names no file.
    """
    program = spec if isinstance(spec, Program) else to_spec(spec).program
    return tuple(_never_an_axis(program) + _given(program) + unbounded_notes(program))

mathspec.Advice(kind, subject, text) dataclass #

One thing the language advises about a file it accepts.

Never an error: each is what a half-written spec looks like too. A consumer prints it, or filters on kind and subject; the text is the language's, so no consumer writes its own.

ATTRIBUTE DESCRIPTION
kind

The pass that said it.

TYPE: AdviceKind

subject

The declaration it is about — a dimension name, a variable name.

TYPE: str

text

The sentence, naming the rewrite.

TYPE: str

kind instance-attribute #

subject instance-attribute #

text instance-attribute #

mathspec.AdviceKind = Literal['never-an-axis', 'given', 'unbounded'] module-attribute #

Errors#

mathspec.MathSpecError #

Bases: ValueError

Base class for every error this package raises on purpose.

mathspec.LanguageError #

Bases: MathSpecError

The spec is not sayable in the language, or does not obey its rules.

mathspec.SchemaError #

Bases: LanguageError

What a load refuses: an unknown key, a bad dtype, a duplicate YAML key, an unparseable or unresolvable expression.

mathspec.DimensionError #

Bases: LanguageError

A dim-set rule was violated. Raised at load time, before any data.

Names#

mathspec.BUILTIN_NAMES = frozenset(BUILTINS) module-attribute #

mathspec.did_you_mean(name, known, *, label='Declared', listing=True) #

The repair clause for an unrecognised name: the near miss, or the set, or nothing where listing is off.

Source code in src/mathspec/errors.py
def did_you_mean(name: str, known: Iterable[str], *, label: str = 'Declared', listing: bool = True) -> str:
    """The repair clause for an unrecognised name: the near miss, or the set, or nothing where *listing* is off."""
    candidates = sorted(known)
    near = difflib.get_close_matches(name, candidates, n=1, cutoff=0.6)
    if near:
        return f"Did you mean '{near[0]}'?"
    return f'{label}: {", ".join(candidates) or "nothing"}.' if listing else ''