Skip to content

math_spec.piecewise

Expand piecewise: blocks into plain variables and constraints.

A block becomes ordinary affine declarations before anything reads the model, under names prefixed with the block's own; what each method emits is tabled in docs/reference/language/piecewise.md. Every rule a block is held to is decided at load, before this runs: the names it references in :class:~math_spec.model.Spec, its links where every expression is typed, and its frame in :func:curve_frame.

Emitted(name, lam, convexity, set, chord, domain_lo, domain_hi, links, assumptions) dataclass #

Every name one block's expansion may write, spelled once for the emitter and the collision check.

The set a block states writes names of its own, and they are reserved whichever method the block declares: which of the two write them is the method's business, and a collision is the file's either way.

assumptions instance-attribute #

by_kind property #

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

chord instance-attribute #

convexity instance-attribute #

domain_hi instance-attribute #

domain_lo instance-attribute #

lam instance-attribute #

name instance-attribute #

set instance-attribute #

of(name, pw) classmethod #

The names block name writes.

Source code in src/math_spec/piecewise.py
@classmethod
def of(cls, name: str, pw: PiecewiseBlock) -> Emitted:
    """The names block *name* writes."""
    return cls(
        name,
        f'{name}_lam',
        f'{name}_convexity',
        sos.Emitted.of(name, 2),
        f'{name}_chord',
        f'{name}_domain_lo',
        f'{name}_domain_hi',
        tuple(f'{name}_link{i}' for i in range(len(pw.links))),
        tuple(assumptions_of(name, pw)),
    )

assumptions_of(block, pw) #

What block assumes of its numbers, by the name the document prints and a refusal quotes.

Every curve assumes its breakpoints are there: a missing parameter row is not absence, it is a zero, so an undeclared breakpoint sits the curve on the origin rather than shortening it. A curve has an x-axis only where two links tie it, so the increasing condition — and the shape it is checked with — exist only there; lp alone needs a segment to state a line for; a mask must be one run.

Read off the block rather than off an expansion, so a model states what it assumes whether or not its curves have been written out. Each condition is an assumptions: entry over the parameters the file declared, its description naming the method and the rewrite that takes a curve of any shape: the expansion writes them into the model, and a model that still declares the block resolves the same entries at load.

Source code in src/math_spec/piecewise.py
def assumptions_of(block: str, pw: PiecewiseBlock) -> dict[str, AssumptionBlock]:
    """What *block* assumes of its numbers, by the name the document prints and a refusal quotes.

    Every curve assumes its breakpoints are there: a missing parameter row is
    not absence, it is a zero, so an undeclared breakpoint sits the curve on
    the origin rather than shortening it. A curve has an x-axis only where two
    links tie it, so the increasing condition — and the shape it is checked
    with — exist only there; ``lp`` alone needs a segment to state a line for;
    a mask must be one run.

    Read off the block rather than off an expansion, so a model states what it
    assumes whether or not its curves have been written out. Each condition is
    an ``assumptions:`` entry over the parameters the file declared, its
    ``description`` naming the method and the rewrite that takes a curve of any
    shape: the expansion writes them into the model, and a model that still
    declares the block resolves the same entries at load.
    """
    d, mask = pw.over, pw.points
    assumed: dict[str, AssumptionBlock] = {}
    assumed[f'{block}_complete'] = AssumptionBlock(
        holds=' AND '.join(dict.fromkeys(link.values for link in pw.links)),
        where=mask,
        description=f"piecewise '{block}': every breakpoint the curve runs through needs a row in "
        f'{_quoted(link.values for link in pw.links)} — a missing row is read as a zero rather than as a '
        f'shorter curve, so it sits the curve on the origin. '
        + (
            f"Bind the rows, or narrow points: '{mask}' to where the curve runs."
            if mask is not None
            else 'Bind the rows, or declare points: to say how far the curve runs.'
        ),
    )
    curvature = _curvature_required(pw)
    if curvature is not None:
        x, y = (link.values for link in pw.curve)
        assumed[f'{block}_increasing'] = AssumptionBlock(
            holds=f'{_back(x, d, 1)} < {x}',
            where=_neighbours(d, mask),
            description=f"piecewise '{block}': method: {pw.method} requires strictly increasing breakpoints in '{x}' along '{d}'",
        )
        assumed[f'{block}_curvature'] = _bends(block, pw, x, y, curvature)
    if pw.method == 'lp':
        assumed[f'{block}_breakpoints'] = AssumptionBlock(
            holds=f'count({mask or pw.curve[0].values}, over={d}) >= 2',
            description=f"piecewise '{block}': method: lp needs at least two breakpoints per curve — the method *is* its "
            f'segment lines, so a curve with no segment states nothing and leaves the bounded link on its own '
            f'bound. Use method: adjacency, sos2 or convex, which pin it to the points it does have.',
        )
    if mask is not None:
        assumed[f'{block}_contiguous'] = AssumptionBlock(
            holds=f'count({_edge(d, mask, "first")}, over={d}) == 1',
            description=f"piecewise '{block}': points: '{mask}' must mark a consecutive run of at least one breakpoint per "
            f'curve — {_GAP[pw.method]}.',
        )
    return assumed

