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.
Files changed (217) hide show
  1. {tgraphx-0.4.0 → tgraphx-0.4.2}/PKG-INFO +1 -1
  2. {tgraphx-0.4.0 → tgraphx-0.4.2}/pyproject.toml +1 -1
  3. tgraphx-0.4.2/tests/test_mining_centrality.py +215 -0
  4. tgraphx-0.4.2/tests/test_mining_generators_spectral.py +259 -0
  5. tgraphx-0.4.2/tests/test_neural_mining_batched.py +174 -0
  6. tgraphx-0.4.2/tests/test_reproducibility.py +244 -0
  7. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/__init__.py +1 -1
  8. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/app.py +50 -0
  9. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/static/dashboard.css +75 -0
  10. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/static/dashboard.js +256 -0
  11. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/__init__.py +120 -0
  12. tgraphx-0.4.2/tgraphx/mining/api.py +234 -0
  13. tgraphx-0.4.2/tgraphx/mining/centrality.py +608 -0
  14. tgraphx-0.4.2/tgraphx/mining/embeddings.py +207 -0
  15. tgraphx-0.4.2/tgraphx/mining/generators.py +468 -0
  16. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/kernels.py +43 -14
  17. tgraphx-0.4.2/tgraphx/mining/label_prop.py +194 -0
  18. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/neural.py +81 -1
  19. tgraphx-0.4.2/tgraphx/mining/spectral.py +340 -0
  20. tgraphx-0.4.2/tgraphx/reproducibility.py +284 -0
  21. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/PKG-INFO +1 -1
  22. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/SOURCES.txt +11 -0
  23. {tgraphx-0.4.0 → tgraphx-0.4.2}/LICENSE +0 -0
  24. {tgraphx-0.4.0 → tgraphx-0.4.2}/README.md +0 -0
  25. {tgraphx-0.4.0 → tgraphx-0.4.2}/setup.cfg +0 -0
  26. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_3d_support.py +0 -0
  27. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_algorithms.py +0 -0
  28. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_amp_compile.py +0 -0
  29. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_backward_compatibility.py +0 -0
  30. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_benchmark_smoke.py +0 -0
  31. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_chunking.py +0 -0
  32. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dashboard.py +0 -0
  33. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dashboard_metadata.py +0 -0
  34. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_cache.py +0 -0
  35. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_docs_claims.py +0 -0
  36. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_download_mocked.py +0 -0
  37. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_dataset_registry.py +0 -0
  38. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_datasets_base.py +0 -0
  39. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_devices.py +0 -0
  40. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_distributed_compat.py +0 -0
  41. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_documentation_claims.py +0 -0
  42. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_edge_features.py +0 -0
  43. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_edge_weight.py +0 -0
  44. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_experiments.py +0 -0
  45. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_explainability.py +0 -0
  46. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_factories.py +0 -0
  47. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_folder_datasets.py +0 -0
  48. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_gat_chunking.py +0 -0
  49. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_gnn_families.py +0 -0
  50. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_gradients.py +0 -0
  51. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph.py +0 -0
  52. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_api.py +0 -0
  53. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_builders.py +0 -0
  54. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_transformer_v027.py +0 -0
  55. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_graph_utils.py +0 -0
  56. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hard_negative_sampling.py +0 -0
  57. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hetero_batch.py +0 -0
  58. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hetero_layers.py +0 -0
  59. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_hetero_sampling.py +0 -0
  60. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_imports.py +0 -0
  61. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_layers.py +0 -0
  62. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_math.py +0 -0
  63. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_math_invariants_v030.py +0 -0
  64. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_metrics.py +0 -0
  65. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_mining_core.py +0 -0
  66. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_mining_structural.py +0 -0
  67. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_model_zoo.py +0 -0
  68. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_models.py +0 -0
  69. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_negative_sampling.py +0 -0
  70. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_neural_mining.py +0 -0
  71. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_packaging.py +0 -0
  72. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_performance_smoke.py +0 -0
  73. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_plotting.py +0 -0
  74. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_public_benchmarks.py +0 -0
  75. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_pyg_dgl_ogb_wrappers.py +0 -0
  76. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_random_walk_sample.py +0 -0
  77. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_release_validation_v030.py +0 -0
  78. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_sampling.py +0 -0
  79. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_sampling_loaders.py +0 -0
  80. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_synthetic_datasets.py +0 -0
  81. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_temporal_sampling.py +0 -0
  82. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_temporal_v025.py +0 -0
  83. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_time_encoding.py +0 -0
  84. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_tiny_overfit_v030.py +0 -0
  85. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_torchvision_wrappers.py +0 -0
  86. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_tracking.py +0 -0
  87. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_training.py +0 -0
  88. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_transforms.py +0 -0
  89. {tgraphx-0.4.0 → tgraphx-0.4.2}/tests/test_v024_features.py +0 -0
  90. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/__init__.py +0 -0
  91. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/connectivity.py +0 -0
  92. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/structural.py +0 -0
  93. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/algorithms/traversal.py +0 -0
  94. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/__init__.py +0 -0
  95. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/dataloader.py +0 -0
  96. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/graph.py +0 -0
  97. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/graph_utils.py +0 -0
  98. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/hetero_batch.py +0 -0
  99. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/hetero_graph.py +0 -0
  100. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/temporal.py +0 -0
  101. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/temporal_batch.py +0 -0
  102. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/core/utils.py +0 -0
  103. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/__init__.py +0 -0
  104. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/dashboard/__main__.py +0 -0
  105. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/__init__.py +0 -0
  106. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/base.py +0 -0
  107. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/cache.py +0 -0
  108. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/converters.py +0 -0
  109. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/dgl_wrappers.py +0 -0
  110. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/download.py +0 -0
  111. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/errors.py +0 -0
  112. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/folder.py +0 -0
  113. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/metadata.py +0 -0
  114. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/ogb_wrappers.py +0 -0
  115. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/pyg_wrappers.py +0 -0
  116. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/registry.py +0 -0
  117. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/synthetic.py +0 -0
  118. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/datasets/torchvision_wrappers.py +0 -0
  119. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/distributed.py +0 -0
  120. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/__init__.py +0 -0
  121. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/callbacks.py +0 -0
  122. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/checkpoints.py +0 -0
  123. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/cli.py +0 -0
  124. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/config.py +0 -0
  125. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/grid.py +0 -0
  126. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/runner.py +0 -0
  127. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/experiments/summarize.py +0 -0
  128. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/__init__.py +0 -0
  129. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/attention.py +0 -0
  130. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/edge_attribution.py +0 -0
  131. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/export.py +0 -0
  132. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/integrated_gradients.py +0 -0
  133. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/patch_heatmap.py +0 -0
  134. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/saliency.py +0 -0
  135. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/explain/utils.py +0 -0
  136. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/graph_builders.py +0 -0
  137. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/hetero_sampling.py +0 -0
  138. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/interop.py +0 -0
  139. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/__init__.py +0 -0
  140. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/_dim.py +0 -0
  141. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/_scatter.py +0 -0
  142. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/aggregator.py +0 -0
  143. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/appnp.py +0 -0
  144. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/attention_message.py +0 -0
  145. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/base.py +0 -0
  146. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/conv_message.py +0 -0
  147. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/factory.py +0 -0
  148. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/gat.py +0 -0
  149. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/gatv2.py +0 -0
  150. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/gin.py +0 -0
  151. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/graph_transformer.py +0 -0
  152. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/hetero.py +0 -0
  153. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/hetero_readout.py +0 -0
  154. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/pooling.py +0 -0
  155. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/safe_pool.py +0 -0
  156. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/sage.py +0 -0
  157. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/temporal_readout.py +0 -0
  158. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/transformer_encodings.py +0 -0
  159. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/layers/vector_gcn.py +0 -0
  160. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/learned_graph.py +0 -0
  161. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/__init__.py +0 -0
  162. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/classification.py +0 -0
  163. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/link_prediction.py +0 -0
  164. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/ogb.py +0 -0
  165. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/ranking.py +0 -0
  166. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/regression.py +0 -0
  167. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/metrics/reports.py +0 -0
  168. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/anomaly.py +0 -0
  169. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/communities.py +0 -0
  170. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/frequent.py +0 -0
  171. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/hetero.py +0 -0
  172. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/link_prediction.py +0 -0
  173. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/motifs.py +0 -0
  174. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/patterns.py +0 -0
  175. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/prototype.py +0 -0
  176. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/random_walk.py +0 -0
  177. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/reports.py +0 -0
  178. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/similarity.py +0 -0
  179. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/structural.py +0 -0
  180. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/mining/temporal.py +0 -0
  181. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/__init__.py +0 -0
  182. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/cnn_encoder.py +0 -0
  183. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/cnn_gnn_model.py +0 -0
  184. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/edge_predictor.py +0 -0
  185. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/factory.py +0 -0
  186. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/graph_classifier.py +0 -0
  187. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/hetero_models.py +0 -0
  188. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/model_zoo.py +0 -0
  189. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/node_classifier.py +0 -0
  190. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/pre_encoder.py +0 -0
  191. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/regressors.py +0 -0
  192. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/models/temporal_models.py +0 -0
  193. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/performance.py +0 -0
  194. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/__init__.py +0 -0
  195. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/graph.py +0 -0
  196. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/layouts.py +0 -0
  197. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/mining.py +0 -0
  198. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/plotting/utils.py +0 -0
  199. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/sampling.py +0 -0
  200. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/sampling_loaders.py +0 -0
  201. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/sampling_negative.py +0 -0
  202. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/temporal/__init__.py +0 -0
  203. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/temporal/time_encoding.py +0 -0
  204. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/temporal_sampling.py +0 -0
  205. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/tracking.py +0 -0
  206. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/training.py +0 -0
  207. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/__init__.py +0 -0
  208. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/compose.py +0 -0
  209. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/features.py +0 -0
  210. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/graph.py +0 -0
  211. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/patch.py +0 -0
  212. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/positional.py +0 -0
  213. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx/transforms/splits.py +0 -0
  214. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/dependency_links.txt +0 -0
  215. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/entry_points.txt +0 -0
  216. {tgraphx-0.4.0 → tgraphx-0.4.2}/tgraphx.egg-info/requires.txt +0 -0
  217. {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.0
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.0"
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}"