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.
Files changed (63) hide show
  1. aeg_shakespeare/__init__.py +52 -0
  2. aeg_shakespeare/_legacy_api.py +299 -0
  3. aeg_shakespeare/analysis/__init__.py +12 -0
  4. aeg_shakespeare/analysis/abelian.py +69 -0
  5. aeg_shakespeare/analysis/algebraic.py +11 -0
  6. aeg_shakespeare/analysis/am.py +19 -0
  7. aeg_shakespeare/analysis/connection.py +74 -0
  8. aeg_shakespeare/analysis/decomposition.py +37 -0
  9. aeg_shakespeare/analysis/module.py +5 -0
  10. aeg_shakespeare/central.py +20 -0
  11. aeg_shakespeare/constraints.py +121 -0
  12. aeg_shakespeare/construction.py +303 -0
  13. aeg_shakespeare/core.py +59 -0
  14. aeg_shakespeare/cost.py +51 -0
  15. aeg_shakespeare/discovery/__init__.py +53 -0
  16. aeg_shakespeare/discovery/coefficient_extension.py +75 -0
  17. aeg_shakespeare/discovery/polynomial.py +342 -0
  18. aeg_shakespeare/discovery/selection.py +146 -0
  19. aeg_shakespeare/discovery/structured.py +201 -0
  20. aeg_shakespeare/families.py +31 -0
  21. aeg_shakespeare/frame.py +5 -0
  22. aeg_shakespeare/function_theory/__init__.py +100 -0
  23. aeg_shakespeare/function_theory/abel_jacobi.py +246 -0
  24. aeg_shakespeare/function_theory/abelian.py +129 -0
  25. aeg_shakespeare/function_theory/algebraic.py +105 -0
  26. aeg_shakespeare/function_theory/am.py +266 -0
  27. aeg_shakespeare/function_theory/intersection.py +321 -0
  28. aeg_shakespeare/function_theory/module.py +118 -0
  29. aeg_shakespeare/function_theory/period_matrix.py +155 -0
  30. aeg_shakespeare/function_theory/periods.py +216 -0
  31. aeg_shakespeare/function_theory/real_branch_cycles.py +286 -0
  32. aeg_shakespeare/function_theory/weierstrass.py +136 -0
  33. aeg_shakespeare/grammar.py +225 -0
  34. aeg_shakespeare/history_geometry.py +276 -0
  35. aeg_shakespeare/linear.py +70 -0
  36. aeg_shakespeare/presentation/__init__.py +23 -0
  37. aeg_shakespeare/presentation/budget.py +27 -0
  38. aeg_shakespeare/presentation/canonicalization.py +143 -0
  39. aeg_shakespeare/presentation/constraints.py +5 -0
  40. aeg_shakespeare/presentation/construction.py +19 -0
  41. aeg_shakespeare/presentation/grammar.py +15 -0
  42. aeg_shakespeare/presentation/history.py +45 -0
  43. aeg_shakespeare/presentation/morphism.py +66 -0
  44. aeg_shakespeare/presentation/relations.py +31 -0
  45. aeg_shakespeare/presentation/search.py +31 -0
  46. aeg_shakespeare/process/__init__.py +16 -0
  47. aeg_shakespeare/process/finite/__init__.py +43 -0
  48. aeg_shakespeare/process/finite/cocycle.py +166 -0
  49. aeg_shakespeare/process/finite/families.py +318 -0
  50. aeg_shakespeare/process/history.py +53 -0
  51. aeg_shakespeare/process/local/__init__.py +7 -0
  52. aeg_shakespeare/process/local/direction.py +88 -0
  53. aeg_shakespeare/process/local/frame.py +73 -0
  54. aeg_shakespeare/process/local/system.py +43 -0
  55. aeg_shakespeare/relations.py +374 -0
  56. aeg_shakespeare/rewrite.py +157 -0
  57. aeg_shakespeare/search.py +286 -0
  58. aeg_shakespeare/signature.py +155 -0
  59. process_geometry-0.0.3.dist-info/METADATA +305 -0
  60. process_geometry-0.0.3.dist-info/RECORD +63 -0
  61. process_geometry-0.0.3.dist-info/WHEEL +5 -0
  62. process_geometry-0.0.3.dist-info/licenses/LICENSE +24 -0
  63. 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