exonware-xwnode 0.0.1.12__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 (132) hide show
  1. exonware/__init__.py +14 -0
  2. exonware/xwnode/__init__.py +127 -0
  3. exonware/xwnode/base.py +676 -0
  4. exonware/xwnode/config.py +178 -0
  5. exonware/xwnode/contracts.py +730 -0
  6. exonware/xwnode/errors.py +503 -0
  7. exonware/xwnode/facade.py +460 -0
  8. exonware/xwnode/strategies/__init__.py +158 -0
  9. exonware/xwnode/strategies/advisor.py +463 -0
  10. exonware/xwnode/strategies/edges/__init__.py +32 -0
  11. exonware/xwnode/strategies/edges/adj_list.py +227 -0
  12. exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
  13. exonware/xwnode/strategies/edges/base.py +169 -0
  14. exonware/xwnode/strategies/flyweight.py +328 -0
  15. exonware/xwnode/strategies/impls/__init__.py +13 -0
  16. exonware/xwnode/strategies/impls/_base_edge.py +403 -0
  17. exonware/xwnode/strategies/impls/_base_node.py +307 -0
  18. exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
  19. exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
  20. exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
  21. exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
  22. exonware/xwnode/strategies/impls/edge_coo.py +533 -0
  23. exonware/xwnode/strategies/impls/edge_csc.py +447 -0
  24. exonware/xwnode/strategies/impls/edge_csr.py +492 -0
  25. exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
  26. exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
  27. exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
  28. exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
  29. exonware/xwnode/strategies/impls/edge_octree.py +574 -0
  30. exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
  31. exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
  32. exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
  33. exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
  34. exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
  35. exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
  36. exonware/xwnode/strategies/manager.py +775 -0
  37. exonware/xwnode/strategies/metrics.py +538 -0
  38. exonware/xwnode/strategies/migration.py +432 -0
  39. exonware/xwnode/strategies/nodes/__init__.py +50 -0
  40. exonware/xwnode/strategies/nodes/_base_node.py +307 -0
  41. exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
  42. exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
  43. exonware/xwnode/strategies/nodes/array_list.py +209 -0
  44. exonware/xwnode/strategies/nodes/base.py +247 -0
  45. exonware/xwnode/strategies/nodes/deque.py +200 -0
  46. exonware/xwnode/strategies/nodes/hash_map.py +135 -0
  47. exonware/xwnode/strategies/nodes/heap.py +307 -0
  48. exonware/xwnode/strategies/nodes/linked_list.py +232 -0
  49. exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
  50. exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
  51. exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
  52. exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
  53. exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
  54. exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
  55. exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
  56. exonware/xwnode/strategies/nodes/node_btree.py +357 -0
  57. exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
  58. exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
  59. exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
  60. exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
  61. exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
  62. exonware/xwnode/strategies/nodes/node_heap.py +191 -0
  63. exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
  64. exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
  65. exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
  66. exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
  67. exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
  68. exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
  69. exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
  70. exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
  71. exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
  72. exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
  73. exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
  74. exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
  75. exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
  76. exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
  77. exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
  78. exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
  79. exonware/xwnode/strategies/nodes/node_treap.py +387 -0
  80. exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
  81. exonware/xwnode/strategies/nodes/node_trie.py +252 -0
  82. exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
  83. exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
  84. exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
  85. exonware/xwnode/strategies/nodes/queue.py +161 -0
  86. exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
  87. exonware/xwnode/strategies/nodes/stack.py +152 -0
  88. exonware/xwnode/strategies/nodes/trie.py +274 -0
  89. exonware/xwnode/strategies/nodes/union_find.py +283 -0
  90. exonware/xwnode/strategies/pattern_detector.py +603 -0
  91. exonware/xwnode/strategies/performance_monitor.py +487 -0
  92. exonware/xwnode/strategies/queries/__init__.py +24 -0
  93. exonware/xwnode/strategies/queries/base.py +236 -0
  94. exonware/xwnode/strategies/queries/cql.py +201 -0
  95. exonware/xwnode/strategies/queries/cypher.py +181 -0
  96. exonware/xwnode/strategies/queries/datalog.py +70 -0
  97. exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
  98. exonware/xwnode/strategies/queries/eql.py +70 -0
  99. exonware/xwnode/strategies/queries/flux.py +70 -0
  100. exonware/xwnode/strategies/queries/gql.py +70 -0
  101. exonware/xwnode/strategies/queries/graphql.py +240 -0
  102. exonware/xwnode/strategies/queries/gremlin.py +181 -0
  103. exonware/xwnode/strategies/queries/hiveql.py +214 -0
  104. exonware/xwnode/strategies/queries/hql.py +70 -0
  105. exonware/xwnode/strategies/queries/jmespath.py +219 -0
  106. exonware/xwnode/strategies/queries/jq.py +66 -0
  107. exonware/xwnode/strategies/queries/json_query.py +66 -0
  108. exonware/xwnode/strategies/queries/jsoniq.py +248 -0
  109. exonware/xwnode/strategies/queries/kql.py +70 -0
  110. exonware/xwnode/strategies/queries/linq.py +238 -0
  111. exonware/xwnode/strategies/queries/logql.py +70 -0
  112. exonware/xwnode/strategies/queries/mql.py +68 -0
  113. exonware/xwnode/strategies/queries/n1ql.py +210 -0
  114. exonware/xwnode/strategies/queries/partiql.py +70 -0
  115. exonware/xwnode/strategies/queries/pig.py +215 -0
  116. exonware/xwnode/strategies/queries/promql.py +70 -0
  117. exonware/xwnode/strategies/queries/sparql.py +220 -0
  118. exonware/xwnode/strategies/queries/sql.py +275 -0
  119. exonware/xwnode/strategies/queries/xml_query.py +66 -0
  120. exonware/xwnode/strategies/queries/xpath.py +223 -0
  121. exonware/xwnode/strategies/queries/xquery.py +258 -0
  122. exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
  123. exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
  124. exonware/xwnode/strategies/registry.py +604 -0
  125. exonware/xwnode/strategies/simple.py +273 -0
  126. exonware/xwnode/strategies/utils.py +532 -0
  127. exonware/xwnode/types.py +912 -0
  128. exonware/xwnode/version.py +78 -0
  129. exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
  130. exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
  131. exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
  132. exonware_xwnode-0.0.1.12.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,558 @@
