Skip to content

math_spec.sos

Expand sos: blocks into the binaries and rows that state the same restriction.

A set becomes ordinary declarations under names prefixed with the block's own, the way a piecewise: block becomes weights and rows; what it emits is tabled in docs/reference/language/piecewise.md. An unpicked member is held at zero from both sides, so the rewrite states the same feasible set whatever sign the member takes — what it needs is a coefficient on each side, which a model declaring a set without is refused at load for (:meth:math_spec.model.Spec validates it) rather than here.

Coefficients = tuple[float | str | None, float | str | None] module-attribute #

Emitted(seg, pick, link) dataclass #

Every name one set's expansion writes, spelled once for the emitter and the collision check.

The linking row is named after what it says, which the two orders do not share: order 2 admits a member in either half of one segment, and order 1 admits it alone. below is the same row from underneath, written only where the member may be nonzero below zero.

below property #

The linking row from underneath.

by_kind property #

Each name by the kind of declaration it would collide with.

pick instance-attribute #

seg instance-attribute #

of(name, order) classmethod #

The names set name of order writes.

Source code in src/math_spec/sos.py
@classmethod
def of(cls, name: str, order: SosType) -> Emitted:
    """The names set *name* of *order* writes."""
    admits = 'adjacency' if order == 2 else 'nonzero'
    return cls(f'{name}_seg', f'{name}_pick', f'{name}_{admits}')

coefficients(domain, lower, upper) #

What a member's two linking rows multiply its binary by, None on a side the model leaves open.

The 0 and 1 a binary's domain fixes, which no bounds block carries; otherwise the member's own declared bounds, each a number or the name of a parameter. A parameter is a coefficient like any other: it is what the row multiplies by, and no rewrite needs to know its value. Nothing else is a coefficient: a number below the bound would cap a picked member the set does not cap, and one above it is a looser row than the bound already states.

Source code in src/math_spec/sos.py
def coefficients(domain: str, lower: float | str | None, upper: float | str | None) -> Coefficients:
    """What a member's two linking rows multiply its binary by, ``None`` on a side the model leaves open.

    The 0 and 1 a binary's domain fixes, which no bounds block carries;
    otherwise the member's own declared bounds, each a number or the name of a
    parameter. A parameter is a coefficient like any other: it is what the row
    multiplies by, and no rewrite needs to know its value. Nothing else is a
    coefficient: a number below the bound would cap a picked member the set
    does not cap, and one above it is a looser row than the bound already
    states.
    """
    if domain == 'binary':
        return 0.0, 1.0
    return lower, upper

emit(raw, name) #

Write what the set name states as declarations of raw, and drop the block.

PARAMETER DESCRIPTION
raw

A model as data, mid-expansion, declaring the set and the variable it runs over.

TYPE: dict[str, object]

name

Which set to lower.

TYPE: str

Source code in src/math_spec/sos.py
def emit(raw: dict[str, object], name: str) -> None:
    """Write what the set *name* states as declarations of *raw*, and drop the block.

    Args:
        raw: A model as data, mid-expansion, declaring the set and the variable
            it runs over.
        name: Which set to lower.
    """
    sets = section(raw, 'sos')
    block = sets.pop(name)
    assert isinstance(block, dict), 'a validated model carries each set as a mapping'
    variable, over, order = block['variable'], block['along'], block['type']
    member = section(raw, 'variables')[variable]
    assert isinstance(member, dict), 'a validated model carries each variable as a mapping'
    dims = list(member['dims'])
    emitted = Emitted.of(name, order)

    section(raw, 'variables')[emitted.seg] = {
        'dims': dims,
        **({'where': member['where']} if member.get('where') else {}),
        'domain': 'binary',
        'description': _SEGMENTS[order],
    }
    picked = emitted.seg if order == 1 else f'{emitted.seg} + shift({emitted.seg}, along={over}, offset=1, edge=0)'
    constraints = section(raw, 'constraints')
    constraints[emitted.pick] = {
        'dims': [d for d in dims if d != over],
        'expression': f'sum({emitted.seg}, over={over}) <= 1',
    }
    below, above = _coefficients(member)
    constraints[emitted.link] = {'dims': dims, 'expression': f'{variable} <= {_scaled(above, picked)}'}
    if below != 0.0:
        constraints[emitted.below] = {'dims': dims, 'expression': f'{variable} >= {_scaled(below, picked)}'}

expand_sets(schema) #

schema with every sos: block written out as binaries and the rows that link them.

The curves an expansion wrote out ride along, because a model whose curves are already written out is the one this is usually asked of.

Source code in src/math_spec/sos.py
def expand_sets(schema: Spec) -> Spec:
    """*schema* with every ``sos:`` block written out as binaries and the rows that link them.

    The curves an expansion wrote out ride along, because a model whose
    curves are already written out is the one this is usually asked of.
    """
    raw = schema.model_dump()
    for name in list(schema.sos):
        emit(raw, name)
    return Spec.model_validate(raw)

section(raw, name) #

The name section of the raw model, created empty where the file declares none.

Source code in src/math_spec/sos.py
def section(raw: dict[str, object], name: str) -> dict[str, object]:
    """The *name* section of the raw model, created empty where the file declares none."""
    section = raw.setdefault(name, {})
    assert isinstance(section, dict), f'{name}: is a mapping in a validated model'
    return section