mathspec 0.1.0__tar.gz → 0.2.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.1.0 → mathspec-0.2.0}/PKG-INFO +15 -15
- {mathspec-0.1.0 → mathspec-0.2.0}/README.md +13 -13
- {mathspec-0.1.0 → mathspec-0.2.0}/pyproject.toml +1 -1
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/__init__.py +5 -5
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/__main__.py +11 -11
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/_expression_parser.py +7 -7
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/_expression_resolver.py +8 -8
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/_sealed.py +1 -1
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/_where_parser.py +7 -7
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/_where_resolver.py +3 -3
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/_yaml.py +9 -9
- mathspec-0.1.0/src/mathspec/advice.py → mathspec-0.2.0/src/mathspec/advising.py +15 -9
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/boundedness.py +2 -2
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/degree.py +4 -4
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/dimensions.py +2 -2
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/errors.py +5 -5
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/exclusivity.py +2 -2
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/expansion.py +1 -1
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/lowering.py +10 -10
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/operators.py +1 -1
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/piecewise.py +15 -15
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/program.py +78 -78
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/resolution.py +12 -12
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/separability.py +2 -2
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/sos.py +12 -12
- mathspec-0.1.0/src/mathspec/model.py → mathspec-0.2.0/src/mathspec/spec.py +42 -43
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/README.md +8 -8
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/__init__.py +42 -42
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/format.py +7 -7
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/latex.py +1 -1
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/legend.py +6 -6
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/markdown.py +3 -3
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/symbols.py +12 -12
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/typst.py +1 -1
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/typesetting/walk.py +5 -5
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/validation.py +16 -16
- {mathspec-0.1.0 → mathspec-0.2.0}/.gitignore +0 -0
- {mathspec-0.1.0 → mathspec-0.2.0}/LICENSE +0 -0
- {mathspec-0.1.0 → mathspec-0.2.0}/LICENSES/Apache-2.0.txt +0 -0
- {mathspec-0.1.0 → mathspec-0.2.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {mathspec-0.1.0 → mathspec-0.2.0}/LICENSES/CC0-1.0.txt +0 -0
- {mathspec-0.1.0 → mathspec-0.2.0}/LICENSES/MIT.txt +0 -0
- {mathspec-0.1.0 → mathspec-0.2.0}/src/mathspec/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mathspec
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Write an optimisation model as a YAML file. Check it and print it as math, with no data and no solver.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
7
7
|
Project-URL: Issues, https://github.com/energy-models/mathspec/issues
|
|
@@ -40,36 +40,36 @@ SPDX-License-Identifier: CC-BY-4.0
|
|
|
40
40
|
|
|
41
41
|
<!--- --8<-- [end:badges] -->
|
|
42
42
|
|
|
43
|
-
**Write an optimisation model as a YAML file. Check it and print it
|
|
44
|
-
with no data and no solver.**
|
|
43
|
+
**Write the specification (spec) of an optimisation model as a YAML file. Check it and print it
|
|
44
|
+
as math, with no data and no solver.**
|
|
45
45
|
|
|
46
|
-
A mathspec file declares four things: the axes
|
|
47
|
-
`snapshot` and `generator`; the data it expects, such as `load`
|
|
48
|
-
decisions the solver makes, such as `dispatch`; and the rules
|
|
49
|
-
obey, such as `sum(dispatch, over=generator) == load`. The file
|
|
50
|
-
[below](#example) is a complete
|
|
46
|
+
A mathspec file states a specification, or spec. A spec declares four things: the axes it runs
|
|
47
|
+
over, such as `snapshot` and `generator`; the data it expects, such as `load`
|
|
48
|
+
and `cost`; the decisions the solver makes, such as `dispatch`; and the rules
|
|
49
|
+
those decisions obey, such as `sum(dispatch, over=generator) == load`. The file
|
|
50
|
+
[below](#example) is a complete spec.
|
|
51
51
|
|
|
52
52
|
<!--- --8<-- [start:engines] -->
|
|
53
53
|
|
|
54
54
|
mathspec builds nothing and solves nothing itself.
|
|
55
55
|
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
56
|
-
[linopy](https://github.com/PyPSA/linopy) build
|
|
57
|
-
Support in both is work in progress.
|
|
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
58
|
|
|
59
59
|
<!--- --8<-- [end:engines] -->
|
|
60
60
|
|
|
61
61
|
<!--- --8<-- [start:benefits] -->
|
|
62
62
|
|
|
63
|
-
- **Check
|
|
63
|
+
- **Check specs in CI, with no data.** A wrong name or
|
|
64
64
|
dimension fails when the file loads, and the error names the fix.
|
|
65
65
|
[Errors →](https://mathspec.readthedocs.io/en/latest/reference/language/errors/)
|
|
66
66
|
- **Publish the math you solve.** The equations in the paper
|
|
67
67
|
print from the file the solver reads.
|
|
68
68
|
[Typeset →](https://mathspec.readthedocs.io/en/latest/reference/typeset/)
|
|
69
|
-
- **One
|
|
69
|
+
- **One spec, many tools.** Engines, renderers and analysers read the spec
|
|
70
70
|
through one public API, so no two of them can read the file differently.
|
|
71
71
|
[Program API →](https://mathspec.readthedocs.io/en/latest/reference/program/)
|
|
72
|
-
- **Write full-size
|
|
72
|
+
- **Write full-size specs.** The spec of PyPSA's `n.optimize()` model is
|
|
73
73
|
one file, with stochastic, multi-period and quadratic variants.
|
|
74
74
|
[PyPSA in one file →](https://mathspec.readthedocs.io/en/latest/examples/pypsa/)
|
|
75
75
|
|
|
@@ -206,7 +206,7 @@ ms.to_typst(spec)
|
|
|
206
206
|
|
|
207
207
|
A [symbol table](https://mathspec.readthedocs.io/en/latest/reference/typeset/#symbol-tables) gives the names their
|
|
208
208
|
conventional spelling, as in the folded block.
|
|
209
|
-
[Print a
|
|
209
|
+
[Print a spec as math](https://mathspec.readthedocs.io/en/latest/howto/print/) does the same from a shell.
|
|
210
210
|
|
|
211
211
|
## Documentation
|
|
212
212
|
|
|
@@ -15,36 +15,36 @@ SPDX-License-Identifier: CC-BY-4.0
|
|
|
15
15
|
|
|
16
16
|
<!--- --8<-- [end:badges] -->
|
|
17
17
|
|
|
18
|
-
**Write an optimisation model as a YAML file. Check it and print it
|
|
19
|
-
with no data and no solver.**
|
|
18
|
+
**Write the specification (spec) of an optimisation model as a YAML file. Check it and print it
|
|
19
|
+
as math, with no data and no solver.**
|
|
20
20
|
|
|
21
|
-
A mathspec file declares four things: the axes
|
|
22
|
-
`snapshot` and `generator`; the data it expects, such as `load`
|
|
23
|
-
decisions the solver makes, such as `dispatch`; and the rules
|
|
24
|
-
obey, such as `sum(dispatch, over=generator) == load`. The file
|
|
25
|
-
[below](#example) is a complete
|
|
21
|
+
A mathspec file states a specification, or spec. A spec declares four things: the axes it runs
|
|
22
|
+
over, such as `snapshot` and `generator`; the data it expects, such as `load`
|
|
23
|
+
and `cost`; the decisions the solver makes, such as `dispatch`; and the rules
|
|
24
|
+
those decisions obey, such as `sum(dispatch, over=generator) == load`. The file
|
|
25
|
+
[below](#example) is a complete spec.
|
|
26
26
|
|
|
27
27
|
<!--- --8<-- [start:engines] -->
|
|
28
28
|
|
|
29
29
|
mathspec builds nothing and solves nothing itself.
|
|
30
30
|
[specsolve](https://github.com/fluxopt/specsolve) and
|
|
31
|
-
[linopy](https://github.com/PyPSA/linopy) build
|
|
32
|
-
Support in both is work in progress.
|
|
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
33
|
|
|
34
34
|
<!--- --8<-- [end:engines] -->
|
|
35
35
|
|
|
36
36
|
<!--- --8<-- [start:benefits] -->
|
|
37
37
|
|
|
38
|
-
- **Check
|
|
38
|
+
- **Check specs in CI, with no data.** A wrong name or
|
|
39
39
|
dimension fails when the file loads, and the error names the fix.
|
|
40
40
|
[Errors →](https://mathspec.readthedocs.io/en/latest/reference/language/errors/)
|
|
41
41
|
- **Publish the math you solve.** The equations in the paper
|
|
42
42
|
print from the file the solver reads.
|
|
43
43
|
[Typeset →](https://mathspec.readthedocs.io/en/latest/reference/typeset/)
|
|
44
|
-
- **One
|
|
44
|
+
- **One spec, many tools.** Engines, renderers and analysers read the spec
|
|
45
45
|
through one public API, so no two of them can read the file differently.
|
|
46
46
|
[Program API →](https://mathspec.readthedocs.io/en/latest/reference/program/)
|
|
47
|
-
- **Write full-size
|
|
47
|
+
- **Write full-size specs.** The spec of PyPSA's `n.optimize()` model is
|
|
48
48
|
one file, with stochastic, multi-period and quadratic variants.
|
|
49
49
|
[PyPSA in one file →](https://mathspec.readthedocs.io/en/latest/examples/pypsa/)
|
|
50
50
|
|
|
@@ -181,7 +181,7 @@ ms.to_typst(spec)
|
|
|
181
181
|
|
|
182
182
|
A [symbol table](https://mathspec.readthedocs.io/en/latest/reference/typeset/#symbol-tables) gives the names their
|
|
183
183
|
conventional spelling, as in the folded block.
|
|
184
|
-
[Print a
|
|
184
|
+
[Print a spec as math](https://mathspec.readthedocs.io/en/latest/howto/print/) does the same from a shell.
|
|
185
185
|
|
|
186
186
|
## Documentation
|
|
187
187
|
|
|
@@ -20,7 +20,7 @@ only-include = ["src", "README.md", "LICENSE", "LICENSES"]
|
|
|
20
20
|
|
|
21
21
|
[project]
|
|
22
22
|
name = "mathspec"
|
|
23
|
-
description = "Write an optimisation model as a YAML file. Check it and print it as math, with no data and no solver."
|
|
23
|
+
description = "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."
|
|
24
24
|
authors = [
|
|
25
25
|
{ name = "Felix Bumann", email = "117816358+FBumann@users.noreply.github.com" },
|
|
26
26
|
{ name = "Bryn Pickering", email = "17178478+brynpickering@users.noreply.github.com" },
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
"""The language: what a YAML file may say, and what it means.
|
|
6
6
|
|
|
7
|
-
Two public states — a
|
|
8
|
-
and its
|
|
9
|
-
|
|
7
|
+
Two public states — a [`Spec`][] is what the file *says*,
|
|
8
|
+
and its [`program`][mathspec.spec.Spec.program] is what it *means* — and
|
|
9
|
+
[`to_spec`][], the one door to both. Everything between them — both grammars
|
|
10
10
|
and the tree they build — is package-private, because a consumer reads a
|
|
11
11
|
program instead.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from mathspec import program
|
|
15
|
-
from mathspec.
|
|
15
|
+
from mathspec.advising import advice
|
|
16
16
|
from mathspec.errors import Advice, AdviceKind, DimensionError, LanguageError, MathSpecError, SchemaError, did_you_mean
|
|
17
|
-
from mathspec.model import Spec
|
|
18
17
|
from mathspec.operators import BUILTIN_NAMES
|
|
18
|
+
from mathspec.spec import Spec
|
|
19
19
|
from mathspec.typesetting import (
|
|
20
20
|
FORMATS,
|
|
21
21
|
SymbolTable,
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
|
-
"""``python -m mathspec <verb>
|
|
5
|
+
"""``python -m mathspec <verb> spec.yaml`` — the shell front.
|
|
6
6
|
|
|
7
7
|
``check`` loads the file and prints the language's advice; one further verb
|
|
8
|
-
per typeset format, read off
|
|
8
|
+
per typeset format, read off [`mathspec.typesetting.FORMATS`][]. Every verb
|
|
9
9
|
reads the file as written, and nothing here writes a formulation out unasked.
|
|
10
10
|
The typeset verbs take ``--expand``, because a shell cannot compose
|
|
11
|
-
|
|
11
|
+
[`expand`][mathspec.spec.Spec.expand] the way a caller does and the rows are a
|
|
12
12
|
different document; ``check`` has no such flag, because advice reads a block
|
|
13
13
|
as the rows it states.
|
|
14
14
|
"""
|
|
@@ -19,7 +19,7 @@ import argparse
|
|
|
19
19
|
import sys
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
|
-
from mathspec.
|
|
22
|
+
from mathspec.advising import advice
|
|
23
23
|
from mathspec.errors import MathSpecError
|
|
24
24
|
from mathspec.typesetting import FORMATS, typeset
|
|
25
25
|
from mathspec.validation import to_spec
|
|
@@ -30,12 +30,12 @@ def parser() -> argparse.ArgumentParser:
|
|
|
30
30
|
front = argparse.ArgumentParser(prog='python -m mathspec')
|
|
31
31
|
verbs = front.add_subparsers(dest='verb', required=True)
|
|
32
32
|
|
|
33
|
-
check = verbs.add_parser('check', help='load a
|
|
34
|
-
check.add_argument('
|
|
33
|
+
check = verbs.add_parser('check', help='load a spec, and print what the language advises')
|
|
34
|
+
check.add_argument('spec', help='path to a mathspec YAML file')
|
|
35
35
|
|
|
36
36
|
for name in FORMATS:
|
|
37
|
-
verb = verbs.add_parser(name, help=f'render a
|
|
38
|
-
verb.add_argument('
|
|
37
|
+
verb = verbs.add_parser(name, help=f'render a spec as {name}')
|
|
38
|
+
verb.add_argument('spec', help='path to a mathspec YAML file')
|
|
39
39
|
verb.add_argument('-o', '--out', help='write here instead of stdout')
|
|
40
40
|
verb.add_argument('--symbols', help='sidecar YAML saying how names should print')
|
|
41
41
|
verb.add_argument('--standalone', action='store_true', help='emit a compilable document')
|
|
@@ -60,15 +60,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
60
60
|
args = parser().parse_args(argv)
|
|
61
61
|
if args.verb == 'check':
|
|
62
62
|
try:
|
|
63
|
-
notes = advice(args.
|
|
63
|
+
notes = advice(args.spec)
|
|
64
64
|
except MathSpecError as e:
|
|
65
65
|
sys.stderr.write(f'{e}\n')
|
|
66
66
|
return 1
|
|
67
67
|
sys.stdout.write(''.join(f'{note}\n' for note in notes))
|
|
68
68
|
return 0
|
|
69
|
-
|
|
69
|
+
spec = to_spec(args.spec).expand() if args.expand else args.spec
|
|
70
70
|
text = typeset(
|
|
71
|
-
|
|
71
|
+
spec,
|
|
72
72
|
args.verb,
|
|
73
73
|
symbols=args.symbols,
|
|
74
74
|
standalone=args.standalone,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"""The syntax tree the expression grammar builds, and the grammar — package-private.
|
|
6
6
|
|
|
7
7
|
Only expansion and resolution read it: resolution rewrites it into the
|
|
8
|
-
|
|
8
|
+
[`mathspec.program`][] vocabulary, which every pass after reads. Arithmetic
|
|
9
9
|
nests anywhere; a comparison appears only at the top of a parsed expression.
|
|
10
10
|
"""
|
|
11
11
|
|
|
@@ -31,7 +31,7 @@ ComparisonOperator = Literal['<=', '>=', '==']
|
|
|
31
31
|
UnaryOperator = Literal['+', '-']
|
|
32
32
|
|
|
33
33
|
#: The arithmetic a binary operator may spell. Closed by the grammar, and the
|
|
34
|
-
#: vocabulary a renderer dispatching on
|
|
34
|
+
#: vocabulary a renderer dispatching on [`BinaryOperatorNode.op`][]
|
|
35
35
|
#: switches over — it keeps no list of its own.
|
|
36
36
|
BinaryOperator = Literal['+', '-', '*', '/', '**']
|
|
37
37
|
|
|
@@ -145,7 +145,7 @@ class ComparisonNode:
|
|
|
145
145
|
right: ArithmeticNode
|
|
146
146
|
|
|
147
147
|
def __str__(self) -> str:
|
|
148
|
-
"""Both sides bare: a comparison is not an
|
|
148
|
+
"""Both sides bare: a comparison is not an [`ArithmeticNode`][], so nothing can take one as an operand."""
|
|
149
149
|
return f'{self.left} {self.op} {self.right}'
|
|
150
150
|
|
|
151
151
|
|
|
@@ -191,7 +191,7 @@ def nodes(*roots: ParsedNode) -> Iterator[ParsedNode]:
|
|
|
191
191
|
"""Every node under *roots*, each root itself included, parents first.
|
|
192
192
|
|
|
193
193
|
The traversal a question about a tree is a filter of, the way
|
|
194
|
-
|
|
194
|
+
[`mathspec.program.walk`][] is for a program.
|
|
195
195
|
"""
|
|
196
196
|
for root in roots:
|
|
197
197
|
yield root
|
|
@@ -199,7 +199,7 @@ def nodes(*roots: ParsedNode) -> Iterator[ParsedNode]:
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def with_children(node: ArithmeticNode, recurse: Callable[[ArithmeticNode], ArithmeticNode]) -> ArithmeticNode:
|
|
202
|
-
"""*node* rebuilt with *recurse* applied to each of its
|
|
202
|
+
"""*node* rebuilt with *recurse* applied to each of its [`children`][]; a leaf comes back as is."""
|
|
203
203
|
if isinstance(node, NumberNode | NameNode | NameListNode | KeywordNode):
|
|
204
204
|
return node
|
|
205
205
|
if isinstance(node, UnaryOperatorNode):
|
|
@@ -346,11 +346,11 @@ def parse_text[T](
|
|
|
346
346
|
child_of: Callable[[T], tuple[T, ...]],
|
|
347
347
|
deep_rewrite: str,
|
|
348
348
|
) -> T:
|
|
349
|
-
"""Parse the whole of *text* with *grammar*, or raise
|
|
349
|
+
"""Parse the whole of *text* with *grammar*, or raise [`SchemaError`][] naming *what* failed to parse.
|
|
350
350
|
|
|
351
351
|
*rewrite* is asked for the predictable mistake at the failure position; its
|
|
352
352
|
sentence, if any, precedes the grammar's own complaint. A tree nesting past
|
|
353
|
-
|
|
353
|
+
[`MAX_DEPTH`][], measured through *child_of*, is refused with
|
|
354
354
|
*deep_rewrite* — and so is one the parser itself ran out of stack on. The
|
|
355
355
|
node comes back as the type *child_of* walks, which is the grammar's word
|
|
356
356
|
for what it builds.
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
"""The expression walk of name resolution: an arithmetic syntax tree into the program's expression nodes.
|
|
6
6
|
|
|
7
|
-
Every operator call is read here — its shape against
|
|
7
|
+
Every operator call is read here — its shape against [`BUILTINS`][],
|
|
8
8
|
its dimension and relation arguments against the namespace — and the node it
|
|
9
|
-
stands for is built.
|
|
9
|
+
stands for is built. [`mathspec.resolution`][] holds the namespace and the
|
|
10
10
|
doors that call this.
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -32,7 +32,6 @@ from mathspec._expression_parser import (
|
|
|
32
32
|
)
|
|
33
33
|
from mathspec.dimensions import dims_of
|
|
34
34
|
from mathspec.errors import DimensionError, SchemaError, did_you_mean
|
|
35
|
-
from mathspec.model import NUMERIC_DTYPES
|
|
36
35
|
from mathspec.operators import (
|
|
37
36
|
AMOUNTS,
|
|
38
37
|
BUILTINS,
|
|
@@ -62,6 +61,7 @@ from mathspec.program import (
|
|
|
62
61
|
carries_variable,
|
|
63
62
|
children,
|
|
64
63
|
)
|
|
64
|
+
from mathspec.spec import NUMERIC_DTYPES
|
|
65
65
|
|
|
66
66
|
if TYPE_CHECKING:
|
|
67
67
|
from collections.abc import Mapping
|
|
@@ -101,7 +101,7 @@ class ExpressionResolver:
|
|
|
101
101
|
return isinstance(value, NameNode) and value.name in self.formals
|
|
102
102
|
|
|
103
103
|
def build(self, node: ArithmeticNode) -> Expression | None:
|
|
104
|
-
"""The program tree *node* stands for, held to
|
|
104
|
+
"""The program tree *node* stands for, held to [`MAX_RESOLVED_DEPTH`][] before anything walks it.
|
|
105
105
|
|
|
106
106
|
The depth is measured with every named expression written in, since
|
|
107
107
|
that is the tree every later pass recurses over, and measured with an
|
|
@@ -175,9 +175,9 @@ class ExpressionResolver:
|
|
|
175
175
|
def _name(self, node: NameNode) -> Expression | None:
|
|
176
176
|
"""A bare name as the variable, parameter or named expression it declares; a dimension or relation is not a value.
|
|
177
177
|
|
|
178
|
-
A named expression arrives as the one node
|
|
178
|
+
A named expression arrives as the one node [`Namespace.named`][]
|
|
179
179
|
built for it; the cast is the one place a
|
|
180
|
-
|
|
180
|
+
[`Named`][mathspec.program.Named] enters a tree typed as a program's,
|
|
181
181
|
which lowering makes true.
|
|
182
182
|
"""
|
|
183
183
|
if node.name in self.formals:
|
|
@@ -360,7 +360,7 @@ class ExpressionResolver:
|
|
|
360
360
|
def _amount(self, value: ArithmeticNode, operator: str, key: str) -> int | str | None:
|
|
361
361
|
"""``offset=`` or ``window=``: a whole number in the operator's range, or the name of a parameter.
|
|
362
362
|
|
|
363
|
-
Closed so that
|
|
363
|
+
Closed so that [`mathspec.dimensions._check_named_amount`][] sees
|
|
364
364
|
every parameter an amount carries, and so that a program's
|
|
365
365
|
``offset`` and ``width`` are the ``int | str`` they say.
|
|
366
366
|
"""
|
|
@@ -441,7 +441,7 @@ class ExpressionResolver:
|
|
|
441
441
|
Constraints sit outside the flat namespace, so this store is consulted
|
|
442
442
|
only here — a bare name in arithmetic never reaches it. A dual standing
|
|
443
443
|
where the math is built is refused separately
|
|
444
|
-
(
|
|
444
|
+
([`mathspec.degree`][]); this pass only types the name.
|
|
445
445
|
"""
|
|
446
446
|
(value,) = node.args
|
|
447
447
|
if self._formal(value):
|
|
@@ -19,7 +19,7 @@ class Sealed[K, V](Mapping[K, V]):
|
|
|
19
19
|
What every group a program hands out is held behind, and a node's
|
|
20
20
|
keyword arguments with them: a consumer cannot rewrite what another
|
|
21
21
|
consumer reads, and the whole crosses a process — which is the one thing
|
|
22
|
-
|
|
22
|
+
[`types.MappingProxyType`][] cannot do. Equal to any mapping with the
|
|
23
23
|
same items, and hashable over them where they are, so a dataclass may
|
|
24
24
|
hold one as a default.
|
|
25
25
|
"""
|
|
@@ -8,8 +8,8 @@ A where string is a boolean algebra over comparisons, and a comparison's
|
|
|
8
8
|
sides are the expression grammar's own arithmetic. What a side *is* — a
|
|
9
9
|
parameter, a dimension, a relation column, a ``position()`` — only the schema
|
|
10
10
|
knows, so the grammar hands both sides over bare and
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
[`mathspec.resolution`][] reads them. The resolved vocabulary lives in
|
|
12
|
+
[`mathspec.program`][].
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -71,7 +71,7 @@ class UnresolvedPredicateCallNode:
|
|
|
71
71
|
arithmetic arguments, and a predicate is not arithmetic. So the where
|
|
72
72
|
grammar reads it, and resolution decides which operator the name is and
|
|
73
73
|
what the kwargs mean. ``kwargs`` is held and hashed as
|
|
74
|
-
|
|
74
|
+
[`FunctionCallNode`][mathspec._expression_parser.FunctionCallNode] holds its own.
|
|
75
75
|
"""
|
|
76
76
|
|
|
77
77
|
name: str
|
|
@@ -86,7 +86,7 @@ class UnresolvedPredicateCallNode:
|
|
|
86
86
|
class UnresolvedCountNode:
|
|
87
87
|
"""``count(<predicate>, over=<dim>) <op> <number>`` — a count against a literal.
|
|
88
88
|
|
|
89
|
-
Its own node rather than an
|
|
89
|
+
Its own node rather than an [`UnresolvedComparisonNode`][] with a call
|
|
90
90
|
on the left: every other comparison has arithmetic on both sides, and
|
|
91
91
|
widening that one to carry a predicate would widen every reader of a side
|
|
92
92
|
with it.
|
|
@@ -103,7 +103,7 @@ class UnresolvedComparisonNode:
|
|
|
103
103
|
|
|
104
104
|
A side is the expression grammar's arithmetic, so a name, a number and a
|
|
105
105
|
``position(...)`` call all arrive as the nodes an expression would carry
|
|
106
|
-
them in, a quoted label as the
|
|
106
|
+
them in, a quoted label as the [`KeywordNode`][] a quoted kwarg is, and a
|
|
107
107
|
relation column in a node of its own.
|
|
108
108
|
"""
|
|
109
109
|
|
|
@@ -272,8 +272,8 @@ def parse_where(text: str) -> Predicate | UnresolvedWhereNode:
|
|
|
272
272
|
"""Parse a where string into an AST, its leaves still unresolved.
|
|
273
273
|
|
|
274
274
|
The connectives and literals are the resolved vocabulary's own; the leaves
|
|
275
|
-
naming declarations are a bare
|
|
276
|
-
node, which only
|
|
275
|
+
naming declarations are a bare [`NameNode`][] or an ``Unresolved*``
|
|
276
|
+
node, which only [`resolve_where`][mathspec.resolution.resolve_where] takes.
|
|
277
277
|
|
|
278
278
|
Raises:
|
|
279
279
|
SchemaError: If *text* is not a where string of the language. A
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
A bare name, a comparison, a count and a predicate read through a relation
|
|
8
8
|
or along a dimension are each typed here against the namespace, and a
|
|
9
9
|
comparison of expressions hands its sides to the expression walk.
|
|
10
|
-
|
|
10
|
+
[`mathspec.resolution`][] holds the namespace and the doors that call this.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
@@ -90,7 +90,7 @@ class WhereResolver:
|
|
|
90
90
|
appended to ``errors``; every sibling is still read, so a mask with two
|
|
91
91
|
faults reports both. ``self_variable`` is the variable whose own
|
|
92
92
|
``where`` is being read, which may not ask whether it exists. A side
|
|
93
|
-
that is an expression is built by an
|
|
93
|
+
that is an expression is built by an [`ExpressionResolver`][] over
|
|
94
94
|
the same namespace.
|
|
95
95
|
"""
|
|
96
96
|
|
|
@@ -169,7 +169,7 @@ class WhereResolver:
|
|
|
169
169
|
"""``shift(<predicate>, along=, offset=)`` or ``at(<predicate>, by=, over=, into=)`` — the two operators that read a predicate and answer one.
|
|
170
170
|
|
|
171
171
|
``count`` answers a number, so it stands on a comparison's side and
|
|
172
|
-
|
|
172
|
+
[`_count`][] reads it there. Anything else naming a predicate is
|
|
173
173
|
refused here rather than resolved into arithmetic it cannot be.
|
|
174
174
|
|
|
175
175
|
An operand that failed to resolve is handed straight back: resolution
|
|
@@ -94,10 +94,10 @@ def read_yaml(path: Path | str) -> dict[str, object]:
|
|
|
94
94
|
return parse_yaml(Path(path).read_text(encoding='utf-8'), str(path))
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def
|
|
98
|
-
"""A
|
|
97
|
+
def read_spec(spec: str | Path) -> dict[str, object]:
|
|
98
|
+
"""A spec from a file or from its text — a newline decides which a ``str`` is.
|
|
99
99
|
|
|
100
|
-
A
|
|
100
|
+
A [`Path`][] names a file, and so does a ``str`` with no
|
|
101
101
|
newline in it, since no path holds one; a ``str`` with a newline is the
|
|
102
102
|
YAML itself, which every file and every ``to_yaml()`` ends with. Nothing
|
|
103
103
|
else is read off the content and nothing is read off the disk to decide.
|
|
@@ -106,15 +106,15 @@ def read_model(model: str | Path) -> dict[str, object]:
|
|
|
106
106
|
FileNotFoundError: A ``str`` with no newline that names no file — a
|
|
107
107
|
one-line text is told to end with one.
|
|
108
108
|
"""
|
|
109
|
-
if isinstance(
|
|
109
|
+
if isinstance(spec, str) and '\n' not in spec and not Path(spec).is_file():
|
|
110
110
|
msg = (
|
|
111
|
-
f'no file named {
|
|
111
|
+
f'no file named {spec!r}. A str with no newline in it is read as a path, and YAML text '
|
|
112
112
|
f'is told apart by one — end the text with a newline, or pass a Path.'
|
|
113
113
|
)
|
|
114
114
|
raise FileNotFoundError(msg)
|
|
115
|
-
if isinstance(
|
|
116
|
-
return read_yaml(Path(
|
|
117
|
-
return parse_yaml(
|
|
115
|
+
if isinstance(spec, Path) or '\n' not in spec:
|
|
116
|
+
return read_yaml(Path(spec))
|
|
117
|
+
return parse_yaml(spec, 'YAML text')
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def parse_yaml(text: str, origin: str = '<string>') -> dict[str, object]:
|
|
@@ -136,6 +136,6 @@ def parse_yaml(text: str, origin: str = '<string>') -> dict[str, object]:
|
|
|
136
136
|
if not data:
|
|
137
137
|
return {}
|
|
138
138
|
if not isinstance(data, dict):
|
|
139
|
-
msg = f'{origin}: a
|
|
139
|
+
msg = f'{origin}: a spec file must be a mapping of sections (dimensions:, variables:, …), got {type(data).__name__}.'
|
|
140
140
|
raise SchemaError(msg)
|
|
141
141
|
return data
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
"""Advice — what is decidable without data and is a note rather than a refusal.
|
|
6
6
|
|
|
7
|
-
One door,
|
|
7
|
+
One door, [`advice`][], over every pass of that kind.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -20,23 +20,29 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from collections.abc import Mapping
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
-
from mathspec.
|
|
23
|
+
from mathspec.spec import Spec
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def advice(
|
|
27
|
-
"""Everything the language advises about *
|
|
26
|
+
def advice(spec: str | Path | Mapping[str, object] | Spec | Program) -> tuple[Advice, ...]:
|
|
27
|
+
"""Everything the language advises about *spec*, decided without data.
|
|
28
|
+
|
|
29
|
+
Advice is a note, not a refusal: a file with advice still loads.
|
|
28
30
|
|
|
29
31
|
Args:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
spec: Anything [`to_spec`][] accepts, or a [`Program`][], read as
|
|
33
|
+
it arrived. A ``piecewise:`` or ``sos:`` block is read as the rows
|
|
34
|
+
it states, so the answer is the one its expansion gets, with
|
|
35
|
+
nothing expanded.
|
|
34
36
|
|
|
35
37
|
Returns:
|
|
36
38
|
The never-an-axis advice in declaration order, then the unboundedness
|
|
37
39
|
advice; ``str()`` of each is its sentence.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
LanguageError: *spec* does not load; [`to_spec`][] says why.
|
|
43
|
+
FileNotFoundError: A ``str`` with no newline that names no file.
|
|
38
44
|
"""
|
|
39
|
-
program =
|
|
45
|
+
program = spec if isinstance(spec, Program) else to_spec(spec).program
|
|
40
46
|
return tuple(_never_an_axis(program) + unbounded_notes(program))
|
|
41
47
|
|
|
42
48
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
|
-
"""Provably unbounded
|
|
5
|
+
"""Provably unbounded specs, named before a solver says a bare ``unbounded``.
|
|
6
6
|
|
|
7
7
|
A variable unbounded on the side its objective term improves toward, and named
|
|
8
8
|
by no constraint, runs to infinity for any data. Which side is read off the
|
|
@@ -84,7 +84,7 @@ def unbounded_notes(program: Program) -> list[Advice]:
|
|
|
84
84
|
Advice(
|
|
85
85
|
'unbounded',
|
|
86
86
|
vname,
|
|
87
|
-
f"Variable '{vname}' makes this
|
|
87
|
+
f"Variable '{vname}' makes this spec unbounded: no constraint names it, and "
|
|
88
88
|
f'bounds.{side} is open, which is the direction a {sign}{vname} term '
|
|
89
89
|
f'improves a {program.objective.sense} objective in. No data can change that, so '
|
|
90
90
|
f'the solve would answer `unbounded` and name nothing.\n'
|
|
@@ -103,7 +103,7 @@ def _degree(node: Expression) -> int:
|
|
|
103
103
|
"""The polynomial degree *node* stands for, counted structurally.
|
|
104
104
|
|
|
105
105
|
A product adds its factors' degrees and a division keeps the dividend's
|
|
106
|
-
(
|
|
106
|
+
([`check_binary`][] has already refused a divisor carrying a variable);
|
|
107
107
|
everything else — a sum, a reduction, a shape operator — is the highest
|
|
108
108
|
degree beneath it. No data, so this answers at ``check`` time, which is
|
|
109
109
|
what stops a cubic from reaching a consumer to be refused by whichever one
|
|
@@ -184,7 +184,7 @@ def _joins_terms(node: Expression) -> bool:
|
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
def check_expression(node: Expression, context: str, *, ceiling: int = 1) -> None:
|
|
187
|
-
"""What the math admits at one position: no dual anywhere under *node*, then
|
|
187
|
+
"""What the math admits at one position: no dual anywhere under *node*, then [`check_binary`][] everywhere in it.
|
|
188
188
|
|
|
189
189
|
Asked of the resolved tree, so a dual or a product reached through a
|
|
190
190
|
macro or a named expression is caught alongside one written in place.
|
|
@@ -192,7 +192,7 @@ def check_expression(node: Expression, context: str, *, ceiling: int = 1) -> Non
|
|
|
192
192
|
|
|
193
193
|
Raises:
|
|
194
194
|
LanguageError: A dual, which exists only after a solve; or what
|
|
195
|
-
|
|
195
|
+
[`check_binary`][] refuses.
|
|
196
196
|
"""
|
|
197
197
|
for found in walk(node):
|
|
198
198
|
if isinstance(found, Dual):
|
|
@@ -205,5 +205,5 @@ def check_expression(node: Expression, context: str, *, ceiling: int = 1) -> Non
|
|
|
205
205
|
|
|
206
206
|
|
|
207
207
|
def calls_dual(node: Expression) -> bool:
|
|
208
|
-
"""Whether a
|
|
208
|
+
"""Whether a [`Dual`][mathspec.program.Dual] stands anywhere under *node*."""
|
|
209
209
|
return any(isinstance(found, Dual) for found in walk(node))
|
|
@@ -53,8 +53,8 @@ from mathspec.program import (
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
if TYPE_CHECKING:
|
|
56
|
-
from mathspec.model import Spec
|
|
57
56
|
from mathspec.program import Program
|
|
57
|
+
from mathspec.spec import Spec
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def dims_of(node: Expression, schema: Spec, context: str) -> frozenset[str]:
|
|
@@ -343,7 +343,7 @@ def _check_where_dims(
|
|
|
343
343
|
"""A predicate may only test dims the frame carries; reducing an outside dim to fit would fail open.
|
|
344
344
|
|
|
345
345
|
The refusal names the leaf that left the frame, reading its dims as
|
|
346
|
-
|
|
346
|
+
[`dims`][mathspec.program.Mask.dims] does.
|
|
347
347
|
"""
|
|
348
348
|
if mask is None:
|
|
349
349
|
return
|
|
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from pydantic import ValidationError
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
#: Which pass an
|
|
19
|
+
#: Which pass an [`Advice`][] comes from. Closed, like the operator set: a
|
|
20
20
|
#: consumer filtering on it can enumerate every value.
|
|
21
21
|
AdviceKind = Literal['never-an-axis', 'unbounded']
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ AdviceKind = Literal['never-an-axis', 'unbounded']
|
|
|
25
25
|
class Advice:
|
|
26
26
|
"""One thing the language advises about a file it accepts.
|
|
27
27
|
|
|
28
|
-
Never an error: each is what a half-written
|
|
28
|
+
Never an error: each is what a half-written spec looks like too. A
|
|
29
29
|
consumer prints it, or filters on ``kind`` and ``subject``; the text is the
|
|
30
30
|
language's, so no consumer writes its own.
|
|
31
31
|
|
|
@@ -48,7 +48,7 @@ class MathSpecError(ValueError):
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class LanguageError(MathSpecError):
|
|
51
|
-
"""The
|
|
51
|
+
"""The spec is not sayable in the language, or does not obey its rules."""
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class SchemaError(LanguageError):
|
|
@@ -71,8 +71,8 @@ def did_you_mean(name: str, known: Iterable[str], *, label: str = 'Declared') ->
|
|
|
71
71
|
def schema_error(exc: ValidationError) -> LanguageError:
|
|
72
72
|
"""A pydantic ``ValidationError`` as one of ours.
|
|
73
73
|
|
|
74
|
-
Returns the original
|
|
75
|
-
error carries one, and a
|
|
74
|
+
Returns the original [`LanguageError`][] subclass where exactly one
|
|
75
|
+
error carries one, and a [`SchemaError`][] otherwise.
|
|
76
76
|
"""
|
|
77
77
|
errors = exc.errors()
|
|
78
78
|
lines = []
|