1
+ """
2
+ Temporal EdgeSet Strategy Implementation
3
+
4
+ This module implements the TEMPORAL_EDGESET strategy for time-aware graphs
5
+ with temporal queries and time-based edge evolution.
6
+ """
7
+
8
+ from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, NamedTuple
9
+ from collections import defaultdict
10
+ import time
11
+ import bisect
12
+ from ._base_edge import aEdgeStrategy
13
+ from ...types import EdgeMode, EdgeTrait
14
+
15
+
16
+ class TimeInterval(NamedTuple):
17
+ """Represents a time interval."""
18
+ start: float
19
+ end: Optional[float] # None means ongoing
20
+
21
+ def contains(self, timestamp: float) -> bool:
22
+ """Check if timestamp is within this interval."""
23
+ return self.start <= timestamp <= (self.end if self.end is not None else float('inf'))
24
+
25
+ def overlaps(self, other: 'TimeInterval') -> bool:
26
+ """Check if this interval overlaps with another."""
27
+ if self.end is None or other.end is None:
28
+ return True # Ongoing intervals always overlap
29
+ return self.start <= other.end and other.start <= self.end
30
+
31
+
32
+ class TemporalEdge:
33
+ """Represents an edge with temporal validity periods."""
34
+
35
+ def __init__(self, edge_id: str, source: str, target: str,
36
+ start_time: float, end_time: Optional[float] = None, **properties):
37
+ self.edge_id = edge_id
38
+ self.source = source
39
+ self.target = target
40
+ self.interval = TimeInterval(start_time, end_time)
41
+ self.properties = properties.copy()
42
+ self.created_at = time.time()
43
+
44
+ def is_active_at(self, timestamp: float) -> bool:
45
+ """Check if edge is active at given timestamp."""
46
+ return self.interval.contains(timestamp)
47
+
48
+ def is_currently_active(self) -> bool:
49
+ """Check if edge is currently active."""
50
+ return self.is_active_at(time.time())
51
+
52
+ def overlaps_with(self, other: 'TemporalEdge') -> bool:
53
+ """Check if this edge's time interval overlaps with another."""
54
+ return self.interval.overlaps(other.interval)
55
+
56
+ def to_dict(self) -> Dict[str, Any]:
57
+ """Convert to dictionary representation."""
58
+ return {
59
+ 'id': self.edge_id,
60
+ 'source': self.source,
61
+ 'target': self.target,
62
+ 'start_time': self.interval.start,
63
+ 'end_time': self.interval.end,
64
+ 'properties': self.properties,
65
+ 'created_at': self.created_at,
66
+ 'is_active': self.is_currently_active()
67
+ }
68
+
69
+
70
+ class xTemporalEdgeSetStrategy(aEdgeStrategy):
71
+ """
72
+ Temporal EdgeSet strategy for time-aware graph management.
73
+
74
+ Provides efficient temporal queries, time-based edge activation/deactivation,
75
+ and historical graph state reconstruction.
76
+ """
77
+
78
+ def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
79
+ """Initialize the Temporal EdgeSet strategy."""
80
+ super().__init__(EdgeMode.TEMPORAL_EDGESET, traits, **options)
81
+
82
+ self.is_directed = options.get('directed', True)
83
+ self.default_duration = options.get('default_duration', None) # None = infinite
84
+ self.time_precision = options.get('time_precision', 0.001) # 1ms precision
85
+
86
+ # Core temporal storage
87
+ # edges_by_time: timestamp -> list of (edge_id, 'start'/'end')
88
+ self._edges_by_time: Dict[float, List[Tuple[str, str]]] = defaultdict(list)
89
+ self._sorted_timestamps: List[float] = [] # Sorted for binary search
90
+
91
+ # Edge storage
92
+ self._edges: Dict[str, TemporalEdge] = {} # edge_id -> TemporalEdge
93
+ self._outgoing: Dict[str, Set[str]] = defaultdict(set) # source -> set of edge_ids
94
+ self._incoming: Dict[str, Set[str]] = defaultdict(set) if self.is_directed else None
95
+
96
+ # Vertex management
97
+ self._vertices: Set[str] = set()
98
+ self._edge_id_counter = 0
99
+ self._current_time_cache = None
100
+ self._cache_timestamp = 0
101
+
102
+ def get_supported_traits(self) -> EdgeTrait:
103
+ """Get the traits supported by the temporal edgeset strategy."""
104
+ return (EdgeTrait.TEMPORAL | EdgeTrait.DIRECTED | EdgeTrait.SPARSE | EdgeTrait.WEIGHTED)
105
+
106
+ def _round_time(self, timestamp: float) -> float:
107
+ """Round timestamp to configured precision."""
108
+ return round(timestamp / self.time_precision) * self.time_precision
109
+
110
+ def _add_time_event(self, timestamp: float, edge_id: str, event_type: str) -> None:
111
+ """Add a time-based event."""
112
+ rounded_time = self._round_time(timestamp)
113
+
114
+ self._edges_by_time[rounded_time].append((edge_id, event_type))
115
+
116
+ # Maintain sorted timestamps
117
+ if rounded_time not in self._sorted_timestamps:
118
+ bisect.insort(self._sorted_timestamps, rounded_time)
119
+
120
+ def _get_active_edges_at(self, timestamp: float) -> Set[str]:
121
+ """Get all active edge IDs at a specific timestamp."""
122
+ # Use cache if timestamp is current
123
+ current_time = time.time()
124
+ if (self._current_time_cache is not None and
125
+ abs(timestamp - current_time) < self.time_precision and
126
+ abs(self._cache_timestamp - current_time) < 1.0): # 1 second cache
127
+ return self._current_time_cache
128
+
129
+ active_edges = set()
130
+
131
+ # Process events up to the timestamp
132
+ for ts in self._sorted_timestamps:
133
+ if ts > timestamp:
134
+ break
135
+
136
+ for edge_id, event_type in self._edges_by_time[ts]:
137
+ if event_type == 'start':
138
+ active_edges.add(edge_id)
139
+ elif event_type == 'end':
140
+ active_edges.discard(edge_id)
141
+
142
+ # Add ongoing edges that started before timestamp
143
+ for edge_id, edge in self._edges.items():
144
+ if (edge.interval.start <= timestamp and
145
+ edge.interval.end is None and
146
+ edge_id not in active_edges):
147
+ active_edges.add(edge_id)
148
+
149
+ # Cache if this is current time
150
+ if abs(timestamp - current_time) < self.time_precision:
151
+ self._current_time_cache = active_edges.copy()
152
+ self._cache_timestamp = current_time
153
+
154
+ return active_edges
155
+
156
+ # ============================================================================
157
+ # CORE EDGE OPERATIONS
158
+ # ============================================================================
159
+
160
+ def add_edge(self, source: str, target: str, **properties) -> str:
161
+ """Add a temporal edge."""
162
+ # Extract temporal properties
163
+ start_time = properties.pop('start_time', time.time())
164
+ end_time = properties.pop('end_time', self.default_duration)
165
+
166
+ if end_time is not None and isinstance(end_time, (int, float)) and end_time > 0:
167
+ # Convert relative duration to absolute end time
168
+ if end_time < start_time:
169
+ end_time = start_time + end_time
170
+
171
+ # Generate edge ID
172
+ edge_id = f"tedge_{self._edge_id_counter}"
173
+ self._edge_id_counter += 1
174
+
175
+ # Create temporal edge
176
+ edge = TemporalEdge(edge_id, source, target, start_time, end_time, **properties)
177
+
178
+ # Store edge
179
+ self._edges[edge_id] = edge
180
+ self._outgoing[source].add(edge_id)
181
+
182
+ if self.is_directed and self._incoming is not None:
183
+ self._incoming[target].add(edge_id)
184
+ elif not self.is_directed and source != target:
185
+ self._outgoing[target].add(edge_id)
186
+
187
+ # Add vertices
188
+ self._vertices.add(source)
189
+ self._vertices.add(target)
190
+
191
+ # Register time events
192
+ self._add_time_event(start_time, edge_id, 'start')
193
+ if end_time is not None:
194
+ self._add_time_event(end_time, edge_id, 'end')
195
+
196
+ # Invalidate cache
197
+ self._current_time_cache = None
198
+
199
+ return edge_id
200
+
201
+ def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
202
+ """Remove a temporal edge (mark as ended)."""
203
+ # Find edge to remove
204
+ target_edge_id = None
205
+
206
+ if edge_id:
207
+ if edge_id in self._edges:
208
+ edge = self._edges[edge_id]
209
+ if edge.source == source and edge.target == target:
210
+ target_edge_id = edge_id
211
+ else:
212
+ # Find first matching edge
213
+ for eid in self._outgoing.get(source, set()):
214
+ edge = self._edges.get(eid)
215
+ if edge and edge.target == target and edge.is_currently_active():
216
+ target_edge_id = eid
217
+ break
218
+
219
+ if not target_edge_id:
220
+ return False
221
+
222
+ edge = self._edges[target_edge_id]
223
+
224
+ # End the edge at current time if it's ongoing
225
+ if edge.interval.end is None:
226
+ current_time = time.time()
227
+ edge.interval = TimeInterval(edge.interval.start, current_time)
228
+ self._add_time_event(current_time, target_edge_id, 'end')
229
+
230
+ # Remove from adjacency lists
231
+ self._outgoing[source].discard(target_edge_id)
232
+ if self.is_directed and self._incoming is not None:
233
+ self._incoming[target].discard(target_edge_id)
234
+ elif not self.is_directed:
235
+ self._outgoing[target].discard(target_edge_id)
236
+
237
+ # Invalidate cache
238
+ self._current_time_cache = None
239
+
240
+ return True
241
+
242
+ def has_edge(self, source: str, target: str, timestamp: Optional[float] = None) -> bool:
243
+ """Check if edge exists at specific time (default: current time)."""
244
+ if timestamp is None:
245
+ timestamp = time.time()
246
+
247
+ active_edges = self._get_active_edges_at(timestamp)
248
+
249
+ for edge_id in active_edges:
250
+ edge = self._edges.get(edge_id)
251
+ if edge and edge.source == source and edge.target == target:
252
+ return True
253
+
254
+ return False
255
+
256
+ def get_edge_data(self, source: str, target: str,
257
+ timestamp: Optional[float] = None) -> Optional[Dict[str, Any]]:
258
+ """Get edge data at specific time."""
259
+ if timestamp is None:
260
+ timestamp = time.time()
261
+
262
+ active_edges = self._get_active_edges_at(timestamp)
263
+
264
+ for edge_id in active_edges:
265
+ edge = self._edges.get(edge_id)
266
+ if edge and edge.source == source and edge.target == target:
267
+ return edge.to_dict()
268
+
269
+ return None
270
+
271
+ def neighbors(self, vertex: str, direction: str = 'out',
272
+ timestamp: Optional[float] = None) -> Iterator[str]:
273
+ """Get neighbors at specific time."""
274
+ if timestamp is None:
275
+ timestamp = time.time()
276
+
277
+ active_edges = self._get_active_edges_at(timestamp)
278
+ seen = set()
279
+
280
+ if direction in ['out', 'both']:
281
+ for edge_id in self._outgoing.get(vertex, set()):
282
+ if edge_id in active_edges:
283
+ edge = self._edges[edge_id]
284
+ if edge.target not in seen:
285
+ seen.add(edge.target)
286
+ yield edge.target
287
+
288
+ if direction in ['in', 'both'] and self.is_directed and self._incoming is not None:
289
+ for edge_id in self._incoming.get(vertex, set()):
290
+ if edge_id in active_edges:
291
+ edge = self._edges[edge_id]
292
+ if edge.source not in seen:
293
+ seen.add(edge.source)
294
+ yield edge.source
295
+
296
+ def degree(self, vertex: str, direction: str = 'out',
297
+ timestamp: Optional[float] = None) -> int:
298
+ """Get degree at specific time."""
299
+ return sum(1 for _ in self.neighbors(vertex, direction, timestamp))
300
+
301
+ def edges(self, data: bool = False, timestamp: Optional[float] = None) -> Iterator[tuple]:
302
+ """Get all edges at specific time."""
303
+ if timestamp is None:
304
+ timestamp = time.time()
305
+
306
+ active_edges = self._get_active_edges_at(timestamp)
307
+ seen = set()
308
+
309
+ for edge_id in active_edges:
310
+ edge = self._edges[edge_id]
311
+ edge_key = (edge.source, edge.target)
312
+
313
+ # Avoid duplicates for undirected graphs
314
+ if not self.is_directed and edge.source > edge.target:
315
+ edge_key = (edge.target, edge.source)
316
+
317
+ if edge_key not in seen:
318
+ seen.add(edge_key)
319
+
320
+ if data:
321
+ yield (edge.source, edge.target, edge.to_dict())
322
+ else:
323
+ yield (edge.source, edge.target)
324
+
325
+ def vertices(self) -> Iterator[str]:
326
+ """Get all vertices."""
327
+ return iter(self._vertices)
328
+
329
+ def __len__(self) -> int:
330
+ """Get the number of currently active edges."""
331
+ return len(self._get_active_edges_at(time.time()))
332
+
333
+ def vertex_count(self) -> int:
334
+ """Get the number of vertices."""
335
+ return len(self._vertices)
336
+
337
+ def clear(self) -> None:
338
+ """Clear all edges and vertices."""
339
+ self._edges.clear()
340
+ self._edges_by_time.clear()
341
+ self._sorted_timestamps.clear()
342
+ self._outgoing.clear()
343
+ if self._incoming is not None:
344
+ self._incoming.clear()
345
+ self._vertices.clear()
346
+ self._edge_id_counter = 0
347
+ self._current_time_cache = None
348
+
349
+ def add_vertex(self, vertex: str) -> None:
350
+ """Add a vertex."""
351
+ self._vertices.add(vertex)
352
+
353
+ def remove_vertex(self, vertex: str) -> bool:
354
+ """Remove a vertex and end all its edges."""
355
+ if vertex not in self._vertices:
356
+ return False
357
+
358
+ current_time = time.time()
359
+
360
+ # End all edges involving this vertex
361
+ for edge_id in list(self._outgoing.get(vertex, set())):
362
+ edge = self._edges.get(edge_id)
363
+ if edge and edge.interval.end is None:
364
+ edge.interval = TimeInterval(edge.interval.start, current_time)
365
+ self._add_time_event(current_time, edge_id, 'end')
366
+
367
+ # End incoming edges
368
+ if self.is_directed and self._incoming is not None:
369
+ for edge_id in list(self._incoming.get(vertex, set())):
370
+ edge = self._edges.get(edge_id)
371
+ if edge and edge.interval.end is None:
372
+ edge.interval = TimeInterval(edge.interval.start, current_time)
373
+ self._add_time_event(current_time, edge_id, 'end')
374
+
375
+ # Clear adjacency lists
376
+ self._outgoing[vertex].clear()
377
+ if self._incoming is not None:
378
+ self._incoming[vertex].clear()
379
+
380
+ self._vertices.remove(vertex)
381
+ self._current_time_cache = None
382
+
383
+ return True
384
+
385
+ # ============================================================================
386
+ # TEMPORAL-SPECIFIC OPERATIONS
387
+ # ============================================================================
388
+
389
+ def get_edge_history(self, source: str, target: str) -> List[Dict[str, Any]]:
390
+ """Get complete temporal history of edges between two vertices."""
391
+ history = []
392
+
393
+ for edge in self._edges.values():
394
+ if edge.source == source and edge.target == target:
395
+ history.append(edge.to_dict())
396
+
397
+ # Sort by start time
398
+ history.sort(key=lambda x: x['start_time'])
399
+ return history
400
+
401
+ def get_graph_at_time(self, timestamp: float) -> Dict[str, Any]:
402
+ """Get complete graph state at specific timestamp."""
403
+ active_edges = self._get_active_edges_at(timestamp)
404
+
405
+ graph_state = {
406
+ 'timestamp': timestamp,
407
+ 'vertices': list(self._vertices),
408
+ 'edges': []
409
+ }
410
+
411
+ for edge_id in active_edges:
412
+ edge = self._edges[edge_id]
413
+ graph_state['edges'].append(edge.to_dict())
414
+
415
+ return graph_state
416
+
417
+ def get_time_range_edges(self, start_time: float, end_time: float) -> List[Dict[str, Any]]:
418
+ """Get all edges that were active during the time range."""
419
+ result = []
420
+
421
+ for edge in self._edges.values():
422
+ # Check if edge interval overlaps with query range
423
+ edge_end = edge.interval.end if edge.interval.end is not None else float('inf')
424
+
425
+ if edge.interval.start <= end_time and edge_end >= start_time:
426
+ result.append(edge.to_dict())
427
+
428
+ return result
429
+
430
+ def get_temporal_path(self, source: str, target: str,
431
+ start_time: float, max_duration: float) -> Optional[List[str]]:
432
+ """Find temporal path respecting edge timing constraints."""
433
+ # Simple temporal BFS
434
+ queue = [(source, start_time, [source])]
435
+ visited = set()
436
+
437
+ while queue:
438
+ current_vertex, current_time, path = queue.pop(0)
439
+
440
+ if current_vertex == target:
441
+ return path
442
+
443
+ state_key = (current_vertex, int(current_time / self.time_precision))
444
+ if state_key in visited:
445
+ continue
446
+ visited.add(state_key)
447
+
448
+ # Explore neighbors at current time
449
+ for neighbor in self.neighbors(current_vertex, 'out', current_time):
450
+ if neighbor not in path: # Avoid cycles
451
+ new_time = current_time + self.time_precision # Move forward in time
452
+ new_path = path + [neighbor]
453
+
454
+ if new_time - start_time <= max_duration:
455
+ queue.append((neighbor, new_time, new_path))
456
+
457
+ return None
458
+
459
+ def compact_old_edges(self, cutoff_time: float) -> int:
460
+ """Remove edges that ended before cutoff time."""
461
+ to_remove = []
462
+
463
+ for edge_id, edge in self._edges.items():
464
+ if (edge.interval.end is not None and
465
+ edge.interval.end < cutoff_time):
466
+ to_remove.append(edge_id)
467
+
468
+ for edge_id in to_remove:
469
+ edge = self._edges[edge_id]
470
+
471
+ # Remove from adjacency lists
472
+ self._outgoing[edge.source].discard(edge_id)
473
+ if self._incoming is not None:
474
+ self._incoming[edge.target].discard(edge_id)
475
+
476
+ # Remove from time events
477
+ start_time = self._round_time(edge.interval.start)
478
+ if edge.interval.end:
479
+ end_time = self._round_time(edge.interval.end)
480
+ if end_time in self._edges_by_time:
481
+ self._edges_by_time[end_time] = [
482
+ (eid, event) for eid, event in self._edges_by_time[end_time]
483
+ if eid != edge_id
484
+ ]
485
+
486
+ # Remove edge
487
+ del self._edges[edge_id]
488
+
489
+ # Clean up empty timestamps
490
+ empty_timestamps = [ts for ts, events in self._edges_by_time.items() if not events]
491
+ for ts in empty_timestamps:
492
+ del self._edges_by_time[ts]
493
+ self._sorted_timestamps.remove(ts)
494
+
495
+ self._current_time_cache = None
496
+ return len(to_remove)
497
+
498
+ def get_temporal_statistics(self) -> Dict[str, Any]:
499
+ """Get statistics about temporal aspects."""
500
+ current_time = time.time()
501
+ active_count = len(self._get_active_edges_at(current_time))
502
+ total_count = len(self._edges)
503
+
504
+ # Calculate average edge duration
505
+ durations = []
506
+ for edge in self._edges.values():
507
+ if edge.interval.end is not None:
508
+ durations.append(edge.interval.end - edge.interval.start)
509
+
510
+ avg_duration = sum(durations) / len(durations) if durations else 0
511
+
512
+ return {
513
+ 'total_edges': total_count,
514
+ 'active_edges': active_count,
515
+ 'ended_edges': total_count - active_count,
516
+ 'time_events': sum(len(events) for events in self._edges_by_time.values()),
517
+ 'unique_timestamps': len(self._sorted_timestamps),
518
+ 'avg_edge_duration': avg_duration,
519
+ 'oldest_edge': min((e.interval.start for e in self._edges.values()), default=current_time),
520
+ 'newest_edge': max((e.interval.start for e in self._edges.values()), default=current_time)
521
+ }
522
+
523
+ # ============================================================================
524
+ # PERFORMANCE CHARACTERISTICS
525
+ # ============================================================================
526
+
527
+ @property
528
+ def backend_info(self) -> Dict[str, Any]:
529
+ """Get backend implementation info."""
530
+ return {
531
+ 'strategy': 'TEMPORAL_EDGESET',
532
+ 'backend': 'Time-indexed edge sets with binary search',
533
+ 'directed': self.is_directed,
534
+ 'time_precision': self.time_precision,
535
+ 'default_duration': self.default_duration,
536
+ 'complexity': {
537
+ 'add_edge': 'O(log T)', # T = number of timestamps
538
+ 'remove_edge': 'O(log T)',
539
+ 'has_edge': 'O(T + E)', # Worst case
540
+ 'temporal_query': 'O(log T + E_active)',
541
+ 'space': 'O(V + E + T)'
542
+ }
543
+ }
544
+
545
+ @property
546
+ def metrics(self) -> Dict[str, Any]:
547
+ """Get performance metrics."""
548
+ stats = self.get_temporal_statistics()
549
+
550
+ return {
551
+ 'vertices': len(self._vertices),
552
+ 'total_edges': stats['total_edges'],
553
+ 'active_edges': stats['active_edges'],
554
+ 'time_events': stats['time_events'],
555
+ 'timestamps': len(self._sorted_timestamps),
556
+ 'memory_usage': f"{stats['total_edges'] * 100 + stats['time_events'] * 20} bytes (estimated)",
557
+ 'cache_hit_rate': 'N/A' # Would track in real implementation
558
+ }