mathspec 0.1.0__py3-none-any.whl
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/__init__.py +59 -0
- mathspec/__main__.py +87 -0
- mathspec/_expression_parser.py +436 -0
- mathspec/_expression_resolver.py +739 -0
- mathspec/_sealed.py +62 -0
- mathspec/_where_parser.py +288 -0
- mathspec/_where_resolver.py +745 -0
- mathspec/_yaml.py +141 -0
- mathspec/advice.py +79 -0
- mathspec/boundedness.py +168 -0
- mathspec/degree.py +209 -0
- mathspec/dimensions.py +377 -0
- mathspec/errors.py +105 -0
- mathspec/exclusivity.py +587 -0
- mathspec/expansion.py +124 -0
- mathspec/lowering.py +302 -0
- mathspec/model.py +905 -0
- mathspec/operators.py +235 -0
- mathspec/piecewise.py +505 -0
- mathspec/program.py +1514 -0
- mathspec/py.typed +0 -0
- mathspec/resolution.py +475 -0
- mathspec/separability.py +185 -0
- mathspec/sos.py +159 -0
- mathspec/typesetting/README.md +110 -0
- mathspec/typesetting/__init__.py +223 -0
- mathspec/typesetting/format.py +273 -0
- mathspec/typesetting/latex.py +139 -0
- mathspec/typesetting/legend.py +286 -0
- mathspec/typesetting/markdown.py +105 -0
- mathspec/typesetting/symbols.py +310 -0
- mathspec/typesetting/typst.py +147 -0
- mathspec/typesetting/walk.py +941 -0
- mathspec/validation.py +338 -0
- mathspec-0.1.0.dist-info/METADATA +247 -0
- mathspec-0.1.0.dist-info/RECORD +38 -0
- mathspec-0.1.0.dist-info/WHEEL +4 -0
- mathspec-0.1.0.dist-info/licenses/LICENSE +21 -0
mathspec/sos.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Expand ``sos:`` blocks into the binaries and rows that state the same restriction.
|
|
6
|
+
|
|
7
|
+
A set becomes ordinary declarations under names prefixed with the block's own,
|
|
8
|
+
the way a ``piecewise:`` block becomes weights and rows; what it emits is
|
|
9
|
+
tabled in ``docs/reference/language/piecewise.md``. An unpicked member is held
|
|
10
|
+
at zero from both sides, so the rewrite states the same feasible set whatever
|
|
11
|
+
sign the member takes — what it needs is a coefficient on each side, which a
|
|
12
|
+
model declaring a set without is refused at load for
|
|
13
|
+
(:meth:`mathspec.model.Spec` validates it) rather than here.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from mathspec.model import Spec
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from mathspec.program import SosType
|
|
25
|
+
|
|
26
|
+
#: One member's two linking coefficients, below and above. ``None`` on a side is
|
|
27
|
+
#: a side the model leaves open, where no rewrite can hold the member at zero.
|
|
28
|
+
Coefficients = tuple[float | str | None, float | str | None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def coefficients(domain: str, lower: float | str | None, upper: float | str | None) -> Coefficients:
|
|
32
|
+
"""What a member's two linking rows multiply its binary by, ``None`` on a side the model leaves open.
|
|
33
|
+
|
|
34
|
+
The 0 and 1 a binary's domain fixes, which no bounds block carries;
|
|
35
|
+
otherwise the member's own declared bounds, each a number or the name of a
|
|
36
|
+
parameter. A parameter is a coefficient like any other: it is what the row
|
|
37
|
+
multiplies by, and no rewrite needs to know its value. Nothing else is a
|
|
38
|
+
coefficient: a number below the bound would cap a picked member the set
|
|
39
|
+
does not cap, and one above it is a looser row than the bound already
|
|
40
|
+
states.
|
|
41
|
+
"""
|
|
42
|
+
if domain == 'binary':
|
|
43
|
+
return 0.0, 1.0
|
|
44
|
+
return lower, upper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Emitted:
|
|
49
|
+
"""Every name one set's expansion writes, spelled once for the emitter and the collision check.
|
|
50
|
+
|
|
51
|
+
The linking row is named after what it says, which the two orders do not
|
|
52
|
+
share: order 2 admits a member in either half of one segment, and order 1
|
|
53
|
+
admits it alone. ``below`` is the same row from underneath, written only
|
|
54
|
+
where the member may be nonzero below zero.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
seg: str
|
|
58
|
+
pick: str
|
|
59
|
+
link: str
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def of(cls, name: str, order: SosType) -> Emitted:
|
|
63
|
+
"""The names set *name* of *order* writes."""
|
|
64
|
+
admits = 'adjacency' if order == 2 else 'nonzero'
|
|
65
|
+
return cls(f'{name}_seg', f'{name}_pick', f'{name}_{admits}')
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def below(self) -> str:
|
|
69
|
+
"""The linking row from underneath."""
|
|
70
|
+
return f'{self.link}_below'
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def by_kind(self) -> tuple[tuple[str, tuple[str, ...]], ...]:
|
|
74
|
+
"""Each name by the kind of declaration it would collide with."""
|
|
75
|
+
return (('variable', (self.seg,)), ('constraint', (self.pick, self.link, self.below)))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#: What the binary says, per order — the description its legend entry carries.
|
|
79
|
+
_SEGMENTS = {
|
|
80
|
+
1: 'a binary per member, 1 where that member may be nonzero',
|
|
81
|
+
2: 'a binary per segment, 1 where the two members it spans may be nonzero',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def expand_sets(schema: Spec) -> Spec:
|
|
86
|
+
"""*schema* with every ``sos:`` block written out as binaries and the rows that link them.
|
|
87
|
+
|
|
88
|
+
The curves an expansion wrote out ride along, because a model whose
|
|
89
|
+
curves are already written out is the one this is usually asked of.
|
|
90
|
+
"""
|
|
91
|
+
raw = schema.model_dump()
|
|
92
|
+
for name in list(schema.sos):
|
|
93
|
+
emit(raw, name)
|
|
94
|
+
return Spec.model_validate(raw)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def emit(raw: dict[str, object], name: str) -> None:
|
|
98
|
+
"""Write what the set *name* states as declarations of *raw*, and drop the block.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
raw: A model as data, mid-expansion, declaring the set and the variable
|
|
102
|
+
it runs over.
|
|
103
|
+
name: Which set to lower.
|
|
104
|
+
"""
|
|
105
|
+
sets = section(raw, 'sos')
|
|
106
|
+
block = sets.pop(name)
|
|
107
|
+
assert isinstance(block, dict), 'a validated model carries each set as a mapping'
|
|
108
|
+
variable, over, order = block['variable'], block['along'], block['type']
|
|
109
|
+
member = section(raw, 'variables')[variable]
|
|
110
|
+
assert isinstance(member, dict), 'a validated model carries each variable as a mapping'
|
|
111
|
+
dims = list(member['dims'])
|
|
112
|
+
emitted = Emitted.of(name, order)
|
|
113
|
+
|
|
114
|
+
section(raw, 'variables')[emitted.seg] = {
|
|
115
|
+
'dims': dims,
|
|
116
|
+
**({'where': member['where']} if member.get('where') else {}),
|
|
117
|
+
'domain': 'binary',
|
|
118
|
+
'description': _SEGMENTS[order],
|
|
119
|
+
}
|
|
120
|
+
picked = emitted.seg if order == 1 else f'{emitted.seg} + shift({emitted.seg}, along={over}, offset=1, edge=0)'
|
|
121
|
+
constraints = section(raw, 'constraints')
|
|
122
|
+
constraints[emitted.pick] = {
|
|
123
|
+
'dims': [d for d in dims if d != over],
|
|
124
|
+
'expression': f'sum({emitted.seg}, over={over}) <= 1',
|
|
125
|
+
}
|
|
126
|
+
below, above = _coefficients(member)
|
|
127
|
+
constraints[emitted.link] = {'dims': dims, 'expression': f'{variable} <= {_scaled(above, picked)}'}
|
|
128
|
+
if below != 0.0:
|
|
129
|
+
constraints[emitted.below] = {'dims': dims, 'expression': f'{variable} >= {_scaled(below, picked)}'}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _scaled(factor: float | str, picked: str) -> str:
|
|
133
|
+
"""The binaries a member is admitted by, times *factor*.
|
|
134
|
+
|
|
135
|
+
A coefficient of 1 is left out. It is the common one — a weight and a binary
|
|
136
|
+
are both bounded by 1 — and ``x <= 1 * (seg)`` is a factor every reader of
|
|
137
|
+
the row has to work out means nothing.
|
|
138
|
+
"""
|
|
139
|
+
return f'({picked})' if factor == 1.0 else f'{factor} * ({picked})'
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _coefficients(member: dict[str, object]) -> tuple[float | str, float | str]:
|
|
143
|
+
"""The two coefficients as an expression writes them, read off the member."""
|
|
144
|
+
declared = member.get('bounds')
|
|
145
|
+
assert declared is None or isinstance(declared, dict), 'a validated model carries a bounds block as a mapping'
|
|
146
|
+
lower, upper = (declared.get('lower'), declared.get('upper')) if declared else (None, None)
|
|
147
|
+
assert isinstance(lower, float | str | None) and isinstance(upper, float | str | None), (
|
|
148
|
+
'a bound is a number, the name of a parameter, or open'
|
|
149
|
+
)
|
|
150
|
+
below, above = coefficients(str(member.get('domain', 'continuous')), lower, upper)
|
|
151
|
+
assert below is not None and above is not None, 'a set with a side left open is refused at load'
|
|
152
|
+
return below, above
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def section(raw: dict[str, object], name: str) -> dict[str, object]:
|
|
156
|
+
"""The *name* section of the raw model, created empty where the file declares none."""
|
|
157
|
+
section = raw.setdefault(name, {})
|
|
158
|
+
assert isinstance(section, dict), f'{name}: is a mapping in a validated model'
|
|
159
|
+
return section
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: mathspec Contributors
|
|
3
|
+
SPDX-License-Identifier: MIT
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# `typesetting/` — the model, printed
|
|
7
|
+
|
|
8
|
+
This package is a consumer of the program. It builds no model and attaches no
|
|
9
|
+
data. It walks a program, the one a loaded `Spec` holds or one handed to it,
|
|
10
|
+
and prints it.
|
|
11
|
+
|
|
12
|
+
| Module | Role |
|
|
13
|
+
| ------------- | ------------------------------------------------------------------------------------------------------ |
|
|
14
|
+
| `__init__.py` | `typeset` / `to_latex` / `to_markdown` / `to_typst`, `typeset_declaration`, and the `FORMATS` registry |
|
|
15
|
+
| `walk.py` | the program's trees to `Line`s. Every decision about the **math**, written once |
|
|
16
|
+
| `legend.py` | the notes under the equations, read off what the program uses |
|
|
17
|
+
| `format.py` | the boundary: what a format must spell, and the operator vocabulary |
|
|
18
|
+
| `symbols.py` | which symbol a name gets, and the `SymbolTable` sidecar that overrides it |
|
|
19
|
+
| `latex.py` | amsmath, the format that lands in a journal |
|
|
20
|
+
| `typst.py` | Typst, the format that compiles without a toolchain |
|
|
21
|
+
| `markdown.py` | GitHub-flavoured Markdown: LaTeX math, with a Markdown document layer |
|
|
22
|
+
|
|
23
|
+
## Why the walk and the formats are separate files
|
|
24
|
+
|
|
25
|
+
`walk.py` makes the decisions about the math. It decides where a bracket changes
|
|
26
|
+
the reading, which dimension a reduction binds, that a mask belongs on the ∀
|
|
27
|
+
rather than in the equation, and that a translation shows at the leaf it
|
|
28
|
+
re-indexes.
|
|
29
|
+
|
|
30
|
+
A `Format` makes a much smaller decision: that a sum is written `\sum_{…}` or
|
|
31
|
+
`sum_(…)`.
|
|
32
|
+
|
|
33
|
+
Those are different questions, and they live in different files. With one
|
|
34
|
+
module, the second format becomes a _copy of the walk_. Two copies of a walk are
|
|
35
|
+
two walks that can disagree about what the model says. That matters more here
|
|
36
|
+
than it looks, because a typeset model is what a reader checks the math against.
|
|
37
|
+
|
|
38
|
+
Two rules keep the split honest:
|
|
39
|
+
|
|
40
|
+
- **A walk emits bare math.** No `$`, and no environment. The format wraps the
|
|
41
|
+
math with `math()` when it embeds it in prose, so the walk never knows which
|
|
42
|
+
mode it is in.
|
|
43
|
+
- **A format spells; it never decides.** No method in `format.py` takes a
|
|
44
|
+
syntax-tree node or a schema. If a format had to look at the model to answer a
|
|
45
|
+
question, that question belongs in the walk.
|
|
46
|
+
|
|
47
|
+
## Notation
|
|
48
|
+
|
|
49
|
+
Symbols are derived by default, so a model prints with no setup at all.
|
|
50
|
+
|
|
51
|
+
A `SymbolTable` overrides the derived symbols. `symbols=` takes the table in
|
|
52
|
+
whichever form the caller already has. The `--symbols` flag on the command line
|
|
53
|
+
is the path case:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
mathspec.to_latex('dispatch.yaml', symbols='dispatch.symbols.yaml') # a path
|
|
57
|
+
mathspec.to_latex('dispatch.yaml', symbols={'notation': 'latex', 'names': {'load': r'\ell'}}) # a dict
|
|
58
|
+
mathspec.to_latex('dispatch.yaml', symbols=mathspec.SymbolTable.load(table)) # the object
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The dict has the same sections as the file, which are `notation`, `dimensions`
|
|
62
|
+
and `names`. It is what the YAML parses to, and not a flat `{name: symbol}` map.
|
|
63
|
+
|
|
64
|
+
Whichever form you use, the table is checked against the model. A key that names
|
|
65
|
+
nothing is an error, and the message gives the near miss. Without that check, a
|
|
66
|
+
silent typo would be a symbol that never applies, and a reader who never finds
|
|
67
|
+
out.
|
|
68
|
+
|
|
69
|
+
Every value is a spelling, and it is printed exactly as written. Nothing parses
|
|
70
|
+
or translates it. `notation` says which language the table is written in, and a
|
|
71
|
+
format that reads the other language refuses the table. Everything past that
|
|
72
|
+
comparison is the caller's business.
|
|
73
|
+
|
|
74
|
+
The table carries **notation only**. What a declaration _is_, which is the prose
|
|
75
|
+
in the right-hand column of the legend, comes from the model's own
|
|
76
|
+
`description:`, read straight off the block. That is the model talking about
|
|
77
|
+
itself, rather than a reader choosing symbols.
|
|
78
|
+
|
|
79
|
+
A description travels with the file, survives a rename, and needs no sidecar.
|
|
80
|
+
The price is that it has to be plain prose, because every format sets the same
|
|
81
|
+
words.
|
|
82
|
+
|
|
83
|
+
## Adding a format
|
|
84
|
+
|
|
85
|
+
1. Add a module here with a class that satisfies `Format`. That means atoms,
|
|
86
|
+
structure, document, and a spelling for every name in `OPERATOR_NAMES`.
|
|
87
|
+
2. Add a row to `FORMATS` in `__init__.py`. The command-line verb comes from the
|
|
88
|
+
key.
|
|
89
|
+
3. Change nothing in `walk.py`. If you find that you need to, then either the
|
|
90
|
+
walk is making a syntax decision that it should not make, or `format.py` is
|
|
91
|
+
missing a method. Fix that, rather than adding a special case.
|
|
92
|
+
|
|
93
|
+
`markdown.py` is the cheap case. Markdown has no math of its own, so it
|
|
94
|
+
subclasses `LatexFormat` and overrides only the document layer.
|
|
95
|
+
|
|
96
|
+
`tests/typesetting/test_typeset.py` runs the shared expectations against
|
|
97
|
+
**every** entry in `FORMATS`, so a new format inherits the whole suite. Two of
|
|
98
|
+
those expectations are the point: every operator name is spelled, and no format
|
|
99
|
+
leaks another format's syntax.
|
|
100
|
+
|
|
101
|
+
## What CI checks, and what it cannot
|
|
102
|
+
|
|
103
|
+
LaTeX and Typst are **compiled** in CI, not just matched as strings. LaTeX needs
|
|
104
|
+
a two-package apt install. Typst is a pip wheel, so the suite compiles it
|
|
105
|
+
in-process.
|
|
106
|
+
|
|
107
|
+
The structural checks run as well. Those check brace balance, environment
|
|
108
|
+
nesting, and `\left` and `\right` pairing, which are the things a _generator_
|
|
109
|
+
gets wrong. But a structural check is not a compile. A malformed `\mathcal`
|
|
110
|
+
passes every one of them.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Typeset a validated model — a *reading* of the math.
|
|
6
|
+
|
|
7
|
+
Symbols are **derived** by default, aiming at unambiguous rather than
|
|
8
|
+
beautiful, so it prints with no setup; a
|
|
9
|
+
:class:`~mathspec.typesetting.symbols.SymbolTable` (``--symbols``) makes it
|
|
10
|
+
conventional. It does not line-break: a wide equation runs off the page.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
import mathspec
|
|
15
|
+
|
|
16
|
+
print(mathspec.to_latex('model.yaml'))
|
|
17
|
+
print(mathspec.to_typst('model.yaml', standalone=True))
|
|
18
|
+
print(mathspec.to_markdown('model.yaml')) # renders as-is on GitHub
|
|
19
|
+
print(mathspec.to_latex('model.yaml', symbols='model.symbols.yaml'))
|
|
20
|
+
|
|
21
|
+
or from a shell::
|
|
22
|
+
|
|
23
|
+
python -m mathspec latex model.yaml --symbols model.symbols.yaml --standalone -o model.tex
|
|
24
|
+
python -m mathspec typst model.yaml --standalone -o model.typ
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import TYPE_CHECKING, Literal, TypedDict, Unpack
|
|
30
|
+
|
|
31
|
+
from mathspec.program import Program
|
|
32
|
+
from mathspec.typesetting.latex import LatexFormat
|
|
33
|
+
from mathspec.typesetting.legend import Legend, notice
|
|
34
|
+
from mathspec.typesetting.markdown import MarkdownFormat
|
|
35
|
+
from mathspec.typesetting.symbols import SymbolTable, symbols_for
|
|
36
|
+
from mathspec.typesetting.typst import TypstFormat
|
|
37
|
+
from mathspec.typesetting.walk import Walk
|
|
38
|
+
from mathspec.validation import to_spec
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import Mapping
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from mathspec.model import Spec
|
|
45
|
+
from mathspec.typesetting.format import Format
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
'FORMATS',
|
|
49
|
+
'FormatName',
|
|
50
|
+
'SymbolTable',
|
|
51
|
+
'to_latex',
|
|
52
|
+
'to_markdown',
|
|
53
|
+
'to_typst',
|
|
54
|
+
'typeset',
|
|
55
|
+
'typeset_declaration',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
#: A format by the name the CLI takes — what every renderer here is asked for.
|
|
59
|
+
FormatName = Literal['latex', 'markdown', 'typst']
|
|
60
|
+
|
|
61
|
+
#: Every format, by name. Adding one is a module plus a row.
|
|
62
|
+
FORMATS: dict[FormatName, Format] = {
|
|
63
|
+
'latex': LatexFormat(),
|
|
64
|
+
'markdown': MarkdownFormat(),
|
|
65
|
+
'typst': TypstFormat(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _Options(TypedDict, total=False):
|
|
70
|
+
"""The keyword arguments :func:`typeset` takes, which the three per-format doors forward whole."""
|
|
71
|
+
|
|
72
|
+
symbols: str | Path | Mapping[str, object] | SymbolTable | None
|
|
73
|
+
standalone: bool
|
|
74
|
+
legend: bool
|
|
75
|
+
numbered: bool
|
|
76
|
+
inline_expressions: bool
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _walk(
|
|
80
|
+
model: str | Path | Mapping[str, object] | Spec | Program,
|
|
81
|
+
fmt: FormatName,
|
|
82
|
+
symbols: str | Path | Mapping[str, object] | SymbolTable | None,
|
|
83
|
+
*,
|
|
84
|
+
inline_expressions: bool,
|
|
85
|
+
) -> Walk:
|
|
86
|
+
"""The loaded, symbol-resolved walk every renderer builds from model, format and table."""
|
|
87
|
+
if fmt not in FORMATS:
|
|
88
|
+
msg = f"'{fmt}' is not a format this package prints. Formats: {', '.join(FORMATS)}."
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
program = model if isinstance(model, Program) else to_spec(model).program
|
|
91
|
+
format_ = FORMATS[fmt]
|
|
92
|
+
if symbols is None:
|
|
93
|
+
symbols = SymbolTable(format_.notation)
|
|
94
|
+
table = symbols if isinstance(symbols, SymbolTable) else SymbolTable.load(symbols)
|
|
95
|
+
return Walk(
|
|
96
|
+
program,
|
|
97
|
+
symbols_for(program, format_, table.checked_against(program)),
|
|
98
|
+
format_,
|
|
99
|
+
inline_expressions=inline_expressions,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def typeset(
|
|
104
|
+
model: str | Path | Mapping[str, object] | Spec | Program,
|
|
105
|
+
fmt: FormatName,
|
|
106
|
+
*,
|
|
107
|
+
symbols: str | Path | Mapping[str, object] | SymbolTable | None = None,
|
|
108
|
+
standalone: bool = False,
|
|
109
|
+
legend: bool = True,
|
|
110
|
+
numbered: bool = True,
|
|
111
|
+
inline_expressions: bool = False,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Render *model*'s math in *fmt*.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
model: Anything :func:`mathspec.to_spec` accepts, or a
|
|
117
|
+
:class:`~mathspec.program.Program`. A ``Spec`` or a ``Program``
|
|
118
|
+
is rendered as it stands, so printing one model in several formats
|
|
119
|
+
reads and checks the file once rather than once per format, and a
|
|
120
|
+
curve prints as the curve it states. Pass ``spec.expand()`` for the rows a solver holds
|
|
121
|
+
instead.
|
|
122
|
+
fmt: What spells the math — a key of :data:`FORMATS`.
|
|
123
|
+
symbols: How names print, as a :class:`SymbolTable`, a path or a
|
|
124
|
+
mapping. Names it does not carry are derived, and it must be
|
|
125
|
+
written in *fmt*'s notation.
|
|
126
|
+
standalone: Emit a compilable document rather than a fragment.
|
|
127
|
+
legend: Prepend the sets/parameters/variables table. The model's own
|
|
128
|
+
``description:`` opens the document either way — it is what the
|
|
129
|
+
file says it is, not a symbol table.
|
|
130
|
+
numbered: Number the equations.
|
|
131
|
+
inline_expressions: Substitute each plain named expression into the equations that
|
|
132
|
+
use it, rather than printing its symbol there and its definition
|
|
133
|
+
once. A cased expression is a definition either way: its block is
|
|
134
|
+
taller than the line it would sit in.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The rendered text.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: *fmt* names no format.
|
|
141
|
+
LanguageError: A model that does not compile; it does not print.
|
|
142
|
+
SchemaError: A symbol table entry naming nothing in the model, or a
|
|
143
|
+
table written in a notation *fmt* does not read.
|
|
144
|
+
"""
|
|
145
|
+
walk = _walk(model, fmt, symbols, inline_expressions=inline_expressions)
|
|
146
|
+
program, format_ = walk.program, walk.format
|
|
147
|
+
|
|
148
|
+
rendered = [
|
|
149
|
+
format_.section(title, format_.equations(lines, numbered=numbered))
|
|
150
|
+
for title, lines in walk.equations()
|
|
151
|
+
if lines
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
blocks = [format_.note(format_.escape(program.description))] if program.description else []
|
|
155
|
+
if legend:
|
|
156
|
+
explained, noticed = Legend(program, walk.symbols, format_), notice(program)
|
|
157
|
+
blocks += [
|
|
158
|
+
format_.section(title, format_.glossary(entries))
|
|
159
|
+
for title, entries in explained.glossaries(noticed, walk.defined())
|
|
160
|
+
]
|
|
161
|
+
blocks += [format_.note(text) for text in explained.convention_notes()]
|
|
162
|
+
blocks += [format_.note(text) for text in explained.translation_notes(noticed)]
|
|
163
|
+
blocks += [format_.note(text) for text in explained.position_notes(noticed)]
|
|
164
|
+
return format_.document([*blocks, *rendered], standalone=standalone)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def typeset_declaration(
|
|
168
|
+
model: str | Path | Mapping[str, object] | Spec | Program,
|
|
169
|
+
name: str,
|
|
170
|
+
fmt: FormatName,
|
|
171
|
+
*,
|
|
172
|
+
symbols: str | Path | Mapping[str, object] | SymbolTable | None = None,
|
|
173
|
+
inline_expressions: bool = True,
|
|
174
|
+
) -> str:
|
|
175
|
+
"""Render one declaration as the bare line the document prints for it.
|
|
176
|
+
|
|
177
|
+
The line the whole-model render prints for it — a named expression's
|
|
178
|
+
definition, a constraint, an assumption, a ``piecewise:`` curve, or a
|
|
179
|
+
variable's domain, quantifier included —
|
|
180
|
+
with no document, label, equation number or math delimiters around it, for
|
|
181
|
+
a math context the caller lays out: a docstring, a table cell. A line on
|
|
182
|
+
its own has no Definitions section beside it, so the plain named
|
|
183
|
+
expressions it uses are substituted unless *inline_expressions* says otherwise; a cased
|
|
184
|
+
one prints by symbol, and a second call with its name prints its block.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
model: Anything :func:`mathspec.to_spec` accepts, or a :class:`~mathspec.program.Program`.
|
|
188
|
+
name: A named expression, constraint, assumption, ``piecewise:``
|
|
189
|
+
block or variable the model declares.
|
|
190
|
+
fmt: What spells the math — a key of :data:`FORMATS`.
|
|
191
|
+
symbols: How names print; see :func:`typeset`.
|
|
192
|
+
inline_expressions: Substitute the plain named expressions the line uses, so it
|
|
193
|
+
stands on its own; ``False`` prints their symbols, as the document
|
|
194
|
+
does. A plain expression asked for by name prints its definition
|
|
195
|
+
either way.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The line, math only.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: *fmt* names no format.
|
|
202
|
+
LanguageError: A model that does not compile; it does not print.
|
|
203
|
+
SchemaError: *name* is declared as none of the five, or as two — a
|
|
204
|
+
constraint may share a variable's name; or a symbol table entry
|
|
205
|
+
names nothing in the model.
|
|
206
|
+
"""
|
|
207
|
+
walk = _walk(model, fmt, symbols, inline_expressions=inline_expressions)
|
|
208
|
+
return walk.format.equation(walk.line(name))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def to_latex(model: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
|
|
212
|
+
"""Render *model* as LaTeX (amsmath ``align``). See :func:`typeset`."""
|
|
213
|
+
return typeset(model, 'latex', **options)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def to_typst(model: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
|
|
217
|
+
"""Render *model* as Typst. See :func:`typeset`."""
|
|
218
|
+
return typeset(model, 'typst', **options)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def to_markdown(model: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
|
|
222
|
+
"""Render *model* as GitHub-flavoured Markdown. See :func:`typeset`."""
|
|
223
|
+
return typeset(model, 'markdown', **options)
|