exonware-xwnode 0.0.1.22__py3-none-any.whl → 0.0.1.23__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 +1 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/common/__init__.py +1 -1
- exonware/xwnode/common/graph/__init__.py +30 -0
- exonware/xwnode/common/graph/caching.py +131 -0
- exonware/xwnode/common/graph/contracts.py +100 -0
- exonware/xwnode/common/graph/errors.py +44 -0
- exonware/xwnode/common/graph/indexing.py +260 -0
- exonware/xwnode/common/graph/manager.py +568 -0
- exonware/xwnode/common/management/__init__.py +3 -5
- exonware/xwnode/common/management/manager.py +2 -2
- exonware/xwnode/common/management/migration.py +3 -3
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +6 -2
- exonware/xwnode/common/monitoring/pattern_detector.py +1 -1
- exonware/xwnode/common/monitoring/performance_monitor.py +5 -1
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/flyweight.py +5 -1
- exonware/xwnode/common/patterns/registry.py +202 -183
- exonware/xwnode/common/utils/__init__.py +25 -11
- exonware/xwnode/common/utils/simple.py +1 -1
- exonware/xwnode/config.py +3 -8
- exonware/xwnode/contracts.py +4 -105
- exonware/xwnode/defs.py +413 -159
- exonware/xwnode/edges/strategies/__init__.py +86 -4
- exonware/xwnode/edges/strategies/_base_edge.py +2 -2
- exonware/xwnode/edges/strategies/adj_list.py +287 -121
- exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
- exonware/xwnode/edges/strategies/base.py +1 -1
- exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
- exonware/xwnode/edges/strategies/bitemporal.py +520 -0
- exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
- exonware/xwnode/edges/strategies/bv_graph.py +664 -0
- exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
- exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
- exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
- exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
- exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
- exonware/xwnode/edges/strategies/edge_list.py +168 -0
- exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
- exonware/xwnode/edges/strategies/euler_tour.py +560 -0
- exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
- exonware/xwnode/edges/strategies/graphblas.py +449 -0
- exonware/xwnode/edges/strategies/hnsw.py +637 -0
- exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
- exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
- exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
- exonware/xwnode/edges/strategies/k2_tree.py +613 -0
- exonware/xwnode/edges/strategies/link_cut.py +626 -0
- exonware/xwnode/edges/strategies/multiplex.py +532 -0
- exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
- exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
- exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
- exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
- exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
- exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
- exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
- exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
- exonware/xwnode/errors.py +3 -6
- exonware/xwnode/facade.py +20 -20
- exonware/xwnode/nodes/strategies/__init__.py +29 -9
- exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
- exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
- exonware/xwnode/nodes/strategies/array_list.py +36 -3
- exonware/xwnode/nodes/strategies/art.py +581 -0
- exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
- exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
- exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
- exonware/xwnode/nodes/strategies/base.py +469 -98
- exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
- exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
- exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
- exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
- exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
- exonware/xwnode/nodes/strategies/contracts.py +1 -1
- exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
- exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
- exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
- exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
- exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
- exonware/xwnode/nodes/strategies/dawg.py +876 -0
- exonware/xwnode/nodes/strategies/deque.py +321 -153
- exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
- exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
- exonware/xwnode/nodes/strategies/hamt.py +403 -0
- exonware/xwnode/nodes/strategies/hash_map.py +354 -67
- exonware/xwnode/nodes/strategies/heap.py +105 -5
- exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
- exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
- exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
- exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
- exonware/xwnode/nodes/strategies/learned_index.py +533 -0
- exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
- exonware/xwnode/nodes/strategies/linked_list.py +316 -119
- exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
- exonware/xwnode/nodes/strategies/masstree.py +130 -0
- exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
- exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
- exonware/xwnode/nodes/strategies/queue.py +249 -120
- exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
- exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
- exonware/xwnode/nodes/strategies/rope.py +717 -0
- exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
- exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
- exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
- exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
- exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
- exonware/xwnode/nodes/strategies/stack.py +244 -112
- exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
- exonware/xwnode/nodes/strategies/t_tree.py +94 -0
- exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
- exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
- exonware/xwnode/nodes/strategies/trie.py +153 -9
- exonware/xwnode/nodes/strategies/union_find.py +111 -5
- exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
- exonware/xwnode/strategies/__init__.py +5 -51
- exonware/xwnode/version.py +3 -3
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
- exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
- exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
- exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
- exonware/xwnode/nodes/strategies/_base_node.py +0 -307
- exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
- exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
- exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
- exonware/xwnode/nodes/strategies/node_heap.py +0 -196
- exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
- exonware/xwnode/nodes/strategies/node_trie.py +0 -257
- exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
- exonware/xwnode/queries/executors/__init__.py +0 -47
- exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
- exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
- exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
- exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
- exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
- exonware/xwnode/queries/executors/array/__init__.py +0 -9
- exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
- exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
- exonware/xwnode/queries/executors/base.py +0 -257
- exonware/xwnode/queries/executors/capability_checker.py +0 -204
- exonware/xwnode/queries/executors/contracts.py +0 -166
- exonware/xwnode/queries/executors/core/__init__.py +0 -17
- exonware/xwnode/queries/executors/core/create_executor.py +0 -96
- exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
- exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
- exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
- exonware/xwnode/queries/executors/core/select_executor.py +0 -152
- exonware/xwnode/queries/executors/core/update_executor.py +0 -102
- exonware/xwnode/queries/executors/data/__init__.py +0 -13
- exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
- exonware/xwnode/queries/executors/data/load_executor.py +0 -50
- exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
- exonware/xwnode/queries/executors/data/store_executor.py +0 -50
- exonware/xwnode/queries/executors/defs.py +0 -93
- exonware/xwnode/queries/executors/engine.py +0 -221
- exonware/xwnode/queries/executors/errors.py +0 -68
- exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
- exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
- exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
- exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
- exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
- exonware/xwnode/queries/executors/graph/__init__.py +0 -15
- exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
- exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
- exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
- exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
- exonware/xwnode/queries/executors/projection/__init__.py +0 -9
- exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
- exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
- exonware/xwnode/queries/executors/registry.py +0 -173
- exonware/xwnode/queries/parsers/__init__.py +0 -26
- exonware/xwnode/queries/parsers/base.py +0 -86
- exonware/xwnode/queries/parsers/contracts.py +0 -46
- exonware/xwnode/queries/parsers/errors.py +0 -53
- exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
- exonware/xwnode/queries/strategies/__init__.py +0 -24
- exonware/xwnode/queries/strategies/base.py +0 -236
- exonware/xwnode/queries/strategies/cql.py +0 -201
- exonware/xwnode/queries/strategies/cypher.py +0 -181
- exonware/xwnode/queries/strategies/datalog.py +0 -70
- exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
- exonware/xwnode/queries/strategies/eql.py +0 -70
- exonware/xwnode/queries/strategies/flux.py +0 -70
- exonware/xwnode/queries/strategies/gql.py +0 -70
- exonware/xwnode/queries/strategies/graphql.py +0 -240
- exonware/xwnode/queries/strategies/gremlin.py +0 -181
- exonware/xwnode/queries/strategies/hiveql.py +0 -214
- exonware/xwnode/queries/strategies/hql.py +0 -70
- exonware/xwnode/queries/strategies/jmespath.py +0 -219
- exonware/xwnode/queries/strategies/jq.py +0 -66
- exonware/xwnode/queries/strategies/json_query.py +0 -66
- exonware/xwnode/queries/strategies/jsoniq.py +0 -248
- exonware/xwnode/queries/strategies/kql.py +0 -70
- exonware/xwnode/queries/strategies/linq.py +0 -238
- exonware/xwnode/queries/strategies/logql.py +0 -70
- exonware/xwnode/queries/strategies/mql.py +0 -68
- exonware/xwnode/queries/strategies/n1ql.py +0 -210
- exonware/xwnode/queries/strategies/partiql.py +0 -70
- exonware/xwnode/queries/strategies/pig.py +0 -215
- exonware/xwnode/queries/strategies/promql.py +0 -70
- exonware/xwnode/queries/strategies/sparql.py +0 -220
- exonware/xwnode/queries/strategies/sql.py +0 -275
- exonware/xwnode/queries/strategies/xml_query.py +0 -66
- exonware/xwnode/queries/strategies/xpath.py +0 -223
- exonware/xwnode/queries/strategies/xquery.py +0 -258
- exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
- exonware/xwnode/queries/strategies/xwquery.py +0 -456
- exonware_xwnode-0.0.1.22.dist-info/RECORD +0 -214
- /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -6,23 +6,61 @@ with O(1) edge operations and efficient matrix-based algorithms.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, Union
|
9
|
-
from .
|
9
|
+
from ._base_edge import AEdgeStrategy
|
10
10
|
from ...defs import EdgeMode, EdgeTrait
|
11
11
|
|
12
12
|
|
13
|
-
class AdjMatrixStrategy(
|
13
|
+
class AdjMatrixStrategy(AEdgeStrategy):
|
14
14
|
"""
|
15
15
|
Adjacency Matrix edge strategy for dense graph representation.
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
WHY this strategy:
|
18
|
+
- O(1) edge lookup - fastest possible for connectivity checks
|
19
|
+
- Matrix multiplication enables powerful graph algorithms (transitive closure, shortest paths)
|
20
|
+
- Cache-friendly for dense graphs (sequential memory access)
|
21
|
+
- Simplest implementation for complete/near-complete graphs
|
22
|
+
|
23
|
+
WHY this implementation:
|
24
|
+
- 2D list-of-lists for dynamic resizing
|
25
|
+
- Auto-expanding capacity for growing graphs
|
26
|
+
- Direct indexing [i][j] provides O(1) access
|
27
|
+
- Stores full edge data (not just boolean) for weighted graphs
|
28
|
+
|
29
|
+
Time Complexity:
|
30
|
+
- Add Edge: O(1) - direct array assignment
|
31
|
+
- Has Edge: O(1) - direct array lookup
|
32
|
+
- Get Neighbors: O(V) - scan entire row
|
33
|
+
- Delete Edge: O(1) - direct array assignment
|
34
|
+
|
35
|
+
Space Complexity: O(V²) - stores all possible edges
|
36
|
+
|
37
|
+
Trade-offs:
|
38
|
+
- Advantage: Fastest edge lookups, simplest for dense graphs
|
39
|
+
- Limitation: Wastes memory on sparse graphs (stores empty cells)
|
40
|
+
- Compared to ADJ_LIST: Use when graph density > 50%
|
41
|
+
|
42
|
+
Best for:
|
43
|
+
- Small complete graphs (cliques, fully connected layers)
|
44
|
+
- Dense graphs (social groups, recommendation systems)
|
45
|
+
- Matrix-based algorithms (Floyd-Warshall, matrix powers)
|
46
|
+
- Graphs where |E| ≈ |V|² (not |E| ≈ |V|)
|
47
|
+
|
48
|
+
Not recommended for:
|
49
|
+
- Sparse graphs (<10% density) - use ADJ_LIST
|
50
|
+
- Very large graphs (>10K vertices) - O(V²) memory prohibitive
|
51
|
+
- Highly dynamic graphs - wasted space on deleted edges
|
52
|
+
|
53
|
+
Following eXonware Priorities:
|
54
|
+
1. Security: Bounds checking on all array accesses
|
55
|
+
2. Usability: Intuitive matrix[i][j] access pattern
|
56
|
+
3. Maintainability: Simple 2D array, minimal logic
|
57
|
+
4. Performance: O(1) operations, cache-friendly for dense
|
58
|
+
5. Extensibility: Easy to add matrix operations, algorithms
|
19
59
|
"""
|
20
60
|
|
21
61
|
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
22
62
|
"""Initialize the Adjacency Matrix strategy."""
|
23
|
-
super().__init__(**options)
|
24
|
-
self._mode = EdgeMode.ADJ_MATRIX
|
25
|
-
self._traits = traits
|
63
|
+
super().__init__(EdgeMode.ADJ_MATRIX, traits, **options)
|
26
64
|
|
27
65
|
self.is_directed = options.get('directed', True)
|
28
66
|
self.initial_capacity = options.get('initial_capacity', 100)
|
@@ -52,107 +90,110 @@ class AdjMatrixStrategy(AGraphEdgeStrategy):
|
|
52
90
|
# ============================================================================
|
53
91
|
|
54
92
|
def _resize_matrix(self, new_capacity: int) -> None:
|
55
|
-
"""Resize the adjacency matrix to
|
93
|
+
"""Resize the adjacency matrix to accommodate more vertices."""
|
56
94
|
old_capacity = self._capacity
|
57
95
|
self._capacity = new_capacity
|
58
96
|
|
59
|
-
#
|
60
|
-
|
97
|
+
# Expand existing rows
|
98
|
+
for row in self._matrix:
|
99
|
+
row.extend([None] * (new_capacity - old_capacity))
|
61
100
|
|
62
|
-
#
|
63
|
-
for
|
64
|
-
|
65
|
-
new_matrix[i][j] = self._matrix[i][j]
|
66
|
-
|
67
|
-
self._matrix = new_matrix
|
101
|
+
# Add new rows
|
102
|
+
for _ in range(old_capacity, new_capacity):
|
103
|
+
self._matrix.append([None] * new_capacity)
|
68
104
|
|
69
105
|
def _get_vertex_index(self, vertex: str) -> int:
|
70
106
|
"""Get or create index for vertex."""
|
71
|
-
if vertex
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
self.
|
77
|
-
|
78
|
-
|
79
|
-
|
107
|
+
if vertex in self._vertex_to_index:
|
108
|
+
return self._vertex_to_index[vertex]
|
109
|
+
|
110
|
+
# Need to add new vertex
|
111
|
+
if self._vertex_count >= self._capacity:
|
112
|
+
self._resize_matrix(self._capacity * 2)
|
113
|
+
|
114
|
+
index = self._vertex_count
|
115
|
+
self._vertex_to_index[vertex] = index
|
116
|
+
self._index_to_vertex[index] = vertex
|
117
|
+
self._vertex_count += 1
|
80
118
|
|
81
|
-
return
|
119
|
+
return index
|
82
120
|
|
83
121
|
def _get_vertex_by_index(self, index: int) -> Optional[str]:
|
84
|
-
"""Get vertex by index."""
|
122
|
+
"""Get vertex name by index."""
|
85
123
|
return self._index_to_vertex.get(index)
|
86
124
|
|
87
125
|
# ============================================================================
|
88
126
|
# CORE EDGE OPERATIONS
|
89
127
|
# ============================================================================
|
90
128
|
|
91
|
-
def add_edge(self,
|
129
|
+
def add_edge(self, source: str, target: str, **properties) -> str:
|
92
130
|
"""Add an edge between source and target vertices."""
|
93
|
-
|
94
|
-
target
|
95
|
-
|
96
|
-
# Validation
|
97
|
-
if not self.allow_self_loops and source == target:
|
98
|
-
raise ValueError("Self loops not allowed")
|
131
|
+
# Validate self-loops
|
132
|
+
if source == target and not self.allow_self_loops:
|
133
|
+
raise ValueError(f"Self-loops not allowed: {source} -> {target}")
|
99
134
|
|
100
135
|
# Get vertex indices
|
101
136
|
source_idx = self._get_vertex_index(source)
|
102
137
|
target_idx = self._get_vertex_index(target)
|
103
138
|
|
139
|
+
# Generate edge ID
|
140
|
+
edge_id = f"edge_{self._edge_id_counter}"
|
141
|
+
self._edge_id_counter += 1
|
142
|
+
|
104
143
|
# Create edge data
|
105
144
|
edge_data = {
|
106
|
-
'
|
107
|
-
'
|
108
|
-
|
145
|
+
'id': edge_id,
|
146
|
+
'source': source,
|
147
|
+
'target': target,
|
148
|
+
'weight': properties.get('weight', self.default_weight),
|
149
|
+
'properties': properties.copy()
|
109
150
|
}
|
110
|
-
self._edge_id_counter += 1
|
111
151
|
|
112
|
-
#
|
113
|
-
if self._matrix[source_idx][target_idx] is None:
|
152
|
+
# Check if edge already exists
|
153
|
+
if self._matrix[source_idx][target_idx] is not None:
|
154
|
+
# Update existing edge
|
155
|
+
self._matrix[source_idx][target_idx] = edge_data
|
156
|
+
else:
|
157
|
+
# Add new edge
|
158
|
+
self._matrix[source_idx][target_idx] = edge_data
|
114
159
|
self._edge_count += 1
|
115
160
|
|
116
|
-
|
117
|
-
|
118
|
-
# Add reverse edge if undirected
|
161
|
+
# For undirected graphs, add symmetric edge
|
119
162
|
if not self.is_directed and source != target:
|
120
163
|
if self._matrix[target_idx][source_idx] is None:
|
121
|
-
self.
|
122
|
-
|
164
|
+
self._matrix[target_idx][source_idx] = edge_data
|
165
|
+
# Don't increment edge count for undirected (it's the same edge)
|
123
166
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
167
|
+
return edge_id
|
168
|
+
|
169
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
170
|
+
"""Remove edge between source and target."""
|
129
171
|
if source not in self._vertex_to_index or target not in self._vertex_to_index:
|
130
172
|
return False
|
131
173
|
|
132
174
|
source_idx = self._vertex_to_index[source]
|
133
175
|
target_idx = self._vertex_to_index[target]
|
134
176
|
|
135
|
-
|
177
|
+
# Check if edge exists
|
178
|
+
if self._matrix[source_idx][target_idx] is None:
|
179
|
+
return False
|
180
|
+
|
181
|
+
# If edge_id specified, verify it matches
|
182
|
+
if edge_id and self._matrix[source_idx][target_idx]['id'] != edge_id:
|
183
|
+
return False
|
136
184
|
|
137
|
-
# Remove edge
|
138
|
-
|
139
|
-
|
140
|
-
self._edge_count -= 1
|
141
|
-
removed = True
|
185
|
+
# Remove edge
|
186
|
+
self._matrix[source_idx][target_idx] = None
|
187
|
+
self._edge_count -= 1
|
142
188
|
|
143
|
-
#
|
189
|
+
# For undirected graphs, remove symmetric edge
|
144
190
|
if not self.is_directed and source != target:
|
145
|
-
|
146
|
-
self._matrix[target_idx][source_idx] = None
|
147
|
-
self._edge_count -= 1
|
191
|
+
self._matrix[target_idx][source_idx] = None
|
148
192
|
|
149
|
-
return
|
193
|
+
return True
|
150
194
|
|
151
|
-
def has_edge(self,
|
152
|
-
"""Check if edge exists."""
|
153
|
-
source = str(from_node)
|
154
|
-
target = str(to_node)
|
155
|
-
|
195
|
+
def has_edge(self, source: str, target: str) -> bool:
|
196
|
+
"""Check if edge exists between source and target."""
|
156
197
|
if source not in self._vertex_to_index or target not in self._vertex_to_index:
|
157
198
|
return False
|
158
199
|
|
@@ -161,204 +202,249 @@ class AdjMatrixStrategy(AGraphEdgeStrategy):
|
|
161
202
|
|
162
203
|
return self._matrix[source_idx][target_idx] is not None
|
163
204
|
|
164
|
-
def
|
165
|
-
"""Get
|
166
|
-
return self._edge_count
|
167
|
-
|
168
|
-
def get_vertex_count(self) -> int:
|
169
|
-
"""Get total number of vertices."""
|
170
|
-
return self._vertex_count
|
171
|
-
|
172
|
-
# ============================================================================
|
173
|
-
# GRAPH EDGE STRATEGY METHODS
|
174
|
-
# ============================================================================
|
175
|
-
|
176
|
-
def get_neighbors(self, node: Any) -> List[Any]:
|
177
|
-
"""Get all neighboring nodes."""
|
178
|
-
vertex = str(node)
|
179
|
-
if vertex not in self._vertex_to_index:
|
180
|
-
return []
|
181
|
-
|
182
|
-
vertex_idx = self._vertex_to_index[vertex]
|
183
|
-
neighbors = []
|
184
|
-
|
185
|
-
for i in range(self._capacity):
|
186
|
-
if self._matrix[vertex_idx][i] is not None:
|
187
|
-
neighbor = self._index_to_vertex.get(i)
|
188
|
-
if neighbor:
|
189
|
-
neighbors.append(neighbor)
|
190
|
-
|
191
|
-
return neighbors
|
192
|
-
|
193
|
-
def get_edge_weight(self, from_node: Any, to_node: Any) -> float:
|
194
|
-
"""Get edge weight."""
|
195
|
-
source = str(from_node)
|
196
|
-
target = str(to_node)
|
197
|
-
|
205
|
+
def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
|
206
|
+
"""Get edge data between source and target."""
|
198
207
|
if source not in self._vertex_to_index or target not in self._vertex_to_index:
|
199
|
-
return
|
208
|
+
return None
|
200
209
|
|
201
210
|
source_idx = self._vertex_to_index[source]
|
202
211
|
target_idx = self._vertex_to_index[target]
|
203
212
|
|
204
|
-
|
205
|
-
|
213
|
+
return self._matrix[source_idx][target_idx]
|
214
|
+
|
215
|
+
def get_edge_weight(self, source: str, target: str) -> Optional[float]:
|
216
|
+
"""Get edge weight between source and target."""
|
217
|
+
edge_data = self.get_edge_data(source, target)
|
218
|
+
return edge_data['weight'] if edge_data else None
|
206
219
|
|
207
|
-
def
|
208
|
-
"""
|
209
|
-
|
210
|
-
|
220
|
+
def neighbors(self, vertex: str, direction: str = 'out') -> Iterator[str]:
|
221
|
+
"""Get neighbors of a vertex."""
|
222
|
+
if vertex not in self._vertex_to_index:
|
223
|
+
return
|
211
224
|
|
212
|
-
|
213
|
-
raise ValueError(f"Edge {source} -> {target} not found")
|
225
|
+
vertex_idx = self._vertex_to_index[vertex]
|
214
226
|
|
215
|
-
|
216
|
-
|
227
|
+
if direction == 'out':
|
228
|
+
# Outgoing neighbors (columns)
|
229
|
+
for target_idx in range(self._vertex_count):
|
230
|
+
if self._matrix[vertex_idx][target_idx] is not None:
|
231
|
+
yield self._index_to_vertex[target_idx]
|
232
|
+
elif direction == 'in':
|
233
|
+
# Incoming neighbors (rows)
|
234
|
+
for source_idx in range(self._vertex_count):
|
235
|
+
if self._matrix[source_idx][vertex_idx] is not None:
|
236
|
+
yield self._index_to_vertex[source_idx]
|
237
|
+
elif direction == 'both':
|
238
|
+
# All neighbors
|
239
|
+
seen = set()
|
240
|
+
for neighbor in self.neighbors(vertex, 'out'):
|
241
|
+
if neighbor not in seen:
|
242
|
+
seen.add(neighbor)
|
243
|
+
yield neighbor
|
244
|
+
for neighbor in self.neighbors(vertex, 'in'):
|
245
|
+
if neighbor not in seen:
|
246
|
+
seen.add(neighbor)
|
247
|
+
yield neighbor
|
248
|
+
|
249
|
+
def degree(self, vertex: str, direction: str = 'out') -> int:
|
250
|
+
"""Get degree of a vertex."""
|
251
|
+
if vertex not in self._vertex_to_index:
|
252
|
+
return 0
|
217
253
|
|
218
|
-
|
219
|
-
raise ValueError(f"Edge {source} -> {target} not found")
|
254
|
+
vertex_idx = self._vertex_to_index[vertex]
|
220
255
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
256
|
+
if direction == 'out':
|
257
|
+
# Count non-None entries in row
|
258
|
+
return sum(1 for target_idx in range(self._vertex_count)
|
259
|
+
if self._matrix[vertex_idx][target_idx] is not None)
|
260
|
+
elif direction == 'in':
|
261
|
+
# Count non-None entries in column
|
262
|
+
return sum(1 for source_idx in range(self._vertex_count)
|
263
|
+
if self._matrix[source_idx][vertex_idx] is not None)
|
264
|
+
elif direction == 'both':
|
265
|
+
out_degree = self.degree(vertex, 'out')
|
266
|
+
in_degree = self.degree(vertex, 'in')
|
267
|
+
# For undirected graphs, avoid double counting
|
268
|
+
return out_degree if not self.is_directed else out_degree + in_degree
|
269
|
+
|
270
|
+
def edges(self, data: bool = False) -> Iterator[tuple]:
|
271
|
+
"""Get all edges in the graph."""
|
272
|
+
for source_idx in range(self._vertex_count):
|
273
|
+
for target_idx in range(self._vertex_count):
|
274
|
+
edge_data = self._matrix[source_idx][target_idx]
|
275
|
+
if edge_data is not None:
|
276
|
+
source = self._index_to_vertex[source_idx]
|
277
|
+
target = self._index_to_vertex[target_idx]
|
278
|
+
|
279
|
+
# For undirected graphs, avoid returning duplicate edges
|
280
|
+
if not self.is_directed and source > target:
|
281
|
+
continue
|
282
|
+
|
283
|
+
if data:
|
284
|
+
yield (source, target, edge_data)
|
285
|
+
else:
|
286
|
+
yield (source, target)
|
287
|
+
|
288
|
+
def vertices(self) -> Iterator[str]:
|
289
|
+
"""Get all vertices in the graph."""
|
290
|
+
for vertex in self._vertex_to_index.keys():
|
291
|
+
yield vertex
|
227
292
|
|
228
|
-
def
|
229
|
-
"""
|
230
|
-
|
231
|
-
return []
|
293
|
+
def __len__(self) -> int:
|
294
|
+
"""Get the number of edges."""
|
295
|
+
return self._edge_count
|
232
296
|
|
233
|
-
def
|
234
|
-
"""Get
|
235
|
-
|
236
|
-
return []
|
297
|
+
def vertex_count(self) -> int:
|
298
|
+
"""Get the number of vertices."""
|
299
|
+
return self._vertex_count
|
237
300
|
|
238
|
-
def
|
239
|
-
"""
|
240
|
-
#
|
241
|
-
|
301
|
+
def clear(self) -> None:
|
302
|
+
"""Clear all edges and vertices."""
|
303
|
+
# Reset matrix
|
304
|
+
for row in self._matrix:
|
305
|
+
for i in range(len(row)):
|
306
|
+
row[i] = None
|
307
|
+
|
308
|
+
# Reset mappings
|
309
|
+
self._vertex_to_index.clear()
|
310
|
+
self._index_to_vertex.clear()
|
311
|
+
self._vertex_count = 0
|
312
|
+
self._edge_count = 0
|
313
|
+
self._edge_id_counter = 0
|
242
314
|
|
243
|
-
def
|
244
|
-
"""
|
245
|
-
|
315
|
+
def add_vertex(self, vertex: str) -> None:
|
316
|
+
"""Add a vertex to the graph."""
|
317
|
+
if vertex not in self._vertex_to_index:
|
318
|
+
self._get_vertex_index(vertex)
|
246
319
|
|
247
|
-
def
|
248
|
-
"""
|
249
|
-
|
250
|
-
|
320
|
+
def remove_vertex(self, vertex: str) -> bool:
|
321
|
+
"""Remove a vertex and all its edges."""
|
322
|
+
if vertex not in self._vertex_to_index:
|
323
|
+
return False
|
324
|
+
|
325
|
+
vertex_idx = self._vertex_to_index[vertex]
|
326
|
+
|
327
|
+
# Count and remove all edges involving this vertex
|
328
|
+
edges_removed = 0
|
329
|
+
|
330
|
+
# Remove outgoing edges (row)
|
331
|
+
for target_idx in range(self._vertex_count):
|
332
|
+
if self._matrix[vertex_idx][target_idx] is not None:
|
333
|
+
self._matrix[vertex_idx][target_idx] = None
|
334
|
+
edges_removed += 1
|
335
|
+
|
336
|
+
# Remove incoming edges (column)
|
337
|
+
for source_idx in range(self._vertex_count):
|
338
|
+
if source_idx != vertex_idx and self._matrix[source_idx][vertex_idx] is not None:
|
339
|
+
self._matrix[source_idx][vertex_idx] = None
|
340
|
+
edges_removed += 1
|
341
|
+
|
342
|
+
self._edge_count -= edges_removed
|
343
|
+
|
344
|
+
# Note: We don't actually remove the vertex from the matrix to avoid
|
345
|
+
# reindexing all other vertices. Instead, we just mark it as removed.
|
346
|
+
del self._vertex_to_index[vertex]
|
347
|
+
del self._index_to_vertex[vertex_idx]
|
348
|
+
|
349
|
+
return True
|
251
350
|
|
252
351
|
# ============================================================================
|
253
|
-
# MATRIX
|
352
|
+
# MATRIX-SPECIFIC OPERATIONS
|
254
353
|
# ============================================================================
|
255
354
|
|
256
355
|
def get_matrix(self) -> List[List[Optional[float]]]:
|
257
|
-
"""Get the adjacency matrix as
|
356
|
+
"""Get the adjacency matrix as weights."""
|
258
357
|
matrix = []
|
259
|
-
for
|
358
|
+
for source_idx in range(self._vertex_count):
|
260
359
|
row = []
|
261
|
-
for
|
262
|
-
edge_data = self._matrix[
|
263
|
-
weight = edge_data
|
360
|
+
for target_idx in range(self._vertex_count):
|
361
|
+
edge_data = self._matrix[source_idx][target_idx]
|
362
|
+
weight = edge_data['weight'] if edge_data else None
|
264
363
|
row.append(weight)
|
265
364
|
matrix.append(row)
|
266
365
|
return matrix
|
267
366
|
|
268
367
|
def get_binary_matrix(self) -> List[List[int]]:
|
269
|
-
"""Get the
|
368
|
+
"""Get the adjacency matrix as binary (0/1)."""
|
270
369
|
matrix = []
|
271
|
-
for
|
370
|
+
for source_idx in range(self._vertex_count):
|
272
371
|
row = []
|
273
|
-
for
|
274
|
-
|
275
|
-
row.append(
|
372
|
+
for target_idx in range(self._vertex_count):
|
373
|
+
edge_exists = self._matrix[source_idx][target_idx] is not None
|
374
|
+
row.append(1 if edge_exists else 0)
|
276
375
|
matrix.append(row)
|
277
376
|
return matrix
|
278
377
|
|
279
378
|
def set_matrix(self, matrix: List[List[Union[float, int, None]]], vertices: List[str]) -> None:
|
280
|
-
"""Set the
|
379
|
+
"""Set the entire matrix from a weight matrix."""
|
380
|
+
if len(matrix) != len(vertices) or any(len(row) != len(vertices) for row in matrix):
|
381
|
+
raise ValueError("Matrix dimensions must match vertex count")
|
382
|
+
|
281
383
|
# Clear existing data
|
282
384
|
self.clear()
|
283
385
|
|
284
|
-
#
|
386
|
+
# Add vertices
|
285
387
|
for vertex in vertices:
|
286
|
-
self.
|
388
|
+
self.add_vertex(vertex)
|
287
389
|
|
288
|
-
#
|
289
|
-
for i,
|
290
|
-
for j,
|
390
|
+
# Add edges based on matrix
|
391
|
+
for i, source in enumerate(vertices):
|
392
|
+
for j, target in enumerate(vertices):
|
393
|
+
weight = matrix[i][j]
|
291
394
|
if weight is not None and weight != 0:
|
292
|
-
source =
|
293
|
-
target = self._index_to_vertex[j]
|
294
|
-
self.add_edge(source, target, weight=float(weight))
|
395
|
+
self.add_edge(source, target, weight=weight)
|
295
396
|
|
296
397
|
def matrix_multiply(self, other: 'xAdjMatrixStrategy') -> 'xAdjMatrixStrategy':
|
297
398
|
"""Multiply this matrix with another adjacency matrix."""
|
298
|
-
|
299
|
-
|
399
|
+
if self._vertex_count != other._vertex_count:
|
400
|
+
raise ValueError("Matrices must have same dimensions")
|
401
|
+
|
402
|
+
result = xAdjMatrixStrategy(
|
403
|
+
traits=self._traits,
|
404
|
+
directed=self.is_directed,
|
405
|
+
initial_capacity=self._vertex_count
|
406
|
+
)
|
407
|
+
|
408
|
+
# Add vertices
|
409
|
+
for vertex in self.vertices():
|
410
|
+
result.add_vertex(vertex)
|
411
|
+
|
412
|
+
# Perform matrix multiplication
|
413
|
+
vertices = list(self.vertices())
|
414
|
+
for i, source in enumerate(vertices):
|
415
|
+
for j, target in enumerate(vertices):
|
416
|
+
sum_value = 0
|
417
|
+
for k in range(self._vertex_count):
|
418
|
+
intermediate = self._index_to_vertex[k]
|
419
|
+
|
420
|
+
weight1 = self.get_edge_weight(source, intermediate)
|
421
|
+
weight2 = other.get_edge_weight(intermediate, target)
|
422
|
+
|
423
|
+
if weight1 is not None and weight2 is not None:
|
424
|
+
sum_value += weight1 * weight2
|
425
|
+
|
426
|
+
if sum_value != 0:
|
427
|
+
result.add_edge(source, target, weight=sum_value)
|
428
|
+
|
429
|
+
return result
|
300
430
|
|
301
431
|
def transpose(self) -> 'xAdjMatrixStrategy':
|
302
|
-
"""Get the transpose of
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
# ============================================================================
|
309
|
-
|
310
|
-
def edges(self) -> Iterator[tuple[Any, Any, Dict[str, Any]]]:
|
311
|
-
"""Get all edges in the graph."""
|
312
|
-
for i in range(self._vertex_count):
|
313
|
-
for j in range(self._vertex_count):
|
314
|
-
if self._matrix[i][j] is not None:
|
315
|
-
source = self._index_to_vertex[i]
|
316
|
-
target = self._index_to_vertex[j]
|
317
|
-
yield (source, target, self._matrix[i][j])
|
318
|
-
|
319
|
-
def vertices(self) -> Iterator[Any]:
|
320
|
-
"""Get all vertices in the graph."""
|
321
|
-
return iter(self._vertex_to_index.keys())
|
322
|
-
|
323
|
-
# ============================================================================
|
324
|
-
# UTILITY METHODS
|
325
|
-
# ============================================================================
|
326
|
-
|
327
|
-
def clear(self) -> None:
|
328
|
-
"""Clear all edges and vertices."""
|
329
|
-
self._matrix = [[None for _ in range(self._capacity)] for _ in range(self._capacity)]
|
330
|
-
self._vertex_to_index.clear()
|
331
|
-
self._index_to_vertex.clear()
|
332
|
-
self._vertex_count = 0
|
333
|
-
self._edge_count = 0
|
334
|
-
self._edge_id_counter = 0
|
335
|
-
|
336
|
-
def add_vertex(self, vertex: str) -> None:
|
337
|
-
"""Add a vertex to the graph."""
|
338
|
-
self._get_vertex_index(vertex)
|
339
|
-
|
340
|
-
def remove_vertex(self, vertex: str) -> bool:
|
341
|
-
"""Remove a vertex and all its incident edges."""
|
342
|
-
if vertex not in self._vertex_to_index:
|
343
|
-
return False
|
432
|
+
"""Get the transpose of this matrix."""
|
433
|
+
result = xAdjMatrixStrategy(
|
434
|
+
traits=self._traits,
|
435
|
+
directed=True, # Transpose is always directed
|
436
|
+
initial_capacity=self._vertex_count
|
437
|
+
)
|
344
438
|
|
345
|
-
|
439
|
+
# Add vertices
|
440
|
+
for vertex in self.vertices():
|
441
|
+
result.add_vertex(vertex)
|
346
442
|
|
347
|
-
#
|
348
|
-
for
|
349
|
-
|
350
|
-
self._matrix[vertex_idx][i] = None
|
351
|
-
self._edge_count -= 1
|
352
|
-
if self._matrix[i][vertex_idx] is not None:
|
353
|
-
self._matrix[i][vertex_idx] = None
|
354
|
-
self._edge_count -= 1
|
355
|
-
|
356
|
-
# Remove vertex from mappings
|
357
|
-
del self._vertex_to_index[vertex]
|
358
|
-
del self._index_to_vertex[vertex_idx]
|
359
|
-
self._vertex_count -= 1
|
443
|
+
# Add transposed edges
|
444
|
+
for source, target, edge_data in self.edges(data=True):
|
445
|
+
result.add_edge(target, source, **edge_data['properties'])
|
360
446
|
|
361
|
-
return
|
447
|
+
return result
|
362
448
|
|
363
449
|
# ============================================================================
|
364
450
|
# PERFORMANCE CHARACTERISTICS
|
@@ -369,12 +455,15 @@ class AdjMatrixStrategy(AGraphEdgeStrategy):
|
|
369
455
|
"""Get backend implementation info."""
|
370
456
|
return {
|
371
457
|
'strategy': 'ADJ_MATRIX',
|
372
|
-
'backend': '2D matrix',
|
458
|
+
'backend': 'Python 2D list matrix',
|
459
|
+
'directed': self.is_directed,
|
460
|
+
'capacity': self._capacity,
|
461
|
+
'utilized': self._vertex_count,
|
373
462
|
'complexity': {
|
374
463
|
'add_edge': 'O(1)',
|
375
464
|
'remove_edge': 'O(1)',
|
376
465
|
'has_edge': 'O(1)',
|
377
|
-
'
|
466
|
+
'neighbors': 'O(V)',
|
378
467
|
'space': 'O(V²)'
|
379
468
|
}
|
380
469
|
}
|
@@ -382,10 +471,15 @@ class AdjMatrixStrategy(AGraphEdgeStrategy):
|
|
382
471
|
@property
|
383
472
|
def metrics(self) -> Dict[str, Any]:
|
384
473
|
"""Get performance metrics."""
|
474
|
+
density = self._edge_count / max(1, self._vertex_count * (self._vertex_count - 1)) if self._vertex_count > 1 else 0
|
475
|
+
memory_utilization = self._vertex_count / max(1, self._capacity) * 100
|
476
|
+
|
385
477
|
return {
|
386
478
|
'vertices': self._vertex_count,
|
387
479
|
'edges': self._edge_count,
|
388
|
-
'
|
389
|
-
'
|
390
|
-
'density':
|
480
|
+
'matrix_capacity': self._capacity,
|
481
|
+
'memory_utilization': f"{memory_utilization:.1f}%",
|
482
|
+
'density': round(density, 4),
|
483
|
+
'memory_usage': f"{self._capacity * self._capacity * 8} bytes (estimated)",
|
484
|
+
'sparsity': f"{(1 - density) * 100:.1f}%"
|
391
485
|
}
|