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,797 @@
|
|
|
1
|
+
"""Transforms that use node positions (``data.pos``), meshes (``data.face``) or images.
|
|
2
|
+
|
|
3
|
+
They run on the host with NumPy and return tensors of the same kind as their inputs, so they work
|
|
4
|
+
with every Keras backend.
|
|
5
|
+
"""
|
|
6
|
+
import math
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, List, Optional, Sequence, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import keras
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from k3_node.data import Data, HeteroData
|
|
15
|
+
from k3_node.transforms.base_transform import BaseTransform, functional_transform
|
|
16
|
+
from k3_node.transforms.utils import match_tensor, to_numpy, to_undirected
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _np(x):
|
|
20
|
+
return None if x is None else to_numpy(x)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _out(value, like, dtype=None):
|
|
24
|
+
"""Returns the NumPy result ``value`` as the same kind of tensor (and dtype) as ``like``."""
|
|
25
|
+
if dtype is None:
|
|
26
|
+
dtype = keras.backend.standardize_dtype(like.dtype) if like is not None else None
|
|
27
|
+
value = np.asarray(value)
|
|
28
|
+
if dtype is not None:
|
|
29
|
+
value = value.astype(dtype)
|
|
30
|
+
if isinstance(like, np.ndarray):
|
|
31
|
+
return value
|
|
32
|
+
return match_tensor(value, like, dtype=dtype)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_tensor(x) -> bool:
|
|
36
|
+
return hasattr(x, "shape") and hasattr(x, "dtype") and len(x.shape) > 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _normalize(x, axis=-1):
|
|
40
|
+
return x / np.maximum(np.linalg.norm(x, axis=axis, keepdims=True), 1e-12)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _set_edge_attr(data, new, cat: bool, like):
|
|
44
|
+
"""Stores ``new`` edge features, appended to the existing ones if ``cat``."""
|
|
45
|
+
pseudo = data.edge_attr
|
|
46
|
+
if pseudo is not None and cat:
|
|
47
|
+
p = _np(pseudo)
|
|
48
|
+
p = p.reshape(-1, 1) if p.ndim == 1 else p
|
|
49
|
+
data.edge_attr = _out(np.concatenate([p, new.astype(p.dtype)], axis=-1), pseudo)
|
|
50
|
+
else:
|
|
51
|
+
data.edge_attr = _out(new, like)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_angle(v1, v2):
|
|
55
|
+
cross = np.cross(v1, v2)
|
|
56
|
+
cross_norm = np.linalg.norm(cross, axis=-1) if cross.ndim > 1 else np.abs(cross)
|
|
57
|
+
return np.arctan2(cross_norm, (v1 * v2).sum(-1))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _point_pair_features(pos_i, pos_j, norm_i, norm_j):
|
|
61
|
+
pseudo = pos_j - pos_i
|
|
62
|
+
return np.stack([
|
|
63
|
+
np.linalg.norm(pseudo, axis=-1),
|
|
64
|
+
_get_angle(norm_i, pseudo),
|
|
65
|
+
_get_angle(norm_j, pseudo),
|
|
66
|
+
_get_angle(norm_i, norm_j),
|
|
67
|
+
], axis=-1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _scatter_sum(src, index, dim_size):
|
|
71
|
+
out = np.zeros((dim_size,) + src.shape[1:], dtype=src.dtype)
|
|
72
|
+
np.add.at(out, index, src)
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _scatter_mean(src, index, dim_size):
|
|
77
|
+
count = np.bincount(index, minlength=dim_size).reshape((-1,) + (1,) * (src.ndim - 1))
|
|
78
|
+
return _scatter_sum(src, index, dim_size) / np.maximum(count, 1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _scatter_max(src, index, dim_size):
|
|
82
|
+
out = np.full((dim_size,) + src.shape[1:], -np.inf, dtype=src.dtype)
|
|
83
|
+
np.maximum.at(out, index, src)
|
|
84
|
+
return np.where(np.isinf(out), 0, out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _edges(data):
|
|
88
|
+
edge_index = _np(data.edge_index)
|
|
89
|
+
return edge_index[0], edge_index[1], _np(data.pos)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@functional_transform('distance')
|
|
93
|
+
class Distance(BaseTransform):
|
|
94
|
+
r"""Saves the relative Euclidean distance of linked nodes in edge attributes
|
|
95
|
+
(functional name: :obj:`distance`).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
norm (bool, optional): If set to :obj:`False`, output will not be
|
|
99
|
+
normalized to the specified interval. (default: :obj:`True`)
|
|
100
|
+
max_value (float, optional): If set and :obj:`norm=True`, normalization
|
|
101
|
+
will be performed based on this value instead of maximum distance.
|
|
102
|
+
(default: :obj:`None`)
|
|
103
|
+
cat (bool, optional): If set to :obj:`False`, all existing edge
|
|
104
|
+
attributes will be replaced. (default: :obj:`True`)
|
|
105
|
+
interval (tuple, optional): A tuple specifying the lower and upper
|
|
106
|
+
bound for normalization. (default: :obj:`(0.0, 1.0)`)
|
|
107
|
+
"""
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
norm: bool = True,
|
|
111
|
+
max_value: Optional[float] = None,
|
|
112
|
+
cat: bool = True,
|
|
113
|
+
interval: Tuple[float, float] = (0.0, 1.0),
|
|
114
|
+
) -> None:
|
|
115
|
+
self.norm = norm
|
|
116
|
+
self.max = max_value
|
|
117
|
+
self.cat = cat
|
|
118
|
+
self.interval = interval
|
|
119
|
+
|
|
120
|
+
def forward(self, data: Data) -> Data:
|
|
121
|
+
assert data.pos is not None
|
|
122
|
+
assert data.edge_index is not None
|
|
123
|
+
row, col, pos = _edges(data)
|
|
124
|
+
|
|
125
|
+
dist = np.linalg.norm(pos[col] - pos[row], axis=-1).reshape(-1, 1)
|
|
126
|
+
|
|
127
|
+
if self.norm and dist.size > 0:
|
|
128
|
+
max_val = dist.max() if self.max is None else self.max
|
|
129
|
+
if max_val > 0:
|
|
130
|
+
dist = dist / max_val
|
|
131
|
+
length = self.interval[1] - self.interval[0]
|
|
132
|
+
dist = length * dist + self.interval[0]
|
|
133
|
+
|
|
134
|
+
_set_edge_attr(data, dist, self.cat, data.pos)
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
def __repr__(self) -> str:
|
|
138
|
+
return (f'{self.__class__.__name__}(norm={self.norm}, '
|
|
139
|
+
f'max_value={self.max})')
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@functional_transform('cartesian')
|
|
143
|
+
class Cartesian(BaseTransform):
|
|
144
|
+
r"""Saves the relative Cartesian coordinates of linked nodes in edge
|
|
145
|
+
attributes (functional name: :obj:`cartesian`).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
norm (bool, optional): If set to :obj:`False`, the output will not be
|
|
149
|
+
normalized to the specified interval. (default: :obj:`True`)
|
|
150
|
+
max_value (float, optional): If set and :obj:`norm=True`, normalization
|
|
151
|
+
will be performed based on this value instead of maximum coordinate.
|
|
152
|
+
(default: :obj:`None`)
|
|
153
|
+
cat (bool, optional): If set to :obj:`False`, all existing edge
|
|
154
|
+
attributes will be replaced. (default: :obj:`True`)
|
|
155
|
+
interval (tuple, optional): A tuple specifying the lower and upper
|
|
156
|
+
bound for normalization. (default: :obj:`(0.0, 1.0)`)
|
|
157
|
+
"""
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
norm: bool = True,
|
|
161
|
+
max_value: Optional[float] = None,
|
|
162
|
+
cat: bool = True,
|
|
163
|
+
interval: Tuple[float, float] = (0.0, 1.0),
|
|
164
|
+
) -> None:
|
|
165
|
+
self.norm = norm
|
|
166
|
+
self.max = max_value
|
|
167
|
+
self.cat = cat
|
|
168
|
+
self.interval = interval
|
|
169
|
+
|
|
170
|
+
def forward(self, data: Data) -> Data:
|
|
171
|
+
assert data.pos is not None
|
|
172
|
+
assert data.edge_index is not None
|
|
173
|
+
row, col, pos = _edges(data)
|
|
174
|
+
|
|
175
|
+
cart = pos[row] - pos[col]
|
|
176
|
+
cart = cart.reshape(-1, 1) if cart.ndim == 1 else cart
|
|
177
|
+
|
|
178
|
+
if self.norm and cart.size > 0:
|
|
179
|
+
max_val = np.abs(cart).max() if self.max is None else self.max
|
|
180
|
+
length = self.interval[1] - self.interval[0]
|
|
181
|
+
center = (self.interval[0] + self.interval[1]) / 2
|
|
182
|
+
if max_val > 0:
|
|
183
|
+
cart = length * (cart / (2 * max_val)) + center
|
|
184
|
+
else:
|
|
185
|
+
cart = cart + center
|
|
186
|
+
|
|
187
|
+
_set_edge_attr(data, cart, self.cat, data.pos)
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
def __repr__(self) -> str:
|
|
191
|
+
return (f'{self.__class__.__name__}(norm={self.norm}, '
|
|
192
|
+
f'max_value={self.max})')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@functional_transform('local_cartesian')
|
|
196
|
+
class LocalCartesian(BaseTransform):
|
|
197
|
+
r"""Saves relative Cartesian coordinates, normalized per neighborhood to an interval
|
|
198
|
+
(functional name: :obj:`local_cartesian`)."""
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
norm: bool = True,
|
|
202
|
+
cat: bool = True,
|
|
203
|
+
interval: Tuple[float, float] = (0.0, 1.0),
|
|
204
|
+
) -> None:
|
|
205
|
+
self.norm = norm
|
|
206
|
+
self.cat = cat
|
|
207
|
+
self.interval = interval
|
|
208
|
+
|
|
209
|
+
def forward(self, data: Data) -> Data:
|
|
210
|
+
assert data.pos is not None
|
|
211
|
+
assert data.edge_index is not None
|
|
212
|
+
row, col, pos = _edges(data)
|
|
213
|
+
|
|
214
|
+
cart = pos[row] - pos[col]
|
|
215
|
+
cart = cart.reshape(-1, 1) if cart.ndim == 1 else cart
|
|
216
|
+
|
|
217
|
+
if self.norm and cart.size > 0:
|
|
218
|
+
max_value = _scatter_max(np.abs(cart), col, pos.shape[0]).max(axis=-1, keepdims=True)
|
|
219
|
+
length = self.interval[1] - self.interval[0]
|
|
220
|
+
center = (self.interval[0] + self.interval[1]) / 2
|
|
221
|
+
denom = 2 * max_value[col]
|
|
222
|
+
denom = np.where(denom == 0, 1.0, denom)
|
|
223
|
+
cart = length * cart / denom + center
|
|
224
|
+
|
|
225
|
+
_set_edge_attr(data, cart, self.cat, data.pos)
|
|
226
|
+
return data
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@functional_transform('polar')
|
|
230
|
+
class Polar(BaseTransform):
|
|
231
|
+
r"""Saves the polar coordinates of linked nodes in its edge attributes
|
|
232
|
+
(functional name: :obj:`polar`)."""
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
norm: bool = True,
|
|
236
|
+
max_value: Optional[float] = None,
|
|
237
|
+
cat: bool = True,
|
|
238
|
+
) -> None:
|
|
239
|
+
self.norm = norm
|
|
240
|
+
self.max = max_value
|
|
241
|
+
self.cat = cat
|
|
242
|
+
|
|
243
|
+
def forward(self, data: Data) -> Data:
|
|
244
|
+
assert data.pos is not None
|
|
245
|
+
assert data.edge_index is not None
|
|
246
|
+
row, col, pos = _edges(data)
|
|
247
|
+
assert pos.ndim == 2 and pos.shape[1] == 2
|
|
248
|
+
|
|
249
|
+
cart = pos[col] - pos[row]
|
|
250
|
+
rho = np.linalg.norm(cart, axis=-1).reshape(-1, 1)
|
|
251
|
+
theta = np.arctan2(cart[..., 1], cart[..., 0]).reshape(-1, 1)
|
|
252
|
+
theta = theta + (theta < 0) * (2 * math.pi)
|
|
253
|
+
|
|
254
|
+
if self.norm:
|
|
255
|
+
max_val = rho.max() if self.max is None else self.max
|
|
256
|
+
if max_val > 0:
|
|
257
|
+
rho = rho / max_val
|
|
258
|
+
theta = theta / (2 * math.pi)
|
|
259
|
+
|
|
260
|
+
_set_edge_attr(data, np.concatenate([rho, theta], axis=-1), self.cat, data.pos)
|
|
261
|
+
return data
|
|
262
|
+
|
|
263
|
+
def __repr__(self) -> str:
|
|
264
|
+
return (f'{self.__class__.__name__}(norm={self.norm}, '
|
|
265
|
+
f'max_value={self.max})')
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@functional_transform('spherical')
|
|
269
|
+
class Spherical(BaseTransform):
|
|
270
|
+
r"""Saves the spherical coordinates of linked nodes in its edge attributes
|
|
271
|
+
(functional name: :obj:`spherical`)."""
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
norm: bool = True,
|
|
275
|
+
max_value: Optional[float] = None,
|
|
276
|
+
cat: bool = True,
|
|
277
|
+
) -> None:
|
|
278
|
+
self.norm = norm
|
|
279
|
+
self.max = max_value
|
|
280
|
+
self.cat = cat
|
|
281
|
+
|
|
282
|
+
def forward(self, data: Data) -> Data:
|
|
283
|
+
assert data.pos is not None
|
|
284
|
+
assert data.edge_index is not None
|
|
285
|
+
row, col, pos = _edges(data)
|
|
286
|
+
assert pos.ndim == 2 and pos.shape[1] == 3
|
|
287
|
+
|
|
288
|
+
cart = pos[col] - pos[row]
|
|
289
|
+
rho = np.linalg.norm(cart, axis=-1).reshape(-1, 1)
|
|
290
|
+
theta = np.arctan2(cart[..., 1], cart[..., 0]).reshape(-1, 1)
|
|
291
|
+
theta = theta + (theta < 0) * (2 * math.pi)
|
|
292
|
+
denom = np.where(rho[:, 0] == 0, 1.0, rho[:, 0])
|
|
293
|
+
phi = np.arccos(np.clip(cart[..., 2] / denom, -1.0, 1.0)).reshape(-1, 1)
|
|
294
|
+
|
|
295
|
+
if self.norm:
|
|
296
|
+
max_val = rho.max() if self.max is None else self.max
|
|
297
|
+
if max_val > 0:
|
|
298
|
+
rho = rho / max_val
|
|
299
|
+
theta = theta / (2 * math.pi)
|
|
300
|
+
phi = phi / math.pi
|
|
301
|
+
|
|
302
|
+
_set_edge_attr(data, np.concatenate([rho, theta, phi], axis=-1), self.cat, data.pos)
|
|
303
|
+
return data
|
|
304
|
+
|
|
305
|
+
def __repr__(self) -> str:
|
|
306
|
+
return (f'{self.__class__.__name__}(norm={self.norm}, '
|
|
307
|
+
f'max_value={self.max})')
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@functional_transform('point_pair_features')
|
|
311
|
+
class PointPairFeatures(BaseTransform):
|
|
312
|
+
r"""Computes rotation-invariant Point Pair Features in edge attributes
|
|
313
|
+
(functional name: :obj:`point_pair_features`)."""
|
|
314
|
+
def __init__(self, cat: bool = True) -> None:
|
|
315
|
+
self.cat = cat
|
|
316
|
+
|
|
317
|
+
def forward(self, data: Data) -> Data:
|
|
318
|
+
assert data.edge_index is not None
|
|
319
|
+
assert data.pos is not None and data.norm is not None
|
|
320
|
+
row, col, pos = _edges(data)
|
|
321
|
+
norm = _np(data.norm)
|
|
322
|
+
assert pos.shape[-1] == 3 and pos.shape == norm.shape
|
|
323
|
+
|
|
324
|
+
ppf = _point_pair_features(pos[row], pos[col], norm[row], norm[col])
|
|
325
|
+
_set_edge_attr(data, ppf, self.cat, data.pos)
|
|
326
|
+
return data
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@functional_transform('center')
|
|
330
|
+
class Center(BaseTransform):
|
|
331
|
+
r"""Centers node positions :obj:`data.pos` around the origin
|
|
332
|
+
(functional name: :obj:`center`).
|
|
333
|
+
"""
|
|
334
|
+
def forward(
|
|
335
|
+
self,
|
|
336
|
+
data: Union[Data, HeteroData],
|
|
337
|
+
) -> Union[Data, HeteroData]:
|
|
338
|
+
for store in data.node_stores:
|
|
339
|
+
if hasattr(store, 'pos') and store.pos is not None:
|
|
340
|
+
pos = _np(store.pos)
|
|
341
|
+
store.pos = _out(pos - pos.mean(axis=-2, keepdims=True), store.pos)
|
|
342
|
+
return data
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@functional_transform('normalize_rotation')
|
|
346
|
+
class NormalizeRotation(BaseTransform):
|
|
347
|
+
r"""Rotates all points according to the eigenvectors of the point cloud
|
|
348
|
+
(functional name: :obj:`normalize_rotation`)."""
|
|
349
|
+
def __init__(self, max_points: int = -1, sort: bool = False) -> None:
|
|
350
|
+
self.max_points = max_points
|
|
351
|
+
self.sort = sort
|
|
352
|
+
|
|
353
|
+
def forward(self, data: Data) -> Data:
|
|
354
|
+
assert data.pos is not None
|
|
355
|
+
pos_all = _np(data.pos)
|
|
356
|
+
pos = pos_all
|
|
357
|
+
|
|
358
|
+
if self.max_points > 0 and pos.shape[0] > self.max_points:
|
|
359
|
+
pos = pos[np.random.permutation(pos.shape[0])[:self.max_points]]
|
|
360
|
+
|
|
361
|
+
pos = pos - pos.mean(axis=0, keepdims=True)
|
|
362
|
+
e, v = np.linalg.eigh(pos.T @ pos)
|
|
363
|
+
if self.sort:
|
|
364
|
+
v = v[:, np.argsort(-e)]
|
|
365
|
+
|
|
366
|
+
data.pos = _out(pos_all @ v, data.pos)
|
|
367
|
+
if 'normal' in data and data.normal is not None:
|
|
368
|
+
data.normal = _out(_normalize(_np(data.normal) @ v), data.normal)
|
|
369
|
+
return data
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@functional_transform('normalize_scale')
|
|
373
|
+
class NormalizeScale(BaseTransform):
|
|
374
|
+
r"""Centers and normalizes node positions to the interval :math:`(-1, 1)`
|
|
375
|
+
(functional name: :obj:`normalize_scale`)."""
|
|
376
|
+
def __init__(self) -> None:
|
|
377
|
+
self.center = Center()
|
|
378
|
+
|
|
379
|
+
def forward(self, data: Data) -> Data:
|
|
380
|
+
data = self.center(data)
|
|
381
|
+
assert data.pos is not None
|
|
382
|
+
pos = _np(data.pos)
|
|
383
|
+
data.pos = _out(pos * ((1.0 / np.abs(pos).max()) * 0.999999), data.pos)
|
|
384
|
+
return data
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@functional_transform('random_jitter')
|
|
388
|
+
class RandomJitter(BaseTransform):
|
|
389
|
+
r"""Translates node positions by randomly sampled translation values within a given interval
|
|
390
|
+
(functional name: :obj:`random_jitter`)."""
|
|
391
|
+
def __init__(
|
|
392
|
+
self,
|
|
393
|
+
translate: Union[float, int, Sequence[Union[float, int]]],
|
|
394
|
+
) -> None:
|
|
395
|
+
self.translate = translate
|
|
396
|
+
|
|
397
|
+
def forward(self, data: Data) -> Data:
|
|
398
|
+
assert data.pos is not None
|
|
399
|
+
pos = _np(data.pos)
|
|
400
|
+
dim = pos.shape[-1]
|
|
401
|
+
|
|
402
|
+
if isinstance(self.translate, (int, float)):
|
|
403
|
+
translate = [self.translate] * dim
|
|
404
|
+
else:
|
|
405
|
+
assert len(self.translate) == dim
|
|
406
|
+
translate = self.translate
|
|
407
|
+
|
|
408
|
+
bound = np.abs(np.asarray(translate, dtype=np.float64))
|
|
409
|
+
data.pos = _out(pos + np.random.uniform(-bound, bound, size=pos.shape), data.pos)
|
|
410
|
+
return data
|
|
411
|
+
|
|
412
|
+
def __repr__(self) -> str:
|
|
413
|
+
return f'{self.__class__.__name__}({self.translate})'
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@functional_transform('random_flip')
|
|
417
|
+
class RandomFlip(BaseTransform):
|
|
418
|
+
"""Flips node positions along a given axis randomly with a given probability
|
|
419
|
+
(functional name: :obj:`random_flip`)."""
|
|
420
|
+
def __init__(self, axis: int, p: float = 0.5) -> None:
|
|
421
|
+
self.axis = axis
|
|
422
|
+
self.p = p
|
|
423
|
+
|
|
424
|
+
def forward(self, data: Data) -> Data:
|
|
425
|
+
assert data.pos is not None
|
|
426
|
+
if random.random() < self.p:
|
|
427
|
+
pos = _np(data.pos).copy()
|
|
428
|
+
pos[..., self.axis] = -pos[..., self.axis]
|
|
429
|
+
data.pos = _out(pos, data.pos)
|
|
430
|
+
return data
|
|
431
|
+
|
|
432
|
+
def __repr__(self) -> str:
|
|
433
|
+
return f'{self.__class__.__name__}(axis={self.axis}, p={self.p})'
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@functional_transform('linear_transformation')
|
|
437
|
+
class LinearTransformation(BaseTransform):
|
|
438
|
+
r"""Transforms node positions :obj:`data.pos` with a square transformation matrix
|
|
439
|
+
(functional name: :obj:`linear_transformation`)."""
|
|
440
|
+
def __init__(self, matrix: Any):
|
|
441
|
+
matrix = np.asarray(_np(matrix))
|
|
442
|
+
assert matrix.ndim == 2, 'Transformation matrix should be two-dimensional.'
|
|
443
|
+
assert matrix.shape[0] == matrix.shape[1], (
|
|
444
|
+
f'Transformation matrix should be square (got {matrix.shape})')
|
|
445
|
+
self.matrix = matrix.T
|
|
446
|
+
|
|
447
|
+
def forward(
|
|
448
|
+
self,
|
|
449
|
+
data: Union[Data, HeteroData],
|
|
450
|
+
) -> Union[Data, HeteroData]:
|
|
451
|
+
for store in data.node_stores:
|
|
452
|
+
if not hasattr(store, 'pos') or store.pos is None:
|
|
453
|
+
continue
|
|
454
|
+
pos = _np(store.pos)
|
|
455
|
+
pos = pos.reshape(-1, 1) if pos.ndim == 1 else pos
|
|
456
|
+
assert pos.shape[-1] == self.matrix.shape[-2], (
|
|
457
|
+
'Node position matrix and transformation matrix have incompatible shape')
|
|
458
|
+
store.pos = _out(pos @ self.matrix.astype(pos.dtype), store.pos)
|
|
459
|
+
return data
|
|
460
|
+
|
|
461
|
+
def __repr__(self) -> str:
|
|
462
|
+
return f'{self.__class__.__name__}(\n{self.matrix}\n)'
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@functional_transform('random_scale')
|
|
466
|
+
class RandomScale(BaseTransform):
|
|
467
|
+
r"""Scales node positions by a randomly sampled factor within a given interval
|
|
468
|
+
(functional name: :obj:`random_scale`)."""
|
|
469
|
+
def __init__(self, scales: Tuple[float, float]) -> None:
|
|
470
|
+
assert isinstance(scales, (tuple, list)) and len(scales) == 2
|
|
471
|
+
self.scales = scales
|
|
472
|
+
|
|
473
|
+
def forward(self, data: Data) -> Data:
|
|
474
|
+
assert data.pos is not None
|
|
475
|
+
data.pos = _out(_np(data.pos) * random.uniform(*self.scales), data.pos)
|
|
476
|
+
return data
|
|
477
|
+
|
|
478
|
+
def __repr__(self) -> str:
|
|
479
|
+
return f'{self.__class__.__name__}({self.scales})'
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@functional_transform('random_rotate')
|
|
483
|
+
class RandomRotate(BaseTransform):
|
|
484
|
+
r"""Rotates node positions around a specific axis by a randomly sampled angle
|
|
485
|
+
(functional name: :obj:`random_rotate`)."""
|
|
486
|
+
def __init__(
|
|
487
|
+
self,
|
|
488
|
+
degrees: Union[Tuple[float, float], float],
|
|
489
|
+
axis: int = 0,
|
|
490
|
+
) -> None:
|
|
491
|
+
if isinstance(degrees, (int, float)):
|
|
492
|
+
degrees = (-abs(degrees), abs(degrees))
|
|
493
|
+
assert isinstance(degrees, (tuple, list)) and len(degrees) == 2
|
|
494
|
+
self.degrees = degrees
|
|
495
|
+
self.axis = axis
|
|
496
|
+
|
|
497
|
+
def forward(self, data: Data) -> Data:
|
|
498
|
+
assert data.pos is not None
|
|
499
|
+
|
|
500
|
+
degree = math.pi * random.uniform(*self.degrees) / 180.0
|
|
501
|
+
sin, cos = math.sin(degree), math.cos(degree)
|
|
502
|
+
|
|
503
|
+
if data.pos.shape[-1] == 2:
|
|
504
|
+
matrix = [[cos, sin], [-sin, cos]]
|
|
505
|
+
elif self.axis == 0:
|
|
506
|
+
matrix = [[1, 0, 0], [0, cos, sin], [0, -sin, cos]]
|
|
507
|
+
elif self.axis == 1:
|
|
508
|
+
matrix = [[cos, 0, -sin], [0, 1, 0], [sin, 0, cos]]
|
|
509
|
+
else:
|
|
510
|
+
matrix = [[cos, sin, 0], [-sin, cos, 0], [0, 0, 1]]
|
|
511
|
+
|
|
512
|
+
return LinearTransformation(np.array(matrix, dtype=np.float32))(data)
|
|
513
|
+
|
|
514
|
+
def __repr__(self) -> str:
|
|
515
|
+
return (f'{self.__class__.__name__}({self.degrees}, '
|
|
516
|
+
f'axis={self.axis})')
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@functional_transform('random_shear')
|
|
520
|
+
class RandomShear(BaseTransform):
|
|
521
|
+
r"""Shears node positions by randomly sampled factors within a given interval
|
|
522
|
+
(functional name: :obj:`random_shear`)."""
|
|
523
|
+
def __init__(self, shear: Union[float, int]) -> None:
|
|
524
|
+
self.shear = abs(shear)
|
|
525
|
+
|
|
526
|
+
def forward(self, data: Data) -> Data:
|
|
527
|
+
assert data.pos is not None
|
|
528
|
+
dim = data.pos.shape[-1]
|
|
529
|
+
matrix = np.random.uniform(-self.shear, self.shear, size=(dim, dim))
|
|
530
|
+
np.fill_diagonal(matrix, 1)
|
|
531
|
+
return LinearTransformation(matrix)(data)
|
|
532
|
+
|
|
533
|
+
def __repr__(self) -> str:
|
|
534
|
+
return f'{self.__class__.__name__}({self.shear})'
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@functional_transform('face_to_edge')
|
|
538
|
+
class FaceToEdge(BaseTransform):
|
|
539
|
+
r"""Converts mesh faces of shape :obj:`[3, num_faces]` or :obj:`[4, num_faces]`
|
|
540
|
+
to edge indices of shape :obj:`[2, num_edges]` (functional name: :obj:`face_to_edge`).
|
|
541
|
+
"""
|
|
542
|
+
def __init__(self, remove_faces: bool = True) -> None:
|
|
543
|
+
self.remove_faces = remove_faces
|
|
544
|
+
|
|
545
|
+
def forward(self, data: Data) -> Data:
|
|
546
|
+
if hasattr(data, 'face') and data.face is not None:
|
|
547
|
+
face = _np(data.face)
|
|
548
|
+
|
|
549
|
+
if face.shape[0] not in [3, 4]:
|
|
550
|
+
raise RuntimeError(f"Expected 'face' tensor with shape "
|
|
551
|
+
f"[3, num_faces] or [4, num_faces] "
|
|
552
|
+
f"(got {list(face.shape)})")
|
|
553
|
+
|
|
554
|
+
if face.shape[0] == 3:
|
|
555
|
+
edge_index = np.concatenate([face[:2], face[1:], face[::2]], axis=1)
|
|
556
|
+
else:
|
|
557
|
+
edge_index = np.concatenate(
|
|
558
|
+
[face[:2], face[1:3], face[2:4], face[::2], face[1::2], face[::3]], axis=1)
|
|
559
|
+
|
|
560
|
+
edge_index = _np(to_undirected(edge_index, num_nodes=data.num_nodes))
|
|
561
|
+
data.edge_index = _out(edge_index, data.face)
|
|
562
|
+
if self.remove_faces:
|
|
563
|
+
data.face = None
|
|
564
|
+
|
|
565
|
+
return data
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@functional_transform('sample_points')
|
|
569
|
+
class SamplePoints(BaseTransform):
|
|
570
|
+
r"""Uniformly samples a fixed number of points on the mesh surfaces
|
|
571
|
+
(functional name: :obj:`sample_points`)."""
|
|
572
|
+
def __init__(
|
|
573
|
+
self,
|
|
574
|
+
num: int,
|
|
575
|
+
remove_faces: bool = True,
|
|
576
|
+
include_normals: bool = False,
|
|
577
|
+
) -> None:
|
|
578
|
+
self.num = num
|
|
579
|
+
self.remove_faces = remove_faces
|
|
580
|
+
self.include_normals = include_normals
|
|
581
|
+
|
|
582
|
+
def forward(self, data: Data) -> Data:
|
|
583
|
+
assert data.pos is not None
|
|
584
|
+
assert data.face is not None
|
|
585
|
+
like = data.pos
|
|
586
|
+
pos, face = _np(data.pos).astype(np.float64), _np(data.face)
|
|
587
|
+
assert pos.shape[1] == 3 and face.shape[0] == 3
|
|
588
|
+
|
|
589
|
+
pos_max = np.abs(pos).max()
|
|
590
|
+
pos = pos / pos_max
|
|
591
|
+
|
|
592
|
+
area = np.linalg.norm(np.cross(pos[face[1]] - pos[face[0]], pos[face[2]] - pos[face[0]]), axis=1) / 2
|
|
593
|
+
sample = np.random.choice(face.shape[1], self.num, replace=True, p=area / area.sum())
|
|
594
|
+
face = face[:, sample]
|
|
595
|
+
|
|
596
|
+
frac = np.random.rand(self.num, 2)
|
|
597
|
+
mask = frac.sum(axis=-1) > 1
|
|
598
|
+
frac[mask] = 1 - frac[mask]
|
|
599
|
+
|
|
600
|
+
vec1 = pos[face[1]] - pos[face[0]]
|
|
601
|
+
vec2 = pos[face[2]] - pos[face[0]]
|
|
602
|
+
|
|
603
|
+
if self.include_normals:
|
|
604
|
+
data.normal = _out(_normalize(np.cross(vec1, vec2)), like)
|
|
605
|
+
|
|
606
|
+
pos_sampled = pos[face[0]] + frac[:, :1] * vec1 + frac[:, 1:] * vec2
|
|
607
|
+
data.pos = _out(pos_sampled * pos_max, like)
|
|
608
|
+
|
|
609
|
+
if self.remove_faces:
|
|
610
|
+
data.face = None
|
|
611
|
+
|
|
612
|
+
return data
|
|
613
|
+
|
|
614
|
+
def __repr__(self) -> str:
|
|
615
|
+
return f'{self.__class__.__name__}({self.num})'
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@functional_transform('fixed_points')
|
|
619
|
+
class FixedPoints(BaseTransform):
|
|
620
|
+
r"""Samples a fixed number of points and features from a point cloud
|
|
621
|
+
(functional name: :obj:`fixed_points`)."""
|
|
622
|
+
def __init__(
|
|
623
|
+
self,
|
|
624
|
+
num: int,
|
|
625
|
+
replace: bool = True,
|
|
626
|
+
allow_duplicates: bool = False,
|
|
627
|
+
) -> None:
|
|
628
|
+
self.num = num
|
|
629
|
+
self.replace = replace
|
|
630
|
+
self.allow_duplicates = allow_duplicates
|
|
631
|
+
|
|
632
|
+
def forward(self, data: Data) -> Data:
|
|
633
|
+
num_nodes = data.num_nodes
|
|
634
|
+
assert num_nodes is not None
|
|
635
|
+
|
|
636
|
+
if self.replace:
|
|
637
|
+
choice = np.random.choice(num_nodes, self.num, replace=True)
|
|
638
|
+
elif not self.allow_duplicates:
|
|
639
|
+
choice = np.random.permutation(num_nodes)[:self.num]
|
|
640
|
+
else:
|
|
641
|
+
choice = np.concatenate([
|
|
642
|
+
np.random.permutation(num_nodes)
|
|
643
|
+
for _ in range(math.ceil(self.num / num_nodes))
|
|
644
|
+
])[:self.num]
|
|
645
|
+
|
|
646
|
+
for key, value in list(data.items()):
|
|
647
|
+
if key == 'num_nodes':
|
|
648
|
+
data.num_nodes = len(choice)
|
|
649
|
+
elif bool(re.search('edge', key)):
|
|
650
|
+
continue
|
|
651
|
+
elif _is_tensor(value) and value.shape[0] == num_nodes and value.shape[0] != 1:
|
|
652
|
+
data[key] = _out(_np(value)[choice], value)
|
|
653
|
+
|
|
654
|
+
return data
|
|
655
|
+
|
|
656
|
+
def __repr__(self) -> str:
|
|
657
|
+
return f'{self.__class__.__name__}({self.num}, replace={self.replace})'
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@functional_transform('generate_mesh_normals')
|
|
661
|
+
class GenerateMeshNormals(BaseTransform):
|
|
662
|
+
r"""Generate normal vectors for each mesh node based on neighboring faces
|
|
663
|
+
(functional name: :obj:`generate_mesh_normals`)."""
|
|
664
|
+
def forward(self, data: Data) -> Data:
|
|
665
|
+
assert data.pos is not None
|
|
666
|
+
assert data.face is not None
|
|
667
|
+
pos, face = _np(data.pos), _np(data.face)
|
|
668
|
+
|
|
669
|
+
face_norm = _normalize(np.cross(pos[face[1]] - pos[face[0]], pos[face[2]] - pos[face[0]]))
|
|
670
|
+
norm = _scatter_sum(np.tile(face_norm, (3, 1)), face.reshape(-1), pos.shape[0])
|
|
671
|
+
data.norm = _out(_normalize(norm), data.pos)
|
|
672
|
+
return data
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@functional_transform('delaunay')
|
|
676
|
+
class Delaunay(BaseTransform):
|
|
677
|
+
r"""Computes the delaunay triangulation of a set of points
|
|
678
|
+
(functional name: :obj:`delaunay`)."""
|
|
679
|
+
def forward(self, data: Data) -> Data:
|
|
680
|
+
assert data.pos is not None
|
|
681
|
+
num_points = data.pos.shape[0]
|
|
682
|
+
|
|
683
|
+
if num_points < 2:
|
|
684
|
+
data.edge_index = _out(np.zeros((2, 0)), None, dtype="int64")
|
|
685
|
+
elif num_points == 2:
|
|
686
|
+
data.edge_index = _out(np.array([[0, 1], [1, 0]]), None, dtype="int64")
|
|
687
|
+
elif num_points == 3:
|
|
688
|
+
data.face = _out(np.array([[0], [1], [2]]), None, dtype="int64")
|
|
689
|
+
else:
|
|
690
|
+
try:
|
|
691
|
+
import scipy.spatial
|
|
692
|
+
tri = scipy.spatial.Delaunay(_np(data.pos), qhull_options='QJ')
|
|
693
|
+
except Exception as e:
|
|
694
|
+
raise RuntimeError(f"Delaunay triangulation failed: {e}")
|
|
695
|
+
data.face = _out(tri.simplices.T, None, dtype="int64")
|
|
696
|
+
|
|
697
|
+
return data
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@functional_transform('to_slic')
|
|
701
|
+
class ToSLIC(BaseTransform):
|
|
702
|
+
r"""Converts an image of shape ``[channels, height, width]`` to a superpixel graph using SLIC
|
|
703
|
+
(functional name: :obj:`to_slic`)."""
|
|
704
|
+
def __init__(
|
|
705
|
+
self,
|
|
706
|
+
add_seg: bool = False,
|
|
707
|
+
add_img: bool = False,
|
|
708
|
+
**kwargs: Any,
|
|
709
|
+
) -> None:
|
|
710
|
+
self.add_seg = add_seg
|
|
711
|
+
self.add_img = add_img
|
|
712
|
+
self.kwargs = kwargs
|
|
713
|
+
|
|
714
|
+
def forward(self, img: Any) -> Data:
|
|
715
|
+
try:
|
|
716
|
+
from skimage.segmentation import slic
|
|
717
|
+
except ImportError:
|
|
718
|
+
raise ImportError("ToSLIC requires scikit-image to be installed.")
|
|
719
|
+
|
|
720
|
+
img_t = np.transpose(_np(img), (1, 2, 0))
|
|
721
|
+
h, w, c = img_t.shape
|
|
722
|
+
|
|
723
|
+
seg = slic(img_t.astype(np.float64), start_label=0, **self.kwargs)
|
|
724
|
+
num_segments = int(seg.max()) + 1
|
|
725
|
+
x = _scatter_mean(img_t.reshape(h * w, c), seg.reshape(-1), num_segments)
|
|
726
|
+
|
|
727
|
+
pos_y, pos_x = np.meshgrid(np.arange(h, dtype=np.float32), np.arange(w, dtype=np.float32), indexing="ij")
|
|
728
|
+
pos = _scatter_mean(np.stack([pos_x.reshape(-1), pos_y.reshape(-1)], axis=-1), seg.reshape(-1), num_segments)
|
|
729
|
+
|
|
730
|
+
data = Data(x=_out(x, img), pos=_out(pos, img, dtype="float32"))
|
|
731
|
+
if self.add_seg:
|
|
732
|
+
data.seg = _out(seg.reshape(1, h, w), None, dtype="int64")
|
|
733
|
+
if self.add_img:
|
|
734
|
+
data.img = _out(np.transpose(img_t, (2, 0, 1)).reshape(1, c, h, w), img)
|
|
735
|
+
return data
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@functional_transform('grid_sampling')
|
|
739
|
+
class GridSampling(BaseTransform):
|
|
740
|
+
r"""Clusters points into fixed-sized voxels (functional name: :obj:`grid_sampling`)."""
|
|
741
|
+
def __init__(
|
|
742
|
+
self,
|
|
743
|
+
size: Union[float, List[float], Any],
|
|
744
|
+
start: Optional[Union[float, List[float], Any]] = None,
|
|
745
|
+
end: Optional[Union[float, List[float], Any]] = None,
|
|
746
|
+
) -> None:
|
|
747
|
+
self.size = size
|
|
748
|
+
self.start = start
|
|
749
|
+
self.end = end
|
|
750
|
+
|
|
751
|
+
def forward(self, data: Data) -> Data:
|
|
752
|
+
num_nodes = data.num_nodes
|
|
753
|
+
assert data.pos is not None
|
|
754
|
+
|
|
755
|
+
pos = _np(data.pos)
|
|
756
|
+
if pos.ndim == 1:
|
|
757
|
+
pos = pos[:, None]
|
|
758
|
+
dim = pos.shape[-1]
|
|
759
|
+
|
|
760
|
+
size = np.broadcast_to(np.asarray(_np(self.size), dtype=pos.dtype), (dim,))
|
|
761
|
+
start = pos.min(axis=0) if self.start is None else np.broadcast_to(
|
|
762
|
+
np.asarray(_np(self.start), dtype=pos.dtype), (dim,))
|
|
763
|
+
|
|
764
|
+
coord = np.floor((pos - start) / size).astype(np.int64)
|
|
765
|
+
if getattr(data, 'batch', None) is not None:
|
|
766
|
+
coord = np.concatenate([coord, _np(data.batch).reshape(-1, 1)], axis=-1)
|
|
767
|
+
|
|
768
|
+
unique, c = np.unique(coord, axis=0, return_inverse=True)
|
|
769
|
+
c = c.reshape(-1)
|
|
770
|
+
num_clusters = unique.shape[0]
|
|
771
|
+
perm = np.zeros(num_clusters, dtype=np.int64)
|
|
772
|
+
perm[c] = np.arange(len(c))
|
|
773
|
+
|
|
774
|
+
for key, item in list(data.items()):
|
|
775
|
+
if bool(re.search('edge', key)):
|
|
776
|
+
raise ValueError(f"'{self.__class__.__name__}' does not support coarsening of edges")
|
|
777
|
+
|
|
778
|
+
if _is_tensor(item) and item.shape[0] == num_nodes:
|
|
779
|
+
value = _np(item)
|
|
780
|
+
if key == 'y':
|
|
781
|
+
one_hot = np.eye(int(value.max()) + 1, dtype=np.float32)[value]
|
|
782
|
+
data[key] = _out(_scatter_sum(one_hot, c, num_clusters).argmax(axis=-1), item)
|
|
783
|
+
elif key == 'batch':
|
|
784
|
+
data[key] = _out(value[perm], item)
|
|
785
|
+
else:
|
|
786
|
+
data[key] = _out(_scatter_mean(value, c, num_clusters), item)
|
|
787
|
+
|
|
788
|
+
data.num_nodes = num_clusters
|
|
789
|
+
return data
|
|
790
|
+
|
|
791
|
+
def __repr__(self) -> str:
|
|
792
|
+
return f'{self.__class__.__name__}(size={self.size})'
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
class RandomTranslate(RandomJitter):
|
|
796
|
+
r"""Alias for RandomJitter."""
|
|
797
|
+
pass
|