k3-node 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- k3_node/__init__.py +122 -0
- k3_node/applications/__init__.py +17 -0
- k3_node/applications/bio/__init__.py +21 -0
- k3_node/applications/chemistry/__init__.py +155 -0
- k3_node/applications/materials/__init__.py +127 -0
- k3_node/applications/materials/basis.py +449 -0
- k3_node/applications/materials/chgnet.py +360 -0
- k3_node/applications/materials/core.py +351 -0
- k3_node/applications/materials/grace.py +246 -0
- k3_node/applications/materials/io.py +230 -0
- k3_node/applications/materials/m3gnet.py +462 -0
- k3_node/applications/materials/megnet.py +395 -0
- k3_node/applications/materials/qet.py +220 -0
- k3_node/applications/materials/readout.py +235 -0
- k3_node/applications/materials/so3net.py +234 -0
- k3_node/applications/materials/tensornet.py +381 -0
- k3_node/applications/materials/test_materials.py +167 -0
- k3_node/applications/materials/wrappers.py +95 -0
- k3_node/data/__init__.py +47 -0
- k3_node/data/batch.py +102 -0
- k3_node/data/collate.py +282 -0
- k3_node/data/data.py +532 -0
- k3_node/data/database.py +154 -0
- k3_node/data/dataset.py +182 -0
- k3_node/data/download.py +49 -0
- k3_node/data/extract.py +45 -0
- k3_node/data/feature_store.py +70 -0
- k3_node/data/graph_store.py +92 -0
- k3_node/data/hetero_data.py +374 -0
- k3_node/data/hypergraph_data.py +59 -0
- k3_node/data/in_memory_dataset.py +177 -0
- k3_node/data/makedirs.py +7 -0
- k3_node/data/on_disk_dataset.py +77 -0
- k3_node/data/separate.py +115 -0
- k3_node/data/storage.py +593 -0
- k3_node/data/temporal.py +154 -0
- k3_node/data/test_batch.py +67 -0
- k3_node/data/test_data.py +68 -0
- k3_node/data/test_dataset_and_stores.py +111 -0
- k3_node/data/test_hetero_data.py +33 -0
- k3_node/data/test_temporal_and_hyper.py +32 -0
- k3_node/data/view.py +43 -0
- k3_node/datasets/__init__.py +88 -0
- k3_node/datasets/actor.py +101 -0
- k3_node/datasets/airports.py +84 -0
- k3_node/datasets/amazon.py +66 -0
- k3_node/datasets/ba2motif_dataset.py +73 -0
- k3_node/datasets/ba_shapes.py +81 -0
- k3_node/datasets/bitcoin_otc.py +77 -0
- k3_node/datasets/citation_full.py +81 -0
- k3_node/datasets/coauthor.py +66 -0
- k3_node/datasets/dblp.py +106 -0
- k3_node/datasets/digits.py +63 -0
- k3_node/datasets/email_eu_core.py +60 -0
- k3_node/datasets/entities.py +158 -0
- k3_node/datasets/explainer_dataset.py +101 -0
- k3_node/datasets/facebook.py +51 -0
- k3_node/datasets/fake.py +256 -0
- k3_node/datasets/freebase.py +90 -0
- k3_node/datasets/geometric_shapes.py +69 -0
- k3_node/datasets/github.py +51 -0
- k3_node/datasets/graph_generator/__init__.py +6 -0
- k3_node/datasets/graph_generator/ba_graph.py +20 -0
- k3_node/datasets/graph_generator/base.py +29 -0
- k3_node/datasets/graph_generator/er_graph.py +21 -0
- k3_node/datasets/icews.py +58 -0
- k3_node/datasets/imdb.py +96 -0
- k3_node/datasets/jodie.py +56 -0
- k3_node/datasets/karate.py +56 -0
- k3_node/datasets/lastfm_asia.py +51 -0
- k3_node/datasets/mesh_correspondence.py +50 -0
- k3_node/datasets/molecule_net.py +148 -0
- k3_node/datasets/motif_generator/__init__.py +7 -0
- k3_node/datasets/motif_generator/base.py +29 -0
- k3_node/datasets/motif_generator/custom.py +17 -0
- k3_node/datasets/motif_generator/cycle.py +25 -0
- k3_node/datasets/motif_generator/house.py +27 -0
- k3_node/datasets/movielens.py +55 -0
- k3_node/datasets/planetoid.py +137 -0
- k3_node/datasets/polblogs.py +63 -0
- k3_node/datasets/ppi.py +189 -0
- k3_node/datasets/qm7.py +65 -0
- k3_node/datasets/qm9.py +132 -0
- k3_node/datasets/reddit.py +121 -0
- k3_node/datasets/sbm_dataset.py +165 -0
- k3_node/datasets/seal.py +74 -0
- k3_node/datasets/shape_scenes.py +92 -0
- k3_node/datasets/test_datasets.py +322 -0
- k3_node/datasets/tu_dataset.py +131 -0
- k3_node/datasets/twitch.py +66 -0
- k3_node/datasets/webkb.py +102 -0
- k3_node/datasets/wikics.py +85 -0
- k3_node/datasets/word_net.py +184 -0
- k3_node/etl/__init__.py +37 -0
- k3_node/etl/encoders.py +248 -0
- k3_node/etl/graph_builders.py +270 -0
- k3_node/etl/relational_to_graph.py +201 -0
- k3_node/etl/table_to_graph.py +244 -0
- k3_node/etl/test_etl.py +318 -0
- k3_node/export/__init__.py +15 -0
- k3_node/export/cross_backend.py +172 -0
- k3_node/export/onnx_exporter.py +190 -0
- k3_node/export/runtime.py +254 -0
- k3_node/export/tensorrt_exporter.py +201 -0
- k3_node/export/test_export.py +337 -0
- k3_node/export/tflite_exporter.py +112 -0
- k3_node/hub/__init__.py +29 -0
- k3_node/hub/dataset_hub.py +242 -0
- k3_node/hub/hub_mixin.py +599 -0
- k3_node/hub/model_card.py +133 -0
- k3_node/hub/test_hub.py +419 -0
- k3_node/io/__init__.py +22 -0
- k3_node/io/fs.py +117 -0
- k3_node/io/npz.py +45 -0
- k3_node/io/off.py +29 -0
- k3_node/io/planetoid.py +98 -0
- k3_node/io/tu.py +137 -0
- k3_node/io/txt_array.py +58 -0
- k3_node/layers/__init__.py +14 -0
- k3_node/layers/aggr/__init__.py +70 -0
- k3_node/layers/aggr/attention.py +77 -0
- k3_node/layers/aggr/base.py +403 -0
- k3_node/layers/aggr/basic.py +412 -0
- k3_node/layers/aggr/deep_sets.py +65 -0
- k3_node/layers/aggr/deepsets.py +29 -0
- k3_node/layers/aggr/equilibrium.py +107 -0
- k3_node/layers/aggr/fused.py +43 -0
- k3_node/layers/aggr/gmt.py +89 -0
- k3_node/layers/aggr/gru.py +58 -0
- k3_node/layers/aggr/lcm.py +143 -0
- k3_node/layers/aggr/lstm.py +58 -0
- k3_node/layers/aggr/mlp.py +75 -0
- k3_node/layers/aggr/multi.py +154 -0
- k3_node/layers/aggr/patch_transformer.py +137 -0
- k3_node/layers/aggr/quantile.py +125 -0
- k3_node/layers/aggr/resolver.py +68 -0
- k3_node/layers/aggr/scaler.py +133 -0
- k3_node/layers/aggr/set2set.py +87 -0
- k3_node/layers/aggr/set_transformer.py +107 -0
- k3_node/layers/aggr/sort.py +68 -0
- k3_node/layers/aggr/test_aggr.py +337 -0
- k3_node/layers/aggr/utils.py +210 -0
- k3_node/layers/aggr/variance_preserving.py +54 -0
- k3_node/layers/attention/__init__.py +5 -0
- k3_node/layers/attention/pair_attention.py +448 -0
- k3_node/layers/attention/performer.py +187 -0
- k3_node/layers/attention/polynormer.py +160 -0
- k3_node/layers/attention/qformer.py +143 -0
- k3_node/layers/attention/sgformer.py +106 -0
- k3_node/layers/attention/test_attention.py +68 -0
- k3_node/layers/attention/test_pair_attention.py +91 -0
- k3_node/layers/conv/__init__.py +149 -0
- k3_node/layers/conv/agnn_conv.py +120 -0
- k3_node/layers/conv/antisymmetric_conv.py +94 -0
- k3_node/layers/conv/appnp.py +105 -0
- k3_node/layers/conv/appnp_conv.py +157 -0
- k3_node/layers/conv/arma_conv.py +231 -0
- k3_node/layers/conv/cg_conv.py +92 -0
- k3_node/layers/conv/cheb_conv.py +137 -0
- k3_node/layers/conv/cluster_gcn_conv.py +102 -0
- k3_node/layers/conv/conv.py +100 -0
- k3_node/layers/conv/crystal_conv.py +140 -0
- k3_node/layers/conv/cugraph.py +84 -0
- k3_node/layers/conv/diffusion_conv.py +144 -0
- k3_node/layers/conv/dir_gnn_conv.py +93 -0
- k3_node/layers/conv/dna_conv.py +192 -0
- k3_node/layers/conv/edge_conv.py +107 -0
- k3_node/layers/conv/eg_conv.py +155 -0
- k3_node/layers/conv/fa_conv.py +107 -0
- k3_node/layers/conv/feast_conv.py +126 -0
- k3_node/layers/conv/film_conv.py +143 -0
- k3_node/layers/conv/gat_conv.py +244 -0
- k3_node/layers/conv/gated_graph_conv.py +136 -0
- k3_node/layers/conv/gatv2_conv.py +205 -0
- k3_node/layers/conv/gcn.py +144 -0
- k3_node/layers/conv/gcn2_conv.py +126 -0
- k3_node/layers/conv/gcn_conv.py +135 -0
- k3_node/layers/conv/gen_conv.py +163 -0
- k3_node/layers/conv/general_conv.py +218 -0
- k3_node/layers/conv/gin_conv.py +218 -0
- k3_node/layers/conv/gmm_conv.py +172 -0
- k3_node/layers/conv/gps_conv.py +153 -0
- k3_node/layers/conv/graph_attention.py +262 -0
- k3_node/layers/conv/graph_conv.py +84 -0
- k3_node/layers/conv/gravnet_conv.py +93 -0
- k3_node/layers/conv/han_conv.py +175 -0
- k3_node/layers/conv/heat_conv.py +131 -0
- k3_node/layers/conv/hetero_conv.py +128 -0
- k3_node/layers/conv/hgt_conv.py +218 -0
- k3_node/layers/conv/hypergraph_conv.py +182 -0
- k3_node/layers/conv/le_conv.py +81 -0
- k3_node/layers/conv/lg_conv.py +58 -0
- k3_node/layers/conv/meshcnn_conv.py +84 -0
- k3_node/layers/conv/message_passing.py +451 -0
- k3_node/layers/conv/mf_conv.py +95 -0
- k3_node/layers/conv/mixhop_conv.py +108 -0
- k3_node/layers/conv/nn_conv.py +110 -0
- k3_node/layers/conv/pan_conv.py +100 -0
- k3_node/layers/conv/pdn_conv.py +109 -0
- k3_node/layers/conv/pna_conv.py +177 -0
- k3_node/layers/conv/point_conv.py +101 -0
- k3_node/layers/conv/point_gnn_conv.py +90 -0
- k3_node/layers/conv/point_transformer_conv.py +132 -0
- k3_node/layers/conv/ppf_conv.py +135 -0
- k3_node/layers/conv/ppnp.py +89 -0
- k3_node/layers/conv/res_gated_graph_conv.py +126 -0
- k3_node/layers/conv/rgat_conv.py +251 -0
- k3_node/layers/conv/rgcn_conv.py +321 -0
- k3_node/layers/conv/sage_conv.py +154 -0
- k3_node/layers/conv/sg_conv.py +96 -0
- k3_node/layers/conv/signed_conv.py +100 -0
- k3_node/layers/conv/simple_conv.py +75 -0
- k3_node/layers/conv/spline_conv.py +182 -0
- k3_node/layers/conv/ssg_conv.py +101 -0
- k3_node/layers/conv/supergat_conv.py +195 -0
- k3_node/layers/conv/tag_conv.py +98 -0
- k3_node/layers/conv/test_backend_consistency.py +164 -0
- k3_node/layers/conv/test_conv.py +176 -0
- k3_node/layers/conv/test_conv_pyg.py +566 -0
- k3_node/layers/conv/transformer_conv.py +168 -0
- k3_node/layers/conv/utils.py +403 -0
- k3_node/layers/conv/wl_conv.py +151 -0
- k3_node/layers/conv/x_conv.py +187 -0
- k3_node/layers/dense/__init__.py +40 -0
- k3_node/layers/dense/dense_gat_conv.py +149 -0
- k3_node/layers/dense/dense_gcn_conv.py +117 -0
- k3_node/layers/dense/dense_gin_conv.py +88 -0
- k3_node/layers/dense/dense_graph_conv.py +95 -0
- k3_node/layers/dense/dense_sage_conv.py +85 -0
- k3_node/layers/dense/diff_pool.py +76 -0
- k3_node/layers/dense/dmon_pool.py +223 -0
- k3_node/layers/dense/linear.py +327 -0
- k3_node/layers/dense/mincut_pool.py +92 -0
- k3_node/layers/dense/test_dense.py +377 -0
- k3_node/layers/functional/__init__.py +13 -0
- k3_node/layers/functional/bro.py +49 -0
- k3_node/layers/functional/edge_dropout.py +55 -0
- k3_node/layers/functional/gini.py +44 -0
- k3_node/layers/functional/test_functional.py +34 -0
- k3_node/layers/kge/__init__.py +17 -0
- k3_node/layers/kge/base.py +255 -0
- k3_node/layers/kge/complex.py +98 -0
- k3_node/layers/kge/distmult.py +79 -0
- k3_node/layers/kge/loader.py +50 -0
- k3_node/layers/kge/rotate.py +103 -0
- k3_node/layers/kge/test_kge.py +76 -0
- k3_node/layers/kge/transe.py +96 -0
- k3_node/layers/norm/__init__.py +23 -0
- k3_node/layers/norm/batch_norm.py +328 -0
- k3_node/layers/norm/diff_group_norm.py +141 -0
- k3_node/layers/norm/graph_norm.py +105 -0
- k3_node/layers/norm/graph_size_norm.py +57 -0
- k3_node/layers/norm/instance_norm.py +163 -0
- k3_node/layers/norm/layer_norm.py +245 -0
- k3_node/layers/norm/mean_subtraction_norm.py +57 -0
- k3_node/layers/norm/msg_norm.py +58 -0
- k3_node/layers/norm/pair_norm.py +94 -0
- k3_node/layers/norm/test_norm.py +275 -0
- k3_node/layers/pool/__init__.py +83 -0
- k3_node/layers/pool/approx_knn.py +101 -0
- k3_node/layers/pool/asap.py +173 -0
- k3_node/layers/pool/avg_pool.py +165 -0
- k3_node/layers/pool/cluster_pool.py +168 -0
- k3_node/layers/pool/connect/__init__.py +10 -0
- k3_node/layers/pool/connect/base.py +103 -0
- k3_node/layers/pool/connect/filter_edges.py +113 -0
- k3_node/layers/pool/consecutive.py +30 -0
- k3_node/layers/pool/decimation.py +48 -0
- k3_node/layers/pool/edge_pool.py +189 -0
- k3_node/layers/pool/glob.py +139 -0
- k3_node/layers/pool/graclus.py +66 -0
- k3_node/layers/pool/knn.py +253 -0
- k3_node/layers/pool/max_pool.py +159 -0
- k3_node/layers/pool/mem_pool.py +145 -0
- k3_node/layers/pool/pan_pool.py +144 -0
- k3_node/layers/pool/point_cloud.py +212 -0
- k3_node/layers/pool/pool.py +119 -0
- k3_node/layers/pool/sag_pool.py +174 -0
- k3_node/layers/pool/select/__init__.py +10 -0
- k3_node/layers/pool/select/base.py +112 -0
- k3_node/layers/pool/select/topk.py +206 -0
- k3_node/layers/pool/test_pool.py +456 -0
- k3_node/layers/pool/topk_pool.py +103 -0
- k3_node/layers/pool/voxel_grid.py +70 -0
- k3_node/layers/unpool/__init__.py +9 -0
- k3_node/layers/unpool/knn_interpolate.py +57 -0
- k3_node/layers/unpool/test_unpool.py +31 -0
- k3_node/loader/__init__.py +62 -0
- k3_node/loader/base.py +69 -0
- k3_node/loader/cache.py +68 -0
- k3_node/loader/cluster.py +127 -0
- k3_node/loader/data_list_loader.py +45 -0
- k3_node/loader/dataloader.py +117 -0
- k3_node/loader/dense_data_loader.py +62 -0
- k3_node/loader/dynamic_batch_sampler.py +93 -0
- k3_node/loader/graph_saint.py +188 -0
- k3_node/loader/hgt_loader.py +90 -0
- k3_node/loader/imbalanced_sampler.py +87 -0
- k3_node/loader/keras_dataset.py +334 -0
- k3_node/loader/link_loader.py +179 -0
- k3_node/loader/link_neighbor_loader.py +202 -0
- k3_node/loader/mixin.py +190 -0
- k3_node/loader/neighbor_loader.py +159 -0
- k3_node/loader/neighbor_sampler.py +167 -0
- k3_node/loader/node_loader.py +185 -0
- k3_node/loader/prefetch.py +115 -0
- k3_node/loader/random_node_loader.py +89 -0
- k3_node/loader/sampler_utils.py +499 -0
- k3_node/loader/shadow.py +115 -0
- k3_node/loader/temporal_dataloader.py +98 -0
- k3_node/loader/test_dataloader.py +113 -0
- k3_node/loader/test_keras_dataset.py +221 -0
- k3_node/loader/test_neighbor_loader.py +122 -0
- k3_node/loader/test_sampler_utils.py +82 -0
- k3_node/loader/test_samplers.py +96 -0
- k3_node/loader/test_subgraph_loaders.py +89 -0
- k3_node/loader/utils.py +232 -0
- k3_node/loader/zip_loader.py +88 -0
- k3_node/metrics.py +94 -0
- k3_node/models/__init__.py +424 -0
- k3_node/models/attentive_fp.py +232 -0
- k3_node/models/attract_repel.py +108 -0
- k3_node/models/autoencoder.py +318 -0
- k3_node/models/basic_gnn.py +443 -0
- k3_node/models/bio/__init__.py +4 -0
- k3_node/models/captum.py +52 -0
- k3_node/models/chemistry/__init__.py +4 -0
- k3_node/models/correct_and_smooth.py +146 -0
- k3_node/models/deep_graph_infomax.py +113 -0
- k3_node/models/deepgcn.py +121 -0
- k3_node/models/dimenet.py +737 -0
- k3_node/models/dimenet_utils.py +153 -0
- k3_node/models/gnnff.py +263 -0
- k3_node/models/gps_model.py +1122 -0
- k3_node/models/gpse.py +638 -0
- k3_node/models/graph_unet.py +199 -0
- k3_node/models/graphmae2.py +954 -0
- k3_node/models/graphormer.py +1258 -0
- k3_node/models/graphormer_3d.py +868 -0
- k3_node/models/grover.py +1066 -0
- k3_node/models/jumping_knowledge.py +200 -0
- k3_node/models/label_prop.py +110 -0
- k3_node/models/lightgcn.py +171 -0
- k3_node/models/linkx.py +181 -0
- k3_node/models/lpformer.py +404 -0
- k3_node/models/mask_label.py +114 -0
- k3_node/models/materials/__init__.py +33 -0
- k3_node/models/meta.py +133 -0
- k3_node/models/metapath2vec.py +234 -0
- k3_node/models/mlp.py +264 -0
- k3_node/models/mole_bert.py +379 -0
- k3_node/models/neural_fingerprint.py +95 -0
- k3_node/models/node2vec.py +213 -0
- k3_node/models/pmlp.py +157 -0
- k3_node/models/polynormer.py +229 -0
- k3_node/models/rect.py +93 -0
- k3_node/models/renet.py +221 -0
- k3_node/models/rev_gnn.py +128 -0
- k3_node/models/schnet.py +484 -0
- k3_node/models/sgformer.py +195 -0
- k3_node/models/signed_gcn.py +185 -0
- k3_node/models/test_attentive_fp.py +32 -0
- k3_node/models/test_attract_repel.py +33 -0
- k3_node/models/test_autoencoder.py +119 -0
- k3_node/models/test_basic_gnn.py +102 -0
- k3_node/models/test_correct_and_smooth.py +40 -0
- k3_node/models/test_deep_graph_infomax.py +68 -0
- k3_node/models/test_deepgcn.py +21 -0
- k3_node/models/test_dimenet.py +86 -0
- k3_node/models/test_domain_apis.py +138 -0
- k3_node/models/test_gnnff.py +24 -0
- k3_node/models/test_gps_model.py +271 -0
- k3_node/models/test_gpse.py +34 -0
- k3_node/models/test_graph_unet.py +26 -0
- k3_node/models/test_graphmae2.py +226 -0
- k3_node/models/test_graphormer.py +233 -0
- k3_node/models/test_graphormer3d.py +163 -0
- k3_node/models/test_grover.py +287 -0
- k3_node/models/test_jumping_knowledge.py +129 -0
- k3_node/models/test_label_prop.py +37 -0
- k3_node/models/test_lightgcn.py +38 -0
- k3_node/models/test_linkx.py +31 -0
- k3_node/models/test_lpformer.py +22 -0
- k3_node/models/test_mask_label.py +90 -0
- k3_node/models/test_meta.py +159 -0
- k3_node/models/test_metapath2vec.py +45 -0
- k3_node/models/test_mlp.py +62 -0
- k3_node/models/test_mole_bert.py +164 -0
- k3_node/models/test_neural_fingerprint.py +13 -0
- k3_node/models/test_node2vec.py +57 -0
- k3_node/models/test_pmlp.py +81 -0
- k3_node/models/test_polynormer.py +104 -0
- k3_node/models/test_rect.py +23 -0
- k3_node/models/test_renet.py +32 -0
- k3_node/models/test_rev_gnn.py +24 -0
- k3_node/models/test_schnet.py +43 -0
- k3_node/models/test_sgformer.py +48 -0
- k3_node/models/test_signed_gcn.py +28 -0
- k3_node/models/test_tgn.py +77 -0
- k3_node/models/test_unimol.py +179 -0
- k3_node/models/test_unimol2.py +114 -0
- k3_node/models/test_unimol_plus.py +131 -0
- k3_node/models/test_visnet.py +44 -0
- k3_node/models/tgn.py +382 -0
- k3_node/models/unimol.py +1156 -0
- k3_node/models/unimol2.py +616 -0
- k3_node/models/unimol_docking_v2.py +301 -0
- k3_node/models/unimol_plus.py +456 -0
- k3_node/models/utils.py +97 -0
- k3_node/models/visnet.py +759 -0
- k3_node/ops/__init__.py +4 -0
- k3_node/ops/conv.py +56 -0
- k3_node/ops/creation.py +43 -0
- k3_node/ops/graph.py +27 -0
- k3_node/ops/host.py +41 -0
- k3_node/ops/matmul.py +49 -0
- k3_node/ops/numpy.py +24 -0
- k3_node/ops/segment.py +54 -0
- k3_node/ops/sparse.py +51 -0
- k3_node/rag/__init__.py +49 -0
- k3_node/rag/encoders.py +312 -0
- k3_node/rag/pipeline.py +192 -0
- k3_node/rag/projector.py +184 -0
- k3_node/rag/subgraph.py +270 -0
- k3_node/rag/test_rag.py +347 -0
- k3_node/rag/verbalizer.py +162 -0
- k3_node/tasks/__init__.py +19 -0
- k3_node/tasks/backbone_resolver.py +125 -0
- k3_node/tasks/base.py +67 -0
- k3_node/tasks/graph_classification.py +270 -0
- k3_node/tasks/graph_regression.py +228 -0
- k3_node/tasks/link_prediction.py +306 -0
- k3_node/tasks/node_classification.py +194 -0
- k3_node/tasks/node_regression.py +138 -0
- k3_node/tasks/test_tasks.py +319 -0
- k3_node/test_docstring_examples.py +106 -0
- k3_node/test_training_forwarding.py +116 -0
- k3_node/training.py +115 -0
- k3_node/transforms/__init__.py +166 -0
- k3_node/transforms/base_transform.py +32 -0
- k3_node/transforms/compose.py +58 -0
- k3_node/transforms/general.py +676 -0
- k3_node/transforms/graph.py +1070 -0
- k3_node/transforms/spatial.py +797 -0
- k3_node/transforms/test_random_link_split.py +45 -0
- k3_node/transforms/test_spatial_transforms.py +65 -0
- k3_node/transforms/test_transforms.py +253 -0
- k3_node/transforms/utils.py +102 -0
- k3_node/utils/__init__.py +5 -0
- k3_node/utils/backend_import.py +12 -0
- k3_node/utils/graph.py +286 -0
- k3_node/utils/keras.py +94 -0
- k3_node/utils/random.py +103 -0
- k3_node/utils/smiles.py +235 -0
- k3_node-1.0.0.dist-info/METADATA +284 -0
- k3_node-1.0.0.dist-info/RECORD +459 -0
- k3_node-1.0.0.dist-info/WHEEL +5 -0
- k3_node-1.0.0.dist-info/licenses/LICENSE +21 -0
- k3_node-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from typing import Callable, List, NamedTuple, Optional, Tuple
|
|
2
|
+
from keras import layers, ops
|
|
3
|
+
import numpy as np
|
|
4
|
+
from k3_node.ops.segment import segment_max, segment_sum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UnpoolInfo(NamedTuple):
|
|
8
|
+
edge_index: any
|
|
9
|
+
cluster: any
|
|
10
|
+
batch: any
|
|
11
|
+
new_edge_score: any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EdgePooling(layers.Layer):
|
|
15
|
+
r"""The edge pooling operator from the `"Towards Graph Pooling by Edge
|
|
16
|
+
Contraction" <https://graphreason.github.io/papers/17.pdf>`__ and
|
|
17
|
+
`"Edge Contraction Pooling for Graph Neural Networks"
|
|
18
|
+
<https://arxiv.org/abs/1905.10990>`__ papers.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np
|
|
23
|
+
from k3_node.layers import EdgePooling
|
|
24
|
+
|
|
25
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
26
|
+
edge_index = np.random.randint(0, 10, size=(2, 30)) # 30 random edges
|
|
27
|
+
batch = np.repeat([0, 1], 5) # nodes 0-4 belong to graph 0, nodes 5-9 to graph 1
|
|
28
|
+
|
|
29
|
+
layer = EdgePooling(in_channels=8)
|
|
30
|
+
x_pool, edge_index_pool, batch_pool, unpool_info = layer(x, edge_index, batch)
|
|
31
|
+
# The number of clusters depends on the learned edge scores
|
|
32
|
+
print(x_pool.shape[0] <= 10, x_pool.shape[1]) # True 8: fewer nodes, same features
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
in_channels: int,
|
|
38
|
+
edge_score_method: Optional[Callable] = None,
|
|
39
|
+
dropout: float = 0.0,
|
|
40
|
+
add_to_edge_score: float = 0.5,
|
|
41
|
+
**kwargs,
|
|
42
|
+
):
|
|
43
|
+
super().__init__(**kwargs)
|
|
44
|
+
self.in_channels = in_channels
|
|
45
|
+
if edge_score_method is None:
|
|
46
|
+
edge_score_method = self.compute_edge_score_softmax
|
|
47
|
+
self.compute_edge_score = edge_score_method
|
|
48
|
+
self.add_to_edge_score = add_to_edge_score
|
|
49
|
+
self.dropout_rate = dropout
|
|
50
|
+
self.drop = layers.Dropout(dropout) if dropout > 0.0 else None
|
|
51
|
+
|
|
52
|
+
self.lin = layers.Dense(1, use_bias=True, name="lin")
|
|
53
|
+
|
|
54
|
+
def build(self, input_shape=None):
|
|
55
|
+
self.lin.build((None, 2 * self.in_channels))
|
|
56
|
+
super().build(input_shape)
|
|
57
|
+
|
|
58
|
+
def reset_parameters(self):
|
|
59
|
+
self.lin.reset_parameters()
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def compute_edge_score_softmax(raw_edge_score, edge_index, num_nodes: int):
|
|
63
|
+
col = ops.cast(edge_index[1], dtype="int32")
|
|
64
|
+
max_score = segment_max(raw_edge_score, col, num_segments=num_nodes)
|
|
65
|
+
max_exp = ops.take(max_score, col, axis=0)
|
|
66
|
+
exp_score = ops.exp(raw_edge_score - max_exp)
|
|
67
|
+
sum_exp = segment_sum(exp_score, col, num_segments=num_nodes)
|
|
68
|
+
sum_exp_taken = ops.take(sum_exp, col, axis=0)
|
|
69
|
+
return exp_score / (sum_exp_taken + 1e-12)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def compute_edge_score_tanh(raw_edge_score, edge_index=None, num_nodes=None):
|
|
73
|
+
return ops.tanh(raw_edge_score)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def compute_edge_score_sigmoid(raw_edge_score, edge_index=None, num_nodes=None):
|
|
77
|
+
return ops.sigmoid(raw_edge_score)
|
|
78
|
+
|
|
79
|
+
def call(
|
|
80
|
+
self,
|
|
81
|
+
x,
|
|
82
|
+
edge_index,
|
|
83
|
+
batch,
|
|
84
|
+
training: bool = False,
|
|
85
|
+
) -> Tuple[any, any, any, UnpoolInfo]:
|
|
86
|
+
r"""Forward pass."""
|
|
87
|
+
row = ops.cast(edge_index[0], dtype="int32")
|
|
88
|
+
col = ops.cast(edge_index[1], dtype="int32")
|
|
89
|
+
|
|
90
|
+
e = ops.concatenate([ops.take(x, row, axis=0), ops.take(x, col, axis=0)], axis=-1)
|
|
91
|
+
e = ops.reshape(self.lin(e), (-1,))
|
|
92
|
+
if self.drop is not None:
|
|
93
|
+
e = self.drop(e, training=training)
|
|
94
|
+
|
|
95
|
+
num_nodes = ops.shape(x)[0]
|
|
96
|
+
e = self.compute_edge_score(e, edge_index, num_nodes)
|
|
97
|
+
e = e + self.add_to_edge_score
|
|
98
|
+
|
|
99
|
+
return self._merge_edges(x, edge_index, batch, e)
|
|
100
|
+
|
|
101
|
+
def _merge_edges(
|
|
102
|
+
self,
|
|
103
|
+
x,
|
|
104
|
+
edge_index,
|
|
105
|
+
batch,
|
|
106
|
+
edge_score,
|
|
107
|
+
) -> Tuple[any, any, any, UnpoolInfo]:
|
|
108
|
+
from k3_node.layers.conv.utils import eager_only_placeholder, host_callback
|
|
109
|
+
if eager_only_placeholder("EdgePooling", x, edge_index, edge_score):
|
|
110
|
+
num_nodes = ops.shape(x)[0]
|
|
111
|
+
unpool_info = UnpoolInfo(edge_index, ops.arange(num_nodes, dtype="int32"), batch, ops.ones((num_nodes,), dtype=x.dtype))
|
|
112
|
+
return x, edge_index, batch, unpool_info
|
|
113
|
+
|
|
114
|
+
num_nodes = int(ops.shape(x)[0])
|
|
115
|
+
num_edges = int(ops.shape(edge_index)[1])
|
|
116
|
+
|
|
117
|
+
def contract(edge_index_np, edge_score_np, batch_np):
|
|
118
|
+
# Greedy contraction on the host; outputs are padded to fixed sizes (N clusters, E edges).
|
|
119
|
+
cluster_np = np.zeros(num_nodes, dtype=np.int64)
|
|
120
|
+
cluster_edge = np.full(num_nodes, -1, dtype=np.int64) # edge merged into each cluster
|
|
121
|
+
mask = np.ones(num_nodes, dtype=bool)
|
|
122
|
+
i = 0
|
|
123
|
+
for edge_idx in np.argsort(-edge_score_np, kind="stable"):
|
|
124
|
+
source, target = edge_index_np[0, edge_idx], edge_index_np[1, edge_idx]
|
|
125
|
+
if not mask[source] or not mask[target]:
|
|
126
|
+
continue
|
|
127
|
+
cluster_edge[i] = edge_idx
|
|
128
|
+
cluster_np[source] = i
|
|
129
|
+
mask[source] = False
|
|
130
|
+
if source != target:
|
|
131
|
+
cluster_np[target] = i
|
|
132
|
+
mask[target] = False
|
|
133
|
+
i += 1
|
|
134
|
+
remaining = np.where(mask)[0]
|
|
135
|
+
cluster_np[remaining] = np.arange(i, i + len(remaining))
|
|
136
|
+
num_clusters = i + len(remaining)
|
|
137
|
+
|
|
138
|
+
unique_edges = np.unique(cluster_np[edge_index_np.astype(np.int64)], axis=1)
|
|
139
|
+
edges_pad = np.zeros((2, num_edges), dtype=np.int64)
|
|
140
|
+
edges_pad[:, : unique_edges.shape[1]] = unique_edges
|
|
141
|
+
|
|
142
|
+
batch_pad = np.zeros(num_nodes, dtype=np.int64)
|
|
143
|
+
batch_pad[cluster_np] = batch_np
|
|
144
|
+
return cluster_np, cluster_edge, num_clusters, edges_pad, unique_edges.shape[1], batch_pad
|
|
145
|
+
|
|
146
|
+
cluster, cluster_edge, num_clusters, edges_pad, num_new_edges, batch_pad = host_callback(
|
|
147
|
+
contract,
|
|
148
|
+
[((num_nodes,), "int32"), ((num_nodes,), "int32"), ((), "int32"),
|
|
149
|
+
((2, num_edges), "int32"), ((), "int32"), ((num_nodes,), "int32")],
|
|
150
|
+
edge_index, edge_score, batch,
|
|
151
|
+
)
|
|
152
|
+
num_clusters, num_new_edges = int(num_clusters), int(num_new_edges)
|
|
153
|
+
|
|
154
|
+
new_x = segment_sum(x, cluster, num_segments=num_clusters)
|
|
155
|
+
|
|
156
|
+
# Score of the edge merged into each cluster (1 for unmatched nodes), gathered from the score
|
|
157
|
+
# tensor so gradients reach the scoring layer.
|
|
158
|
+
cluster_edge = cluster_edge[:num_clusters]
|
|
159
|
+
if num_edges > 0:
|
|
160
|
+
gathered = ops.take(ops.cast(edge_score, x.dtype), ops.maximum(cluster_edge, 0), axis=0)
|
|
161
|
+
new_edge_score = ops.where(cluster_edge >= 0, gathered, ops.ones_like(gathered))
|
|
162
|
+
else:
|
|
163
|
+
new_edge_score = ops.ones((num_clusters,), dtype=x.dtype)
|
|
164
|
+
new_x = new_x * ops.reshape(new_edge_score, (-1, 1))
|
|
165
|
+
|
|
166
|
+
new_edge_index = ops.cast(edges_pad[:, :num_new_edges], edge_index.dtype)
|
|
167
|
+
new_batch = ops.cast(batch_pad[:num_clusters], batch.dtype)
|
|
168
|
+
|
|
169
|
+
unpool_info = UnpoolInfo(
|
|
170
|
+
edge_index=edge_index,
|
|
171
|
+
cluster=cluster,
|
|
172
|
+
batch=batch,
|
|
173
|
+
new_edge_score=new_edge_score,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return new_x, new_edge_index, new_batch, unpool_info
|
|
177
|
+
|
|
178
|
+
def unpool(
|
|
179
|
+
self,
|
|
180
|
+
x,
|
|
181
|
+
unpool_info: UnpoolInfo,
|
|
182
|
+
) -> Tuple[any, any, any]:
|
|
183
|
+
r"""Unpools a previous edge pooling step."""
|
|
184
|
+
new_x = x / ops.reshape(unpool_info.new_edge_score, (-1, 1))
|
|
185
|
+
new_x = ops.take(new_x, ops.cast(unpool_info.cluster, dtype="int32"), axis=0)
|
|
186
|
+
return new_x, unpool_info.edge_index, unpool_info.batch
|
|
187
|
+
|
|
188
|
+
def __repr__(self) -> str:
|
|
189
|
+
return f"{self.__class__.__name__}({self.in_channels})"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from keras import ops
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from k3_node.layers.conv.utils import is_tracing
|
|
6
|
+
from k3_node.ops.segment import segment_max, segment_sum
|
|
7
|
+
from k3_node.ops.host import to_numpy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _infer_size(batch, size=None):
|
|
11
|
+
if size is not None:
|
|
12
|
+
return size
|
|
13
|
+
import keras
|
|
14
|
+
from keras.src.backend.common.symbolic_scope import in_symbolic_scope
|
|
15
|
+
|
|
16
|
+
if in_symbolic_scope() or (hasattr(batch, "is_meta") and batch.is_meta):
|
|
17
|
+
# Keras shape inference on placeholder values: any size gives the right output rank.
|
|
18
|
+
return 1 if keras.config.backend() == "jax" else None
|
|
19
|
+
if is_tracing(batch):
|
|
20
|
+
if keras.config.backend() == "jax":
|
|
21
|
+
# The number of graphs depends on the values in `batch`, which are unknown inside
|
|
22
|
+
# jax.jit; guessing would silently merge every graph into one.
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"Cannot infer the number of graphs from `batch` inside a compiled JAX function. "
|
|
25
|
+
"Pass `size=` to the pooling function (or `batch_size=` to the model)."
|
|
26
|
+
)
|
|
27
|
+
# TensorFlow graph mode supports a dynamic number of segments.
|
|
28
|
+
return ops.cast(ops.max(batch), "int32") + 1
|
|
29
|
+
# Use the maximum (as PyG does), not the last entry: pooling layers such as EdgePooling
|
|
30
|
+
# return batch vectors that are not sorted by graph.
|
|
31
|
+
try:
|
|
32
|
+
if ops.shape(batch)[0] == 0:
|
|
33
|
+
return 0
|
|
34
|
+
return int(to_numpy(batch).max()) + 1
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
try:
|
|
38
|
+
return ops.cast(ops.max(batch), "int32") + 1
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def global_add_pool(x, batch: Optional[any] = None, size: Optional[int] = None):
|
|
44
|
+
r"""Returns batch-wise graph-level-outputs by adding node features
|
|
45
|
+
across the node dimension.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
```python
|
|
49
|
+
import numpy as np
|
|
50
|
+
from k3_node.layers import global_add_pool
|
|
51
|
+
|
|
52
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
53
|
+
batch = np.repeat([0, 1], 5) # nodes 0-4 belong to graph 0, nodes 5-9 to graph 1
|
|
54
|
+
|
|
55
|
+
out = global_add_pool(x, batch) # one row per graph
|
|
56
|
+
print(tuple(out.shape)) # (2, 8)
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
if batch is None:
|
|
60
|
+
dim = -1 if len(ops.shape(x)) == 1 else 0
|
|
61
|
+
keepdims = len(ops.shape(x)) <= 2
|
|
62
|
+
out = ops.sum(x, axis=dim, keepdims=keepdims)
|
|
63
|
+
if len(ops.shape(x)) == 1:
|
|
64
|
+
out = ops.reshape(out, (1, 1))
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
batch = ops.cast(batch, dtype="int32")
|
|
68
|
+
if batch.shape[0] is not None and batch.shape[0] == 0:
|
|
69
|
+
num_seg = size if size is not None else 0
|
|
70
|
+
return ops.zeros((num_seg,) + tuple(ops.shape(x)[1:]), dtype=x.dtype)
|
|
71
|
+
size = _infer_size(batch, size)
|
|
72
|
+
return segment_sum(x, batch, num_segments=size)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def global_mean_pool(x, batch: Optional[any] = None, size: Optional[int] = None):
|
|
76
|
+
r"""Returns batch-wise graph-level-outputs by averaging node features
|
|
77
|
+
across the node dimension.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
```python
|
|
81
|
+
import numpy as np
|
|
82
|
+
from k3_node.layers import global_mean_pool
|
|
83
|
+
|
|
84
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
85
|
+
batch = np.repeat([0, 1], 5) # nodes 0-4 belong to graph 0, nodes 5-9 to graph 1
|
|
86
|
+
|
|
87
|
+
out = global_mean_pool(x, batch) # one row per graph
|
|
88
|
+
print(tuple(out.shape)) # (2, 8)
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
if batch is None:
|
|
92
|
+
dim = -1 if len(ops.shape(x)) == 1 else 0
|
|
93
|
+
keepdims = len(ops.shape(x)) <= 2
|
|
94
|
+
out = ops.mean(x, axis=dim, keepdims=keepdims)
|
|
95
|
+
if len(ops.shape(x)) == 1:
|
|
96
|
+
out = ops.reshape(out, (1, 1))
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
batch = ops.cast(batch, dtype="int32")
|
|
100
|
+
if batch.shape[0] is not None and batch.shape[0] == 0:
|
|
101
|
+
num_seg = size if size is not None else 0
|
|
102
|
+
return ops.zeros((num_seg,) + tuple(ops.shape(x)[1:]), dtype=x.dtype)
|
|
103
|
+
size = _infer_size(batch, size)
|
|
104
|
+
sum_val = segment_sum(x, batch, num_segments=size)
|
|
105
|
+
ones = ops.ones_like(x)
|
|
106
|
+
count = segment_sum(ones, batch, num_segments=size)
|
|
107
|
+
return sum_val / ops.maximum(count, 1.0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def global_max_pool(x, batch: Optional[any] = None, size: Optional[int] = None):
|
|
111
|
+
r"""Returns batch-wise graph-level-outputs by taking the channel-wise
|
|
112
|
+
maximum across the node dimension.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
```python
|
|
116
|
+
import numpy as np
|
|
117
|
+
from k3_node.layers import global_max_pool
|
|
118
|
+
|
|
119
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
120
|
+
batch = np.repeat([0, 1], 5) # nodes 0-4 belong to graph 0, nodes 5-9 to graph 1
|
|
121
|
+
|
|
122
|
+
out = global_max_pool(x, batch) # one row per graph
|
|
123
|
+
print(tuple(out.shape)) # (2, 8)
|
|
124
|
+
```
|
|
125
|
+
"""
|
|
126
|
+
if batch is None:
|
|
127
|
+
dim = -1 if len(ops.shape(x)) == 1 else 0
|
|
128
|
+
keepdims = len(ops.shape(x)) <= 2
|
|
129
|
+
out = ops.max(x, axis=dim, keepdims=keepdims)
|
|
130
|
+
if len(ops.shape(x)) == 1:
|
|
131
|
+
out = ops.reshape(out, (1, 1))
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
batch = ops.cast(batch, dtype="int32")
|
|
135
|
+
if batch.shape[0] is not None and batch.shape[0] == 0:
|
|
136
|
+
num_seg = size if size is not None else 0
|
|
137
|
+
return ops.zeros((num_seg,) + tuple(ops.shape(x)[1:]), dtype=x.dtype)
|
|
138
|
+
size = _infer_size(batch, size)
|
|
139
|
+
return segment_max(x, batch, num_segments=size)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from keras import ops
|
|
3
|
+
import numpy as np
|
|
4
|
+
from k3_node.ops.host import to_numpy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def graclus(
|
|
8
|
+
edge_index,
|
|
9
|
+
weight: Optional[any] = None,
|
|
10
|
+
num_nodes: Optional[int] = None,
|
|
11
|
+
):
|
|
12
|
+
r"""A greedy clustering algorithm of picking an unmarked vertex and matching
|
|
13
|
+
it with one of its unmarked neighbors that maximizes its edge weight.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```python
|
|
17
|
+
import numpy as np
|
|
18
|
+
from k3_node.layers import graclus
|
|
19
|
+
|
|
20
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
21
|
+
edge_index = np.random.randint(0, 10, size=(2, 30)) # 30 random edges
|
|
22
|
+
|
|
23
|
+
cluster = graclus(edge_index, num_nodes=10) # greedy matching of neighboring nodes
|
|
24
|
+
print(tuple(cluster.shape)) # (10,): a cluster id for every node
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
edge_index_np = to_numpy(edge_index).astype(np.int64)
|
|
28
|
+
if num_nodes is None:
|
|
29
|
+
num_nodes = int(np.max(edge_index_np)) + 1 if edge_index_np.size > 0 else 0
|
|
30
|
+
|
|
31
|
+
if weight is not None:
|
|
32
|
+
weight_np = to_numpy(weight)
|
|
33
|
+
else:
|
|
34
|
+
weight_np = np.ones(edge_index_np.shape[1], dtype=np.float32)
|
|
35
|
+
|
|
36
|
+
# Build adjacency list
|
|
37
|
+
adj = [[] for _ in range(num_nodes)]
|
|
38
|
+
for idx in range(edge_index_np.shape[1]):
|
|
39
|
+
u = edge_index_np[0, idx]
|
|
40
|
+
v = edge_index_np[1, idx]
|
|
41
|
+
w = weight_np[idx]
|
|
42
|
+
adj[u].append((v, w))
|
|
43
|
+
|
|
44
|
+
cluster_np = np.full(num_nodes, -1, dtype=np.int64)
|
|
45
|
+
c = 0
|
|
46
|
+
for u in range(num_nodes):
|
|
47
|
+
if cluster_np[u] != -1:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
best_v = None
|
|
51
|
+
best_w = -float("inf")
|
|
52
|
+
for v, w in adj[u]:
|
|
53
|
+
if v != u and cluster_np[v] == -1:
|
|
54
|
+
if w > best_w:
|
|
55
|
+
best_w = w
|
|
56
|
+
best_v = v
|
|
57
|
+
|
|
58
|
+
if best_v is not None:
|
|
59
|
+
cluster_np[u] = c
|
|
60
|
+
cluster_np[best_v] = c
|
|
61
|
+
else:
|
|
62
|
+
cluster_np[u] = c
|
|
63
|
+
c += 1
|
|
64
|
+
|
|
65
|
+
return ops.convert_to_tensor(cluster_np, dtype="int64")
|
|
66
|
+
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from typing import NamedTuple, Optional
|
|
2
|
+
from keras import ops
|
|
3
|
+
import numpy as np
|
|
4
|
+
from k3_node.ops.creation import repeat
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KNNOutput(NamedTuple):
|
|
8
|
+
score: any
|
|
9
|
+
index: any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KNNIndex:
|
|
13
|
+
r"""A base class to perform k-nearest neighbor search."""
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
index_factory: Optional[str] = None,
|
|
17
|
+
emb: Optional[any] = None,
|
|
18
|
+
reserve: Optional[int] = None,
|
|
19
|
+
):
|
|
20
|
+
self.index_factory = index_factory
|
|
21
|
+
self.reserve = reserve
|
|
22
|
+
self.emb = None
|
|
23
|
+
if emb is not None:
|
|
24
|
+
self.add(emb)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def numel(self) -> int:
|
|
28
|
+
if self.emb is None:
|
|
29
|
+
return 0
|
|
30
|
+
return ops.shape(self.emb)[0]
|
|
31
|
+
|
|
32
|
+
def add(self, emb):
|
|
33
|
+
if self.emb is None:
|
|
34
|
+
self.emb = emb
|
|
35
|
+
else:
|
|
36
|
+
self.emb = ops.concatenate([self.emb, emb], axis=0)
|
|
37
|
+
|
|
38
|
+
def get_emb(self):
|
|
39
|
+
return self.emb
|
|
40
|
+
|
|
41
|
+
def search(self, query, k: int) -> KNNOutput:
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class L2KNNIndex(KNNIndex):
|
|
46
|
+
r"""k-NN search using squared Euclidean (L2) distance."""
|
|
47
|
+
def __init__(self, emb: Optional[any] = None, **kwargs):
|
|
48
|
+
super().__init__(index_factory="IndexFlatL2", emb=emb, **kwargs)
|
|
49
|
+
|
|
50
|
+
def search(self, query, k: int) -> KNNOutput:
|
|
51
|
+
# query: [M, F], emb: [N, F]
|
|
52
|
+
q_exp = ops.expand_dims(query, axis=1) # [M, 1, F]
|
|
53
|
+
e_exp = ops.expand_dims(self.emb, axis=0) # [1, N, F]
|
|
54
|
+
dist = ops.sum(ops.power(q_exp - e_exp, 2), axis=-1) # [M, N]
|
|
55
|
+
|
|
56
|
+
top_k_neg_score, top_k_idx = ops.top_k(-dist, k=k, sorted=True)
|
|
57
|
+
return KNNOutput(score=-top_k_neg_score, index=top_k_idx)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MIPSKNNIndex(KNNIndex):
|
|
61
|
+
r"""k-NN search using Maximum Inner Product Search (MIPS)."""
|
|
62
|
+
def __init__(self, emb: Optional[any] = None, **kwargs):
|
|
63
|
+
super().__init__(index_factory="IndexFlatIP", emb=emb, **kwargs)
|
|
64
|
+
|
|
65
|
+
def search(self, query, k: int) -> KNNOutput:
|
|
66
|
+
# query: [M, F], emb: [N, F]
|
|
67
|
+
score_mat = ops.matmul(query, ops.transpose(self.emb)) # [M, N]
|
|
68
|
+
top_k_score, top_k_idx = ops.top_k(score_mat, k=k, sorted=True)
|
|
69
|
+
return KNNOutput(score=top_k_score, index=top_k_idx)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ApproxL2KNNIndex(L2KNNIndex):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ApproxMIPSKNNIndex(MIPSKNNIndex):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def knn(
|
|
81
|
+
x,
|
|
82
|
+
y,
|
|
83
|
+
k: int,
|
|
84
|
+
batch_x: Optional[any] = None,
|
|
85
|
+
batch_y: Optional[any] = None,
|
|
86
|
+
cosine: bool = False,
|
|
87
|
+
num_workers: int = 1,
|
|
88
|
+
batch_size: Optional[int] = None,
|
|
89
|
+
):
|
|
90
|
+
r"""Finds for each element in `y` the `k` nearest points in `x`.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
```python
|
|
94
|
+
import numpy as np
|
|
95
|
+
from k3_node.layers import knn
|
|
96
|
+
|
|
97
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
98
|
+
pos = np.random.rand(10, 3).astype("float32") # 3D positions
|
|
99
|
+
query = np.random.rand(4, 3).astype("float32") # 4 query points
|
|
100
|
+
|
|
101
|
+
assign = knn(pos, query, k=3) # 3 nearest points in `pos` for every query point
|
|
102
|
+
print(tuple(assign.shape)) # (2, 12): [2, num_query * k]: (query index, point index)
|
|
103
|
+
```
|
|
104
|
+
"""
|
|
105
|
+
x = ops.convert_to_tensor(x)
|
|
106
|
+
y = ops.convert_to_tensor(y)
|
|
107
|
+
if len(ops.shape(x)) == 1:
|
|
108
|
+
x = ops.expand_dims(x, axis=-1)
|
|
109
|
+
if len(ops.shape(y)) == 1:
|
|
110
|
+
y = ops.expand_dims(y, axis=-1)
|
|
111
|
+
|
|
112
|
+
col = _knn_indices(x, y, k, batch_x, batch_y, cosine=cosine) # [M, k] indices into x
|
|
113
|
+
M = ops.shape(y)[0]
|
|
114
|
+
row = repeat(ops.arange(0, M, dtype="int64"), k)
|
|
115
|
+
return ops.stack([row, ops.reshape(ops.cast(col, "int64"), (-1,))], axis=0)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _pairwise(y, x, cosine):
|
|
119
|
+
"""Squared distances (or cosine distances) between the rows of ``y`` and ``x``, without
|
|
120
|
+
materializing ``[M, N, features]`` differences."""
|
|
121
|
+
if cosine:
|
|
122
|
+
y = y / ops.maximum(ops.norm(y, axis=-1, keepdims=True), 1e-12)
|
|
123
|
+
x = x / ops.maximum(ops.norm(x, axis=-1, keepdims=True), 1e-12)
|
|
124
|
+
return 1.0 - ops.matmul(y, ops.swapaxes(x, -1, -2))
|
|
125
|
+
y_sq = ops.sum(y * y, axis=-1, keepdims=True)
|
|
126
|
+
x_sq = ops.expand_dims(ops.sum(x * x, axis=-1), -2)
|
|
127
|
+
return ops.maximum(y_sq + x_sq - 2.0 * ops.matmul(y, ops.swapaxes(x, -1, -2)), 0.0)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _knn_indices(x, y, k, batch_x=None, batch_y=None, cosine=False, exclude_self=False):
|
|
131
|
+
"""Indices ``[M, k]`` into ``x`` of the ``k`` nearest points of every row of ``y`` within the
|
|
132
|
+
same example. Eagerly, distances are computed per example (``[examples, n, n]``); when compiled,
|
|
133
|
+
in chunks of query rows masked by example."""
|
|
134
|
+
from k3_node.layers.aggr.base import to_dense_batch
|
|
135
|
+
from k3_node.layers.conv.utils import is_tracing
|
|
136
|
+
|
|
137
|
+
N, M = ops.shape(x)[0], ops.shape(y)[0]
|
|
138
|
+
if isinstance(M, int) and isinstance(N, int) and (M == 0 or N == 0): # sizes are tensors while tracing
|
|
139
|
+
return ops.zeros((M, k), dtype="int32")
|
|
140
|
+
ids = [b for b in (batch_x, batch_y) if b is not None]
|
|
141
|
+
if not any(is_tracing(t) for t in [x, y] + ids) and isinstance(N, int) and isinstance(M, int):
|
|
142
|
+
try:
|
|
143
|
+
bx = np.zeros(N, np.int64) if batch_x is None else np.asarray(ops.convert_to_numpy(batch_x)).astype(np.int64)
|
|
144
|
+
by = np.zeros(M, np.int64) if batch_y is None else np.asarray(ops.convert_to_numpy(batch_y)).astype(np.int64)
|
|
145
|
+
num_examples = int(max(bx.max(initial=-1), by.max(initial=-1))) + 1
|
|
146
|
+
x_dense, x_mask = to_dense_batch(x, bx, dim_size=num_examples)
|
|
147
|
+
y_dense, _ = to_dense_batch(y, by, dim_size=num_examples)
|
|
148
|
+
dist = _pairwise(y_dense, x_dense, cosine) # [examples, max_y, max_x]
|
|
149
|
+
dist = ops.where(ops.expand_dims(x_mask, 1), dist, 1e9)
|
|
150
|
+
x_start = np.concatenate([[0], np.cumsum(np.bincount(bx, minlength=num_examples))])[:-1]
|
|
151
|
+
y_start = np.concatenate([[0], np.cumsum(np.bincount(by, minlength=num_examples))])[:-1]
|
|
152
|
+
y_local = np.arange(M) - y_start[by]
|
|
153
|
+
if exclude_self: # x and y are the same points
|
|
154
|
+
dist = ops.where(ops.expand_dims(ops.eye(dist.shape[1], dist.shape[2]), 0) > 0, 1e9, dist)
|
|
155
|
+
kk = min(k, int(dist.shape[2]))
|
|
156
|
+
_, idx = ops.top_k(-dist, k=kk, sorted=True) # local indices [examples, max_y, kk]
|
|
157
|
+
flat = ops.reshape(idx, (-1, kk))
|
|
158
|
+
local = ops.take(flat, by * int(dist.shape[1]) + y_local, axis=0) # [M, kk]
|
|
159
|
+
out = local + ops.convert_to_tensor(x_start[by][:, None].astype(np.int64), dtype=local.dtype)
|
|
160
|
+
if kk < k: # fewer candidates than k: repeat the last one
|
|
161
|
+
out = ops.concatenate([out] + [out[:, -1:]] * (k - kk), axis=1)
|
|
162
|
+
return out
|
|
163
|
+
except (TypeError, ValueError, NotImplementedError, RuntimeError):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
if not isinstance(N, int) and y is x and batch_x is not None and batch_y is batch_x:
|
|
167
|
+
# compiled knn_graph with a point count known only at run time (TensorFlow): per example,
|
|
168
|
+
# since the chunks below would all be the full [N, N] distance matrix
|
|
169
|
+
return _knn_graph_indices_traced(x, k, ops.cast(batch_x, "int32"), cosine, exclude_self)
|
|
170
|
+
|
|
171
|
+
# Compiled: masked distances for chunks of query rows ([chunk, N] memory at a time)
|
|
172
|
+
bx = ops.zeros((N,), "int32") if batch_x is None else ops.cast(batch_x, "int32")
|
|
173
|
+
by = ops.zeros((M,), "int32") if batch_y is None else ops.cast(batch_y, "int32")
|
|
174
|
+
chunk = M if not isinstance(M, int) or not isinstance(N, int) else max(1, min(M, (1 << 24) // max(N, 1)))
|
|
175
|
+
outs = []
|
|
176
|
+
for start in range(0, M, chunk) if isinstance(M, int) else [0]:
|
|
177
|
+
stop = start + chunk if isinstance(M, int) else None
|
|
178
|
+
dist = _pairwise(y[start:stop], x, cosine)
|
|
179
|
+
mask = ops.expand_dims(by[start:stop], 1) != ops.expand_dims(bx, 0)
|
|
180
|
+
if exclude_self:
|
|
181
|
+
rows = ops.arange(ops.shape(dist)[0], dtype="int32") + start
|
|
182
|
+
mask = ops.logical_or(mask, ops.expand_dims(rows, 1) == ops.expand_dims(ops.arange(N, dtype="int32"), 0))
|
|
183
|
+
kk = min(k, N) if isinstance(N, int) else k
|
|
184
|
+
_, idx = ops.top_k(-ops.where(mask, 1e9, dist), k=kk, sorted=True)
|
|
185
|
+
if kk < k: # fewer candidates than k: repeat the last one, as in the eager path
|
|
186
|
+
idx = ops.concatenate([idx] + [idx[:, -1:]] * (k - kk), axis=1)
|
|
187
|
+
outs.append(idx)
|
|
188
|
+
return outs[0] if len(outs) == 1 else ops.concatenate(outs, axis=0)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _knn_graph_indices_traced(x, k, batch, cosine, exclude_self):
|
|
192
|
+
"""``_knn_indices(x, x, ...)`` for sizes only known at run time: distances within each example
|
|
193
|
+
(``[examples, max_points, max_points]``), like the eager path. Needs ``k`` <= points per example."""
|
|
194
|
+
from k3_node.layers.aggr.base import to_dense_batch
|
|
195
|
+
from k3_node.ops.segment import segment_sum
|
|
196
|
+
|
|
197
|
+
num_points = ops.shape(x)[0]
|
|
198
|
+
x_dense, mask = to_dense_batch(x, batch)
|
|
199
|
+
n = ops.shape(x_dense)[1]
|
|
200
|
+
dist = ops.where(ops.expand_dims(mask, 1), _pairwise(x_dense, x_dense, cosine), 1e9)
|
|
201
|
+
if exclude_self:
|
|
202
|
+
same = ops.expand_dims(ops.arange(n), 1) == ops.expand_dims(ops.arange(n), 0)
|
|
203
|
+
dist = ops.where(ops.expand_dims(same, 0), 1e9, dist)
|
|
204
|
+
kk = min(k, x_dense.shape[1]) if isinstance(x_dense.shape[1], int) else k
|
|
205
|
+
_, idx = ops.top_k(-dist, k=kk, sorted=True) # local indices [examples, max_points, kk]
|
|
206
|
+
if kk < k: # fewer candidates than k (e.g. Keras' shape inference): repeat the last one
|
|
207
|
+
idx = ops.concatenate([idx] + [idx[..., -1:]] * (k - kk), axis=-1)
|
|
208
|
+
counts = segment_sum(ops.ones_like(batch), batch, num_segments=num_points)
|
|
209
|
+
starts = ops.take(ops.cumsum(counts) - counts, batch, axis=0) # first point of each point's example
|
|
210
|
+
local = ops.arange(num_points, dtype="int32") - starts
|
|
211
|
+
rows = ops.take(ops.reshape(idx, (-1, k)), batch * ops.cast(n, "int32") + local, axis=0)
|
|
212
|
+
return rows + ops.cast(ops.expand_dims(starts, 1), rows.dtype)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def knn_graph(
|
|
216
|
+
x,
|
|
217
|
+
k: int,
|
|
218
|
+
batch: Optional[any] = None,
|
|
219
|
+
loop: bool = False,
|
|
220
|
+
flow: str = "source_to_target",
|
|
221
|
+
cosine: bool = False,
|
|
222
|
+
num_workers: int = 1,
|
|
223
|
+
batch_size: Optional[int] = None,
|
|
224
|
+
):
|
|
225
|
+
r"""Computes graph edges to the nearest `k` points.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
```python
|
|
229
|
+
import numpy as np
|
|
230
|
+
from k3_node.layers import knn_graph
|
|
231
|
+
|
|
232
|
+
x = np.random.rand(10, 8).astype("float32") # 10 nodes with 8 features each
|
|
233
|
+
pos = np.random.rand(10, 3).astype("float32") # 3D positions
|
|
234
|
+
|
|
235
|
+
edge_index = knn_graph(pos, k=3) # connect every point to its 3 nearest neighbors
|
|
236
|
+
print(tuple(edge_index.shape)) # (2, 30)
|
|
237
|
+
```
|
|
238
|
+
"""
|
|
239
|
+
assert flow in ["source_to_target", "target_to_source"]
|
|
240
|
+
x = ops.convert_to_tensor(x)
|
|
241
|
+
if len(ops.shape(x)) == 1:
|
|
242
|
+
x = ops.expand_dims(x, axis=-1)
|
|
243
|
+
|
|
244
|
+
N = ops.shape(x)[0]
|
|
245
|
+
col_indices = _knn_indices(x, x, k, batch, batch, cosine=cosine, exclude_self=not loop)
|
|
246
|
+
row = repeat(ops.arange(0, N, dtype="int64"), k)
|
|
247
|
+
col = ops.reshape(ops.cast(col_indices, "int64"), (-1,))
|
|
248
|
+
|
|
249
|
+
if flow == "source_to_target":
|
|
250
|
+
return ops.stack([col, row], axis=0)
|
|
251
|
+
else:
|
|
252
|
+
return ops.stack([row, col], axis=0)
|
|
253
|
+
|