tgraphx 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {tgraphx-0.3.0 → tgraphx-0.4.0}/PKG-INFO +3 -3
- {tgraphx-0.3.0 → tgraphx-0.4.0}/pyproject.toml +3 -3
- tgraphx-0.4.0/tests/test_algorithms.py +275 -0
- tgraphx-0.4.0/tests/test_graph_utils.py +118 -0
- tgraphx-0.4.0/tests/test_hard_negative_sampling.py +155 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_imports.py +1 -0
- tgraphx-0.4.0/tests/test_mining_core.py +628 -0
- tgraphx-0.4.0/tests/test_mining_structural.py +134 -0
- tgraphx-0.4.0/tests/test_negative_sampling.py +309 -0
- tgraphx-0.4.0/tests/test_neural_mining.py +393 -0
- tgraphx-0.4.0/tests/test_plotting.py +240 -0
- tgraphx-0.4.0/tests/test_public_benchmarks.py +159 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_synthetic_datasets.py +1 -1
- tgraphx-0.4.0/tests/test_time_encoding.py +163 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_tiny_overfit_v030.py +3 -3
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/__init__.py +18 -1
- tgraphx-0.4.0/tgraphx/algorithms/__init__.py +57 -0
- tgraphx-0.4.0/tgraphx/algorithms/connectivity.py +180 -0
- tgraphx-0.4.0/tgraphx/algorithms/structural.py +117 -0
- tgraphx-0.4.0/tgraphx/algorithms/traversal.py +208 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/folder.py +4 -3
- tgraphx-0.4.0/tgraphx/mining/__init__.py +253 -0
- tgraphx-0.4.0/tgraphx/mining/anomaly.py +230 -0
- tgraphx-0.4.0/tgraphx/mining/communities.py +189 -0
- tgraphx-0.4.0/tgraphx/mining/frequent.py +131 -0
- tgraphx-0.4.0/tgraphx/mining/hetero.py +85 -0
- tgraphx-0.4.0/tgraphx/mining/kernels.py +256 -0
- tgraphx-0.4.0/tgraphx/mining/link_prediction.py +253 -0
- tgraphx-0.4.0/tgraphx/mining/motifs.py +238 -0
- tgraphx-0.4.0/tgraphx/mining/neural.py +627 -0
- tgraphx-0.4.0/tgraphx/mining/patterns.py +175 -0
- tgraphx-0.4.0/tgraphx/mining/prototype.py +580 -0
- tgraphx-0.4.0/tgraphx/mining/random_walk.py +178 -0
- tgraphx-0.4.0/tgraphx/mining/reports.py +175 -0
- tgraphx-0.4.0/tgraphx/mining/similarity.py +150 -0
- tgraphx-0.4.0/tgraphx/mining/structural.py +335 -0
- tgraphx-0.4.0/tgraphx/mining/temporal.py +181 -0
- tgraphx-0.4.0/tgraphx/plotting/__init__.py +78 -0
- tgraphx-0.4.0/tgraphx/plotting/graph.py +257 -0
- tgraphx-0.4.0/tgraphx/plotting/layouts.py +140 -0
- tgraphx-0.4.0/tgraphx/plotting/mining.py +445 -0
- tgraphx-0.4.0/tgraphx/plotting/utils.py +87 -0
- tgraphx-0.4.0/tgraphx/sampling_negative.py +565 -0
- tgraphx-0.4.0/tgraphx/temporal/__init__.py +29 -0
- tgraphx-0.4.0/tgraphx/temporal/time_encoding.py +118 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx.egg-info/PKG-INFO +3 -3
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx.egg-info/SOURCES.txt +38 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx.egg-info/requires.txt +1 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/LICENSE +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/README.md +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/setup.cfg +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_3d_support.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_amp_compile.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_backward_compatibility.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_benchmark_smoke.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_chunking.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_dashboard.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_dashboard_metadata.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_dataset_cache.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_dataset_docs_claims.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_dataset_download_mocked.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_dataset_registry.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_datasets_base.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_devices.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_distributed_compat.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_documentation_claims.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_edge_features.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_edge_weight.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_experiments.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_explainability.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_factories.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_folder_datasets.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_gat_chunking.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_gnn_families.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_gradients.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_graph.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_graph_api.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_graph_builders.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_graph_transformer_v027.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_hetero_batch.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_hetero_layers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_hetero_sampling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_layers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_math.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_math_invariants_v030.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_metrics.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_model_zoo.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_models.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_packaging.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_performance_smoke.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_pyg_dgl_ogb_wrappers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_random_walk_sample.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_release_validation_v030.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_sampling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_sampling_loaders.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_temporal_sampling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_temporal_v025.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_torchvision_wrappers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_tracking.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_training.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_transforms.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tests/test_v024_features.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/dataloader.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/graph.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/graph_utils.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/hetero_batch.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/hetero_graph.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/temporal.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/temporal_batch.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/core/utils.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/dashboard/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/dashboard/__main__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/dashboard/app.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/dashboard/static/dashboard.css +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/dashboard/static/dashboard.js +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/base.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/cache.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/converters.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/dgl_wrappers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/download.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/errors.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/metadata.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/ogb_wrappers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/pyg_wrappers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/registry.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/synthetic.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/datasets/torchvision_wrappers.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/distributed.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/callbacks.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/checkpoints.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/cli.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/config.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/grid.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/runner.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/experiments/summarize.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/attention.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/edge_attribution.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/export.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/integrated_gradients.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/patch_heatmap.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/saliency.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/explain/utils.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/graph_builders.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/hetero_sampling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/interop.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/_dim.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/_scatter.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/aggregator.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/appnp.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/attention_message.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/base.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/conv_message.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/factory.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/gat.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/gatv2.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/gin.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/graph_transformer.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/hetero.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/hetero_readout.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/pooling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/safe_pool.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/sage.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/temporal_readout.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/transformer_encodings.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/layers/vector_gcn.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/learned_graph.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/classification.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/link_prediction.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/ogb.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/ranking.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/regression.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/metrics/reports.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/cnn_encoder.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/cnn_gnn_model.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/edge_predictor.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/factory.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/graph_classifier.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/hetero_models.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/model_zoo.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/node_classifier.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/pre_encoder.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/regressors.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/models/temporal_models.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/performance.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/sampling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/sampling_loaders.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/temporal_sampling.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/tracking.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/training.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/__init__.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/compose.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/features.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/graph.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/patch.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/positional.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx/transforms/splits.py +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx.egg-info/dependency_links.txt +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx.egg-info/entry_points.txt +0 -0
- {tgraphx-0.3.0 → tgraphx-0.4.0}/tgraphx.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tgraphx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Tensor-aware graph neural networks preserving spatial node feature layouts
|
|
5
5
|
Author-email: Arash Sajjadi <arash.sajjadi@usask.ca>
|
|
6
6
|
Maintainer-email: Arash Sajjadi <arash.sajjadi@usask.ca>
|
|
@@ -18,7 +18,6 @@ Classifier: Intended Audience :: Developers
|
|
|
18
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
19
19
|
Classifier: Operating System :: OS Independent
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -26,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
26
25
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
27
26
|
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
28
27
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.10
|
|
30
29
|
Description-Content-Type: text/markdown
|
|
31
30
|
License-File: LICENSE
|
|
32
31
|
Requires-Dist: torch>=1.13
|
|
@@ -34,6 +33,7 @@ Requires-Dist: torchvision>=0.14
|
|
|
34
33
|
Requires-Dist: pyyaml>=5.4
|
|
35
34
|
Provides-Extra: dev
|
|
36
35
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
36
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
37
37
|
Requires-Dist: build>=0.10; extra == "dev"
|
|
38
38
|
Requires-Dist: twine>=4.0; extra == "dev"
|
|
39
39
|
Provides-Extra: monitoring
|
|
@@ -5,10 +5,10 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tgraphx"
|
|
7
7
|
# Keep this in sync with tgraphx/__init__.py::__version__
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.4.0"
|
|
9
9
|
description = "Tensor-aware graph neural networks preserving spatial node feature layouts"
|
|
10
10
|
readme = "README.md"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
12
|
license = {text = "MIT"}
|
|
13
13
|
authors = [
|
|
14
14
|
{name = "Arash Sajjadi", email = "arash.sajjadi@usask.ca"},
|
|
@@ -35,7 +35,6 @@ classifiers = [
|
|
|
35
35
|
"License :: OSI Approved :: MIT License",
|
|
36
36
|
"Operating System :: OS Independent",
|
|
37
37
|
"Programming Language :: Python :: 3",
|
|
38
|
-
"Programming Language :: Python :: 3.9",
|
|
39
38
|
"Programming Language :: Python :: 3.10",
|
|
40
39
|
"Programming Language :: Python :: 3.11",
|
|
41
40
|
"Programming Language :: Python :: 3.12",
|
|
@@ -53,6 +52,7 @@ dependencies = [
|
|
|
53
52
|
[project.optional-dependencies]
|
|
54
53
|
dev = [
|
|
55
54
|
"pytest>=7.0",
|
|
55
|
+
"matplotlib>=3.7",
|
|
56
56
|
"build>=0.10",
|
|
57
57
|
"twine>=4.0",
|
|
58
58
|
]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Tests for tgraphx.algorithms — v0.3.2 beta primitives.
|
|
2
|
+
|
|
3
|
+
Covers connectivity (connected_components, is_connected, count) and
|
|
4
|
+
traversal (bfs_layers, bfs_edges, shortest_path_length).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
from tgraphx.algorithms import (
|
|
12
|
+
bfs_edges,
|
|
13
|
+
bfs_layers,
|
|
14
|
+
connected_components,
|
|
15
|
+
is_connected,
|
|
16
|
+
number_connected_components,
|
|
17
|
+
shortest_path_length,
|
|
18
|
+
weakly_connected_components,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── connectivity ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestConnectedComponents:
|
|
26
|
+
def test_two_disjoint_paths(self):
|
|
27
|
+
# Components: {0, 1, 2} and {3, 4}.
|
|
28
|
+
edge_index = torch.tensor([[0, 1, 3], [1, 2, 4]], dtype=torch.long)
|
|
29
|
+
labels = connected_components(edge_index, num_nodes=5)
|
|
30
|
+
assert labels.tolist() == [0, 0, 0, 1, 1]
|
|
31
|
+
assert number_connected_components(edge_index, 5) == 2
|
|
32
|
+
|
|
33
|
+
def test_one_connected_graph(self):
|
|
34
|
+
# 0-1-2-0 triangle.
|
|
35
|
+
edge_index = torch.tensor([[0, 1, 2], [1, 2, 0]], dtype=torch.long)
|
|
36
|
+
labels = connected_components(edge_index, 3)
|
|
37
|
+
assert labels.tolist() == [0, 0, 0]
|
|
38
|
+
assert is_connected(edge_index, 3)
|
|
39
|
+
assert number_connected_components(edge_index, 3) == 1
|
|
40
|
+
|
|
41
|
+
def test_isolated_nodes(self):
|
|
42
|
+
edge_index = torch.zeros((2, 0), dtype=torch.long)
|
|
43
|
+
labels = connected_components(edge_index, num_nodes=4)
|
|
44
|
+
# 4 isolated nodes ⇒ 4 components, deterministic 0..3.
|
|
45
|
+
assert labels.tolist() == [0, 1, 2, 3]
|
|
46
|
+
assert number_connected_components(edge_index, 4) == 4
|
|
47
|
+
assert not is_connected(edge_index, 4)
|
|
48
|
+
|
|
49
|
+
def test_single_node_is_connected(self):
|
|
50
|
+
assert is_connected(torch.zeros((2, 0), dtype=torch.long), num_nodes=1)
|
|
51
|
+
|
|
52
|
+
def test_empty_graph_not_connected(self):
|
|
53
|
+
assert not is_connected(torch.zeros((2, 0), dtype=torch.long), num_nodes=0)
|
|
54
|
+
assert number_connected_components(
|
|
55
|
+
torch.zeros((2, 0), dtype=torch.long), num_nodes=0,
|
|
56
|
+
) == 0
|
|
57
|
+
|
|
58
|
+
def test_directed_treated_as_undirected(self):
|
|
59
|
+
# Edge 0→1 only (not 1→0).
|
|
60
|
+
edge_index = torch.tensor([[0], [1]], dtype=torch.long)
|
|
61
|
+
labels = connected_components(edge_index, num_nodes=2)
|
|
62
|
+
assert labels.tolist() == [0, 0]
|
|
63
|
+
|
|
64
|
+
def test_weakly_connected_alias(self):
|
|
65
|
+
edge_index = torch.tensor([[0, 1, 2, 4], [1, 2, 3, 5]], dtype=torch.long)
|
|
66
|
+
a = connected_components(edge_index, 6)
|
|
67
|
+
b = weakly_connected_components(edge_index, 6)
|
|
68
|
+
assert torch.equal(a, b)
|
|
69
|
+
|
|
70
|
+
def test_compact_label_ids(self):
|
|
71
|
+
# Components are returned as contiguous [0, K).
|
|
72
|
+
edge_index = torch.tensor([[1, 2, 5], [2, 3, 6]], dtype=torch.long)
|
|
73
|
+
labels = connected_components(edge_index, num_nodes=7)
|
|
74
|
+
unique = torch.unique(labels)
|
|
75
|
+
# Labels must form a contiguous range starting at 0.
|
|
76
|
+
assert unique.tolist() == list(range(unique.numel()))
|
|
77
|
+
|
|
78
|
+
def test_validates_edge_index_shape(self):
|
|
79
|
+
with pytest.raises(ValueError, match="\\[2, E\\]"):
|
|
80
|
+
connected_components(torch.zeros(3, 4, dtype=torch.long), num_nodes=4)
|
|
81
|
+
|
|
82
|
+
def test_validates_edge_id_range(self):
|
|
83
|
+
bad = torch.tensor([[0, 5], [1, 2]], dtype=torch.long)
|
|
84
|
+
with pytest.raises(ValueError, match="out of range"):
|
|
85
|
+
connected_components(bad, num_nodes=4)
|
|
86
|
+
|
|
87
|
+
def test_inferred_num_nodes(self):
|
|
88
|
+
edge_index = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
89
|
+
labels = connected_components(edge_index) # infers num_nodes=3
|
|
90
|
+
assert labels.numel() == 3
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── BFS layers ───────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestBFSLayers:
|
|
97
|
+
def test_chain(self):
|
|
98
|
+
# 0 → 1 → 2 → 3
|
|
99
|
+
edge_index = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
100
|
+
layers = bfs_layers(edge_index, source=0, num_nodes=4)
|
|
101
|
+
assert [l.tolist() for l in layers] == [[0], [1], [2], [3]]
|
|
102
|
+
|
|
103
|
+
def test_branching(self):
|
|
104
|
+
# 0 → {1, 2}; 1 → 3
|
|
105
|
+
edge_index = torch.tensor([[0, 0, 1], [1, 2, 3]], dtype=torch.long)
|
|
106
|
+
layers = bfs_layers(edge_index, source=0, num_nodes=4)
|
|
107
|
+
assert layers[0].tolist() == [0]
|
|
108
|
+
assert sorted(layers[1].tolist()) == [1, 2]
|
|
109
|
+
assert layers[2].tolist() == [3]
|
|
110
|
+
|
|
111
|
+
def test_isolated_source(self):
|
|
112
|
+
layers = bfs_layers(torch.zeros((2, 0), dtype=torch.long),
|
|
113
|
+
source=0, num_nodes=3)
|
|
114
|
+
assert [l.tolist() for l in layers] == [[0]]
|
|
115
|
+
|
|
116
|
+
def test_max_hops(self):
|
|
117
|
+
edge_index = torch.tensor([[0, 1, 2, 3], [1, 2, 3, 4]], dtype=torch.long)
|
|
118
|
+
layers = bfs_layers(edge_index, source=0, num_nodes=5, max_hops=2)
|
|
119
|
+
assert sum(l.numel() for l in layers) == 3 # source + 2 hops
|
|
120
|
+
|
|
121
|
+
def test_invalid_source(self):
|
|
122
|
+
with pytest.raises(ValueError, match="source"):
|
|
123
|
+
bfs_layers(torch.zeros((2, 0), dtype=torch.long),
|
|
124
|
+
source=10, num_nodes=4)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── BFS edges ────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestBFSEdges:
|
|
131
|
+
def test_chain_edges(self):
|
|
132
|
+
edge_index = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
133
|
+
bfs = bfs_edges(edge_index, source=0, num_nodes=4)
|
|
134
|
+
# Expect exactly the chain.
|
|
135
|
+
assert bfs.tolist() == [[0, 1, 2], [1, 2, 3]]
|
|
136
|
+
|
|
137
|
+
def test_no_edges_returns_empty(self):
|
|
138
|
+
bfs = bfs_edges(torch.zeros((2, 0), dtype=torch.long),
|
|
139
|
+
source=0, num_nodes=3)
|
|
140
|
+
assert bfs.shape == (2, 0)
|
|
141
|
+
|
|
142
|
+
def test_branching_count(self):
|
|
143
|
+
# 0 → {1, 2}; 1 → 3
|
|
144
|
+
edge_index = torch.tensor([[0, 0, 1], [1, 2, 3]], dtype=torch.long)
|
|
145
|
+
bfs = bfs_edges(edge_index, source=0, num_nodes=4)
|
|
146
|
+
# Exactly 3 reachable non-source nodes ⇒ 3 BFS edges.
|
|
147
|
+
assert bfs.size(1) == 3
|
|
148
|
+
# Each child node appears once (BFS tree property).
|
|
149
|
+
assert sorted(bfs[1].tolist()) == [1, 2, 3]
|
|
150
|
+
|
|
151
|
+
def test_no_revisit(self):
|
|
152
|
+
# Cycle: 0 → 1 → 2 → 0
|
|
153
|
+
edge_index = torch.tensor([[0, 1, 2], [1, 2, 0]], dtype=torch.long)
|
|
154
|
+
bfs = bfs_edges(edge_index, source=0, num_nodes=3)
|
|
155
|
+
# Only 2 BFS edges — node 0 is the source and is not revisited.
|
|
156
|
+
assert bfs.size(1) == 2
|
|
157
|
+
|
|
158
|
+
def test_max_hops(self):
|
|
159
|
+
edge_index = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
160
|
+
bfs = bfs_edges(edge_index, source=0, num_nodes=4, max_hops=1)
|
|
161
|
+
# With 1 hop only, BFS tree edges = 1.
|
|
162
|
+
assert bfs.size(1) == 1
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── Shortest path length ─────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TestShortestPathLength:
|
|
169
|
+
def test_chain(self):
|
|
170
|
+
edge_index = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
171
|
+
d = shortest_path_length(edge_index, source=0, num_nodes=4)
|
|
172
|
+
assert d.tolist() == [0, 1, 2, 3]
|
|
173
|
+
|
|
174
|
+
def test_unreachable_node(self):
|
|
175
|
+
# 0 → 1 ; 2 isolated (in directed sense)
|
|
176
|
+
edge_index = torch.tensor([[0], [1]], dtype=torch.long)
|
|
177
|
+
d = shortest_path_length(edge_index, source=0, num_nodes=3)
|
|
178
|
+
assert d[0].item() == 0
|
|
179
|
+
assert d[1].item() == 1
|
|
180
|
+
assert d[2].item() == -1
|
|
181
|
+
|
|
182
|
+
def test_self_distance(self):
|
|
183
|
+
edge_index = torch.zeros((2, 0), dtype=torch.long)
|
|
184
|
+
d = shortest_path_length(edge_index, source=0, num_nodes=3)
|
|
185
|
+
assert d[0].item() == 0
|
|
186
|
+
assert d[1].item() == -1
|
|
187
|
+
assert d[2].item() == -1
|
|
188
|
+
|
|
189
|
+
def test_branching_distances(self):
|
|
190
|
+
# 0 → {1, 2}; 1 → 3
|
|
191
|
+
edge_index = torch.tensor([[0, 0, 1], [1, 2, 3]], dtype=torch.long)
|
|
192
|
+
d = shortest_path_length(edge_index, source=0, num_nodes=4)
|
|
193
|
+
assert d.tolist() == [0, 1, 1, 2]
|
|
194
|
+
|
|
195
|
+
def test_directed_one_way(self):
|
|
196
|
+
# 0 → 1, 1 → 2, no reverse edges. From 2 nothing is reachable.
|
|
197
|
+
edge_index = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
198
|
+
d = shortest_path_length(edge_index, source=2, num_nodes=3)
|
|
199
|
+
assert d[2].item() == 0
|
|
200
|
+
assert d[0].item() == -1
|
|
201
|
+
assert d[1].item() == -1
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── Edge cases and robustness ─────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestEdgeCasesAndValidation:
|
|
208
|
+
def test_self_loop_edges_do_not_crash_bfs(self):
|
|
209
|
+
# Self-loops should be ignored by BFS (node already visited).
|
|
210
|
+
edge_index = torch.tensor([[0, 0, 1], [0, 1, 2]], dtype=torch.long)
|
|
211
|
+
layers = bfs_layers(edge_index, source=0, num_nodes=3)
|
|
212
|
+
all_nodes = set()
|
|
213
|
+
for l in layers:
|
|
214
|
+
all_nodes.update(l.tolist())
|
|
215
|
+
assert 0 in all_nodes and 1 in all_nodes and 2 in all_nodes
|
|
216
|
+
|
|
217
|
+
def test_self_loop_edges_do_not_crash_components(self):
|
|
218
|
+
edge_index = torch.tensor([[0, 1, 1], [1, 2, 1]], dtype=torch.long) # node 1 self-loop
|
|
219
|
+
labels = connected_components(edge_index, num_nodes=3)
|
|
220
|
+
assert labels.tolist() == [0, 0, 0]
|
|
221
|
+
|
|
222
|
+
def test_duplicate_edges_do_not_duplicate_components(self):
|
|
223
|
+
# Duplicate edge 0→1 should not change component structure.
|
|
224
|
+
edge_index = torch.tensor([[0, 0, 1], [1, 1, 2]], dtype=torch.long)
|
|
225
|
+
labels = connected_components(edge_index, num_nodes=3)
|
|
226
|
+
assert labels.tolist() == [0, 0, 0]
|
|
227
|
+
|
|
228
|
+
def test_bfs_out_of_range_raises(self):
|
|
229
|
+
edge_index = torch.tensor([[0, 1], [1, 5]], dtype=torch.long)
|
|
230
|
+
with pytest.raises(ValueError, match="out of range"):
|
|
231
|
+
bfs_layers(edge_index, source=0, num_nodes=3)
|
|
232
|
+
|
|
233
|
+
def test_shortest_path_out_of_range_raises(self):
|
|
234
|
+
edge_index = torch.tensor([[0, 1], [1, 5]], dtype=torch.long)
|
|
235
|
+
with pytest.raises(ValueError, match="out of range"):
|
|
236
|
+
shortest_path_length(edge_index, source=0, num_nodes=3)
|
|
237
|
+
|
|
238
|
+
def test_large_sparse_graph_smoke(self):
|
|
239
|
+
# 1000-node ring, both directions.
|
|
240
|
+
N = 1000
|
|
241
|
+
src = torch.arange(N)
|
|
242
|
+
dst = (src + 1) % N
|
|
243
|
+
edge_index = torch.stack([
|
|
244
|
+
torch.cat([src, dst]),
|
|
245
|
+
torch.cat([dst, src]),
|
|
246
|
+
], dim=0)
|
|
247
|
+
labels = connected_components(edge_index, num_nodes=N)
|
|
248
|
+
assert int(number_connected_components(edge_index, N)) == 1
|
|
249
|
+
d = shortest_path_length(edge_index, source=0, num_nodes=N)
|
|
250
|
+
# Max distance in a ring is N//2.
|
|
251
|
+
assert d.min().item() == 0
|
|
252
|
+
assert d.max().item() == N // 2
|
|
253
|
+
|
|
254
|
+
@pytest.mark.skipif(
|
|
255
|
+
not pytest.importorskip("networkx", reason="networkx not installed"),
|
|
256
|
+
reason="networkx not installed",
|
|
257
|
+
)
|
|
258
|
+
def test_parity_with_networkx_components(self):
|
|
259
|
+
"""Optional parity test if networkx is installed."""
|
|
260
|
+
import networkx as nx
|
|
261
|
+
G = nx.Graph()
|
|
262
|
+
edges = [(0, 1), (1, 2), (4, 5)]
|
|
263
|
+
G.add_edges_from(edges)
|
|
264
|
+
# All 7 nodes must be present in both graphs for a fair comparison.
|
|
265
|
+
for n in range(7):
|
|
266
|
+
G.add_node(n)
|
|
267
|
+
edge_index = torch.tensor(
|
|
268
|
+
[[u for u, v in edges] + [v for u, v in edges],
|
|
269
|
+
[v for u, v in edges] + [u for u, v in edges]],
|
|
270
|
+
dtype=torch.long,
|
|
271
|
+
)
|
|
272
|
+
labels = connected_components(edge_index, num_nodes=7)
|
|
273
|
+
nx_count = nx.number_connected_components(G)
|
|
274
|
+
tgx_count = int(labels.max().item()) + 1
|
|
275
|
+
assert tgx_count == nx_count
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Tests for tgraphx.algorithms.structural — degree utilities (v0.3.2 beta).
|
|
2
|
+
|
|
3
|
+
Invariants tested
|
|
4
|
+
-----------------
|
|
5
|
+
- out-degree, in-degree, total-degree all correct on directed graph.
|
|
6
|
+
- Isolated nodes have degree 0.
|
|
7
|
+
- Self-loops count correctly.
|
|
8
|
+
- Empty edge_index returns all-zero degrees.
|
|
9
|
+
- dtype and device preserved.
|
|
10
|
+
- degree_features shape and log_scale behavior.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from tgraphx.algorithms import degree, degree_features
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestDegree:
|
|
21
|
+
def test_out_degree_chain(self):
|
|
22
|
+
# 0 → 1 → 2 → 3
|
|
23
|
+
ei = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
24
|
+
d = degree(ei, num_nodes=4, mode="out")
|
|
25
|
+
assert d.tolist() == [1, 1, 1, 0]
|
|
26
|
+
|
|
27
|
+
def test_in_degree_chain(self):
|
|
28
|
+
ei = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
29
|
+
d = degree(ei, num_nodes=4, mode="in")
|
|
30
|
+
assert d.tolist() == [0, 1, 1, 1]
|
|
31
|
+
|
|
32
|
+
def test_total_degree_chain(self):
|
|
33
|
+
ei = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long)
|
|
34
|
+
d = degree(ei, num_nodes=4, mode="both")
|
|
35
|
+
assert d.tolist() == [1, 2, 2, 1]
|
|
36
|
+
|
|
37
|
+
def test_isolated_nodes_zero(self):
|
|
38
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
39
|
+
d = degree(ei, num_nodes=5, mode="out")
|
|
40
|
+
assert d.tolist() == [0, 0, 0, 0, 0]
|
|
41
|
+
|
|
42
|
+
def test_empty_edge_index_all_zero(self):
|
|
43
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
44
|
+
d = degree(ei, num_nodes=3)
|
|
45
|
+
assert d.shape == (3,)
|
|
46
|
+
assert d.sum().item() == 0
|
|
47
|
+
|
|
48
|
+
def test_self_loop_counts_out_and_in(self):
|
|
49
|
+
# Self-loop at node 0.
|
|
50
|
+
ei = torch.tensor([[0], [0]], dtype=torch.long)
|
|
51
|
+
assert degree(ei, num_nodes=2, mode="out").tolist() == [1, 0]
|
|
52
|
+
assert degree(ei, num_nodes=2, mode="in").tolist() == [1, 0]
|
|
53
|
+
assert degree(ei, num_nodes=2, mode="both").tolist() == [2, 0]
|
|
54
|
+
|
|
55
|
+
def test_dtype_override(self):
|
|
56
|
+
ei = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
57
|
+
d = degree(ei, num_nodes=3, dtype=torch.float32)
|
|
58
|
+
assert d.dtype == torch.float32
|
|
59
|
+
|
|
60
|
+
def test_inferred_num_nodes(self):
|
|
61
|
+
ei = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
62
|
+
d = degree(ei) # infers num_nodes=3
|
|
63
|
+
assert d.shape == (3,)
|
|
64
|
+
|
|
65
|
+
def test_out_degree_star(self):
|
|
66
|
+
# Hub at node 0 connected to 1,2,3.
|
|
67
|
+
ei = torch.tensor([[0, 0, 0], [1, 2, 3]], dtype=torch.long)
|
|
68
|
+
d = degree(ei, num_nodes=4, mode="out")
|
|
69
|
+
assert d.tolist() == [3, 0, 0, 0]
|
|
70
|
+
|
|
71
|
+
def test_invalid_mode(self):
|
|
72
|
+
ei = torch.tensor([[0], [1]], dtype=torch.long)
|
|
73
|
+
with pytest.raises(ValueError, match="mode"):
|
|
74
|
+
degree(ei, num_nodes=2, mode="all")
|
|
75
|
+
|
|
76
|
+
def test_invalid_edge_index_shape(self):
|
|
77
|
+
with pytest.raises(ValueError, match="\\[2, E\\]"):
|
|
78
|
+
degree(torch.zeros(3, 4, dtype=torch.long), num_nodes=4)
|
|
79
|
+
|
|
80
|
+
def test_edge_id_out_of_range(self):
|
|
81
|
+
ei = torch.tensor([[0, 5], [1, 2]], dtype=torch.long)
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
degree(ei, num_nodes=4, mode="out")
|
|
84
|
+
|
|
85
|
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
|
86
|
+
def test_cuda_device_preserved(self):
|
|
87
|
+
ei = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long).cuda()
|
|
88
|
+
d = degree(ei, num_nodes=4, mode="out")
|
|
89
|
+
assert d.device.type == "cuda"
|
|
90
|
+
assert d.tolist() == [1, 1, 1, 0]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestDegreeFeatures:
|
|
94
|
+
def test_shape(self):
|
|
95
|
+
ei = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
96
|
+
feats = degree_features(ei, num_nodes=3)
|
|
97
|
+
assert feats.shape == (3, 3)
|
|
98
|
+
|
|
99
|
+
def test_columns(self):
|
|
100
|
+
# 0 → 1 → 2
|
|
101
|
+
ei = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
102
|
+
feats = degree_features(ei, num_nodes=3)
|
|
103
|
+
# out_deg: [1,1,0]; in_deg: [0,1,1]; total: [1,2,1]
|
|
104
|
+
assert feats[:, 0].tolist() == [1, 1, 0]
|
|
105
|
+
assert feats[:, 1].tolist() == [0, 1, 1]
|
|
106
|
+
assert feats[:, 2].tolist() == [1, 2, 1]
|
|
107
|
+
|
|
108
|
+
def test_log_scale(self):
|
|
109
|
+
ei = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
110
|
+
feats = degree_features(ei, num_nodes=3, log_scale=True)
|
|
111
|
+
assert feats.dtype == torch.float32
|
|
112
|
+
# All values should be >= 0 (log1p of non-negative integers).
|
|
113
|
+
assert (feats >= 0).all()
|
|
114
|
+
|
|
115
|
+
def test_empty_graph(self):
|
|
116
|
+
feats = degree_features(torch.zeros((2, 0), dtype=torch.long), num_nodes=4)
|
|
117
|
+
assert feats.shape == (4, 3)
|
|
118
|
+
assert feats.sum().item() == 0
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Tests for hard_negative_sampling (v0.3.2 beta).
|
|
2
|
+
|
|
3
|
+
Invariants tested
|
|
4
|
+
-----------------
|
|
5
|
+
- No false negatives (no positive edge returned).
|
|
6
|
+
- No self-loops.
|
|
7
|
+
- No duplicates in output.
|
|
8
|
+
- Hard negatives have higher mean similarity than random negatives on a
|
|
9
|
+
toy embedding where similarity is strongly correlated with id proximity.
|
|
10
|
+
- Deterministic with seed.
|
|
11
|
+
- No global RNG pollution.
|
|
12
|
+
- force_undirected semantics.
|
|
13
|
+
- candidate_pool_size exhaustion produces warning.
|
|
14
|
+
- Device preserved (CUDA optional).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import warnings
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
import torch
|
|
22
|
+
|
|
23
|
+
from tgraphx import hard_negative_sampling, negative_sampling
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _chain_graph(N: int):
|
|
27
|
+
ei = torch.tensor([[i for i in range(N - 1)], [i + 1 for i in range(N - 1)]], dtype=torch.long)
|
|
28
|
+
emb = torch.zeros(N, 4)
|
|
29
|
+
for i in range(N):
|
|
30
|
+
emb[i, 0] = float(i) / N
|
|
31
|
+
return ei, emb, N
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestHardNegativeSampling:
|
|
35
|
+
def test_no_false_negatives(self):
|
|
36
|
+
ei, emb, N = _chain_graph(8)
|
|
37
|
+
neg = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=0)
|
|
38
|
+
pos = {(int(ei[0, c]), int(ei[1, c])) for c in range(ei.size(1))}
|
|
39
|
+
for c in range(neg.size(1)):
|
|
40
|
+
assert (int(neg[0, c]), int(neg[1, c])) not in pos
|
|
41
|
+
|
|
42
|
+
def test_no_self_loops(self):
|
|
43
|
+
ei, emb, N = _chain_graph(8)
|
|
44
|
+
neg = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=0)
|
|
45
|
+
for c in range(neg.size(1)):
|
|
46
|
+
assert int(neg[0, c]) != int(neg[1, c])
|
|
47
|
+
|
|
48
|
+
def test_no_duplicates(self):
|
|
49
|
+
ei, emb, N = _chain_graph(10)
|
|
50
|
+
neg = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=8, seed=0)
|
|
51
|
+
pairs = [(int(neg[0, c]), int(neg[1, c])) for c in range(neg.size(1))]
|
|
52
|
+
assert len(pairs) == len(set(pairs))
|
|
53
|
+
|
|
54
|
+
def test_harder_than_random(self):
|
|
55
|
+
ei, emb, N = _chain_graph(10)
|
|
56
|
+
emb_norm = emb / (emb.norm(dim=1, keepdim=True) + 1e-8)
|
|
57
|
+
|
|
58
|
+
neg_hard = hard_negative_sampling(
|
|
59
|
+
ei, emb, num_nodes=N, num_neg_samples=4,
|
|
60
|
+
candidate_pool_size=512, seed=0,
|
|
61
|
+
)
|
|
62
|
+
neg_rand = negative_sampling(ei, N, num_neg_samples=4, seed=0)
|
|
63
|
+
|
|
64
|
+
def mean_sim(ne):
|
|
65
|
+
if ne.size(1) == 0:
|
|
66
|
+
return 0.0
|
|
67
|
+
return float((emb_norm[ne[0]] * emb_norm[ne[1]]).sum(dim=1).mean().item())
|
|
68
|
+
|
|
69
|
+
# Hard negatives should have at least as high mean similarity as random.
|
|
70
|
+
assert mean_sim(neg_hard) >= mean_sim(neg_rand) - 0.05
|
|
71
|
+
|
|
72
|
+
def test_determinism(self):
|
|
73
|
+
ei, emb, N = _chain_graph(8)
|
|
74
|
+
a = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=7)
|
|
75
|
+
b = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=7)
|
|
76
|
+
assert torch.equal(a, b)
|
|
77
|
+
|
|
78
|
+
def test_different_seeds_different_output(self):
|
|
79
|
+
ei, emb, N = _chain_graph(12)
|
|
80
|
+
a = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=1)
|
|
81
|
+
b = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=2)
|
|
82
|
+
# Different seeds should (very likely) produce different results.
|
|
83
|
+
assert not torch.equal(a, b)
|
|
84
|
+
|
|
85
|
+
def test_no_global_rng_pollution(self):
|
|
86
|
+
ei, emb, N = _chain_graph(8)
|
|
87
|
+
torch.manual_seed(99)
|
|
88
|
+
before = torch.rand(3)
|
|
89
|
+
torch.manual_seed(99)
|
|
90
|
+
hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, seed=42)
|
|
91
|
+
after = torch.rand(3)
|
|
92
|
+
assert torch.equal(before, after)
|
|
93
|
+
|
|
94
|
+
def test_output_dtype_and_device(self):
|
|
95
|
+
ei, emb, N = _chain_graph(6)
|
|
96
|
+
neg = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=2, seed=0)
|
|
97
|
+
assert neg.dtype == torch.long
|
|
98
|
+
assert neg.device == ei.device
|
|
99
|
+
|
|
100
|
+
def test_force_undirected_no_reverse_positives(self):
|
|
101
|
+
ei = torch.tensor([[0, 1], [1, 0]], dtype=torch.long)
|
|
102
|
+
emb = torch.randn(4, 4)
|
|
103
|
+
neg = hard_negative_sampling(
|
|
104
|
+
ei, emb, num_nodes=4, num_neg_samples=4,
|
|
105
|
+
force_undirected=True, seed=0,
|
|
106
|
+
)
|
|
107
|
+
forbidden = {(0, 1), (1, 0)}
|
|
108
|
+
for c in range(neg.size(1)):
|
|
109
|
+
assert (int(neg[0, c]), int(neg[1, c])) not in forbidden
|
|
110
|
+
|
|
111
|
+
def test_cosine_vs_dot_different(self):
|
|
112
|
+
ei, emb, N = _chain_graph(8)
|
|
113
|
+
c = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, method="cosine", seed=0)
|
|
114
|
+
d = hard_negative_sampling(ei, emb, num_nodes=N, num_neg_samples=4, method="dot", seed=0)
|
|
115
|
+
# Both should be valid (no false negatives).
|
|
116
|
+
pos = {(int(ei[0, i]), int(ei[1, i])) for i in range(ei.size(1))}
|
|
117
|
+
for ne in (c, d):
|
|
118
|
+
for col in range(ne.size(1)):
|
|
119
|
+
assert (int(ne[0, col]), int(ne[1, col])) not in pos
|
|
120
|
+
|
|
121
|
+
def test_small_pool_warns(self):
|
|
122
|
+
ei, emb, N = _chain_graph(8)
|
|
123
|
+
with warnings.catch_warnings(record=True) as w:
|
|
124
|
+
warnings.simplefilter("always")
|
|
125
|
+
neg = hard_negative_sampling(
|
|
126
|
+
ei, emb, num_nodes=N, num_neg_samples=100,
|
|
127
|
+
candidate_pool_size=4, seed=0,
|
|
128
|
+
)
|
|
129
|
+
# Should warn about returning fewer than requested.
|
|
130
|
+
assert any("candidate_pool_size" in str(warning.message) or
|
|
131
|
+
"hard_negative_sampling" in str(warning.message)
|
|
132
|
+
for warning in w) or neg.size(1) < 100
|
|
133
|
+
|
|
134
|
+
def test_invalid_method(self):
|
|
135
|
+
ei, emb, N = _chain_graph(4)
|
|
136
|
+
with pytest.raises(ValueError, match="method"):
|
|
137
|
+
hard_negative_sampling(ei, emb, num_nodes=N, method="euclidean")
|
|
138
|
+
|
|
139
|
+
def test_invalid_pool_size(self):
|
|
140
|
+
ei, emb, N = _chain_graph(4)
|
|
141
|
+
with pytest.raises(ValueError, match="candidate_pool_size"):
|
|
142
|
+
hard_negative_sampling(ei, emb, num_nodes=N, candidate_pool_size=0)
|
|
143
|
+
|
|
144
|
+
def test_embeddings_too_small(self):
|
|
145
|
+
ei = torch.tensor([[0, 1], [1, 2]], dtype=torch.long)
|
|
146
|
+
emb = torch.zeros(2, 4) # only 2 embeddings for 3 nodes
|
|
147
|
+
with pytest.raises(ValueError, match="node_embeddings"):
|
|
148
|
+
hard_negative_sampling(ei, emb, num_nodes=3)
|
|
149
|
+
|
|
150
|
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
|
151
|
+
def test_cuda_device_preserved(self):
|
|
152
|
+
ei = torch.tensor([[0, 1, 2], [1, 2, 3]], dtype=torch.long).cuda()
|
|
153
|
+
emb = torch.randn(5, 4).cuda()
|
|
154
|
+
neg = hard_negative_sampling(ei, emb, num_nodes=5, num_neg_samples=4, seed=0)
|
|
155
|
+
assert neg.device.type == "cuda"
|