Skip to content

math_spec.boundedness

Provably unbounded models, named before a solver says a bare unbounded.

A variable that is unbounded on the side its objective term improves toward and appears in no constraint runs to infinity for any data at all. Advice rather than a refusal, because the same shape is what a half-written model looks like.

Which side improves is read off the sign the variable enters the objective with: under minimize a +v term runs down toward lower. Where that sign is not decidable without data — a parameter coefficient, or occurrences of both signs — nothing is claimed.

Sign = Literal['+', '-'] | None module-attribute #

unbounded_notes(program) #

Name every variable the objective can drive to infinity unopposed.

Asked of the program rather than the file: every fact the rule reads is a declaration — the objective's sense and its terms, the variables each constraint names, the two bounds — and by the time a program exists a piecewise: block has already become the constraints it expands into, which is where the variables it names are held.

RETURNS DESCRIPTION
list[Advice]

One note per variable that is unbounded on the side its objective term

list[Advice]

improves toward and named by no constraint.

Source code in src/math_spec/boundedness.py
def unbounded_notes(program: Program) -> list[Advice]:
    """Name every variable the objective can drive to infinity unopposed.

    Asked of the program rather than the file: every fact the rule reads is a
    declaration — the objective's sense and its terms, the variables each
    constraint names, the two bounds — and by the time a program exists a
    ``piecewise:`` block has already become the constraints it expands into,
    which is where the variables it names are held.

    Returns:
        One note per variable that is unbounded on the side its objective term
        improves toward and named by no constraint.
    """
    if program.objective is None:
        return []

    constrained = {block.variable for block in program.sos.values()}
    for constraint in program.constraints.values():
        constrained |= variables_of(constraint.lhs, constraint.rhs)

    signs: dict[str, Sign] = {}
    _walk(program.objective.expression, '+', signs)

    minimize = program.objective.sense == 'minimize'
    notes: list[Advice] = []
    for vname, sign in signs.items():
        if sign is None or vname in constrained:
            continue
        side = 'lower' if minimize == (sign == '+') else 'upper'
        if _is_open(program.variables[vname], side):
            notes.append(
                Advice(
                    'unbounded',
                    vname,
                    f"Variable '{vname}' makes this model unbounded: no constraint names it, and "
                    f'bounds.{side} is {_OPEN[side]}, which is the direction a {sign}{vname} term '
                    f'improves a {program.objective.sense} objective in. No data can change that, so '
                    f'the solve would answer `unbounded` and name nothing.\n'
                    f'Give it a finite bounds.{side}, or the constraint that was meant to define it.',
                )
            )
    return notes