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,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
|
+
)
|
aeg_shakespeare/core.py
ADDED
|
@@ -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
|
+
]
|
aeg_shakespeare/cost.py
ADDED
|
@@ -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)
|