diff --git a/meson.build b/meson.build index ba9b925..ef9fa60 100644 --- a/meson.build +++ b/meson.build @@ -8,7 +8,7 @@ inc = include_directories('graph_lib/include') graphlib_dep = declare_dependency(include_directories : inc) tests = [ - ['Test', 'test/test.cpp'], + ['Test_Directed_Network', 'test/test_directed_network.cpp'], ] test_inc = [] diff --git a/test/test.cpp b/test/test.cpp deleted file mode 100644 index c571f8e..0000000 --- a/test/test.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "undirected_network.hpp" -#include "util/network_generation.hpp" -#include -#include -#include -#include -#include - -TEST_CASE("Testing the network class") { - using namespace Graph; - using DirectedNetwork = UndirectedNetwork; - - // Generate some network - const size_t n_agents = 20; - const size_t n_connections = 10; - std::mt19937 gen(0); - - REQUIRE(1 == 1); -} \ No newline at end of file diff --git a/test/test_directed_network.cpp b/test/test_directed_network.cpp new file mode 100644 index 0000000..4e81dc1 --- /dev/null +++ b/test/test_directed_network.cpp @@ -0,0 +1,180 @@ +#include "directed_network.hpp" +#include "util/network_generation.hpp" +#include +#include +#include +#include +#include +#include + +TEST_CASE("Testing the directed network class") { + using namespace Graph; + using DirectedNetwork = DirectedNetwork; + using WeightT = double; + + // Generate some network + const size_t n_agents = 4; + + auto network = DirectedNetwork( + std::vector>{{1, 2}, {1}, {0}, {}}, + std::vector>{{0.5, 0.5}, {0.5}, {0.2}, {}}, + DirectedNetwork::EdgeDirection::Incoming); + + // Does n_agents work? + REQUIRE(network.n_agents() == n_agents); + // Does n_edges work? + REQUIRE(network.n_edges() == 4); + + // Check that the function for setting neighbours and a single weight work + // Agent 3 + std::vector neigh{{0, 10}}; // new neighbours + std::vector weight{0.5, 0.5}; // new weights (const) + network.set_neighbours_and_weights(3, neigh, 0.5); + auto buffer_w_get = network.get_weights(3); + + REQUIRE_THAT(weight, Catch::Matchers::UnorderedRangeEquals(buffer_w_get)); + + SECTION("Checking that set_weight, get_neighbour work") { + + weight = {0.25, 0.55}; + network.set_weights(3, weight); + auto buffer_w_get = network.get_weights(3); + REQUIRE_THAT(weight, Catch::Matchers::UnorderedRangeEquals(buffer_w_get)); + REQUIRE(network.n_edges(3) == 2); + + size_t n = network.get_neighbours(3)[0]; + REQUIRE(n == neigh[0]); + n = 2; + // Set the neighbour + network.set_edge(3, 0, n); + REQUIRE(network.get_neighbours(3)[0] == 2); + + DirectedNetwork::WeightT w = network.get_weights(3)[1]; + REQUIRE(w == 0.55); + w = 0.9; + network.set_edge_weight(3, 1, w); + REQUIRE(network.get_weights(3)[1] == w); + } + + SECTION("Checking that set_neighbours_and_weights works with a vector of " + "weights, push_back and transpose") { + // Change the connections for agent 3 + std::vector buffer_n{0}; // new neighbours + std::vector buffer_w{0.1}; // new weights + network.set_neighbours_and_weights(3, buffer_n, buffer_w); + + // Make sure the changes worked + auto buffer_n_get = network.get_neighbours(3); + auto buffer_w_get = network.get_weights(3); + + REQUIRE_THAT(buffer_n_get, Catch::Matchers::UnorderedRangeEquals(buffer_n)); + REQUIRE_THAT(buffer_w_get, Catch::Matchers::UnorderedRangeEquals(buffer_w)); + + // Check that the push_back function works for agent 3 + buffer_n.push_back(1); // new neighbour + buffer_w.push_back(1.0); // new weight for this new connection + network.push_back_neighbour_and_weight( + 3, 1, 1.0); // new connection added with weight + // Check that the change worked for the push_back function + buffer_n_get = network.get_neighbours(3); + buffer_w_get = network.get_weights(3); + REQUIRE_THAT(buffer_n_get, Catch::Matchers::UnorderedRangeEquals(buffer_n)); + REQUIRE_THAT(buffer_w_get, Catch::Matchers::UnorderedRangeEquals(buffer_w)); + + // Now we test the toggle_incoming_outgoing() function + + // First record all the old edges as tuples (i,j,w) where this edge goes + // from j -> i with weight w + std::set> old_edges; + for (size_t i_agent = 0; i_agent < network.n_agents(); i_agent++) { + auto buffer_n = network.get_neighbours(i_agent); + auto buffer_w = network.get_weights(i_agent); + + for (size_t i_neighbour = 0; i_neighbour < buffer_n.size(); + i_neighbour++) { + auto neighbour = buffer_n[i_neighbour]; + auto weight = buffer_w[i_neighbour]; + std::tuple edge{ + i_agent, neighbour, weight}; + old_edges.insert(edge); + } + } + + auto old_direction = network.direction(); + network.toggle_incoming_outgoing(); + auto new_direction = network.direction(); + + // Direction should have changed as well + REQUIRE(old_direction != new_direction); + + // Now we go over the toggled network and try to re-identify all edges + for (size_t i_agent = 0; i_agent < network.n_agents(); i_agent++) { + auto buffer_n = network.get_neighbours(i_agent); + auto buffer_w = network.get_weights(i_agent); + + for (size_t i_neighbour = 0; i_neighbour < buffer_n.size(); + i_neighbour++) { + auto neighbour = buffer_n[i_neighbour]; + auto weight = buffer_w[i_neighbour]; + std::tuple edge{ + neighbour, i_agent, weight}; // Note that i_agent and neighbour are + // flipped compared to before + REQUIRE(old_edges.contains(edge)); // can we find the transposed edge? + old_edges.extract(edge); // extract the edge afterwards + } + } + + REQUIRE(old_edges.empty()); + } + + SECTION("Test remove double counting") { + // clang-format off + std::vector> neighbour_list = { + { 2, 1, 1, 0 }, + { 2, 0, 1, 2}, + { 1, 1, 0, 2, 1 }, + {}, + {3,1} + }; + + std::vector> weight_list = { + { -1, 1, 2, 0 }, + { -1, 1, 2, -1 }, + { -1, 1, 2, 3, 1 }, + {}, + {1, 1} + }; + + std::vector> neighbour_no_double_counting = { + { 0, 1, 2 }, + { 0, 1, 2}, + { 0, 1, 2}, + {}, + {1,3} + }; + + std::vector> weights_no_double_counting = { + { 0, 3, -1 }, + { 1, 2, -2}, + { 2, 1, 3}, + {}, + {1,1} + }; + // clang-format on + + auto network = Graph::DirectedNetwork( + std::move(neighbour_list), std::move(weight_list), + Graph::DirectedNetwork::EdgeDirection::Incoming); + + network.remove_double_counting(); + + for (size_t i_agent = 0; i_agent < network.n_agents(); i_agent++) { + auto weights = network.get_weights(i_agent); + auto neighbours = network.get_neighbours(i_agent); + REQUIRE_THAT(neighbours, Catch::Matchers::RangeEquals( + neighbour_no_double_counting[i_agent])); + REQUIRE_THAT(weights, Catch::Matchers::RangeEquals( + weights_no_double_counting[i_agent])); + } + } +} \ No newline at end of file diff --git a/test/util/network_generation.hpp b/test/util/network_generation.hpp new file mode 100644 index 0000000..5b530a0 --- /dev/null +++ b/test/util/network_generation.hpp @@ -0,0 +1,206 @@ +#pragma once +#include "directed_network.hpp" +#include "undirected_network.hpp" +#include +#include +#include + +// These are usually inside Seldon. We have some functions here to help test... + +namespace Graph::DirectedNetworkGeneration { + +// @TODO generate_fully_connected does not need to be overloaded..perhaps a +// std::optional instead to reduce code duplication? +template +DirectedNetwork generate_fully_connected( + size_t n_agents, + typename DirectedNetwork::WeightT weight = 0.0) { + using DirectedNetworkT = DirectedNetwork; + using WeightT = typename DirectedNetworkT::WeightT; + + std::vector> + neighbour_list; // Neighbour list for the connections + std::vector> + weight_list; // List for the interaction weights of each connection + auto incoming_neighbour_buffer = + std::vector(n_agents); // for the j_agents indices connected to + // i_agent (adjacencies/neighbours) + auto incoming_neighbour_weights = std::vector( + n_agents, weight); // Vector of weights of the j neighbours of i + + // Create the incoming_neighbour_buffer once. This will contain all agents, + // including itself + for (size_t i_agent = 0; i_agent < n_agents; ++i_agent) { + incoming_neighbour_buffer[i_agent] = i_agent; + } + + // Loop through all the agents and update the neighbour_list and weight_list + for (size_t i_agent = 0; i_agent < n_agents; ++i_agent) { + // Add the neighbour vector for i_agent to the neighbour list + neighbour_list.push_back(incoming_neighbour_buffer); + // Add the weight interactions for the neighbours of i_agent + weight_list.push_back(incoming_neighbour_weights); + + } // end of loop through n_agents + + return DirectedNetworkT(std::move(neighbour_list), std::move(weight_list), + DirectedNetworkT::EdgeDirection::Incoming); +} + +template +DirectedNetwork generate_fully_connected(size_t n_agents, + std::mt19937 &gen) { + using DirectedNetworkT = DirectedNetwork; + using WeightT = typename DirectedNetworkT::WeightT; + + std::vector> + neighbour_list; // Neighbour list for the connections + std::vector> + weight_list; // List for the interaction weights of each connection + auto incoming_neighbour_buffer = + std::vector(n_agents); // for the j_agents indices connected to + // i_agent (adjacencies/neighbours) + std::uniform_real_distribution<> dis( + 0.0, 1.0); // Values don't matter, will be normalized + auto incoming_neighbour_weights = std::vector( + n_agents); // Vector of weights of the j neighbours of i + WeightT outgoing_norm_weight = 0; + + // Create the incoming_neighbour_buffer once. This will contain all agents, + // including itself + for (size_t i_agent = 0; i_agent < n_agents; ++i_agent) { + incoming_neighbour_buffer[i_agent] = i_agent; + } + + // Loop through all the agents and create the neighbour_list and weight_list + for (size_t i_agent = 0; i_agent < n_agents; ++i_agent) { + + outgoing_norm_weight = 0.0; + + // Initialize the weights + for (size_t j = 0; j < n_agents; ++j) { + incoming_neighbour_weights[j] = dis(gen); // Draw the weight + outgoing_norm_weight += incoming_neighbour_weights[j]; + } + + // --------- + // Normalize the weights so that the row sums to 1 + // Might be specific to the DeGroot model? + for (size_t j = 0; j < incoming_neighbour_buffer.size(); ++j) { + incoming_neighbour_weights[j] /= outgoing_norm_weight; + } + + // Add the neighbour vector for i_agent to the neighbour list + neighbour_list.push_back(incoming_neighbour_buffer); + // Add the weight interactions for the neighbours of i_agent + weight_list.push_back(incoming_neighbour_weights); + + } // end of loop through n_agents + + return DirectedNetworkT(std::move(neighbour_list), std::move(weight_list), + DirectedNetworkT::EdgeDirection::Incoming); +} + +/* Constructs a new network on a square lattice of edge length n_edge (with + * PBCs)*/ +template +DirectedNetwork generate_square_lattice( + size_t n_edge, typename DirectedNetwork::WeightT weight = 0.0) { + using DirectedNetworkT = DirectedNetwork; + using WeightT = typename DirectedNetworkT::WeightT; + auto n_agents = n_edge * n_edge; + + // Create an empty DirectedNetwork + auto network = DirectedNetworkT(n_agents); + + auto wrap_edge_index = [&](int k) { + if (k >= int(n_edge)) { + return k - int(n_edge); + } else if (k < 0) { + return int(n_edge) + k; + } else { + return k; + } + }; + + auto linear_index = [&](int i, int j) { + auto idx = wrap_edge_index(i) + n_edge * wrap_edge_index(j); + return idx; + }; + + for (int i = 0; i < int(n_edge); i++) { + // other edge + for (int j = 0; j < int(n_edge); j++) { + // Central agent + auto central_index = linear_index(i, j); + + // clang-format off + std::vector neighbours = { + linear_index( i - 1, j ), + linear_index( i + 1, j ), + linear_index( i, j - 1 ), + linear_index( i, j + 1 ) + }; + + // clang-format on + network.set_neighbours_and_weights(central_index, neighbours, weight); + } + } + + return network; +} + +} // namespace Graph::DirectedNetworkGeneration + +namespace Graph::UndirectedNetworkGeneration { +/* Constructs a new network on a square lattice of edge length n_edge (with + * PBCs)*/ +template +UndirectedNetwork generate_square_lattice( + size_t n_edge, + typename UndirectedNetwork::WeightT weight = 0.0) { + using UndirectedNetworkT = UndirectedNetwork; + using WeightT = typename UndirectedNetworkT::WeightT; + auto n_agents = n_edge * n_edge; + + // Create an empty DirectedNetwork + auto network = UndirectedNetworkT(n_agents); + + auto wrap_edge_index = [&](int k) { + if (k >= int(n_edge)) { + return k - int(n_edge); + } else if (k < 0) { + return int(n_edge) + k; + } else { + return k; + } + }; + + auto linear_index = [&](int i, int j) { + auto idx = wrap_edge_index(i) + n_edge * wrap_edge_index(j); + return idx; + }; + + for (int i = 0; i < int(n_edge); i++) { + // other edge + for (int j = 0; j < int(n_edge); j++) { + // Central agent + auto central_index = linear_index(i, j); + + // clang-format off + std::vector neighbours = { + linear_index( i - 1, j ), + linear_index( i, j - 1 ), + }; + + // clang-format on + for (size_t j_idx = 0; j_idx < neighbours.size(); j_idx++) { + network.push_back_neighbour_and_weight(central_index, neighbours[j_idx], + weight); + } + } + } + + return network; +} +} // namespace Graph::UndirectedNetworkGeneration \ No newline at end of file