ampzip 1.0.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.
Files changed (71) hide show
  1. ampzip/__init__.py +7 -0
  2. ampzip/__main__.py +7 -0
  3. ampzip/_version.py +26 -0
  4. ampzip/atoms.py +74 -0
  5. ampzip/search/__init__.py +5 -0
  6. ampzip/search/action_space.py +101 -0
  7. ampzip/search/actions.py +135 -0
  8. ampzip/search/finiteflow.py +367 -0
  9. ampzip/search/gbfs.py +707 -0
  10. ampzip/search/gbfs_checkpoint.py +260 -0
  11. ampzip/search/objective_atoms.py +111 -0
  12. ampzip/search/observation.py +64 -0
  13. ampzip/search/spurious.py +309 -0
  14. ampzip/search/value_den.py +96 -0
  15. ampzip/simplifier/__init__.py +41 -0
  16. ampzip/simplifier/__main__.py +5 -0
  17. ampzip/simplifier/_paths.py +97 -0
  18. ampzip/simplifier/_source.py +84 -0
  19. ampzip/simplifier/algorithms/__init__.py +5 -0
  20. ampzip/simplifier/algorithms/_post.py +353 -0
  21. ampzip/simplifier/algorithms/_post_worker.py +45 -0
  22. ampzip/simplifier/algorithms/base.py +27 -0
  23. ampzip/simplifier/algorithms/cds.py +463 -0
  24. ampzip/simplifier/algorithms/gbfs.py +302 -0
  25. ampzip/simplifier/algorithms/gbfs_c.py +366 -0
  26. ampzip/simplifier/algorithms/gbfs_d.py +33 -0
  27. ampzip/simplifier/algorithms/gbfs_e.py +356 -0
  28. ampzip/simplifier/algorithms/gbfs_f.py +92 -0
  29. ampzip/simplifier/algorithms/sft_beam.py +172 -0
  30. ampzip/simplifier/algorithms/sft_greedy.py +264 -0
  31. ampzip/simplifier/atomize.py +237 -0
  32. ampzip/simplifier/cli.py +452 -0
  33. ampzip/simplifier/fetch_backends.py +280 -0
  34. ampzip/simplifier/input_limits.py +293 -0
  35. ampzip/simplifier/limits.py +139 -0
  36. ampzip/simplifier/log.py +53 -0
  37. ampzip/simplifier/parse.py +246 -0
  38. ampzip/simplifier/patches/sft_eval_extensions.patch +126 -0
  39. ampzip/simplifier/reconstruct.py +426 -0
  40. ampzip/simplifier/result.py +74 -0
  41. ampzip/simplifier/runner.py +606 -0
  42. ampzip/simplifier/validate.py +358 -0
  43. ampzip/verify/__init__.py +18 -0
  44. ampzip/verify/field.py +72 -0
  45. ampzip/verify/kinematics.py +83 -0
  46. ampzip/verify/twistor.py +166 -0
  47. ampzip/verify/verifier.py +243 -0
  48. ampzip/webservice/README.md +95 -0
  49. ampzip/webservice/__init__.py +24 -0
  50. ampzip/webservice/__main__.py +30 -0
  51. ampzip/webservice/app.py +561 -0
  52. ampzip/webservice/audit.py +106 -0
  53. ampzip/webservice/auth.py +161 -0
  54. ampzip/webservice/config.py +221 -0
  55. ampzip/webservice/deploy/cloudflared-config.example.yml +17 -0
  56. ampzip/webservice/deploy/cloudflared.service +20 -0
  57. ampzip/webservice/deploy/webservice.env.example +65 -0
  58. ampzip/webservice/deploy/webservice.service +33 -0
  59. ampzip/webservice/hashpw.py +51 -0
  60. ampzip/webservice/jobs.py +281 -0
  61. ampzip/webservice/pwhash.py +34 -0
  62. ampzip/webservice/ratelimit.py +37 -0
  63. ampzip/webservice/safety.py +19 -0
  64. ampzip/webservice/static/index.html +1573 -0
  65. ampzip/webservice/static/mathjax/LICENSE +202 -0
  66. ampzip/webservice/static/mathjax/tex-svg.js +1 -0
  67. ampzip-1.0.0.dist-info/METADATA +171 -0
  68. ampzip-1.0.0.dist-info/RECORD +71 -0
  69. ampzip-1.0.0.dist-info/WHEEL +4 -0
  70. ampzip-1.0.0.dist-info/entry_points.txt +3 -0
  71. ampzip-1.0.0.dist-info/licenses/LICENSE +21 -0
