mathspec 0.2.0__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.0 → mathspec-0.3.0}/PKG-INFO +56 -37
- {mathspec-0.2.0 → mathspec-0.3.0}/README.md +55 -36
- {mathspec-0.2.0 → mathspec-0.3.0}/pyproject.toml +3 -2
- mathspec-0.3.0/src/mathspec/__init__.py +60 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/__main__.py +51 -14
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/_expression_parser.py +56 -32
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/_expression_resolver.py +316 -249
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/_where_parser.py +24 -16
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/_where_resolver.py +151 -125
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/_yaml.py +5 -10
- mathspec-0.3.0/src/mathspec/advising.py +137 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/boundedness.py +11 -11
- mathspec-0.3.0/src/mathspec/canonical.py +268 -0
- mathspec-0.3.0/src/mathspec/composition.py +965 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/degree.py +17 -52
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/dimensions.py +116 -120
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/errors.py +16 -31
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/exclusivity.py +78 -71
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/expansion.py +56 -10
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/lowering.py +135 -21
- mathspec-0.3.0/src/mathspec/operators.py +178 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/piecewise.py +43 -24
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/program.py +333 -110
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/resolution.py +134 -37
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/separability.py +20 -20
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/sos.py +1 -1
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/spec.py +428 -99
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/__init__.py +26 -9
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/format.py +20 -2
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/legend.py +96 -10
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/markdown.py +1 -1
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/symbols.py +46 -20
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/walk.py +133 -109
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/validation.py +67 -49
- mathspec-0.2.0/src/mathspec/__init__.py +0 -59
- mathspec-0.2.0/src/mathspec/advising.py +0 -85
- mathspec-0.2.0/src/mathspec/operators.py +0 -235
- {mathspec-0.2.0 → mathspec-0.3.0}/.gitignore +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/LICENSE +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/LICENSES/Apache-2.0.txt +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/LICENSES/CC0-1.0.txt +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/LICENSES/MIT.txt +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/_sealed.py +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/py.typed +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/README.md +0 -0
- {mathspec-0.2.0 → mathspec-0.3.0}/src/mathspec/typesetting/latex.py +0 -0
- {mathspec-0.2.0 → 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,20 +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
|
-
|
|
51
|
-
|
|
52
|
-
<!--- --8<-- [start:engines] -->
|
|
53
|
-
|
|
54
|
-
mathspec builds nothing and solves nothing itself.
|
|
55
|
-
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
56
|
-
[linopy](https://github.com/PyPSA/linopy) build a model from a spec and its
|
|
57
|
-
data, and solve it. Support in both is work in progress.
|
|
58
|
-
|
|
59
|
-
<!--- --8<-- [end:engines] -->
|
|
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.
|
|
60
51
|
|
|
61
52
|
<!--- --8<-- [start:benefits] -->
|
|
62
53
|
|
|
@@ -123,6 +114,14 @@ Markdown is one of three formats, and GitHub renders it here.
|
|
|
123
114
|
|
|
124
115
|
Least-cost dispatch of a generator fleet against an hourly load.
|
|
125
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
|
+
|
|
126
125
|
#### Objective
|
|
127
126
|
|
|
128
127
|
```math
|
|
@@ -137,14 +136,6 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
137
136
|
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
|
|
138
137
|
```
|
|
139
138
|
|
|
140
|
-
#### Variable domains
|
|
141
|
-
|
|
142
|
-
**`dispatch`**
|
|
143
|
-
|
|
144
|
-
```math
|
|
145
|
-
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
|
|
146
|
-
```
|
|
147
|
-
|
|
148
139
|
<details>
|
|
149
140
|
<summary>The whole document: a symbol table, and the legend it prints</summary>
|
|
150
141
|
|
|
@@ -171,6 +162,12 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
171
162
|
|---|---|
|
|
172
163
|
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
|
|
173
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
|
+
|
|
174
171
|
#### Objective
|
|
175
172
|
|
|
176
173
|
$`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}`$
|
|
@@ -181,12 +178,6 @@ $`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdo
|
|
|
181
178
|
|
|
182
179
|
$`\sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}`$
|
|
183
180
|
|
|
184
|
-
#### Variable domains
|
|
185
|
-
|
|
186
|
-
**`dispatch`**
|
|
187
|
-
|
|
188
|
-
$`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0`$
|
|
189
|
-
|
|
190
181
|
</details>
|
|
191
182
|
|
|
192
183
|
<!-- readme-math:end -->
|
|
@@ -208,6 +199,35 @@ A [symbol table](https://mathspec.readthedocs.io/en/latest/reference/typeset/#sy
|
|
|
208
199
|
conventional spelling, as in the folded block.
|
|
209
200
|
[Print a spec as math](https://mathspec.readthedocs.io/en/latest/howto/print/) does the same from a shell.
|
|
210
201
|
|
|
202
|
+
## Engines and other tools
|
|
203
|
+
|
|
204
|
+
<!--- --8<-- [start:engines] -->
|
|
205
|
+
|
|
206
|
+
mathspec builds nothing and solves nothing itself. Instead,
|
|
207
|
+
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
208
|
+
[linopy](https://github.com/PyPSA/linopy) build a model from a spec and its
|
|
209
|
+
data, and solve it. Support in both is work in progress. Any other tool can
|
|
210
|
+
read the same spec through the
|
|
211
|
+
[Program API](https://mathspec.readthedocs.io/en/latest/reference/program/).
|
|
212
|
+
The solid boxes are mathspec; the dashed boxes are outside it.
|
|
213
|
+
|
|
214
|
+
```mermaid
|
|
215
|
+
flowchart LR
|
|
216
|
+
accTitle: What mathspec does, and what other tools do with a spec
|
|
217
|
+
accDescr: A YAML file loads into a Spec and the Program it lowers to. mathspec checks the spec and prints it as math, with no data. Outside mathspec, drawn dashed, an engine such as specsolve or linopy reads the same spec, takes your data and returns your answers, and any other tool, such as a renderer or an analyser, reads the same spec through the Program API.
|
|
218
|
+
Y(["your spec<br/>one YAML file"]) --> SPEC["<b>Spec</b> and the <b>Program</b> it lowers to<br/><i>checked before any data exists</i>"]
|
|
219
|
+
SPEC --> CHECK["<b>check it</b><br/>python -m mathspec check"]
|
|
220
|
+
SPEC --> SHOW["<b>print it as math</b><br/>LaTeX · Typst · Markdown"]
|
|
221
|
+
SPEC -.-> OTHER["<b>your own tool</b><br/>a renderer · an analyser · …<br/>reads the Program API"]
|
|
222
|
+
SPEC -.-> ENGINE["<b>an engine</b><br/>specsolve · linopy<br/>builds and solves the model"]
|
|
223
|
+
DATA[("your data")] -.-> ENGINE
|
|
224
|
+
ENGINE -.-> ANS(["your answers"])
|
|
225
|
+
classDef outside stroke-dasharray:5 4
|
|
226
|
+
class ENGINE,DATA,ANS,OTHER outside
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
<!--- --8<-- [end:engines] -->
|
|
230
|
+
|
|
211
231
|
## Documentation
|
|
212
232
|
|
|
213
233
|
The documentation is at <https://mathspec.readthedocs.io>.
|
|
@@ -220,11 +240,10 @@ See [installation](https://mathspec.readthedocs.io/en/latest/howto/installation/
|
|
|
220
240
|
## Prior art
|
|
221
241
|
|
|
222
242
|
Every file under `src/` was written in [specsolve](https://github.com/fluxopt/specsolve)
|
|
223
|
-
and extracted here. The keys
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
[linopy](https://github.com/PyPSA/linopy)
|
|
227
|
-
`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).
|
|
228
247
|
|
|
229
248
|
## Status
|
|
230
249
|
|
|
@@ -235,9 +254,9 @@ Alpha, pre-1.0.
|
|
|
235
254
|
**Breaking changes land without a deprecation cycle.** Pin an exact version if
|
|
236
255
|
you depend on this, and read the
|
|
237
256
|
[changelog](https://github.com/energy-models/mathspec/blob/main/CHANGELOG.md)
|
|
238
|
-
before upgrading.
|
|
239
|
-
and all three typeset formats, and the LaTeX
|
|
240
|
-
|
|
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.
|
|
241
260
|
|
|
242
261
|
<!--- --8<-- [end:status] -->
|
|
243
262
|
|
|
@@ -18,20 +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
|
-
|
|
26
|
-
|
|
27
|
-
<!--- --8<-- [start:engines] -->
|
|
28
|
-
|
|
29
|
-
mathspec builds nothing and solves nothing itself.
|
|
30
|
-
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
31
|
-
[linopy](https://github.com/PyPSA/linopy) build a model from a spec and its
|
|
32
|
-
data, and solve it. Support in both is work in progress.
|
|
33
|
-
|
|
34
|
-
<!--- --8<-- [end:engines] -->
|
|
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.
|
|
35
26
|
|
|
36
27
|
<!--- --8<-- [start:benefits] -->
|
|
37
28
|
|
|
@@ -98,6 +89,14 @@ Markdown is one of three formats, and GitHub renders it here.
|
|
|
98
89
|
|
|
99
90
|
Least-cost dispatch of a generator fleet against an hourly load.
|
|
100
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
|
+
|
|
101
100
|
#### Objective
|
|
102
101
|
|
|
103
102
|
```math
|
|
@@ -112,14 +111,6 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
112
111
|
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
|
|
113
112
|
```
|
|
114
113
|
|
|
115
|
-
#### Variable domains
|
|
116
|
-
|
|
117
|
-
**`dispatch`**
|
|
118
|
-
|
|
119
|
-
```math
|
|
120
|
-
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
|
|
121
|
-
```
|
|
122
|
-
|
|
123
114
|
<details>
|
|
124
115
|
<summary>The whole document: a symbol table, and the legend it prints</summary>
|
|
125
116
|
|
|
@@ -146,6 +137,12 @@ Least-cost dispatch of a generator fleet against an hourly load.
|
|
|
146
137
|
|---|---|
|
|
147
138
|
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
|
|
148
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
|
+
|
|
149
146
|
#### Objective
|
|
150
147
|
|
|
151
148
|
$`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}`$
|
|
@@ -156,12 +153,6 @@ $`\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdo
|
|
|
156
153
|
|
|
157
154
|
$`\sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}`$
|
|
158
155
|
|
|
159
|
-
#### Variable domains
|
|
160
|
-
|
|
161
|
-
**`dispatch`**
|
|
162
|
-
|
|
163
|
-
$`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0`$
|
|
164
|
-
|
|
165
156
|
</details>
|
|
166
157
|
|
|
167
158
|
<!-- readme-math:end -->
|
|
@@ -183,6 +174,35 @@ A [symbol table](https://mathspec.readthedocs.io/en/latest/reference/typeset/#sy
|
|
|
183
174
|
conventional spelling, as in the folded block.
|
|
184
175
|
[Print a spec as math](https://mathspec.readthedocs.io/en/latest/howto/print/) does the same from a shell.
|
|
185
176
|
|
|
177
|
+
## Engines and other tools
|
|
178
|
+
|
|
179
|
+
<!--- --8<-- [start:engines] -->
|
|
180
|
+
|
|
181
|
+
mathspec builds nothing and solves nothing itself. Instead,
|
|
182
|
+
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
183
|
+
[linopy](https://github.com/PyPSA/linopy) build a model from a spec and its
|
|
184
|
+
data, and solve it. Support in both is work in progress. Any other tool can
|
|
185
|
+
read the same spec through the
|
|
186
|
+
[Program API](https://mathspec.readthedocs.io/en/latest/reference/program/).
|
|
187
|
+
The solid boxes are mathspec; the dashed boxes are outside it.
|
|
188
|
+
|
|
189
|
+
```mermaid
|
|
190
|
+
flowchart LR
|
|
191
|
+
accTitle: What mathspec does, and what other tools do with a spec
|
|
192
|
+
accDescr: A YAML file loads into a Spec and the Program it lowers to. mathspec checks the spec and prints it as math, with no data. Outside mathspec, drawn dashed, an engine such as specsolve or linopy reads the same spec, takes your data and returns your answers, and any other tool, such as a renderer or an analyser, reads the same spec through the Program API.
|
|
193
|
+
Y(["your spec<br/>one YAML file"]) --> SPEC["<b>Spec</b> and the <b>Program</b> it lowers to<br/><i>checked before any data exists</i>"]
|
|
194
|
+
SPEC --> CHECK["<b>check it</b><br/>python -m mathspec check"]
|
|
195
|
+
SPEC --> SHOW["<b>print it as math</b><br/>LaTeX · Typst · Markdown"]
|
|
196
|
+
SPEC -.-> OTHER["<b>your own tool</b><br/>a renderer · an analyser · …<br/>reads the Program API"]
|
|
197
|
+
SPEC -.-> ENGINE["<b>an engine</b><br/>specsolve · linopy<br/>builds and solves the model"]
|
|
198
|
+
DATA[("your data")] -.-> ENGINE
|
|
199
|
+
ENGINE -.-> ANS(["your answers"])
|
|
200
|
+
classDef outside stroke-dasharray:5 4
|
|
201
|
+
class ENGINE,DATA,ANS,OTHER outside
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
<!--- --8<-- [end:engines] -->
|
|
205
|
+
|
|
186
206
|
## Documentation
|
|
187
207
|
|
|
188
208
|
The documentation is at <https://mathspec.readthedocs.io>.
|
|
@@ -195,11 +215,10 @@ See [installation](https://mathspec.readthedocs.io/en/latest/howto/installation/
|
|
|
195
215
|
## Prior art
|
|
196
216
|
|
|
197
217
|
Every file under `src/` was written in [specsolve](https://github.com/fluxopt/specsolve)
|
|
198
|
-
and extracted here. The keys
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
[linopy](https://github.com/PyPSA/linopy)
|
|
202
|
-
`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).
|
|
203
222
|
|
|
204
223
|
## Status
|
|
205
224
|
|
|
@@ -210,9 +229,9 @@ Alpha, pre-1.0.
|
|
|
210
229
|
**Breaking changes land without a deprecation cycle.** Pin an exact version if
|
|
211
230
|
you depend on this, and read the
|
|
212
231
|
[changelog](https://github.com/energy-models/mathspec/blob/main/CHANGELOG.md)
|
|
213
|
-
before upgrading.
|
|
214
|
-
and all three typeset formats, and the LaTeX
|
|
215
|
-
|
|
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.
|
|
216
235
|
|
|
217
236
|
<!--- --8<-- [end:status] -->
|
|
218
237
|
|
|
@@ -211,12 +211,13 @@ extend-exclude = ["CHANGELOG.md"]
|
|
|
211
211
|
[tool.typos.default.extend-words]
|
|
212
212
|
# Each of these is deliberate, and none is reachable by fixing a spelling.
|
|
213
213
|
#
|
|
214
|
-
# `wher` and `
|
|
215
|
-
# did-you-mean suggester, so correcting them deletes the case. `missable` is the
|
|
214
|
+
# `wher`, `generatr` and `balnce` are misspellings *on purpose*: they are the
|
|
215
|
+
# input to the did-you-mean suggester, so correcting them deletes the case. `missable` is the
|
|
216
216
|
# word the sentence wants — a coordinate that can go missing — and is not
|
|
217
217
|
# "miscible". `unparseable` is the accepted variant and is the name of a test.
|
|
218
218
|
wher = "wher"
|
|
219
219
|
generatr = "generatr"
|
|
220
|
+
balnce = "balnce"
|
|
220
221
|
missable = "missable"
|
|
221
222
|
unparseable = "unparseable"
|
|
222
223
|
# `fom` is PyPSA's `fom_cost` — fixed operation and maintenance — quoted by name
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""The language: what a YAML file may say, and what it means.
|
|
6
|
+
|
|
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.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from mathspec import errors, program, spec
|
|
23
|
+
from mathspec.advising import advice
|
|
24
|
+
from mathspec.composition import merge, override
|
|
25
|
+
from mathspec.typesetting import (
|
|
26
|
+
FormatName,
|
|
27
|
+
SymbolTable,
|
|
28
|
+
to_latex,
|
|
29
|
+
to_markdown,
|
|
30
|
+
to_typst,
|
|
31
|
+
typeset,
|
|
32
|
+
typeset_declaration,
|
|
33
|
+
)
|
|
34
|
+
from mathspec.validation import to_spec
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
'FormatName',
|
|
38
|
+
'SymbolTable',
|
|
39
|
+
'advice',
|
|
40
|
+
'errors',
|
|
41
|
+
'merge',
|
|
42
|
+
'override',
|
|
43
|
+
'program',
|
|
44
|
+
'spec',
|
|
45
|
+
'to_latex',
|
|
46
|
+
'to_markdown',
|
|
47
|
+
'to_spec',
|
|
48
|
+
'to_typst',
|
|
49
|
+
'typeset',
|
|
50
|
+
'typeset_declaration',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
import warnings as _warnings
|
|
54
|
+
from importlib import metadata as _metadata
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
__version__ = _metadata.version(__name__)
|
|
58
|
+
except _metadata.PackageNotFoundError as e: # pragma: no cover
|
|
59
|
+
_warnings.warn(f'Could not determine version of {__name__}\n{e!s}', stacklevel=2)
|
|
60
|
+
__version__ = 'unknown'
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
"""``python -m mathspec <verb> spec.yaml`` — the shell front.
|
|
6
6
|
|
|
7
|
-
``check`` loads the file and prints the language's advice;
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
``check`` loads the file and prints the language's advice; ``canonical``
|
|
8
|
+
writes the spec in the form two files that state the same spec share, or with
|
|
9
|
+
``--check`` asks whether the file is already in that form; one further verb per
|
|
10
|
+
typeset format, read off [`mathspec.typesetting.FORMATS`][]. Every verb reads
|
|
11
|
+
the file as written, and nothing here writes a formulation out unasked.
|
|
10
12
|
The typeset verbs take ``--expand``, because a shell cannot compose
|
|
11
13
|
[`expand`][mathspec.spec.Spec.expand] the way a caller does and the rows are a
|
|
12
14
|
different document; ``check`` has no such flag, because advice reads a block
|
|
@@ -33,6 +35,13 @@ def parser() -> argparse.ArgumentParser:
|
|
|
33
35
|
check = verbs.add_parser('check', help='load a spec, and print what the language advises')
|
|
34
36
|
check.add_argument('spec', help='path to a mathspec YAML file')
|
|
35
37
|
|
|
38
|
+
canonical = verbs.add_parser('canonical', help='write a spec in the form two files that state it share')
|
|
39
|
+
canonical.add_argument('spec', help='path to a mathspec YAML file')
|
|
40
|
+
target = canonical.add_mutually_exclusive_group()
|
|
41
|
+
target.add_argument('-o', '--out', help='write here instead of stdout')
|
|
42
|
+
target.add_argument('--write', action='store_true', help='rewrite the file in the form, dropping its comments')
|
|
43
|
+
target.add_argument('--check', action='store_true', help='exit 1 if the file is not in the form, writing nothing')
|
|
44
|
+
|
|
36
45
|
for name in FORMATS:
|
|
37
46
|
verb = verbs.add_parser(name, help=f'render a spec as {name}')
|
|
38
47
|
verb.add_argument('spec', help='path to a mathspec YAML file')
|
|
@@ -52,10 +61,26 @@ def parser() -> argparse.ArgumentParser:
|
|
|
52
61
|
return front
|
|
53
62
|
|
|
54
63
|
|
|
64
|
+
def _checked(path: str, text: str) -> int:
|
|
65
|
+
"""Exit status 0 if the file at *path* already holds *text*, the form, and 1 with the rewrite on stderr if not.
|
|
66
|
+
|
|
67
|
+
A CI job runs this, so it compares bytes and writes nothing: the job fails,
|
|
68
|
+
and the author runs the rewrite the message names.
|
|
69
|
+
"""
|
|
70
|
+
if Path(path).read_text(encoding='utf-8') == text:
|
|
71
|
+
return 0
|
|
72
|
+
sys.stderr.write(
|
|
73
|
+
f'{path} is not in the canonical form. Run `python -m mathspec canonical --write {path}` to rewrite it.\n'
|
|
74
|
+
)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
|
|
55
78
|
def main(argv: list[str] | None = None) -> int:
|
|
56
79
|
"""Run one verb; a refused file is its message on stderr and exit status 1.
|
|
57
80
|
|
|
58
|
-
Advice is not a refusal: ``check`` prints it and exits 0.
|
|
81
|
+
Advice is not a refusal: ``check`` prints it and exits 0. A file that
|
|
82
|
+
``canonical --check`` finds out of the form exits 1, with the rewrite on
|
|
83
|
+
stderr.
|
|
59
84
|
"""
|
|
60
85
|
args = parser().parse_args(argv)
|
|
61
86
|
if args.verb == 'check':
|
|
@@ -66,16 +91,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
66
91
|
return 1
|
|
67
92
|
sys.stdout.write(''.join(f'{note}\n' for note in notes))
|
|
68
93
|
return 0
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
if args.verb == 'canonical':
|
|
95
|
+
try:
|
|
96
|
+
text = to_spec(args.spec).to_yaml(canonical=True)
|
|
97
|
+
except MathSpecError as e:
|
|
98
|
+
sys.stderr.write(f'{e}\n')
|
|
99
|
+
return 1
|
|
100
|
+
if args.check:
|
|
101
|
+
return _checked(args.spec, text)
|
|
102
|
+
if args.write:
|
|
103
|
+
Path(args.spec).write_text(text, encoding='utf-8')
|
|
104
|
+
return 0
|
|
105
|
+
else:
|
|
106
|
+
spec = to_spec(args.spec).expand() if args.expand else args.spec
|
|
107
|
+
text = typeset(
|
|
108
|
+
spec,
|
|
109
|
+
args.verb,
|
|
110
|
+
symbols=args.symbols,
|
|
111
|
+
standalone=args.standalone,
|
|
112
|
+
legend=not args.no_legend,
|
|
113
|
+
numbered=not args.no_numbers,
|
|
114
|
+
inline_expressions=args.inline_expressions,
|
|
115
|
+
)
|
|
79
116
|
if args.out:
|
|
80
117
|
Path(args.out).write_text(text, encoding='utf-8')
|
|
81
118
|
else:
|
|
@@ -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))
|
|
@@ -220,8 +244,15 @@ def with_children(node: ArithmeticNode, recurse: Callable[[ArithmeticNode], Arit
|
|
|
220
244
|
# ---------------------------------------------------------------------------
|
|
221
245
|
|
|
222
246
|
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
#: A bracketed list of names as a kwarg value, in an expression and in a where
|
|
248
|
+
#: string alike.
|
|
249
|
+
NAME_LIST = (pp.Suppress('[') + pp.DelimitedList(pp.Regex(NAME)) + pp.Suppress(']')).set_parse_action(
|
|
250
|
+
lambda t: NameListNode(tuple(str(x) for x in t))
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
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.
|
|
225
256
|
|
|
226
257
|
``inf`` is a ``pp.Keyword`` rather than a ``pp.Literal``, which would
|
|
227
258
|
match the prefix of ``inflow``.
|
|
@@ -235,10 +266,10 @@ def _build_grammar() -> tuple[pp.ParserElement, pp.ParserElement]:
|
|
|
235
266
|
name = pp.Regex(NAME)
|
|
236
267
|
|
|
237
268
|
quoted = (pp.QuotedString("'") | pp.QuotedString('"')).set_parse_action(lambda t: KeywordNode(str(t[0])))
|
|
238
|
-
|
|
239
|
-
lambda t:
|
|
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:]))
|
|
240
271
|
)
|
|
241
|
-
kwarg = (name + pp.Suppress('=') + (quoted |
|
|
272
|
+
kwarg = (name + pp.Suppress('=') + (quoted | columns | NAME_LIST | arith)).set_parse_action(lambda t: (t[0], t[1]))
|
|
242
273
|
pos_arg = arith
|
|
243
274
|
arg_list = pp.Optional(pp.DelimitedList(kwarg | pos_arg))
|
|
244
275
|
func_call = (name + pp.Suppress('(') + arg_list + pp.Suppress(')')).set_parse_action(_make_func_call)
|
|
@@ -264,7 +295,7 @@ def _build_grammar() -> tuple[pp.ParserElement, pp.ParserElement]:
|
|
|
264
295
|
expression = (arith + pp.Optional(comparator + arith)).set_parse_action(
|
|
265
296
|
lambda t: ComparisonNode(t[1], t[0], t[2]) if len(t) == 3 else t[0]
|
|
266
297
|
)
|
|
267
|
-
return arith, expression
|
|
298
|
+
return arith, expression, columns
|
|
268
299
|
|
|
269
300
|
|
|
270
301
|
def _make_func_call(tokens: pp.ParseResults) -> FunctionCallNode:
|
|
@@ -282,7 +313,7 @@ def keywords[V](name: str, pairs: Iterable[tuple[str, V]]) -> dict[str, V]:
|
|
|
282
313
|
kwargs: dict[str, V] = {}
|
|
283
314
|
for key, value in pairs:
|
|
284
315
|
if key in kwargs:
|
|
285
|
-
msg = f'{name}({key}=) is given twice.
|
|
316
|
+
msg = f'{name}({key}=) is given twice. Delete one of them.'
|
|
286
317
|
raise SchemaError(msg)
|
|
287
318
|
kwargs[key] = value
|
|
288
319
|
return kwargs
|
|
@@ -304,7 +335,9 @@ def _make_power(tokens: pp.ParseResults) -> ArithmeticNode:
|
|
|
304
335
|
|
|
305
336
|
#: The arithmetic half on its own, for the where grammar to put a predicate's
|
|
306
337
|
#: comparator over: one grammar for what a side may say, wherever it stands.
|
|
307
|
-
|
|
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()
|
|
308
341
|
|
|
309
342
|
|
|
310
343
|
#: How deep a tree the language admits. Every pass over an expression recurses,
|
|
@@ -359,7 +392,7 @@ def parse_text[T](
|
|
|
359
392
|
result = grammar.parse_string(text, parse_all=True)
|
|
360
393
|
except pp.ParseException as e:
|
|
361
394
|
hint = rewrite(text, e.loc)
|
|
362
|
-
msg = f'
|
|
395
|
+
msg = f'Cannot parse the {what} {text!r}.\n{f"{hint}\n" if hint is not None else ""}{e}'
|
|
363
396
|
raise SchemaError(msg) from e
|
|
364
397
|
except RecursionError:
|
|
365
398
|
raise SchemaError(_too_deep(what, text, None, deep_rewrite)) from None
|
|
@@ -377,22 +410,13 @@ def _named_rewrite(text: str, loc: int) -> str | None:
|
|
|
377
410
|
"""
|
|
378
411
|
rest = text[loc:].lstrip()
|
|
379
412
|
if rest.startswith(get_args(ComparisonOperator)):
|
|
380
|
-
return
|
|
381
|
-
f"'{rest[:2]}' follows a complete comparison, and an expression carries "
|
|
382
|
-
f'one comparison, at the top. Split the chain into two constraints.'
|
|
383
|
-
)
|
|
413
|
+
return f"'{rest[:2]}' follows a complete comparison. Split the chain into two constraints."
|
|
384
414
|
if rest.startswith('!='):
|
|
385
|
-
return
|
|
386
|
-
"'!=' is not a constraint sense — the senses are <=, >= and ==. "
|
|
387
|
-
'Holding rows apart is a where matter: write the test in where:, where != is legal.'
|
|
388
|
-
)
|
|
415
|
+
return "'!=' is not a constraint sense. Write <=, >= or ==, or move the != test to where:."
|
|
389
416
|
if rest.startswith(('<', '>')):
|
|
390
|
-
return f"'{rest[0]}' is not a constraint sense
|
|
417
|
+
return f"'{rest[0]}' is not a constraint sense. Write '{rest[0]}=' instead."
|
|
391
418
|
if rest.startswith('='):
|
|
392
|
-
return (
|
|
393
|
-
"'=' on its own is how a kwarg is written inside a call, like sum(x, over=d). "
|
|
394
|
-
'Equality between two sides is written ==.'
|
|
395
|
-
)
|
|
419
|
+
return "A single '=' passes a keyword argument, as in sum(x, over=d). Write == for equality."
|
|
396
420
|
if rest.startswith('^'):
|
|
397
421
|
return "power is written '**', not '^'."
|
|
398
422
|
return None
|