Skip to content

math_spec.lowering

Lower a validated model to a :class:~math_spec.program.Program.

One lowering, on the language side: it reads the typed AST and emits declarations with names resolved and shapes fixed, and reaches no consumer. A construct with no lowering raises :class:~math_spec.errors.LanguageError naming its rewrite.

curve_left_as_written_message(blocks) #

The refusal for a model lowered with its piecewise: blocks still to be written out.

Source code in src/math_spec/lowering.py
def curve_left_as_written_message(blocks: list[str]) -> str:
    """The refusal for a model lowered with its ``piecewise:`` blocks still to be written out."""
    named = ', '.join(f"'{block}'" for block in blocks)
    return (
        f'piecewise: {named} states rows rather than being one, and a program holds the rows. Pass '
        f"spec.expand('piecewise'), which writes each block out as the variables and constraints it states "
        f'and keeps every sos: block for a consumer that takes a set — or spec.expand(), which writes the '
        f'sets out as binaries and linking rows too.'
    )

lower_program(expanded) #

Compile a model whose curves are written out into a :class:~math_spec.program.Program.

A domain: binary variable lowers with fixed 0/1 bounds. A sos: block lowers as itself — a program carries a set, and :meth:~math_spec.model.Spec.expand is what states one as binaries instead. A piecewise: block does not lower at all: it states rows, and :meth:~math_spec.model.Spec.expand is what writes them, so a model still carrying one is refused rather than written out on the caller's behalf.

PARAMETER DESCRIPTION
expanded

A model with no piecewise: block left, which :meth:~math_spec.model.Spec.expand returns.

TYPE: Spec

RAISES DESCRIPTION
LanguageError

A construct outside the language, named with its rewrite, or a piecewise: block left as written.

Source code in src/math_spec/lowering.py
def lower_program(expanded: Spec) -> program.Program:
    """Compile a model whose curves are written out into a :class:`~math_spec.program.Program`.

    A ``domain: binary`` variable lowers with fixed 0/1 bounds. A ``sos:``
    block lowers as itself — a program carries a set, and
    :meth:`~math_spec.model.Spec.expand` is what states one as binaries
    instead. A ``piecewise:`` block does not lower at all: it states rows, and
    :meth:`~math_spec.model.Spec.expand` is what writes them, so a model still
    carrying one is refused rather than written out on the caller's behalf.

    Args:
        expanded: A model with no ``piecewise:`` block left, which
            :meth:`~math_spec.model.Spec.expand` returns.

    Raises:
        LanguageError: A construct outside the language, named with its
            rewrite, or a ``piecewise:`` block left as written.
    """
    if expanded.piecewise:
        raise LanguageError(curve_left_as_written_message(sorted(expanded.piecewise)))
    resolved = expanded.resolved
    parameters = {
        name: program.ParameterDeclaration(tuple(pdef.dims), pdef.dtype) for name, pdef in expanded.parameters.items()
    }

    variables = {}
    for vname, vdef in expanded.variables.items():
        domain = vdef.domain
        if domain == 'binary':
            lower, upper = program.Constant(0.0), program.Constant(1.0)
        else:
            lower, upper = _bound_expression(vdef.bounds.lower), _bound_expression(vdef.bounds.upper)
        variables[vname] = program.VariableDeclaration(
            tuple(vdef.dims),
            where=_Lowering(expanded, f"variable '{vname}'").mask(resolved.variables[vname]),
            lower=lower,
            upper=upper,
            domain=domain,
            absence=vdef.absence,
        )

    constraints = {}
    for cname, cdef in expanded.constraints.items():
        expression, where = resolved.constraints[cname]
        lowering = _Lowering(expanded, f"constraint '{cname}'")
        constraints[cname] = program.ConstraintDeclaration(
            tuple(cdef.dims),
            lhs=lowering.expr(expression.left),
            sense=expression.op,
            rhs=lowering.expr(expression.right),
            where=lowering.mask(where),
        )

    objective = None
    if (odef := expanded.objective) is not None:
        assert resolved.objective is not None, 'validation resolves the objective the file declares'
        objective = program.ObjectiveDeclaration(
            odef.sense,
            _Lowering(expanded, 'the objective').expr(resolved.objective),
        )

    dimensions = {dname: program.DimensionDeclaration(ddef.dtype) for dname, ddef in expanded.dimensions.items()}
    sos = {
        sname: program.SosDeclaration(
            sdef.variable,
            sdef.along,
            sos_type=sdef.type,
        )
        for sname, sdef in expanded.sos.items()
    }
    expressions: dict[str, program.ExpressionDeclaration] = {}
    for name, ast in resolved.expressions.items():
        expressions[name] = program.ExpressionDeclaration(
            _Lowering(expanded, f"named expression '{name}'").expr(ast), in_math=name in resolved.read_by_the_math
        )
    piecewise = {
        name: declaration_of(pw, _Lowering(expanded, f"piecewise '{name}'").mask(resolved.expanded_piecewise[name]))
        for name, pw in expanded._expanded_piecewise.items()
    }

    return program.Program(
        parameters=parameters,
        variables=variables,
        constraints=constraints,
        objective=objective,
        dimensions=dimensions,
        relations=resolved.relations,
        sos=sos,
        piecewise=piecewise,
        assumptions=_assumptions(expanded),
        expressions=expressions,
    )

to_program(spec) #

spec as a :class:~math_spec.program.Program — the public door.

Takes whatever you have: a YAML path, the YAML itself, a mapping, a loaded model, or a program already. Idempotent, so a caller that does not know which it holds can call this and be sure. The model is lowered as it arrived: nothing is written out here, so a piecewise: block still in it is refused, naming :meth:~math_spec.model.Spec.expand.

PARAMETER DESCRIPTION
spec

What to read the declarations from.

TYPE: str | Path | Mapping[str, object] | Spec | Program

RETURNS DESCRIPTION
Program

Every declaration the file makes, with names resolved and shapes

Program

fixed.

RAISES DESCRIPTION
SchemaError

The file is not a valid model.

LanguageError

A construct outside the language, named with its rewrite, or a piecewise: block left as written.

Source code in src/math_spec/lowering.py
def to_program(spec: str | Path | Mapping[str, object] | Spec | program.Program) -> program.Program:
    """*spec* as a :class:`~math_spec.program.Program` — the public door.

    Takes whatever you have: a YAML path, the YAML itself, a mapping, a loaded
    model, or a program already. Idempotent, so a caller that does not know
    which it holds can call this and be sure. The model is lowered as it
    arrived: nothing is written out here, so a ``piecewise:`` block still in
    it is refused, naming :meth:`~math_spec.model.Spec.expand`.

    Args:
        spec: What to read the declarations from.

    Returns:
        Every declaration the file makes, with names resolved and shapes
        fixed.

    Raises:
        SchemaError: The file is not a valid model.
        LanguageError: A construct outside the language, named with its
            rewrite, or a ``piecewise:`` block left as written.
    """
    if isinstance(spec, program.Program):
        return spec
    return lower_program(to_spec(spec))