process-geometry 0.0.3__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.
- aeg_shakespeare/__init__.py +52 -0
- aeg_shakespeare/_legacy_api.py +299 -0
- aeg_shakespeare/analysis/__init__.py +12 -0
- aeg_shakespeare/analysis/abelian.py +69 -0
- aeg_shakespeare/analysis/algebraic.py +11 -0
- aeg_shakespeare/analysis/am.py +19 -0
- aeg_shakespeare/analysis/connection.py +74 -0
- aeg_shakespeare/analysis/decomposition.py +37 -0
- aeg_shakespeare/analysis/module.py +5 -0
- aeg_shakespeare/central.py +20 -0
- aeg_shakespeare/constraints.py +121 -0
- aeg_shakespeare/construction.py +303 -0
- aeg_shakespeare/core.py +59 -0
- aeg_shakespeare/cost.py +51 -0
- aeg_shakespeare/discovery/__init__.py +53 -0
- aeg_shakespeare/discovery/coefficient_extension.py +75 -0
- aeg_shakespeare/discovery/polynomial.py +342 -0
- aeg_shakespeare/discovery/selection.py +146 -0
- aeg_shakespeare/discovery/structured.py +201 -0
- aeg_shakespeare/families.py +31 -0
- aeg_shakespeare/frame.py +5 -0
- aeg_shakespeare/function_theory/__init__.py +100 -0
- aeg_shakespeare/function_theory/abel_jacobi.py +246 -0
- aeg_shakespeare/function_theory/abelian.py +129 -0
- aeg_shakespeare/function_theory/algebraic.py +105 -0
- aeg_shakespeare/function_theory/am.py +266 -0
- aeg_shakespeare/function_theory/intersection.py +321 -0
- aeg_shakespeare/function_theory/module.py +118 -0
- aeg_shakespeare/function_theory/period_matrix.py +155 -0
- aeg_shakespeare/function_theory/periods.py +216 -0
- aeg_shakespeare/function_theory/real_branch_cycles.py +286 -0
- aeg_shakespeare/function_theory/weierstrass.py +136 -0
- aeg_shakespeare/grammar.py +225 -0
- aeg_shakespeare/history_geometry.py +276 -0
- aeg_shakespeare/linear.py +70 -0
- aeg_shakespeare/presentation/__init__.py +23 -0
- aeg_shakespeare/presentation/budget.py +27 -0
- aeg_shakespeare/presentation/canonicalization.py +143 -0
- aeg_shakespeare/presentation/constraints.py +5 -0
- aeg_shakespeare/presentation/construction.py +19 -0
- aeg_shakespeare/presentation/grammar.py +15 -0
- aeg_shakespeare/presentation/history.py +45 -0
- aeg_shakespeare/presentation/morphism.py +66 -0
- aeg_shakespeare/presentation/relations.py +31 -0
- aeg_shakespeare/presentation/search.py +31 -0
- aeg_shakespeare/process/__init__.py +16 -0
- aeg_shakespeare/process/finite/__init__.py +43 -0
- aeg_shakespeare/process/finite/cocycle.py +166 -0
- aeg_shakespeare/process/finite/families.py +318 -0
- aeg_shakespeare/process/history.py +53 -0
- aeg_shakespeare/process/local/__init__.py +7 -0
- aeg_shakespeare/process/local/direction.py +88 -0
- aeg_shakespeare/process/local/frame.py +73 -0
- aeg_shakespeare/process/local/system.py +43 -0
- aeg_shakespeare/relations.py +374 -0
- aeg_shakespeare/rewrite.py +157 -0
- aeg_shakespeare/search.py +286 -0
- aeg_shakespeare/signature.py +155 -0
- process_geometry-0.0.3.dist-info/METADATA +305 -0
- process_geometry-0.0.3.dist-info/RECORD +63 -0
- process_geometry-0.0.3.dist-info/WHEEL +5 -0
- process_geometry-0.0.3.dist-info/licenses/LICENSE +24 -0
- process_geometry-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Exact cubic algebraization data for a genus-one process quotient.
|
|
2
|
+
|
|
3
|
+
Mathematical lineage
|
|
4
|
+
--------------------
|
|
5
|
+
The Weierstrass model is one of the decisive bridges between elliptic function
|
|
6
|
+
theory and algebraic curves. A nonsingular cubic can be written in the short
|
|
7
|
+
form
|
|
8
|
+
|
|
9
|
+
W^2 = 4 X^3 - g2 X - g3,
|
|
10
|
+
|
|
11
|
+
and the same invariants ``g2`` and ``g3`` occur in the differential equation of
|
|
12
|
+
the Weierstrass ``wp`` function. The discriminant ``g2^3-27*g3^2`` detects
|
|
13
|
+
singularity, while the modular invariant records the complex-isomorphism class.
|
|
14
|
+
See NIST DLMF §§23.2, 23.3, and 23.19 and Silverman, *The Arithmetic of
|
|
15
|
+
Elliptic Curves*.
|
|
16
|
+
|
|
17
|
+
Shakespeare reconstruction
|
|
18
|
+
---------------------------
|
|
19
|
+
This module is downstream of process reduction. It does not decide that a
|
|
20
|
+
problem "should use Weierstrass functions". Instead, once a process has
|
|
21
|
+
already emitted a nonsingular cubic ``y^2=P_3(x)``, we expose the exact affine
|
|
22
|
+
change of variables that turns that algebraic quotient into short Weierstrass
|
|
23
|
+
form. The resulting invariants can then be compared with independently
|
|
24
|
+
constructed period data.
|
|
25
|
+
|
|
26
|
+
Boundary
|
|
27
|
+
--------
|
|
28
|
+
Equal ``j`` is a statement about complex elliptic-curve isomorphism after the
|
|
29
|
+
usual field hypotheses; it is not equality of process presentations, physical
|
|
30
|
+
systems, period bases, or tasks. This module does not construct ``wp`` or a
|
|
31
|
+
period lattice and does not choose an analytic branch of the Abelian integral.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
|
|
38
|
+
import sympy as sp
|
|
39
|
+
|
|
40
|
+
from .algebraic import HyperellipticProfile
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class WeierstrassCubicProfile:
|
|
45
|
+
"""Short-Weierstrass invariants and exact coordinate map for ``y^2=P_3(x)``."""
|
|
46
|
+
|
|
47
|
+
curve: HyperellipticProfile
|
|
48
|
+
X: sp.Symbol
|
|
49
|
+
W: sp.Symbol
|
|
50
|
+
x_image: sp.Expr
|
|
51
|
+
y_image: sp.Expr
|
|
52
|
+
g2: sp.Expr
|
|
53
|
+
g3: sp.Expr
|
|
54
|
+
discriminant: sp.Expr
|
|
55
|
+
klein_J: sp.Expr
|
|
56
|
+
j_invariant: sp.Expr
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def relation(self) -> sp.Expr:
|
|
60
|
+
"""Return ``W^2 - (4 X^3 - g2 X - g3)``."""
|
|
61
|
+
|
|
62
|
+
return sp.expand(self.W**2 - (4 * self.X**3 - self.g2 * self.X - self.g3))
|
|
63
|
+
|
|
64
|
+
def transformation_residual(self) -> sp.Expr:
|
|
65
|
+
"""Verify the affine map modulo the original cubic relation."""
|
|
66
|
+
|
|
67
|
+
pulled = sp.expand(
|
|
68
|
+
self.relation.subs({self.X: self.x_image, self.W: self.y_image})
|
|
69
|
+
)
|
|
70
|
+
reduced = sp.expand(pulled.subs(self.curve.y**2, self.curve.polynomial))
|
|
71
|
+
return sp.factor(reduced)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def weierstrass_cubic_profile(
|
|
75
|
+
curve: HyperellipticProfile,
|
|
76
|
+
X: sp.Symbol,
|
|
77
|
+
W: sp.Symbol,
|
|
78
|
+
) -> WeierstrassCubicProfile:
|
|
79
|
+
"""Convert a cubic ``y^2=P_3(x)`` to short Weierstrass form exactly.
|
|
80
|
+
|
|
81
|
+
Write
|
|
82
|
+
|
|
83
|
+
P_3(x) = a x^3 + b x^2 + c x + d.
|
|
84
|
+
|
|
85
|
+
The affine change
|
|
86
|
+
|
|
87
|
+
X = (3 a x + b) / 12,
|
|
88
|
+
W = a y / 4
|
|
89
|
+
|
|
90
|
+
gives
|
|
91
|
+
|
|
92
|
+
W^2 = 4 X^3 - g2 X - g3
|
|
93
|
+
|
|
94
|
+
with
|
|
95
|
+
|
|
96
|
+
g2 = (b^2 - 3 a c) / 12,
|
|
97
|
+
g3 = (-2 b^3 + 9 a b c - 27 a^2 d) / 432.
|
|
98
|
+
|
|
99
|
+
``Klein J`` follows the DLMF convention ``J=g2^3/Delta``; the algebraic
|
|
100
|
+
``j`` invariant is ``1728*J``.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
if curve.degree != 3:
|
|
104
|
+
raise ValueError("Weierstrass cubic profile requires degree exactly 3")
|
|
105
|
+
if curve.generic_genus != 1:
|
|
106
|
+
raise ValueError("Weierstrass cubic profile requires a generically smooth cubic")
|
|
107
|
+
if X == W:
|
|
108
|
+
raise ValueError("Weierstrass coordinates X and W must be distinct")
|
|
109
|
+
|
|
110
|
+
poly = sp.Poly(curve.polynomial, curve.x)
|
|
111
|
+
a, b, c, d = poly.all_coeffs()
|
|
112
|
+
x_image = sp.cancel((3 * a * curve.x + b) / 12)
|
|
113
|
+
y_image = sp.cancel(a * curve.y / 4)
|
|
114
|
+
g2 = sp.factor((b**2 - 3 * a * c) / 12)
|
|
115
|
+
g3 = sp.factor((-2 * b**3 + 9 * a * b * c - 27 * a**2 * d) / 432)
|
|
116
|
+
discriminant = sp.factor(g2**3 - 27 * g3**2)
|
|
117
|
+
if discriminant == 0:
|
|
118
|
+
raise ValueError("short Weierstrass discriminant vanishes identically")
|
|
119
|
+
klein_J = sp.cancel(g2**3 / discriminant)
|
|
120
|
+
j_invariant = sp.cancel(1728 * klein_J)
|
|
121
|
+
|
|
122
|
+
profile = WeierstrassCubicProfile(
|
|
123
|
+
curve=curve,
|
|
124
|
+
X=X,
|
|
125
|
+
W=W,
|
|
126
|
+
x_image=x_image,
|
|
127
|
+
y_image=y_image,
|
|
128
|
+
g2=g2,
|
|
129
|
+
g3=g3,
|
|
130
|
+
discriminant=discriminant,
|
|
131
|
+
klein_J=klein_J,
|
|
132
|
+
j_invariant=j_invariant,
|
|
133
|
+
)
|
|
134
|
+
if profile.transformation_residual() != 0:
|
|
135
|
+
raise AssertionError("internal Weierstrass transformation certificate failed")
|
|
136
|
+
return profile
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Bounded discovery of process-generated finite grammars.
|
|
2
|
+
|
|
3
|
+
This module moves Shakespeare one step earlier than relation decomposition. A
|
|
4
|
+
caller supplies seed expressions and a process action; the library grows the
|
|
5
|
+
smallest exact process span it can find within an explicit search budget. If the
|
|
6
|
+
span closes, the existing template-free relation machinery is applied to that
|
|
7
|
+
discovered grammar.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections import deque
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Sequence
|
|
15
|
+
|
|
16
|
+
import sympy as sp
|
|
17
|
+
|
|
18
|
+
from .presentation.budget import SearchBudget
|
|
19
|
+
from .process.local import ProcessSystem
|
|
20
|
+
from .relations import (
|
|
21
|
+
RelationDecomposition,
|
|
22
|
+
coefficient_vector,
|
|
23
|
+
decompose,
|
|
24
|
+
discover_relation_decomposition,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class GeneratedGrammar:
|
|
30
|
+
"""A finite grammar generated from seed expressions by process action.
|
|
31
|
+
|
|
32
|
+
``basis`` contains only expressions that added a new exact direction to the
|
|
33
|
+
current span. ``depths`` records the process depth at which each basis item
|
|
34
|
+
first appeared. ``residuals`` are process images that escaped the discovered
|
|
35
|
+
span because a search bound was reached.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
seeds: tuple[sp.Expr, ...]
|
|
39
|
+
basis: tuple[sp.Expr, ...]
|
|
40
|
+
depths: tuple[int, ...]
|
|
41
|
+
residuals: tuple[sp.Expr, ...]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def closed(self) -> bool:
|
|
45
|
+
return not self.residuals
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def dimension(self) -> int:
|
|
49
|
+
return len(self.basis)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def max_depth(self) -> int:
|
|
53
|
+
return max(self.depths, default=0)
|
|
54
|
+
|
|
55
|
+
def growth_profile(self) -> tuple[int, ...]:
|
|
56
|
+
"""Cumulative number of independent grammar directions by depth."""
|
|
57
|
+
if not self.depths:
|
|
58
|
+
return ()
|
|
59
|
+
return tuple(
|
|
60
|
+
sum(depth <= level for depth in self.depths)
|
|
61
|
+
for level in range(self.max_depth + 1)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class GeneratedPresentation:
|
|
67
|
+
"""A generated grammar plus its discovered relation presentation."""
|
|
68
|
+
|
|
69
|
+
grammar: GeneratedGrammar
|
|
70
|
+
relations: RelationDecomposition | None
|
|
71
|
+
seed_coordinates: tuple[tuple[sp.Expr, ...], ...] = ()
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def complete(self) -> bool:
|
|
75
|
+
return (
|
|
76
|
+
self.grammar.closed
|
|
77
|
+
and self.relations is not None
|
|
78
|
+
and self.relations.complete
|
|
79
|
+
and len(self.seed_coordinates) == len(self.grammar.seeds)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def primitives(self) -> tuple[sp.Expr, ...]:
|
|
84
|
+
if self.relations is None:
|
|
85
|
+
return ()
|
|
86
|
+
return self.relations.primitives
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _in_span(
|
|
90
|
+
expr: sp.Expr,
|
|
91
|
+
basis: Sequence[sp.Expr],
|
|
92
|
+
variables: Sequence[sp.Symbol],
|
|
93
|
+
) -> bool:
|
|
94
|
+
expr = sp.expand(sp.sympify(expr))
|
|
95
|
+
if expr == 0:
|
|
96
|
+
return True
|
|
97
|
+
if not basis:
|
|
98
|
+
return False
|
|
99
|
+
try:
|
|
100
|
+
coefficient_vector(expr, basis, variables)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _polynomial_degree(expr: sp.Expr, variables: Sequence[sp.Symbol]) -> int:
|
|
107
|
+
try:
|
|
108
|
+
degree = sp.Poly(sp.expand(expr), *variables).total_degree()
|
|
109
|
+
except sp.PolynomialError as exc:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"generated-grammar discovery currently requires polynomial expressions"
|
|
112
|
+
) from exc
|
|
113
|
+
if degree is sp.S.NegativeInfinity:
|
|
114
|
+
return 0
|
|
115
|
+
return int(degree)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _append_unique(residuals: list[sp.Expr], expr: sp.Expr) -> None:
|
|
119
|
+
expr = sp.expand(expr)
|
|
120
|
+
if all(sp.expand(expr - current) != 0 for current in residuals):
|
|
121
|
+
residuals.append(expr)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def discover_generated_grammar(
|
|
125
|
+
system: ProcessSystem,
|
|
126
|
+
seeds: Sequence[sp.Expr],
|
|
127
|
+
budget: SearchBudget | None = None,
|
|
128
|
+
) -> GeneratedGrammar:
|
|
129
|
+
"""Grow a finite process grammar from caller-supplied seed expressions.
|
|
130
|
+
|
|
131
|
+
The algorithm performs an exact, bounded closure search. It repeatedly
|
|
132
|
+
applies the represented process generator and adds an expression only when
|
|
133
|
+
it lies outside the current span. Search stops locally when history depth,
|
|
134
|
+
polynomial degree, or the primitive-addition budget is exceeded; escaping
|
|
135
|
+
expressions are returned as residual certificates rather than projected
|
|
136
|
+
away.
|
|
137
|
+
|
|
138
|
+
``max_new_primitives`` counts new directions added *after* the independent
|
|
139
|
+
seed directions.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
budget = budget or SearchBudget()
|
|
143
|
+
normalized_seeds = tuple(sp.expand(sp.sympify(seed)) for seed in seeds)
|
|
144
|
+
if not normalized_seeds:
|
|
145
|
+
raise ValueError("at least one seed expression is required")
|
|
146
|
+
|
|
147
|
+
basis: list[sp.Expr] = []
|
|
148
|
+
depths: list[int] = []
|
|
149
|
+
queue: deque[tuple[sp.Expr, int]] = deque()
|
|
150
|
+
|
|
151
|
+
for seed in normalized_seeds:
|
|
152
|
+
_polynomial_degree(seed, system.assignments)
|
|
153
|
+
if not _in_span(seed, basis, system.assignments):
|
|
154
|
+
basis.append(seed)
|
|
155
|
+
depths.append(0)
|
|
156
|
+
queue.append((seed, 0))
|
|
157
|
+
|
|
158
|
+
if not basis:
|
|
159
|
+
raise ValueError("at least one nonzero independent seed is required")
|
|
160
|
+
|
|
161
|
+
additions = 0
|
|
162
|
+
residuals: list[sp.Expr] = []
|
|
163
|
+
|
|
164
|
+
while queue:
|
|
165
|
+
expression, depth = queue.popleft()
|
|
166
|
+
derived = sp.expand(system.derive(expression))
|
|
167
|
+
if _in_span(derived, basis, system.assignments):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if depth >= budget.max_history_depth:
|
|
171
|
+
_append_unique(residuals, derived)
|
|
172
|
+
continue
|
|
173
|
+
if _polynomial_degree(derived, system.assignments) > budget.max_expression_degree:
|
|
174
|
+
_append_unique(residuals, derived)
|
|
175
|
+
continue
|
|
176
|
+
if additions >= budget.max_new_primitives:
|
|
177
|
+
_append_unique(residuals, derived)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
basis.append(derived)
|
|
181
|
+
depths.append(depth + 1)
|
|
182
|
+
queue.append((derived, depth + 1))
|
|
183
|
+
additions += 1
|
|
184
|
+
|
|
185
|
+
for expression in basis:
|
|
186
|
+
derived = sp.expand(system.derive(expression))
|
|
187
|
+
if not _in_span(derived, basis, system.assignments):
|
|
188
|
+
_append_unique(residuals, derived)
|
|
189
|
+
|
|
190
|
+
return GeneratedGrammar(
|
|
191
|
+
seeds=normalized_seeds,
|
|
192
|
+
basis=tuple(basis),
|
|
193
|
+
depths=tuple(depths),
|
|
194
|
+
residuals=tuple(residuals),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def discover_generated_presentation(
|
|
199
|
+
system: ProcessSystem,
|
|
200
|
+
seeds: Sequence[sp.Expr],
|
|
201
|
+
budget: SearchBudget | None = None,
|
|
202
|
+
) -> GeneratedPresentation:
|
|
203
|
+
"""Discover a finite grammar, its relation factors, primitives, and decoder."""
|
|
204
|
+
budget = budget or SearchBudget()
|
|
205
|
+
grammar = discover_generated_grammar(system, seeds, budget=budget)
|
|
206
|
+
if not grammar.closed:
|
|
207
|
+
return GeneratedPresentation(grammar=grammar, relations=None)
|
|
208
|
+
|
|
209
|
+
relations = discover_relation_decomposition(
|
|
210
|
+
system,
|
|
211
|
+
grammar.basis,
|
|
212
|
+
max_order=budget.max_relation_order,
|
|
213
|
+
)
|
|
214
|
+
if relations is None or not relations.complete:
|
|
215
|
+
return GeneratedPresentation(grammar=grammar, relations=relations)
|
|
216
|
+
|
|
217
|
+
seed_coordinates = tuple(
|
|
218
|
+
decompose(seed, relations.primitives, system.assignments)
|
|
219
|
+
for seed in grammar.seeds
|
|
220
|
+
)
|
|
221
|
+
return GeneratedPresentation(
|
|
222
|
+
grammar=grammar,
|
|
223
|
+
relations=relations,
|
|
224
|
+
seed_coordinates=seed_coordinates,
|
|
225
|
+
)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Finite history geometry and prefix-code representation strategies.
|
|
2
|
+
|
|
3
|
+
This module makes one representation scheme explicit: ordered process histories
|
|
4
|
+
form a prefix tree, process time can be measured by root-to-node depth, and the
|
|
5
|
+
number of distinguishable prefixes at each depth gives a finite boundary/frontier
|
|
6
|
+
profile. A Huffman code is provided as one optional way to redistribute depth
|
|
7
|
+
when a probability/usage measure on task-relevant outcomes is supplied.
|
|
8
|
+
|
|
9
|
+
The tree geometry is a representation object, not a claim that every notion of
|
|
10
|
+
computational time or space is identical to tree depth or boundary width.
|
|
11
|
+
Likewise Huffman coding is a strategy layered on top of history/task structure;
|
|
12
|
+
it is not part of Shakespeare's process ontology.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
import heapq
|
|
19
|
+
import itertools
|
|
20
|
+
import math
|
|
21
|
+
from typing import Callable, Generic, Hashable, Mapping, Sequence, TypeVar
|
|
22
|
+
|
|
23
|
+
from .process.history import ProcessWord
|
|
24
|
+
|
|
25
|
+
StepT = TypeVar("StepT")
|
|
26
|
+
SymbolT = TypeVar("SymbolT", bound=Hashable)
|
|
27
|
+
KeyT = TypeVar("KeyT", bound=Hashable)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class BoundaryProfile:
|
|
32
|
+
"""Finite prefix-frontier profile of a history family.
|
|
33
|
+
|
|
34
|
+
``widths[d]`` is the number of distinguishable prefixes at process depth
|
|
35
|
+
``d``. The root is depth zero. ``information_widths`` stores
|
|
36
|
+
``log2(width)`` and is therefore the number of bits needed merely to name a
|
|
37
|
+
frontier element under an ideal uniform code; it is not by itself a memory
|
|
38
|
+
bound for an arbitrary execution model.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
widths: tuple[int, ...]
|
|
42
|
+
information_widths: tuple[float, ...]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def max_depth(self) -> int:
|
|
46
|
+
return len(self.widths) - 1
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def peak_width(self) -> int:
|
|
50
|
+
return max(self.widths, default=0)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def peak_information_width(self) -> float:
|
|
54
|
+
return max(self.information_widths, default=0.0)
|
|
55
|
+
|
|
56
|
+
def exponential_growth_rates(self) -> tuple[float, ...]:
|
|
57
|
+
"""Return ``log(width[d]) / d`` for positive depths."""
|
|
58
|
+
rates: list[float] = []
|
|
59
|
+
for depth, width in enumerate(self.widths[1:], start=1):
|
|
60
|
+
rates.append(math.log(width) / depth if width > 0 else -math.inf)
|
|
61
|
+
return tuple(rates)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def history_depth(
|
|
65
|
+
history: ProcessWord[StepT],
|
|
66
|
+
step_cost: Mapping[StepT, float] | Callable[[StepT], float] | None = None,
|
|
67
|
+
) -> float:
|
|
68
|
+
"""Return unweighted or caller-weighted process depth of one history."""
|
|
69
|
+
if step_cost is None:
|
|
70
|
+
return float(history.depth)
|
|
71
|
+
|
|
72
|
+
if callable(step_cost):
|
|
73
|
+
cost_of = step_cost
|
|
74
|
+
else:
|
|
75
|
+
cost_of = step_cost.__getitem__
|
|
76
|
+
|
|
77
|
+
total = 0.0
|
|
78
|
+
for step in history:
|
|
79
|
+
value = float(cost_of(step))
|
|
80
|
+
if not math.isfinite(value) or value < 0:
|
|
81
|
+
raise ValueError("step costs must be finite and non-negative")
|
|
82
|
+
total += value
|
|
83
|
+
return total
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def boundary_profile(
|
|
87
|
+
histories: Sequence[ProcessWord[StepT]],
|
|
88
|
+
*,
|
|
89
|
+
max_depth: int | None = None,
|
|
90
|
+
quotient_key: Callable[[ProcessWord[StepT]], Hashable] | None = None,
|
|
91
|
+
) -> BoundaryProfile:
|
|
92
|
+
"""Compute finite frontier widths for a set of literal histories."""
|
|
93
|
+
histories = tuple(histories)
|
|
94
|
+
if max_depth is None:
|
|
95
|
+
bound = max((history.depth for history in histories), default=0)
|
|
96
|
+
else:
|
|
97
|
+
if max_depth < 0:
|
|
98
|
+
raise ValueError("max_depth must be non-negative")
|
|
99
|
+
bound = max_depth
|
|
100
|
+
|
|
101
|
+
identify = quotient_key or (lambda word: word)
|
|
102
|
+
widths: list[int] = []
|
|
103
|
+
information: list[float] = []
|
|
104
|
+
|
|
105
|
+
for depth in range(bound + 1):
|
|
106
|
+
prefixes: set[Hashable] = set()
|
|
107
|
+
if depth == 0:
|
|
108
|
+
prefixes.add(identify(ProcessWord()))
|
|
109
|
+
else:
|
|
110
|
+
for history in histories:
|
|
111
|
+
if history.depth < depth:
|
|
112
|
+
continue
|
|
113
|
+
prefix = ProcessWord(history.steps[:depth])
|
|
114
|
+
prefixes.add(identify(prefix))
|
|
115
|
+
width = len(prefixes)
|
|
116
|
+
widths.append(width)
|
|
117
|
+
information.append(math.log2(width) if width > 0 else -math.inf)
|
|
118
|
+
|
|
119
|
+
return BoundaryProfile(tuple(widths), tuple(information))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class PrefixCodeMetrics:
|
|
124
|
+
"""Geometry/information metrics of one finite prefix presentation."""
|
|
125
|
+
|
|
126
|
+
expected_depth: float
|
|
127
|
+
worst_depth: int
|
|
128
|
+
kraft_sum: float
|
|
129
|
+
entropy: float
|
|
130
|
+
redundancy: float
|
|
131
|
+
leaf_count: int
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class PrefixCode(Generic[SymbolT]):
|
|
136
|
+
"""A finite binary prefix code together with its source weights."""
|
|
137
|
+
|
|
138
|
+
codes: Mapping[SymbolT, tuple[int, ...]]
|
|
139
|
+
weights: Mapping[SymbolT, float]
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
if set(self.codes) != set(self.weights):
|
|
143
|
+
raise ValueError("codes and weights must have identical symbol sets")
|
|
144
|
+
for code in self.codes.values():
|
|
145
|
+
if not code or any(bit not in (0, 1) for bit in code):
|
|
146
|
+
raise ValueError("binary codes must be non-empty tuples of 0/1")
|
|
147
|
+
if not self.is_prefix_free():
|
|
148
|
+
raise ValueError("codes are not prefix-free")
|
|
149
|
+
|
|
150
|
+
def is_prefix_free(self) -> bool:
|
|
151
|
+
codewords = tuple(self.codes.values())
|
|
152
|
+
for i, left in enumerate(codewords):
|
|
153
|
+
for j, right in enumerate(codewords):
|
|
154
|
+
if i == j:
|
|
155
|
+
continue
|
|
156
|
+
if len(left) <= len(right) and right[: len(left)] == left:
|
|
157
|
+
return False
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def metrics(self) -> PrefixCodeMetrics:
|
|
161
|
+
total = sum(float(weight) for weight in self.weights.values())
|
|
162
|
+
if total <= 0:
|
|
163
|
+
raise ValueError("total code weight must be positive")
|
|
164
|
+
|
|
165
|
+
expected = 0.0
|
|
166
|
+
entropy = 0.0
|
|
167
|
+
for symbol, raw_weight in self.weights.items():
|
|
168
|
+
probability = float(raw_weight) / total
|
|
169
|
+
if probability > 0:
|
|
170
|
+
expected += probability * len(self.codes[symbol])
|
|
171
|
+
entropy -= probability * math.log2(probability)
|
|
172
|
+
|
|
173
|
+
kraft = sum(2.0 ** (-len(code)) for code in self.codes.values())
|
|
174
|
+
worst = max((len(code) for code in self.codes.values()), default=0)
|
|
175
|
+
return PrefixCodeMetrics(
|
|
176
|
+
expected_depth=expected,
|
|
177
|
+
worst_depth=worst,
|
|
178
|
+
kraft_sum=kraft,
|
|
179
|
+
entropy=entropy,
|
|
180
|
+
redundancy=expected - entropy,
|
|
181
|
+
leaf_count=len(self.codes),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def encode(self, symbols: Sequence[SymbolT]) -> tuple[int, ...]:
|
|
185
|
+
bits: list[int] = []
|
|
186
|
+
for symbol in symbols:
|
|
187
|
+
try:
|
|
188
|
+
bits.extend(self.codes[symbol])
|
|
189
|
+
except KeyError as exc:
|
|
190
|
+
raise KeyError(f"symbol has no prefix code: {symbol!r}") from exc
|
|
191
|
+
return tuple(bits)
|
|
192
|
+
|
|
193
|
+
def decode(self, bits: Sequence[int]) -> tuple[SymbolT, ...]:
|
|
194
|
+
"""Decode a concatenation of codewords; reject incomplete prefixes."""
|
|
195
|
+
inverse = {code: symbol for symbol, code in self.codes.items()}
|
|
196
|
+
out: list[SymbolT] = []
|
|
197
|
+
prefix: tuple[int, ...] = ()
|
|
198
|
+
valid_prefixes = {
|
|
199
|
+
code[:length]
|
|
200
|
+
for code in inverse
|
|
201
|
+
for length in range(1, len(code) + 1)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for raw_bit in bits:
|
|
205
|
+
if raw_bit not in (0, 1):
|
|
206
|
+
raise ValueError("encoded stream must contain only 0/1")
|
|
207
|
+
prefix = prefix + (int(raw_bit),)
|
|
208
|
+
if prefix in inverse:
|
|
209
|
+
out.append(inverse[prefix])
|
|
210
|
+
prefix = ()
|
|
211
|
+
elif prefix not in valid_prefixes:
|
|
212
|
+
raise ValueError("bit stream does not match the prefix code")
|
|
213
|
+
|
|
214
|
+
if prefix:
|
|
215
|
+
raise ValueError("bit stream ends in an incomplete codeword")
|
|
216
|
+
return tuple(out)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class _HuffmanNode(Generic[SymbolT]):
|
|
221
|
+
symbol: SymbolT | None = None
|
|
222
|
+
left: "_HuffmanNode[SymbolT] | None" = None
|
|
223
|
+
right: "_HuffmanNode[SymbolT] | None" = None
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def is_leaf(self) -> bool:
|
|
227
|
+
return self.symbol is not None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def huffman_prefix_code(weights: Mapping[SymbolT, float]) -> PrefixCode[SymbolT]:
|
|
231
|
+
"""Build a deterministic binary Huffman code for a finite weighted boundary."""
|
|
232
|
+
if not weights:
|
|
233
|
+
raise ValueError("at least one symbol is required")
|
|
234
|
+
|
|
235
|
+
normalized: dict[SymbolT, float] = {}
|
|
236
|
+
for symbol, raw_weight in weights.items():
|
|
237
|
+
weight = float(raw_weight)
|
|
238
|
+
if not math.isfinite(weight) or weight < 0:
|
|
239
|
+
raise ValueError("Huffman weights must be finite and non-negative")
|
|
240
|
+
normalized[symbol] = weight
|
|
241
|
+
if sum(normalized.values()) <= 0:
|
|
242
|
+
raise ValueError("at least one Huffman weight must be positive")
|
|
243
|
+
|
|
244
|
+
counter = itertools.count()
|
|
245
|
+
heap: list[tuple[float, int, _HuffmanNode[SymbolT]]] = []
|
|
246
|
+
for symbol, weight in normalized.items():
|
|
247
|
+
heapq.heappush(heap, (weight, next(counter), _HuffmanNode(symbol=symbol)))
|
|
248
|
+
|
|
249
|
+
if len(heap) == 1:
|
|
250
|
+
_weight, _serial, node = heap[0]
|
|
251
|
+
assert node.symbol is not None
|
|
252
|
+
return PrefixCode(codes={node.symbol: (0,)}, weights=normalized)
|
|
253
|
+
|
|
254
|
+
while len(heap) > 1:
|
|
255
|
+
left_weight, _left_serial, left = heapq.heappop(heap)
|
|
256
|
+
right_weight, _right_serial, right = heapq.heappop(heap)
|
|
257
|
+
parent = _HuffmanNode(left=left, right=right)
|
|
258
|
+
heapq.heappush(
|
|
259
|
+
heap,
|
|
260
|
+
(left_weight + right_weight, next(counter), parent),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
root = heap[0][2]
|
|
264
|
+
codes: dict[SymbolT, tuple[int, ...]] = {}
|
|
265
|
+
|
|
266
|
+
def visit(node: _HuffmanNode[SymbolT], prefix: tuple[int, ...]) -> None:
|
|
267
|
+
if node.is_leaf:
|
|
268
|
+
assert node.symbol is not None
|
|
269
|
+
codes[node.symbol] = prefix
|
|
270
|
+
return
|
|
271
|
+
assert node.left is not None and node.right is not None
|
|
272
|
+
visit(node.left, prefix + (0,))
|
|
273
|
+
visit(node.right, prefix + (1,))
|
|
274
|
+
|
|
275
|
+
visit(root, ())
|
|
276
|
+
return PrefixCode(codes=codes, weights=normalized)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Linear/Krylov calibration backend.
|
|
2
|
+
|
|
3
|
+
This module exists to verify that ordinary linear-algebra structure can be
|
|
4
|
+
recovered *from process return relations*. Eigen/Jordan data are intentionally
|
|
5
|
+
not part of the discovery API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from functools import reduce
|
|
12
|
+
from math import gcd
|
|
13
|
+
|
|
14
|
+
import sympy as sp
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class KrylovReturnRelation:
|
|
19
|
+
coefficients: tuple[sp.Expr, ...]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def order(self) -> int:
|
|
23
|
+
return len(self.coefficients) - 1
|
|
24
|
+
|
|
25
|
+
def as_polynomial(self, symbol: sp.Symbol | None = None) -> sp.Expr:
|
|
26
|
+
z = symbol or sp.Symbol("X")
|
|
27
|
+
return sp.expand(sum(c * z**i for i, c in enumerate(self.coefficients)))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _primitive(v: sp.Matrix) -> tuple[sp.Expr, ...]:
|
|
31
|
+
qs = [sp.Rational(x) for x in v]
|
|
32
|
+
lcm = sp.ilcm(*[q.q for q in qs]) if qs else 1
|
|
33
|
+
ints = [int(q * lcm) for q in qs]
|
|
34
|
+
nz = [abs(i) for i in ints if i]
|
|
35
|
+
common = reduce(gcd, nz) if nz else 1
|
|
36
|
+
ints = [i // common for i in ints]
|
|
37
|
+
if next((i for i in ints if i), 1) < 0:
|
|
38
|
+
ints = [-i for i in ints]
|
|
39
|
+
return tuple(sp.Integer(i) for i in ints)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def discover_krylov_relation(
|
|
43
|
+
operator: sp.Matrix,
|
|
44
|
+
vector: sp.Matrix,
|
|
45
|
+
max_order: int | None = None,
|
|
46
|
+
) -> KrylovReturnRelation | None:
|
|
47
|
+
"""Discover the shortest bounded recurrence in ``v, Xv, X^2v, ...``."""
|
|
48
|
+
operator = sp.Matrix(operator)
|
|
49
|
+
vector = sp.Matrix(vector)
|
|
50
|
+
if operator.rows != operator.cols:
|
|
51
|
+
raise ValueError("operator must be square")
|
|
52
|
+
if vector.rows != operator.rows or vector.cols != 1:
|
|
53
|
+
raise ValueError("vector must be a compatible column vector")
|
|
54
|
+
bound = max_order if max_order is not None else operator.rows + 1
|
|
55
|
+
orbit = [vector]
|
|
56
|
+
for _ in range(bound):
|
|
57
|
+
orbit.append(operator * orbit[-1])
|
|
58
|
+
for order in range(1, bound + 1):
|
|
59
|
+
matrix = sp.Matrix.hstack(*orbit[: order + 1])
|
|
60
|
+
candidates = [v for v in matrix.nullspace() if v[-1] != 0]
|
|
61
|
+
if candidates:
|
|
62
|
+
coeffs = _primitive(candidates[0])
|
|
63
|
+
certificate = sum(
|
|
64
|
+
(coeffs[i] * orbit[i] for i in range(order + 1)),
|
|
65
|
+
sp.zeros(operator.rows, 1),
|
|
66
|
+
)
|
|
67
|
+
if certificate != sp.zeros(operator.rows, 1):
|
|
68
|
+
raise AssertionError("Krylov return-relation verification failed")
|
|
69
|
+
return KrylovReturnRelation(coeffs)
|
|
70
|
+
return None
|