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,374 @@
|
|
|
1
|
+
"""Generic relation discovery for bounded process presentations.
|
|
2
|
+
|
|
3
|
+
Linear algebra is used only as a computational backend for discovering exact
|
|
4
|
+
relations among process-generated expressions. The public results are process
|
|
5
|
+
relations, relation kernels, and decompositions of finite process grammars; no
|
|
6
|
+
spectral interpretation is required by the API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from functools import reduce
|
|
13
|
+
from math import gcd
|
|
14
|
+
from typing import Sequence
|
|
15
|
+
|
|
16
|
+
import sympy as sp
|
|
17
|
+
|
|
18
|
+
from .process.local import ProcessSystem
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ProcessPolynomialRelation:
|
|
23
|
+
"""A constant-coefficient process relation ``sum c_i D^i = 0``.
|
|
24
|
+
|
|
25
|
+
Coefficients are stored in ascending process order: ``(c_0, ..., c_n)``.
|
|
26
|
+
The relation is an algebraic certificate; callers may interpret it further,
|
|
27
|
+
but Shakespeare does not attach eigenvalue/frequency semantics to it.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
coefficients: tuple[sp.Expr, ...]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def order(self) -> int:
|
|
34
|
+
return len(self.coefficients) - 1
|
|
35
|
+
|
|
36
|
+
def as_expr(self, symbol: sp.Symbol | None = None) -> sp.Expr:
|
|
37
|
+
z = symbol or sp.Symbol("D")
|
|
38
|
+
return sp.expand(sum(c * z**i for i, c in enumerate(self.coefficients)))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ReturnRelation:
|
|
43
|
+
"""A process polynomial relation attached to one expression."""
|
|
44
|
+
|
|
45
|
+
expression: sp.Expr
|
|
46
|
+
coefficients: tuple[sp.Expr, ...]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def order(self) -> int:
|
|
50
|
+
return len(self.coefficients) - 1
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def relation(self) -> ProcessPolynomialRelation:
|
|
54
|
+
return ProcessPolynomialRelation(self.coefficients)
|
|
55
|
+
|
|
56
|
+
def as_expr(self, symbol: sp.Symbol | None = None) -> sp.Expr:
|
|
57
|
+
return self.relation.as_expr(symbol)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class RelationKernel:
|
|
62
|
+
"""Primitive expressions satisfying one discovered or declared relation."""
|
|
63
|
+
|
|
64
|
+
coefficients: tuple[sp.Expr, ...]
|
|
65
|
+
primitives: tuple[sp.Expr, ...]
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def order(self) -> int:
|
|
69
|
+
return len(self.coefficients) - 1
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def relation(self) -> ProcessPolynomialRelation:
|
|
73
|
+
return ProcessPolynomialRelation(self.coefficients)
|
|
74
|
+
|
|
75
|
+
def as_expr(self, symbol: sp.Symbol | None = None) -> sp.Expr:
|
|
76
|
+
return self.relation.as_expr(symbol)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class RelationDecomposition:
|
|
81
|
+
"""Template-free relation decomposition of a finite closed grammar."""
|
|
82
|
+
|
|
83
|
+
global_relation: ProcessPolynomialRelation
|
|
84
|
+
components: tuple[RelationKernel, ...]
|
|
85
|
+
complete: bool
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def primitives(self) -> tuple[sp.Expr, ...]:
|
|
89
|
+
return tuple(
|
|
90
|
+
primitive
|
|
91
|
+
for component in self.components
|
|
92
|
+
for primitive in component.primitives
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _normalize_coordinate_vector(v: Sequence[sp.Expr] | sp.Matrix) -> sp.Matrix:
|
|
97
|
+
"""Normalize a coordinate vector without changing its length."""
|
|
98
|
+
values = [sp.simplify(sp.sympify(value)) for value in list(v)]
|
|
99
|
+
if not values or all(value == 0 for value in values):
|
|
100
|
+
return sp.Matrix(values)
|
|
101
|
+
|
|
102
|
+
if all(value.is_Rational is True for value in values):
|
|
103
|
+
rationals = [sp.Rational(value) for value in values]
|
|
104
|
+
denominators = [q.q for q in rationals]
|
|
105
|
+
lcm = sp.ilcm(*denominators) if denominators else 1
|
|
106
|
+
ints = [int(q * lcm) for q in rationals]
|
|
107
|
+
nonzero = [abs(value) for value in ints if value]
|
|
108
|
+
common = reduce(gcd, nonzero) if nonzero else 1
|
|
109
|
+
ints = [value // common for value in ints]
|
|
110
|
+
first = next((value for value in ints if value), 1)
|
|
111
|
+
if first < 0:
|
|
112
|
+
ints = [-value for value in ints]
|
|
113
|
+
return sp.Matrix([sp.Integer(value) for value in ints])
|
|
114
|
+
|
|
115
|
+
pivot = next(value for value in values if value != 0)
|
|
116
|
+
return sp.Matrix([sp.simplify(value / pivot) for value in values])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _normalize_relation_coefficients(
|
|
120
|
+
coefficients: Sequence[sp.Expr] | sp.Matrix,
|
|
121
|
+
) -> tuple[sp.Expr, ...]:
|
|
122
|
+
"""Normalize a relation up to nonzero scalar multiplication."""
|
|
123
|
+
values = [sp.simplify(sp.sympify(value)) for value in list(coefficients)]
|
|
124
|
+
while len(values) > 1 and values[-1] == 0:
|
|
125
|
+
values.pop()
|
|
126
|
+
if not values or all(value == 0 for value in values):
|
|
127
|
+
raise ValueError("the zero relation is not informative")
|
|
128
|
+
|
|
129
|
+
if all(value.is_Rational is True for value in values):
|
|
130
|
+
return tuple(_normalize_coordinate_vector(values))
|
|
131
|
+
|
|
132
|
+
leading = values[-1]
|
|
133
|
+
return tuple(sp.simplify(value / leading) for value in values)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _ambient_monomials(
|
|
137
|
+
expressions: Sequence[sp.Expr],
|
|
138
|
+
variables: Sequence[sp.Symbol],
|
|
139
|
+
) -> list[tuple[int, ...]]:
|
|
140
|
+
monomials: set[tuple[int, ...]] = set()
|
|
141
|
+
for expression in expressions:
|
|
142
|
+
poly = sp.Poly(sp.expand(expression), *variables)
|
|
143
|
+
monomials.update(poly.monoms())
|
|
144
|
+
return sorted(monomials, reverse=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _ambient_column(
|
|
148
|
+
expr: sp.Expr,
|
|
149
|
+
monomials: Sequence[tuple[int, ...]],
|
|
150
|
+
variables: Sequence[sp.Symbol],
|
|
151
|
+
) -> sp.Matrix:
|
|
152
|
+
poly = sp.Poly(sp.expand(expr), *variables)
|
|
153
|
+
return sp.Matrix([poly.coeff_monomial(monomial) for monomial in monomials])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def coefficient_vector(
|
|
157
|
+
expr: sp.Expr,
|
|
158
|
+
basis: Sequence[sp.Expr],
|
|
159
|
+
variables: Sequence[sp.Symbol],
|
|
160
|
+
) -> sp.Matrix:
|
|
161
|
+
"""Return exact coordinates of ``expr`` in an arbitrary polynomial basis."""
|
|
162
|
+
basis = tuple(sp.expand(sp.sympify(item)) for item in basis)
|
|
163
|
+
if not basis:
|
|
164
|
+
raise ValueError("basis must be non-empty")
|
|
165
|
+
|
|
166
|
+
monomials = _ambient_monomials((*basis, sp.expand(sp.sympify(expr))), variables)
|
|
167
|
+
matrix = sp.Matrix.hstack(
|
|
168
|
+
*[_ambient_column(item, monomials, variables) for item in basis]
|
|
169
|
+
)
|
|
170
|
+
if matrix.rank() < len(basis):
|
|
171
|
+
raise ValueError("basis expressions are linearly dependent")
|
|
172
|
+
|
|
173
|
+
target = _ambient_column(expr, monomials, variables)
|
|
174
|
+
solution = sp.linsolve((matrix, target))
|
|
175
|
+
if solution is sp.EmptySet or not solution:
|
|
176
|
+
raise ValueError("expression is not in the supplied basis span")
|
|
177
|
+
row = next(iter(solution))
|
|
178
|
+
return sp.Matrix([sp.simplify(value) for value in row])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def action_matrix(system: ProcessSystem, basis: Sequence[sp.Expr]) -> sp.Matrix:
|
|
182
|
+
"""Matrix backend for process action on a declared finite grammar."""
|
|
183
|
+
basis = tuple(basis)
|
|
184
|
+
columns: list[sp.Matrix] = []
|
|
185
|
+
for item in basis:
|
|
186
|
+
derived = sp.expand(system.derive(item))
|
|
187
|
+
try:
|
|
188
|
+
vector = coefficient_vector(derived, basis, system.assignments)
|
|
189
|
+
except ValueError as exc:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"declared basis is not closed under the process action"
|
|
192
|
+
) from exc
|
|
193
|
+
columns.append(vector)
|
|
194
|
+
return sp.Matrix.hstack(*columns)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _apply_relation(
|
|
198
|
+
system: ProcessSystem,
|
|
199
|
+
expr: sp.Expr,
|
|
200
|
+
coefficients: Sequence[sp.Expr],
|
|
201
|
+
) -> sp.Expr:
|
|
202
|
+
orbit = system.iterate(expr, len(coefficients) - 1)
|
|
203
|
+
return sp.expand(sum(c * orbit[i] for i, c in enumerate(coefficients)))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def discover_return_relation(
|
|
207
|
+
system: ProcessSystem,
|
|
208
|
+
expr: sp.Expr,
|
|
209
|
+
max_order: int = 8,
|
|
210
|
+
) -> ReturnRelation | None:
|
|
211
|
+
"""Find the first exact constant-coefficient relation among ``D^i(expr)``."""
|
|
212
|
+
orbit = system.iterate(expr, max_order)
|
|
213
|
+
monomials: set[tuple[int, ...]] = set()
|
|
214
|
+
polys: list[sp.Poly] = []
|
|
215
|
+
for item in orbit:
|
|
216
|
+
poly = sp.Poly(item, *system.assignments)
|
|
217
|
+
polys.append(poly)
|
|
218
|
+
monomials.update(poly.monoms())
|
|
219
|
+
monomial_list = sorted(monomials, reverse=True)
|
|
220
|
+
|
|
221
|
+
for order in range(1, max_order + 1):
|
|
222
|
+
columns = [
|
|
223
|
+
sp.Matrix([polys[i].coeff_monomial(m) for m in monomial_list])
|
|
224
|
+
for i in range(order + 1)
|
|
225
|
+
]
|
|
226
|
+
matrix = sp.Matrix.hstack(*columns)
|
|
227
|
+
candidates = [
|
|
228
|
+
vector
|
|
229
|
+
for vector in matrix.nullspace()
|
|
230
|
+
if sp.simplify(vector[-1]) != 0
|
|
231
|
+
]
|
|
232
|
+
if not candidates:
|
|
233
|
+
continue
|
|
234
|
+
coefficients = _normalize_relation_coefficients(candidates[0])
|
|
235
|
+
if sp.simplify(_apply_relation(system, expr, coefficients)) != 0:
|
|
236
|
+
raise AssertionError("internal relation verification failed")
|
|
237
|
+
return ReturnRelation(sp.expand(expr), coefficients)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def discover_operator_relation(
|
|
242
|
+
operator: sp.Matrix,
|
|
243
|
+
max_order: int | None = None,
|
|
244
|
+
) -> ProcessPolynomialRelation | None:
|
|
245
|
+
"""Discover the shortest polynomial relation satisfied by a finite action."""
|
|
246
|
+
operator = sp.Matrix(operator)
|
|
247
|
+
if operator.rows != operator.cols:
|
|
248
|
+
raise ValueError("operator must be square")
|
|
249
|
+
if operator.rows == 0:
|
|
250
|
+
raise ValueError("operator must be non-empty")
|
|
251
|
+
|
|
252
|
+
size = operator.rows
|
|
253
|
+
bound = size if max_order is None else min(max_order, size)
|
|
254
|
+
if bound < 1:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
powers = [sp.eye(size)]
|
|
258
|
+
columns = [sp.Matrix(list(powers[0]))]
|
|
259
|
+
for _order in range(1, bound + 1):
|
|
260
|
+
powers.append(sp.expand(powers[-1] * operator))
|
|
261
|
+
columns.append(sp.Matrix(list(powers[-1])))
|
|
262
|
+
matrix = sp.Matrix.hstack(*columns)
|
|
263
|
+
candidates = [
|
|
264
|
+
vector
|
|
265
|
+
for vector in matrix.nullspace()
|
|
266
|
+
if sp.simplify(vector[-1]) != 0
|
|
267
|
+
]
|
|
268
|
+
if not candidates:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
coefficients = _normalize_relation_coefficients(candidates[0])
|
|
272
|
+
certificate = sp.zeros(size)
|
|
273
|
+
for index, coefficient in enumerate(coefficients):
|
|
274
|
+
certificate += coefficient * powers[index]
|
|
275
|
+
certificate = certificate.applyfunc(sp.simplify)
|
|
276
|
+
if certificate != sp.zeros(size):
|
|
277
|
+
raise AssertionError("operator-relation verification failed")
|
|
278
|
+
return ProcessPolynomialRelation(coefficients)
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def factor_process_relation(
|
|
283
|
+
relation: ProcessPolynomialRelation,
|
|
284
|
+
) -> tuple[ProcessPolynomialRelation, ...]:
|
|
285
|
+
"""Factor a process relation into pairwise-coprime primary factors."""
|
|
286
|
+
symbol = sp.Symbol("_D")
|
|
287
|
+
expression = relation.as_expr(symbol)
|
|
288
|
+
_unit, factors = sp.factor_list(expression, symbol)
|
|
289
|
+
if not factors:
|
|
290
|
+
return (relation,)
|
|
291
|
+
|
|
292
|
+
result: list[ProcessPolynomialRelation] = []
|
|
293
|
+
for factor, multiplicity in factors:
|
|
294
|
+
primary = sp.Poly(sp.expand(factor**multiplicity), symbol)
|
|
295
|
+
coefficients = tuple(reversed(primary.all_coeffs()))
|
|
296
|
+
result.append(
|
|
297
|
+
ProcessPolynomialRelation(_normalize_relation_coefficients(coefficients))
|
|
298
|
+
)
|
|
299
|
+
return tuple(result)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def discover_relation_kernel(
|
|
303
|
+
system: ProcessSystem,
|
|
304
|
+
basis: Sequence[sp.Expr],
|
|
305
|
+
coefficients: Sequence[sp.Expr],
|
|
306
|
+
) -> RelationKernel:
|
|
307
|
+
"""Discover all primitives in ``span(basis)`` satisfying a process relation."""
|
|
308
|
+
coefficients = _normalize_relation_coefficients(coefficients)
|
|
309
|
+
action = action_matrix(system, basis)
|
|
310
|
+
operator = sp.zeros(len(basis))
|
|
311
|
+
power = sp.eye(len(basis))
|
|
312
|
+
for coefficient in coefficients:
|
|
313
|
+
operator += coefficient * power
|
|
314
|
+
power = power * action
|
|
315
|
+
|
|
316
|
+
primitives: list[sp.Expr] = []
|
|
317
|
+
for raw in operator.nullspace():
|
|
318
|
+
vector = _normalize_coordinate_vector(raw)
|
|
319
|
+
expr = sp.expand(sum(vector[i] * basis[i] for i in range(len(basis))))
|
|
320
|
+
if sp.simplify(_apply_relation(system, expr, coefficients)) != 0:
|
|
321
|
+
raise AssertionError("relation-kernel verification failed")
|
|
322
|
+
if expr not in primitives:
|
|
323
|
+
primitives.append(expr)
|
|
324
|
+
return RelationKernel(coefficients=coefficients, primitives=tuple(primitives))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def discover_relation_decomposition(
|
|
328
|
+
system: ProcessSystem,
|
|
329
|
+
basis: Sequence[sp.Expr],
|
|
330
|
+
max_order: int | None = None,
|
|
331
|
+
) -> RelationDecomposition | None:
|
|
332
|
+
"""Discover relation factors and their primitive subgrammars jointly."""
|
|
333
|
+
basis = tuple(basis)
|
|
334
|
+
if not basis:
|
|
335
|
+
raise ValueError("basis must be non-empty")
|
|
336
|
+
|
|
337
|
+
action = action_matrix(system, basis)
|
|
338
|
+
global_relation = discover_operator_relation(action, max_order=max_order)
|
|
339
|
+
if global_relation is None:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
components = tuple(
|
|
343
|
+
discover_relation_kernel(system, basis, factor.coefficients)
|
|
344
|
+
for factor in factor_process_relation(global_relation)
|
|
345
|
+
)
|
|
346
|
+
primitives = tuple(
|
|
347
|
+
primitive
|
|
348
|
+
for component in components
|
|
349
|
+
for primitive in component.primitives
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
complete = False
|
|
353
|
+
if primitives:
|
|
354
|
+
coordinate_columns = [
|
|
355
|
+
coefficient_vector(primitive, basis, system.assignments)
|
|
356
|
+
for primitive in primitives
|
|
357
|
+
]
|
|
358
|
+
complete = sp.Matrix.hstack(*coordinate_columns).rank() == len(basis)
|
|
359
|
+
|
|
360
|
+
return RelationDecomposition(
|
|
361
|
+
global_relation=global_relation,
|
|
362
|
+
components=components,
|
|
363
|
+
complete=complete,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def decompose(
|
|
368
|
+
target: sp.Expr,
|
|
369
|
+
primitives: Sequence[sp.Expr],
|
|
370
|
+
variables: Sequence[sp.Symbol],
|
|
371
|
+
) -> tuple[sp.Expr, ...]:
|
|
372
|
+
"""Express ``target`` uniquely in a supplied primitive grammar."""
|
|
373
|
+
vector = coefficient_vector(target, primitives, variables)
|
|
374
|
+
return tuple(sp.simplify(value) for value in vector)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Exact rewriting for ordered process histories.
|
|
2
|
+
|
|
3
|
+
This module is deliberately independent of SymPy and derivation-style process
|
|
4
|
+
systems. It provides a first executable layer for noncommutative finite process
|
|
5
|
+
presentations: literal histories are preserved, oriented process relations are
|
|
6
|
+
applied explicitly, and every normalization carries a rewrite trace.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Generic, Literal, Sequence, TypeVar
|
|
13
|
+
|
|
14
|
+
from .process.history import ProcessWord
|
|
15
|
+
|
|
16
|
+
StepT = TypeVar("StepT")
|
|
17
|
+
RewriteReason = Literal["normal_form", "cycle", "max_steps"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class WordRewriteRule(Generic[StepT]):
|
|
22
|
+
"""An oriented exact relation between two finite process words.
|
|
23
|
+
|
|
24
|
+
The rule rewrites a contiguous occurrence of ``lhs`` to ``rhs``. An empty
|
|
25
|
+
left-hand side is forbidden because it would permit insertion at every
|
|
26
|
+
history position and destroy the bounded-normalization semantics.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
lhs: ProcessWord[StepT]
|
|
30
|
+
rhs: ProcessWord[StepT]
|
|
31
|
+
name: str = ""
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
if not self.lhs.steps:
|
|
35
|
+
raise ValueError("rewrite-rule lhs must be non-empty")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class RewriteStep(Generic[StepT]):
|
|
40
|
+
"""One certified history rewrite."""
|
|
41
|
+
|
|
42
|
+
rule: WordRewriteRule[StepT]
|
|
43
|
+
position: int
|
|
44
|
+
before: ProcessWord[StepT]
|
|
45
|
+
after: ProcessWord[StepT]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class RewriteResult(Generic[StepT]):
|
|
50
|
+
"""Result and certificate of a bounded normalization attempt."""
|
|
51
|
+
|
|
52
|
+
original: ProcessWord[StepT]
|
|
53
|
+
normal_form: ProcessWord[StepT]
|
|
54
|
+
trace: tuple[RewriteStep[StepT], ...]
|
|
55
|
+
terminated: bool
|
|
56
|
+
reason: RewriteReason
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def rewrite_steps(self) -> int:
|
|
60
|
+
return len(self.trace)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def depth_delta(self) -> int:
|
|
64
|
+
"""Final history depth minus original history depth."""
|
|
65
|
+
|
|
66
|
+
return self.normal_form.depth - self.original.depth
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rewrite_once(
|
|
70
|
+
history: ProcessWord[StepT],
|
|
71
|
+
rules: Sequence[WordRewriteRule[StepT]],
|
|
72
|
+
) -> RewriteStep[StepT] | None:
|
|
73
|
+
"""Apply one deterministic leftmost rewrite.
|
|
74
|
+
|
|
75
|
+
Positions are searched from left to right. If several rules match at the
|
|
76
|
+
same position, their order in ``rules`` is the tie breaker.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
steps = history.steps
|
|
80
|
+
for position in range(len(steps)):
|
|
81
|
+
for rule in rules:
|
|
82
|
+
width = len(rule.lhs.steps)
|
|
83
|
+
if steps[position : position + width] != rule.lhs.steps:
|
|
84
|
+
continue
|
|
85
|
+
after = ProcessWord(
|
|
86
|
+
steps[:position] + rule.rhs.steps + steps[position + width :]
|
|
87
|
+
)
|
|
88
|
+
return RewriteStep(
|
|
89
|
+
rule=rule,
|
|
90
|
+
position=position,
|
|
91
|
+
before=history,
|
|
92
|
+
after=after,
|
|
93
|
+
)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def normalize_word(
|
|
98
|
+
history: ProcessWord[StepT],
|
|
99
|
+
rules: Sequence[WordRewriteRule[StepT]],
|
|
100
|
+
*,
|
|
101
|
+
max_steps: int = 1_000,
|
|
102
|
+
detect_cycles: bool = True,
|
|
103
|
+
) -> RewriteResult[StepT]:
|
|
104
|
+
"""Normalize a process word under explicit oriented relations.
|
|
105
|
+
|
|
106
|
+
The function never assumes that the supplied rewrite system terminates or
|
|
107
|
+
is confluent. It returns ``terminated=False`` with an explicit reason when a
|
|
108
|
+
cycle or step bound is encountered. Literal input history and the complete
|
|
109
|
+
sequence of process-relation applications remain available in ``trace``.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
if max_steps < 0:
|
|
113
|
+
raise ValueError("max_steps must be non-negative")
|
|
114
|
+
|
|
115
|
+
current = history
|
|
116
|
+
trace: list[RewriteStep[StepT]] = []
|
|
117
|
+
seen: list[ProcessWord[StepT]] = [history]
|
|
118
|
+
|
|
119
|
+
for _ in range(max_steps):
|
|
120
|
+
step = rewrite_once(current, rules)
|
|
121
|
+
if step is None:
|
|
122
|
+
return RewriteResult(
|
|
123
|
+
original=history,
|
|
124
|
+
normal_form=current,
|
|
125
|
+
trace=tuple(trace),
|
|
126
|
+
terminated=True,
|
|
127
|
+
reason="normal_form",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
current = step.after
|
|
131
|
+
trace.append(step)
|
|
132
|
+
|
|
133
|
+
if detect_cycles and any(current == previous for previous in seen):
|
|
134
|
+
return RewriteResult(
|
|
135
|
+
original=history,
|
|
136
|
+
normal_form=current,
|
|
137
|
+
trace=tuple(trace),
|
|
138
|
+
terminated=False,
|
|
139
|
+
reason="cycle",
|
|
140
|
+
)
|
|
141
|
+
seen.append(current)
|
|
142
|
+
|
|
143
|
+
if rewrite_once(current, rules) is None:
|
|
144
|
+
return RewriteResult(
|
|
145
|
+
original=history,
|
|
146
|
+
normal_form=current,
|
|
147
|
+
trace=tuple(trace),
|
|
148
|
+
terminated=True,
|
|
149
|
+
reason="normal_form",
|
|
150
|
+
)
|
|
151
|
+
return RewriteResult(
|
|
152
|
+
original=history,
|
|
153
|
+
normal_form=current,
|
|
154
|
+
trace=tuple(trace),
|
|
155
|
+
terminated=False,
|
|
156
|
+
reason="max_steps",
|
|
157
|
+
)
|