Skip to content

math_spec.merge

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

A component library is a fixed set of templates agreeing on a port and flow convention, and wiring a specific system is rows in a connectivity table rather than generated YAML. What that needs of the language is one function: take the templates and hand back one model, so the whole thing is validated, resolved and lowered exactly once, as a file is.

A fragment is not a :class:~math_spec.model.Spec. It is read as YAML and merged unvalidated, so a template may name what a sibling declares — a shared bus, the flow every component writes into — without being a model on its own. Nothing here resolves a name or checks a dim: the merged mapping goes through :func:~math_spec.validation.to_spec like any other, against one flat namespace, and every rule the language has applies to it there and nowhere else.

Two kinds of declaration, and the split is what merging means:

  • dimensions and lookups are the coordinate space, which templates share on purpose. Declared twice and agreeing, they are one declaration; declared twice and disagreeing, the disagreement is the error.
  • Everything else is the math, which a template owns. Declared twice it is a collision, whichever fragment wrote it second, because two templates claiming one name is the composition being wrong rather than the file.

Names are not rewritten here. Qualified names are their own question (#29), and until they land a library keeps its templates apart by naming them apart — which the collision error above is what enforces.

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

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

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

merge(fragments, description=None) #

Compose fragments into one unvalidated model mapping.

PARAMETER DESCRIPTION
fragments

What each fragment is called, to the fragment — the same str | Path | dict | Spec every other verb takes, a str being a path as it is there. The name is what an error calls it, so it is the template's name rather than a path. Order is the order given, and the merged model's declarations keep it.

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

description

What the composed model is. A fragment's own description is about the fragment, so it is not carried and not joined — the composition is a different thing from its parts and says so itself, or says nothing.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
dict[str, Any]

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

dict[str, Any]

in it has been resolved, name-checked or lowered: merging decides what

dict[str, Any]

the declarations are, and the language decides whether they say

dict[str, Any]

anything.

RAISES DESCRIPTION
LanguageError

Two fragments declare one owned name; two fragments disagree about a shared declaration; two fragments pin different language versions; or the objectives disagree about sense.

Source code in src/math_spec/merge.py
def merge(
    fragments: Mapping[str, str | Path | dict[str, Any] | Spec], description: str | None = None
) -> dict[str, Any]:
    """Compose *fragments* into one unvalidated model mapping.

    Args:
        fragments: What each fragment is called, to the fragment — the same
            ``str | Path | dict | Spec`` every other verb takes, a ``str``
            being a path as it is there. The name is what an error calls it,
            so it is the template's name rather than a path. Order is the
            order given, and the merged model's declarations keep it.
        description: What the *composed* model is. A fragment's own
            ``description`` is about the fragment, so it is not carried and not
            joined — the composition is a different thing from its parts and
            says so itself, or says nothing.

    Returns:
        One mapping, ready for :func:`~math_spec.validation.to_spec`. Nothing
        in it has been resolved, name-checked or lowered: merging decides what
        the declarations *are*, and the language decides whether they say
        anything.

    Raises:
        LanguageError: Two fragments declare one owned name; two fragments
            disagree about a shared declaration; two fragments pin different
            language versions; or the objectives disagree about ``sense``.
    """
    read = {name: _sections(fragment) for name, fragment in fragments.items()}
    merged: dict[str, Any] = {'version': _one_version(read)}
    if description is not None:
        merged['description'] = description

    for section in SHARED_SECTIONS:
        if block := _shared(read, section):
            merged[section] = block
    for section in OWNED_SECTIONS:
        if block := _owned(read, section):
            merged[section] = block
    if (objective := _objective(read)) is not None:
        merged['objective'] = objective
    return merged