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,121 @@
1
+ """Exact algebraic constraints and quotient reduction.
2
+
3
+ Mathematical lineage
4
+ --------------------
5
+ Classical mechanics is often presented *after* coordinates have already solved
6
+ part of the geometry: a pendulum becomes an angle, a rigid body becomes a chart,
7
+ and the remaining equations are written on that chosen coordinate space. The
8
+ Shakespeare reconstruction reverses that order. It keeps the primitive
9
+ assignments and their relations visible, then asks which dynamics preserve the
10
+ relations and what quotient those relations force.
11
+
12
+ This is why constraints are not treated as side conditions. A relation such as
13
+ ``q.q - 1 = 0`` is part of the process presentation. Requiring its successive
14
+ process images to vanish can determine otherwise unresolved process terms; only
15
+ later may a familiar coordinate description appear as a classical shadow.
16
+
17
+ Implementation
18
+ --------------
19
+ ``AlgebraicConstraintSet`` represents the polynomial quotient exactly through a
20
+ Groebner backend. ``constraint_prolongation`` records the successive history
21
+ ``r, D r, D^2 r, ...`` without assuming a named mechanical model.
22
+
23
+ Boundary
24
+ --------
25
+ This module does not claim that every useful constraint is polynomial, nor that
26
+ Groebner reduction is the ontology of equality. It is one exact backend for a
27
+ relation layer whose meaning is supplied by the process presentation.
28
+
29
+ See ``docs/07-classical-calibration-program.md`` and
30
+ ``docs/09-literate-programming-and-mathematical-lineage.md``.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from dataclasses import dataclass
36
+ from functools import cached_property
37
+ from typing import Sequence
38
+
39
+ import sympy as sp
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class AlgebraicConstraintSet:
44
+ """A polynomial quotient specified by generators of an algebraic ideal."""
45
+
46
+ variables: tuple[sp.Symbol, ...]
47
+ relations: tuple[sp.Expr, ...]
48
+ order: str = "grevlex"
49
+
50
+ def __post_init__(self) -> None:
51
+ if not self.variables:
52
+ raise ValueError("constraint set requires at least one variable")
53
+ variables = tuple(self.variables)
54
+ if len(set(variables)) != len(variables):
55
+ raise ValueError("constraint variables must be distinct")
56
+ relations = tuple(sp.expand(sp.sympify(relation)) for relation in self.relations)
57
+ if any(relation == 0 for relation in relations):
58
+ relations = tuple(relation for relation in relations if relation != 0)
59
+ object.__setattr__(self, "variables", variables)
60
+ object.__setattr__(self, "relations", relations)
61
+
62
+ @cached_property
63
+ def groebner_basis(self) -> sp.GroebnerBasis | None:
64
+ if not self.relations:
65
+ return None
66
+ try:
67
+ return sp.groebner(self.relations, *self.variables, order=self.order)
68
+ except sp.PolynomialError as exc:
69
+ raise ValueError("constraints must be polynomial in the declared variables") from exc
70
+
71
+ def reduce(self, expr: sp.Expr) -> sp.Expr:
72
+ """Return the exact normal remainder modulo the constraint ideal."""
73
+
74
+ expr = sp.expand(sp.sympify(expr))
75
+ if self.groebner_basis is None:
76
+ return expr
77
+ try:
78
+ _quotients, remainder = self.groebner_basis.reduce(expr)
79
+ except sp.PolynomialError as exc:
80
+ raise ValueError("expression must be polynomial in the declared variables") from exc
81
+ return sp.expand(remainder)
82
+
83
+ def contains(self, expr: sp.Expr) -> bool:
84
+ """Whether ``expr = 0`` follows from the declared algebraic relations."""
85
+
86
+ return self.reduce(expr) == 0
87
+
88
+ def equivalent(self, left: sp.Expr, right: sp.Expr) -> bool:
89
+ """Whether two expressions agree in the quotient algebra."""
90
+
91
+ return self.contains(sp.expand(sp.sympify(left) - sp.sympify(right)))
92
+
93
+ def adjoin(self, *relations: sp.Expr) -> "AlgebraicConstraintSet":
94
+ """Return the quotient obtained by adding more exact relations."""
95
+
96
+ return AlgebraicConstraintSet(
97
+ variables=self.variables,
98
+ relations=self.relations + tuple(relations),
99
+ order=self.order,
100
+ )
101
+
102
+
103
+ def constraint_prolongation(
104
+ derive,
105
+ relation: sp.Expr,
106
+ *,
107
+ order: int,
108
+ ) -> tuple[sp.Expr, ...]:
109
+ """Return ``relation, D relation, ..., D^order relation``.
110
+
111
+ ``derive`` is caller-supplied so this utility can be used with
112
+ ``ProcessSystem``, ``ProcessFrame`` generators, or future process backends.
113
+ No dynamics-specific multiplier model is assumed here.
114
+ """
115
+
116
+ if order < 0:
117
+ raise ValueError("order must be non-negative")
118
+ values = [sp.expand(sp.sympify(relation))]
119
+ for _ in range(order):
120
+ values.append(sp.expand(sp.sympify(derive(values[-1]))))
121
+ return tuple(values)
@@ -0,0 +1,303 @@
1
+ """Construction-history-preserving primitive proposals.
2
+
3
+ Candidate primitives should not be identified merely because their final SymPy
4
+ expressions are equal. This module provides a small symbolic proposal IR that
5
+ keeps an explicit construction tree, operation costs, and bounded generation.
6
+ It is intentionally separate from presentation evaluation/Pareto search.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from itertools import product
13
+ import math
14
+ from typing import Callable, Sequence
15
+
16
+ import sympy as sp
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SymbolicOperation:
21
+ """Caller-declared symbolic operation available to proposal generation.
22
+
23
+ Parameters
24
+ ----------
25
+ name:
26
+ Stable display name used in construction certificates.
27
+ arity:
28
+ Number of input expressions.
29
+ apply:
30
+ Callable implementing the operation's symbolic semantics. Callable
31
+ identity remains part of construction identity, so two operations with
32
+ the same display name are not silently equated.
33
+ cost:
34
+ Non-negative construction cost of one use.
35
+ commutative:
36
+ If true, argument permutations are considered the same construction for
37
+ this operation. Associativity is *not* assumed automatically.
38
+ """
39
+
40
+ name: str
41
+ arity: int
42
+ apply: Callable[..., sp.Expr] = field(repr=False)
43
+ cost: float = 1.0
44
+ commutative: bool = False
45
+
46
+ def __post_init__(self) -> None:
47
+ if not self.name:
48
+ raise ValueError("operation name must be non-empty")
49
+ if self.arity < 1:
50
+ raise ValueError("operation arity must be positive")
51
+ if not math.isfinite(float(self.cost)) or self.cost < 0:
52
+ raise ValueError("operation cost must be finite and non-negative")
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class PrimitiveConstruction:
57
+ """A tree certificate for how one candidate primitive was built."""
58
+
59
+ atom: sp.Expr | None = None
60
+ operation: SymbolicOperation | None = None
61
+ children: tuple["PrimitiveConstruction", ...] = ()
62
+
63
+ def __post_init__(self) -> None:
64
+ has_atom = self.atom is not None
65
+ has_operation = self.operation is not None
66
+ if has_atom == has_operation:
67
+ raise ValueError("construction must contain exactly one atom or operation")
68
+ if has_atom:
69
+ if self.children:
70
+ raise ValueError("atomic construction cannot have children")
71
+ object.__setattr__(self, "atom", sp.expand(sp.sympify(self.atom)))
72
+ return
73
+ assert self.operation is not None
74
+ if len(self.children) != self.operation.arity:
75
+ raise ValueError("construction child count must match operation arity")
76
+
77
+ @classmethod
78
+ def atomic(cls, expr: sp.Expr) -> "PrimitiveConstruction":
79
+ return cls(atom=sp.expand(sp.sympify(expr)))
80
+
81
+ @property
82
+ def depth(self) -> int:
83
+ if self.atom is not None:
84
+ return 0
85
+ return 1 + max((child.depth for child in self.children), default=0)
86
+
87
+ @property
88
+ def operation_count(self) -> int:
89
+ if self.atom is not None:
90
+ return 0
91
+ return 1 + sum(child.operation_count for child in self.children)
92
+
93
+ @property
94
+ def cost(self) -> float:
95
+ if self.atom is not None:
96
+ return 0.0
97
+ assert self.operation is not None
98
+ return float(self.operation.cost) + sum(child.cost for child in self.children)
99
+
100
+ def recipe(self) -> str:
101
+ if self.atom is not None:
102
+ return str(self.atom)
103
+ assert self.operation is not None
104
+ arguments = ", ".join(child.recipe() for child in self.children)
105
+ return f"{self.operation.name}({arguments})"
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class PrimitiveProposal:
110
+ """A candidate expression together with its non-erased construction tree."""
111
+
112
+ expression: sp.Expr
113
+ construction: PrimitiveConstruction
114
+
115
+ @property
116
+ def cost(self) -> float:
117
+ return self.construction.cost
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class RejectedPrimitiveProposal:
122
+ construction: PrimitiveConstruction
123
+ reason: str
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class PrimitiveProposalResult:
128
+ proposals: tuple[PrimitiveProposal, ...]
129
+ rejected: tuple[RejectedPrimitiveProposal, ...]
130
+ truncated: bool = False
131
+
132
+ @property
133
+ def semantic_expression_count(self) -> int:
134
+ """Count distinct final expressions without erasing proposal histories."""
135
+
136
+ return len({sp.srepr(proposal.expression) for proposal in self.proposals})
137
+
138
+
139
+ def _polynomial_degree(expr: sp.Expr, variables: Sequence[sp.Symbol]) -> int:
140
+ try:
141
+ degree = sp.Poly(sp.expand(expr), *variables).total_degree()
142
+ except sp.PolynomialError as exc:
143
+ raise ValueError("proposal is not polynomial in the declared variables") from exc
144
+ if degree is sp.S.NegativeInfinity:
145
+ return 0
146
+ return int(degree)
147
+
148
+
149
+ def _construction_key(construction: PrimitiveConstruction) -> str:
150
+ return construction.recipe()
151
+
152
+
153
+ def generate_primitive_proposals(
154
+ atoms: Sequence[sp.Expr],
155
+ operations: Sequence[SymbolicOperation],
156
+ *,
157
+ variables: Sequence[sp.Symbol],
158
+ max_depth: int,
159
+ max_degree: int,
160
+ max_candidates: int = 256,
161
+ include_atoms: bool = True,
162
+ ) -> PrimitiveProposalResult:
163
+ """Generate bounded symbolic primitive proposals without erasing recipes.
164
+
165
+ Generation proceeds by construction depth. Semantic duplicates are kept if
166
+ they have different construction trees. The only automatic construction
167
+ quotient is argument permutation for operations explicitly declared
168
+ commutative by the caller.
169
+
170
+ ``max_candidates`` bounds generated non-atomic proposals; atoms are caller
171
+ inputs and are not charged against that bound.
172
+ """
173
+
174
+ if max_depth < 0:
175
+ raise ValueError("max_depth must be non-negative")
176
+ if max_degree < 0:
177
+ raise ValueError("max_degree must be non-negative")
178
+ if max_candidates < 0:
179
+ raise ValueError("max_candidates must be non-negative")
180
+
181
+ variables = tuple(variables)
182
+ constructions: list[PrimitiveConstruction] = []
183
+ seen_constructions: set[PrimitiveConstruction] = set()
184
+ proposals: list[PrimitiveProposal] = []
185
+ rejected: list[RejectedPrimitiveProposal] = []
186
+
187
+ for atom in atoms:
188
+ construction = PrimitiveConstruction.atomic(atom)
189
+ if construction in seen_constructions:
190
+ continue
191
+ seen_constructions.add(construction)
192
+ expression = construction.atom
193
+ assert expression is not None
194
+ try:
195
+ degree = _polynomial_degree(expression, variables)
196
+ except ValueError as exc:
197
+ rejected.append(RejectedPrimitiveProposal(construction, str(exc)))
198
+ continue
199
+ if degree > max_degree:
200
+ rejected.append(
201
+ RejectedPrimitiveProposal(
202
+ construction,
203
+ f"polynomial degree {degree} exceeds bound {max_degree}",
204
+ )
205
+ )
206
+ continue
207
+ constructions.append(construction)
208
+ if include_atoms:
209
+ proposals.append(PrimitiveProposal(expression, construction))
210
+
211
+ if max_candidates == 0:
212
+ return PrimitiveProposalResult(
213
+ proposals=tuple(proposals),
214
+ rejected=tuple(rejected),
215
+ truncated=max_depth > 0 and bool(operations) and bool(constructions),
216
+ )
217
+
218
+ generated_nonatoms = 0
219
+
220
+ for depth in range(1, max_depth + 1):
221
+ pool = tuple(constructions)
222
+ if not pool:
223
+ break
224
+
225
+ for operation in operations:
226
+ for children in product(pool, repeat=operation.arity):
227
+ if max(child.depth for child in children) != depth - 1:
228
+ continue
229
+ if operation.commutative:
230
+ keys = tuple(_construction_key(child) for child in children)
231
+ if keys != tuple(sorted(keys)):
232
+ continue
233
+
234
+ construction = PrimitiveConstruction(
235
+ operation=operation,
236
+ children=tuple(children),
237
+ )
238
+ if construction in seen_constructions:
239
+ continue
240
+ seen_constructions.add(construction)
241
+
242
+ try:
243
+ expression = _evaluate_construction(construction)
244
+ except Exception as exc: # caller operation boundary
245
+ rejected.append(
246
+ RejectedPrimitiveProposal(
247
+ construction,
248
+ f"operation evaluation failed: {exc}",
249
+ )
250
+ )
251
+ continue
252
+
253
+ if expression == 0:
254
+ rejected.append(
255
+ RejectedPrimitiveProposal(
256
+ construction,
257
+ "zero is not a primitive proposal",
258
+ )
259
+ )
260
+ continue
261
+
262
+ try:
263
+ degree = _polynomial_degree(expression, variables)
264
+ except ValueError as exc:
265
+ rejected.append(RejectedPrimitiveProposal(construction, str(exc)))
266
+ continue
267
+ if degree > max_degree:
268
+ rejected.append(
269
+ RejectedPrimitiveProposal(
270
+ construction,
271
+ f"polynomial degree {degree} exceeds bound {max_degree}",
272
+ )
273
+ )
274
+ continue
275
+
276
+ proposals.append(PrimitiveProposal(expression, construction))
277
+ constructions.append(construction)
278
+ generated_nonatoms += 1
279
+ if generated_nonatoms >= max_candidates:
280
+ return PrimitiveProposalResult(
281
+ proposals=tuple(proposals),
282
+ rejected=tuple(rejected),
283
+ truncated=True,
284
+ )
285
+
286
+ return PrimitiveProposalResult(
287
+ proposals=tuple(proposals),
288
+ rejected=tuple(rejected),
289
+ truncated=False,
290
+ )
291
+
292
+
293
+ def _evaluate_construction(construction: PrimitiveConstruction) -> sp.Expr:
294
+ if construction.atom is not None:
295
+ return construction.atom
296
+ assert construction.operation is not None
297
+ return sp.expand(
298
+ sp.sympify(
299
+ construction.operation.apply(
300
+ *[_evaluate_construction(child) for child in construction.children]
301
+ )
302
+ )
303
+ )
@@ -0,0 +1,59 @@
1
+ """Compatibility/backend module after semantic core decomposition.
2
+
3
+ Canonical semantic ownership is now:
4
+
5
+ - ``ProcessWord`` and ``interpret_history`` -> ``process.history``;
6
+ - ``ProcessSystem`` -> ``process.local``;
7
+ - ``SearchBudget`` -> ``presentation.budget``.
8
+
9
+ This module retains those identity-preserving imports for pre-refactor internal
10
+ paths and keeps ``homogeneous_monomials`` as a small SymPy backend helper. It is
11
+ no longer the architectural center of the public API.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Sequence
17
+
18
+ import sympy as sp
19
+
20
+ from .presentation.budget import SearchBudget
21
+ from .process.history import ProcessWord, interpret_history
22
+ from .process.local import ProcessSystem
23
+
24
+
25
+ def homogeneous_monomials(assignments: Sequence[sp.Symbol], degree: int) -> list[sp.Expr]:
26
+ """Enumerate commutative monomials of fixed total degree.
27
+
28
+ This is a SymPy-backend utility, not a restriction on the process ontology.
29
+ """
30
+ if degree < 0:
31
+ raise ValueError("degree must be non-negative")
32
+ assignments = tuple(assignments)
33
+ if not assignments:
34
+ return [sp.S.One] if degree == 0 else []
35
+
36
+ def compositions(total: int, n: int):
37
+ if n == 1:
38
+ yield (total,)
39
+ return
40
+ for i in range(total, -1, -1):
41
+ for rest in compositions(total - i, n - 1):
42
+ yield (i,) + rest
43
+
44
+ result: list[sp.Expr] = []
45
+ for exponents in compositions(degree, len(assignments)):
46
+ monomial = sp.S.One
47
+ for symbol, exponent in zip(assignments, exponents):
48
+ monomial *= symbol**exponent
49
+ result.append(sp.expand(monomial))
50
+ return result
51
+
52
+
53
+ __all__ = [
54
+ "ProcessWord",
55
+ "interpret_history",
56
+ "ProcessSystem",
57
+ "SearchBudget",
58
+ "homogeneous_monomials",
59
+ ]
@@ -0,0 +1,51 @@
1
+ """Cost objects for process-representation search.
2
+
3
+ Costs stay multi-axis by default. A caller may scalarize them later, but the
4
+ library does not hide trade-offs between grammar size, relation complexity,
5
+ history depth, decoder complexity, and task error.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Mapping
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PresentationCost:
16
+ """Multi-axis cost of a process presentation."""
17
+
18
+ grammar: float = 0.0
19
+ relations: float = 0.0
20
+ history: float = 0.0
21
+ decoder: float = 0.0
22
+ task_error: float = 0.0
23
+
24
+ def components(self) -> Mapping[str, float]:
25
+ return {
26
+ "grammar": self.grammar,
27
+ "relations": self.relations,
28
+ "history": self.history,
29
+ "decoder": self.decoder,
30
+ "task_error": self.task_error,
31
+ }
32
+
33
+ def scalarize(self, weights: Mapping[str, float] | None = None) -> float:
34
+ """Return a caller-controlled weighted sum.
35
+
36
+ Missing weights default to ``1``. Shakespeare does not prescribe one
37
+ universal scalar objective because different tasks may value grammar,
38
+ history, and decoding differently.
39
+ """
40
+
41
+ weights = weights or {}
42
+ return sum(weights.get(name, 1.0) * value for name, value in self.components().items())
43
+
44
+ def dominates(self, other: "PresentationCost") -> bool:
45
+ """Whether this cost Pareto-dominates ``other``."""
46
+
47
+ mine = tuple(self.components().values())
48
+ theirs = tuple(other.components().values())
49
+ return all(a <= b for a, b in zip(mine, theirs)) and any(
50
+ a < b for a, b in zip(mine, theirs)
51
+ )
@@ -0,0 +1,53 @@
1
+ """Process-first representation discovery backends."""
2
+
3
+ from .coefficient_extension import factor_process_relation_over_extension
4
+ from .polynomial import (
5
+ ObservableQuotient,
6
+ ObservableRelation,
7
+ PolynomialInvariant,
8
+ PolynomialInvariantDiscovery,
9
+ PolynomialObserverBasis,
10
+ discover_first_order_process_quotient,
11
+ discover_observable_relations,
12
+ discover_polynomial_invariants,
13
+ generate_polynomial_observer_basis,
14
+ )
15
+ from .selection import (
16
+ FirstOrderObservablePresentation,
17
+ search_first_order_process_quotients,
18
+ structural_first_order_quotient_cost,
19
+ )
20
+ from .structured import (
21
+ PairableAtom,
22
+ PairingConstruction,
23
+ PairingSpec,
24
+ StructuredObserverProposal,
25
+ StructuredObserverProposalResult,
26
+ euclidean_pairing,
27
+ generate_pairing_observers,
28
+ nonstationary_observer_proposals,
29
+ )
30
+
31
+ __all__ = [
32
+ "factor_process_relation_over_extension",
33
+ "ObservableQuotient",
34
+ "ObservableRelation",
35
+ "PolynomialInvariant",
36
+ "PolynomialInvariantDiscovery",
37
+ "PolynomialObserverBasis",
38
+ "discover_first_order_process_quotient",
39
+ "discover_observable_relations",
40
+ "discover_polynomial_invariants",
41
+ "generate_polynomial_observer_basis",
42
+ "FirstOrderObservablePresentation",
43
+ "search_first_order_process_quotients",
44
+ "structural_first_order_quotient_cost",
45
+ "PairableAtom",
46
+ "PairingConstruction",
47
+ "PairingSpec",
48
+ "StructuredObserverProposal",
49
+ "StructuredObserverProposalResult",
50
+ "euclidean_pairing",
51
+ "generate_pairing_observers",
52
+ "nonstationary_observer_proposals",
53
+ ]
@@ -0,0 +1,75 @@
1
+ """Explicit coefficient-language extensions for process-relation factorization.
2
+
3
+ A discovered process relation is always meaningful before any root or spectral
4
+ interpretation. Sometimes a caller may nevertheless ask whether the same
5
+ relation admits a finer factorization after enlarging the coefficient language.
6
+ This module provides exactly that experiment and nothing more.
7
+
8
+ It deliberately does not define eigenvalues, eigenspaces, a spectral theorem,
9
+ or an automatic policy for choosing coefficient fields. The extension is an
10
+ explicit representation proposal supplied by the caller.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sympy as sp
16
+
17
+ from ..relations import ProcessPolynomialRelation
18
+
19
+
20
+ def factor_process_relation_over_extension(
21
+ relation: ProcessPolynomialRelation,
22
+ extension,
23
+ ) -> tuple[ProcessPolynomialRelation, ...]:
24
+ """Factor ``relation`` after an explicit algebraic coefficient extension.
25
+
26
+ Parameters
27
+ ----------
28
+ relation:
29
+ The already-discovered constant-coefficient process relation.
30
+ extension:
31
+ A SymPy algebraic extension accepted by ``factor_list``; for example
32
+ ``sp.I`` for adjoining a square root of ``-1``.
33
+
34
+ Notes
35
+ -----
36
+ Multiplicity is retained inside each primary factor, matching the ordinary
37
+ Shakespeare relation-factor convention. Returned factors are normalized
38
+ to monic process polynomials. No semantic meaning is attached to their
39
+ roots by this routine.
40
+ """
41
+
42
+ if extension is None:
43
+ raise ValueError("an explicit coefficient extension is required")
44
+
45
+ symbol = sp.Symbol("_D")
46
+ expression = relation.as_expr(symbol)
47
+ _unit, factors = sp.factor_list(
48
+ expression,
49
+ symbol,
50
+ extension=extension,
51
+ )
52
+ if not factors:
53
+ return (relation,)
54
+
55
+ result: list[ProcessPolynomialRelation] = []
56
+ for factor, multiplicity in factors:
57
+ primary = sp.Poly(
58
+ sp.expand(factor**multiplicity),
59
+ symbol,
60
+ extension=extension,
61
+ ).monic()
62
+ coefficients = tuple(
63
+ sp.simplify(value)
64
+ for value in reversed(primary.all_coeffs())
65
+ )
66
+ result.append(ProcessPolynomialRelation(coefficients))
67
+
68
+ original = sp.Poly(expression, symbol, extension=extension).monic().as_expr()
69
+ rebuilt = sp.expand(
70
+ sp.prod(factor.as_expr(symbol) for factor in result)
71
+ )
72
+ if sp.simplify(sp.expand(original - rebuilt)) != 0:
73
+ raise AssertionError("coefficient-extension factorization certificate failed")
74
+
75
+ return tuple(result)