Skip to content

math_spec.expression_parser

pyparsing-based expression parser for math expressions.

Parses strings like sum(p * cost, over=generator) == load into an AST that can be evaluated against a namespace of linopy variables and xarray parameters.

ArithmeticNode is the arithmetic-only union: every nested expression position (operands, args, kwargs) accepts it and nothing else, and ComparisonNode appears only at the top of a parsed expression.

ArithmeticNode = NumberNode | NameNode | NameListNode | VariableNode | ParameterNode | DimensionNode | LookupNode | EdgeNode | KeywordNode | UnaryOperatorNode | BinaryOperatorNode | FunctionCallNode | CasesNode module-attribute #

BranchNode = UnaryOperatorNode | BinaryOperatorNode | ComparisonNode | FunctionCallNode | CasesNode module-attribute #

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

ExpressionNode = ArithmeticNode | ComparisonNode module-attribute #

KwargNode = DimensionNode | LookupNode | EdgeNode module-attribute #

LeafNode = NumberNode | VariableNode | ParameterNode | KwargNode | UnresolvedNode module-attribute #

REAL = '\\d+\\.\\d*([eE][+-]?\\d+)?|\\d+[eE][+-]?\\d+' module-attribute #

UnresolvedNode = NameNode | NameListNode | KeywordNode module-attribute #

BinaryOperatorNode(op, left, right) dataclass #

left instance-attribute #

op instance-attribute #

right instance-attribute #

CaseArm(label, when, value) dataclass #

One region of a :class:CasesNode: where it applies, and the value there.

when is None on the last arm and only there — the fallback, which is what makes the quantity total without anything having to prove it.

label instance-attribute #

value instance-attribute #

when instance-attribute #

CasesNode(name, arms) dataclass #

A value defined by region — a named expression's cases:, inlined.

Built by :mod:math_spec.expansion where a reference to a cased expression stood; there is no grammar for it, since a file writes the cases on the declaration rather than at the use site.

The arms are ordered and the last is the fallback, so exactly one applies at every coordinate without a checker having to establish it. The frame is not carried here: it is on the declaration, which every consumer needing it already holds.

arms instance-attribute #

name instance-attribute #

ComparisonNode(op, left, right) dataclass #

left instance-attribute #

op instance-attribute #

right instance-attribute #

DimensionNode(name) dataclass #

A resolved reference to a declared dimension.

Only legal in operator kwarg values (sum(x, over=generator)), never as a value in arithmetic — a dimension is a coordinate space, not data.

name instance-attribute #

EdgeNode(policy) dataclass #

A resolved edge policy, legal only as an edge= value.

A number in the same position stays a :class:NumberNode: the value the vacated positions contribute.

policy instance-attribute #

FunctionCallNode(name, args=list(), kwargs=dict()) dataclass #

args = field(default_factory=list) class-attribute instance-attribute #

kwargs = field(default_factory=dict) class-attribute instance-attribute #

name instance-attribute #

KeywordNode(value) dataclass #

A quoted closed keyword in a kwarg value — shift(..., edge='wrap').

Unresolved: which keywords the kwarg accepts is the operator's business.

value instance-attribute #

LookupNode(names, dimension, into) dataclass #

A resolved reference to one or more declared lookups, legal only in a kwarg value.

dimension is the one every lookup is over — what sum consumes and at produces — and into the targets, one per name in the order written; sum(x, by=[gen_bus, gen_tech]) is one grouping, not two.

dimension instance-attribute #

into instance-attribute #

names instance-attribute #

shown property #

The kwarg value as the author wrote it, for an error message.

NameListNode(names) dataclass #

A bracketed list of names in a kwarg value — sum(x, by=[a, b]).

Unresolved: which kind of name the kwarg admits is the operator's business.

names instance-attribute #

shown property #

The kwarg value as the author wrote it, for an error message.

NameNode(name) dataclass #

An unresolved token — a name whose kind is not yet known.

The parser cannot know whether p is a variable, a parameter or a dimension; only the schema knows. resolution.py rewrites every one of these into one of the typed nodes below, so a NameNode never reaches a backend. If you find one there, resolution was skipped.

name instance-attribute #

NumberNode(value) dataclass #

value instance-attribute #

ParameterNode(name) dataclass #

A resolved reference to a declared parameter.

name instance-attribute #

UnaryOperatorNode(op, operand) dataclass #

op instance-attribute #

operand instance-attribute #

VariableNode(name) dataclass #

A resolved reference to a declared decision variable.

name instance-attribute #

children(node) #

The sub-expressions of node — the structural half of any walk.

Every pass that recurses the whole tree and acts only at certain leaves goes through here, so a node added later reaches all of them. A pass whose answer differs per node type dispatches itself and keeps its assert_never; this is for the ones that only need to get everywhere.

An operator's kwargs are children too — a dimension or coordinate is an ordinary node in a kwarg value, which is what lets a macro bind a formal.

Source code in src/math_spec/expression_parser.py
def children(node: ExpressionNode) -> tuple[ArithmeticNode, ...]:
    """The sub-expressions of *node* — the structural half of any walk.

    Every pass that recurses the whole tree and acts only at certain leaves
    goes through here, so a node added later reaches all of them. A pass whose
    *answer* differs per node type dispatches itself and keeps its
    ``assert_never``; this is for the ones that only need to get everywhere.

    An operator's kwargs are children too — a dimension or coordinate is an
    ordinary node in a kwarg value, which is what lets a macro bind a formal.
    """
    if isinstance(node, UnaryOperatorNode):
        return (node.operand,)
    if isinstance(node, (BinaryOperatorNode, ComparisonNode)):
        return (node.left, node.right)
    if isinstance(node, FunctionCallNode):
        return (*node.args, *node.kwargs.values())
    if isinstance(node, CasesNode):
        # the values only: a `when` is a mask over the frame, not a value in it
        return tuple(arm.value for arm in node.arms)
    return ()

parse_expression(text) #

Parse a math expression string into an AST.

RAISES DESCRIPTION
SchemaError

If text is not an expression of the language.

Source code in src/math_spec/expression_parser.py
def parse_expression(text: str) -> ExpressionNode:
    """Parse a math expression string into an AST.

    Raises:
        SchemaError: If *text* is not an expression of the language.
    """
    try:
        result = _GRAMMAR.parse_string(text, parse_all=True)
    except pp.ParseException as e:
        msg = f'Failed to parse expression: {text!r}\n{e}'
        raise SchemaError(msg) from e
    return cast('ExpressionNode', result[0])

shown(names) #

Names as a kwarg value is written: bare when one, bracketed when several.

Source code in src/math_spec/expression_parser.py
def shown(names: tuple[str, ...]) -> str:
    """Names as a kwarg value is written: bare when one, bracketed when several."""
    return names[0] if len(names) == 1 else f'[{", ".join(names)}]'