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.
- exonware/__init__.py +14 -0
- exonware/xwnode/__init__.py +127 -0
- exonware/xwnode/base.py +676 -0
- exonware/xwnode/config.py +178 -0
- exonware/xwnode/contracts.py +730 -0
- exonware/xwnode/errors.py +503 -0
- exonware/xwnode/facade.py +460 -0
- exonware/xwnode/strategies/__init__.py +158 -0
- exonware/xwnode/strategies/advisor.py +463 -0
- exonware/xwnode/strategies/edges/__init__.py +32 -0
- exonware/xwnode/strategies/edges/adj_list.py +227 -0
- exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
- exonware/xwnode/strategies/edges/base.py +169 -0
- exonware/xwnode/strategies/flyweight.py +328 -0
- exonware/xwnode/strategies/impls/__init__.py +13 -0
- exonware/xwnode/strategies/impls/_base_edge.py +403 -0
- exonware/xwnode/strategies/impls/_base_node.py +307 -0
- exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
- exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
- exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
- exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
- exonware/xwnode/strategies/impls/edge_coo.py +533 -0
- exonware/xwnode/strategies/impls/edge_csc.py +447 -0
- exonware/xwnode/strategies/impls/edge_csr.py +492 -0
- exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
- exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
- exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
- exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
- exonware/xwnode/strategies/impls/edge_octree.py +574 -0
- exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
- exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
- exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
- exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
- exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
- exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
- exonware/xwnode/strategies/manager.py +775 -0
- exonware/xwnode/strategies/metrics.py +538 -0
- exonware/xwnode/strategies/migration.py +432 -0
- exonware/xwnode/strategies/nodes/__init__.py +50 -0
- exonware/xwnode/strategies/nodes/_base_node.py +307 -0
- exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
- exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
- exonware/xwnode/strategies/nodes/array_list.py +209 -0
- exonware/xwnode/strategies/nodes/base.py +247 -0
- exonware/xwnode/strategies/nodes/deque.py +200 -0
- exonware/xwnode/strategies/nodes/hash_map.py +135 -0
- exonware/xwnode/strategies/nodes/heap.py +307 -0
- exonware/xwnode/strategies/nodes/linked_list.py +232 -0
- exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
- exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
- exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
- exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
- exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
- exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
- exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
- exonware/xwnode/strategies/nodes/node_btree.py +357 -0
- exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
- exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
- exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
- exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
- exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
- exonware/xwnode/strategies/nodes/node_heap.py +191 -0
- exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
- exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
- exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
- exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
- exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
- exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
- exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
- exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
- exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
- exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
- exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
- exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
- exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
- exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
- exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
- exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
- exonware/xwnode/strategies/nodes/node_treap.py +387 -0
- exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
- exonware/xwnode/strategies/nodes/node_trie.py +252 -0
- exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
- exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
- exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
- exonware/xwnode/strategies/nodes/queue.py +161 -0
- exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
- exonware/xwnode/strategies/nodes/stack.py +152 -0
- exonware/xwnode/strategies/nodes/trie.py +274 -0
- exonware/xwnode/strategies/nodes/union_find.py +283 -0
- exonware/xwnode/strategies/pattern_detector.py +603 -0
- exonware/xwnode/strategies/performance_monitor.py +487 -0
- exonware/xwnode/strategies/queries/__init__.py +24 -0
- exonware/xwnode/strategies/queries/base.py +236 -0
- exonware/xwnode/strategies/queries/cql.py +201 -0
- exonware/xwnode/strategies/queries/cypher.py +181 -0
- exonware/xwnode/strategies/queries/datalog.py +70 -0
- exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
- exonware/xwnode/strategies/queries/eql.py +70 -0
- exonware/xwnode/strategies/queries/flux.py +70 -0
- exonware/xwnode/strategies/queries/gql.py +70 -0
- exonware/xwnode/strategies/queries/graphql.py +240 -0
- exonware/xwnode/strategies/queries/gremlin.py +181 -0
- exonware/xwnode/strategies/queries/hiveql.py +214 -0
- exonware/xwnode/strategies/queries/hql.py +70 -0
- exonware/xwnode/strategies/queries/jmespath.py +219 -0
- exonware/xwnode/strategies/queries/jq.py +66 -0
- exonware/xwnode/strategies/queries/json_query.py +66 -0
- exonware/xwnode/strategies/queries/jsoniq.py +248 -0
- exonware/xwnode/strategies/queries/kql.py +70 -0
- exonware/xwnode/strategies/queries/linq.py +238 -0
- exonware/xwnode/strategies/queries/logql.py +70 -0
- exonware/xwnode/strategies/queries/mql.py +68 -0
- exonware/xwnode/strategies/queries/n1ql.py +210 -0
- exonware/xwnode/strategies/queries/partiql.py +70 -0
- exonware/xwnode/strategies/queries/pig.py +215 -0
- exonware/xwnode/strategies/queries/promql.py +70 -0
- exonware/xwnode/strategies/queries/sparql.py +220 -0
- exonware/xwnode/strategies/queries/sql.py +275 -0
- exonware/xwnode/strategies/queries/xml_query.py +66 -0
- exonware/xwnode/strategies/queries/xpath.py +223 -0
- exonware/xwnode/strategies/queries/xquery.py +258 -0
- exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
- exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
- exonware/xwnode/strategies/registry.py +604 -0
- exonware/xwnode/strategies/simple.py +273 -0
- exonware/xwnode/strategies/utils.py +532 -0
- exonware/xwnode/types.py +912 -0
- exonware/xwnode/version.py +78 -0
- exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
- exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
- exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
- 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
|
+
}
|