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. A link expression is judged before expansion, so a refusal names the link the file wrote rather than an emitted constraint.

Assumed #

Bases: NamedTuple

One condition a method puts on the numbers, as the language writes it.

holds and where are where strings, resolved like any the file wrote. description is the sentence a refusal quotes, which names the method and the rewrite that takes a curve of any shape.

description instance-attribute #

holds instance-attribute #

where instance-attribute #

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 a where string over the parameters the file declared: the expansion writes them into assumptions:, and a model that still declares the block derives the same text at load.

Source code in src/math_spec/piecewise.py
def assumptions_of(block: str, pw: PiecewiseBlock) -> dict[str, Assumed]:
    """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
    a where string over the parameters the file declared: the expansion writes
    them into ``assumptions:``, and a model that still declares the block
    derives the same text at load.
    """
    d, mask = pw.over, pw.points
    assumed: dict[str, Assumed] = {}
    assumed[f'{block}_complete'] = Assumed(
        ' AND '.join(dict.fromkeys(link.values for link in pw.links)),
        mask,
        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'] = Assumed(
            f'{_back(x, d, 1)} < {x}',
            _neighbours(d, mask),
            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'] = Assumed(
            f'count({mask or pw.curve[0].values}, over={d}) >= 2',
            None,
            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'] = Assumed(
            f'count({_edge(d, mask, "first")}, over={d}) == 1',
            None,
            f"piecewise '{block}': points: '{mask}' must mark a consecutive run of at least one breakpoint per "
            f'curve — the chord row joins a breakpoint to the one before it, and the domain rows sit on the '
            f"curve's own first and last.",
        )
    return assumed

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).

RAISES DESCRIPTION
PiecewiseExpansionError

A block naming something that does not exist, or emitting a name the file already declares.

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).

    Raises:
        PiecewiseExpansionError: A block naming something that does not exist,
            or emitting a name the file already declares.
    """
    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':
            emit(raw, name)
    expanded = Spec.model_validate(raw)
    expanded._expanded_piecewise = dict(schema.piecewise)
    return expanded