mathspec 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mathspec-0.2.1 → mathspec-0.3.0}/PKG-INFO +28 -29
- {mathspec-0.2.1 → mathspec-0.3.0}/README.md +27 -28
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/__init__.py +18 -20
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/_expression_parser.py +50 -30
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/_expression_resolver.py +315 -264
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/_where_parser.py +24 -17
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/_where_resolver.py +126 -122
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/_yaml.py +5 -10
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/advising.py +16 -16
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/boundedness.py +7 -9
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/canonical.py +1 -1
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/composition.py +134 -69
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/degree.py +16 -25
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/dimensions.py +86 -116
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/errors.py +13 -28
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/exclusivity.py +78 -49
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/expansion.py +35 -8
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/lowering.py +40 -28
- mathspec-0.3.0/src/mathspec/operators.py +178 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/piecewise.py +43 -24
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/program.py +275 -102
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/resolution.py +122 -30
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/separability.py +20 -20
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/sos.py +1 -1
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/spec.py +303 -95
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/__init__.py +11 -11
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/format.py +20 -2
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/legend.py +55 -10
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/markdown.py +1 -1
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/symbols.py +12 -7
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/walk.py +99 -99
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/validation.py +44 -49
- mathspec-0.2.1/src/mathspec/operators.py +0 -235
- {mathspec-0.2.1 → mathspec-0.3.0}/.gitignore +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/LICENSE +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/LICENSES/Apache-2.0.txt +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/LICENSES/CC0-1.0.txt +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/LICENSES/MIT.txt +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/pyproject.toml +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/__main__.py +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/_sealed.py +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/py.typed +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/README.md +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/latex.py +0 -0
- {mathspec-0.2.1 → mathspec-0.3.0}/src/mathspec/typesetting/typst.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mathspec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Write the specification (spec) of an optimisation model as a YAML file. Check it and print it as math, with no data and no solver.
|
|
5
5
|
Project-URL: Documentation, https://mathspec.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/energy-models/mathspec
|
|
@@ -43,11 +43,11 @@ SPDX-License-Identifier: CC-BY-4.0
|
|
|
43
43
|
**Write the specification (spec) of an optimisation model as a YAML file. Check it and print it
|
|
44
44
|
as math, with no data and no solver.**
|
|
45
45
|
|
|
46
|
-
A
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
A spec declares four things: the dimensions it runs over, such as `snapshot`
|
|
47
|
+
and `generator`; the data it expects, such as `load` and `cost`; the decisions
|
|
48
|
+
the solver makes, such as `dispatch`; and the rules those decisions obey, such
|
|
49
|
+
as `sum(dispatch, over=generator) == load`. The file [below](#example) is a
|
|
50
|
+
complete spec.
|
|
51
51
|
|
|
52
52
|
<!--- --8<-- [start:benefits] -->
|
|
53
53
|
|
|
@@ -114,6 +114,14 @@ Markdown is one of three formats, and GitHub renders it here.
|
|
|
114
114
|
|
|
115
115
|
Least-cost dispatch of a generator fleet against an hourly load.
|
|
116
116
|
|
|
117
|
+
#### Variable domains
|
|
118
|
+
|
|
119
|
+
**`dispatch`**
|
|
120
|
+
|
|
121
|
+
```math
|
|
122
|
+
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
|
|
123
|
+
```
|
|
124
|
+
|
|
117
125
|
#### Objective
|
|
118
126
|
|
|
119
127
|
```math
|
|
@@ -128,14 +136,6 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
128
136
|
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
|
|
129
137
|
```
|
|
130
138
|
|
|
131
|
-
#### Variable domains
|
|
132
|
-
|
|
133
|
-
**`dispatch`**
|
|
134
|
-
|
|
135
|
-
```math
|
|
136
|
-
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
|
|
137
|
-
```
|
|
138
|
-
|
|
139
139
|
<details>
|
|
140
140
|
<summary>The whole document: a symbol table, and the legend it prints</summary>
|
|
141
141
|
|
|
@@ -162,6 +162,12 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
162
162
|
|---|---|
|
|
163
163
|
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
|
|
164
164
|
|
|
165
|
+
#### Variable domains
|
|
166
|
+
|
|
167
|
+
**`dispatch`**
|
|
168
|
+
|
|
169
|
+
$`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0`$
|
|
170
|
+
|
|
165
171
|
#### Objective
|
|
166
172
|
|
|
167
173
|
$`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}`$
|
|
@@ -172,12 +178,6 @@ $`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdo
|
|
|
172
178
|
|
|
173
179
|
$`\sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}`$
|
|
174
180
|
|
|
175
|
-
#### Variable domains
|
|
176
|
-
|
|
177
|
-
**`dispatch`**
|
|
178
|
-
|
|
179
|
-
$`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0`$
|
|
180
|
-
|
|
181
181
|
</details>
|
|
182
182
|
|
|
183
183
|
<!-- readme-math:end -->
|
|
@@ -203,7 +203,7 @@ conventional spelling, as in the folded block.
|
|
|
203
203
|
|
|
204
204
|
<!--- --8<-- [start:engines] -->
|
|
205
205
|
|
|
206
|
-
mathspec builds nothing and solves nothing itself.
|
|
206
|
+
mathspec builds nothing and solves nothing itself. Instead,
|
|
207
207
|
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
208
208
|
[linopy](https://github.com/PyPSA/linopy) build a model from a spec and its
|
|
209
209
|
data, and solve it. Support in both is work in progress. Any other tool can
|
|
@@ -240,11 +240,10 @@ See [installation](https://mathspec.readthedocs.io/en/latest/howto/installation/
|
|
|
240
240
|
## Prior art
|
|
241
241
|
|
|
242
242
|
Every file under `src/` was written in [specsolve](https://github.com/fluxopt/specsolve)
|
|
243
|
-
and extracted here. The keys
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
[linopy](https://github.com/PyPSA/linopy)
|
|
247
|
-
`sum(over=)` and the dimension rules are named against.
|
|
243
|
+
and extracted here. The keys come from
|
|
244
|
+
[Calliope](https://github.com/calliope-project/calliope): math in YAML, one
|
|
245
|
+
block per component, `dims:` and a `where:` string. `sum(over=)` and the
|
|
246
|
+
dimension rules take their names from [linopy](https://github.com/PyPSA/linopy).
|
|
248
247
|
|
|
249
248
|
## Status
|
|
250
249
|
|
|
@@ -255,9 +254,9 @@ Alpha, pre-1.0.
|
|
|
255
254
|
**Breaking changes land without a deprecation cycle.** Pin an exact version if
|
|
256
255
|
you depend on this, and read the
|
|
257
256
|
[changelog](https://github.com/energy-models/mathspec/blob/main/CHANGELOG.md)
|
|
258
|
-
before upgrading.
|
|
259
|
-
and all three typeset formats, and the LaTeX
|
|
260
|
-
|
|
257
|
+
before upgrading. The test suite passes every construct through the schema,
|
|
258
|
+
the parsers and all three typeset formats, and compiles the LaTeX. The YAML that
|
|
259
|
+
mathspec accepts can still change.
|
|
261
260
|
|
|
262
261
|
<!--- --8<-- [end:status] -->
|
|
263
262
|
|
|
@@ -18,11 +18,11 @@ SPDX-License-Identifier: CC-BY-4.0
|
|
|
18
18
|
**Write the specification (spec) of an optimisation model as a YAML file. Check it and print it
|
|
19
19
|
as math, with no data and no solver.**
|
|
20
20
|
|
|
21
|
-
A
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
A spec declares four things: the dimensions it runs over, such as `snapshot`
|
|
22
|
+
and `generator`; the data it expects, such as `load` and `cost`; the decisions
|
|
23
|
+
the solver makes, such as `dispatch`; and the rules those decisions obey, such
|
|
24
|
+
as `sum(dispatch, over=generator) == load`. The file [below](#example) is a
|
|
25
|
+
complete spec.
|
|
26
26
|
|
|
27
27
|
<!--- --8<-- [start:benefits] -->
|
|
28
28
|
|
|
@@ -89,6 +89,14 @@ Markdown is one of three formats, and GitHub renders it here.
|
|
|
89
89
|
|
|
90
90
|
Least-cost dispatch of a generator fleet against an hourly load.
|
|
91
91
|
|
|
92
|
+
#### Variable domains
|
|
93
|
+
|
|
94
|
+
**`dispatch`**
|
|
95
|
+
|
|
96
|
+
```math
|
|
97
|
+
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
|
|
98
|
+
```
|
|
99
|
+
|
|
92
100
|
#### Objective
|
|
93
101
|
|
|
94
102
|
```math
|
|
@@ -103,14 +111,6 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
103
111
|
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
#### Variable domains
|
|
107
|
-
|
|
108
|
-
**`dispatch`**
|
|
109
|
-
|
|
110
|
-
```math
|
|
111
|
-
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
|
|
112
|
-
```
|
|
113
|
-
|
|
114
114
|
<details>
|
|
115
115
|
<summary>The whole document: a symbol table, and the legend it prints</summary>
|
|
116
116
|
|
|
@@ -137,6 +137,12 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
137
137
|
|---|---|
|
|
138
138
|
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
|
|
139
139
|
|
|
140
|
+
#### Variable domains
|
|
141
|
+
|
|
142
|
+
**`dispatch`**
|
|
143
|
+
|
|
144
|
+
$`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0`$
|
|
145
|
+
|
|
140
146
|
#### Objective
|
|
141
147
|
|
|
142
148
|
$`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}`$
|
|
@@ -147,12 +153,6 @@ $`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdo
|
|
|
147
153
|
|
|
148
154
|
$`\sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}`$
|
|
149
155
|
|
|
150
|
-
#### Variable domains
|
|
151
|
-
|
|
152
|
-
**`dispatch`**
|
|
153
|
-
|
|
154
|
-
$`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0`$
|
|
155
|
-
|
|
156
156
|
</details>
|
|
157
157
|
|
|
158
158
|
<!-- readme-math:end -->
|
|
@@ -178,7 +178,7 @@ conventional spelling, as in the folded block.
|
|
|
178
178
|
|
|
179
179
|
<!--- --8<-- [start:engines] -->
|
|
180
180
|
|
|
181
|
-
mathspec builds nothing and solves nothing itself.
|
|
181
|
+
mathspec builds nothing and solves nothing itself. Instead,
|
|
182
182
|
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
183
183
|
[linopy](https://github.com/PyPSA/linopy) build a model from a spec and its
|
|
184
184
|
data, and solve it. Support in both is work in progress. Any other tool can
|
|
@@ -215,11 +215,10 @@ See [installation](https://mathspec.readthedocs.io/en/latest/howto/installation/
|
|
|
215
215
|
## Prior art
|
|
216
216
|
|
|
217
217
|
Every file under `src/` was written in [specsolve](https://github.com/fluxopt/specsolve)
|
|
218
|
-
and extracted here. The keys
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
[linopy](https://github.com/PyPSA/linopy)
|
|
222
|
-
`sum(over=)` and the dimension rules are named against.
|
|
218
|
+
and extracted here. The keys come from
|
|
219
|
+
[Calliope](https://github.com/calliope-project/calliope): math in YAML, one
|
|
220
|
+
block per component, `dims:` and a `where:` string. `sum(over=)` and the
|
|
221
|
+
dimension rules take their names from [linopy](https://github.com/PyPSA/linopy).
|
|
223
222
|
|
|
224
223
|
## Status
|
|
225
224
|
|
|
@@ -230,9 +229,9 @@ Alpha, pre-1.0.
|
|
|
230
229
|
**Breaking changes land without a deprecation cycle.** Pin an exact version if
|
|
231
230
|
you depend on this, and read the
|
|
232
231
|
[changelog](https://github.com/energy-models/mathspec/blob/main/CHANGELOG.md)
|
|
233
|
-
before upgrading.
|
|
234
|
-
and all three typeset formats, and the LaTeX
|
|
235
|
-
|
|
232
|
+
before upgrading. The test suite passes every construct through the schema,
|
|
233
|
+
the parsers and all three typeset formats, and compiles the LaTeX. The YAML that
|
|
234
|
+
mathspec accepts can still change.
|
|
236
235
|
|
|
237
236
|
<!--- --8<-- [end:status] -->
|
|
238
237
|
|
|
@@ -4,21 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
"""The language: what a YAML file may say, and what it means.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
and
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
The top level is what a consumer calls: [`to_spec`][], the one door to a
|
|
8
|
+
file, and the verbs over what it returns. The rest of the surface is three
|
|
9
|
+
modules, each with one rule:
|
|
10
|
+
|
|
11
|
+
- [`mathspec.spec`][] is what the file *says*: [`Spec`][mathspec.spec.Spec]
|
|
12
|
+
and the blocks it holds.
|
|
13
|
+
- [`mathspec.program`][] is what the file *means*: the
|
|
14
|
+
[`Program`][mathspec.program.Program] a spec lowers to, its nodes, and the
|
|
15
|
+
[`Advice`][mathspec.program.Advice] the language gives about it.
|
|
16
|
+
- [`mathspec.errors`][] is what a consumer catches.
|
|
17
|
+
|
|
18
|
+
Everything between them — both grammars and the tree they build — is
|
|
19
|
+
package-private, because a consumer reads a program instead.
|
|
12
20
|
"""
|
|
13
21
|
|
|
14
|
-
from mathspec import program
|
|
22
|
+
from mathspec import errors, program, spec
|
|
15
23
|
from mathspec.advising import advice
|
|
16
24
|
from mathspec.composition import merge, override
|
|
17
|
-
from mathspec.errors import Advice, AdviceKind, DimensionError, LanguageError, MathSpecError, SchemaError, did_you_mean
|
|
18
|
-
from mathspec.operators import BUILTIN_NAMES
|
|
19
|
-
from mathspec.spec import Spec
|
|
20
25
|
from mathspec.typesetting import (
|
|
21
|
-
|
|
26
|
+
FormatName,
|
|
22
27
|
SymbolTable,
|
|
23
28
|
to_latex,
|
|
24
29
|
to_markdown,
|
|
@@ -29,21 +34,14 @@ from mathspec.typesetting import (
|
|
|
29
34
|
from mathspec.validation import to_spec
|
|
30
35
|
|
|
31
36
|
__all__ = [
|
|
32
|
-
'
|
|
33
|
-
'FORMATS',
|
|
34
|
-
'Advice',
|
|
35
|
-
'AdviceKind',
|
|
36
|
-
'DimensionError',
|
|
37
|
-
'LanguageError',
|
|
38
|
-
'MathSpecError',
|
|
39
|
-
'SchemaError',
|
|
40
|
-
'Spec',
|
|
37
|
+
'FormatName',
|
|
41
38
|
'SymbolTable',
|
|
42
39
|
'advice',
|
|
43
|
-
'
|
|
40
|
+
'errors',
|
|
44
41
|
'merge',
|
|
45
42
|
'override',
|
|
46
43
|
'program',
|
|
44
|
+
'spec',
|
|
47
45
|
'to_latex',
|
|
48
46
|
'to_markdown',
|
|
49
47
|
'to_spec',
|
|
@@ -23,23 +23,23 @@ from mathspec.errors import SchemaError
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
24
|
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
25
25
|
|
|
26
|
-
#: The
|
|
27
|
-
#:
|
|
26
|
+
#: The three comparison operators an expression may use. A constraint reads
|
|
27
|
+
#: its sense from this operator.
|
|
28
28
|
ComparisonOperator = Literal['<=', '>=', '==']
|
|
29
29
|
|
|
30
30
|
#: The sign a unary operator applies to its operand.
|
|
31
31
|
UnaryOperator = Literal['+', '-']
|
|
32
32
|
|
|
33
|
-
#: The arithmetic a binary operator may spell.
|
|
34
|
-
#:
|
|
35
|
-
#:
|
|
33
|
+
#: The arithmetic a binary operator may spell. The grammar closes the set, and
|
|
34
|
+
#: a renderer that dispatches on [`BinaryOperatorNode.op`][] switches over it
|
|
35
|
+
#: and keeps no list of its own.
|
|
36
36
|
BinaryOperator = Literal['+', '-', '*', '/', '**']
|
|
37
37
|
|
|
38
38
|
#: What an expression writes to refer to a declaration, and so what a
|
|
39
39
|
#: declaration may be named.
|
|
40
40
|
NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'
|
|
41
41
|
|
|
42
|
-
#: A float
|
|
42
|
+
#: A float has a fractional part or an exponent. The unary operator carries the sign.
|
|
43
43
|
REAL = r'\d+\.\d*([eE][+-]?\d+)?|\d+[eE][+-]?\d+'
|
|
44
44
|
|
|
45
45
|
# ---------------------------------------------------------------------------
|
|
@@ -79,6 +79,22 @@ class NameListNode:
|
|
|
79
79
|
return shown(self.names)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class ColumnsNode:
|
|
84
|
+
"""Columns of one relation in a kwarg value — ``sum(x, over=g, by=gen_bus[bus])``.
|
|
85
|
+
|
|
86
|
+
The relation is written once and its columns after it, so one call cannot
|
|
87
|
+
name columns of two tables. Unresolved: which columns the kwarg admits is
|
|
88
|
+
the operator's business.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
relation: str
|
|
92
|
+
columns: tuple[str, ...]
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
return f'{self.relation}[{", ".join(self.columns)}]'
|
|
96
|
+
|
|
97
|
+
|
|
82
98
|
@dataclass(frozen=True)
|
|
83
99
|
class KeywordNode:
|
|
84
100
|
"""A quoted closed keyword in a kwarg value — ``shift(..., edge='wrap')``.
|
|
@@ -131,10 +147,18 @@ class FunctionCallNode:
|
|
|
131
147
|
return f'{self.name}({", ".join(passed)})'
|
|
132
148
|
|
|
133
149
|
|
|
134
|
-
#: Every arithmetic node the grammar builds. A name, a name list
|
|
135
|
-
#: keyword are what resolution reads for their kind; the
|
|
150
|
+
#: Every arithmetic node the grammar builds. A name, a name list, a column
|
|
151
|
+
#: selection and a quoted keyword are what resolution reads for their kind; the
|
|
152
|
+
#: rest is structure.
|
|
136
153
|
ArithmeticNode = (
|
|
137
|
-
NumberNode
|
|
154
|
+
NumberNode
|
|
155
|
+
| NameNode
|
|
156
|
+
| NameListNode
|
|
157
|
+
| ColumnsNode
|
|
158
|
+
| KeywordNode
|
|
159
|
+
| UnaryOperatorNode
|
|
160
|
+
| BinaryOperatorNode
|
|
161
|
+
| FunctionCallNode
|
|
138
162
|
)
|
|
139
163
|
|
|
140
164
|
|
|
@@ -200,7 +224,7 @@ def nodes(*roots: ParsedNode) -> Iterator[ParsedNode]:
|
|
|
200
224
|
|
|
201
225
|
def with_children(node: ArithmeticNode, recurse: Callable[[ArithmeticNode], ArithmeticNode]) -> ArithmeticNode:
|
|
202
226
|
"""*node* rebuilt with *recurse* applied to each of its [`children`][]; a leaf comes back as is."""
|
|
203
|
-
if isinstance(node, NumberNode | NameNode | NameListNode | KeywordNode):
|
|
227
|
+
if isinstance(node, NumberNode | NameNode | NameListNode | ColumnsNode | KeywordNode):
|
|
204
228
|
return node
|
|
205
229
|
if isinstance(node, UnaryOperatorNode):
|
|
206
230
|
return UnaryOperatorNode(node.op, recurse(node.operand))
|
|
@@ -227,8 +251,8 @@ NAME_LIST = (pp.Suppress('[') + pp.DelimitedList(pp.Regex(NAME)) + pp.Suppress('
|
|
|
227
251
|
)
|
|
228
252
|
|
|
229
253
|
|
|
230
|
-
def _build_grammar() -> tuple[pp.ParserElement, pp.ParserElement]:
|
|
231
|
-
"""The arithmetic grammar,
|
|
254
|
+
def _build_grammar() -> tuple[pp.ParserElement, pp.ParserElement, pp.ParserElement]:
|
|
255
|
+
"""The arithmetic grammar, the expression grammar that puts one comparison over it, and a column selection.
|
|
232
256
|
|
|
233
257
|
``inf`` is a ``pp.Keyword`` rather than a ``pp.Literal``, which would
|
|
234
258
|
match the prefix of ``inflow``.
|
|
@@ -242,7 +266,10 @@ def _build_grammar() -> tuple[pp.ParserElement, pp.ParserElement]:
|
|
|
242
266
|
name = pp.Regex(NAME)
|
|
243
267
|
|
|
244
268
|
quoted = (pp.QuotedString("'") | pp.QuotedString('"')).set_parse_action(lambda t: KeywordNode(str(t[0])))
|
|
245
|
-
|
|
269
|
+
columns = (name + pp.Suppress('[') + pp.DelimitedList(name) + pp.Suppress(']')).set_parse_action(
|
|
270
|
+
lambda t: ColumnsNode(str(t[0]), tuple(str(x) for x in t[1:]))
|
|
271
|
+
)
|
|
272
|
+
kwarg = (name + pp.Suppress('=') + (quoted | columns | NAME_LIST | arith)).set_parse_action(lambda t: (t[0], t[1]))
|
|
246
273
|
pos_arg = arith
|
|
247
274
|
arg_list = pp.Optional(pp.DelimitedList(kwarg | pos_arg))
|
|
248
275
|
func_call = (name + pp.Suppress('(') + arg_list + pp.Suppress(')')).set_parse_action(_make_func_call)
|
|
@@ -268,7 +295,7 @@ def _build_grammar() -> tuple[pp.ParserElement, pp.ParserElement]:
|
|
|
268
295
|
expression = (arith + pp.Optional(comparator + arith)).set_parse_action(
|
|
269
296
|
lambda t: ComparisonNode(t[1], t[0], t[2]) if len(t) == 3 else t[0]
|
|
270
297
|
)
|
|
271
|
-
return arith, expression
|
|
298
|
+
return arith, expression, columns
|
|
272
299
|
|
|
273
300
|
|
|
274
301
|
def _make_func_call(tokens: pp.ParseResults) -> FunctionCallNode:
|
|
@@ -286,7 +313,7 @@ def keywords[V](name: str, pairs: Iterable[tuple[str, V]]) -> dict[str, V]:
|
|
|
286
313
|
kwargs: dict[str, V] = {}
|
|
287
314
|
for key, value in pairs:
|
|
288
315
|
if key in kwargs:
|
|
289
|
-
msg = f'{name}({key}=) is given twice.
|
|
316
|
+
msg = f'{name}({key}=) is given twice. Delete one of them.'
|
|
290
317
|
raise SchemaError(msg)
|
|
291
318
|
kwargs[key] = value
|
|
292
319
|
return kwargs
|
|
@@ -308,7 +335,9 @@ def _make_power(tokens: pp.ParseResults) -> ArithmeticNode:
|
|
|
308
335
|
|
|
309
336
|
#: The arithmetic half on its own, for the where grammar to put a predicate's
|
|
310
337
|
#: comparator over: one grammar for what a side may say, wherever it stands.
|
|
311
|
-
|
|
338
|
+
#: The column selection is shared too, so a kwarg reads one the same way in
|
|
339
|
+
#: both grammars.
|
|
340
|
+
ARITHMETIC, _GRAMMAR, COLUMNS = _build_grammar()
|
|
312
341
|
|
|
313
342
|
|
|
314
343
|
#: How deep a tree the language admits. Every pass over an expression recurses,
|
|
@@ -363,7 +392,7 @@ def parse_text[T](
|
|
|
363
392
|
result = grammar.parse_string(text, parse_all=True)
|
|
364
393
|
except pp.ParseException as e:
|
|
365
394
|
hint = rewrite(text, e.loc)
|
|
366
|
-
msg = f'
|
|
395
|
+
msg = f'Cannot parse the {what} {text!r}.\n{f"{hint}\n" if hint is not None else ""}{e}'
|
|
367
396
|
raise SchemaError(msg) from e
|
|
368
397
|
except RecursionError:
|
|
369
398
|
raise SchemaError(_too_deep(what, text, None, deep_rewrite)) from None
|
|
@@ -381,22 +410,13 @@ def _named_rewrite(text: str, loc: int) -> str | None:
|
|
|
381
410
|
"""
|
|
382
411
|
rest = text[loc:].lstrip()
|
|
383
412
|
if rest.startswith(get_args(ComparisonOperator)):
|
|
384
|
-
return
|
|
385
|
-
f"'{rest[:2]}' follows a complete comparison, and an expression carries "
|
|
386
|
-
f'one comparison, at the top. Split the chain into two constraints.'
|
|
387
|
-
)
|
|
413
|
+
return f"'{rest[:2]}' follows a complete comparison. Split the chain into two constraints."
|
|
388
414
|
if rest.startswith('!='):
|
|
389
|
-
return
|
|
390
|
-
"'!=' is not a constraint sense — the senses are <=, >= and ==. "
|
|
391
|
-
'Holding rows apart is a where matter: write the test in where:, where != is legal.'
|
|
392
|
-
)
|
|
415
|
+
return "'!=' is not a constraint sense. Write <=, >= or ==, or move the != test to where:."
|
|
393
416
|
if rest.startswith(('<', '>')):
|
|
394
|
-
return f"'{rest[0]}' is not a constraint sense
|
|
417
|
+
return f"'{rest[0]}' is not a constraint sense. Write '{rest[0]}=' instead."
|
|
395
418
|
if rest.startswith('='):
|
|
396
|
-
return (
|
|
397
|
-
"'=' on its own is how a kwarg is written inside a call, like sum(x, over=d). "
|
|
398
|
-
'Equality between two sides is written ==.'
|
|
399
|
-
)
|
|
419
|
+
return "A single '=' passes a keyword argument, as in sum(x, over=d). Write == for equality."
|
|
400
420
|
if rest.startswith('^'):
|
|
401
421
|
return "power is written '**', not '^'."
|
|
402
422
|
return None
|