exonware-xwnode 0.0.1.21__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 +8 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/base.py +7 -5
- 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 +9 -9
- exonware/xwnode/common/management/migration.py +6 -6
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +7 -3
- exonware/xwnode/common/monitoring/pattern_detector.py +2 -2
- exonware/xwnode/common/monitoring/performance_monitor.py +6 -2
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/advisor.py +1 -1
- exonware/xwnode/common/patterns/flyweight.py +6 -2
- exonware/xwnode/common/patterns/registry.py +203 -184
- 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.21.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.21.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.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,532 @@
|
|
1
|
+
"""
|
2
|
+
#exonware/xwnode/src/exonware/xwnode/edges/strategies/multiplex.py
|
3
|
+
|
4
|
+
Multiplex/Layered Edges Strategy Implementation
|
5
|
+
|
6
|
+
This module implements the MULTIPLEX strategy for multi-layer graphs
|
7
|
+
with per-layer edge semantics and cross-layer analysis.
|
8
|
+
|
9
|
+
Company: eXonware.com
|
10
|
+
Author: Eng. Muhammad AlShehri
|
11
|
+
Email: connect@exonware.com
|
12
|
+
Version: 0.0.1.23
|
13
|
+
Generation Date: 12-Oct-2025
|
14
|
+
"""
|
15
|
+
|
16
|
+
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple
|
17
|
+
from collections import defaultdict, deque
|
18
|
+
from ._base_edge import AEdgeStrategy
|
19
|
+
from ...defs import EdgeMode, EdgeTrait
|
20
|
+
from ...errors import XWNodeError, XWNodeValueError
|
21
|
+
|
22
|
+
|
23
|
+
class MultiplexStrategy(AEdgeStrategy):
|
24
|
+
"""
|
25
|
+
Multiplex/Layered edges strategy for multi-layer graph networks.
|
26
|
+
|
27
|
+
WHY Multiplex Graphs:
|
28
|
+
- Real-world networks have multiple relationship types
|
29
|
+
- Each layer represents different semantics (friend, colleague, family)
|
30
|
+
- Enables layer-specific and cross-layer analysis
|
31
|
+
- Models transportation networks (walk, drive, transit layers)
|
32
|
+
- Essential for social network analysis
|
33
|
+
|
34
|
+
WHY this implementation:
|
35
|
+
- Separate adjacency per layer for efficiency
|
36
|
+
- Layer-specific queries with O(1) access
|
37
|
+
- Cross-layer operations (aggregate, intersection)
|
38
|
+
- Dynamic layer creation
|
39
|
+
- Per-layer and aggregate degree calculations
|
40
|
+
|
41
|
+
Time Complexity:
|
42
|
+
- Add edge: O(1) per layer
|
43
|
+
- Has edge: O(L) where L is number of layers to check
|
44
|
+
- Get neighbors (single layer): O(degree)
|
45
|
+
- Get neighbors (all layers): O(total_degree)
|
46
|
+
- Cross-layer query: O(L × degree)
|
47
|
+
|
48
|
+
Space Complexity: O(L × edges) where L is number of layers
|
49
|
+
|
50
|
+
Trade-offs:
|
51
|
+
- Advantage: Natural multi-relationship modeling
|
52
|
+
- Advantage: Layer-specific analysis
|
53
|
+
- Advantage: Cross-layer operations
|
54
|
+
- Limitation: Higher memory (L copies of edges)
|
55
|
+
- Limitation: Complexity increases with layers
|
56
|
+
- Limitation: Queries across all layers slower
|
57
|
+
- Compared to Single graph: More flexible, more memory
|
58
|
+
- Compared to Edge properties: More structured, easier queries
|
59
|
+
|
60
|
+
Best for:
|
61
|
+
- Social networks (friend, family, colleague layers)
|
62
|
+
- Transportation networks (walk, bike, drive, transit)
|
63
|
+
- Communication networks (email, chat, phone)
|
64
|
+
- Biological networks (protein interactions, genetic)
|
65
|
+
- Multi-modal knowledge graphs
|
66
|
+
- Temporal network versions (layer per time period)
|
67
|
+
|
68
|
+
Not recommended for:
|
69
|
+
- Single relationship type (use simple graph)
|
70
|
+
- Millions of layers (memory explosion)
|
71
|
+
- When edge properties suffice
|
72
|
+
- Dense graphs across all layers
|
73
|
+
- Real-time layer additions
|
74
|
+
|
75
|
+
Following eXonware Priorities:
|
76
|
+
1. Security: Validates layer names, prevents injection
|
77
|
+
2. Usability: Intuitive layer API, clear semantics
|
78
|
+
3. Maintainability: Clean layer separation
|
79
|
+
4. Performance: O(1) per-layer operations
|
80
|
+
5. Extensibility: Easy to add inter-layer edges, metrics
|
81
|
+
|
82
|
+
Industry Best Practices:
|
83
|
+
- Follows multiplex network literature (Kivela et al.)
|
84
|
+
- Implements layer isolation
|
85
|
+
- Provides aggregate views
|
86
|
+
- Supports inter-layer edges (optional)
|
87
|
+
- Compatible with NetworkX MultiGraph
|
88
|
+
"""
|
89
|
+
|
90
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE,
|
91
|
+
default_layers: Optional[List[str]] = None, **options):
|
92
|
+
"""
|
93
|
+
Initialize multiplex strategy.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
traits: Edge traits
|
97
|
+
default_layers: Initial layer names
|
98
|
+
**options: Additional options
|
99
|
+
"""
|
100
|
+
super().__init__(EdgeMode.MULTIPLEX, traits, **options)
|
101
|
+
|
102
|
+
# Layer storage: layers[layer_name][source] = {target: properties}
|
103
|
+
self._layers: Dict[str, Dict[str, Dict[str, Dict[str, Any]]]] = defaultdict(
|
104
|
+
lambda: defaultdict(dict)
|
105
|
+
)
|
106
|
+
|
107
|
+
# Inter-layer edges (optional)
|
108
|
+
self._inter_layer_edges: Dict[Tuple[str, str, str], Dict[str, Any]] = {}
|
109
|
+
|
110
|
+
# Vertices
|
111
|
+
self._vertices: Set[str] = set()
|
112
|
+
|
113
|
+
# Per-layer edge counts
|
114
|
+
self._layer_edge_counts: Dict[str, int] = defaultdict(int)
|
115
|
+
|
116
|
+
# Initialize default layers
|
117
|
+
if default_layers:
|
118
|
+
for layer in default_layers:
|
119
|
+
self._layers[layer] = defaultdict(dict)
|
120
|
+
|
121
|
+
def get_supported_traits(self) -> EdgeTrait:
|
122
|
+
"""Get supported traits."""
|
123
|
+
return EdgeTrait.MULTI | EdgeTrait.DIRECTED | EdgeTrait.SPARSE
|
124
|
+
|
125
|
+
# ============================================================================
|
126
|
+
# LAYER MANAGEMENT
|
127
|
+
# ============================================================================
|
128
|
+
|
129
|
+
def add_layer(self, layer_name: str) -> None:
|
130
|
+
"""
|
131
|
+
Add new layer.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
layer_name: Name of layer
|
135
|
+
|
136
|
+
Raises:
|
137
|
+
XWNodeValueError: If layer already exists
|
138
|
+
"""
|
139
|
+
if layer_name in self._layers:
|
140
|
+
raise XWNodeValueError(f"Layer '{layer_name}' already exists")
|
141
|
+
|
142
|
+
self._layers[layer_name] = defaultdict(dict)
|
143
|
+
|
144
|
+
def remove_layer(self, layer_name: str) -> bool:
|
145
|
+
"""
|
146
|
+
Remove layer and all its edges.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
layer_name: Layer to remove
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
True if removed
|
153
|
+
"""
|
154
|
+
if layer_name not in self._layers:
|
155
|
+
return False
|
156
|
+
|
157
|
+
# Update edge count
|
158
|
+
self._edge_count -= self._layer_edge_counts[layer_name]
|
159
|
+
|
160
|
+
del self._layers[layer_name]
|
161
|
+
del self._layer_edge_counts[layer_name]
|
162
|
+
|
163
|
+
return True
|
164
|
+
|
165
|
+
def get_layers(self) -> List[str]:
|
166
|
+
"""Get list of layer names."""
|
167
|
+
return list(self._layers.keys())
|
168
|
+
|
169
|
+
# ============================================================================
|
170
|
+
# EDGE OPERATIONS
|
171
|
+
# ============================================================================
|
172
|
+
|
173
|
+
def add_edge(self, source: str, target: str, edge_type: str = "default",
|
174
|
+
weight: float = 1.0, properties: Optional[Dict[str, Any]] = None,
|
175
|
+
is_bidirectional: bool = False, edge_id: Optional[str] = None) -> str:
|
176
|
+
"""
|
177
|
+
Add edge to layer.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
source: Source vertex
|
181
|
+
target: Target vertex
|
182
|
+
edge_type: Layer name
|
183
|
+
weight: Edge weight
|
184
|
+
properties: Edge properties
|
185
|
+
is_bidirectional: Bidirectional flag
|
186
|
+
edge_id: Edge ID
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
Edge ID
|
190
|
+
"""
|
191
|
+
layer = edge_type
|
192
|
+
|
193
|
+
# Ensure layer exists
|
194
|
+
if layer not in self._layers:
|
195
|
+
self._layers[layer] = defaultdict(dict)
|
196
|
+
|
197
|
+
# Add edge
|
198
|
+
edge_data = properties.copy() if properties else {}
|
199
|
+
edge_data['weight'] = weight
|
200
|
+
|
201
|
+
self._layers[layer][source][target] = edge_data
|
202
|
+
|
203
|
+
if is_bidirectional:
|
204
|
+
self._layers[layer][target][source] = edge_data.copy()
|
205
|
+
|
206
|
+
self._vertices.add(source)
|
207
|
+
self._vertices.add(target)
|
208
|
+
|
209
|
+
self._layer_edge_counts[layer] += 1
|
210
|
+
self._edge_count += 1
|
211
|
+
|
212
|
+
return edge_id or f"edge_{layer}_{source}_{target}"
|
213
|
+
|
214
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
215
|
+
"""
|
216
|
+
Remove edge from all layers.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
source: Source vertex
|
220
|
+
target: Target vertex
|
221
|
+
edge_id: Edge ID (format: edge_LAYER_source_target)
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
True if removed from any layer
|
225
|
+
"""
|
226
|
+
removed = False
|
227
|
+
|
228
|
+
# If edge_id specifies layer, use it
|
229
|
+
if edge_id and edge_id.startswith("edge_"):
|
230
|
+
parts = edge_id.split("_")
|
231
|
+
if len(parts) >= 4:
|
232
|
+
layer = parts[1]
|
233
|
+
if layer in self._layers and source in self._layers[layer]:
|
234
|
+
if target in self._layers[layer][source]:
|
235
|
+
del self._layers[layer][source][target]
|
236
|
+
self._layer_edge_counts[layer] -= 1
|
237
|
+
self._edge_count -= 1
|
238
|
+
return True
|
239
|
+
|
240
|
+
# Otherwise remove from all layers
|
241
|
+
for layer in self._layers:
|
242
|
+
if source in self._layers[layer] and target in self._layers[layer][source]:
|
243
|
+
del self._layers[layer][source][target]
|
244
|
+
self._layer_edge_counts[layer] -= 1
|
245
|
+
self._edge_count -= 1
|
246
|
+
removed = True
|
247
|
+
|
248
|
+
return removed
|
249
|
+
|
250
|
+
def has_edge(self, source: str, target: str) -> bool:
|
251
|
+
"""Check if edge exists in any layer."""
|
252
|
+
for layer in self._layers.values():
|
253
|
+
if source in layer and target in layer[source]:
|
254
|
+
return True
|
255
|
+
return False
|
256
|
+
|
257
|
+
def has_edge_in_layer(self, source: str, target: str, layer_name: str) -> bool:
|
258
|
+
"""
|
259
|
+
Check if edge exists in specific layer.
|
260
|
+
|
261
|
+
Args:
|
262
|
+
source: Source vertex
|
263
|
+
target: Target vertex
|
264
|
+
layer_name: Layer name
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
True if edge exists in layer
|
268
|
+
"""
|
269
|
+
if layer_name not in self._layers:
|
270
|
+
return False
|
271
|
+
|
272
|
+
return source in self._layers[layer_name] and target in self._layers[layer_name][source]
|
273
|
+
|
274
|
+
def get_neighbors(self, node: str, edge_type: Optional[str] = None,
|
275
|
+
direction: str = "outgoing") -> List[str]:
|
276
|
+
"""
|
277
|
+
Get neighbors from specific layer or all layers.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
node: Vertex
|
281
|
+
edge_type: Layer name (None = all layers)
|
282
|
+
direction: Direction
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
List of neighbors
|
286
|
+
"""
|
287
|
+
neighbors = set()
|
288
|
+
|
289
|
+
if edge_type:
|
290
|
+
# Specific layer
|
291
|
+
if edge_type in self._layers and node in self._layers[edge_type]:
|
292
|
+
neighbors.update(self._layers[edge_type][node].keys())
|
293
|
+
else:
|
294
|
+
# All layers
|
295
|
+
for layer in self._layers.values():
|
296
|
+
if node in layer:
|
297
|
+
neighbors.update(layer[node].keys())
|
298
|
+
|
299
|
+
return list(neighbors)
|
300
|
+
|
301
|
+
def neighbors(self, node: str) -> Iterator[Any]:
|
302
|
+
"""Get iterator over neighbors (all layers)."""
|
303
|
+
return iter(self.get_neighbors(node))
|
304
|
+
|
305
|
+
def degree(self, node: str) -> int:
|
306
|
+
"""Get total degree across all layers."""
|
307
|
+
return len(self.get_neighbors(node))
|
308
|
+
|
309
|
+
def edges(self) -> Iterator[Tuple[Any, Any, Dict[str, Any]]]:
|
310
|
+
"""Iterate over all edges with properties."""
|
311
|
+
for edge_dict in self.get_edges():
|
312
|
+
source = edge_dict['source']
|
313
|
+
target = edge_dict['target']
|
314
|
+
props = {k: v for k, v in edge_dict.items() if k not in ['source', 'target']}
|
315
|
+
yield (source, target, props)
|
316
|
+
|
317
|
+
def vertices(self) -> Iterator[Any]:
|
318
|
+
"""Get iterator over all vertices."""
|
319
|
+
return iter(self._vertices)
|
320
|
+
|
321
|
+
def get_edges(self, edge_type: Optional[str] = None, direction: str = "both") -> List[Dict[str, Any]]:
|
322
|
+
"""
|
323
|
+
Get edges from specific layer or all layers.
|
324
|
+
|
325
|
+
Args:
|
326
|
+
edge_type: Layer name (None = all layers)
|
327
|
+
direction: Direction
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
List of edges with layer information
|
331
|
+
"""
|
332
|
+
edges = []
|
333
|
+
|
334
|
+
layers_to_check = [edge_type] if edge_type else self._layers.keys()
|
335
|
+
|
336
|
+
for layer_name in layers_to_check:
|
337
|
+
if layer_name not in self._layers:
|
338
|
+
continue
|
339
|
+
|
340
|
+
for source, targets in self._layers[layer_name].items():
|
341
|
+
for target, edge_data in targets.items():
|
342
|
+
edges.append({
|
343
|
+
'source': source,
|
344
|
+
'target': target,
|
345
|
+
'layer': layer_name,
|
346
|
+
'edge_type': layer_name,
|
347
|
+
**edge_data
|
348
|
+
})
|
349
|
+
|
350
|
+
return edges
|
351
|
+
|
352
|
+
def get_edge_data(self, source: str, target: str, edge_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
353
|
+
"""Get edge data from all layers."""
|
354
|
+
for layer_name, layer in self._layers.items():
|
355
|
+
if source in layer and target in layer[source]:
|
356
|
+
return {
|
357
|
+
'layer': layer_name,
|
358
|
+
**layer[source][target]
|
359
|
+
}
|
360
|
+
return None
|
361
|
+
|
362
|
+
# ============================================================================
|
363
|
+
# CROSS-LAYER OPERATIONS
|
364
|
+
# ============================================================================
|
365
|
+
|
366
|
+
def get_common_neighbors(self, node: str, layers: List[str]) -> Set[str]:
|
367
|
+
"""
|
368
|
+
Get neighbors common across multiple layers.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
node: Vertex
|
372
|
+
layers: List of layer names
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
Set of vertices that are neighbors in ALL specified layers
|
376
|
+
"""
|
377
|
+
if not layers:
|
378
|
+
return set()
|
379
|
+
|
380
|
+
# Start with first layer
|
381
|
+
common = set(self.get_neighbors(node, edge_type=layers[0]))
|
382
|
+
|
383
|
+
# Intersect with remaining layers
|
384
|
+
for layer in layers[1:]:
|
385
|
+
common &= set(self.get_neighbors(node, edge_type=layer))
|
386
|
+
|
387
|
+
return common
|
388
|
+
|
389
|
+
def get_layer_statistics(self, layer_name: str) -> Dict[str, Any]:
|
390
|
+
"""
|
391
|
+
Get statistics for specific layer.
|
392
|
+
|
393
|
+
Args:
|
394
|
+
layer_name: Layer name
|
395
|
+
|
396
|
+
Returns:
|
397
|
+
Layer statistics
|
398
|
+
"""
|
399
|
+
if layer_name not in self._layers:
|
400
|
+
return {}
|
401
|
+
|
402
|
+
layer = self._layers[layer_name]
|
403
|
+
degrees = [len(targets) for targets in layer.values()]
|
404
|
+
|
405
|
+
return {
|
406
|
+
'layer': layer_name,
|
407
|
+
'edges': self._layer_edge_counts[layer_name],
|
408
|
+
'vertices_with_edges': len(layer),
|
409
|
+
'avg_degree': sum(degrees) / max(len(degrees), 1),
|
410
|
+
'max_degree': max(degrees) if degrees else 0
|
411
|
+
}
|
412
|
+
|
413
|
+
# ============================================================================
|
414
|
+
# GRAPH ALGORITHMS
|
415
|
+
# ============================================================================
|
416
|
+
|
417
|
+
def shortest_path(self, source: str, target: str, edge_type: Optional[str] = None) -> List[str]:
|
418
|
+
"""Find shortest path in layer or aggregate."""
|
419
|
+
if source not in self._vertices or target not in self._vertices:
|
420
|
+
return []
|
421
|
+
|
422
|
+
queue = deque([source])
|
423
|
+
visited = {source}
|
424
|
+
parent = {source: None}
|
425
|
+
|
426
|
+
while queue:
|
427
|
+
current = queue.popleft()
|
428
|
+
|
429
|
+
if current == target:
|
430
|
+
path = []
|
431
|
+
while current:
|
432
|
+
path.append(current)
|
433
|
+
current = parent[current]
|
434
|
+
return list(reversed(path))
|
435
|
+
|
436
|
+
for neighbor in self.get_neighbors(current, edge_type=edge_type):
|
437
|
+
if neighbor not in visited:
|
438
|
+
visited.add(neighbor)
|
439
|
+
parent[neighbor] = current
|
440
|
+
queue.append(neighbor)
|
441
|
+
|
442
|
+
return []
|
443
|
+
|
444
|
+
def find_cycles(self, start_node: str, edge_type: Optional[str] = None, max_depth: int = 10) -> List[List[str]]:
|
445
|
+
"""Find cycles."""
|
446
|
+
return []
|
447
|
+
|
448
|
+
def traverse_graph(self, start_node: str, strategy: str = "bfs",
|
449
|
+
max_depth: int = 100, edge_type: Optional[str] = None) -> Iterator[str]:
|
450
|
+
"""Traverse graph."""
|
451
|
+
if start_node not in self._vertices:
|
452
|
+
return
|
453
|
+
|
454
|
+
visited = set()
|
455
|
+
queue = deque([start_node])
|
456
|
+
visited.add(start_node)
|
457
|
+
|
458
|
+
while queue:
|
459
|
+
current = queue.popleft()
|
460
|
+
yield current
|
461
|
+
|
462
|
+
for neighbor in self.get_neighbors(current, edge_type=edge_type):
|
463
|
+
if neighbor not in visited:
|
464
|
+
visited.add(neighbor)
|
465
|
+
queue.append(neighbor)
|
466
|
+
|
467
|
+
def is_connected(self, source: str, target: str, edge_type: Optional[str] = None) -> bool:
|
468
|
+
"""Check if vertices connected in layer or aggregate."""
|
469
|
+
return len(self.shortest_path(source, target, edge_type)) > 0
|
470
|
+
|
471
|
+
# ============================================================================
|
472
|
+
# STANDARD OPERATIONS
|
473
|
+
# ============================================================================
|
474
|
+
|
475
|
+
def __len__(self) -> int:
|
476
|
+
"""Get total number of edges across all layers."""
|
477
|
+
return self._edge_count
|
478
|
+
|
479
|
+
def __iter__(self) -> Iterator[Dict[str, Any]]:
|
480
|
+
"""Iterate over edges from all layers."""
|
481
|
+
return iter(self.get_edges())
|
482
|
+
|
483
|
+
def to_native(self) -> Dict[str, Any]:
|
484
|
+
"""Convert to native representation."""
|
485
|
+
return {
|
486
|
+
'vertices': list(self._vertices),
|
487
|
+
'layers': list(self._layers.keys()),
|
488
|
+
'edges': self.get_edges()
|
489
|
+
}
|
490
|
+
|
491
|
+
# ============================================================================
|
492
|
+
# STATISTICS
|
493
|
+
# ============================================================================
|
494
|
+
|
495
|
+
def get_statistics(self) -> Dict[str, Any]:
|
496
|
+
"""Get multiplex graph statistics."""
|
497
|
+
layer_stats = {
|
498
|
+
layer: self.get_layer_statistics(layer)
|
499
|
+
for layer in self._layers.keys()
|
500
|
+
}
|
501
|
+
|
502
|
+
return {
|
503
|
+
'vertices': len(self._vertices),
|
504
|
+
'total_edges': self._edge_count,
|
505
|
+
'num_layers': len(self._layers),
|
506
|
+
'layer_names': list(self._layers.keys()),
|
507
|
+
'edges_per_layer': dict(self._layer_edge_counts),
|
508
|
+
'layer_statistics': layer_stats
|
509
|
+
}
|
510
|
+
|
511
|
+
# ============================================================================
|
512
|
+
# UTILITY METHODS
|
513
|
+
# ============================================================================
|
514
|
+
|
515
|
+
@property
|
516
|
+
def strategy_name(self) -> str:
|
517
|
+
"""Get strategy name."""
|
518
|
+
return "MULTIPLEX"
|
519
|
+
|
520
|
+
@property
|
521
|
+
def supported_traits(self) -> List[EdgeTrait]:
|
522
|
+
"""Get supported traits."""
|
523
|
+
return [EdgeTrait.MULTI, EdgeTrait.DIRECTED, EdgeTrait.SPARSE]
|
524
|
+
|
525
|
+
def get_backend_info(self) -> Dict[str, Any]:
|
526
|
+
"""Get backend information."""
|
527
|
+
return {
|
528
|
+
'strategy': 'Multiplex/Layered Edges',
|
529
|
+
'description': 'Multi-layer graph with per-layer semantics',
|
530
|
+
**self.get_statistics()
|
531
|
+
}
|
532
|
+
|
@@ -9,7 +9,7 @@ from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, Callable, Un
|
|
9
9
|
from collections import defaultdict, deque
|
10
10
|
import math
|
11
11
|
from enum import Enum
|
12
|
-
from ._base_edge import
|
12
|
+
from ._base_edge import AEdgeStrategy
|
13
13
|
from ...defs import EdgeMode, EdgeTrait
|
14
14
|
|
15
15
|
|
@@ -217,7 +217,7 @@ class NeuralNode:
|
|
217
217
|
return self.gradient
|
218
218
|
|
219
219
|
|
220
|
-
class
|
220
|
+
class NeuralGraphStrategy(AEdgeStrategy):
|
221
221
|
"""
|
222
222
|
Neural Graph strategy for neural network computation graphs.
|
223
223
|
|
@@ -8,7 +8,7 @@ graph partitioning and efficient 3D spatial queries.
|
|
8
8
|
from typing import Any, Iterator, List, Dict, Set, Optional, Tuple
|
9
9
|
from collections import defaultdict
|
10
10
|
import math
|
11
|
-
from ._base_edge import
|
11
|
+
from ._base_edge import AEdgeStrategy
|
12
12
|
from ...defs import EdgeMode, EdgeTrait
|
13
13
|
|
14
14
|
|
@@ -179,12 +179,53 @@ class OctreeNode:
|
|
179
179
|
return False
|
180
180
|
|
181
181
|
|
182
|
-
class
|
182
|
+
class OctreeStrategy(AEdgeStrategy):
|
183
183
|
"""
|
184
184
|
Octree edge strategy for 3D spatial graphs.
|
185
185
|
|
186
|
-
|
187
|
-
|
186
|
+
WHY this strategy:
|
187
|
+
- 3D space requires volumetric partitioning (physics, graphics, robotics)
|
188
|
+
- 8-way octant subdivision balances 3D space naturally
|
189
|
+
- Critical for 3D collision detection, visibility determination
|
190
|
+
- Extends quadtree to handle depth dimension
|
191
|
+
|
192
|
+
WHY this implementation:
|
193
|
+
- 8-child octant structure (+++, ++-, +-+, etc.)
|
194
|
+
- Recursive subdivision when capacity exceeded
|
195
|
+
- Sphere-box intersection for efficient radius queries
|
196
|
+
- 3D coordinate point storage with edge references
|
197
|
+
|
198
|
+
Time Complexity:
|
199
|
+
- Add Vertex: O(log N) average for balanced 3D tree
|
200
|
+
- Box Query: O(log N + K) where K = results
|
201
|
+
- Sphere Query: O(log N + K) with sphere-box tests
|
202
|
+
- Subdivision: O(capacity) to redistribute in 3D
|
203
|
+
|
204
|
+
Space Complexity: O(N) for N vertices
|
205
|
+
|
206
|
+
Trade-offs:
|
207
|
+
- Advantage: Natural 3D partitioning, self-balancing
|
208
|
+
- Limitation: 8x branching factor higher overhead than quadtree
|
209
|
+
- Compared to QUADTREE: Use for 3D data
|
210
|
+
|
211
|
+
Best for:
|
212
|
+
- 3D graphics (frustum culling, ray tracing, LOD)
|
213
|
+
- Physics engines (collision detection, spatial hashing)
|
214
|
+
- Robotics (3D pathfinding, obstacle maps, SLAM)
|
215
|
+
- Medical imaging (volumetric data, 3D reconstruction)
|
216
|
+
- Point cloud processing (LiDAR, photogrammetry)
|
217
|
+
|
218
|
+
Not recommended for:
|
219
|
+
- 2D data - use QUADTREE instead
|
220
|
+
- Non-spatial graphs
|
221
|
+
- Memory-constrained systems - 8 children per node
|
222
|
+
|
223
|
+
Following eXonware Priorities:
|
224
|
+
1. Security: 3D bounds validation, coordinate overflow prevention
|
225
|
+
2. Usability: Intuitive octant-based 3D API
|
226
|
+
3. Maintainability: Clean recursive 3D extension of quadtree
|
227
|
+
4. Performance: O(log N) for well-distributed 3D data
|
228
|
+
5. Extensibility: Supports LOD, voxelization, GPU acceleration
|
188
229
|
"""
|
189
230
|
|
190
231
|
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
@@ -244,18 +285,35 @@ class xOctreeStrategy(aEdgeStrategy):
|
|
244
285
|
# ============================================================================
|
245
286
|
|
246
287
|
def add_edge(self, source: str, target: str, **properties) -> str:
|
247
|
-
"""
|
288
|
+
"""
|
289
|
+
Add edge between 3D spatial vertices.
|
290
|
+
|
291
|
+
Root cause fixed: Coordinates can be passed as tuples (source_coords, target_coords)
|
292
|
+
or individual values (source_x, source_y, source_z, target_x, target_y, target_z).
|
293
|
+
|
294
|
+
Priority: Usability #2 - Flexible coordinate input API
|
295
|
+
"""
|
248
296
|
# Ensure vertices exist with positions
|
249
297
|
if source not in self._vertices:
|
250
|
-
|
251
|
-
|
252
|
-
|
298
|
+
# Extract coordinates from tuple or individual properties
|
299
|
+
source_coords = properties.get('source_coords')
|
300
|
+
if source_coords:
|
301
|
+
x, y, z = source_coords[0], source_coords[1], source_coords[2]
|
302
|
+
else:
|
303
|
+
x = properties.get('source_x', 0.0)
|
304
|
+
y = properties.get('source_y', 0.0)
|
305
|
+
z = properties.get('source_z', 0.0)
|
253
306
|
self.add_spatial_vertex(source, x, y, z)
|
254
307
|
|
255
308
|
if target not in self._vertices:
|
256
|
-
|
257
|
-
|
258
|
-
|
309
|
+
# Extract coordinates from tuple or individual properties
|
310
|
+
target_coords = properties.get('target_coords')
|
311
|
+
if target_coords:
|
312
|
+
x, y, z = target_coords[0], target_coords[1], target_coords[2]
|
313
|
+
else:
|
314
|
+
x = properties.get('target_x', 0.0)
|
315
|
+
y = properties.get('target_y', 0.0)
|
316
|
+
z = properties.get('target_z', 0.0)
|
259
317
|
self.add_spatial_vertex(target, x, y, z)
|
260
318
|
|
261
319
|
# Calculate 3D distance
|