Skip to content

math_spec.composition

Several files into one model, before any of them is validated.

Two verbs, and they answer different questions. :func:merge composes peers: fragments that each own part of the math, where a name two of them declare is a collision and the order they are given in means nothing. :func:override lays patches over a base: what a framework ships and a project extends, where a name the patch declares is the point. They compose as override(merge({...}), {...}), which builds the model and then configures the run.

What :func:merge does with each section:

  • A dimension or a relation every fragment may declare, and the ones that do have to say the same thing about it. Prose is not a claim, so two descriptions of one dimension agree, and the first fragment's is carried.
  • Every other declaration is owned. A name two fragments declare is refused, both named.
  • The objectives are summed, each term in parentheses, in the fragments' name order, and the senses have to agree.
  • A given declaration is folded into the declaration that introduces the name, once the reader is checked to say the same as the introducer or less. Two fragments that both only read a name have to read it the same way, and a fragment that declares a name and reads it as well is refused. What no fragment introduces stays under given: for a consumer to bind.

A patch says only what it changes, because declarations are laid over a field at a time::

constraints:
  ramp: {dims: [snapshot, generator, investment_period]}

A patch is not a :class:~math_spec.model.Spec. It is read before validation, so it may carry null where a declaration would go and may name what only its base declares. Nothing here resolves a name or checks a dim: the laid mapping goes through :func:~math_spec.validation.to_spec like any other file.

What a patch may say, and what is refused:

  • A partial entry edits, and a whole one creates. An entry that does not validate as a declaration on its own has to land on one the base declares, and a miss is refused with the near miss named.
  • Sibling patches are disjoint. Two patches writing one field is refused, both named, so the order they are given in never decides a model. Layering is written out as override(override(base, …), …).
  • A patch adjusts the math, not the coordinate space. A dimensions or relations entry may be added or restated word for word, never changed and never removed.
  • A declaration set to null is removed, and a removal of what the base does not declare is refused. The marker is positional: constraints: {ramp: null} removes the constraint, where variables: {p: {where: null}} sets that variable's mask to none, which is a value the schema takes. A whole section set to null is refused, because it removes nothing.
  • given: is laid over one kind at a time, by the same rules as any owned section.

GIVEN_KINDS = {'variables': 'given variable', 'constraints': 'given constraint'} module-attribute #

IRREGULAR = {'piecewise': 'piecewise curve', 'sos': 'special-ordered set', 'objective': 'objective'} module-attribute #

OWNED_SECTIONS = ('parameters', 'variables', 'constraints', 'expressions', 'macros', 'piecewise', 'sos') module-attribute #

SECTIONS = (*SHARED_SECTIONS, *OWNED_SECTIONS, 'given') module-attribute #

SHARED_SECTIONS = ('dimensions', 'relations') module-attribute #

merge(fragments, description=None) #

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

PARAMETER DESCRIPTION
fragments

What each fragment is called, to the fragment: a YAML path, YAML text, a mapping, or a loaded :class:~math_spec.model.Spec. The name is what an error calls it. The order they are given in does not reach the result.

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

description

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

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
dict[str, object]

One mapping, ready for :func:~math_spec.validation.to_spec. Nothing

dict[str, object]

in it has been resolved, name-checked or lowered, and it shares no

dict[str, object]

object with any fragment. A given declaration a sibling introduces is

dict[str, object]

folded away; one nothing introduces stays under given:.

RAISES DESCRIPTION
LanguageError

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; a fragment declares a name and reads it as well; two fragments pin different language versions; or their objectives run opposite ways.

FileNotFoundError

A str with no newline that names no file.

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

    Args:
        fragments: What each fragment is called, to the fragment: a YAML path,
            YAML text, a mapping, or a loaded :class:`~math_spec.model.Spec`.
            The name is what an error calls it. The order they are given in
            does not reach the result.
        description: What the composed model is. A fragment's own
            ``description`` is about the fragment, and is not carried.

    Returns:
        One mapping, ready for :func:`~math_spec.validation.to_spec`. Nothing
        in it has been resolved, name-checked or lowered, and it shares no
        object with any fragment. A given declaration a sibling introduces is
        folded away; one nothing introduces stays under ``given:``.

    Raises:
        LanguageError: 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; a fragment declares a name and reads it as
            well; two fragments pin different language versions; or their
            objectives run opposite ways.
        FileNotFoundError: A ``str`` with no newline that names no file.
    """
    read = {name: deepcopy(_declarations(fragment)) for name, fragment in fragments.items()}
    merged: dict[str, object] = {}
    if (version := _one_version(read)) is not None:
        merged['version'] = version
    if description is not None:
        merged['description'] = description
    for section in SHARED_SECTIONS:
        if agreed := _agreed(read, section, _singular(section)):
            merged[section] = agreed
    for section in OWNED_SECTIONS:
        if claimed := _claimed(read, section):
            merged[section] = claimed
    if given := _folded(read, merged):
        merged['given'] = given
    if (objective := _summed_objective(read)) is not None:
        merged['objective'] = objective
    return merged

override(base, patches) #

base with each patch laid over it, and nothing laid over another patch.

PARAMETER DESCRIPTION
base

The model being extended: a YAML path, YAML text, a mapping, or a loaded :class:~math_spec.model.Spec.

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

patches

What each patch is called, to the patch. The name is what an error calls it. The patches must write disjoint fields, so the order they are given in cannot change the result.

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

RETURNS DESCRIPTION
dict[str, object]

One mapping, ready for :func:~math_spec.validation.to_spec. Nothing

dict[str, object]

in it has been resolved, name-checked or lowered, and it shares no

dict[str, object]

object with base or any patch.

RAISES DESCRIPTION
LanguageError

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; a patch sets a whole section to null; or two patches write one field.

FileNotFoundError

A str with no newline that names no file.

Source code in src/math_spec/composition.py
def override(
    base: str | Path | dict[str, object] | Spec,
    patches: Mapping[str, str | Path | dict[str, object] | Spec],
) -> dict[str, object]:
    """*base* with each patch laid over it, and nothing laid over another patch.

    Args:
        base: The model being extended: a YAML path, YAML text, a mapping, or a
            loaded :class:`~math_spec.model.Spec`.
        patches: What each patch is called, to the patch. The name is what an
            error calls it. The patches must write disjoint fields, so the
            order they are given in cannot change the result.

    Returns:
        One mapping, ready for :func:`~math_spec.validation.to_spec`. Nothing
        in it has been resolved, name-checked or lowered, and it shares no
        object with *base* or any patch.

    Raises:
        LanguageError: 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; a patch sets a whole section
            to ``null``; or two patches write one field.
        FileNotFoundError: A ``str`` with no newline that names no file.
    """
    read = {name: _declarations(patch) for name, patch in patches.items()}
    _disjoint(read)

    result = deepcopy(_declarations(base))
    for name, patch in read.items():
        result = _lay_over(result, deepcopy(patch), name)
    return result