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,318 @@
|
|
|
1
|
+
"""Finite parameterized process families and their scalar responses.
|
|
2
|
+
|
|
3
|
+
This module is deliberately smaller than a group-, representation-, or harmonic-
|
|
4
|
+
analysis framework. It records only the structure forced by the Translation,
|
|
5
|
+
Dilation, and Addition/Multiplication calibration sequence:
|
|
6
|
+
|
|
7
|
+
* a named finite process family whose parameters have an explicit composition
|
|
8
|
+
law;
|
|
9
|
+
* scalar multiplicative responses to one family;
|
|
10
|
+
* an action of one family on the parameter space of another; and
|
|
11
|
+
* transport/compatibility checks for those scalar responses.
|
|
12
|
+
|
|
13
|
+
Topology, measure, inverses, Lie theory, spectra, Fourier transforms, and
|
|
14
|
+
operator-valued representations are intentionally absent. They should enter
|
|
15
|
+
only when later mathematical vignettes require them.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Callable, Generic, Sequence, TypeVar
|
|
22
|
+
|
|
23
|
+
import sympy as sp
|
|
24
|
+
|
|
25
|
+
ParamT = TypeVar("ParamT")
|
|
26
|
+
ActingParamT = TypeVar("ActingParamT")
|
|
27
|
+
TargetParamT = TypeVar("TargetParamT")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _default_parameter_equivalence(left, right) -> bool:
|
|
31
|
+
"""Small exact/symbolic equality helper for family parameters.
|
|
32
|
+
|
|
33
|
+
Tuples are compared componentwise. Scalar symbolic values are compared by
|
|
34
|
+
simplifying their difference when possible. Callers with richer parameter
|
|
35
|
+
objects may supply their own equivalence predicate to ``ProcessFamily``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if isinstance(left, tuple) and isinstance(right, tuple):
|
|
39
|
+
return len(left) == len(right) and all(
|
|
40
|
+
_default_parameter_equivalence(a, b) for a, b in zip(left, right)
|
|
41
|
+
)
|
|
42
|
+
try:
|
|
43
|
+
return sp.simplify(sp.sympify(left) - sp.sympify(right)) == 0
|
|
44
|
+
except (TypeError, ValueError, sp.SympifyError):
|
|
45
|
+
return left == right
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class FamilyStep(Generic[ParamT]):
|
|
50
|
+
"""One finite step from a named parameterized process family."""
|
|
51
|
+
|
|
52
|
+
family_name: str
|
|
53
|
+
parameter: ParamT
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ProcessFamily(Generic[ParamT]):
|
|
58
|
+
"""A finite process family with an explicit parameter-composition law.
|
|
59
|
+
|
|
60
|
+
``ProcessFamily`` intentionally does not assert that the parameters form a
|
|
61
|
+
group. A family may later turn out to have identities, inverses, topology,
|
|
62
|
+
or measure, but none of those structures are required here.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
combine: Callable[[ParamT, ParamT], ParamT] = field(repr=False, compare=False)
|
|
67
|
+
identity: ParamT | None = None
|
|
68
|
+
equivalent: Callable[[ParamT, ParamT], bool] | None = field(
|
|
69
|
+
default=None,
|
|
70
|
+
repr=False,
|
|
71
|
+
compare=False,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
if not self.name:
|
|
76
|
+
raise ValueError("process-family name must be non-empty")
|
|
77
|
+
|
|
78
|
+
def step(self, parameter: ParamT) -> FamilyStep[ParamT]:
|
|
79
|
+
return FamilyStep(self.name, parameter)
|
|
80
|
+
|
|
81
|
+
def compose_parameters(self, left: ParamT, right: ParamT) -> ParamT:
|
|
82
|
+
"""Parameter of the composed finite process ``F_left F_right``."""
|
|
83
|
+
|
|
84
|
+
return self.combine(left, right)
|
|
85
|
+
|
|
86
|
+
def parameters_equivalent(self, left: ParamT, right: ParamT) -> bool:
|
|
87
|
+
predicate = self.equivalent or _default_parameter_equivalence
|
|
88
|
+
return bool(predicate(left, right))
|
|
89
|
+
|
|
90
|
+
def fold_parameters(self, parameters: Sequence[ParamT]) -> ParamT:
|
|
91
|
+
"""Reduce an ordered same-family history to one family parameter."""
|
|
92
|
+
|
|
93
|
+
parameters = tuple(parameters)
|
|
94
|
+
if not parameters:
|
|
95
|
+
if self.identity is None:
|
|
96
|
+
raise ValueError("empty family history requires a declared identity")
|
|
97
|
+
return self.identity
|
|
98
|
+
value = parameters[0]
|
|
99
|
+
for parameter in parameters[1:]:
|
|
100
|
+
value = self.compose_parameters(value, parameter)
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class ProcessCharacter(Generic[ParamT]):
|
|
106
|
+
"""A SymPy-valued scalar response candidate for one finite process family.
|
|
107
|
+
|
|
108
|
+
The defining mathematical test is multiplicativity with respect to the
|
|
109
|
+
family's own composition law. Analytic regularity and completeness are not
|
|
110
|
+
part of this object.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
family: ProcessFamily[ParamT]
|
|
114
|
+
response: Callable[[ParamT], sp.Expr] = field(repr=False, compare=False)
|
|
115
|
+
label: object | None = None
|
|
116
|
+
simplify: Callable[[sp.Expr], sp.Expr] = field(
|
|
117
|
+
default=sp.simplify,
|
|
118
|
+
repr=False,
|
|
119
|
+
compare=False,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def value(self, parameter: ParamT) -> sp.Expr:
|
|
123
|
+
return sp.sympify(self.response(parameter))
|
|
124
|
+
|
|
125
|
+
def multiplicativity_residual(self, left: ParamT, right: ParamT) -> sp.Expr:
|
|
126
|
+
combined = self.family.compose_parameters(left, right)
|
|
127
|
+
residual = self.value(combined) - self.value(left) * self.value(right)
|
|
128
|
+
return sp.sympify(self.simplify(residual))
|
|
129
|
+
|
|
130
|
+
def normalization_residual(self) -> sp.Expr:
|
|
131
|
+
if self.family.identity is None:
|
|
132
|
+
raise ValueError("character normalization requires a declared family identity")
|
|
133
|
+
return sp.sympify(self.simplify(self.value(self.family.identity) - 1))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class CharacterVerification:
|
|
138
|
+
"""Exact residual certificate for a bounded character check."""
|
|
139
|
+
|
|
140
|
+
multiplicativity_residuals: tuple[sp.Expr, ...]
|
|
141
|
+
normalization_residual: sp.Expr | None = None
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def exact(self) -> bool:
|
|
145
|
+
return all(residual == 0 for residual in self.multiplicativity_residuals) and (
|
|
146
|
+
self.normalization_residual in (None, 0)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def verify_process_character(
|
|
151
|
+
character: ProcessCharacter[ParamT],
|
|
152
|
+
parameter_pairs: Sequence[tuple[ParamT, ParamT]],
|
|
153
|
+
*,
|
|
154
|
+
check_identity: bool = True,
|
|
155
|
+
) -> CharacterVerification:
|
|
156
|
+
"""Verify multiplicativity on caller-supplied parameter pairs.
|
|
157
|
+
|
|
158
|
+
This is a bounded exact certificate, not a proof over an unbounded parameter
|
|
159
|
+
domain unless the supplied symbolic pairs themselves establish such an
|
|
160
|
+
identity through the backend simplifier.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
residuals = tuple(
|
|
164
|
+
character.multiplicativity_residual(left, right)
|
|
165
|
+
for left, right in parameter_pairs
|
|
166
|
+
)
|
|
167
|
+
normalization = None
|
|
168
|
+
if check_identity and character.family.identity is not None:
|
|
169
|
+
normalization = character.normalization_residual()
|
|
170
|
+
return CharacterVerification(residuals, normalization)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class FamilyAction(Generic[ActingParamT, TargetParamT]):
|
|
175
|
+
"""An explicit action of one process family on another family's parameters.
|
|
176
|
+
|
|
177
|
+
For a left action ``alpha`` the intended law is
|
|
178
|
+
|
|
179
|
+
``alpha(g*h, b) = alpha(g, alpha(h, b))``
|
|
180
|
+
|
|
181
|
+
together with preservation of the target-family composition law. The
|
|
182
|
+
action itself is primary; no semidirect-product or group object is created.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
acting: ProcessFamily[ActingParamT]
|
|
186
|
+
target: ProcessFamily[TargetParamT]
|
|
187
|
+
apply: Callable[[ActingParamT, TargetParamT], TargetParamT] = field(
|
|
188
|
+
repr=False,
|
|
189
|
+
compare=False,
|
|
190
|
+
)
|
|
191
|
+
name: str = "action"
|
|
192
|
+
|
|
193
|
+
def __post_init__(self) -> None:
|
|
194
|
+
if not self.name:
|
|
195
|
+
raise ValueError("family-action name must be non-empty")
|
|
196
|
+
|
|
197
|
+
def transport_parameter(
|
|
198
|
+
self,
|
|
199
|
+
acting_parameter: ActingParamT,
|
|
200
|
+
target_parameter: TargetParamT,
|
|
201
|
+
) -> TargetParamT:
|
|
202
|
+
return self.apply(acting_parameter, target_parameter)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class FamilyActionVerification:
|
|
207
|
+
"""Bounded law certificate for a ``FamilyAction``."""
|
|
208
|
+
|
|
209
|
+
target_homomorphism_checks: tuple[bool, ...]
|
|
210
|
+
acting_composition_checks: tuple[bool, ...]
|
|
211
|
+
identity_checks: tuple[bool, ...] = ()
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def exact(self) -> bool:
|
|
215
|
+
return all(
|
|
216
|
+
(*self.target_homomorphism_checks, *self.acting_composition_checks, *self.identity_checks)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def verify_family_action(
|
|
221
|
+
action: FamilyAction[ActingParamT, TargetParamT],
|
|
222
|
+
*,
|
|
223
|
+
acting_parameters: Sequence[ActingParamT] = (),
|
|
224
|
+
acting_pairs: Sequence[tuple[ActingParamT, ActingParamT]] = (),
|
|
225
|
+
target_parameters: Sequence[TargetParamT] = (),
|
|
226
|
+
target_pairs: Sequence[tuple[TargetParamT, TargetParamT]] = (),
|
|
227
|
+
check_identities: bool = True,
|
|
228
|
+
) -> FamilyActionVerification:
|
|
229
|
+
"""Verify the two elementary finite-action laws on supplied samples."""
|
|
230
|
+
|
|
231
|
+
target_checks: list[bool] = []
|
|
232
|
+
for acting_parameter in acting_parameters:
|
|
233
|
+
for left, right in target_pairs:
|
|
234
|
+
lhs = action.transport_parameter(
|
|
235
|
+
acting_parameter,
|
|
236
|
+
action.target.compose_parameters(left, right),
|
|
237
|
+
)
|
|
238
|
+
rhs = action.target.compose_parameters(
|
|
239
|
+
action.transport_parameter(acting_parameter, left),
|
|
240
|
+
action.transport_parameter(acting_parameter, right),
|
|
241
|
+
)
|
|
242
|
+
target_checks.append(action.target.parameters_equivalent(lhs, rhs))
|
|
243
|
+
|
|
244
|
+
acting_checks: list[bool] = []
|
|
245
|
+
for left, right in acting_pairs:
|
|
246
|
+
combined = action.acting.compose_parameters(left, right)
|
|
247
|
+
for target_parameter in target_parameters:
|
|
248
|
+
lhs = action.transport_parameter(combined, target_parameter)
|
|
249
|
+
rhs = action.transport_parameter(
|
|
250
|
+
left,
|
|
251
|
+
action.transport_parameter(right, target_parameter),
|
|
252
|
+
)
|
|
253
|
+
acting_checks.append(action.target.parameters_equivalent(lhs, rhs))
|
|
254
|
+
|
|
255
|
+
identity_checks: list[bool] = []
|
|
256
|
+
if check_identities:
|
|
257
|
+
if action.acting.identity is not None:
|
|
258
|
+
for target_parameter in target_parameters:
|
|
259
|
+
image = action.transport_parameter(action.acting.identity, target_parameter)
|
|
260
|
+
identity_checks.append(
|
|
261
|
+
action.target.parameters_equivalent(image, target_parameter)
|
|
262
|
+
)
|
|
263
|
+
if action.target.identity is not None:
|
|
264
|
+
for acting_parameter in acting_parameters:
|
|
265
|
+
image = action.transport_parameter(
|
|
266
|
+
acting_parameter,
|
|
267
|
+
action.target.identity,
|
|
268
|
+
)
|
|
269
|
+
identity_checks.append(
|
|
270
|
+
action.target.parameters_equivalent(image, action.target.identity)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return FamilyActionVerification(
|
|
274
|
+
target_homomorphism_checks=tuple(target_checks),
|
|
275
|
+
acting_composition_checks=tuple(acting_checks),
|
|
276
|
+
identity_checks=tuple(identity_checks),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def transport_process_character(
|
|
281
|
+
character: ProcessCharacter[TargetParamT],
|
|
282
|
+
action: FamilyAction[ActingParamT, TargetParamT],
|
|
283
|
+
acting_parameter: ActingParamT,
|
|
284
|
+
*,
|
|
285
|
+
label: object | None = None,
|
|
286
|
+
) -> ProcessCharacter[TargetParamT]:
|
|
287
|
+
"""Pull a target-family character along one finite family action."""
|
|
288
|
+
|
|
289
|
+
if action.target.name != character.family.name:
|
|
290
|
+
raise ValueError("family action target must match character family")
|
|
291
|
+
transported_label = label
|
|
292
|
+
if transported_label is None:
|
|
293
|
+
transported_label = (action.name, acting_parameter, character.label)
|
|
294
|
+
return ProcessCharacter(
|
|
295
|
+
family=character.family,
|
|
296
|
+
response=lambda parameter: character.value(
|
|
297
|
+
action.transport_parameter(acting_parameter, parameter)
|
|
298
|
+
),
|
|
299
|
+
label=transported_label,
|
|
300
|
+
simplify=character.simplify,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def character_invariance_residual(
|
|
305
|
+
character: ProcessCharacter[TargetParamT],
|
|
306
|
+
action: FamilyAction[ActingParamT, TargetParamT],
|
|
307
|
+
acting_parameter: ActingParamT,
|
|
308
|
+
target_parameter: TargetParamT,
|
|
309
|
+
) -> sp.Expr:
|
|
310
|
+
"""Residual for whether a scalar character survives a family action unchanged.
|
|
311
|
+
|
|
312
|
+
Nonzero residual is an obstruction to treating the target character as an
|
|
313
|
+
action-invariant scalar response of the combined finite process structure.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
transported = transport_process_character(character, action, acting_parameter)
|
|
317
|
+
residual = transported.value(target_parameter) - character.value(target_parameter)
|
|
318
|
+
return sp.sympify(character.simplify(residual))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Literal ordered process histories and caller-supplied semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Generic, Iterator, TypeVar
|
|
7
|
+
|
|
8
|
+
StepT = TypeVar("StepT")
|
|
9
|
+
StateT = TypeVar("StateT")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ProcessWord(Generic[StepT]):
|
|
14
|
+
"""An ordered finite process history.
|
|
15
|
+
|
|
16
|
+
``ProcessWord`` deliberately stores steps without interpreting them. The
|
|
17
|
+
same history can therefore be given different semantics by different
|
|
18
|
+
presentations or observers.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
steps: tuple[StepT, ...] = ()
|
|
22
|
+
|
|
23
|
+
def __iter__(self) -> Iterator[StepT]:
|
|
24
|
+
return iter(self.steps)
|
|
25
|
+
|
|
26
|
+
def __len__(self) -> int:
|
|
27
|
+
return len(self.steps)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def depth(self) -> int:
|
|
31
|
+
return len(self.steps)
|
|
32
|
+
|
|
33
|
+
def then(self, step: StepT) -> "ProcessWord[StepT]":
|
|
34
|
+
return ProcessWord(self.steps + (step,))
|
|
35
|
+
|
|
36
|
+
def compose(self, other: "ProcessWord[StepT]") -> "ProcessWord[StepT]":
|
|
37
|
+
return ProcessWord(self.steps + other.steps)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def interpret_history(
|
|
41
|
+
history: ProcessWord[StepT],
|
|
42
|
+
initial: StateT,
|
|
43
|
+
transition,
|
|
44
|
+
) -> StateT:
|
|
45
|
+
"""Interpret an ordered history under a caller-supplied transition rule."""
|
|
46
|
+
|
|
47
|
+
state = initial
|
|
48
|
+
for step in history:
|
|
49
|
+
state = transition(state, step)
|
|
50
|
+
return state
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["ProcessWord", "interpret_history"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Experimental local process directions over a declared ``ProcessFrame``.
|
|
2
|
+
|
|
3
|
+
This module is the first vertical slice for the canonical-observer programme.
|
|
4
|
+
A ``ProcessDirection`` is deliberately smaller than a path, flow, solver, or
|
|
5
|
+
connection: it is only the local combination
|
|
6
|
+
|
|
7
|
+
D = sum_i u_i X_i
|
|
8
|
+
|
|
9
|
+
of already-declared process generators. Ordinary assignment ODEs are obtained
|
|
10
|
+
only after projecting this process direction to the assignment symbols.
|
|
11
|
+
|
|
12
|
+
The object is intentionally not reparameterization-invariant. Two proportional
|
|
13
|
+
directions can have different analytic and reconstruction costs (Sundman-type
|
|
14
|
+
regularization is the motivating red team), so quotienting them is a task-level
|
|
15
|
+
decision rather than process ontology.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Mapping
|
|
22
|
+
|
|
23
|
+
import sympy as sp
|
|
24
|
+
|
|
25
|
+
from .frame import ProcessFrame
|
|
26
|
+
from .system import ProcessSystem
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ProcessDirection:
|
|
31
|
+
"""One local linear combination of named process generators.
|
|
32
|
+
|
|
33
|
+
Missing frame generators have coefficient zero. Coefficients may depend on
|
|
34
|
+
assignments, external parameters, or other symbolic local data; the class
|
|
35
|
+
does not interpret where those coefficients came from.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
frame: ProcessFrame
|
|
39
|
+
coefficients: Mapping[str, sp.Expr]
|
|
40
|
+
label: str = ""
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
unknown = set(self.coefficients) - set(self.frame.names)
|
|
44
|
+
if unknown:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"direction coefficients reference unknown generators: "
|
|
47
|
+
f"{sorted(unknown)}"
|
|
48
|
+
)
|
|
49
|
+
normalized = {
|
|
50
|
+
name: sp.expand(sp.sympify(value))
|
|
51
|
+
for name, value in self.coefficients.items()
|
|
52
|
+
}
|
|
53
|
+
object.__setattr__(self, "coefficients", normalized)
|
|
54
|
+
|
|
55
|
+
def coefficient(self, generator: str) -> sp.Expr:
|
|
56
|
+
if generator not in self.frame.generators:
|
|
57
|
+
raise KeyError(f"unknown process generator: {generator!r}")
|
|
58
|
+
return sp.sympify(self.coefficients.get(generator, sp.S.Zero))
|
|
59
|
+
|
|
60
|
+
def apply(self, expr: sp.Expr) -> sp.Expr:
|
|
61
|
+
"""Apply the combined process direction to one symbolic expression."""
|
|
62
|
+
|
|
63
|
+
out = sp.S.Zero
|
|
64
|
+
for generator in self.frame.names:
|
|
65
|
+
coefficient = self.coefficient(generator)
|
|
66
|
+
if coefficient != 0:
|
|
67
|
+
out += coefficient * self.frame.apply(generator, expr)
|
|
68
|
+
return sp.expand(out)
|
|
69
|
+
|
|
70
|
+
def assignment_rules(self) -> Mapping[sp.Symbol, sp.Expr]:
|
|
71
|
+
"""Return the assignment shadow of this process direction."""
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
assignment: self.apply(assignment)
|
|
75
|
+
for assignment in self.frame.assignments
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def as_system(self, *, name: str = "D") -> ProcessSystem:
|
|
79
|
+
"""Lower the direction to the existing one-generator symbolic backend."""
|
|
80
|
+
|
|
81
|
+
return ProcessSystem(
|
|
82
|
+
assignments=self.frame.assignments,
|
|
83
|
+
rules=self.assignment_rules(),
|
|
84
|
+
name=name,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["ProcessDirection"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Generic symbolic frames with multiple ordered process generators.
|
|
2
|
+
|
|
3
|
+
A ``ProcessFrame`` is a representation backend for several named derivations on
|
|
4
|
+
one assignment algebra. It connects literal ``ProcessWord`` history to symbolic
|
|
5
|
+
process calculus without assuming commutativity or a universal Lie algebra.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Mapping
|
|
12
|
+
|
|
13
|
+
import sympy as sp
|
|
14
|
+
|
|
15
|
+
from ..history import ProcessWord
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ProcessFrame:
|
|
20
|
+
"""A finite family of named derivations on a symbolic assignment algebra."""
|
|
21
|
+
|
|
22
|
+
assignments: tuple[sp.Symbol, ...]
|
|
23
|
+
generators: Mapping[str, Mapping[sp.Symbol, sp.Expr]]
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if not self.assignments:
|
|
27
|
+
raise ValueError("process frame requires at least one assignment")
|
|
28
|
+
if not self.generators:
|
|
29
|
+
raise ValueError("process frame requires at least one generator")
|
|
30
|
+
for name, rules in self.generators.items():
|
|
31
|
+
if not name:
|
|
32
|
+
raise ValueError("generator names must be non-empty")
|
|
33
|
+
missing = [symbol for symbol in self.assignments if symbol not in rules]
|
|
34
|
+
if missing:
|
|
35
|
+
raise ValueError(f"generator {name!r} is missing rules for {missing}")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def names(self) -> tuple[str, ...]:
|
|
39
|
+
return tuple(self.generators)
|
|
40
|
+
|
|
41
|
+
def apply(self, generator: str, expr: sp.Expr) -> sp.Expr:
|
|
42
|
+
"""Apply one named derivation using the Leibniz/chain rule."""
|
|
43
|
+
if generator not in self.generators:
|
|
44
|
+
raise KeyError(f"unknown process generator: {generator!r}")
|
|
45
|
+
expr = sp.sympify(expr)
|
|
46
|
+
rules = self.generators[generator]
|
|
47
|
+
out = sp.S.Zero
|
|
48
|
+
for assignment in self.assignments:
|
|
49
|
+
out += sp.diff(expr, assignment) * rules[assignment]
|
|
50
|
+
return sp.expand(out)
|
|
51
|
+
|
|
52
|
+
def apply_word(self, word: ProcessWord[str], expr: sp.Expr) -> sp.Expr:
|
|
53
|
+
"""Apply generators in literal process-history order."""
|
|
54
|
+
value = sp.sympify(expr)
|
|
55
|
+
for generator in word:
|
|
56
|
+
value = self.apply(generator, value)
|
|
57
|
+
return sp.expand(value)
|
|
58
|
+
|
|
59
|
+
def commutator(self, left: str, right: str, expr: sp.Expr) -> sp.Expr:
|
|
60
|
+
"""Return ``[left,right] expr = left(right(expr))-right(left(expr))``."""
|
|
61
|
+
return sp.expand(
|
|
62
|
+
self.apply(left, self.apply(right, expr))
|
|
63
|
+
- self.apply(right, self.apply(left, expr))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def iterate(self, generator: str, expr: sp.Expr, count: int) -> sp.Expr:
|
|
67
|
+
"""Apply one generator repeatedly ``count`` times."""
|
|
68
|
+
if count < 0:
|
|
69
|
+
raise ValueError("count must be non-negative")
|
|
70
|
+
value = sp.sympify(expr)
|
|
71
|
+
for _ in range(count):
|
|
72
|
+
value = self.apply(generator, value)
|
|
73
|
+
return sp.expand(value)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""One-generator local process realizations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
import sympy as sp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ProcessSystem:
|
|
13
|
+
"""A named derivation-style representation of a local process generator.
|
|
14
|
+
|
|
15
|
+
``derive`` is one symbolic realization of process action. Shakespeare does
|
|
16
|
+
not equate this backend with the full process-history ontology.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
assignments: tuple[sp.Symbol, ...]
|
|
20
|
+
rules: Mapping[sp.Symbol, sp.Expr]
|
|
21
|
+
name: str = "D"
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
missing = [a for a in self.assignments if a not in self.rules]
|
|
25
|
+
if missing:
|
|
26
|
+
raise ValueError(f"missing process rules for assignments: {missing}")
|
|
27
|
+
|
|
28
|
+
def derive(self, expr: sp.Expr) -> sp.Expr:
|
|
29
|
+
"""Apply the represented process generator using the Leibniz chain rule."""
|
|
30
|
+
expr = sp.sympify(expr)
|
|
31
|
+
out = sp.S.Zero
|
|
32
|
+
for assignment in self.assignments:
|
|
33
|
+
out += sp.diff(expr, assignment) * self.rules[assignment]
|
|
34
|
+
return sp.expand(out)
|
|
35
|
+
|
|
36
|
+
def iterate(self, expr: sp.Expr, depth: int) -> list[sp.Expr]:
|
|
37
|
+
"""Return ``[expr, D(expr), ..., D**depth(expr)]``."""
|
|
38
|
+
if depth < 0:
|
|
39
|
+
raise ValueError("depth must be non-negative")
|
|
40
|
+
orbit = [sp.expand(sp.sympify(expr))]
|
|
41
|
+
for _ in range(depth):
|
|
42
|
+
orbit.append(self.derive(orbit[-1]))
|
|
43
|
+
return orbit
|