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