tgraphx 0.4.0__tar.gz → 0.4.2__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.4.0 → tgraphx-0.4.2}/PKG-INFO +1 -1
- {tgraphx-0.4.0 → tgraphx-0.4.2}/pyproject.toml +1 -1
- tgraphx-0.4.2/tests/test_mining_centrality.py +215 -0
- tgraphx-0.4.2/tests/test_mining_generators_spectral.py +259 -0
- tgraphx-0.4.2/tests/test_neural_mining_batched.py +174 -0
- tgraphx-0.4.2/tests/test_reproducibility.py +244 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/__init__.py +1 -1
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/app.py +50 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/static/dashboard.css +75 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/static/dashboard.js +256 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/__init__.py +120 -0
- tgraphx-0.4.2/tgraphx/mining/api.py +234 -0
- tgraphx-0.4.2/tgraphx/mining/centrality.py +608 -0
- tgraphx-0.4.2/tgraphx/mining/embeddings.py +207 -0
- tgraphx-0.4.2/tgraphx/mining/generators.py +468 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/kernels.py +43 -14
- tgraphx-0.4.2/tgraphx/mining/label_prop.py +194 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/neural.py +81 -1
- tgraphx-0.4.2/tgraphx/mining/spectral.py +340 -0
- tgraphx-0.4.2/tgraphx/reproducibility.py +284 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/PKG-INFO +1 -1
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/SOURCES.txt +11 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/LICENSE +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/README.md +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/setup.cfg +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_3d_support.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_algorithms.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_amp_compile.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_backward_compatibility.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_benchmark_smoke.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_chunking.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dashboard.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dashboard_metadata.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_cache.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_docs_claims.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_download_mocked.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_registry.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_datasets_base.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_devices.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_distributed_compat.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_documentation_claims.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_edge_features.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_edge_weight.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_experiments.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_explainability.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_factories.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_folder_datasets.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_gat_chunking.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_gnn_families.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_gradients.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_api.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_builders.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_transformer_v027.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_utils.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hard_negative_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hetero_batch.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hetero_layers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hetero_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_imports.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_layers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_math.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_math_invariants_v030.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_metrics.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_mining_core.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_mining_structural.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_model_zoo.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_models.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_negative_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_neural_mining.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_packaging.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_performance_smoke.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_plotting.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_public_benchmarks.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_pyg_dgl_ogb_wrappers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_random_walk_sample.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_release_validation_v030.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_sampling_loaders.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_synthetic_datasets.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_temporal_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_temporal_v025.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_time_encoding.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_tiny_overfit_v030.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_torchvision_wrappers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_tracking.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_training.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_transforms.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_v024_features.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/connectivity.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/structural.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/traversal.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/dataloader.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/graph.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/graph_utils.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/hetero_batch.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/hetero_graph.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/temporal.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/temporal_batch.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/utils.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/__main__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/base.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/cache.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/converters.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/dgl_wrappers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/download.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/errors.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/folder.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/metadata.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/ogb_wrappers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/pyg_wrappers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/registry.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/synthetic.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/torchvision_wrappers.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/distributed.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/callbacks.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/checkpoints.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/cli.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/config.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/grid.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/runner.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/summarize.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/attention.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/edge_attribution.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/export.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/integrated_gradients.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/patch_heatmap.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/saliency.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/utils.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/graph_builders.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/hetero_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/interop.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/_dim.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/_scatter.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/aggregator.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/appnp.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/attention_message.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/base.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/conv_message.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/factory.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/gat.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/gatv2.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/gin.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/graph_transformer.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/hetero.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/hetero_readout.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/pooling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/safe_pool.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/sage.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/temporal_readout.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/transformer_encodings.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/vector_gcn.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/learned_graph.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/classification.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/link_prediction.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/ogb.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/ranking.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/regression.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/reports.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/anomaly.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/communities.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/frequent.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/hetero.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/link_prediction.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/motifs.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/patterns.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/prototype.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/random_walk.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/reports.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/similarity.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/structural.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/temporal.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/cnn_encoder.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/cnn_gnn_model.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/edge_predictor.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/factory.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/graph_classifier.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/hetero_models.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/model_zoo.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/node_classifier.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/pre_encoder.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/regressors.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/temporal_models.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/performance.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/graph.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/layouts.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/mining.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/utils.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/sampling_loaders.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/sampling_negative.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/temporal/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/temporal/time_encoding.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/temporal_sampling.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/tracking.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/training.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/__init__.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/compose.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/features.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/graph.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/patch.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/positional.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/splits.py +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/dependency_links.txt +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/entry_points.txt +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/requires.txt +0 -0
- {tgraphx-0.4.0 → tgraphx-0.4.2}/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.4.
|
|
3
|
+
Version: 0.4.2
|
|
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>
|
|
@@ -5,7 +5,7 @@ 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.4.
|
|
8
|
+
version = "0.4.2"
|
|
9
9
|
description = "Tensor-aware graph neural networks preserving spatial node feature layouts"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Tests for tgraphx.mining.centrality — centrality algorithms."""
|
|
2
|
+
import math
|
|
3
|
+
import pytest
|
|
4
|
+
import torch
|
|
5
|
+
from tgraphx.mining import (
|
|
6
|
+
degree_centrality, pagerank, personalized_pagerank, hits,
|
|
7
|
+
katz_centrality, closeness_centrality, harmonic_centrality,
|
|
8
|
+
betweenness_centrality, eigenvector_centrality, k_core_numbers,
|
|
9
|
+
in_degree_centrality, out_degree_centrality,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _chain(N=4):
|
|
14
|
+
src = list(range(N-1)) + list(range(1, N))
|
|
15
|
+
dst = list(range(1, N)) + list(range(N-1))
|
|
16
|
+
return torch.tensor([src, dst], dtype=torch.long), N
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _star(N=5):
|
|
20
|
+
src = [0]*(N-1) + list(range(1, N))
|
|
21
|
+
dst = list(range(1, N)) + [0]*(N-1)
|
|
22
|
+
return torch.tensor([src, dst], dtype=torch.long), N
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _triangle():
|
|
26
|
+
ei = torch.tensor([[0,1,2,1,2,0],[1,2,0,0,1,2]], dtype=torch.long)
|
|
27
|
+
return ei, 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestDegreeCentrality:
|
|
31
|
+
def test_star_hub_highest(self):
|
|
32
|
+
ei, N = _star()
|
|
33
|
+
dc = degree_centrality(ei, N, directed=False)
|
|
34
|
+
assert float(dc[0]) == float(dc.max())
|
|
35
|
+
|
|
36
|
+
def test_range_zero_one(self):
|
|
37
|
+
ei, N = _chain()
|
|
38
|
+
dc = degree_centrality(ei, N)
|
|
39
|
+
assert (dc >= 0).all() and (dc <= 1).all()
|
|
40
|
+
|
|
41
|
+
def test_isolated_zero(self):
|
|
42
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
43
|
+
dc = degree_centrality(ei, 5)
|
|
44
|
+
assert dc.sum() == 0.0
|
|
45
|
+
|
|
46
|
+
def test_complete_all_one(self):
|
|
47
|
+
from tgraphx.mining import complete_graph
|
|
48
|
+
ei, N = complete_graph(4)
|
|
49
|
+
dc = degree_centrality(ei, N, directed=False)
|
|
50
|
+
assert all(abs(float(v) - 1.0) < 1e-5 for v in dc.tolist())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestPageRank:
|
|
54
|
+
def test_sums_to_one(self):
|
|
55
|
+
ei, N = _chain()
|
|
56
|
+
pr = pagerank(ei, N)
|
|
57
|
+
assert abs(float(pr.sum()) - 1.0) < 1e-4
|
|
58
|
+
|
|
59
|
+
def test_non_negative(self):
|
|
60
|
+
ei, N = _chain()
|
|
61
|
+
pr = pagerank(ei, N)
|
|
62
|
+
assert (pr >= 0).all()
|
|
63
|
+
|
|
64
|
+
def test_uniform_on_symmetric_complete(self):
|
|
65
|
+
from tgraphx.mining import complete_graph
|
|
66
|
+
ei, N = complete_graph(4)
|
|
67
|
+
pr = pagerank(ei, N)
|
|
68
|
+
assert pr.std().item() < 0.01 # all equal for symmetric complete graph
|
|
69
|
+
|
|
70
|
+
def test_empty_graph(self):
|
|
71
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
72
|
+
pr = pagerank(ei, 4)
|
|
73
|
+
# All equal: 1/N
|
|
74
|
+
assert pr.shape == (4,)
|
|
75
|
+
assert (pr >= 0).all()
|
|
76
|
+
|
|
77
|
+
def test_deterministic(self):
|
|
78
|
+
ei, N = _chain()
|
|
79
|
+
pr1 = pagerank(ei, N)
|
|
80
|
+
pr2 = pagerank(ei, N)
|
|
81
|
+
assert torch.equal(pr1, pr2)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestPersonalisedPageRank:
|
|
85
|
+
def test_teleport_to_seed(self):
|
|
86
|
+
"""PPR from node 0 should assign higher weight to node 0's side."""
|
|
87
|
+
ei, N = _chain(6)
|
|
88
|
+
pers = torch.zeros(6)
|
|
89
|
+
pers[0] = 1.0
|
|
90
|
+
ppr = personalized_pagerank(ei, N, pers)
|
|
91
|
+
assert float(ppr[0]) >= float(ppr[N-1])
|
|
92
|
+
|
|
93
|
+
def test_sums_to_one(self):
|
|
94
|
+
ei, N = _chain()
|
|
95
|
+
pers = torch.ones(N)
|
|
96
|
+
ppr = personalized_pagerank(ei, N, pers)
|
|
97
|
+
assert abs(float(ppr.sum()) - 1.0) < 1e-4
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestHITS:
|
|
101
|
+
def test_shapes(self):
|
|
102
|
+
ei, N = _chain()
|
|
103
|
+
h, a = hits(ei, N)
|
|
104
|
+
assert h.shape == (N,) and a.shape == (N,)
|
|
105
|
+
|
|
106
|
+
def test_non_negative(self):
|
|
107
|
+
ei, N = _chain()
|
|
108
|
+
h, a = hits(ei, N)
|
|
109
|
+
assert (h >= 0).all() and (a >= 0).all()
|
|
110
|
+
|
|
111
|
+
def test_empty_graph(self):
|
|
112
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
113
|
+
h, a = hits(ei, 3)
|
|
114
|
+
assert h.shape == (3,) and a.shape == (3,)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestKatzCentrality:
|
|
118
|
+
def test_non_negative(self):
|
|
119
|
+
ei, N = _chain()
|
|
120
|
+
k = katz_centrality(ei, N)
|
|
121
|
+
assert (k >= 0).all()
|
|
122
|
+
|
|
123
|
+
def test_higher_degree_higher_katz(self):
|
|
124
|
+
# In a chain, middle nodes should have >= katz as leaves.
|
|
125
|
+
ei, N = _chain(5)
|
|
126
|
+
k = katz_centrality(ei, N)
|
|
127
|
+
assert float(k[0]) <= float(k[2]) # leaf <= middle
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestClosenessCentrality:
|
|
131
|
+
def test_hand_computed_triangle(self):
|
|
132
|
+
"""All K3 nodes have closeness = 1 (distance to each other node = 1)."""
|
|
133
|
+
ei, N = _triangle()
|
|
134
|
+
cc = closeness_centrality(ei, N)
|
|
135
|
+
for v in cc.tolist():
|
|
136
|
+
assert abs(v - 1.0) < 1e-5
|
|
137
|
+
|
|
138
|
+
def test_path_center_highest(self):
|
|
139
|
+
ei, N = _chain(5) # 0-1-2-3-4
|
|
140
|
+
cc = closeness_centrality(ei, N)
|
|
141
|
+
# Node 2 (center) has highest closeness.
|
|
142
|
+
assert int(cc.argmax().item()) == 2
|
|
143
|
+
|
|
144
|
+
def test_size_guard(self):
|
|
145
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
146
|
+
with pytest.raises(ValueError, match="max_nodes_exact"):
|
|
147
|
+
closeness_centrality(ei, 2001, max_nodes_exact=2000)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestHarmonicCentrality:
|
|
151
|
+
def test_non_negative(self):
|
|
152
|
+
ei, N = _chain()
|
|
153
|
+
hc = harmonic_centrality(ei, N)
|
|
154
|
+
assert (hc >= 0).all()
|
|
155
|
+
|
|
156
|
+
def test_star_hub_highest(self):
|
|
157
|
+
ei, N = _star()
|
|
158
|
+
hc = harmonic_centrality(ei, N)
|
|
159
|
+
assert int(hc.argmax().item()) == 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestBetweennessCentrality:
|
|
163
|
+
def test_star_hub_highest(self):
|
|
164
|
+
"""Hub node 0 lies on all shortest paths between leaves."""
|
|
165
|
+
ei, N = _star()
|
|
166
|
+
bc = betweenness_centrality(ei, N)
|
|
167
|
+
assert int(bc.argmax().item()) == 0
|
|
168
|
+
|
|
169
|
+
def test_path_middle_highest(self):
|
|
170
|
+
ei, N = _chain(5)
|
|
171
|
+
bc = betweenness_centrality(ei, N)
|
|
172
|
+
# Middle node 2 has highest betweenness in a 5-node path.
|
|
173
|
+
assert int(bc.argmax().item()) == 2
|
|
174
|
+
|
|
175
|
+
def test_triangle_all_zero(self):
|
|
176
|
+
"""In K3, no node lies strictly between two others (only direct edges)."""
|
|
177
|
+
ei, N = _triangle()
|
|
178
|
+
bc = betweenness_centrality(ei, N)
|
|
179
|
+
assert bc.max().item() < 1e-6
|
|
180
|
+
|
|
181
|
+
def test_size_guard(self):
|
|
182
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
183
|
+
with pytest.raises(ValueError, match="max_nodes_exact"):
|
|
184
|
+
betweenness_centrality(ei, 501, max_nodes_exact=500)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestEigenvectorCentrality:
|
|
188
|
+
def test_non_negative(self):
|
|
189
|
+
ei, N = _chain()
|
|
190
|
+
ec = eigenvector_centrality(ei, N)
|
|
191
|
+
assert (ec >= 0).all()
|
|
192
|
+
|
|
193
|
+
def test_star_hub_highest(self):
|
|
194
|
+
ei, N = _star()
|
|
195
|
+
ec = eigenvector_centrality(ei, N)
|
|
196
|
+
assert int(ec.argmax().item()) == 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestKCoreNumbers:
|
|
200
|
+
def test_path_core_one(self):
|
|
201
|
+
"""In a path graph all nodes have k-core = 1 (or 0 for isolated)."""
|
|
202
|
+
ei, N = _chain()
|
|
203
|
+
kc = k_core_numbers(ei, N)
|
|
204
|
+
assert kc.max().item() == 1
|
|
205
|
+
|
|
206
|
+
def test_triangle_core_two(self):
|
|
207
|
+
"""In K3 all nodes have k-core = 2."""
|
|
208
|
+
ei, N = _triangle()
|
|
209
|
+
kc = k_core_numbers(ei, N)
|
|
210
|
+
assert kc.min().item() == 2 and kc.max().item() == 2
|
|
211
|
+
|
|
212
|
+
def test_isolated_core_zero(self):
|
|
213
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
214
|
+
kc = k_core_numbers(ei, 5)
|
|
215
|
+
assert kc.max().item() == 0
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Tests for graph generators, spectral analysis, label propagation, and embeddings."""
|
|
2
|
+
import pytest
|
|
3
|
+
import torch
|
|
4
|
+
from tgraphx.mining import (
|
|
5
|
+
erdos_renyi_graph, barabasi_albert_graph, stochastic_block_model_graph,
|
|
6
|
+
watts_strogatz_graph, complete_graph, cycle_graph, path_graph, star_graph,
|
|
7
|
+
karate_club_graph, synthetic_anomaly_graph, motif_injected_graph,
|
|
8
|
+
graph_laplacian, normalized_laplacian, laplacian_eigenvalues,
|
|
9
|
+
fiedler_vector, algebraic_connectivity, laplacian_eigvec_positional_encoding,
|
|
10
|
+
spectral_clustering, spectral_distance, dirichlet_energy,
|
|
11
|
+
label_propagation, LabelPropagationClassifier,
|
|
12
|
+
extract_node_embeddings, extract_graph_embeddings,
|
|
13
|
+
embedding_similarity_matrix, embedding_pairwise_distances,
|
|
14
|
+
embedding_nearest_neighbors,
|
|
15
|
+
analyze_graph,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestGenerators:
|
|
20
|
+
def test_erdos_renyi_deterministic(self):
|
|
21
|
+
ei1, N1 = erdos_renyi_graph(20, 0.3, seed=0)
|
|
22
|
+
ei2, N2 = erdos_renyi_graph(20, 0.3, seed=0)
|
|
23
|
+
assert torch.equal(ei1, ei2) and N1 == N2 == 20
|
|
24
|
+
|
|
25
|
+
def test_erdos_renyi_density(self):
|
|
26
|
+
"""Dense ER graph should have more edges than sparse one."""
|
|
27
|
+
ei_dense, _ = erdos_renyi_graph(20, 0.8, seed=42)
|
|
28
|
+
ei_sparse, _ = erdos_renyi_graph(20, 0.1, seed=42)
|
|
29
|
+
assert ei_dense.size(1) > ei_sparse.size(1)
|
|
30
|
+
|
|
31
|
+
def test_barabasi_albert_connects(self):
|
|
32
|
+
ei, N = barabasi_albert_graph(20, m=2, seed=0)
|
|
33
|
+
assert N == 20
|
|
34
|
+
assert ei.size(1) > 0
|
|
35
|
+
|
|
36
|
+
def test_sbm_communities_larger_p_in(self):
|
|
37
|
+
"""SBM with higher p_in should have more intra-community edges."""
|
|
38
|
+
ei, N, labels = stochastic_block_model_graph([5, 5], p_in=0.8, p_out=0.05, seed=0)
|
|
39
|
+
intra = sum(int(labels[ei[0,k]]) == int(labels[ei[1,k]]) for k in range(ei.size(1)))
|
|
40
|
+
inter = ei.size(1) - intra
|
|
41
|
+
assert intra > inter
|
|
42
|
+
|
|
43
|
+
def test_watts_strogatz_shape(self):
|
|
44
|
+
ei, N = watts_strogatz_graph(10, k=4, p=0.1, seed=0)
|
|
45
|
+
assert N == 10
|
|
46
|
+
|
|
47
|
+
def test_complete_graph_edges(self):
|
|
48
|
+
ei, N = complete_graph(4)
|
|
49
|
+
assert ei.size(1) == 4 * 3 # 12 directed edges in K4
|
|
50
|
+
|
|
51
|
+
def test_cycle_graph_edges(self):
|
|
52
|
+
ei, N = cycle_graph(5)
|
|
53
|
+
assert ei.size(1) == 10 # 5 undirected * 2 directions
|
|
54
|
+
|
|
55
|
+
def test_path_graph_edges(self):
|
|
56
|
+
ei, N = path_graph(5)
|
|
57
|
+
assert ei.size(1) == 8 # 4 undirected edges
|
|
58
|
+
|
|
59
|
+
def test_star_graph_hub_degree(self):
|
|
60
|
+
ei, N = star_graph(5)
|
|
61
|
+
deg = torch.zeros(N, dtype=torch.long)
|
|
62
|
+
ones = torch.ones(ei.size(1), dtype=torch.long)
|
|
63
|
+
deg.scatter_add_(0, ei[0], ones)
|
|
64
|
+
assert int(deg[0]) == 4 # hub has degree 4
|
|
65
|
+
|
|
66
|
+
def test_karate_club(self):
|
|
67
|
+
ei, N = karate_club_graph()
|
|
68
|
+
assert N == 34
|
|
69
|
+
|
|
70
|
+
def test_anomaly_graph_mask_matches(self):
|
|
71
|
+
ei, N, mask = synthetic_anomaly_graph(20, num_anomalous=3, seed=0)
|
|
72
|
+
assert int(mask.sum()) == 3
|
|
73
|
+
|
|
74
|
+
def test_motif_injected(self):
|
|
75
|
+
ei, N, tri_nodes = motif_injected_graph(20, num_triangles=2, seed=0)
|
|
76
|
+
assert tri_nodes.numel() == 6
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestSpectral:
|
|
80
|
+
def test_laplacian_positive_semidefinite(self):
|
|
81
|
+
"""Laplacian eigenvalues should be >= 0."""
|
|
82
|
+
ei, N = path_graph(5)
|
|
83
|
+
L = graph_laplacian(ei, N)
|
|
84
|
+
evals = torch.linalg.eigvalsh(L)
|
|
85
|
+
assert (evals >= -1e-6).all()
|
|
86
|
+
|
|
87
|
+
def test_laplacian_row_sums_zero(self):
|
|
88
|
+
"""L * 1 = 0 (row sums are zero)."""
|
|
89
|
+
ei, N = complete_graph(4)
|
|
90
|
+
L = graph_laplacian(ei, N)
|
|
91
|
+
row_sums = L.sum(dim=1)
|
|
92
|
+
assert (row_sums.abs() < 1e-5).all()
|
|
93
|
+
|
|
94
|
+
def test_normalized_laplacian_spectrum_range(self):
|
|
95
|
+
"""Normalised Laplacian eigenvalues should be in [0, 2]."""
|
|
96
|
+
ei, N = cycle_graph(6)
|
|
97
|
+
L_norm = normalized_laplacian(ei, N)
|
|
98
|
+
evals = torch.linalg.eigvalsh(L_norm)
|
|
99
|
+
assert float(evals.min()) >= -1e-6
|
|
100
|
+
assert float(evals.max()) <= 2.0 + 1e-6
|
|
101
|
+
|
|
102
|
+
def test_fiedler_vector_orthogonal_to_ones(self):
|
|
103
|
+
"""Fiedler vector should be approximately orthogonal to the all-ones vector."""
|
|
104
|
+
ei, N = path_graph(6)
|
|
105
|
+
fv, lam2 = fiedler_vector(ei, N)
|
|
106
|
+
dot = float(fv.sum().abs().item())
|
|
107
|
+
assert dot < 0.1
|
|
108
|
+
|
|
109
|
+
def test_algebraic_connectivity_zero_disconnected(self):
|
|
110
|
+
ei = torch.tensor([[0,1],[1,0]], dtype=torch.long) # 2 isolated components
|
|
111
|
+
N = 4 # nodes 2,3 are isolated
|
|
112
|
+
lam2 = algebraic_connectivity(ei, N)
|
|
113
|
+
assert lam2 < 1e-6 # disconnected → λ₂ = 0
|
|
114
|
+
|
|
115
|
+
def test_algebraic_connectivity_positive_connected(self):
|
|
116
|
+
ei, N = complete_graph(4)
|
|
117
|
+
lam2 = algebraic_connectivity(ei, N)
|
|
118
|
+
assert lam2 > 0
|
|
119
|
+
|
|
120
|
+
def test_laplacian_pe_shape(self):
|
|
121
|
+
ei, N = path_graph(8)
|
|
122
|
+
enc = laplacian_eigvec_positional_encoding(ei, N, k=4)
|
|
123
|
+
assert enc.shape == (N, 4)
|
|
124
|
+
assert enc.dtype == torch.float32
|
|
125
|
+
|
|
126
|
+
def test_spectral_clustering_shape(self):
|
|
127
|
+
ei, N, labels = stochastic_block_model_graph([4, 4], p_in=0.8, p_out=0.05, seed=0)
|
|
128
|
+
cluster_labels = spectral_clustering(ei, N, num_clusters=2, seed=0)
|
|
129
|
+
assert cluster_labels.shape == (N,)
|
|
130
|
+
assert set(cluster_labels.tolist()) <= {0, 1}
|
|
131
|
+
|
|
132
|
+
def test_spectral_distance_zero_same_graph(self):
|
|
133
|
+
ei, N = path_graph(5)
|
|
134
|
+
d = spectral_distance(ei, N, ei, N)
|
|
135
|
+
assert d < 1e-5
|
|
136
|
+
|
|
137
|
+
def test_spectral_distance_positive_different(self):
|
|
138
|
+
ei1, N1 = path_graph(5)
|
|
139
|
+
ei2, N2 = complete_graph(5)
|
|
140
|
+
d = spectral_distance(ei1, N1, ei2, N2)
|
|
141
|
+
assert d > 0
|
|
142
|
+
|
|
143
|
+
def test_dirichlet_energy_smooth(self):
|
|
144
|
+
"""Constant node features should have zero Dirichlet energy."""
|
|
145
|
+
ei, N = path_graph(5)
|
|
146
|
+
x = torch.ones(N, 4)
|
|
147
|
+
e = dirichlet_energy(x, ei, N)
|
|
148
|
+
assert abs(e) < 1e-5
|
|
149
|
+
|
|
150
|
+
def test_spectral_size_guard(self):
|
|
151
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
152
|
+
with pytest.raises(ValueError, match="max_nodes"):
|
|
153
|
+
graph_laplacian(ei, 2001, max_nodes=2000)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestLabelPropagation:
|
|
157
|
+
def _two_clique_data(self):
|
|
158
|
+
"""Two connected cliques + labels only on clique centers."""
|
|
159
|
+
# Clique 1: 0,1,2 Clique 2: 3,4,5
|
|
160
|
+
src = [0,0,1,3,3,4,0,1,2,3,4,5]
|
|
161
|
+
dst = [1,2,2,4,5,5,0,1,2,3,4,5]
|
|
162
|
+
ei = torch.tensor([src+dst, dst+src], dtype=torch.long)
|
|
163
|
+
ei = torch.unique(ei, dim=1)
|
|
164
|
+
N = 6
|
|
165
|
+
y = torch.tensor([0, 0, 0, 1, 1, 1], dtype=torch.long)
|
|
166
|
+
# Only reveal label for node 0 and node 3.
|
|
167
|
+
mask = torch.tensor([True, False, False, True, False, False])
|
|
168
|
+
return ei, N, y, mask
|
|
169
|
+
|
|
170
|
+
def test_propagates_correct_labels(self):
|
|
171
|
+
ei, N, y, mask = self._two_clique_data()
|
|
172
|
+
Z = label_propagation(ei, N, y, mask, num_classes=2)
|
|
173
|
+
preds = Z.argmax(dim=1)
|
|
174
|
+
# Nodes 1,2 should get class 0; nodes 4,5 should get class 1.
|
|
175
|
+
assert int(preds[1]) == 0 and int(preds[2]) == 0
|
|
176
|
+
assert int(preds[4]) == 1 and int(preds[5]) == 1
|
|
177
|
+
|
|
178
|
+
def test_output_shape(self):
|
|
179
|
+
ei, N, y, mask = self._two_clique_data()
|
|
180
|
+
Z = label_propagation(ei, N, y, mask, num_classes=2)
|
|
181
|
+
assert Z.shape == (N, 2)
|
|
182
|
+
|
|
183
|
+
def test_labeled_nodes_stay_close_to_seed(self):
|
|
184
|
+
"""Labeled nodes should keep high probability for their own class."""
|
|
185
|
+
ei, N, y, mask = self._two_clique_data()
|
|
186
|
+
Z = label_propagation(ei, N, y, mask, num_classes=2, alpha=0.5)
|
|
187
|
+
# Node 0 labeled class 0 — still high probability class 0.
|
|
188
|
+
assert float(Z[0, 0]) >= float(Z[0, 1])
|
|
189
|
+
|
|
190
|
+
def test_classifier_api(self):
|
|
191
|
+
ei, N, y, mask = self._two_clique_data()
|
|
192
|
+
clf = LabelPropagationClassifier(alpha=0.9)
|
|
193
|
+
Z = clf.fit_predict(ei, N, y, mask, num_classes=2)
|
|
194
|
+
preds = clf.predict()
|
|
195
|
+
assert preds.shape == (N,)
|
|
196
|
+
assert Z.shape == (N, 2)
|
|
197
|
+
|
|
198
|
+
def test_deterministic(self):
|
|
199
|
+
ei, N, y, mask = self._two_clique_data()
|
|
200
|
+
Z1 = label_propagation(ei, N, y, mask, num_classes=2)
|
|
201
|
+
Z2 = label_propagation(ei, N, y, mask, num_classes=2)
|
|
202
|
+
assert torch.equal(Z1, Z2)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestEmbeddings:
|
|
206
|
+
def test_extract_node_embeddings_shape(self):
|
|
207
|
+
import torch.nn as nn
|
|
208
|
+
model = nn.Sequential(nn.Linear(4, 8), nn.ReLU())
|
|
209
|
+
# Model that ignores edge_index for simplicity.
|
|
210
|
+
class _SimpleModel(nn.Module):
|
|
211
|
+
def __init__(self): super().__init__(); self.lin = nn.Linear(4, 8)
|
|
212
|
+
def forward(self, x, ei): return self.lin(x)
|
|
213
|
+
m = _SimpleModel()
|
|
214
|
+
ei = torch.tensor([[0,1],[1,2]], dtype=torch.long)
|
|
215
|
+
x = torch.randn(3, 4)
|
|
216
|
+
emb = extract_node_embeddings(m, ei, x, no_grad=True)
|
|
217
|
+
assert emb.shape == (3, 8)
|
|
218
|
+
assert not emb.requires_grad
|
|
219
|
+
|
|
220
|
+
def test_extract_graph_embeddings_shape(self):
|
|
221
|
+
class _M(torch.nn.Module):
|
|
222
|
+
def __init__(self): super().__init__(); self.lin = torch.nn.Linear(4, 6)
|
|
223
|
+
def forward(self, x, ei): return self.lin(x)
|
|
224
|
+
m = _M()
|
|
225
|
+
graphs = [
|
|
226
|
+
{"node_features": torch.randn(3, 4), "edge_index": torch.tensor([[0],[1]], dtype=torch.long)},
|
|
227
|
+
{"node_features": torch.randn(4, 4), "edge_index": torch.tensor([[0,1],[1,2]], dtype=torch.long)},
|
|
228
|
+
]
|
|
229
|
+
embs = extract_graph_embeddings(m, graphs, pooling="mean")
|
|
230
|
+
assert embs.shape == (2, 6)
|
|
231
|
+
|
|
232
|
+
def test_embedding_similarity_cosine_symmetric(self):
|
|
233
|
+
emb = torch.randn(5, 8)
|
|
234
|
+
S = embedding_similarity_matrix(emb, method="cosine")
|
|
235
|
+
assert S.shape == (5, 5)
|
|
236
|
+
assert torch.allclose(S, S.t(), atol=1e-5)
|
|
237
|
+
# Diagonal should be ~1.
|
|
238
|
+
assert (S.diagonal() - 1.0).abs().max() < 1e-4
|
|
239
|
+
|
|
240
|
+
def test_embedding_pairwise_distances_non_negative(self):
|
|
241
|
+
emb = torch.randn(4, 8)
|
|
242
|
+
D = embedding_pairwise_distances(emb)
|
|
243
|
+
assert (D >= 0).all()
|
|
244
|
+
assert (D.diagonal() < 1e-5).all()
|
|
245
|
+
|
|
246
|
+
def test_embedding_nearest_neighbors(self):
|
|
247
|
+
emb = torch.eye(5) # orthogonal — nearest is self
|
|
248
|
+
idx, scores = embedding_nearest_neighbors(emb, emb, k=2, method="cosine")
|
|
249
|
+
assert idx.shape == (5, 2)
|
|
250
|
+
# First nearest neighbour of each is itself.
|
|
251
|
+
assert (idx[:, 0] == torch.arange(5)).all()
|
|
252
|
+
|
|
253
|
+
def test_analyze_graph_runs(self):
|
|
254
|
+
ei, N = path_graph(8)
|
|
255
|
+
result = analyze_graph(ei, N, include_spectral=True)
|
|
256
|
+
assert "summary" in result
|
|
257
|
+
assert "motifs" in result
|
|
258
|
+
assert "centrality" in result
|
|
259
|
+
assert isinstance(result["summary"], dict)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Tests for batched PrototypeMembershipScorer and neural mining benchmark.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- score_batch_fast output shape matches score_batch
|
|
5
|
+
- backward works through score_batch_fast
|
|
6
|
+
- no cross-graph edge leakage in batched forward
|
|
7
|
+
- benchmark_neural_mining --help and --small --json run
|
|
8
|
+
- benchmark JSON schema validated
|
|
9
|
+
- loss decreased in benchmark tasks
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
from tgraphx.mining import PrototypeMembershipScorer
|
|
22
|
+
|
|
23
|
+
BENCH_DIR = Path(__file__).resolve().parent.parent / "benchmarks" / "mining"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Batched scorer ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_candidates(n: int = 3, N: int = 5, D: int = 4):
|
|
30
|
+
"""Build n candidate graph dicts. N must be >= 5 for the default edge_index."""
|
|
31
|
+
N = max(N, 5) # edge_index goes up to node 4
|
|
32
|
+
ei = torch.tensor([[0,1,2,3],[1,2,3,4]], dtype=torch.long)
|
|
33
|
+
candidates = []
|
|
34
|
+
for _ in range(n):
|
|
35
|
+
x = torch.randn(N, D)
|
|
36
|
+
candidates.append({
|
|
37
|
+
"node_features": x,
|
|
38
|
+
"edge_index": ei,
|
|
39
|
+
"query_idx": N - 1,
|
|
40
|
+
})
|
|
41
|
+
return candidates
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestBatchedMembershipScorer:
|
|
45
|
+
def test_shape_matches_sequential(self):
|
|
46
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
47
|
+
cands = _make_candidates(3)
|
|
48
|
+
seq = model.score_batch(cands)
|
|
49
|
+
fast = model.score_batch_fast(cands)
|
|
50
|
+
assert seq.shape == fast.shape == (3,)
|
|
51
|
+
|
|
52
|
+
def test_values_close_to_sequential(self):
|
|
53
|
+
"""Fast and sequential results should be identical (same forward)."""
|
|
54
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
55
|
+
model.eval()
|
|
56
|
+
cands = _make_candidates(2, N=4, D=4)
|
|
57
|
+
with torch.no_grad():
|
|
58
|
+
seq = model.score_batch(cands)
|
|
59
|
+
fast = model.score_batch_fast(cands)
|
|
60
|
+
assert torch.allclose(seq, fast, atol=1e-5), f"seq={seq} fast={fast}"
|
|
61
|
+
|
|
62
|
+
def test_backward_through_fast(self):
|
|
63
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
64
|
+
cands = _make_candidates(2)
|
|
65
|
+
logits = model.score_batch_fast(cands)
|
|
66
|
+
logits.sum().backward()
|
|
67
|
+
for name, p in model.named_parameters():
|
|
68
|
+
assert p.grad is not None, f"No grad for {name}"
|
|
69
|
+
|
|
70
|
+
def test_gradients_finite(self):
|
|
71
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
72
|
+
cands = _make_candidates(3)
|
|
73
|
+
model.score_batch_fast(cands).sum().backward()
|
|
74
|
+
for p in model.parameters():
|
|
75
|
+
if p.grad is not None:
|
|
76
|
+
assert torch.isfinite(p.grad).all()
|
|
77
|
+
|
|
78
|
+
def test_no_cross_graph_edge(self):
|
|
79
|
+
"""Different query positions should give different logits."""
|
|
80
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
81
|
+
model.eval()
|
|
82
|
+
x = torch.randn(5, 4)
|
|
83
|
+
ei = torch.tensor([[0,1,2,3],[1,2,3,4]], dtype=torch.long)
|
|
84
|
+
cands = [
|
|
85
|
+
{"node_features": x.clone(), "edge_index": ei, "query_idx": 4},
|
|
86
|
+
{"node_features": x.clone(), "edge_index": ei, "query_idx": 0},
|
|
87
|
+
]
|
|
88
|
+
with torch.no_grad():
|
|
89
|
+
logits = model.score_batch_fast(cands)
|
|
90
|
+
# Different query positions → different scores.
|
|
91
|
+
# (Not guaranteed but almost certain with random weights)
|
|
92
|
+
assert logits.shape == (2,)
|
|
93
|
+
assert torch.isfinite(logits).all()
|
|
94
|
+
|
|
95
|
+
def test_empty_raises(self):
|
|
96
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
97
|
+
with pytest.raises(ValueError):
|
|
98
|
+
model.score_batch_fast([])
|
|
99
|
+
with pytest.raises(ValueError):
|
|
100
|
+
model.score_batch([])
|
|
101
|
+
|
|
102
|
+
def test_single_node_graph(self):
|
|
103
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4)
|
|
104
|
+
x = torch.randn(1, 4)
|
|
105
|
+
ei = torch.zeros((2, 0), dtype=torch.long)
|
|
106
|
+
cands = [{"node_features": x, "edge_index": ei, "query_idx": 0}]
|
|
107
|
+
logits = model.score_batch_fast(cands)
|
|
108
|
+
assert logits.shape == (1,)
|
|
109
|
+
assert torch.isfinite(logits).all()
|
|
110
|
+
|
|
111
|
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
|
112
|
+
def test_cuda_fast_batch(self):
|
|
113
|
+
model = PrototypeMembershipScorer(in_dim=4, hidden_dim=8, out_dim=4).cuda()
|
|
114
|
+
ei = torch.tensor([[0,1],[1,2]], dtype=torch.long).cuda()
|
|
115
|
+
cands = [
|
|
116
|
+
{"node_features": torch.randn(3, 4).cuda(), "edge_index": ei, "query_idx": 2},
|
|
117
|
+
{"node_features": torch.randn(3, 4).cuda(), "edge_index": ei, "query_idx": 1},
|
|
118
|
+
]
|
|
119
|
+
logits = model.score_batch_fast(cands)
|
|
120
|
+
assert logits.device.type == "cuda"
|
|
121
|
+
assert logits.shape == (2,)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ── Neural mining benchmark ───────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run_bench(*args):
|
|
128
|
+
cmd = [sys.executable, str(BENCH_DIR / "benchmark_neural_mining.py"), *args]
|
|
129
|
+
return subprocess.run(cmd, capture_output=True, text=True)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestNeuralMiningBenchmark:
|
|
133
|
+
def test_help(self):
|
|
134
|
+
res = _run_bench("--help")
|
|
135
|
+
assert res.returncode == 0
|
|
136
|
+
assert "--small" in res.stdout + res.stderr
|
|
137
|
+
|
|
138
|
+
def test_small_runs(self):
|
|
139
|
+
res = _run_bench("--small")
|
|
140
|
+
assert res.returncode == 0, res.stderr + res.stdout
|
|
141
|
+
|
|
142
|
+
def test_small_json_parseable(self):
|
|
143
|
+
res = _run_bench("--small", "--json")
|
|
144
|
+
assert res.returncode == 0, res.stderr
|
|
145
|
+
data = json.loads(res.stdout)
|
|
146
|
+
assert "benchmark" in data
|
|
147
|
+
assert "tasks" in data
|
|
148
|
+
assert "prototype_membership" in data["tasks"]
|
|
149
|
+
assert "graph_pattern_classifier" in data["tasks"]
|
|
150
|
+
assert "anomaly_autoencoder" in data["tasks"]
|
|
151
|
+
|
|
152
|
+
def test_loss_decreased_on_all_tasks(self):
|
|
153
|
+
res = _run_bench("--small", "--json", "--seed", "42", "--epochs", "5")
|
|
154
|
+
assert res.returncode == 0, res.stderr
|
|
155
|
+
data = json.loads(res.stdout)
|
|
156
|
+
for task_name, task in data["tasks"].items():
|
|
157
|
+
assert task["loss_decreased"], (
|
|
158
|
+
f"Loss did not decrease for {task_name}: "
|
|
159
|
+
f"{task['initial_loss']} → {task['final_loss']}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def test_json_schema(self):
|
|
163
|
+
res = _run_bench("--small", "--json")
|
|
164
|
+
data = json.loads(res.stdout)
|
|
165
|
+
assert data["tgraphx_version"]
|
|
166
|
+
assert data["device"]
|
|
167
|
+
assert isinstance(data["epochs"], int)
|
|
168
|
+
for task in data["tasks"].values():
|
|
169
|
+
for key in ["train_time_s", "initial_loss", "final_loss",
|
|
170
|
+
"loss_decreased", "gradient_health"]:
|
|
171
|
+
assert key in task, f"Missing key {key} in {task}"
|
|
172
|
+
gh = task["gradient_health"]
|
|
173
|
+
assert gh["has_nonzero"], f"All-zero gradients in {task}"
|
|
174
|
+
assert not gh["any_nan"], f"NaN gradients in {task}"
|