Skip to content

math_spec.where_parser

pyparsing-based parser for where strings — grammar and AST only.

Parses strings like "p_max > 0 AND NOT is_must_run" into an AST. What a mask means is each backend's business: the eager lane evaluates the AST against an xr.Dataset (builder.evaluate_where), the relational lane lowers it to SQL predicates (lowering._lower_where).

Kept dependency-free on purpose — validation.py and lowering.py are linopy-free by hard rule 3, and they import this module.

NotNode, AndNode and OrNode reference the WhereNode union in their annotations before it is defined, which works only because from __future__ import annotations makes annotations strings — removing that future-import requires reordering the definitions.

PredicateOperator = Literal['<=', '>=', '==', '!=', '<', '>'] module-attribute #

WhereNode = BooleanLiteralNode | UnresolvedNameNode | UnresolvedComparisonNode | UnresolvedPositionNode | DimensionPositionNode | ParameterDefinedNode | VariableDefinedNode | ParameterComparisonNode | DimensionComparisonNode | LookupComparisonNode | LookupPairComparisonNode | LookupDefinedNode | NotNode | AndNode | OrNode module-attribute #

AndNode(left, right) dataclass #

left instance-attribute #

right instance-attribute #

BooleanLiteralNode(value) dataclass #

value instance-attribute #

DimensionComparisonNode(name, op, value) dataclass #

Compare a dimension's own coordinates against a literal.

name instance-attribute #

op instance-attribute #

value instance-attribute #

DimensionPositionNode(name, op, position, by=None) dataclass #

Compare where a row sits along a dimension against a position.

where: "position(snapshot) == 0" — the boundary of a recurrence named by where it sits rather than by the label that happens to be there, so the clause survives the index being relabelled. Negative counts from the end, -1 being the last.

Both sides are integers, which is what makes every comparator read one way. Naming the coordinate at a position and comparing coordinates against it left an ordering meaning either that or a comparison of positions, and the two part company on an axis whose coordinates do not arrive sorted (#32).

With by it is the boundary of each group the lookup makes — position(snapshot, by=period_of) == 0 is every period's first snapshot, and a row reads its own group's, the broadcast at(by=) already defines.

Resolved rather than lowered to a literal: which label sits at a position is a property of the data, so the position travels and each lane reads it off the coordinate order it already holds.

by = None class-attribute instance-attribute #

name instance-attribute #

op instance-attribute #

position instance-attribute #

LookupComparisonNode(name, over, op, value) dataclass #

Compare a lookup's values against a literal — period_of == 2030.

over is the dimension the lookup maps out of, copied off the declaration during resolution so the frame check and both lanes read it here rather than looking the lookup up again.

name instance-attribute #

op instance-attribute #

over instance-attribute #

value instance-attribute #

LookupDefinedNode(name, over) dataclass #

True where the named lookup has a value — the partial-lookup case.

A lookup may be partial: a null says the label belongs to no group (a generator on no bus, a line with one open end). This is how a declaration asks for the labels that do map, spelled as a bare name exactly as a parameter's definedness is.

name instance-attribute #

over instance-attribute #

LookupPairComparisonNode(name, other, over, op) dataclass #

Compare two lookups over one dimension — from != to.

The one comparison whose both sides are structure: two maps out of the same dimension, tested row by row on that dimension's own table. Over different dims there is no row to compare them on, which resolution refuses.

name instance-attribute #

op instance-attribute #

other instance-attribute #

over instance-attribute #

NotNode(operand) dataclass #

operand instance-attribute #

OrNode(left, right) dataclass #

left instance-attribute #

right instance-attribute #

ParameterComparisonNode(name, op, value) dataclass #

Compare a parameter against a literal, element-wise.

name instance-attribute #

op instance-attribute #

value instance-attribute #

ParameterDefinedNode(name) dataclass #

True wherever the named parameter is non-null and finite.

name instance-attribute #

UnresolvedComparisonNode(name, op, value, quoted=False) dataclass #

A comparison against an unresolved name. resolution.py types it.

name instance-attribute #

op instance-attribute #

quoted = False class-attribute instance-attribute #

value instance-attribute #

UnresolvedNameNode(name) dataclass #

A bare name — unresolved. resolution.py types it.

name instance-attribute #

UnresolvedPositionNode(dimension, op, position, by=None) dataclass #

position(dim) <op> i before the name is checked.

Kept apart from :class:UnresolvedComparisonNode because its left-hand side is not a name but an application to one, which no bare name can carry. resolution.py types it into :class:DimensionPositionNode.

by = None class-attribute instance-attribute #

dimension instance-attribute #

op instance-attribute #

position instance-attribute #

VariableDefinedNode(name) dataclass #

True at the coordinates where the named variable exists.

The variable counterpart of :class:ParameterDefinedNode, and spelled the same way — a bare name. A parameter's bare name asks whether it has a value here; a variable's asks whether it exists here.

name instance-attribute #

parse_where(text) #

Parse a where string into an AST.

With parse_all and a single top-level alternative, element 0 of the parse result is the root node.

Source code in src/math_spec/where_parser.py
def parse_where(text: str) -> WhereNode:
    """Parse a where string into an AST.

    With ``parse_all`` and a single top-level alternative, element 0 of the
    parse result is the root node.
    """
    try:
        result = _WHERE_GRAMMAR.parse_string(text, parse_all=True)
    except pp.ParseException as e:
        msg = f'Failed to parse where string: {text!r}\n{e}'
        if _INDEX_CALL.search(text):
            msg += _INDEX_REWRITE
        raise SchemaError(msg) from e
    return cast('WhereNode', result[0])