curve_frame(schema, name, pw, links) #

The dimensions block name builds one curve per coordinate of: every one its links and its gate carry.

In declaration order, because iterating a set would vary the emitted dims — and every column index behind it — per process. links are the block's link expressions typed, as :attr:~math_spec.resolution.Resolved.piecewise holds them.

RAISES DESCRIPTION
DimensionError

A link or the gate carries the breakpoint dimension, or a values or points: parameter varies along a dimension no link expression carries.

Source code in src/math_spec/piecewise.py
def curve_frame(schema: Spec, name: str, pw: PiecewiseBlock, links: Iterable[Expression]) -> tuple[str, ...]:
    """The dimensions block *name* builds one curve per coordinate of: every one its links and its gate carry.

    In declaration order, because iterating a set would vary the emitted
    ``dims`` — and every column index behind it — per process. *links* are the
    block's link expressions typed, as
    :attr:`~math_spec.resolution.Resolved.piecewise` holds them.

    Raises:
        DimensionError: A link or the gate carries the breakpoint dimension, or
            a values or ``points:`` parameter varies along a dimension no link
            expression carries.
    """
    context = f"piecewise '{name}'"
    carried = [(f'link {i} expression', dims_of(node, schema, f'{context} link {i}')) for i, node in enumerate(links)]
    if pw.activity is not None:
        carried.append(('activity', frozenset(schema.variables[pw.activity].dims)))
    frame: list[str] = []
    for what, found in carried:
        for d in (d for d in schema.dimensions if d in found):
            if d == pw.over:
                raise DimensionError(f"{context}: {what} already carries the breakpoint dim '{pw.over}'")
            if d not in frame:
                frame.append(d)
    for i, link in enumerate(pw.links):
        if stray := [d for d in schema.parameters[link.values].dims if d != pw.over and d not in frame]:
            raise DimensionError(
                f"{context}: link {i} values parameter '{link.values}' carries {stray}, which no link "
                f'expression does — the block builds one curve per coordinate of {frame}, so a curve '
                f'varying along {stray} has nothing to vary against. Declare a link expression over '
                f"it, or drop it from '{link.values}'."
            )
    if pw.points is not None and pw.nominated is None:
        mask = schema.parameters[pw.points].dims
        if stray := [d for d in mask if d != pw.over and d not in frame]:
            raise DimensionError(
                f"{context}: points parameter '{pw.points}' carries {stray}, which the links do not — "
                f"a mask says which of the block's own coordinates exist, and cannot add coordinates"
            )
    return tuple(frame)

declaration_of(pw) #

The curve of one expanded block, as a program carries it.

Source code in src/math_spec/piecewise.py
def declaration_of(pw: PiecewiseBlock) -> PiecewiseDeclaration:
    """The curve of one expanded block, as a program carries it."""
    return PiecewiseDeclaration(
        over=pw.over,
        method=pw.method,
        breakpoints=tuple(link.values for link in pw.links),
    )

expand_piecewise(schema) #

schema with every piecewise: block written out — schema itself where it declares none.

A method: adjacency block states its restriction as the set method: sos2 states, and then that set is written out here too: the binaries are what the method is, so the model that comes back carries no set of its own (:func:math_spec.sos.emit is where they are spelled).

Source code in src/math_spec/piecewise.py
def expand_piecewise(schema: Spec) -> Spec:
    """*schema* with every ``piecewise:`` block written out — *schema* itself where it declares none.

    A ``method: adjacency`` block states its restriction as the set
    ``method: sos2`` states, and then that set is written out here too: the
    binaries are what the method *is*, so the model that comes back carries no
    set of its own (:func:`math_spec.sos.emit` is where they are spelled).
    """
    if not schema.piecewise:
        return schema

    raw = schema.model_dump()
    raw.setdefault('variables', {})
    raw.setdefault('constraints', {})
    for name, pw in schema.piecewise.items():
        _Block(schema, raw, name, pw).expand()
    raw['piecewise'].clear()
    for name, pw in schema.piecewise.items():
        if pw.method == 'adjacency':
            sos.emit(raw, name)
    expanded = Spec.model_validate(raw)
    expanded._expanded_piecewise = dict(schema.piecewise)
    return expanded