ampzip/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """ampzip: training-free simplification of spinor-helicity amplitudes.
2
+
3
+ ``simplifier`` holds the CLI, the input check and the backends, ``search`` the best-first
4
+ search, ``verify`` the finite-field test and ``webservice`` the HTTP service. The moves are
5
+ executed by the extension module ``ampzip_engine`` built from ``ampzip/engine``.
6
+ """
7
+ from ._version import __version__ # noqa: F401
ampzip/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """`python -m ampzip` runs the simplifier CLI; `python -m ampzip.webservice` runs the web service."""
2
+ import sys
3
+
4
+ from .simplifier.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
ampzip/_version.py ADDED
@@ -0,0 +1,26 @@
1
+ """The package version, from one source.
2
+
3
+ An installed distribution reports the version in its metadata, taken from `pyproject.toml`
4
+ at build time. A source checkout run without installing reads the same field from the
5
+ `pyproject.toml` next to the package.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from importlib.metadata import PackageNotFoundError, version as _dist_version
10
+ from pathlib import Path
11
+
12
+
13
+ def _read() -> str:
14
+ try:
15
+ return _dist_version("ampzip")
16
+ except PackageNotFoundError:
17
+ pass
18
+ try:
19
+ import tomllib
20
+ data = tomllib.loads((Path(__file__).resolve().parents[1] / "pyproject.toml").read_text())
21
+ return str(data["project"]["version"]) + "+source"
22
+ except Exception: # noqa: BLE001 no pyproject next to the package
23
+ return "0+unknown"
24
+
25
+
26
+ __version__ = _read()
ampzip/atoms.py ADDED
@@ -0,0 +1,74 @@
1
+ """The bracket atoms and their canonical ordering.
2
+
3
+ `ab(i, j)` is the angle bracket ⟨i j⟩ and `sb(i, j)` the square bracket [i j], both as
4
+ uninterpreted sympy functions of two leg labels. Every expression the package handles is
5
+ built from these two atoms and `opaque_den`, the engine's marker for a polynomial
6
+ denominator factor, which is created where it is needed.
7
+
8
+ `canonical_order(expr)` rewrites every bracket to `i < j`, pushing the antisymmetry sign into
9
+ the coefficient: `ab(2, 1) -> -ab(1, 2)`. The engine, the atomizer and the display layer all
10
+ rely on this spelling, so a Mandelstam `s_ij = ⟨ij⟩[ji]` is stored as `-ab(i,j)*sb(i,j)`.
11
+
12
+ `ATOMS` is the registry of atom classes by name that the input check consults. A new atom
13
+ kind is added by registering it here together with its canonicalisation rule.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Dict, Type
18
+
19
+ import sympy as sp
20
+
21
+
22
+ def _leg_labels(printer, args):
23
+ """The leg labels for display. An integer prints as itself, and a symbol such as `p_{3}`
24
+ prints as its bare label."""
25
+ return [printer._print(a).replace("p_{", "").replace("}", "") for a in args]
26
+
27
+
28
+ class _Bracket(sp.Function):
29
+ """A two-leg bracket. Subclasses only choose the delimiters."""
30
+
31
+ _delims = ("", "")
32
+
33
+ # sympy would infer is_number from the integer leg labels and then evaluate bracket
34
+ # products numerically while printing, which takes seconds on a power of a sum.
35
+ is_number = False
36
+
37
+ def _latex(self, printer, exp=1):
38
+ i, j = _leg_labels(printer, self.args)
39
+ left, right = self._delims
40
+ body = f"{left}{i} {j}{right}"
41
+ return body if exp == 1 else f"{body}^{{{exp}}}"
42
+
43
+
44
+ class ab(_Bracket):
45
+ """Angle bracket ⟨i j⟩ of legs i and j."""
46
+
47
+ _delims = (r"\langle ", r" \rangle")
48
+
49
+
50
+ class sb(_Bracket):
51
+ """Square bracket [i j] of legs i and j."""
52
+
53
+ _delims = (r"\left[ ", r" \right]")
54
+
55
+
56
+ ATOMS: Dict[str, Type[sp.Function]] = {"ab": ab, "sb": sb}
57
+
58
+
59
+ def bracket_classes():
60
+ """The two bracket classes in the order every caller unpacks them, `ab, sb`."""
61
+ return ab, sb
62
+
63
+
64
+ def canonical_order(expr: sp.Expr) -> sp.Expr:
65
+ """Rewrite each two-leg bracket with its legs ascending, times -1 when they were swapped.
66
+
67
+ It applies to every two-argument function atom in `expr`, which is what the bracket
68
+ antisymmetry needs, and leaves one-argument atoms such as `opaque_den` alone.
69
+ """
70
+ replace = {}
71
+ for fun in expr.atoms(sp.Function):
72
+ if len(fun.args) == 2 and fun.args[0] > fun.args[1]:
73
+ replace[fun] = fun.func(fun.args[1], fun.args[0]) * (-1)
74
+ return expr.subs(replace)
@@ -0,0 +1,5 @@
1
+ """Best-first search over the engine `ampzip_engine`.
2
+
3
+ `actions` enumerates legal moves on an `Expr` handle and `gbfs` runs a best-first search
4
+ ranked by bracket count. The heuristic is `Expr.factored_bracket_count`.
5
+ """
@@ -0,0 +1,101 @@
1
+ """A fixed-size action encoding and legality mask for a learned policy.
2
+
3
+ It follows the SFT action spaces of `RL_amplitudes_claude/src/action_space_{4,5,6}pt.py`.
4
+
5
+ action = bracket_idx * (n_term_slots * identities_per_bracket)
6
+ + term_slot * identities_per_bracket
7
+ + identity_idx
8
+
9
+ The sizes n_brackets x n_term_slots x identities_per_bracket are 12x11x11 = 1452 at 4
10
+ points, 20x26x19 = 9880 at 5 and 30x31x32 = 29760 at 6. The per-n `max_terms` defaults
11
+ below set n_term_slots. `term_slot=0` is the whole expression and `term_slot=k>0` the
12
+ engine's k-1'th numerator term. The identity order is that of `actions.identity_menu`,
13
+ Schouten, then momentum, then momentum_sq.
14
+
15
+ The mask marks the legal action indices, so a policy can exclude illegal actions when
16
+ selecting from its logits. `decode_to_move` maps a selected index to an engine move for
17
+ `Expr.apply`. The sequential search enumerates moves through `actions.legal_moves` and the
18
+ parallel expansion through `Expr.legal_move_list`. Neither uses this fixed encoding.
19
+
20
+ The default term-slot limits are 10, 25 and 30 for 4, 5 and 6-point inputs. Single-term
21
+ actions exist only for the first `max_terms` numerator terms.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import numpy as np
26
+
27
+ from .actions import identity_menu, _term_brackets
28
+
29
+ # The per-n term-slot cap, with n_term_slots = 1 + max_terms, matching the action-space sizes
30
+ # above. It bounds how many per-term slots the fixed encoding can represent and is the
31
+ # search's own choice, like the term_slot ordering.
32
+ _DEFAULT_MAX_TERMS = {4: 10, 5: 25, 6: 30}
33
+
34
+
35
+ def default_max_terms(n: int) -> int:
36
+ return _DEFAULT_MAX_TERMS.get(n, 10)
37
+
38
+
39
+ class ActionSpace:
40
+ def __init__(self, n: int = 4, max_terms: int | None = None):
41
+ self.n = n
42
+ if max_terms is None:
43
+ max_terms = default_max_terms(n)
44
+ self.max_terms = max_terms
45
+ self.n_term_slots = 1 + max_terms
46
+ self.brackets = self._canonical_brackets(n) # [(square,i,j)], ab before sb
47
+ self._bidx = {b: i for i, b in enumerate(self.brackets)}
48
+ # The menu has the same size for every bracket.
49
+ self.identities_per_bracket = len(identity_menu(False, 1, 2, n))
50
+ self.n_actions = len(self.brackets) * self.n_term_slots * self.identities_per_bracket
51
+
52
+ @staticmethod
53
+ def _canonical_brackets(n: int):
54
+ out = []
55
+ for square in (False, True): # ab, then sb
56
+ for i in range(1, n + 1):
57
+ for j in range(i + 1, n + 1):
58
+ out.append((square, i, j))
59
+ return out
60
+
61
+ def encode(self, bracket_idx: int, term_slot: int, identity_idx: int) -> int:
62
+ return (bracket_idx * (self.n_term_slots * self.identities_per_bracket)
63
+ + term_slot * self.identities_per_bracket + identity_idx)
64
+
65
+ def decode(self, action: int):
66
+ idy = action % self.identities_per_bracket
67
+ rest = action // self.identities_per_bracket
68
+ term_slot = rest % self.n_term_slots
69
+ bracket_idx = rest // self.n_term_slots
70
+ return bracket_idx, term_slot, idy
71
+
72
+ def decode_to_move(self, action: int):
73
+ """The move (square,i,j,term_slot,identity,params) of an action index, for Expr.apply."""
74
+ bracket_idx, term_slot, idy = self.decode(action)
75
+ square, i, j = self.brackets[bracket_idx]
76
+ ident, params = identity_menu(square, i, j, self.n)[idy]
77
+ return (square, i, j, term_slot, ident, params)
78
+
79
+ def mask(self, handle) -> np.ndarray:
80
+ """The 0/1 legality vector of length n_actions for the current expression.
81
+
82
+ Every identity of a bracket is legal once the bracket is present, as in
83
+ `build_action_mask_v3`.
84
+ """
85
+ m = np.zeros(self.n_actions, dtype=np.float32)
86
+ n_id = self.identities_per_bracket
87
+ present = handle.distinct_brackets()
88
+ per_term = [_term_brackets(s) for s in handle.term_monos()]
89
+ for b in present:
90
+ b_idx = self._bidx.get(tuple(b))
91
+ if b_idx is None:
92
+ continue
93
+ base = self.encode(b_idx, 0, 0) # the whole-expression block
94
+ m[base:base + n_id] = 1.0
95
+ for k, bset in enumerate(per_term):
96
+ if k >= self.max_terms:
97
+ break
98
+ if tuple(b) in bset:
99
+ base = self.encode(b_idx, k + 1, 0)
100
+ m[base:base + n_id] = 1.0
101
+ return m
@@ -0,0 +1,135 @@
1
+ """Legal-move enumeration on an `ampzip_engine.Expr` handle.
2
+
3
+ A move is `(square, i, j, term_slot, identity, params)`. It applies the identity to the
4
+ bracket `bk(i,j)`, square False being ab and True sb, either to the whole expression with
5
+ `term_slot=0` or to one numerator term with `term_slot=k>0`, the engine's k-1'th term.
6
+
7
+ The per-bracket identity menu reproduces the SFT action spaces of
8
+ `RL_amplitudes_claude/src/action_space_{4,5,6}pt.py::get_identity_types_for_bracket`. Per
9
+ bracket there are C(n-2,2) Schouten, 2(n-1) momentum and 2^(n-2) momentum_sq identities,
10
+ 11 at 4 points, 19 at 5 and 32 at 6.
11
+
12
+ Legality follows `eval_sft_4pt.py::build_action_mask_v3`. A bracket move is legal at
13
+ `term_slot=0` when the bracket is in the numerator and at `term_slot=k>0` when the bracket
14
+ is in that term. There is no identity-specific masking. The full identity menu is offered
15
+ once the bracket is present, and degenerate identities are filtered when the engine applies
16
+ them.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ import re
22
+ from itertools import combinations
23
+
24
+ _BRACKET_RE = re.compile(r"\b(ab|sb)\((\d+),\s*(\d+)\)")
25
+
26
+
27
+ def identity_menu(square: bool, i: int, j: int, n: int):
28
+ """The (identity, params) menu for bracket `bk(i,j)`, 11, 19 or 32 entries at n = 4, 5 or 6.
29
+
30
+ The order matches the SFT action spaces, Schouten, then momentum, then momentum_sq. The
31
+ params encoding matches `ampzip_engine`, [pk, pl] for schouten, [pout1, pout2] for
32
+ momentum and the mom_add list for momentum_sq.
33
+ """
34
+ other = [m for m in range(1, n + 1) if m != i and m != j]
35
+ menu = []
36
+ # Schouten, every one of the C(n-2, 2) pairs of the other legs in combinations order.
37
+ for pk, pl in combinations(other, 2):
38
+ menu.append(("schouten", [pk, pl]))
39
+ # Momentum, pout1 among the bracket's own legs and pout2 among pout1 and the other legs, 2(n-1) in all.
40
+ for pout1 in (i, j):
41
+ for pout2 in [pout1] + other:
42
+ menu.append(("momentum", [pout1, pout2]))
43
+ # Momentum_sq, every subset of the other legs as mom_add, 2^(n-2) in all.
44
+ for size in range(len(other) + 1):
45
+ for mom_add in combinations(other, size):
46
+ menu.append(("momentum_sq", list(mom_add)))
47
+ return menu
48
+
49
+
50
+ def _term_brackets(mono_str: str):
51
+ """The set of (square, i, j) brackets present in a canonical monomial string."""
52
+ return {
53
+ (typ == "sb", int(a), int(b))
54
+ for typ, a, b in _BRACKET_RE.findall(mono_str)
55
+ }
56
+
57
+
58
+ def legal_moves(handle, n: int):
59
+ """All legal `(square, i, j, term_slot, identity, params)` moves for `handle`.
60
+
61
+ It yields `term_slot=0`, the whole expression, for every present numerator bracket, plus
62
+ `term_slot=k+1` for each numerator term that contains the bracket.
63
+
64
+ The engine also enumerates moves through `Expr.legal_moves`, but returning a
65
+ Python tuple, string and parameter list for every move can make it slower than
66
+ this function, which reuses each identity's `params` list across term slots.
67
+ The threaded search avoids these allocations by keeping the move list in Rust
68
+ with `Expr.legal_move_list` and applying slices through `Expr.apply_slice_coded`,
69
+ falling back to `Expr.apply_slice` on older engines.
70
+ """
71
+ present = handle.distinct_brackets() # the [(square, i, j)] of the numerator
72
+ if not present:
73
+ return []
74
+ per_term = [_term_brackets(m) for m in handle.term_monos()]
75
+ moves = []
76
+ for square, i, j in present:
77
+ menu = identity_menu(square, i, j, n)
78
+ # the whole expression
79
+ for ident, params in menu:
80
+ moves.append((square, i, j, 0, ident, params))
81
+ # a single term
82
+ for k, bset in enumerate(per_term):
83
+ if (square, i, j) in bset:
84
+ for ident, params in menu:
85
+ moves.append((square, i, j, k + 1, ident, params))
86
+ return moves
87
+
88
+
89
+ # The explicit name of the Python move enumerator, kept for compatibility and for comparison
90
+ # with the Rust implementation.
91
+ legal_moves_py = legal_moves
92
+
93
+
94
+ def move_rule(square, i, j, identity, params, n):
95
+ """The local rewrite a move performs on `bk(i,j)`, as `(lhs_str, rhs_str)` in ab/sb
96
+ notation. The right-hand side comes from applying the identity to the isolated bracket,
97
+ so it is the literal substitution the engine makes, signs and all, with no re-derivation
98
+ of the algebra here. Returns `(lhs, None)` when the engine cannot build it, as with
99
+ degenerate params.
100
+ """
101
+ import ampzip_engine as _E
102
+ lhs = f"{'sb' if square else 'ab'}({i},{j})"
103
+ try:
104
+ rhs = _E.Expr(lhs).apply(square, i, j, identity, list(params), n, term_slot=0).to_str()
105
+ except Exception:
106
+ rhs = None
107
+ return lhs, rhs
108
+
109
+
110
+ def annotate_trajectory_rules(traj, n):
111
+ """Add the ab/sb strings `rule_lhs` and `rule_rhs` to each trajectory step in place,
112
+ reconstructed from its `bracket`, `identity` and `params`. A step missing fields or whose
113
+ rule cannot be built is left as it is. It accepts the raw rollout trajectory, where
114
+ params is a list, and a serialised one, where params is a string.
115
+ """
116
+ if not traj:
117
+ return traj
118
+ for step in traj:
119
+ if not isinstance(step, dict) or step.get("rule_lhs"):
120
+ continue
121
+ m = _BRACKET_RE.search(str(step.get("bracket", "")))
122
+ ident, params = step.get("identity"), step.get("params")
123
+ if not m or ident is None or params is None:
124
+ continue
125
+ if not isinstance(params, (list, tuple)):
126
+ try:
127
+ params = ast.literal_eval(str(params))
128
+ except Exception:
129
+ continue
130
+ square, ii, jj = (m.group(1) == "sb"), int(m.group(2)), int(m.group(3))
131
+ lhs, rhs = move_rule(square, ii, jj, ident, list(params), n)
132
+ step["rule_lhs"] = lhs
133
+ if rhs is not None:
134
+ step["rule_rhs"] = rhs
135
+ return traj