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 module-attribute #

BranchNode = UnaryOperatorNode | BinaryOperatorNode | ComparisonNode | FunctionCallNode 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 #

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())
    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)}]'