Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-12-16 09:25:34

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include <boost/test/unit_test.hpp>
0010 
0011 #include "ActsPlugins/Gnn/detail/CantorEdge.hpp"
0012 #include "ActsPlugins/Gnn/detail/TensorVectorConversion.hpp"
0013 #include "ActsPlugins/Gnn/detail/buildEdges.hpp"
0014 
0015 #include <algorithm>
0016 #include <cassert>
0017 #include <iostream>
0018 
0019 #include <Eigen/Core>
0020 #include <torch/torch.h>
0021 
0022 using namespace ActsPlugins;
0023 using namespace ActsPlugins::detail;
0024 
0025 using CantorPair = CantorEdge<int>;
0026 
0027 #define PRINT 0
0028 
0029 float distance(const at::Tensor &a, const at::Tensor &b) {
0030   assert(a.sizes() == b.sizes());
0031   assert(a.sizes().size() == 1);
0032 
0033   return std::sqrt(((a - b) * (a - b)).sum().item().to<float>());
0034 }
0035 
0036 #if PRINT
0037 std::ostream &operator<<(std::ostream &os, CantorPair p) {
0038   auto [a, b] = p.inverse();
0039   os << "(" << a << "," << b << ")";
0040   return os;
0041 }
0042 #endif
0043 
0044 template <typename edge_builder_t>
0045 void test_random_graph(int emb_dim, int n_nodes, float r, int knn,
0046                        const edge_builder_t &edgeBuilder) {
0047   // Create a random point cloud
0048   auto random_features = at::randn({n_nodes, emb_dim});
0049 
0050   // Generate the truth via brute-force
0051   Eigen::MatrixXf distance_matrix(n_nodes, n_nodes);
0052 
0053   std::vector<CantorPair> edges_ref_cantor;
0054   std::vector<int> edge_counts(n_nodes, 0);
0055 
0056   for (int i = 0; i < n_nodes; ++i) {
0057     for (int j = i; j < n_nodes; ++j) {
0058       const auto d = distance(random_features[i], random_features[j]);
0059       distance_matrix(i, j) = d;
0060       distance_matrix(j, i) = d;
0061 
0062       if (d < r && i != j) {
0063         edges_ref_cantor.emplace_back(i, j);
0064         edge_counts[i]++;
0065       }
0066     }
0067   }
0068 
0069   const auto max_edges =
0070       *std::max_element(edge_counts.begin(), edge_counts.end());
0071 
0072   // If this is not the case, the test is ill-formed
0073   // knn specifies how many edges can be found by the function at max. Thus, we
0074   // should design the test in a way, that our brute-force test algorithm does
0075   // not find more edges than the algorithm that we test against it can find
0076   BOOST_REQUIRE(max_edges <= knn);
0077 
0078   // Run the edge building
0079   auto edges_test = edgeBuilder(random_features, r, knn);
0080 
0081   // Map the edges to cantor pairs
0082   std::vector<CantorPair> edges_test_cantor;
0083 
0084   for (int i = 0; i < edges_test.size(1); ++i) {
0085     const auto a = edges_test[0][i].template item<int>();
0086     const auto b = edges_test[1][i].template item<int>();
0087     edges_test_cantor.push_back(a < b ? CantorPair(a, b) : CantorPair(b, a));
0088   }
0089 
0090   std::ranges::sort(edges_ref_cantor, std::less<CantorPair>{});
0091   std::ranges::sort(edges_test_cantor, std::less<CantorPair>{});
0092 
0093 #if PRINT
0094   std::cout << "test size " << edges_test_cantor.size() << std::endl;
0095   std::cout << "ref size " << edges_ref_cantor.size() << std::endl;
0096   std::cout << "test: ";
0097   std::copy(
0098       edges_test_cantor.begin(),
0099       edges_test_cantor.begin() + std::min(edges_test_cantor.size(), 10ul),
0100       std::ostream_iterator<CantorPair>(std::cout, " "));
0101   std::cout << std::endl;
0102   std::cout << "ref: ";
0103   std::copy(edges_ref_cantor.begin(),
0104             edges_ref_cantor.begin() + std::min(edges_ref_cantor.size(), 10ul),
0105             std::ostream_iterator<CantorPair>(std::cout, " "));
0106   std::cout << std::endl;
0107 #endif
0108 
0109   // Check
0110   BOOST_CHECK_EQUAL(edges_ref_cantor.size(), edges_test_cantor.size());
0111   BOOST_CHECK(std::equal(edges_test_cantor.begin(), edges_test_cantor.end(),
0112                          edges_ref_cantor.begin()));
0113 }
0114 
0115 namespace ActsTests {
0116 
0117 BOOST_AUTO_TEST_SUITE(GnnSuite)
0118 
0119 BOOST_AUTO_TEST_CASE(test_cantor_pair_functions) {
0120   int a = 345;
0121   int b = 23;
0122   // Use non-sorted cantor pair to make this work
0123   const auto [aa, bb] = CantorPair(a, b, false).inverse();
0124   BOOST_CHECK_EQUAL(a, aa);
0125   BOOST_CHECK_EQUAL(b, bb);
0126 }
0127 
0128 BOOST_AUTO_TEST_CASE(test_cantor_pair_sorted) {
0129   int a = 345;
0130   int b = 23;
0131   CantorPair c1(a, b);
0132   CantorPair c2(b, a);
0133   BOOST_CHECK_EQUAL(c1.value(), c2.value());
0134 }
0135 
0136 const int emb_dim = 3;
0137 const int n_nodes = 20;
0138 const float r = 1.5;
0139 const int knn = 50;
0140 const int seed = 42;
0141 
0142 BOOST_AUTO_TEST_CASE(test_random_graph_edge_building_cuda,
0143                      *boost::unit_test::precondition([](auto) {
0144                        return torch::cuda::is_available();
0145                      })) {
0146   torch::manual_seed(seed);
0147 
0148   auto cudaEdgeBuilder = [](auto &features, auto radius, auto k) {
0149     auto features_cuda = features.to(torch::kCUDA);
0150     return buildEdgesFRNN(features_cuda, radius, k);
0151   };
0152 
0153   test_random_graph(emb_dim, n_nodes, r, knn, cudaEdgeBuilder);
0154 }
0155 
0156 BOOST_AUTO_TEST_CASE(test_random_graph_edge_building_kdtree) {
0157   torch::manual_seed(seed);
0158 
0159   auto cpuEdgeBuilder = [](auto &features, auto radius, auto k) {
0160     auto features_cpu = features.to(torch::kCPU);
0161     return buildEdgesKDTree(features_cpu, radius, k);
0162   };
0163 
0164   test_random_graph(emb_dim, n_nodes, r, knn, cpuEdgeBuilder);
0165 }
0166 
0167 BOOST_AUTO_TEST_CASE(test_self_loop_removal) {
0168   // clang-format off
0169   std::vector<std::int64_t> edges = {
0170     1,1,
0171     2,3,
0172     2,2,
0173     5,4,
0174   };
0175   // clang-format on
0176 
0177   auto opts = torch::TensorOptions().dtype(torch::kInt64);
0178   const auto edgeTensor =
0179       torch::from_blob(edges.data(), {static_cast<long>(edges.size() / 2), 2},
0180                        opts)
0181           .transpose(0, 1);
0182 
0183   const auto withoutSelfLoops =
0184       postprocessEdgeTensor(edgeTensor, true, false, false)
0185           .transpose(1, 0)
0186           .flatten();
0187 
0188   const std::vector<std::int64_t> postEdges(
0189       withoutSelfLoops.data_ptr<std::int64_t>(),
0190       withoutSelfLoops.data_ptr<std::int64_t>() + withoutSelfLoops.numel());
0191 
0192   // clang-format off
0193   const std::vector<std::int64_t> ref = {
0194     2,3,
0195     5,4,
0196   };
0197   // clang-format on
0198 
0199   BOOST_CHECK_EQUAL(ref, postEdges);
0200 }
0201 
0202 BOOST_AUTO_TEST_CASE(test_duplicate_removal) {
0203   // clang-format off
0204   std::vector<std::int64_t> edges = {
0205     1,2,
0206     2,1,   // duplicate, flipped
0207     3,2,
0208     3,2,   // duplicate, not flipped
0209     7,6,   // should be flipped
0210   };
0211   // clang-format on
0212 
0213   auto opts = torch::TensorOptions().dtype(torch::kInt64);
0214   const auto edgeTensor =
0215       torch::from_blob(edges.data(), {static_cast<long>(edges.size() / 2), 2},
0216                        opts)
0217           .transpose(0, 1);
0218 
0219   const auto withoutDups = postprocessEdgeTensor(edgeTensor, false, true, false)
0220                                .transpose(1, 0)
0221                                .flatten();
0222 
0223   const std::vector<std::int64_t> postEdges(
0224       withoutDups.data_ptr<std::int64_t>(),
0225       withoutDups.data_ptr<std::int64_t>() + withoutDups.numel());
0226 
0227   // clang-format off
0228   const std::vector<std::int64_t> ref = {
0229     1,2,
0230     2,3,
0231     6,7,
0232   };
0233   // clang-format on
0234 
0235   BOOST_CHECK_EQUAL(ref, postEdges);
0236 }
0237 
0238 BOOST_AUTO_TEST_CASE(test_random_flip) {
0239   torch::manual_seed(seed);
0240 
0241   // clang-format off
0242   std::vector<std::int64_t> edges = {
0243     1,2,
0244     2,3,
0245     3,4,
0246     4,5,
0247   };
0248   // clang-format on
0249 
0250   auto opts = torch::TensorOptions().dtype(torch::kInt64);
0251   const auto edgeTensor =
0252       torch::from_blob(edges.data(), {static_cast<long>(edges.size() / 2), 2},
0253                        opts)
0254           .transpose(0, 1);
0255 
0256   const auto flipped = postprocessEdgeTensor(edgeTensor, false, false, true)
0257                            .transpose(0, 1)
0258                            .flatten();
0259 
0260   const std::vector<std::int64_t> postEdges(
0261       flipped.data_ptr<std::int64_t>(),
0262       flipped.data_ptr<std::int64_t>() + flipped.numel());
0263 
0264   BOOST_CHECK_EQUAL(postEdges.size(), edges.size());
0265   for (auto preIt = edges.begin(); preIt != edges.end(); preIt += 2) {
0266     int found = 0;
0267 
0268     for (auto postIt = postEdges.begin(); postIt != postEdges.end();
0269          postIt += 2) {
0270       bool noflp = (*preIt == *postIt) and *(preIt + 1) == *(postIt + 1);
0271       bool flp = *preIt == *(postIt + 1) and *(preIt + 1) == *(postIt);
0272 
0273       found += (flp or noflp);
0274     }
0275 
0276     BOOST_CHECK_EQUAL(found, 1);
0277   }
0278 }
0279 
0280 BOOST_AUTO_TEST_SUITE_END()
0281 
0282 }  // namespace ActsTests