process-geometry 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. aeg_shakespeare/__init__.py +52 -0
  2. aeg_shakespeare/_legacy_api.py +299 -0
  3. aeg_shakespeare/analysis/__init__.py +12 -0
  4. aeg_shakespeare/analysis/abelian.py +69 -0
  5. aeg_shakespeare/analysis/algebraic.py +11 -0
  6. aeg_shakespeare/analysis/am.py +19 -0
  7. aeg_shakespeare/analysis/connection.py +74 -0
  8. aeg_shakespeare/analysis/decomposition.py +37 -0
  9. aeg_shakespeare/analysis/module.py +5 -0
  10. aeg_shakespeare/central.py +20 -0
  11. aeg_shakespeare/constraints.py +121 -0
  12. aeg_shakespeare/construction.py +303 -0
  13. aeg_shakespeare/core.py +59 -0
  14. aeg_shakespeare/cost.py +51 -0
  15. aeg_shakespeare/discovery/__init__.py +53 -0
  16. aeg_shakespeare/discovery/coefficient_extension.py +75 -0
  17. aeg_shakespeare/discovery/polynomial.py +342 -0
  18. aeg_shakespeare/discovery/selection.py +146 -0
  19. aeg_shakespeare/discovery/structured.py +201 -0
  20. aeg_shakespeare/families.py +31 -0
  21. aeg_shakespeare/frame.py +5 -0
  22. aeg_shakespeare/function_theory/__init__.py +100 -0
  23. aeg_shakespeare/function_theory/abel_jacobi.py +246 -0
  24. aeg_shakespeare/function_theory/abelian.py +129 -0
  25. aeg_shakespeare/function_theory/algebraic.py +105 -0
  26. aeg_shakespeare/function_theory/am.py +266 -0
  27. aeg_shakespeare/function_theory/intersection.py +321 -0
  28. aeg_shakespeare/function_theory/module.py +118 -0
  29. aeg_shakespeare/function_theory/period_matrix.py +155 -0
  30. aeg_shakespeare/function_theory/periods.py +216 -0
  31. aeg_shakespeare/function_theory/real_branch_cycles.py +286 -0
  32. aeg_shakespeare/function_theory/weierstrass.py +136 -0
  33. aeg_shakespeare/grammar.py +225 -0
  34. aeg_shakespeare/history_geometry.py +276 -0
  35. aeg_shakespeare/linear.py +70 -0
  36. aeg_shakespeare/presentation/__init__.py +23 -0
  37. aeg_shakespeare/presentation/budget.py +27 -0
  38. aeg_shakespeare/presentation/canonicalization.py +143 -0
  39. aeg_shakespeare/presentation/constraints.py +5 -0
  40. aeg_shakespeare/presentation/construction.py +19 -0
  41. aeg_shakespeare/presentation/grammar.py +15 -0
  42. aeg_shakespeare/presentation/history.py +45 -0
  43. aeg_shakespeare/presentation/morphism.py +66 -0
  44. aeg_shakespeare/presentation/relations.py +31 -0
  45. aeg_shakespeare/presentation/search.py +31 -0
  46. aeg_shakespeare/process/__init__.py +16 -0
  47. aeg_shakespeare/process/finite/__init__.py +43 -0
  48. aeg_shakespeare/process/finite/cocycle.py +166 -0
  49. aeg_shakespeare/process/finite/families.py +318 -0
  50. aeg_shakespeare/process/history.py +53 -0
  51. aeg_shakespeare/process/local/__init__.py +7 -0
  52. aeg_shakespeare/process/local/direction.py +88 -0
  53. aeg_shakespeare/process/local/frame.py +73 -0
  54. aeg_shakespeare/process/local/system.py +43 -0
  55. aeg_shakespeare/relations.py +374 -0
  56. aeg_shakespeare/rewrite.py +157 -0
  57. aeg_shakespeare/search.py +286 -0
  58. aeg_shakespeare/signature.py +155 -0
  59. process_geometry-0.0.3.dist-info/METADATA +305 -0
  60. process_geometry-0.0.3.dist-info/RECORD +63 -0
  61. process_geometry-0.0.3.dist-info/WHEEL +5 -0
  62. process_geometry-0.0.3.dist-info/licenses/LICENSE +24 -0
  63. process_geometry-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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,7 @@
1
+ """Local/infinitesimal process realizations."""
2
+
3
+ from .direction import ProcessDirection
4
+ from .frame import ProcessFrame
5
+ from .system import ProcessSystem
6
+
7
+ __all__ = ["ProcessSystem", "ProcessFrame", "ProcessDirection"]
@@ -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