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. The node
dataclasses reference the 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.
ArithmeticNode = NumberNode | NameNode | NameListNode | VariableNode | ParameterNode | DimensionNode | LookupNode | EdgeNode | KeywordNode | UnaryOperatorNode | BinaryOperatorNode | FunctionCallNode | CasesNode
module-attribute
#
ComparisonOperator = Literal['<=', '>=', '==']
module-attribute
#
ExpressionNode = ArithmeticNode | ComparisonNode
module-attribute
#
BinaryOperatorNode(op, left, right)
dataclass
#
CaseArm(label, when, value)
dataclass
#
CasesNode(name, foreach, 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, because a file writes the cases on the
declaration rather than at the use site.
The arms partition foreach — checked at load
(:mod:math_spec.partition) — so exactly one applies at every coordinate
and the value is a value rather than a choice. That is what lets this be an
ordinary arithmetic node: a consumer selects per coordinate, the way a
where already filters, and nothing about the shape of the plan depends
on data.
ComparisonNode(op, left, right)
dataclass
#
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 for shift(x, over=d, offset=n, edge='wrap').
Only legal as the value of shift's edge= kwarg. Like
:class:DimensionNode and :class:LookupNode this names neither data
nor a coordinate — it is a closed keyword, and the only one the language
has. A number in the same position stays an ordinary
:class:NumberNode, the value the vacated positions contribute, so one
kwarg carries all three edge policies and no second kwarg can contradict
it.
policy
instance-attribute
#
FunctionCallNode(name, args=list(), kwargs=dict())
dataclass
#
KeywordNode(value)
dataclass
#
A quoted closed keyword in a kwarg value — shift(..., edge='wrap').
Unresolved on purpose: which keywords a kwarg accepts is the operator's
business, so this only records that the author wrote a literal rather
than a name. resolution.py turns it into the typed node the kwarg
wants, or reports it as not one of that kwarg's keywords.
value
instance-attribute
#
LookupNode(names, dimension, into)
dataclass
#
A resolved reference to one or more declared lookups.
Only legal in operator kwarg values (sum(x, by=to)). Like
:class:DimensionNode this names structure, not data. The lookups carry
their own dimensions: dimension is the one they are all over — what
sum consumes and at produces — and into the ones their values
are labels of, one per name and in the order written, all copied off the
declarations once here so no backend has to re-derive them.
Plural because grouping through several lookups at once is one grouping,
not a composition of two: sum(x, by=[gen_bus, gen_tech]) consumes
generator once and produces both targets. The one-name case is the
same node with one-element tuples, so no consumer branches on arity.
NameListNode(names)
dataclass
#
A bracketed list of names in a kwarg value — sum(x, by=[a, b]).
Unresolved on purpose, and unresolvable here: which kind of name a kwarg
admits is the operator's business. Like :class:NameNode this never
reaches a backend — resolution.py rewrites it into the one typed node
its kwarg wants, so a pass that meets one ran before resolution.
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
#
ParameterNode(name)
dataclass
#
A resolved reference to a declared parameter.
name
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
parse_expression(text)
#
Parse a math expression string into an AST.
With parse_all and a single top-level alternative, element 0 of the
parse result is the root node.
| RETURNS | DESCRIPTION |
|---|---|
ExpressionNode
|
One of |
ExpressionNode
|
|