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,342 @@
|
|
|
1
|
+
"""Bounded polynomial discovery for process observables, invariants, and quotients.
|
|
2
|
+
|
|
3
|
+
Mathematical pressure
|
|
4
|
+
---------------------
|
|
5
|
+
The first Shakespeare layers can preserve ordered histories, exact constraints,
|
|
6
|
+
finite grammars, and presentation costs once a useful representation has been
|
|
7
|
+
proposed. A remaining gap appears earlier: classical analysis often hands the
|
|
8
|
+
solver a good observable or first integral before the real calculation begins.
|
|
9
|
+
|
|
10
|
+
This module provides a deliberately small exact backend for removing part of
|
|
11
|
+
that prior choice. It searches a bounded polynomial observer grammar, discovers
|
|
12
|
+
first integrals as null directions of the represented process modulo declared
|
|
13
|
+
constraints, and eliminates source assignments to expose exact relations among
|
|
14
|
+
chosen process observables.
|
|
15
|
+
|
|
16
|
+
The polynomial grammar is a search proposal language, not a claim that every
|
|
17
|
+
useful observer is polynomial. Likewise Groebner elimination and exact linear
|
|
18
|
+
algebra are discovery/certificate backends rather than Shakespeare's ontology
|
|
19
|
+
of process equality.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Sequence
|
|
26
|
+
|
|
27
|
+
import sympy as sp
|
|
28
|
+
|
|
29
|
+
from ..core import homogeneous_monomials
|
|
30
|
+
from ..presentation.constraints import AlgebraicConstraintSet
|
|
31
|
+
from ..process.local import ProcessSystem
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _polynomial_dict(
|
|
35
|
+
expr: sp.Expr,
|
|
36
|
+
variables: Sequence[sp.Symbol],
|
|
37
|
+
) -> dict[tuple[int, ...], sp.Expr]:
|
|
38
|
+
try:
|
|
39
|
+
return sp.Poly(
|
|
40
|
+
sp.expand(sp.sympify(expr)),
|
|
41
|
+
*variables,
|
|
42
|
+
domain="EX",
|
|
43
|
+
).as_dict()
|
|
44
|
+
except sp.PolynomialError as exc:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"discovery expressions must be polynomial in the declared variables"
|
|
47
|
+
) from exc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _coefficient_matrix(
|
|
51
|
+
expressions: Sequence[sp.Expr],
|
|
52
|
+
variables: Sequence[sp.Symbol],
|
|
53
|
+
) -> sp.Matrix:
|
|
54
|
+
dictionaries = [_polynomial_dict(expression, variables) for expression in expressions]
|
|
55
|
+
monomials = sorted(
|
|
56
|
+
set().union(*(set(dictionary) for dictionary in dictionaries)),
|
|
57
|
+
reverse=True,
|
|
58
|
+
)
|
|
59
|
+
if not monomials:
|
|
60
|
+
return sp.zeros(0, len(expressions))
|
|
61
|
+
return sp.Matrix(
|
|
62
|
+
[
|
|
63
|
+
[dictionary.get(monomial, sp.S.Zero) for dictionary in dictionaries]
|
|
64
|
+
for monomial in monomials
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _independent_polynomials(
|
|
70
|
+
expressions: Sequence[sp.Expr],
|
|
71
|
+
variables: Sequence[sp.Symbol],
|
|
72
|
+
) -> tuple[sp.Expr, ...]:
|
|
73
|
+
basis: list[sp.Expr] = []
|
|
74
|
+
rank = 0
|
|
75
|
+
for expression in expressions:
|
|
76
|
+
expression = sp.expand(sp.sympify(expression))
|
|
77
|
+
if expression == 0:
|
|
78
|
+
continue
|
|
79
|
+
candidate = basis + [expression]
|
|
80
|
+
candidate_rank = int(_coefficient_matrix(candidate, variables).rank())
|
|
81
|
+
if candidate_rank > rank:
|
|
82
|
+
basis.append(expression)
|
|
83
|
+
rank = candidate_rank
|
|
84
|
+
return tuple(basis)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class PolynomialObserverBasis:
|
|
89
|
+
expressions: tuple[sp.Expr, ...]
|
|
90
|
+
max_degree: int
|
|
91
|
+
raw_candidate_count: int
|
|
92
|
+
quotient_reduced: bool
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
if self.max_degree < 0:
|
|
96
|
+
raise ValueError("max_degree must be non-negative")
|
|
97
|
+
if self.raw_candidate_count < len(self.expressions):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"raw_candidate_count cannot be smaller than the retained basis"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def generate_polynomial_observer_basis(
|
|
104
|
+
assignments: Sequence[sp.Symbol],
|
|
105
|
+
*,
|
|
106
|
+
max_degree: int,
|
|
107
|
+
constraints: AlgebraicConstraintSet | None = None,
|
|
108
|
+
include_constant: bool = False,
|
|
109
|
+
) -> PolynomialObserverBasis:
|
|
110
|
+
if max_degree < 0:
|
|
111
|
+
raise ValueError("max_degree must be non-negative")
|
|
112
|
+
assignments = tuple(assignments)
|
|
113
|
+
if not assignments and max_degree > 0:
|
|
114
|
+
raise ValueError("at least one assignment is required for positive degree")
|
|
115
|
+
|
|
116
|
+
if constraints is not None:
|
|
117
|
+
unknown = set(assignments) - set(constraints.variables)
|
|
118
|
+
if unknown:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"assignments missing from constraint variables: "
|
|
121
|
+
f"{sorted(map(str, unknown))}"
|
|
122
|
+
)
|
|
123
|
+
coordinate_variables = constraints.variables
|
|
124
|
+
else:
|
|
125
|
+
coordinate_variables = assignments
|
|
126
|
+
|
|
127
|
+
raw: list[sp.Expr] = []
|
|
128
|
+
start_degree = 0 if include_constant else 1
|
|
129
|
+
for degree in range(start_degree, max_degree + 1):
|
|
130
|
+
raw.extend(homogeneous_monomials(assignments, degree))
|
|
131
|
+
|
|
132
|
+
reduced = [
|
|
133
|
+
constraints.reduce(expression)
|
|
134
|
+
if constraints is not None
|
|
135
|
+
else sp.expand(expression)
|
|
136
|
+
for expression in raw
|
|
137
|
+
]
|
|
138
|
+
independent = _independent_polynomials(reduced, coordinate_variables)
|
|
139
|
+
return PolynomialObserverBasis(
|
|
140
|
+
expressions=independent,
|
|
141
|
+
max_degree=max_degree,
|
|
142
|
+
raw_candidate_count=len(raw),
|
|
143
|
+
quotient_reduced=constraints is not None,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class PolynomialInvariant:
|
|
149
|
+
expression: sp.Expr
|
|
150
|
+
coordinates: tuple[sp.Expr, ...]
|
|
151
|
+
derivative_remainder: sp.Expr
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def certified(self) -> bool:
|
|
155
|
+
return sp.expand(self.derivative_remainder) == 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class PolynomialInvariantDiscovery:
|
|
160
|
+
observer_basis: PolynomialObserverBasis
|
|
161
|
+
invariants: tuple[PolynomialInvariant, ...]
|
|
162
|
+
derivative_rank: int
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def nullity(self) -> int:
|
|
166
|
+
return len(self.observer_basis.expressions) - self.derivative_rank
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def discover_polynomial_invariants(
|
|
170
|
+
system: ProcessSystem,
|
|
171
|
+
*,
|
|
172
|
+
constraints: AlgebraicConstraintSet | None = None,
|
|
173
|
+
max_degree: int,
|
|
174
|
+
include_constant: bool = True,
|
|
175
|
+
) -> PolynomialInvariantDiscovery:
|
|
176
|
+
basis = generate_polynomial_observer_basis(
|
|
177
|
+
system.assignments,
|
|
178
|
+
max_degree=max_degree,
|
|
179
|
+
constraints=constraints,
|
|
180
|
+
include_constant=include_constant,
|
|
181
|
+
)
|
|
182
|
+
coordinate_variables = constraints.variables if constraints is not None else system.assignments
|
|
183
|
+
|
|
184
|
+
derivatives = tuple(
|
|
185
|
+
constraints.reduce(system.derive(expression))
|
|
186
|
+
if constraints is not None
|
|
187
|
+
else sp.expand(system.derive(expression))
|
|
188
|
+
for expression in basis.expressions
|
|
189
|
+
)
|
|
190
|
+
derivative_matrix = _coefficient_matrix(derivatives, coordinate_variables)
|
|
191
|
+
assignment_set = set(system.assignments)
|
|
192
|
+
|
|
193
|
+
invariants: list[PolynomialInvariant] = []
|
|
194
|
+
for vector in derivative_matrix.nullspace():
|
|
195
|
+
coordinates = tuple(sp.simplify(value) for value in vector)
|
|
196
|
+
expression = sp.expand(
|
|
197
|
+
sum(
|
|
198
|
+
coefficient * basis_expression
|
|
199
|
+
for coefficient, basis_expression in zip(coordinates, basis.expressions)
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
reduced_expression = constraints.reduce(expression) if constraints is not None else expression
|
|
203
|
+
if reduced_expression.free_symbols.isdisjoint(assignment_set):
|
|
204
|
+
continue
|
|
205
|
+
derivative_remainder = (
|
|
206
|
+
constraints.reduce(system.derive(expression))
|
|
207
|
+
if constraints is not None
|
|
208
|
+
else sp.expand(system.derive(expression))
|
|
209
|
+
)
|
|
210
|
+
invariants.append(
|
|
211
|
+
PolynomialInvariant(
|
|
212
|
+
expression=sp.factor(reduced_expression),
|
|
213
|
+
coordinates=coordinates,
|
|
214
|
+
derivative_remainder=sp.factor(derivative_remainder),
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return PolynomialInvariantDiscovery(
|
|
219
|
+
observer_basis=basis,
|
|
220
|
+
invariants=tuple(invariants),
|
|
221
|
+
derivative_rank=int(derivative_matrix.rank()),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True)
|
|
226
|
+
class ObservableRelation:
|
|
227
|
+
relation: sp.Expr
|
|
228
|
+
pullback_remainder: sp.Expr
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def certified(self) -> bool:
|
|
232
|
+
return sp.expand(self.pullback_remainder) == 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class ObservableQuotient:
|
|
237
|
+
symbols: tuple[sp.Symbol, ...]
|
|
238
|
+
observables: tuple[sp.Expr, ...]
|
|
239
|
+
parameters: tuple[sp.Symbol, ...]
|
|
240
|
+
eliminated_variables: tuple[sp.Symbol, ...]
|
|
241
|
+
relations: tuple[ObservableRelation, ...]
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def complete_certificates(self) -> bool:
|
|
245
|
+
return all(relation.certified for relation in self.relations)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def discover_observable_relations(
|
|
249
|
+
observables: Sequence[sp.Expr],
|
|
250
|
+
symbols: Sequence[sp.Symbol],
|
|
251
|
+
*,
|
|
252
|
+
constraints: AlgebraicConstraintSet,
|
|
253
|
+
source_variables: Sequence[sp.Symbol],
|
|
254
|
+
parameters: Sequence[sp.Symbol] = (),
|
|
255
|
+
) -> ObservableQuotient:
|
|
256
|
+
observables = tuple(sp.expand(sp.sympify(item)) for item in observables)
|
|
257
|
+
symbols = tuple(symbols)
|
|
258
|
+
source_variables = tuple(source_variables)
|
|
259
|
+
parameters = tuple(parameters)
|
|
260
|
+
|
|
261
|
+
if not observables or len(observables) != len(symbols):
|
|
262
|
+
raise ValueError("observables and symbols must be non-empty and have equal length")
|
|
263
|
+
if len(set(symbols)) != len(symbols):
|
|
264
|
+
raise ValueError("observable symbols must be distinct")
|
|
265
|
+
if len(set(source_variables)) != len(source_variables):
|
|
266
|
+
raise ValueError("source variables must be distinct")
|
|
267
|
+
if len(set(parameters)) != len(parameters):
|
|
268
|
+
raise ValueError("parameters must be distinct")
|
|
269
|
+
|
|
270
|
+
constraint_variables = set(constraints.variables)
|
|
271
|
+
source_set = set(source_variables)
|
|
272
|
+
parameter_set = set(parameters)
|
|
273
|
+
symbol_set = set(symbols)
|
|
274
|
+
if source_set & parameter_set:
|
|
275
|
+
raise ValueError("source variables and parameters must be disjoint")
|
|
276
|
+
if symbol_set & constraint_variables:
|
|
277
|
+
raise ValueError("observable symbols must be fresh relative to constraint variables")
|
|
278
|
+
if source_set | parameter_set != constraint_variables:
|
|
279
|
+
missing = constraint_variables - (source_set | parameter_set)
|
|
280
|
+
extra = (source_set | parameter_set) - constraint_variables
|
|
281
|
+
raise ValueError(
|
|
282
|
+
"source_variables + parameters must partition constraint variables; "
|
|
283
|
+
f"missing={sorted(map(str, missing))}, extra={sorted(map(str, extra))}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
generator_order = source_variables + symbols + parameters
|
|
287
|
+
defining_relations = list(constraints.relations)
|
|
288
|
+
defining_relations.extend(
|
|
289
|
+
sp.expand(symbol - observable)
|
|
290
|
+
for symbol, observable in zip(symbols, observables)
|
|
291
|
+
)
|
|
292
|
+
try:
|
|
293
|
+
elimination_basis = sp.groebner(
|
|
294
|
+
defining_relations,
|
|
295
|
+
*generator_order,
|
|
296
|
+
order="lex",
|
|
297
|
+
)
|
|
298
|
+
except sp.PolynomialError as exc:
|
|
299
|
+
raise ValueError("observable elimination requires polynomial relations") from exc
|
|
300
|
+
|
|
301
|
+
substitutions = dict(zip(symbols, observables))
|
|
302
|
+
relation_certificates: list[ObservableRelation] = []
|
|
303
|
+
for polynomial in elimination_basis.polys:
|
|
304
|
+
relation = sp.expand(polynomial.as_expr())
|
|
305
|
+
if relation.free_symbols & source_set:
|
|
306
|
+
continue
|
|
307
|
+
if not relation.free_symbols & symbol_set:
|
|
308
|
+
continue
|
|
309
|
+
relation = sp.factor(relation)
|
|
310
|
+
pullback = sp.expand(relation.subs(substitutions))
|
|
311
|
+
relation_certificates.append(
|
|
312
|
+
ObservableRelation(
|
|
313
|
+
relation=relation,
|
|
314
|
+
pullback_remainder=sp.factor(constraints.reduce(pullback)),
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return ObservableQuotient(
|
|
319
|
+
symbols=symbols,
|
|
320
|
+
observables=observables,
|
|
321
|
+
parameters=parameters,
|
|
322
|
+
eliminated_variables=source_variables,
|
|
323
|
+
relations=tuple(relation_certificates),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def discover_first_order_process_quotient(
|
|
328
|
+
system: ProcessSystem,
|
|
329
|
+
observable: sp.Expr,
|
|
330
|
+
*,
|
|
331
|
+
observable_symbol: sp.Symbol,
|
|
332
|
+
derivative_symbol: sp.Symbol,
|
|
333
|
+
constraints: AlgebraicConstraintSet,
|
|
334
|
+
parameters: Sequence[sp.Symbol] = (),
|
|
335
|
+
) -> ObservableQuotient:
|
|
336
|
+
return discover_observable_relations(
|
|
337
|
+
(sp.sympify(observable), system.derive(observable)),
|
|
338
|
+
(observable_symbol, derivative_symbol),
|
|
339
|
+
constraints=constraints,
|
|
340
|
+
source_variables=system.assignments,
|
|
341
|
+
parameters=parameters,
|
|
342
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Costed search over first-order algebraic observer quotients.
|
|
2
|
+
|
|
3
|
+
This module connects the discovery front-end to Shakespeare's multi-axis
|
|
4
|
+
presentation search. The task here is deliberately narrow: among a
|
|
5
|
+
caller-declared family of observer candidates, find those whose first process
|
|
6
|
+
jet ``(F, D F)`` closes by certified algebraic relations on a declared leaf,
|
|
7
|
+
and compare the resulting presentations without imposing a universal scalar
|
|
8
|
+
objective.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import math
|
|
15
|
+
from typing import Callable, Sequence
|
|
16
|
+
|
|
17
|
+
import sympy as sp
|
|
18
|
+
|
|
19
|
+
from ..presentation.constraints import AlgebraicConstraintSet
|
|
20
|
+
from ..presentation.search import (
|
|
21
|
+
PresentationCandidate,
|
|
22
|
+
PresentationCost,
|
|
23
|
+
PresentationSearchResult,
|
|
24
|
+
pareto_frontier,
|
|
25
|
+
)
|
|
26
|
+
from ..process.local import ProcessSystem
|
|
27
|
+
from .polynomial import ObservableQuotient, discover_first_order_process_quotient
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class FirstOrderObservablePresentation:
|
|
32
|
+
"""One observer together with its discovered first-order algebraic quotient."""
|
|
33
|
+
|
|
34
|
+
observable: sp.Expr
|
|
35
|
+
derivative: sp.Expr
|
|
36
|
+
quotient: ObservableQuotient
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def algebraically_closed(self) -> bool:
|
|
40
|
+
return bool(self.quotient.relations) and self.quotient.complete_certificates
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _expression_complexity(expr: sp.Expr) -> float:
|
|
44
|
+
expr = sp.expand(sp.sympify(expr))
|
|
45
|
+
return float(int(sp.count_ops(expr, visual=False)) + 1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _relation_complexity(
|
|
49
|
+
relation: sp.Expr,
|
|
50
|
+
variables: Sequence[sp.Symbol],
|
|
51
|
+
) -> float:
|
|
52
|
+
"""Transparent degree-plus-support proxy for one algebraic relation."""
|
|
53
|
+
try:
|
|
54
|
+
polynomial = sp.Poly(sp.expand(relation), *variables, domain="EX")
|
|
55
|
+
except sp.PolynomialError as exc:
|
|
56
|
+
raise ValueError("quotient relation must be polynomial") from exc
|
|
57
|
+
return float(int(polynomial.total_degree()) + len(polynomial.terms()))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def structural_first_order_quotient_cost(
|
|
61
|
+
presentation: FirstOrderObservablePresentation,
|
|
62
|
+
) -> PresentationCost:
|
|
63
|
+
"""Default multi-axis cost for a first-order algebraic observer presentation."""
|
|
64
|
+
relation_variables = (
|
|
65
|
+
presentation.quotient.symbols + presentation.quotient.parameters
|
|
66
|
+
)
|
|
67
|
+
relation_cost = sum(
|
|
68
|
+
_relation_complexity(item.relation, relation_variables)
|
|
69
|
+
for item in presentation.quotient.relations
|
|
70
|
+
)
|
|
71
|
+
return PresentationCost(
|
|
72
|
+
grammar=(
|
|
73
|
+
_expression_complexity(presentation.observable)
|
|
74
|
+
+ _expression_complexity(presentation.derivative)
|
|
75
|
+
),
|
|
76
|
+
relations=float(relation_cost),
|
|
77
|
+
history=1.0,
|
|
78
|
+
decoder=0.0,
|
|
79
|
+
task_error=0.0 if presentation.algebraically_closed else math.inf,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _fresh_symbol(base: str, forbidden: set[sp.Symbol]) -> sp.Symbol:
|
|
84
|
+
candidate = sp.Symbol(base)
|
|
85
|
+
suffix = 0
|
|
86
|
+
while candidate in forbidden:
|
|
87
|
+
suffix += 1
|
|
88
|
+
candidate = sp.Symbol(f"{base}_{suffix}")
|
|
89
|
+
forbidden.add(candidate)
|
|
90
|
+
return candidate
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def search_first_order_process_quotients(
|
|
94
|
+
system: ProcessSystem,
|
|
95
|
+
observer_candidates: Sequence[sp.Expr],
|
|
96
|
+
*,
|
|
97
|
+
constraints: AlgebraicConstraintSet,
|
|
98
|
+
parameters: Sequence[sp.Symbol] = (),
|
|
99
|
+
cost_model: Callable[
|
|
100
|
+
[FirstOrderObservablePresentation], PresentationCost
|
|
101
|
+
] | None = None,
|
|
102
|
+
) -> PresentationSearchResult[FirstOrderObservablePresentation]:
|
|
103
|
+
"""Compare candidate observers by certified first-order algebraic closure."""
|
|
104
|
+
observer_candidates = tuple(
|
|
105
|
+
sp.expand(sp.sympify(candidate)) for candidate in observer_candidates
|
|
106
|
+
)
|
|
107
|
+
if not observer_candidates:
|
|
108
|
+
raise ValueError("at least one observer candidate is required")
|
|
109
|
+
|
|
110
|
+
score = cost_model or structural_first_order_quotient_cost
|
|
111
|
+
forbidden = set(constraints.variables)
|
|
112
|
+
evaluated: list[PresentationCandidate[FirstOrderObservablePresentation]] = []
|
|
113
|
+
|
|
114
|
+
for index, observable in enumerate(observer_candidates):
|
|
115
|
+
observable_symbol = _fresh_symbol(f"U{index}", forbidden)
|
|
116
|
+
derivative_symbol = _fresh_symbol(f"Y{index}", forbidden)
|
|
117
|
+
quotient = discover_first_order_process_quotient(
|
|
118
|
+
system,
|
|
119
|
+
observable,
|
|
120
|
+
observable_symbol=observable_symbol,
|
|
121
|
+
derivative_symbol=derivative_symbol,
|
|
122
|
+
constraints=constraints,
|
|
123
|
+
parameters=parameters,
|
|
124
|
+
)
|
|
125
|
+
payload = FirstOrderObservablePresentation(
|
|
126
|
+
observable=observable,
|
|
127
|
+
derivative=system.derive(observable),
|
|
128
|
+
quotient=quotient,
|
|
129
|
+
)
|
|
130
|
+
evaluated.append(
|
|
131
|
+
PresentationCandidate(
|
|
132
|
+
payload=payload,
|
|
133
|
+
cost=score(payload),
|
|
134
|
+
sufficient=payload.algebraically_closed,
|
|
135
|
+
label=str(observable),
|
|
136
|
+
certificate=tuple(
|
|
137
|
+
relation.relation for relation in quotient.relations
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
evaluated_tuple = tuple(evaluated)
|
|
143
|
+
return PresentationSearchResult(
|
|
144
|
+
evaluated=evaluated_tuple,
|
|
145
|
+
pareto=pareto_frontier(evaluated_tuple),
|
|
146
|
+
)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Minimal structured-observer proposals for process discovery.
|
|
2
|
+
|
|
3
|
+
This module is deliberately narrower than a general typed mathematical IR.
|
|
4
|
+
The first calibration only needs one fact that bare scalar assignment symbols
|
|
5
|
+
cannot express: several primitive objects may belong to the same pairable sort,
|
|
6
|
+
and a caller-supplied pairing may turn two such objects into a scalar observer.
|
|
7
|
+
|
|
8
|
+
The abstraction stops there. It does not assume vector-space addition, scalar
|
|
9
|
+
multiplication, bases, matrices, Fourier analysis, or a universal theory
|
|
10
|
+
protocol. Those structures should enter only when later mathematical
|
|
11
|
+
vignettes force them.
|
|
12
|
+
|
|
13
|
+
A proposal retains two layers at once:
|
|
14
|
+
|
|
15
|
+
* ``construction`` records the structured recipe, e.g. ``pair(q,e)``;
|
|
16
|
+
* ``expression`` is the scalar backend lowering consumed by the existing
|
|
17
|
+
polynomial discovery layer.
|
|
18
|
+
|
|
19
|
+
Thus a coordinate expression such as ``qy`` need not become the ontology of
|
|
20
|
+
an observer whose actual construction is ``pair(q,e)``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from itertools import combinations_with_replacement, product
|
|
27
|
+
from typing import Callable, Sequence
|
|
28
|
+
|
|
29
|
+
import sympy as sp
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class PairableAtom:
|
|
34
|
+
"""One named structured atom with a backend component realization.
|
|
35
|
+
|
|
36
|
+
``sort`` is intentionally only an equality tag. The current layer knows no
|
|
37
|
+
axioms associated with that tag; it merely prevents a pairing from being
|
|
38
|
+
applied across incompatible primitive kinds.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
components: tuple[sp.Expr, ...]
|
|
43
|
+
sort: str = "pairable"
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
if not self.name:
|
|
47
|
+
raise ValueError("structured atom name must be non-empty")
|
|
48
|
+
if not self.sort:
|
|
49
|
+
raise ValueError("structured atom sort must be non-empty")
|
|
50
|
+
components = tuple(sp.expand(sp.sympify(item)) for item in self.components)
|
|
51
|
+
if not components:
|
|
52
|
+
raise ValueError("pairable atom requires at least one component")
|
|
53
|
+
object.__setattr__(self, "components", components)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class PairingSpec:
|
|
58
|
+
"""A caller-declared scalar pairing on one structured sort.
|
|
59
|
+
|
|
60
|
+
``lower`` receives the two component tuples and returns the exact scalar
|
|
61
|
+
backend expression. Symmetry affects construction enumeration only; no
|
|
62
|
+
bilinearity, positivity, or other vector-space law is inferred.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
sort: str
|
|
67
|
+
lower: Callable[[tuple[sp.Expr, ...], tuple[sp.Expr, ...]], sp.Expr]
|
|
68
|
+
symmetric: bool = True
|
|
69
|
+
cost: float = 1.0
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
if not self.name:
|
|
73
|
+
raise ValueError("pairing name must be non-empty")
|
|
74
|
+
if not self.sort:
|
|
75
|
+
raise ValueError("pairing sort must be non-empty")
|
|
76
|
+
if self.cost < 0:
|
|
77
|
+
raise ValueError("pairing cost must be non-negative")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class PairingConstruction:
|
|
82
|
+
"""Construction certificate for one scalar observer proposal."""
|
|
83
|
+
|
|
84
|
+
pairing_name: str
|
|
85
|
+
left_name: str
|
|
86
|
+
right_name: str
|
|
87
|
+
cost: float
|
|
88
|
+
|
|
89
|
+
def recipe(self) -> str:
|
|
90
|
+
return f"{self.pairing_name}({self.left_name},{self.right_name})"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class StructuredObserverProposal:
|
|
95
|
+
"""A scalar backend expression together with its structured origin."""
|
|
96
|
+
|
|
97
|
+
expression: sp.Expr
|
|
98
|
+
construction: PairingConstruction
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def cost(self) -> float:
|
|
102
|
+
return self.construction.cost
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class StructuredObserverProposalResult:
|
|
107
|
+
proposals: tuple[StructuredObserverProposal, ...]
|
|
108
|
+
rejected: tuple[str, ...] = ()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def euclidean_pairing(
|
|
112
|
+
*,
|
|
113
|
+
name: str = "pair",
|
|
114
|
+
sort: str = "euclidean",
|
|
115
|
+
cost: float = 1.0,
|
|
116
|
+
) -> PairingSpec:
|
|
117
|
+
"""Return the finite-coordinate Euclidean pairing used by calibrations.
|
|
118
|
+
|
|
119
|
+
This convenience constructor is a backend realization, not a declaration
|
|
120
|
+
that every pairable sort is Euclidean.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def lower(left: tuple[sp.Expr, ...], right: tuple[sp.Expr, ...]) -> sp.Expr:
|
|
124
|
+
if len(left) != len(right):
|
|
125
|
+
raise ValueError("pairing requires equal component dimensions")
|
|
126
|
+
return sp.expand(sum(a * b for a, b in zip(left, right)))
|
|
127
|
+
|
|
128
|
+
return PairingSpec(name=name, sort=sort, lower=lower, symmetric=True, cost=cost)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def generate_pairing_observers(
|
|
132
|
+
atoms: Sequence[PairableAtom],
|
|
133
|
+
pairing: PairingSpec,
|
|
134
|
+
) -> StructuredObserverProposalResult:
|
|
135
|
+
"""Generate all bounded depth-one scalar observers from one pairing.
|
|
136
|
+
|
|
137
|
+
The caller supplies the primitive atoms and the pairing itself. Shakespeare
|
|
138
|
+
supplies no vector-space laws beyond the explicitly declared symmetry used
|
|
139
|
+
to avoid duplicate recipes.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
atoms = tuple(atoms)
|
|
143
|
+
if not atoms:
|
|
144
|
+
raise ValueError("at least one structured atom is required")
|
|
145
|
+
|
|
146
|
+
eligible = tuple(atom for atom in atoms if atom.sort == pairing.sort)
|
|
147
|
+
rejected = tuple(
|
|
148
|
+
f"{atom.name}: sort {atom.sort!r} is incompatible with pairing sort {pairing.sort!r}"
|
|
149
|
+
for atom in atoms
|
|
150
|
+
if atom.sort != pairing.sort
|
|
151
|
+
)
|
|
152
|
+
if not eligible:
|
|
153
|
+
return StructuredObserverProposalResult((), rejected)
|
|
154
|
+
|
|
155
|
+
pairs = (
|
|
156
|
+
combinations_with_replacement(eligible, 2)
|
|
157
|
+
if pairing.symmetric
|
|
158
|
+
else product(eligible, repeat=2)
|
|
159
|
+
)
|
|
160
|
+
proposals: list[StructuredObserverProposal] = []
|
|
161
|
+
for left, right in pairs:
|
|
162
|
+
try:
|
|
163
|
+
expression = sp.expand(
|
|
164
|
+
sp.sympify(pairing.lower(left.components, right.components))
|
|
165
|
+
)
|
|
166
|
+
except Exception as exc: # caller pairing boundary
|
|
167
|
+
rejected += (f"{pairing.name}({left.name},{right.name}): {exc}",)
|
|
168
|
+
continue
|
|
169
|
+
proposals.append(
|
|
170
|
+
StructuredObserverProposal(
|
|
171
|
+
expression=expression,
|
|
172
|
+
construction=PairingConstruction(
|
|
173
|
+
pairing_name=pairing.name,
|
|
174
|
+
left_name=left.name,
|
|
175
|
+
right_name=right.name,
|
|
176
|
+
cost=float(pairing.cost),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return StructuredObserverProposalResult(tuple(proposals), rejected)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def nonstationary_observer_proposals(
|
|
185
|
+
system,
|
|
186
|
+
proposals: Sequence[StructuredObserverProposal],
|
|
187
|
+
*,
|
|
188
|
+
constraints,
|
|
189
|
+
) -> tuple[StructuredObserverProposal, ...]:
|
|
190
|
+
"""Keep proposals whose first process derivative survives the quotient.
|
|
191
|
+
|
|
192
|
+
This is a task filter, not an equality rule: stationary constructions remain
|
|
193
|
+
valid constructions, but they are insufficient for the present task of
|
|
194
|
+
finding a one-dimensional evolving observer.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
return tuple(
|
|
198
|
+
proposal
|
|
199
|
+
for proposal in proposals
|
|
200
|
+
if not constraints.contains(system.derive(proposal.expression))
|
|
201
|
+
)
|