math_spec.merge
Several files into one model, before any of them is validated.
Two verbs, and they obey opposite laws. :func:merge composes peers: a
name two of them declare is a collision, and the order they are given in does
not matter. :func:override layers a base and its patches: a name the
patch declares is the point, and the order is the whole instruction. Neither
is a mode of the other — erroring on a shared name and letting the later one
win cannot both be true of one call.
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:
dimensionsandlookupsare 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.
A patch is not a model either, for the same reason a fragment is not: it is
read before validation, so it may carry null where a declaration would go
and name what only its base declares. That is what keeps the removal marker out
of every file a reviewer reads as a model — nothing in the schema gains a key,
and null never appears in a validated Spec.
IRREGULAR = {'piecewise': 'piecewise curve', 'sos': 'special-ordered set'}
module-attribute
#
OWNED_SECTIONS = ('parameters', 'variables', 'constraints', 'expressions', 'macros', 'piecewise', 'sos')
module-attribute
#
SECTIONS = (*SHARED_SECTIONS, *OWNED_SECTIONS)
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
|
description
|
What the composed model is. A fragment's own
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict[str, Any]
|
One mapping, ready for :func: |
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 |
Source code in src/math_spec/merge.py
override(base, patches)
#
base with each patch laid over it in turn, later winning.
What a framework ships and a project extends. The patch says only what it changes, so a constraint whose dimensions grew is the one line that grew::
flow_out_max: {foreach: [node, tech, carrier, snapshot, investstep]}
A declaration the patch sets to null is removed, which is the
one thing an ordered list of files cannot say for itself: a declaration the
patch does not mention is left alone, so without a marker there is no way
to spell a deletion. The marker is positional and means nothing deeper
down — constraints: {ramp: null} removes the constraint, where
variables: {p: {where: null}} sets that variable's mask to none, which
is an ordinary value the schema already takes.
Everything else is laid over a declaration at a time and a field at a
time: a mapping is merged into the mapping below it and anything else
replaces, so a patch naming one field keeps the rest of the declaration it
lands on. That is what lets a patch be short, and the reason to read the
composed model rather than the patch: :meth:~math_spec.model.Spec.to_yaml
on the result is the artifact a reviewer diffs against the base.
| PARAMETER | DESCRIPTION |
|---|---|
base
|
The model being extended — whatever every other verb takes. |
patches
|
What each patch is called, to the patch, in the order they are laid on. The name is what an error calls it. Order is the instruction, so this is the one verb here where giving the same arguments differently means a different model. |
| RETURNS | DESCRIPTION |
|---|---|
dict[str, Any]
|
One mapping, ready for :func: |
with
|
func: |
| RAISES | DESCRIPTION |
|---|---|
LanguageError
|
A patch removes a declaration its base does not have, named with the near miss — a stale patch, or a section confused for another. |