earthsci-astdiff 0.1.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.
- earthsci_astdiff-0.1.0/.gitignore +5 -0
- earthsci_astdiff-0.1.0/PKG-INFO +44 -0
- earthsci_astdiff-0.1.0/README.md +33 -0
- earthsci_astdiff-0.1.0/pyproject.toml +21 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/__init__.py +37 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/bands.py +465 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/clip.py +210 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/cse.py +121 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/emit.py +166 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/expr_helpers.py +197 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/factorization.py +103 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/inline.py +284 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/jacobian.py +83 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/numeric.py +177 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/scalar_rules.py +292 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/simplify_branches.py +73 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/simplify_port.py +92 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/structure.py +54 -0
- earthsci_astdiff-0.1.0/src/earthsci_astdiff/system.py +270 -0
- earthsci_astdiff-0.1.0/tests/conftest.py +36 -0
- earthsci_astdiff-0.1.0/tests/test_bands.py +65 -0
- earthsci_astdiff-0.1.0/tests/test_blocks.py +39 -0
- earthsci_astdiff-0.1.0/tests/test_contracting_observed.py +207 -0
- earthsci_astdiff-0.1.0/tests/test_faq_tag.py +64 -0
- earthsci_astdiff-0.1.0/tests/test_goldens.py +83 -0
- earthsci_astdiff-0.1.0/tests/test_observed.py +112 -0
- earthsci_astdiff-0.1.0/tests/test_prune.py +62 -0
- earthsci_astdiff-0.1.0/tests/test_scalar_rules.py +119 -0
- earthsci_astdiff-0.1.0/tests/test_values.py +50 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: earthsci-astdiff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Analytical sparse Jacobians for EarthSciAST (.esm) models by symbolic differentiation of the expression AST
|
|
5
|
+
License-Expression: AGPL-3.0
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: earthsci-ast
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest; extra == 'test'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# earthsci-astdiff (Python)
|
|
13
|
+
|
|
14
|
+
Python binding of [EarthSciASTDiff](../../README.md): analytical sparse
|
|
15
|
+
Jacobians for `.esm` models, by rule-based differentiation of the expression
|
|
16
|
+
AST. Implements the derivative semantics of
|
|
17
|
+
[`esm-jacobian-spec.md`](../../esm-jacobian-spec.md) (§3 per-operator rules,
|
|
18
|
+
§4 band entries incl. `contracted` dimensions) against the
|
|
19
|
+
[`earthsci-ast`](../earthsci-ast-py) types, and is gated by the same
|
|
20
|
+
band-list goldens as the Julia reference (`tests/goldens/*.bands.json`,
|
|
21
|
+
byte-identical deterministic JSON).
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import earthsci_ast
|
|
25
|
+
from earthsci_astdiff import jacobian_bands, serialize_band, stable_json
|
|
26
|
+
|
|
27
|
+
file = earthsci_ast.load_path("model.esm")
|
|
28
|
+
entries = jacobian_bands(file, "MyModel") # ∂f/∂u as Band entries
|
|
29
|
+
entries_p = jacobian_bands(file, "MyModel", wrt="parameters")
|
|
30
|
+
text = stable_json([serialize_band(e) for e in entries])
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Scope: the band calculus (`jacobian_bands`), the §4 entry serialization
|
|
34
|
+
(`serialize_band` / `parse_band` / `stable_json`), the structural sparsity
|
|
35
|
+
pattern (`jacobian_pattern`, plain COO index lists — no scipy dependency),
|
|
36
|
+
and numeric assembly (`assemble_jacobian` → `{(row, col): value}` summed per
|
|
37
|
+
§4 additivity) — a correctness surface evaluated per cell through the
|
|
38
|
+
official numpy-interpreter runtime and gated against the Julia reference's
|
|
39
|
+
value goldens (`tests/goldens/*.jvals.json`, exact key set + 1e-10 relative).
|
|
40
|
+
The emission side is in as well: `jacobian_block` / `parse_jacobian_block`
|
|
41
|
+
with the §4.1 template CSE (`cse_templates` / `expand_templates`), the
|
|
42
|
+
structure classification, and the factorization plans (per-cell block LU,
|
|
43
|
+
Markowitz ordering + fill-in) — byte-identical to the reference's emitted
|
|
44
|
+
blocks (`tests/goldens/*.block.json`). Production-rate assembly follows.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# earthsci-astdiff (Python)
|
|
2
|
+
|
|
3
|
+
Python binding of [EarthSciASTDiff](../../README.md): analytical sparse
|
|
4
|
+
Jacobians for `.esm` models, by rule-based differentiation of the expression
|
|
5
|
+
AST. Implements the derivative semantics of
|
|
6
|
+
[`esm-jacobian-spec.md`](../../esm-jacobian-spec.md) (§3 per-operator rules,
|
|
7
|
+
§4 band entries incl. `contracted` dimensions) against the
|
|
8
|
+
[`earthsci-ast`](../earthsci-ast-py) types, and is gated by the same
|
|
9
|
+
band-list goldens as the Julia reference (`tests/goldens/*.bands.json`,
|
|
10
|
+
byte-identical deterministic JSON).
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import earthsci_ast
|
|
14
|
+
from earthsci_astdiff import jacobian_bands, serialize_band, stable_json
|
|
15
|
+
|
|
16
|
+
file = earthsci_ast.load_path("model.esm")
|
|
17
|
+
entries = jacobian_bands(file, "MyModel") # ∂f/∂u as Band entries
|
|
18
|
+
entries_p = jacobian_bands(file, "MyModel", wrt="parameters")
|
|
19
|
+
text = stable_json([serialize_band(e) for e in entries])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Scope: the band calculus (`jacobian_bands`), the §4 entry serialization
|
|
23
|
+
(`serialize_band` / `parse_band` / `stable_json`), the structural sparsity
|
|
24
|
+
pattern (`jacobian_pattern`, plain COO index lists — no scipy dependency),
|
|
25
|
+
and numeric assembly (`assemble_jacobian` → `{(row, col): value}` summed per
|
|
26
|
+
§4 additivity) — a correctness surface evaluated per cell through the
|
|
27
|
+
official numpy-interpreter runtime and gated against the Julia reference's
|
|
28
|
+
value goldens (`tests/goldens/*.jvals.json`, exact key set + 1e-10 relative).
|
|
29
|
+
The emission side is in as well: `jacobian_block` / `parse_jacobian_block`
|
|
30
|
+
with the §4.1 template CSE (`cse_templates` / `expand_templates`), the
|
|
31
|
+
structure classification, and the factorization plans (per-cell block LU,
|
|
32
|
+
Markowitz ordering + fill-in) — byte-identical to the reference's emitted
|
|
33
|
+
blocks (`tests/goldens/*.block.json`). Production-rate assembly follows.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "earthsci-astdiff"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Analytical sparse Jacobians for EarthSciAST (.esm) models by symbolic differentiation of the expression AST"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "AGPL-3.0"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = ["earthsci-ast"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
test = ["pytest"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/earthsci_astdiff"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Analytical sparse Jacobians for EarthSciAST (.esm) models.
|
|
2
|
+
|
|
3
|
+
Python binding of the EarthSciASTDiff band calculus — see
|
|
4
|
+
``esm-jacobian-spec.md`` at the repository root for the normative derivative
|
|
5
|
+
semantics (§3) and the band-entry format (§4). Gated by the same
|
|
6
|
+
deterministic-JSON goldens as the Julia reference.
|
|
7
|
+
"""
|
|
8
|
+
from .bands import Band, BandError, Ctx, bands, merge_bands, normalize_band
|
|
9
|
+
from .clip import clip_regions
|
|
10
|
+
from .cse import cse_templates, expand_templates
|
|
11
|
+
from .emit import (jacobian_block, jacobian_pattern, parse_band,
|
|
12
|
+
parse_jacobian_block, scatter_pairs, serialize_band)
|
|
13
|
+
from .factorization import (BlockDiagonalPlan, lu_fill,
|
|
14
|
+
markowitz_ordering, plan_factorization,
|
|
15
|
+
serialize_plan)
|
|
16
|
+
from .structure import detect_structure
|
|
17
|
+
from .expr_helpers import Site, ckey, skey, stable_json
|
|
18
|
+
from .inline import InlineError, inline_observed
|
|
19
|
+
from .jacobian import JacEntry, jacobian_bands
|
|
20
|
+
from .numeric import EvalError, assemble_jacobian
|
|
21
|
+
from .scalar_rules import DerivativeRuleError, dscalar
|
|
22
|
+
from .simplify_branches import canon_lits, simp, simplify_branches
|
|
23
|
+
from .system import SysView, ctx_of, sysview
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Band", "BandError", "Ctx", "bands", "merge_bands", "normalize_band",
|
|
27
|
+
"clip_regions", "jacobian_pattern", "parse_band", "scatter_pairs",
|
|
28
|
+
"jacobian_block", "parse_jacobian_block", "cse_templates",
|
|
29
|
+
"BlockDiagonalPlan", "plan_factorization", "serialize_plan",
|
|
30
|
+
"lu_fill", "markowitz_ordering",
|
|
31
|
+
"expand_templates", "detect_structure",
|
|
32
|
+
"serialize_band", "Site", "ckey", "skey", "stable_json",
|
|
33
|
+
"InlineError", "inline_observed", "JacEntry", "jacobian_bands",
|
|
34
|
+
"EvalError", "assemble_jacobian",
|
|
35
|
+
"DerivativeRuleError", "dscalar", "canon_lits", "simp",
|
|
36
|
+
"simplify_branches", "SysView", "ctx_of", "sysview",
|
|
37
|
+
]
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""The array-level band calculus (bands.jl): differentiate an array-valued
|
|
2
|
+
RHS w.r.t. one variable, producing structured bands."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from earthsci_ast.esm_types import Expr, ExprNode
|
|
8
|
+
from earthsci_ast.expr_walk import map_children
|
|
9
|
+
from earthsci_ast.expression import free_variables
|
|
10
|
+
from earthsci_ast.substitute import substitute
|
|
11
|
+
|
|
12
|
+
from .clip import clip_regions
|
|
13
|
+
from .expr_helpers import Site, add, ckey, is_zero, mul, neg, op, region_bound, skey
|
|
14
|
+
from .inline import all_names, binders
|
|
15
|
+
from .scalar_rules import dscalar, plain_sum, reduction
|
|
16
|
+
from .simplify_branches import canon_lits, simp
|
|
17
|
+
from .simplify_port import simplify_jl as simplify
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Band:
|
|
22
|
+
"""One structured block of ∂(rhs of the equation for u)/∂v — see the
|
|
23
|
+
Julia reference docstring and esm-jacobian-spec.md §4. ``contracted``
|
|
24
|
+
holds free contracted column dimensions ``(name, lo, hi)``."""
|
|
25
|
+
|
|
26
|
+
rows: list[tuple[int, int]]
|
|
27
|
+
ridx: list # str names or int singletons
|
|
28
|
+
cidx: list # column index expressions
|
|
29
|
+
coef: Expr
|
|
30
|
+
contracted: list[tuple[str, int, int]] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Ctx:
|
|
35
|
+
index_sets: dict[str, int]
|
|
36
|
+
shapes: dict[str, list[int]]
|
|
37
|
+
params: set[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BandError(Exception):
|
|
41
|
+
"""Raised when an expression has a form the band calculus cannot differentiate
|
|
42
|
+
(for example a filtered ``faq`` node or a non-``"+"`` semiring)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _set_size(name: str, ctx: Ctx) -> int:
|
|
46
|
+
if name not in ctx.index_sets:
|
|
47
|
+
raise BandError(f"range over undeclared index set `{name}`")
|
|
48
|
+
return ctx.index_sets[name]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _range_of(r, ctx: Ctx) -> tuple[int, int]:
|
|
52
|
+
if isinstance(r, dict):
|
|
53
|
+
return (1, _set_size(str(r["from"]), ctx))
|
|
54
|
+
if isinstance(r, (list, tuple)):
|
|
55
|
+
return (region_bound(r[0]), region_bound(r[-1]))
|
|
56
|
+
# typed IndexSetRef-like object with a `from`/`from_` attribute
|
|
57
|
+
frm = getattr(r, "from_", None) or getattr(r, "from_set", None)
|
|
58
|
+
if frm is not None:
|
|
59
|
+
return (1, _set_size(str(frm), ctx))
|
|
60
|
+
raise BandError(f"unsupported range spec {r!r}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_array(ctx: Ctx, name) -> bool:
|
|
64
|
+
return isinstance(name, str) and name in ctx.shapes
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sites_of(e: Expr, v: str, acc: dict[str, Site] | None = None) -> dict[str, Site]:
|
|
68
|
+
if acc is None:
|
|
69
|
+
acc = {}
|
|
70
|
+
if isinstance(e, str) and e == v:
|
|
71
|
+
s = Site.of(e)
|
|
72
|
+
acc[s.key] = s
|
|
73
|
+
elif isinstance(e, ExprNode) and e.op == "index" and e.args[0] == v:
|
|
74
|
+
s = Site.of(e)
|
|
75
|
+
acc[s.key] = s
|
|
76
|
+
elif isinstance(e, ExprNode):
|
|
77
|
+
map_children(e, lambda x: (sites_of(x, v, acc), x)[1])
|
|
78
|
+
return acc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def sorted_sites(e: Expr, v: str) -> list[Site]:
|
|
82
|
+
d = sites_of(e, v)
|
|
83
|
+
return [d[k] for k in sorted(d)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_AMBIGUOUS = object()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _nested_sums(e: Expr, v: str, outer) -> dict:
|
|
90
|
+
"""For each site of ``v`` under a nested scalar faq sum, the indices the
|
|
91
|
+
sums ENCLOSING it bind (bands.jl ``_nested_sums``): site key → {name →
|
|
92
|
+
range spec, or ``_AMBIGUOUS`` for a name also bound by ``outer``, bound
|
|
93
|
+
twice on the way down, or over different ranges at different
|
|
94
|
+
occurrences}. Position-aware: a sum elsewhere in the body does not
|
|
95
|
+
affect a site outside it."""
|
|
96
|
+
acc: dict = {}
|
|
97
|
+
_nested_walk(acc, e, v, set(outer), [])
|
|
98
|
+
return acc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _nested_walk(acc: dict, e: Expr, v: str, outer: set, stack: list) -> None:
|
|
102
|
+
if (isinstance(e, str) and e == v) or (
|
|
103
|
+
isinstance(e, ExprNode) and e.op == "index" and e.args[0] == v):
|
|
104
|
+
if not stack:
|
|
105
|
+
return
|
|
106
|
+
d = acc.setdefault(Site.of(e).key, {})
|
|
107
|
+
seen: set = set()
|
|
108
|
+
for n, r in stack:
|
|
109
|
+
amb = n in outer or n in seen or (n in d and d[n] != r)
|
|
110
|
+
d[n] = _AMBIGUOUS if amb else r
|
|
111
|
+
seen.add(n)
|
|
112
|
+
return
|
|
113
|
+
if not isinstance(e, ExprNode):
|
|
114
|
+
return
|
|
115
|
+
pushed = 0
|
|
116
|
+
if e.op == "faq" and not (e.output_idx or []) and e.ranges:
|
|
117
|
+
for n, r in e.ranges.items():
|
|
118
|
+
stack.append((str(n), r))
|
|
119
|
+
pushed += 1
|
|
120
|
+
map_children(e, lambda x: (_nested_walk(acc, x, v, outer, stack), x)[1])
|
|
121
|
+
del stack[len(stack) - pushed:]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _subst_free(e: Expr, m: dict) -> Expr:
|
|
125
|
+
"""Substitution that does not descend into a faq for the names it binds."""
|
|
126
|
+
if isinstance(e, str):
|
|
127
|
+
return m.get(e, e)
|
|
128
|
+
if not isinstance(e, ExprNode):
|
|
129
|
+
return e
|
|
130
|
+
if e.op == "faq":
|
|
131
|
+
bound = binders(e)
|
|
132
|
+
if any(n in m for n in bound):
|
|
133
|
+
m = {k: v for k, v in m.items() if k not in bound}
|
|
134
|
+
if not m:
|
|
135
|
+
return e
|
|
136
|
+
return map_children(e, lambda x: _subst_free(x, m))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _lift_nested(nested: dict, s: Site, body: Expr, cidx: list, coef: Expr,
|
|
140
|
+
ctx: Ctx, outer, taken: list):
|
|
141
|
+
"""A site under a nested sum whose column reads the sum's index: that
|
|
142
|
+
index becomes a CONTRACTED column dimension of the band, renamed apart in
|
|
143
|
+
``cidx`` and in the coefficient's free occurrences (bands.jl
|
|
144
|
+
``_lift_nested``). ``taken`` is a per-body lazy cache (``[None]``) of the
|
|
145
|
+
body's names."""
|
|
146
|
+
d = nested.get(s.key)
|
|
147
|
+
if not d:
|
|
148
|
+
return cidx, coef, []
|
|
149
|
+
read: set[str] = set()
|
|
150
|
+
for c in cidx:
|
|
151
|
+
read |= set(free_variables(c))
|
|
152
|
+
names = sorted(n for n in d if n in read)
|
|
153
|
+
if not names:
|
|
154
|
+
return cidx, coef, []
|
|
155
|
+
if taken[0] is None:
|
|
156
|
+
taken[0] = all_names(body, set(outer))
|
|
157
|
+
ren: dict = {}
|
|
158
|
+
extra = []
|
|
159
|
+
for n in names:
|
|
160
|
+
if d[n] is _AMBIGUOUS:
|
|
161
|
+
raise BandError(
|
|
162
|
+
f"a column reads `{n}`, which the sums around it bind "
|
|
163
|
+
"ambiguously (shadowing an enclosing index, or over two "
|
|
164
|
+
"different ranges)")
|
|
165
|
+
f = n + "_j"
|
|
166
|
+
while f in taken[0] or f in ren.values():
|
|
167
|
+
f += "_j"
|
|
168
|
+
ren[n] = f
|
|
169
|
+
extra.append((f, *_range_of(d[n], ctx)))
|
|
170
|
+
return ([substitute(c, ren) for c in cidx], _subst_free(coef, ren), extra)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def is_array_expr(e: Expr, ctx: Ctx) -> bool:
|
|
174
|
+
if isinstance(e, str):
|
|
175
|
+
return is_array(ctx, e)
|
|
176
|
+
if not isinstance(e, ExprNode):
|
|
177
|
+
return False
|
|
178
|
+
if e.op in ("faq", "makearray", "broadcast",
|
|
179
|
+
"reshape", "transpose", "concat"):
|
|
180
|
+
return True
|
|
181
|
+
if e.op == "index":
|
|
182
|
+
return False
|
|
183
|
+
return any(is_array_expr(x, ctx) for x in e.args)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def occurs_var(e: Expr, v: str) -> bool:
|
|
187
|
+
return v in free_variables(e)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def bands(rhs: Expr, v: str, ctx: Ctx, shape_u: list[int] | None = None) -> list[Band]:
|
|
191
|
+
"""Differentiate an equation RHS (already observed-inlined) w.r.t. ``v``."""
|
|
192
|
+
shape_u = shape_u or []
|
|
193
|
+
out: list[Band] = []
|
|
194
|
+
if not shape_u:
|
|
195
|
+
nested = _nested_sums(rhs, v, [])
|
|
196
|
+
taken: list = [None]
|
|
197
|
+
for s in sorted_sites(rhs, v):
|
|
198
|
+
cidx, d, extra = _lift_nested(nested, s, rhs, _site_cidx(s),
|
|
199
|
+
dscalar(rhs, s), ctx, [], taken)
|
|
200
|
+
coef = simp(d)
|
|
201
|
+
if is_zero(coef):
|
|
202
|
+
continue
|
|
203
|
+
out.append(Band([], [], cidx, coef, extra))
|
|
204
|
+
return out
|
|
205
|
+
_bands_array(out, rhs, v, ctx, shape_u, [1])
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _site_cidx(s: Site) -> list:
|
|
210
|
+
return [] if isinstance(s.expr, str) else list(s.expr.args[1:])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _scalar_bands(out, e: Expr, v: str, ctx: Ctx, rows, ridx, scale) -> None:
|
|
214
|
+
outer = [r for r in ridx if isinstance(r, str)]
|
|
215
|
+
for rows2, body in clip_regions(e, ridx, rows):
|
|
216
|
+
nested = _nested_sums(body, v, outer)
|
|
217
|
+
taken: list = [None]
|
|
218
|
+
for s in sorted_sites(body, v):
|
|
219
|
+
cidx, d, extra = _lift_nested(nested, s, body, _site_cidx(s),
|
|
220
|
+
dscalar(body, s), ctx, outer, taken)
|
|
221
|
+
coef = simp(mul(d, *scale))
|
|
222
|
+
if is_zero(coef):
|
|
223
|
+
continue
|
|
224
|
+
out.append(Band(rows2, list(ridx), cidx, coef, extra))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _bands_array(out, e: Expr, v: str, ctx: Ctx, shape_u, scale) -> None:
|
|
228
|
+
if isinstance(e, str):
|
|
229
|
+
if e != v:
|
|
230
|
+
return
|
|
231
|
+
names = [f"_r{k + 1}" for k in range(len(shape_u))]
|
|
232
|
+
out.append(Band([(1, n) for n in shape_u], list(names),
|
|
233
|
+
[n for n in names], mul(*scale)))
|
|
234
|
+
return
|
|
235
|
+
if not isinstance(e, ExprNode):
|
|
236
|
+
return
|
|
237
|
+
o = e.op
|
|
238
|
+
if o == "+":
|
|
239
|
+
for x in e.args:
|
|
240
|
+
_bands_array(out, x, v, ctx, shape_u, scale)
|
|
241
|
+
elif o == "-":
|
|
242
|
+
_bands_array(out, e.args[0], v, ctx, shape_u, scale)
|
|
243
|
+
if len(e.args) == 2:
|
|
244
|
+
_bands_array(out, e.args[1], v, ctx, shape_u, scale + [-1])
|
|
245
|
+
elif o == "neg":
|
|
246
|
+
_bands_array(out, e.args[0], v, ctx, shape_u, scale + [-1])
|
|
247
|
+
elif o == "*":
|
|
248
|
+
arr = [x for x in e.args if is_array_expr(x, ctx)]
|
|
249
|
+
sc = [x for x in e.args if not is_array_expr(x, ctx)]
|
|
250
|
+
if len(arr) != 1:
|
|
251
|
+
raise BandError(
|
|
252
|
+
f"elementwise product of {len(arr)} array-valued factors at "
|
|
253
|
+
"array level; wrap in a faq node")
|
|
254
|
+
if any(occurs_var(x, v) for x in sc):
|
|
255
|
+
raise BandError(
|
|
256
|
+
f"scalar factor depends on `{v}` in an array-level product")
|
|
257
|
+
_bands_array(out, arr[0], v, ctx, shape_u, scale + sc)
|
|
258
|
+
elif o == "/":
|
|
259
|
+
if is_array_expr(e.args[1], ctx):
|
|
260
|
+
raise BandError("array-valued divisor at array level")
|
|
261
|
+
_bands_array(out, e.args[0], v, ctx, shape_u,
|
|
262
|
+
scale + [op("/", 1, e.args[1])])
|
|
263
|
+
elif o == "makearray":
|
|
264
|
+
_bands_makearray(out, e, v, ctx, scale)
|
|
265
|
+
elif o == "faq":
|
|
266
|
+
_bands_faq(out, e, v, ctx, shape_u, scale)
|
|
267
|
+
elif o == "index":
|
|
268
|
+
names = [f"_r{k + 1}" for k in range(len(shape_u))]
|
|
269
|
+
_scalar_bands(out, e, v, ctx, [(1, n) for n in shape_u], names, scale)
|
|
270
|
+
else:
|
|
271
|
+
if is_array_expr(e, ctx):
|
|
272
|
+
names = [f"_r{k + 1}" for k in range(len(shape_u))]
|
|
273
|
+
idx = [n for n in names]
|
|
274
|
+
body = _push_index(e, idx, ctx)
|
|
275
|
+
agg = ExprNode(op="faq", args=[],
|
|
276
|
+
output_idx=list(names), expr=body,
|
|
277
|
+
ranges={n: [1, shape_u[k]]
|
|
278
|
+
for k, n in enumerate(names)})
|
|
279
|
+
_bands_array(out, agg, v, ctx, shape_u, scale)
|
|
280
|
+
else:
|
|
281
|
+
names = [f"_r{k + 1}" for k in range(len(shape_u))]
|
|
282
|
+
_scalar_bands(out, e, v, ctx, [(1, n) for n in shape_u], names, scale)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _bands_faq(out, e: ExprNode, v: str, ctx: Ctx, shape_u, scale) -> None:
|
|
286
|
+
oidx = [str(x) for x in e.output_idx if isinstance(x, str)]
|
|
287
|
+
if len(oidx) != len(e.output_idx):
|
|
288
|
+
raise BandError("singleton `1` output_idx entries not supported yet")
|
|
289
|
+
if e.filter is not None:
|
|
290
|
+
raise BandError("filtered faq nodes not supported (the filter gate "
|
|
291
|
+
"would be dropped from the derivative)")
|
|
292
|
+
ranges = e.ranges or {}
|
|
293
|
+
cnames = sorted(str(n) for n in ranges if str(n) not in oidx)
|
|
294
|
+
if not plain_sum(e):
|
|
295
|
+
raise BandError(f"{reduction(e)} (non-smooth semiring reductions "
|
|
296
|
+
"have no bands)")
|
|
297
|
+
crange = [(n, *_range_of(ranges[n], ctx)) for n in cnames]
|
|
298
|
+
rows = []
|
|
299
|
+
for k, n in enumerate(oidx):
|
|
300
|
+
rows.append(_range_of(ranges[n], ctx) if n in ranges
|
|
301
|
+
else (1, shape_u[k]))
|
|
302
|
+
body = e.expr
|
|
303
|
+
outer = oidx + cnames
|
|
304
|
+
for rows2, body2 in clip_regions(body, oidx, rows):
|
|
305
|
+
nested = _nested_sums(body2, v, outer)
|
|
306
|
+
taken: list = [None]
|
|
307
|
+
for s in sorted_sites(body2, v):
|
|
308
|
+
if isinstance(s.expr, str) and is_array(ctx, v):
|
|
309
|
+
raise BandError(
|
|
310
|
+
f"whole-array reference to `{v}` inside a faq body")
|
|
311
|
+
cidx, d, extra = _lift_nested(nested, s, body2, _site_cidx(s),
|
|
312
|
+
dscalar(body2, s), ctx, outer, taken)
|
|
313
|
+
if not crange:
|
|
314
|
+
coef = simp(mul(d, *scale))
|
|
315
|
+
if not is_zero(coef):
|
|
316
|
+
out.append(Band(rows2, list(oidx), cidx, coef, extra))
|
|
317
|
+
elif not any(any(n in free_variables(c) for c in cidx)
|
|
318
|
+
for n, _, _ in crange):
|
|
319
|
+
red = ExprNode(op="faq", args=[], output_idx=[],
|
|
320
|
+
reduce="+", expr=d,
|
|
321
|
+
ranges={n: [lo, hi] for n, lo, hi in crange})
|
|
322
|
+
coef = simp(mul(red, *scale))
|
|
323
|
+
if not is_zero(coef):
|
|
324
|
+
out.append(Band(rows2, list(oidx), cidx, coef, extra))
|
|
325
|
+
else:
|
|
326
|
+
coef = simp(mul(d, *scale))
|
|
327
|
+
if not is_zero(coef):
|
|
328
|
+
out.append(Band(rows2, list(oidx), cidx, coef,
|
|
329
|
+
list(crange) + extra))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _bands_makearray(out, e: ExprNode, v: str, ctx: Ctx, scale) -> None:
|
|
333
|
+
for region, val in zip(e.regions, e.values):
|
|
334
|
+
rows = [(region_bound(r[0]), region_bound(r[1])) for r in region]
|
|
335
|
+
if any(hi < lo for lo, hi in rows):
|
|
336
|
+
continue # folded-empty region
|
|
337
|
+
nonsing = [k for k, (lo, hi) in enumerate(rows) if hi > lo]
|
|
338
|
+
if is_array_expr(val, ctx):
|
|
339
|
+
sub: list[Band] = []
|
|
340
|
+
val_fullrank = (isinstance(val, ExprNode) and val.op == "faq"
|
|
341
|
+
and len(val.output_idx) == len(rows))
|
|
342
|
+
if val_fullrank:
|
|
343
|
+
sub_shape = [hi - lo + 1 for lo, hi in rows]
|
|
344
|
+
else:
|
|
345
|
+
sub_shape = [rows[k][1] - rows[k][0] + 1 for k in nonsing]
|
|
346
|
+
_bands_array(sub, val, v, ctx, sub_shape, scale)
|
|
347
|
+
if isinstance(val, ExprNode) and val.op == "faq":
|
|
348
|
+
vr = val.ranges or {}
|
|
349
|
+
origins = [_range_of(vr[str(n)], ctx)[0] if str(n) in vr else 1
|
|
350
|
+
for n in val.output_idx]
|
|
351
|
+
else:
|
|
352
|
+
origins = [1] * len(nonsing)
|
|
353
|
+
for b in sub:
|
|
354
|
+
ridx: list = []
|
|
355
|
+
subst: dict[str, Expr] = {}
|
|
356
|
+
rows2: list[tuple[int, int]] = []
|
|
357
|
+
fullrank = len(b.ridx) == len(rows)
|
|
358
|
+
kk = -1
|
|
359
|
+
for k, (lo, hi) in enumerate(rows):
|
|
360
|
+
if fullrank or hi > lo:
|
|
361
|
+
kk += 1
|
|
362
|
+
r = b.ridx[kk]
|
|
363
|
+
rlo, rhi = b.rows[kk]
|
|
364
|
+
shift = lo - origins[kk]
|
|
365
|
+
if isinstance(r, str):
|
|
366
|
+
ridx.append(r)
|
|
367
|
+
if shift != 0:
|
|
368
|
+
subst[r] = add(r, -shift)
|
|
369
|
+
rows2.append((rlo + shift, rhi + shift))
|
|
370
|
+
else:
|
|
371
|
+
ridx.append(r + shift)
|
|
372
|
+
rows2.append((r + shift, r + shift))
|
|
373
|
+
else:
|
|
374
|
+
ridx.append(lo)
|
|
375
|
+
rows2.append((lo, lo))
|
|
376
|
+
coef = b.coef if not subst else substitute(b.coef, subst)
|
|
377
|
+
cidx = b.cidx if not subst else [substitute(c, subst)
|
|
378
|
+
for c in b.cidx]
|
|
379
|
+
out.append(Band(rows2, ridx, cidx, coef, b.contracted))
|
|
380
|
+
else:
|
|
381
|
+
_scalar_bands(out, val, v, ctx, rows, [lo for lo, _ in rows], scale)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _push_index(e: Expr, idx: list, ctx: Ctx) -> Expr:
|
|
385
|
+
if isinstance(e, str):
|
|
386
|
+
return op("index", e, *idx) if is_array(ctx, e) else e
|
|
387
|
+
if isinstance(e, ExprNode):
|
|
388
|
+
if e.op in ("faq", "makearray", "index"):
|
|
389
|
+
raise BandError(f"cannot push an index into `{e.op}` nested "
|
|
390
|
+
"under an elementwise op")
|
|
391
|
+
return map_children(e, lambda x: _push_index(x, idx, ctx))
|
|
392
|
+
return e
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def normalize_band(b: Band) -> Band:
|
|
396
|
+
"""Pin singleton named row dims (and one-value contractions) to constants."""
|
|
397
|
+
subst: dict[str, Expr] = {}
|
|
398
|
+
ridx: list = []
|
|
399
|
+
for k, r in enumerate(b.ridx):
|
|
400
|
+
lo, hi = b.rows[k]
|
|
401
|
+
if isinstance(r, str) and lo == hi:
|
|
402
|
+
subst[r] = lo
|
|
403
|
+
ridx.append(lo)
|
|
404
|
+
else:
|
|
405
|
+
ridx.append(r)
|
|
406
|
+
contracted = []
|
|
407
|
+
for n, lo, hi in b.contracted:
|
|
408
|
+
if lo == hi:
|
|
409
|
+
subst[n] = lo
|
|
410
|
+
else:
|
|
411
|
+
contracted.append((n, lo, hi))
|
|
412
|
+
if not subst:
|
|
413
|
+
return b
|
|
414
|
+
return Band(b.rows, ridx,
|
|
415
|
+
[canon_lits(simplify(substitute(c, subst))) for c in b.cidx],
|
|
416
|
+
simp(substitute(b.coef, subst)), contracted)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def merge_bands(bs: list[Band]) -> list[Band]:
|
|
420
|
+
"""Coalesce bands identical in (ridx, cidx, coef, contracted) whose row
|
|
421
|
+
boxes are adjacent along exactly one dimension. Order-preserving."""
|
|
422
|
+
if len(bs) < 2:
|
|
423
|
+
return bs
|
|
424
|
+
|
|
425
|
+
def key(b: Band):
|
|
426
|
+
return (tuple(b.ridx), tuple(skey(c) for c in b.cidx), skey(b.coef),
|
|
427
|
+
tuple(b.contracted))
|
|
428
|
+
|
|
429
|
+
ks = [key(b) for b in bs]
|
|
430
|
+
out = list(bs)
|
|
431
|
+
alive = [True] * len(out)
|
|
432
|
+
changed = True
|
|
433
|
+
while changed:
|
|
434
|
+
changed = False
|
|
435
|
+
for a in range(len(out)):
|
|
436
|
+
if not alive[a]:
|
|
437
|
+
continue
|
|
438
|
+
for b in range(a + 1, len(out)):
|
|
439
|
+
if not alive[b] or ks[a] != ks[b]:
|
|
440
|
+
continue
|
|
441
|
+
ra, rb = out[a].rows, out[b].rows
|
|
442
|
+
if len(ra) != len(rb):
|
|
443
|
+
continue
|
|
444
|
+
d = -1
|
|
445
|
+
ok = True
|
|
446
|
+
for k in range(len(ra)):
|
|
447
|
+
if ra[k] == rb[k]:
|
|
448
|
+
continue
|
|
449
|
+
if d < 0 and (ra[k][1] + 1 == rb[k][0]
|
|
450
|
+
or rb[k][1] + 1 == ra[k][0]):
|
|
451
|
+
d = k
|
|
452
|
+
else:
|
|
453
|
+
ok = False
|
|
454
|
+
break
|
|
455
|
+
if not ok or d < 0:
|
|
456
|
+
continue
|
|
457
|
+
lo = min(ra[d][0], rb[d][0])
|
|
458
|
+
hi = max(ra[d][1], rb[d][1])
|
|
459
|
+
rows = list(ra)
|
|
460
|
+
rows[d] = (lo, hi)
|
|
461
|
+
out[a] = Band(rows, out[a].ridx, out[a].cidx, out[a].coef,
|
|
462
|
+
out[a].contracted)
|
|
463
|
+
alive[b] = False
|
|
464
|
+
changed = True
|
|
465
|
+
return [b for k, b in enumerate(out) if alive[k]]
|