ruleflow 0.1.0__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.
- core/__init__.py +0 -0
- core/engine.py +527 -0
- core/graph.py +33 -0
- core/numlib.py +191 -0
- core/prettier.py +101 -0
- core/signals.py +52 -0
- core/vec.py +349 -0
- implementations/__init__.py +5 -0
- implementations/sss.py +50 -0
- integrations/__init__.py +3 -0
- integrations/enumerator.py +130 -0
- integrations/icat/__init__.py +4 -0
- integrations/icat/core.py +9 -0
- lang/__init__.py +1 -0
- lang/implementation.py +310 -0
- lang/interpreter.py +264 -0
- lang/parser.py +369 -0
- lang/testing.py +46 -0
- ruleflow-0.1.0.dist-info/METADATA +17 -0
- ruleflow-0.1.0.dist-info/RECORD +34 -0
- ruleflow-0.1.0.dist-info/WHEEL +5 -0
- ruleflow-0.1.0.dist-info/licenses/LICENSE +121 -0
- ruleflow-0.1.0.dist-info/top_level.txt +6 -0
- studio/__init__.py +0 -0
- studio/config.py +46 -0
- studio/model.py +137 -0
- studio/stdplgns/analysis.py +306 -0
- studio/stdplgns/explore.py +559 -0
- studio/stdplgns/run.py +207 -0
- studio/view.py +496 -0
- studio-plugins/gephi/gephi-live.py +0 -0
- studio-plugins/llm/_llm_module.py +56 -0
- studio-plugins/sessie/enum.py +0 -0
- studio-plugins/tunes.py +393 -0
core/__init__.py
ADDED
|
File without changes
|
core/engine.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
from typing import Any, Sequence, MutableSequence, NamedTuple, Iterator, cast, Self
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from copy import copy
|
|
5
|
+
from core.signals import Signal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ==== engine ====
|
|
9
|
+
@dataclass(slots=True) # we use slots to get C-like mutable struct behavior (NamedTuple is similar but immutable)
|
|
10
|
+
class Cell:
|
|
11
|
+
"""A single (technically) mutable unit within a universe/string (a.k.a. Quanta). However, it is usually treated as immutable using copy() and hash().
|
|
12
|
+
A cell is analogous to a discrete spacial-unit and quanta is the matter that fills up that unit of space.
|
|
13
|
+
It is at this smallest unit of space that we care about causality.
|
|
14
|
+
|
|
15
|
+
Policies:
|
|
16
|
+
- The Cell class should not contain any fields other than the quanta and the metadata. This is so copies can be made easily.
|
|
17
|
+
|
|
18
|
+
Future Considerations:
|
|
19
|
+
- Add additional metadata/tags fields.
|
|
20
|
+
"""
|
|
21
|
+
quanta: Any
|
|
22
|
+
|
|
23
|
+
# NOTE: the metadata is the ONLY thing that makes cells differentiable (other than quanta of course)
|
|
24
|
+
# Metadata regarding the creation and destruction of the cell... stored as indices to the events array.
|
|
25
|
+
created_at: int = 0 # this is the ONLY piece important metadata needed for a causality graph (can only be created one, so one event index)
|
|
26
|
+
destroyed_at: tuple[int, ...] = () # OPTIONAL metadata useful for analysis. Is an array of event indices (multiple indices for multiway systems)
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
"""String representation of quanta"""
|
|
30
|
+
return str(self.quanta)
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return repr(self.quanta)
|
|
34
|
+
|
|
35
|
+
def __eq__(self, other: Cell):
|
|
36
|
+
"""Semantic equality (use is for true equality)"""
|
|
37
|
+
return self.quanta == other.quanta
|
|
38
|
+
|
|
39
|
+
# noinspection PyDunderSlots,PyUnresolvedReferences
|
|
40
|
+
def __copy__(self) -> Cell:
|
|
41
|
+
n: Cell = object.__new__(self.__class__)
|
|
42
|
+
n.quanta = self.quanta
|
|
43
|
+
n.created_at = self.created_at
|
|
44
|
+
n.destroyed_at = self.destroyed_at
|
|
45
|
+
return n
|
|
46
|
+
|
|
47
|
+
def __deepcopy__(self, memo) -> Cell:
|
|
48
|
+
return self.__copy__() # force normal copy() behavior
|
|
49
|
+
|
|
50
|
+
def __hash__(self): # implemented to make Cell hashable (so can be used as keys in dict for instance)
|
|
51
|
+
return hash(self.quanta)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SpaceState(ABC):
|
|
55
|
+
"""Mutable container made up of `Cells` (a.k.a. Universe State of Space).
|
|
56
|
+
|
|
57
|
+
Policies:
|
|
58
|
+
- Should NOT be used as a simple container for Cells (in a replacement rule for instance), it should only be used for actual space states in events/time. Any other container should be in the form Sequence[Cell].
|
|
59
|
+
- All modifier methods must make sure to create new cells or cell copies if causality is to be tracked properly using the DeltaSets.
|
|
60
|
+
- All modifier methods (that create/destroy cells) should return DeltaCellSet containing the destroyed and created cells.
|
|
61
|
+
- All official SpaceStates must be created in this engine.py file. If one wants to create a 4D SpaceState, for instance, they must inherit from this, implement the methods, etc.
|
|
62
|
+
- All SpaceStates that inherit from this class must implement the modifier methods. If `find`, `len`, etc. are not sufficient helpers, additional helpers may be created here (if they are general enough), or in the subclasses ideally.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def __str__(self):
|
|
67
|
+
"""String representation of SpaceState"""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def __repr__(self):
|
|
71
|
+
"""Repr String representation of SpaceState"""
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def __eq__(self, other: SpaceState) -> bool:
|
|
75
|
+
"""Semantic equality (use `is` for true equality)"""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def __len__(self) -> int | Any:
|
|
79
|
+
"""Should return the *size* of a container... whatever that may mean for N^1 or N^2 or N^3 spaces."""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def __bool__(self) -> bool:
|
|
83
|
+
"""Should return the bool state of the space (has any contents)."""
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def __hash__(self):
|
|
87
|
+
"""Should make the SpaceState hashable so that it can be stored in a hash table."""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def __copy__(self) -> SpaceState | Any:
|
|
91
|
+
"""Copies the SpaceState (self), but does not copy the cells (internal fields) themselves
|
|
92
|
+
(it only retains references to them). It is a shallow copy.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def __getitem__(self, item: int | slice) -> Cell | Sequence[Cell] | Any:
|
|
97
|
+
"""Enables getting subspaces with slicing: space[0][1] of an N^2 space for instance."""
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def get_all_cells(self) -> Sequence[Cell] | Iterator[Cell]:
|
|
101
|
+
"""Returns all the cells that live in the SpaceState... regardless of the spaces dimensions.
|
|
102
|
+
This is useful for modifying all the cells in the SpaceState."""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def find(self, subspace: Cell | Sequence[Cell] | Any) -> Iterator[int | Any]:
|
|
106
|
+
"""Find the `instances` number of occurrences of subspaces in the space (in any order desired) and return a
|
|
107
|
+
sequence of index positions or more complex positions. An empty set is returned if no matches are found.
|
|
108
|
+
If `instances` is -1, all subspaces should be matched.
|
|
109
|
+
Note that `instances` are useful for creating multi-way systems for example."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SpaceState1D(SpaceState):
|
|
113
|
+
"""A SpaceState for a single dimensions (string) of space units (cells).
|
|
114
|
+
|
|
115
|
+
If sparse is set to True, a persistent data structure is used to share pointers between changes (can save a lot of memory)."""
|
|
116
|
+
__slots__ = 'cells',
|
|
117
|
+
|
|
118
|
+
def __init__(self, cells: MutableSequence[Cell]) -> None:
|
|
119
|
+
self.cells: MutableSequence[Cell] = cells
|
|
120
|
+
|
|
121
|
+
def __str__(self):
|
|
122
|
+
return ''.join((str(c) for c in self.cells))
|
|
123
|
+
|
|
124
|
+
def __repr__(self):
|
|
125
|
+
return str(self)
|
|
126
|
+
|
|
127
|
+
def __eq__(self, other: SpaceState1D) -> bool:
|
|
128
|
+
for sc, oc in zip(self.cells, other.cells):
|
|
129
|
+
if sc.quanta != oc.quanta:
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def __len__(self) -> int:
|
|
134
|
+
return len(self.cells)
|
|
135
|
+
|
|
136
|
+
def __bool__(self) -> bool:
|
|
137
|
+
return bool(self.cells)
|
|
138
|
+
|
|
139
|
+
def __hash__(self):
|
|
140
|
+
return hash(tuple(self.cells))
|
|
141
|
+
|
|
142
|
+
def __copy__(self) -> SpaceState1D:
|
|
143
|
+
new_space: SpaceState1D = object.__new__(self.__class__) # create new object without using init
|
|
144
|
+
new_space.cells = copy(self.cells)
|
|
145
|
+
return new_space
|
|
146
|
+
|
|
147
|
+
def __getitem__(self, item: int | slice) -> Cell | Sequence[Cell]:
|
|
148
|
+
return self.cells[item]
|
|
149
|
+
|
|
150
|
+
def get_all_cells(self) -> Sequence[Cell]:
|
|
151
|
+
return self.cells
|
|
152
|
+
|
|
153
|
+
def find(self, subspace: Sequence[Cell]) -> Iterator[tuple[int, int]]:
|
|
154
|
+
subspace_len: int = len(subspace)
|
|
155
|
+
for i in range(len(self.cells) - subspace_len + 1): # we use left-to-right search
|
|
156
|
+
if all(self.cells[i + j] == subspace[j] for j in range(subspace_len) if subspace[j].quanta != '.'):
|
|
157
|
+
yield i, i + subspace_len
|
|
158
|
+
|
|
159
|
+
# ==== Custom Modifiers ====
|
|
160
|
+
def substitute(self, selector: tuple[int, int], new: Sequence[Cell]) -> DeltaCell:
|
|
161
|
+
start, end = selector
|
|
162
|
+
destroyed: tuple[Cell, ...] = tuple(self.cells[start:end])
|
|
163
|
+
self.cells[start:end] = new
|
|
164
|
+
return DeltaCell(destroyed, new)
|
|
165
|
+
|
|
166
|
+
def insert(self, selector: int, new: Sequence[Cell]) -> DeltaCell:
|
|
167
|
+
if selector < 0:
|
|
168
|
+
selector = len(self.cells) + selector + 1
|
|
169
|
+
self.cells[selector:selector] = new
|
|
170
|
+
return DeltaCell((), new)
|
|
171
|
+
|
|
172
|
+
def overwrite(self, selector: int, new: Sequence[Cell]) -> DeltaCell:
|
|
173
|
+
destroyed: tuple[Cell, ...] = ()
|
|
174
|
+
new_: tuple[Cell, ...] = () # only here due to "_" being a cursor jump/skip operator
|
|
175
|
+
if selector < 0:
|
|
176
|
+
selector = len(self.cells) + selector
|
|
177
|
+
for i in range(len(new)):
|
|
178
|
+
idx = selector + i
|
|
179
|
+
new_char: Cell = new[i]
|
|
180
|
+
if new_char.quanta == '_': # skip these
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
destroyed += (self.cells[idx],)
|
|
184
|
+
self.cells[idx] = new_char
|
|
185
|
+
except IndexError:
|
|
186
|
+
self.cells.append(new_char)
|
|
187
|
+
new_ += (new_char,)
|
|
188
|
+
return DeltaCell(destroyed, new_)
|
|
189
|
+
|
|
190
|
+
def delete(self, selector: tuple[int, int]) -> DeltaCell:
|
|
191
|
+
start, end = selector
|
|
192
|
+
destroyed: tuple[Cell, ...] = tuple(self.cells[start:end])
|
|
193
|
+
self.cells[start:end] = ()
|
|
194
|
+
return DeltaCell(destroyed, ())
|
|
195
|
+
|
|
196
|
+
def shift(self, selector: tuple[int, int], k: int) -> DeltaCell:
|
|
197
|
+
start, end = selector
|
|
198
|
+
if end < 0: end = len(self.cells) + end
|
|
199
|
+
if start < 0: start = len(self.cells) + start
|
|
200
|
+
if k == 0:
|
|
201
|
+
pass
|
|
202
|
+
elif k < 0:
|
|
203
|
+
k = abs(k)
|
|
204
|
+
self.cells[end:end] = self.cells[start - k:start] # insert "before" to "after"
|
|
205
|
+
self.cells[start - k:start] = () # delete before
|
|
206
|
+
else:
|
|
207
|
+
temp = self.cells[end:end + k] # delete "after" but remember it
|
|
208
|
+
self.cells[end:end + k] = ()
|
|
209
|
+
self.cells[start:start] = temp # insert "after" to "before"
|
|
210
|
+
return DeltaCell((), ())
|
|
211
|
+
|
|
212
|
+
def swap(self, selector1: tuple[int, int], selector2: tuple[int, int]) -> DeltaCell:
|
|
213
|
+
start1, end1 = selector1
|
|
214
|
+
if end1 < 0: end1 = len(self.cells) + end1
|
|
215
|
+
if start1 < 0: start1 = len(self.cells) + start1
|
|
216
|
+
start2, end2 = selector2
|
|
217
|
+
if end2 < 0: end2 = len(self.cells) + end2
|
|
218
|
+
if start2 < 0: start2 = len(self.cells) + start2
|
|
219
|
+
if (start1 < start2 < end1 or start1 < end2 < end1
|
|
220
|
+
or start2 < start1 < end2 or start2 < end1 < end2): # we do additional checks to ensure that huge slices are still caught.
|
|
221
|
+
raise IndexError('The selector indices cannot overlap!')
|
|
222
|
+
if start2 < start1:
|
|
223
|
+
start1, start2 = start2, start1
|
|
224
|
+
end1, end2 = end2, end1
|
|
225
|
+
temp1 = self.cells[start1:end1]
|
|
226
|
+
temp2 = self.cells[start2:end2]
|
|
227
|
+
self.cells[start2:end2] = temp1
|
|
228
|
+
self.cells[start1:end1] = temp2
|
|
229
|
+
return DeltaCell((), ())
|
|
230
|
+
|
|
231
|
+
def reverse(self, selector: tuple[int, int]) -> DeltaCell:
|
|
232
|
+
start, end = selector
|
|
233
|
+
self.cells[start:end] = self.cells[start:end][::-1]
|
|
234
|
+
return DeltaCell((), ())
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class SpaceState2D(SpaceState):
|
|
238
|
+
"""It is here that we implement the 2D SpaceState. Just a placeholder for now."""
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class SpaceStateGraph(SpaceState):
|
|
243
|
+
"""It is here that we implement the graph SpaceState. Just a placeholder for now."""
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class RuleMatch(NamedTuple):
|
|
248
|
+
"""An object that represents a rule match. This is returned by Rule.match() and passed to Rule.apply()."""
|
|
249
|
+
space: SpaceState
|
|
250
|
+
matches: Sequence[tuple[int, int]] | Any # Any is to support higher dimension matches.
|
|
251
|
+
conflicts: set[int] # conflicting matches (idx of the match) that must be resolved.
|
|
252
|
+
metadata: Any = None # optional metadata
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Rule(ABC):
|
|
256
|
+
def __init__(self):
|
|
257
|
+
"""Should take arguments that define the rule behavior. For instance, ``SubstitutionRule(match: string, replace: string)`` should be for a rule that finds a matching substring and replaces it.
|
|
258
|
+
``InsertionRule(insert: string, at_idx: string)`` should be a rule that inserts a string at the specified index. Whatever the init arguments are, they must be created as fields internally in an elegant format.
|
|
259
|
+
|
|
260
|
+
The Rule should be responsible for duplicating (or not) the SpaceState(s) when applying itself. This way,
|
|
261
|
+
multi-way systems are supported because the Rule can apply multiple different modifications to multiple
|
|
262
|
+
different SpaceStates if necessary.
|
|
263
|
+
|
|
264
|
+
Note that all the code is assuming that multi-way systems take place for multiple modifications. However, if we want to modify a SpaceState, without creating branches, we must do that in the Rule itself (i.e. having entire "rulesets" within rules).
|
|
265
|
+
"""
|
|
266
|
+
# metadata
|
|
267
|
+
self.id: str = '' # could be used to filter rules.
|
|
268
|
+
|
|
269
|
+
# Flags (these are only those which modify default RuleSet behavior)
|
|
270
|
+
self.disabled: bool = False # if the rule is disabled (dead)
|
|
271
|
+
self.group: int | str = 0 # group together rules this way.
|
|
272
|
+
self.group_break: bool = True # break out of the group upon successful application of rule.
|
|
273
|
+
self.always_apply: bool = False # always apply this rule no matter what (disregards grouping)
|
|
274
|
+
# NOTE: any and all additional flags that modify internal rule behavior MUST (for the sake of clarity) be in the implementation of the rule.
|
|
275
|
+
|
|
276
|
+
@abstractmethod
|
|
277
|
+
def match(self, spaces: Sequence[SpaceState]) -> Sequence[RuleMatch]:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
@abstractmethod
|
|
281
|
+
def apply(self, rule_matches: Sequence[RuleMatch]) -> Sequence[DeltaSpace]:
|
|
282
|
+
"""Applies the rule to the given ``SpaceState(s)``. Modified SpaceStates are returned.
|
|
283
|
+
Important for implementation: *new/copied* SpaceState(s) must be created, modified, and returned.
|
|
284
|
+
|
|
285
|
+
Rule is responsible for taking all current states to provide maximum flexibility (so different rules can have different behavior: sessies + messies) (TRUST ME!!! I doubted my past self on this and then wasted a bunch of time... just keep it as-is you crazy future self!)
|
|
286
|
+
"""
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class RuleSet:
|
|
291
|
+
"""This contains the Rules that can be applied. Additional, more complex, behavior can be implemented by subclassing it.
|
|
292
|
+
|
|
293
|
+
Note that all the code is engineered around assuming multi-way systems for more than one rule being applied.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def __init__(self, rules: list[Rule]):
|
|
297
|
+
"""This should be implemented by subclasses.
|
|
298
|
+
This should ideally accept a list of Rules either as objects or as strings that should then be parsed into their corresponding rules. The rules should be stored in array."""
|
|
299
|
+
self.rules: list[Rule] = rules
|
|
300
|
+
|
|
301
|
+
def __str__(self) -> str:
|
|
302
|
+
return str(self.rules)
|
|
303
|
+
|
|
304
|
+
def __repr__(self) -> str:
|
|
305
|
+
return str(self)
|
|
306
|
+
|
|
307
|
+
def apply(self, to_spaces: Sequence[SpaceState]) -> list[DeltaSpaces]:
|
|
308
|
+
"""Applies the Rules to the given spaces, and returns a sequence of the DeltaSpaceSet."""
|
|
309
|
+
group_management: dict = {
|
|
310
|
+
# group IDs go here along with whether they are active - id: bool
|
|
311
|
+
}
|
|
312
|
+
applied_rules: list[DeltaSpaces] = []
|
|
313
|
+
for rule in self.rules:
|
|
314
|
+
if rule.disabled:
|
|
315
|
+
continue
|
|
316
|
+
active: bool = group_management.setdefault(rule.group, True)
|
|
317
|
+
if not active and not rule.always_apply:
|
|
318
|
+
continue
|
|
319
|
+
rule_matches: Sequence[RuleMatch] = rule.match(to_spaces)
|
|
320
|
+
if rule_matches: # if there are any rule matches.
|
|
321
|
+
space_deltas: DeltaSpaces = DeltaSpaces(rule.apply(rule_matches), rule)
|
|
322
|
+
if space_deltas: # to be robust in case a complex rule still fails (even though input matches were found we can't guarantee that it will always work)
|
|
323
|
+
applied_rules.append(space_deltas)
|
|
324
|
+
if rule.group_break: group_management[rule.group] = False
|
|
325
|
+
return applied_rules
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class DeltaCell(NamedTuple): # the cells that were created and destroyed by some SpaceState.modifier() method.
|
|
329
|
+
destroyed_cells: Sequence[Cell]
|
|
330
|
+
new_cells: Sequence[Cell]
|
|
331
|
+
|
|
332
|
+
def __bool__(self) -> bool:
|
|
333
|
+
return bool(self.destroyed_cells) or bool(self.new_cells) # if any changes occurred, return true.
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class DeltaSpace(NamedTuple): # returned by Rule.apply() in a Sequence[DeltaSpace]
|
|
337
|
+
"""Single application of a rule within Rule.apply()."""
|
|
338
|
+
input_space: SpaceState # we always have this filled so that we know what spaces had what changes (if any) made
|
|
339
|
+
output_space: Sequence[SpaceState | None] # can include many children branches
|
|
340
|
+
cell_deltas: Sequence[DeltaCell] # should be aligned with output_space array (so branches align)
|
|
341
|
+
|
|
342
|
+
def __bool__(self) -> bool:
|
|
343
|
+
return any(self.output_space) or any(self.cell_deltas) # we check both to be as robust as possible... what if a rule does not return delta cells due to modifying but not adding or deleting?
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class DeltaSpaces(NamedTuple): # returned by RuleSet.apply() in a Sequence[DeltaSpaces]
|
|
347
|
+
"""All delta spaces that happened under a given rule."""
|
|
348
|
+
space_deltas: Sequence[DeltaSpace]
|
|
349
|
+
rule: Rule | None
|
|
350
|
+
|
|
351
|
+
def __bool__(self) -> bool:
|
|
352
|
+
return any(self.space_deltas) # if any changes were recorded.
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@dataclass(slots=True)
|
|
356
|
+
class Event:
|
|
357
|
+
time: int # also known as time - should be unique to every event
|
|
358
|
+
space_deltas: list[DeltaSpaces] # all space deltas (organized by the rules they were applied under)
|
|
359
|
+
|
|
360
|
+
# metadata
|
|
361
|
+
inert: bool = False # if true, the new event caused no changes to the system.
|
|
362
|
+
weight: int | float = 1 # could be used for weighted causality tracking. (think of it as a time multiplier/dilator)
|
|
363
|
+
causal_distance_to_creation: int = 0 # minimum distance (min number of nodes) to the creation event node.
|
|
364
|
+
|
|
365
|
+
@property # maybe cache this?
|
|
366
|
+
def affected_cells(self) -> Iterator[DeltaCell]:
|
|
367
|
+
"""Returns all cell deltas"""
|
|
368
|
+
for r in self.space_deltas:
|
|
369
|
+
for space_delta in r.space_deltas:
|
|
370
|
+
for cell_delta in space_delta.cell_deltas:
|
|
371
|
+
if cell_delta:
|
|
372
|
+
yield cell_delta
|
|
373
|
+
|
|
374
|
+
@property # maybe cache this?
|
|
375
|
+
def causally_connected_events(self) -> Iterator[int]:
|
|
376
|
+
"""Returns events (stored as indices) whose created cells were destroyed by this event"""
|
|
377
|
+
for delta in self.affected_cells:
|
|
378
|
+
for cell in delta.destroyed_cells:
|
|
379
|
+
yield cell.created_at
|
|
380
|
+
|
|
381
|
+
@property # maybe cache this?
|
|
382
|
+
def spaces(self) -> Iterator[SpaceState]:
|
|
383
|
+
"""Returns all newly created spaces"""
|
|
384
|
+
for r in self.space_deltas:
|
|
385
|
+
for space_delta in r.space_deltas:
|
|
386
|
+
for space in space_delta.output_space:
|
|
387
|
+
if space is not None:
|
|
388
|
+
yield space
|
|
389
|
+
|
|
390
|
+
@property # maybe cache this?
|
|
391
|
+
def spaces_with_metadata(self) -> Iterator[tuple[DeltaSpaces, DeltaSpace, SpaceState]]:
|
|
392
|
+
"""Returns all newly created spaces along with their metadata (in the parent structure)"""
|
|
393
|
+
for r in self.space_deltas:
|
|
394
|
+
for space_delta in r.space_deltas:
|
|
395
|
+
for space in space_delta.output_space:
|
|
396
|
+
if space is not None:
|
|
397
|
+
yield r, space_delta, space
|
|
398
|
+
|
|
399
|
+
def __str__(self):
|
|
400
|
+
return '[' + ', '.join(str(space) for space in self.spaces) + ']' # TODO remove this to a dedication printer
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class Flow:
|
|
404
|
+
"""The base class for a rule flow, additional behavior should be implemented by subclassing this class."""
|
|
405
|
+
|
|
406
|
+
# Signals (can be used to live update analysis objects like the causal graph)
|
|
407
|
+
on_evolved_step: Signal[Self] = Signal()
|
|
408
|
+
on_evolved_n: Signal[Self, int] = Signal() # after all evolves
|
|
409
|
+
on_undone_step: Signal[Self] = Signal()
|
|
410
|
+
on_undone_n: Signal[Self, int] = Signal() # after all undo's
|
|
411
|
+
on_clear: Signal[Self] = Signal()
|
|
412
|
+
on_ruleset_set: Signal[Self] = Signal()
|
|
413
|
+
|
|
414
|
+
def __init__(self):
|
|
415
|
+
self.ruleset: RuleSet = RuleSet([]) # can be changed at any time to provide a new set of rules.
|
|
416
|
+
self.events: list[Event] = [] # defaults to empty... but nothing will work properly
|
|
417
|
+
|
|
418
|
+
# progress tracking attributes
|
|
419
|
+
self.n_step_progress: float = 0 # percentage of steps run by some_method_n().
|
|
420
|
+
|
|
421
|
+
def set_ruleset(self, ruleset: RuleSet) -> None:
|
|
422
|
+
"""Used to set the rule set"""
|
|
423
|
+
self.ruleset: RuleSet = ruleset
|
|
424
|
+
self.on_ruleset_set.emit(self)
|
|
425
|
+
|
|
426
|
+
def set_initial_space(self, initial_space: Sequence[SpaceState]) -> None:
|
|
427
|
+
"""Used to set the initial space"""
|
|
428
|
+
if not self.events:
|
|
429
|
+
self.events.append(cast(Event, cast(object, 0)))
|
|
430
|
+
self.events[0] = Event(0, [DeltaSpaces(tuple((DeltaSpace(i, (i,), (DeltaCell((), ()),)) for i in initial_space)), None)]) # initial output space must be `i` as well so that next evolve() works.
|
|
431
|
+
for i in initial_space:
|
|
432
|
+
for cell in i.get_all_cells():
|
|
433
|
+
cell.created_at = 0
|
|
434
|
+
|
|
435
|
+
def clear_evolution(self) -> None:
|
|
436
|
+
"""Clear the evolution."""
|
|
437
|
+
del self.events[1:]
|
|
438
|
+
self.on_clear.emit(self)
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def current_event(self) -> Event:
|
|
442
|
+
return self.events[-1]
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def current_event_idx(self) -> int:
|
|
446
|
+
return len(self.events) - 1
|
|
447
|
+
|
|
448
|
+
def _evolve(self) -> None:
|
|
449
|
+
""" Evolve the system by one step.
|
|
450
|
+
|
|
451
|
+
This can be reimplemented by subclasses to modify behavior. As it stands, it does the following:
|
|
452
|
+
- apply the rules to the current space states using RuleSet.apply()
|
|
453
|
+
- if a rule was successfully applied, create a new event and increment the time ``step``
|
|
454
|
+
- Update event and cell metadata (important for tracking causality)
|
|
455
|
+
- set the applied rules (the applied rules are associated with the space states they modified)
|
|
456
|
+
- extract all the modified space states from the applied rules and add them to the space states of the Event.
|
|
457
|
+
"""
|
|
458
|
+
applied_rules: list[DeltaSpaces] = self.ruleset.apply(to_spaces=tuple(self.current_event.spaces))
|
|
459
|
+
if not any(applied_rules): # if no rules made any modifications to the spaces
|
|
460
|
+
self.current_event.inert = True
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
# Create a new event and process it
|
|
464
|
+
self.events.append(
|
|
465
|
+
Event(self.current_event.time + 1, space_deltas=applied_rules) # create a new event
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# process causality
|
|
469
|
+
current_event_idx: int = self.current_event_idx
|
|
470
|
+
for ar in applied_rules:
|
|
471
|
+
for sd in ar.space_deltas:
|
|
472
|
+
for dc in sd.cell_deltas:
|
|
473
|
+
for cell in dc.new_cells:
|
|
474
|
+
cell.created_at = current_event_idx
|
|
475
|
+
for cell in dc.destroyed_cells:
|
|
476
|
+
cell.destroyed_at += (current_event_idx,) # first one, of course, will be the main lineage
|
|
477
|
+
|
|
478
|
+
# process causal distance to creation
|
|
479
|
+
min_prev: int = min((self.events[e_idx].causal_distance_to_creation
|
|
480
|
+
for e_idx in self.current_event.causally_connected_events),
|
|
481
|
+
default=-1)
|
|
482
|
+
self.current_event.causal_distance_to_creation = min_prev + 1
|
|
483
|
+
|
|
484
|
+
# emit any signals
|
|
485
|
+
self.on_evolved_step.emit(self)
|
|
486
|
+
|
|
487
|
+
def evolve(self, n_steps: int, break_when_inert: bool = False) -> None:
|
|
488
|
+
"""Evolve the system n steps."""
|
|
489
|
+
i: int = 0
|
|
490
|
+
while i < n_steps:
|
|
491
|
+
# print(str(next(self.current_event.spaces).cells.search_buffer).replace('A', '\x1b[1;41m A \x1b[0m').replace('B', '\x1b[1;42m B \x1b[0m')) # if we want to see how the buffer changes.
|
|
492
|
+
self.n_step_progress = (i + 1) / n_steps
|
|
493
|
+
i += 1
|
|
494
|
+
self._evolve()
|
|
495
|
+
if break_when_inert and self.current_event.inert:
|
|
496
|
+
break
|
|
497
|
+
|
|
498
|
+
# emit any signals
|
|
499
|
+
self.on_evolved_n.emit(self, n_steps)
|
|
500
|
+
|
|
501
|
+
def _undo(self) -> None:
|
|
502
|
+
"""undo the last event..."""
|
|
503
|
+
if self.current_event_idx == 0:
|
|
504
|
+
return
|
|
505
|
+
for ar in self.current_event.space_deltas:
|
|
506
|
+
for sd in ar.space_deltas:
|
|
507
|
+
for dc in sd.cell_deltas:
|
|
508
|
+
for cell in dc.destroyed_cells:
|
|
509
|
+
cell.destroyed_at = tuple(i for i in cell.destroyed_at if i != self.current_event_idx)
|
|
510
|
+
self.events.pop()
|
|
511
|
+
|
|
512
|
+
# emit any signals
|
|
513
|
+
self.on_undone_step.emit(self)
|
|
514
|
+
|
|
515
|
+
def undo(self, n_steps: int) -> None:
|
|
516
|
+
for _ in range(n_steps):
|
|
517
|
+
self.n_step_progress = (_ + 1) / n_steps
|
|
518
|
+
self._undo()
|
|
519
|
+
|
|
520
|
+
self.on_undone_n.emit(self, n_steps)
|
|
521
|
+
|
|
522
|
+
def __str__(self) -> str:
|
|
523
|
+
return '\n'.join(str(e) for e in self.events)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if __name__ == '__main__':
|
|
527
|
+
pass
|
core/graph.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Provides all the tools for optimized and rigorous graph analysis.
|
|
2
|
+
|
|
3
|
+
FRAMEWORK NOTES:
|
|
4
|
+
- use pyvis Network to render interactive graphs.
|
|
5
|
+
|
|
6
|
+
TODO:
|
|
7
|
+
- Redesign to support live graph updating (to keep up to date with flow).
|
|
8
|
+
- Add more tools for seamless analysis and integrations.
|
|
9
|
+
"""
|
|
10
|
+
from core.engine import Flow
|
|
11
|
+
from networkx import MultiDiGraph
|
|
12
|
+
from typing import Sequence, Self
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventCausalityGraph(MultiDiGraph):
|
|
16
|
+
def build(self, flow: Flow,
|
|
17
|
+
event_range: tuple[int, int, int],
|
|
18
|
+
collapse_multi_edges: bool = False) -> Self:
|
|
19
|
+
# construct causal graph - because each node is literally the time, and thus index, it can be used to query to the actual event for more granular information.
|
|
20
|
+
connected_container: type[tuple] | type[set] = set if collapse_multi_edges else tuple
|
|
21
|
+
for event in flow.events[event_range[0]:event_range[1]+1:event_range[2]]:
|
|
22
|
+
causally_connected: Sequence[int] = connected_container(event.causally_connected_events)
|
|
23
|
+
self.add_node(
|
|
24
|
+
event.time,
|
|
25
|
+
# these get passed on to the nodes of VisJS network.
|
|
26
|
+
label=f'{event.time}',
|
|
27
|
+
title=f' Causal Distance: {event.causal_distance_to_creation}\n'
|
|
28
|
+
f'Connected Events: {len(causally_connected)}',
|
|
29
|
+
shape='box'
|
|
30
|
+
)
|
|
31
|
+
for parent_time in causally_connected:
|
|
32
|
+
self.add_edge(parent_time, event.time)
|
|
33
|
+
return self
|