From efc08a91186f5ec988c590b34b9ddf4e0f2110f6 Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Tue, 19 Mar 2024 18:27:03 +0000 Subject: [PATCH 01/24] ActivityDrivenModel: Default initialize in class The ConfigParser sets variables and checks them, so defaults should not live inside the class Co-authored-by: Moritz Sallermann --- include/models/ActivityDrivenModel.hpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/include/models/ActivityDrivenModel.hpp b/include/models/ActivityDrivenModel.hpp index b1f40c2..69c7d37 100644 --- a/include/models/ActivityDrivenModel.hpp +++ b/include/models/ActivityDrivenModel.hpp @@ -67,26 +67,26 @@ class ActivityDrivenModel : public Model // Model-specific parameters double dt = 0.01; // Timestep for the integration of the coupled ODEs // Various free parameters - int m = 10; // Number of agents contacted, when the agent is active - double eps = 0.01; // Minimum activity epsilon; a_i belongs to [epsilon,1] - double gamma = 2.1; // Exponent of activity power law distribution of activities - double alpha = 3.0; // Controversialness of the issue, must be greater than 0. - double homophily = 0.5; // aka beta. if zero, agents pick their interaction partners at random + int m{}; // Number of agents contacted, when the agent is active + double eps{}; // Minimum activity epsilon; a_i belongs to [epsilon,1] + double gamma{}; // Exponent of activity power law distribution of activities + double alpha{}; // Controversialness of the issue, must be greater than 0. + double homophily{}; // aka beta. if zero, agents pick their interaction partners at random // Reciprocity aka r. probability that when agent i contacts j via weighted reservoir sampling // j also sends feedback to i. So every agent can have more than m incoming connections - double reciprocity = 0.5; - double K = 3.0; // Social interaction strength; K>0 + double reciprocity{}; + double K{}; // Social interaction strength; K>0 bool mean_activities = false; bool mean_weights = false; double convergence_tol = 1e-12; // TODO: ?? - bool use_reluctances = false; - double reluctance_mean = 1.0; - double reluctance_sigma = 0.25; - double reluctance_eps = 0.01; - double covariance_factor = 0.0; + bool use_reluctances = false; + double reluctance_mean{}; + double reluctance_sigma{}; + double reluctance_eps{}; + double covariance_factor{}; // bot @TODO: less hacky From 43f5f8f40f694de226df58d629657dc36d13531e Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Tue, 19 Mar 2024 18:28:25 +0000 Subject: [PATCH 02/24] draw_k_from_n: Made ignore_idx optional Now you can use the draw_k_from_n function without repitition while optionally including or excluding a particular index from 0 to N Co-authored-by: Moritz Sallermann --- include/util/math.hpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/include/util/math.hpp b/include/util/math.hpp index 607fe72..53520fd 100644 --- a/include/util/math.hpp +++ b/include/util/math.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include #include @@ -13,7 +14,8 @@ namespace Seldon // drawing from n agents (without duplication) // ignore_idx ignores the index of the agent itself, since we will later add the agent itself ourselves to prevent duplication inline void draw_unique_k_from_n( - std::size_t ignore_idx, std::size_t k, std::size_t n, std::vector & buffer, std::mt19937 & gen ) + std::optional ignore_idx, std::size_t k, std::size_t n, std::vector & buffer, + std::mt19937 & gen ) { struct SequenceGenerator { @@ -24,15 +26,15 @@ inline void draw_unique_k_from_n( using pointer = size_t *; // or also value_type* using reference = size_t &; - SequenceGenerator( const size_t i_, const size_t ignore_idx ) : i( i_ ), ignore_idx( ignore_idx ) + SequenceGenerator( const size_t i_, std::optional ignore_idx ) : i( i_ ), ignore_idx( ignore_idx ) { - if( i == ignore_idx ) + if( ignore_idx.has_value() && i == ignore_idx.value() ) { i++; } } size_t i; - size_t ignore_idx; + std::optional ignore_idx; size_t & operator*() { @@ -45,7 +47,7 @@ inline void draw_unique_k_from_n( SequenceGenerator & operator++() { i++; - if( i == ignore_idx ) + if( ignore_idx.has_value() && i == ignore_idx ) i++; return *this; } From a1b7ccce495f87a8878d9fc926c92485979844e2 Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Tue, 19 Mar 2024 19:30:43 +0000 Subject: [PATCH 03/24] DeffuantModel: WIP init of Deffuant model This compiles but segfaults when you run the example. Some silly prints inside simulation for ad-hoc debugging. Co-authored-by: Moritz Sallermann --- examples/Deffuant/conf.toml | 21 ++++++++++ include/config_parser.hpp | 13 +++++- include/connectivity.hpp | 2 +- include/models/DeffuantModel.hpp | 35 ++++++++++++++++ include/simulation.hpp | 48 ++++++++++++++++----- meson.build | 1 + src/config_parser.cpp | 71 +++++++++++++++++++++----------- src/models/DeffuantModel.cpp | 45 ++++++++++++++++++++ test/test_network.cpp | 2 +- 9 files changed, 200 insertions(+), 38 deletions(-) create mode 100644 examples/Deffuant/conf.toml create mode 100644 include/models/DeffuantModel.hpp create mode 100644 src/models/DeffuantModel.cpp diff --git a/examples/Deffuant/conf.toml b/examples/Deffuant/conf.toml new file mode 100644 index 0000000..d8ab8a4 --- /dev/null +++ b/examples/Deffuant/conf.toml @@ -0,0 +1,21 @@ +[simulation] +model = "Deffuant" +# rng_seed = 120 # Leaving this empty will pick a random seed + +[io] +# n_output_network = 20 # Write the network every 20 iterations +n_output_agents = 1 # Write the opinions of agents after every iteration +print_progress = true # Print the iteration time ; if not set, then does not prints +output_initial = true # Print the initial opinions and network file from step 0. If not set, this is true by default. +start_output = 1 # Start writing out opinions and/or network files from this iteration. If not set, this is 1. + +[model] +max_iterations = 1000 # If not set, max iterations is infinite + +[Deffuant] +homophily_threshold = 0.2 # d in the paper; agents interact if difference in opinion is less than this value +mu = 0.5 # convergence parameter; similar to social interaction strength K (0,0.5] + +[network] +number_of_agents = 1000 +connections_per_agent = 0 diff --git a/include/config_parser.hpp b/include/config_parser.hpp index c0e14cb..04e28dc 100644 --- a/include/config_parser.hpp +++ b/include/config_parser.hpp @@ -24,7 +24,8 @@ namespace Seldon::Config enum class Model { DeGroot, - ActivityDrivenModel + ActivityDrivenModel, + DeffuantModel }; struct OutputSettings @@ -43,6 +44,14 @@ struct DeGrootSettings double convergence_tol; }; +struct DeffuantSettings +{ + std::optional max_iterations = std::nullopt; + double homophily_threshold + = 0.2; // d in the paper; agents interact if difference in opinion is less than this value + double mu = 0.5; // convergence parameter; similar to social interaction strength K (0,0.5] +}; + struct ActivityDrivenSettings { std::optional max_iterations = std::nullopt; @@ -81,7 +90,7 @@ struct SimulationOptions std::string model_string; int rng_seed = std::random_device()(); OutputSettings output_settings; - std::variant model_settings; + std::variant model_settings; InitialNetworkSettings network_settings; }; diff --git a/include/connectivity.hpp b/include/connectivity.hpp index a9d0159..1af9c8d 100644 --- a/include/connectivity.hpp +++ b/include/connectivity.hpp @@ -75,7 +75,7 @@ class TarjanConnectivityAlgo { lowest[v] = std::min( lowest[v], num[u] ); } // u not processed - } // u has been visited + } // u has been visited } // Now v has been processed diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp new file mode 100644 index 0000000..2cba82d --- /dev/null +++ b/include/models/DeffuantModel.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "agents/simple_agent.hpp" +#include "model.hpp" +#include "network.hpp" +#include +#include +#include +#include +#include +#include + +namespace Seldon +{ + +class DeffuantModel : public Model +{ +public: + using AgentT = SimpleAgent; + using NetworkT = Network; + + double homophily_threshold = 0.2; // d in paper + double mu = 0.5; // convergence parameter + + DeffuantModel( NetworkT & network, std::mt19937 & gen ); + + void iteration() override; + // bool finished() override; + +private: + NetworkT & network; + std::mt19937 & gen; // reference to simulation Mersenne-Twister engine +}; + +} // namespace Seldon \ No newline at end of file diff --git a/include/simulation.hpp b/include/simulation.hpp index eab282c..574e30f 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -1,6 +1,7 @@ #pragma once #include "config_parser.hpp" +#include "fmt/core.h" #include "network.hpp" #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -81,20 +83,44 @@ class Simulation : public SimulationInterface if constexpr( std::is_same_v ) { - auto degroot_settings = std::get( options.model_settings ); - - // DeGroot specific parameters - model = [&]() + fmt::print( "I am a simple agent\n" ); + if( options.model == Config::Model::DeGroot ) { - auto model = std::make_unique( network ); - model->max_iterations = degroot_settings.max_iterations; - model->convergence_tol = degroot_settings.convergence_tol; - return model; - }(); + auto degroot_settings = std::get( options.model_settings ); - if( cli_agent_file.has_value() ) + // DeGroot specific parameters + model = [&]() + { + auto model = std::make_unique( network ); + model->max_iterations = degroot_settings.max_iterations; + model->convergence_tol = degroot_settings.convergence_tol; + return model; + }(); + + if( cli_agent_file.has_value() ) + { + network.agents = agents_from_file( cli_agent_file.value() ); + } + } + else if( options.model == Config::Model::DeffuantModel ) { - network.agents = agents_from_file( cli_agent_file.value() ); + fmt::print( "I am inside the Deffuant model loop.\n" ); + auto deffuant_settings = std::get( options.model_settings ); + + // Deffuant model specific parameters + model = [&]() + { + auto model = std::make_unique( network, gen ); + model->max_iterations = deffuant_settings.max_iterations; + model->homophily_threshold = deffuant_settings.homophily_threshold; + model->mu = deffuant_settings.mu; + return model; + }(); + + if( cli_agent_file.has_value() ) + { + network.agents = agents_from_file( cli_agent_file.value() ); + } } } else if constexpr( std::is_same_v ) diff --git a/meson.build b/meson.build index 7f146e0..27c81e3 100644 --- a/meson.build +++ b/meson.build @@ -10,6 +10,7 @@ sources_seldon = [ 'src/config_parser.cpp', 'src/models/DeGroot.cpp', 'src/models/ActivityDrivenModel.cpp', + 'src/models/DeffuantModel.cpp', 'src/util/tomlplusplus.cpp', ] diff --git a/src/config_parser.cpp b/src/config_parser.cpp index a5e799a..aa4aafa 100644 --- a/src/config_parser.cpp +++ b/src/config_parser.cpp @@ -25,6 +25,10 @@ Model model_string_to_enum( std::string_view model_string ) { return Model::ActivityDrivenModel; } + else if( model_string == "Deffuant" ) + { + return Model::DeffuantModel; + } throw std::runtime_error( fmt::format( "Invalid model string {}", model_string ) ); } @@ -62,36 +66,44 @@ SimulationOptions parse_config_file( std::string_view config_file_path ) if( options.model == Model::DeGroot ) { - auto model_settings = DeGrootSettings(); - model_settings.max_iterations = tbl["model"]["max_iterations"].value(); - model_settings.convergence_tol = tbl["DeGroot"]["convergence"].value_or( model_settings.convergence_tol ); - options.model_settings = model_settings; + auto model_settings = DeGrootSettings(); + model_settings.max_iterations = tbl["model"]["max_iterations"].value(); + set_if_specified( model_settings.convergence_tol, tbl[options.model_string]["convergence"] ); + options.model_settings = model_settings; + } + else if( options.model == Model::DeffuantModel ) + { + auto model_settings = DeffuantSettings(); + model_settings.max_iterations = tbl["model"]["max_iterations"].value(); + set_if_specified( model_settings.homophily_threshold, tbl[options.model_string]["homophily_threshold"] ); + set_if_specified( model_settings.mu, tbl[options.model_string]["mu"] ); + options.model_settings = model_settings; } else if( options.model == Model::ActivityDrivenModel ) { auto model_settings = ActivityDrivenSettings(); - set_if_specified( model_settings.dt, tbl["ActivityDriven"]["dt"] ); - set_if_specified( model_settings.m, tbl["ActivityDriven"]["m"] ); - set_if_specified( model_settings.eps, tbl["ActivityDriven"]["eps"] ); - set_if_specified( model_settings.gamma, tbl["ActivityDriven"]["gamma"] ); - set_if_specified( model_settings.homophily, tbl["ActivityDriven"]["homophily"] ); - set_if_specified( model_settings.reciprocity, tbl["ActivityDriven"]["reciprocity"] ); - set_if_specified( model_settings.alpha, tbl["ActivityDriven"]["alpha"] ); - set_if_specified( model_settings.K, tbl["ActivityDriven"]["K"] ); + set_if_specified( model_settings.dt, tbl[options.model_string]["dt"] ); + set_if_specified( model_settings.m, tbl[options.model_string]["m"] ); + set_if_specified( model_settings.eps, tbl[options.model_string]["eps"] ); + set_if_specified( model_settings.gamma, tbl[options.model_string]["gamma"] ); + set_if_specified( model_settings.homophily, tbl[options.model_string]["homophily"] ); + set_if_specified( model_settings.reciprocity, tbl[options.model_string]["reciprocity"] ); + set_if_specified( model_settings.alpha, tbl[options.model_string]["alpha"] ); + set_if_specified( model_settings.K, tbl[options.model_string]["K"] ); // Mean activity model options - set_if_specified( model_settings.mean_activities, tbl["ActivityDriven"]["mean_activities"] ); - set_if_specified( model_settings.mean_weights, tbl["ActivityDriven"]["mean_weights"] ); + set_if_specified( model_settings.mean_activities, tbl[options.model_string]["mean_activities"] ); + set_if_specified( model_settings.mean_weights, tbl[options.model_string]["mean_weights"] ); // Reluctances - set_if_specified( model_settings.use_reluctances, tbl["ActivityDriven"]["reluctances"] ); - set_if_specified( model_settings.reluctance_mean, tbl["ActivityDriven"]["reluctance_mean"] ); - set_if_specified( model_settings.reluctance_sigma, tbl["ActivityDriven"]["reluctance_sigma"] ); - set_if_specified( model_settings.reluctance_eps, tbl["ActivityDriven"]["reluctance_eps"] ); + set_if_specified( model_settings.use_reluctances, tbl[options.model_string]["reluctances"] ); + set_if_specified( model_settings.reluctance_mean, tbl[options.model_string]["reluctance_mean"] ); + set_if_specified( model_settings.reluctance_sigma, tbl[options.model_string]["reluctance_sigma"] ); + set_if_specified( model_settings.reluctance_eps, tbl[options.model_string]["reluctance_eps"] ); model_settings.max_iterations = tbl["model"]["max_iterations"].value(); // bot - set_if_specified( model_settings.n_bots, tbl["ActivityDriven"]["n_bots"] ); + set_if_specified( model_settings.n_bots, tbl[options.model_string]["n_bots"] ); auto push_back_bot_array = [&]( auto toml_node, auto & options_array, auto default_value ) { @@ -118,10 +130,10 @@ SimulationOptions parse_config_file( std::string_view config_file_path ) } }; - auto bot_opinion = tbl["ActivityDriven"]["bot_opinion"]; - auto bot_m = tbl["ActivityDriven"]["bot_m"]; - auto bot_activity = tbl["ActivityDriven"]["bot_activity"]; - auto bot_homophily = tbl["ActivityDriven"]["bot_homophily"]; + auto bot_opinion = tbl[options.model_string]["bot_opinion"]; + auto bot_m = tbl[options.model_string]["bot_m"]; + auto bot_activity = tbl[options.model_string]["bot_activity"]; + auto bot_homophily = tbl[options.model_string]["bot_homophily"]; push_back_bot_array( bot_m, model_settings.bot_m, model_settings.m ); push_back_bot_array( bot_opinion, model_settings.bot_opinion, 0.0 ); @@ -197,6 +209,12 @@ void validate_settings( const SimulationOptions & options ) auto model_settings = std::get( options.model_settings ); check( name_and_var( model_settings.convergence_tol ), geq_zero ); } + else if( options.model == Model::DeffuantModel ) + { + auto model_settings = std::get( options.model_settings ); + check( name_and_var( model_settings.homophily_threshold ), g_zero ); + check( name_and_var( model_settings.mu ), []( auto x ) { return x >= 0 && x <= 1; } ); + } } void print_settings( const SimulationOptions & options ) @@ -246,6 +264,13 @@ void print_settings( const SimulationOptions & options ) fmt::print( " max_iterations {}\n", model_settings.max_iterations ); fmt::print( " convergence_tol {}\n", model_settings.convergence_tol ); } + else if( options.model == Model::DeffuantModel ) + { + auto model_settings = std::get( options.model_settings ); + fmt::print( " max_iterations {}\n", model_settings.max_iterations ); + fmt::print( " homophily_threshold {}\n", model_settings.homophily_threshold ); + fmt::print( " mu {}\n", model_settings.mu ); + } fmt::print( "[Network]\n" ); fmt::print( " n_agents {}\n", options.network_settings.n_agents ); diff --git a/src/models/DeffuantModel.cpp b/src/models/DeffuantModel.cpp new file mode 100644 index 0000000..22a10f1 --- /dev/null +++ b/src/models/DeffuantModel.cpp @@ -0,0 +1,45 @@ +#include "models/DeffuantModel.hpp" +#include "network.hpp" +#include "util/math.hpp" +#include +#include +#include +#include + +namespace Seldon +{ + +DeffuantModel::DeffuantModel( NetworkT & network, std::mt19937 & gen ) + : Model(), network( network ), gen( gen ) +{ +} + +void DeffuantModel::iteration() +{ + Model::iteration(); // Update n_iterations + + // Although the model defines each iteration as choosing *one* + // pair of agents, we will define each iteration as sampling + // n_agents pairs (similar to the time unit in evolution plots in the paper) + for( size_t i = 0; i < network.n_agents(); i++ ) + { + + auto interacting_agents = std::vector(); + + // Pick any two agents to interact, randomly, without repetition + draw_unique_k_from_n( std::nullopt, 2, network.n_agents(), interacting_agents, gen ); + + auto & agent1 = network.agents[interacting_agents[0]]; + auto & agent2 = network.agents[interacting_agents[1]]; + + // Update rule + auto opinion_diff = agent1.data.opinion - agent2.data.opinion; + // Only if less than homophily_threshold + if( std::abs( opinion_diff ) < homophily_threshold ) + { + agent1.data.opinion -= mu * opinion_diff; + agent2.data.opinion += mu * opinion_diff; + } + } +} +} // namespace Seldon \ No newline at end of file diff --git a/test/test_network.cpp b/test/test_network.cpp index f17e44b..48d83da 100644 --- a/test/test_network.cpp +++ b/test/test_network.cpp @@ -112,7 +112,7 @@ TEST_CASE( "Testing the network class" ) auto weight = buffer_w[i_neighbour]; std::tuple edge{ neighbour, i_agent, weight - }; // Note that i_agent and neighbour are flipped compared to before + }; // 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 } From 8308376e574196856cd6d6a71ec11b4f646aad9d Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Tue, 19 Mar 2024 19:45:44 +0000 Subject: [PATCH 04/24] Deffuant: Fixed creation of model in simulation Co-authored-by: Amrita Goswami --- include/simulation.hpp | 2 -- src/main.cpp | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/include/simulation.hpp b/include/simulation.hpp index 574e30f..bdf8354 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -83,7 +83,6 @@ class Simulation : public SimulationInterface if constexpr( std::is_same_v ) { - fmt::print( "I am a simple agent\n" ); if( options.model == Config::Model::DeGroot ) { auto degroot_settings = std::get( options.model_settings ); @@ -104,7 +103,6 @@ class Simulation : public SimulationInterface } else if( options.model == Config::Model::DeffuantModel ) { - fmt::print( "I am inside the Deffuant model loop.\n" ); auto deffuant_settings = std::get( options.model_settings ); // Deffuant model specific parameters diff --git a/src/main.cpp b/src/main.cpp index 78a6db9..9343730 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "config_parser.hpp" #include "models/DeGroot.hpp" +#include "models/DeffuantModel.hpp" #include "simulation.hpp" #include #include @@ -69,6 +70,15 @@ int main( int argc, char * argv[] ) simulation = std::make_unique>( simulation_options, network_file, agent_file ); } + else if( simulation_options.model == Seldon::Config::Model::DeffuantModel ) + { + simulation = std::make_unique>( + simulation_options, network_file, agent_file ); + } + else + { + throw std::runtime_error( "Model has not been created" ); + } simulation->run( output_dir_path ); From 075d80330eb94a8287b43fe955b5ab0cc25ad432 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Tue, 19 Mar 2024 19:57:58 +0000 Subject: [PATCH 05/24] ActivityDrivenModel: public members first More clarity. The public interface of a class should be the most visible Co-authored-by: Amrita Goswami --- include/models/ActivityDrivenModel.hpp | 86 +++++++++++++------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/include/models/ActivityDrivenModel.hpp b/include/models/ActivityDrivenModel.hpp index 69c7d37..7459409 100644 --- a/include/models/ActivityDrivenModel.hpp +++ b/include/models/ActivityDrivenModel.hpp @@ -19,6 +19,48 @@ class ActivityDrivenModel : public Model using AgentT = ActivityAgent; using NetworkT = Network; + ActivityDrivenModel( NetworkT & network, std::mt19937 & gen ); + + void get_agents_from_power_law(); // This needs to be called after eps and gamma have been set + + void iteration() override; + + // Model-specific parameters + double dt = 0.01; // Timestep for the integration of the coupled ODEs + // Various free parameters + int m{}; // Number of agents contacted, when the agent is active + double eps{}; // Minimum activity epsilon; a_i belongs to [epsilon,1] + double gamma{}; // Exponent of activity power law distribution of activities + double alpha{}; // Controversialness of the issue, must be greater than 0. + double homophily{}; // aka beta. if zero, agents pick their interaction partners at random + // Reciprocity aka r. probability that when agent i contacts j via weighted reservoir sampling + // j also sends feedback to i. So every agent can have more than m incoming connections + double reciprocity{}; + double K{}; // Social interaction strength; K>0 + + bool mean_activities = false; + bool mean_weights = false; + + double convergence_tol = 1e-12; // TODO: ?? + + bool use_reluctances = false; + double reluctance_mean{}; + double reluctance_sigma{}; + double reluctance_eps{}; + double covariance_factor{}; + + // bot @TODO: less hacky + size_t n_bots = 0; // The first n_bots agents are bots + std::vector bot_m = std::vector( 0 ); + std::vector bot_activity = std::vector( 0 ); + std::vector bot_opinion = std::vector( 0 ); + std::vector bot_homophily = std::vector( 0 ); + + [[nodiscard]] bool bot_present() const + { + return n_bots > 0; + } + private: NetworkT & network; std::vector> contact_prob_list; // Probability of choosing i in 1 to m rounds @@ -62,50 +104,6 @@ class ActivityDrivenModel : public Model void update_network_probabilistic(); void update_network_mean(); void update_network(); - -public: - // Model-specific parameters - double dt = 0.01; // Timestep for the integration of the coupled ODEs - // Various free parameters - int m{}; // Number of agents contacted, when the agent is active - double eps{}; // Minimum activity epsilon; a_i belongs to [epsilon,1] - double gamma{}; // Exponent of activity power law distribution of activities - double alpha{}; // Controversialness of the issue, must be greater than 0. - double homophily{}; // aka beta. if zero, agents pick their interaction partners at random - // Reciprocity aka r. probability that when agent i contacts j via weighted reservoir sampling - // j also sends feedback to i. So every agent can have more than m incoming connections - double reciprocity{}; - double K{}; // Social interaction strength; K>0 - - bool mean_activities = false; - bool mean_weights = false; - - double convergence_tol = 1e-12; // TODO: ?? - - bool use_reluctances = false; - double reluctance_mean{}; - double reluctance_sigma{}; - double reluctance_eps{}; - double covariance_factor{}; - - // bot @TODO: less hacky - - size_t n_bots = 0; // The first n_bots agents are bots - std::vector bot_m = std::vector( 0 ); - std::vector bot_activity = std::vector( 0 ); - std::vector bot_opinion = std::vector( 0 ); - std::vector bot_homophily = std::vector( 0 ); - - [[nodiscard]] bool bot_present() const - { - return n_bots > 0; - } - - ActivityDrivenModel( NetworkT & network, std::mt19937 & gen ); - - void get_agents_from_power_law(); // This needs to be called after eps and gamma have been set - - void iteration() override; }; } // namespace Seldon \ No newline at end of file From 2a0924caaa56e37536e3165ac972b2d3ce6cb6f1 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Tue, 19 Mar 2024 20:25:19 +0000 Subject: [PATCH 06/24] Deffuant: unit test and initial agent opinions Implemented a simple unit test with two agents for the basic deffuant model. Also initialize the agents uniformly on the interval [0,1] in the constructor (like we do in the DeGroot model). Co-authored-by: Amrita Goswami --- meson.build | 1 + src/models/DeffuantModel.cpp | 5 +++ test/res/deffuant_2agents.toml | 21 ++++++++++++ test/test_deffuant.cpp | 62 ++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 test/res/deffuant_2agents.toml create mode 100644 test/test_deffuant.cpp diff --git a/meson.build b/meson.build index 27c81e3..c28b7c3 100644 --- a/meson.build +++ b/meson.build @@ -25,6 +25,7 @@ tests = [ ['Test_Tarjan', 'test/test_tarjan.cpp'], ['Test_DeGroot', 'test/test_deGroot.cpp'], ['Test_Activity_Driven', 'test/test_activity.cpp'], + ['Test_Deffuant', 'test/test_deffuant.cpp'], ['Test_Network', 'test/test_network.cpp'], ['Test_Network_Generation', 'test/test_network_generation.cpp'], ['Test_Sampling', 'test/test_sampling.cpp'], diff --git a/src/models/DeffuantModel.cpp b/src/models/DeffuantModel.cpp index 22a10f1..c4c6308 100644 --- a/src/models/DeffuantModel.cpp +++ b/src/models/DeffuantModel.cpp @@ -12,6 +12,10 @@ namespace Seldon DeffuantModel::DeffuantModel( NetworkT & network, std::mt19937 & gen ) : Model(), network( network ), gen( gen ) { + for( size_t i = 0; i < network.agents.size(); i++ ) + { + network.agents[i].data.opinion = double( i ) / double( network.agents.size() ); + } } void DeffuantModel::iteration() @@ -34,6 +38,7 @@ void DeffuantModel::iteration() // Update rule auto opinion_diff = agent1.data.opinion - agent2.data.opinion; + // Only if less than homophily_threshold if( std::abs( opinion_diff ) < homophily_threshold ) { diff --git a/test/res/deffuant_2agents.toml b/test/res/deffuant_2agents.toml new file mode 100644 index 0000000..20bc605 --- /dev/null +++ b/test/res/deffuant_2agents.toml @@ -0,0 +1,21 @@ +[simulation] +model = "Deffuant" +# rng_seed = 120 # Leaving this empty will pick a random seed + +[io] +# n_output_network = 20 # Write the network every 20 iterations +# n_output_agents = 1 # Write the opinions of agents after every iteration +# print_progress = false # Print the iteration time ; if not set, then does not prints +# output_initial = true # Print the initial opinions and network file from step 0. If not set, this is true by default. +# start_output = 1 # Start writing out opinions and/or network files from this iteration. If not set, this is 1. + +[model] +max_iterations = 10 # If not set, max iterations is infinite + +[Deffuant] +homophily_threshold = 0.2 # d in the paper; agents interact if difference in opinion is less than this value +mu = 0.5 # convergence parameter; similar to social interaction strength K (0,0.5] + +[network] +number_of_agents = 2 +connections_per_agent = 0 diff --git a/test/test_deffuant.cpp b/test/test_deffuant.cpp new file mode 100644 index 0000000..5e37762 --- /dev/null +++ b/test/test_deffuant.cpp @@ -0,0 +1,62 @@ +#include "catch2/matchers/catch_matchers.hpp" +#include "models/DeffuantModel.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +TEST_CASE( "Test the basic deffuant model for two agents", "[deffuantTwoAgents]" ) +{ + using namespace Seldon; + using namespace Catch::Matchers; + using AgentT = DeffuantModel::AgentT; + + auto proj_root_path = fs::current_path(); + auto input_file = proj_root_path / fs::path( "test/res/deffuant_2agents.toml" ); + + auto options = Config::parse_config_file( input_file.string() ); + + auto simulation = Simulation( options, std::nullopt, std::nullopt ); + + // We need an output path for Simulation, but we won't write anything out there + fs::path output_dir_path = proj_root_path / fs::path( "test/output_deffuant" ); + + auto model_settings = std::get( options.model_settings ); + auto mu = model_settings.mu; + auto homophily_threshold = model_settings.homophily_threshold; + + // references to agent opinion + auto & agent1_opinion = simulation.network.agents[0].data.opinion; + auto & agent2_opinion = simulation.network.agents[1].data.opinion; + + // agents are too far apart, we dont expect any change with the iterations + double agent1_init = homophily_threshold * 1.1; + double agent2_init = 0; + + agent1_opinion = agent1_init; + agent2_opinion = agent2_init; + + simulation.run( output_dir_path ); + + REQUIRE_THAT( agent1_opinion, WithinRel( agent1_init ) ); + REQUIRE_THAT( agent2_opinion, WithinRel( agent2_init ) ); + + // agents are close enough, they should converge + agent1_init = homophily_threshold * 0.9; + agent2_init = 0; + + agent1_opinion = agent1_init; + agent2_opinion = agent2_init; + + simulation.run( output_dir_path ); + + auto n_iterations = model_settings.max_iterations.value(); + double expected_diff = std::pow( 1.0 - 2.0 * mu, 2 * n_iterations ) * ( agent1_init - agent2_init ); + + REQUIRE_THAT( agent1_opinion - agent2_opinion, WithinRel( expected_diff ) ); +} \ No newline at end of file From 7c4991ab8432dacfe1cf36ff37b8df75e54df282 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Wed, 20 Mar 2024 13:37:32 +0000 Subject: [PATCH 07/24] Network: Implement `remove_double_counting` This function sorts the adjacency list by neighbour idx and removes doubly counted edges by summing up their weight. Co-authored-by: Amrita Goswami --- include/network.hpp | 60 +++++++++++++++++++++++++++++++++++++++++++ test/test_network.cpp | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/include/network.hpp b/include/network.hpp index 997ad70..4e2ccff 100644 --- a/include/network.hpp +++ b/include/network.hpp @@ -228,6 +228,66 @@ class Network } } + /* + Sorts the neighbours by index and removes doubly counted edges by summing the weights + */ + void remove_double_counting() + { + std::vector sorting_indices{}; + + for( size_t idx_agent = 0; idx_agent < n_agents(); idx_agent++ ) + { + + auto & neighbours = neighbour_list[idx_agent]; + auto & weights = weight_list[idx_agent]; + + std::vector weights_copy{}; + std::vector neighbours_copy{}; + + const auto n_neighbours = neighbours.size(); + + // First we will the sorting_indices array + sorting_indices.resize( n_neighbours ); + std::iota( sorting_indices.begin(), sorting_indices.end(), 0 ); + + // Then, we figure out how to sort the neighbour indices list + std::sort( sorting_indices.begin(), sorting_indices.end(), [&]( auto i1, auto i2 ) { + return neighbours[i1] < neighbours[i2]; + } ); + + std::optional last_neighbour_index = std::nullopt; + for( size_t i = 0; i < n_neighbours; i++ ) + { + const auto sort_idx = sorting_indices[i]; + const auto current_neigbhour_idx = neighbours[sort_idx]; + const auto current_weight = weights[sort_idx]; + + if( last_neighbour_index != current_neigbhour_idx ) + { + weights_copy.push_back( current_weight ); + neighbours_copy.push_back( current_neigbhour_idx ); + last_neighbour_index = current_neigbhour_idx; + } + else + { + weights_copy.back() += current_weight; + } + } + + weight_list[idx_agent] = weights_copy; + neighbour_list[idx_agent] = neighbours_copy; + } + } + + /* + Clears the network + */ + void clear() + { + neighbour_list.clear(); + weight_list.clear(); + } + private: std::vector> neighbour_list; // Neighbour list for the connections std::vector> weight_list; // List for the interaction weights of each connection diff --git a/test/test_network.cpp b/test/test_network.cpp index 48d83da..37d42df 100644 --- a/test/test_network.cpp +++ b/test/test_network.cpp @@ -120,4 +120,55 @@ TEST_CASE( "Testing the network class" ) 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 = Seldon::Network( + std::move( neighbour_list ), std::move( weight_list ), Seldon::Network::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 ); + fmt::print( "i_agent {}\n", 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 From 51cc25f75475c2eb74a74db12c6d610991db1e9a Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Wed, 20 Mar 2024 13:56:15 +0000 Subject: [PATCH 08/24] Network: Constructor that jus takes n_agent Also fixed the clear() function and removed some prints in test_network --- include/network.hpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/include/network.hpp b/include/network.hpp index 4e2ccff..9f00c19 100644 --- a/include/network.hpp +++ b/include/network.hpp @@ -44,6 +44,13 @@ class Network Network() = default; + Network( size_t n_agents ) + : agents( std::vector( n_agents ) ), + neighbour_list( std::vector>( n_agents, std::vector{} ) ), + weight_list( std::vector>( n_agents, std::vector{} ) ) + { + } + Network( std::vector> && neighbour_list, std::vector> && weight_list, EdgeDirection direction ) @@ -59,7 +66,7 @@ class Network */ [[nodiscard]] std::size_t n_agents() const { - return neighbour_list.size(); + return agents.size(); } /* @@ -284,14 +291,17 @@ class Network */ void clear() { - neighbour_list.clear(); - weight_list.clear(); + for( auto & w : weight_list ) + w.clear(); + + for( auto & n : neighbour_list ) + n.clear(); } private: - std::vector> neighbour_list; // Neighbour list for the connections - std::vector> weight_list; // List for the interaction weights of each connection - EdgeDirection _direction; + std::vector> neighbour_list{}; // Neighbour list for the connections + std::vector> weight_list{}; // List for the interaction weights of each connection + EdgeDirection _direction{}; }; } // namespace Seldon \ No newline at end of file From 4f5489f6384e4bd0fb5c82e10e5c82e1572b0253 Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Wed, 20 Mar 2024 15:46:50 +0000 Subject: [PATCH 09/24] Network: Generates square lattice neighbours Generates a neighbour list such that agents are on a square lattice, and each agent has 4 neighbours. PBCs are implemented. TODO: perhaps move from size_t to int? Co-authored-by: Moritz Sallermann --- include/network.hpp | 6 ++-- include/network_generation.hpp | 60 ++++++++++++++++++++++++++++++++++ test/test_network.cpp | 25 ++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/include/network.hpp b/include/network.hpp index 9f00c19..579ba22 100644 --- a/include/network.hpp +++ b/include/network.hpp @@ -258,9 +258,9 @@ class Network std::iota( sorting_indices.begin(), sorting_indices.end(), 0 ); // Then, we figure out how to sort the neighbour indices list - std::sort( sorting_indices.begin(), sorting_indices.end(), [&]( auto i1, auto i2 ) { - return neighbours[i1] < neighbours[i2]; - } ); + std::sort( + sorting_indices.begin(), sorting_indices.end(), + [&]( auto i1, auto i2 ) { return neighbours[i1] < neighbours[i2]; } ); std::optional last_neighbour_index = std::nullopt; for( size_t i = 0; i < n_neighbours; i++ ) diff --git a/include/network_generation.hpp b/include/network_generation.hpp index 9025785..0c3d994 100644 --- a/include/network_generation.hpp +++ b/include/network_generation.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Seldon::NetworkGeneration { @@ -72,6 +73,7 @@ generate_n_connections( size_t n_agents, size_t n_connections, bool self_interac return NetworkT( std::move( neighbour_list ), std::move( weight_list ), NetworkT::EdgeDirection::Incoming ); } +// @TODO generate_fully_connected does not need to be overloaded..perhaps a std::optional instead to reduce code duplication? template Network generate_fully_connected( size_t n_agents, typename Network::WeightT weight = 0.0 ) { @@ -234,4 +236,62 @@ Network generate_from_file( const std::string & file ) return NetworkT( std::move( neighbour_list ), std::move( weight_list ), NetworkT::EdgeDirection::Incoming ); } + +/* Constructs a new network on a square lattice of edge length n_edge (with PBCs) + */ +template +Network generate_square_lattice( size_t n_edge, typename Network::WeightT weight = 0.0 ) +{ + using NetworkT = Network; + using WeightT = typename NetworkT::WeightT; + auto n_agents = n_edge * n_edge; + + // Create an empty Network + auto network = NetworkT( 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 Seldon::NetworkGeneration \ No newline at end of file diff --git a/test/test_network.cpp b/test/test_network.cpp index 37d42df..57239e7 100644 --- a/test/test_network.cpp +++ b/test/test_network.cpp @@ -171,4 +171,29 @@ TEST_CASE( "Testing the network class" ) REQUIRE_THAT( weights, Catch::Matchers::RangeEquals( weights_no_double_counting[i_agent] ) ); } } + + SECTION( "Test the generation of a square lattice neighbour list for three agents" ) + { + // clang-format off + std::vector> desired_neighbour_list = { + { 2, 1, 3, 6 }, + { 2, 0, 4, 7 }, + { 0, 1, 5, 8 }, + { 4, 5, 6, 0 }, + { 5, 3, 1, 7 }, + { 3, 4, 2, 8 }, + { 7, 8, 3, 0 }, + { 8, 6, 4, 1 }, + { 7, 6, 5, 2 }, + }; + // clang-format on + + auto network = Seldon::NetworkGeneration::generate_square_lattice( 3 ); + + for( size_t i_agent = 0; i_agent < network.n_agents(); i_agent++ ) + { + auto neighbours = network.get_neighbours( i_agent ); + REQUIRE_THAT( neighbours, Catch::Matchers::UnorderedRangeEquals( desired_neighbour_list[i_agent] ) ); + } + } } \ No newline at end of file From 82e88d154e35a60da7ba623accdb2d0bf8e6e36a Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Wed, 20 Mar 2024 15:56:19 +0000 Subject: [PATCH 10/24] Format: Pin clang-format version in environment To reduce unnecessary commits with minor formatting changes, we have pinned clang-format to 18.1.1. Different versions of clang-format apparently produce different outputs. Co-authored-by: Moritz Sallermann --- environment.yml | 2 +- include/connectivity.hpp | 2 +- test/test_network.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index 8ac5e71..6ebf1ee 100644 --- a/environment.yml +++ b/environment.yml @@ -16,6 +16,6 @@ dependencies: - cmake - ninja - cpp-argparse - - clang-format + - clang-format=18.1.1 - tomlplusplus - catch2 \ No newline at end of file diff --git a/include/connectivity.hpp b/include/connectivity.hpp index 1af9c8d..a9d0159 100644 --- a/include/connectivity.hpp +++ b/include/connectivity.hpp @@ -75,7 +75,7 @@ class TarjanConnectivityAlgo { lowest[v] = std::min( lowest[v], num[u] ); } // u not processed - } // u has been visited + } // u has been visited } // Now v has been processed diff --git a/test/test_network.cpp b/test/test_network.cpp index 57239e7..f1d2bfa 100644 --- a/test/test_network.cpp +++ b/test/test_network.cpp @@ -112,7 +112,7 @@ TEST_CASE( "Testing the network class" ) auto weight = buffer_w[i_neighbour]; std::tuple edge{ neighbour, i_agent, weight - }; // Note that i_agent and neighbour are flipped compared to before + }; // 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 } From d5baabbeda7775f76d89032a1961c92434e5d770 Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Wed, 20 Mar 2024 16:17:59 +0000 Subject: [PATCH 11/24] Deffuant: Model with square lattice neighbours Implemented the Deffuant model with a square lattice neighbour list. TODO: Actually parse these options. Co-authored-by: Moritz Sallermann --- include/models/DeffuantModel.hpp | 8 +++++-- src/models/DeffuantModel.cpp | 38 ++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index 2cba82d..3a472db 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -19,8 +19,9 @@ class DeffuantModel : public Model using AgentT = SimpleAgent; using NetworkT = Network; - double homophily_threshold = 0.2; // d in paper - double mu = 0.5; // convergence parameter + double homophily_threshold = 0.2; // d in paper + double mu = 0.5; // convergence parameter + bool use_network = false; // for the basic Deffuant model DeffuantModel( NetworkT & network, std::mt19937 & gen ); @@ -30,6 +31,9 @@ class DeffuantModel : public Model private: NetworkT & network; std::mt19937 & gen; // reference to simulation Mersenne-Twister engine + + // Select interacting agents + std::vector select_interacting_agent_pair(); }; } // namespace Seldon \ No newline at end of file diff --git a/src/models/DeffuantModel.cpp b/src/models/DeffuantModel.cpp index c4c6308..a717434 100644 --- a/src/models/DeffuantModel.cpp +++ b/src/models/DeffuantModel.cpp @@ -18,6 +18,39 @@ DeffuantModel::DeffuantModel( NetworkT & network, std::mt19937 & gen ) } } +std::vector DeffuantModel::select_interacting_agent_pair() +{ + + auto interacting_agents = std::vector(); + + // If the basic model is being used, then search from all possible agents + if( !use_network ) + { + + // Pick any two agents to interact, randomly, without repetition + draw_unique_k_from_n( std::nullopt, 2, network.n_agents(), interacting_agents, gen ); + + return interacting_agents; + } + else + { + // First select an agent randomly + auto dist = std::uniform_int_distribution( 0, network.n_agents() - 1 ); + auto agent1_idx = dist( gen ); + interacting_agents.push_back( agent1_idx ); + + // Choose a neighbour randomly from the neighbour list of agent1_idx + auto neighbours = network.get_neighbours( agent1_idx ); + auto n_neighbours = neighbours.size(); + auto dist_n = std::uniform_int_distribution( 0, n_neighbours - 1 ); + auto index_in_neigh = dist_n( gen ); // Index inside neighbours list + auto agent2_idx = neighbours[index_in_neigh]; + interacting_agents.push_back( agent2_idx ); + + return interacting_agents; + } +} + void DeffuantModel::iteration() { Model::iteration(); // Update n_iterations @@ -28,10 +61,7 @@ void DeffuantModel::iteration() for( size_t i = 0; i < network.n_agents(); i++ ) { - auto interacting_agents = std::vector(); - - // Pick any two agents to interact, randomly, without repetition - draw_unique_k_from_n( std::nullopt, 2, network.n_agents(), interacting_agents, gen ); + auto interacting_agents = select_interacting_agent_pair(); auto & agent1 = network.agents[interacting_agents[0]]; auto & agent2 = network.agents[interacting_agents[1]]; From 23ead10431750ebbc96cfbeaa27042e95a8a6d03 Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Wed, 20 Mar 2024 16:43:53 +0000 Subject: [PATCH 12/24] Deffuant: neighbours in square lattice We now support an implementation of the Deffuant model on a square lattice. TODO: unit test! --- examples/Deffuant/conf.toml | 1 + include/config_parser.hpp | 5 +++-- include/models/DeffuantModel.hpp | 2 +- include/simulation.hpp | 2 +- src/config_parser.cpp | 2 ++ src/models/DeffuantModel.cpp | 18 ++++++++++++++++-- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/examples/Deffuant/conf.toml b/examples/Deffuant/conf.toml index d8ab8a4..4e9bbf3 100644 --- a/examples/Deffuant/conf.toml +++ b/examples/Deffuant/conf.toml @@ -15,6 +15,7 @@ max_iterations = 1000 # If not set, max iterations is infinite [Deffuant] homophily_threshold = 0.2 # d in the paper; agents interact if difference in opinion is less than this value mu = 0.5 # convergence parameter; similar to social interaction strength K (0,0.5] +use_network = false # If true, will use a square lattice Will throw if sqrt(n_agents) is not an integer [network] number_of_agents = 1000 diff --git a/include/config_parser.hpp b/include/config_parser.hpp index 04e28dc..5efe3ca 100644 --- a/include/config_parser.hpp +++ b/include/config_parser.hpp @@ -48,8 +48,9 @@ struct DeffuantSettings { std::optional max_iterations = std::nullopt; double homophily_threshold - = 0.2; // d in the paper; agents interact if difference in opinion is less than this value - double mu = 0.5; // convergence parameter; similar to social interaction strength K (0,0.5] + = 0.2; // d in the paper; agents interact if difference in opinion is less than this value + double mu = 0.5; // convergence parameter; similar to social interaction strength K (0,0.5] + bool use_network = false; }; struct ActivityDrivenSettings diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index 3a472db..4e88975 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -23,7 +23,7 @@ class DeffuantModel : public Model double mu = 0.5; // convergence parameter bool use_network = false; // for the basic Deffuant model - DeffuantModel( NetworkT & network, std::mt19937 & gen ); + DeffuantModel( NetworkT & network, std::mt19937 & gen, bool use_network ); void iteration() override; // bool finished() override; diff --git a/include/simulation.hpp b/include/simulation.hpp index bdf8354..2a15609 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -108,7 +108,7 @@ class Simulation : public SimulationInterface // Deffuant model specific parameters model = [&]() { - auto model = std::make_unique( network, gen ); + auto model = std::make_unique( network, gen, deffuant_settings.use_network ); model->max_iterations = deffuant_settings.max_iterations; model->homophily_threshold = deffuant_settings.homophily_threshold; model->mu = deffuant_settings.mu; diff --git a/src/config_parser.cpp b/src/config_parser.cpp index aa4aafa..b872eb8 100644 --- a/src/config_parser.cpp +++ b/src/config_parser.cpp @@ -77,6 +77,7 @@ SimulationOptions parse_config_file( std::string_view config_file_path ) model_settings.max_iterations = tbl["model"]["max_iterations"].value(); set_if_specified( model_settings.homophily_threshold, tbl[options.model_string]["homophily_threshold"] ); set_if_specified( model_settings.mu, tbl[options.model_string]["mu"] ); + set_if_specified( model_settings.use_network, tbl[options.model_string]["use_network"] ); options.model_settings = model_settings; } else if( options.model == Model::ActivityDrivenModel ) @@ -270,6 +271,7 @@ void print_settings( const SimulationOptions & options ) fmt::print( " max_iterations {}\n", model_settings.max_iterations ); fmt::print( " homophily_threshold {}\n", model_settings.homophily_threshold ); fmt::print( " mu {}\n", model_settings.mu ); + fmt::print( " use_network {}\n", model_settings.use_network ); } fmt::print( "[Network]\n" ); diff --git a/src/models/DeffuantModel.cpp b/src/models/DeffuantModel.cpp index a717434..bc66cc6 100644 --- a/src/models/DeffuantModel.cpp +++ b/src/models/DeffuantModel.cpp @@ -1,17 +1,31 @@ #include "models/DeffuantModel.hpp" #include "network.hpp" +#include "network_generation.hpp" #include "util/math.hpp" +#include #include #include #include +#include #include namespace Seldon { -DeffuantModel::DeffuantModel( NetworkT & network, std::mt19937 & gen ) - : Model(), network( network ), gen( gen ) +DeffuantModel::DeffuantModel( NetworkT & network, std::mt19937 & gen, bool use_network ) + : Model(), use_network( use_network ), network( network ), gen( gen ) { + // Generate the network as a square lattice if use_network is true + if( use_network ) + { + size_t n_edge = std::sqrt( network.n_agents() ); + if( n_edge * n_edge != network.n_agents() ) + { + throw std::runtime_error( "Number of agents is not a square number." ); + } + network = NetworkGeneration::generate_square_lattice( n_edge ); + } + for( size_t i = 0; i < network.agents.size(); i++ ) { network.agents[i].data.opinion = double( i ) / double( network.agents.size() ); From 58b510793a4c630db5c35b17360c134c08b0799c Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Wed, 20 Mar 2024 17:22:18 +0000 Subject: [PATCH 13/24] Deffuant: unit test for lattice model We use a 16x16 lattice and observe the formation of two stable clusters. Co-authored-by: Amrita Goswami --- test/res/deffuant_16x16_agents.toml | 22 ++++++++++++ test/test_deffuant.cpp | 53 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 test/res/deffuant_16x16_agents.toml diff --git a/test/res/deffuant_16x16_agents.toml b/test/res/deffuant_16x16_agents.toml new file mode 100644 index 0000000..fcb0d8e --- /dev/null +++ b/test/res/deffuant_16x16_agents.toml @@ -0,0 +1,22 @@ +[simulation] +model = "Deffuant" +# rng_seed = 120 # Leaving this empty will pick a random seed + +[io] +# n_output_network = 20 # Write the network every 20 iterations +# n_output_agents = 1 # Write the opinions of agents after every iteration +# print_progress = false # Print the iteration time ; if not set, then does not prints +# output_initial = true # Print the initial opinions and network file from step 0. If not set, this is true by default. +# start_output = 1 # Start writing out opinions and/or network files from this iteration. If not set, this is 1. + +[model] +max_iterations = 10000 # If not set, max iterations is infinite + +[Deffuant] +homophily_threshold = 1.0 # d in the paper; agents interact if difference in opinion is less than this value +mu = 0.5 # convergence parameter; similar to social interaction strength K (0,0.5] +use_network = true + +[network] +number_of_agents = 256 +connections_per_agent = 0 diff --git a/test/test_deffuant.cpp b/test/test_deffuant.cpp index 5e37762..0504467 100644 --- a/test/test_deffuant.cpp +++ b/test/test_deffuant.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -59,4 +60,56 @@ TEST_CASE( "Test the basic deffuant model for two agents", "[deffuantTwoAgents]" double expected_diff = std::pow( 1.0 - 2.0 * mu, 2 * n_iterations ) * ( agent1_init - agent2_init ); REQUIRE_THAT( agent1_opinion - agent2_opinion, WithinRel( expected_diff ) ); +} + +TEST_CASE( "Test the lattice deffuant model for 16x16 agents", "[deffuantLattice16x16]" ) +{ + using namespace Seldon; + using namespace Catch::Matchers; + using AgentT = DeffuantModel::AgentT; + + auto proj_root_path = fs::current_path(); + auto input_file = proj_root_path / fs::path( "test/res/deffuant_16x16_agents.toml" ); + + auto options = Config::parse_config_file( input_file.string() ); + + auto simulation = Simulation( options, std::nullopt, std::nullopt ); + + // We need an output path for Simulation, but we won't write anything out there + fs::path output_dir_path = proj_root_path / fs::path( "test/output_deffuant" ); + + auto model_settings = std::get( options.model_settings ); + auto homophily_threshold = model_settings.homophily_threshold; + + auto n_agents = simulation.network.n_agents(); + + // first half with low opinions + size_t n_agents_half = n_agents / 2; + double avg_opinion = 0; + for( size_t idx_agent = 0; idx_agent < n_agents_half; idx_agent++ ) + { + auto op = -homophily_threshold - 0.5 * idx_agent / n_agents * homophily_threshold; + avg_opinion += op / double( n_agents_half ); + simulation.network.agents[idx_agent].data.opinion = op; + } + + // second half with low opinions + for( size_t idx_agent = n_agents_half; idx_agent < n_agents; idx_agent++ ) + { + auto op = homophily_threshold + 0.5 * ( idx_agent - n_agents_half ) / n_agents * homophily_threshold; + simulation.network.agents[idx_agent].data.opinion = op; + } + + // The two halves are so far apart that they should not interact an therefore form two stable clusters. + simulation.run( output_dir_path ); + + for( size_t idx_agent = 0; idx_agent < n_agents_half; idx_agent++ ) + { + REQUIRE_THAT( simulation.network.agents[idx_agent].data.opinion, WithinRel( avg_opinion ) ); + } + + for( size_t idx_agent = n_agents_half; idx_agent < n_agents; idx_agent++ ) + { + REQUIRE_THAT( simulation.network.agents[idx_agent].data.opinion, WithinRel( -avg_opinion ) ); + } } \ No newline at end of file From 5379b35898a7b259a07ca756779bbf071e28cc4f Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Wed, 20 Mar 2024 18:25:50 +0000 Subject: [PATCH 14/24] util/misc.hpp: `parse_comma_separated_list` Utility function that parses a string that is in teh format of a comma separated list and executes a callback function for each entry in the list. Also added a unit test for it. --- include/network_generation.hpp | 4 ++-- include/util/misc.hpp | 28 ++++++++++++++++++++++++++++ meson.build | 1 + test/test_util.cpp | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 test/test_util.cpp diff --git a/include/network_generation.hpp b/include/network_generation.hpp index 0c3d994..e71b50f 100644 --- a/include/network_generation.hpp +++ b/include/network_generation.hpp @@ -197,6 +197,7 @@ Network generate_from_file( const std::string & file ) neighbour_list.emplace_back( 0 ); weight_list.emplace_back( 0 ); + //@TODO: refactor with util/parse_comma_separated_list size_t start_of_column = 0; bool finished_row = false; size_t idx_column = 0; @@ -237,8 +238,7 @@ Network generate_from_file( const std::string & file ) return NetworkT( std::move( neighbour_list ), std::move( weight_list ), NetworkT::EdgeDirection::Incoming ); } -/* Constructs a new network on a square lattice of edge length n_edge (with PBCs) - */ +/* Constructs a new network on a square lattice of edge length n_edge (with PBCs)*/ template Network generate_square_lattice( size_t n_edge, typename Network::WeightT weight = 0.0 ) { diff --git a/include/util/misc.hpp b/include/util/misc.hpp index 0baa0da..9c48cad 100644 --- a/include/util/misc.hpp +++ b/include/util/misc.hpp @@ -31,4 +31,32 @@ inline std::string get_file_contents( const std::string & filename ) throw( std::runtime_error( "Cannot read file." ) ); } +/* +Executes `callback` for each substring in a comma separated list. +If the input is "a_d, b, 1", it would call the callback function like +so: + callback(0, "a d") + callback(1, " b") + callback(2, " 1") +*/ +template +void parse_comma_separated_list( const std::string & str, CallbackT & callback ) +{ + int idx_entry = 0; + auto pos_cur = -1; // Have to initialize from -1, because we start looking one past pos_cur + while( true ) + { + auto pos_next = str.find( ',', pos_cur + 1 ); + auto substr = str.substr( pos_cur + 1, pos_next ); + callback( idx_entry, substr ); + + pos_cur = pos_next; + idx_entry++; + if( pos_next == std::string::npos ) + { + break; + } + } +} + } // namespace Seldon \ No newline at end of file diff --git a/meson.build b/meson.build index c28b7c3..d7a9bec 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,7 @@ tests = [ ['Test_Network_Generation', 'test/test_network_generation.cpp'], ['Test_Sampling', 'test/test_sampling.cpp'], ['Test_IO', 'test/test_io.cpp'], + ['Test_Util', 'test/test_util.cpp'], ] Catch2 = dependency('Catch2', method : 'cmake', modules : ['Catch2::Catch2WithMain', 'Catch2::Catch2']) diff --git a/test/test_util.cpp b/test/test_util.cpp new file mode 100644 index 0000000..bf72fec --- /dev/null +++ b/test/test_util.cpp @@ -0,0 +1,34 @@ +#include "catch2/matchers/catch_matchers.hpp" +#include "util/misc.hpp" + +#include +#include +#include + +TEST_CASE( "Test parse_comma_separated_list", "[util_parse_list]" ) +{ + const std::string str = "12, aa, -2.0, 10, 13.0 \n"; + + std::vector dbl_vec{}; + std::vector int_vec{}; + + std::vector dbl_vec_expected = { -2.0, 13.0 }; + std::vector int_vec_expected = { 12, 10 }; + + auto callback = [&]( int idx_list, std::string & substr ) + { + if( idx_list == 0 || idx_list == 3 ) + { + int_vec.push_back( std::stoi( substr ) ); + } + else if( idx_list == 2 || idx_list == 4 ) + { + dbl_vec.push_back( std::stod( substr ) ); + } + }; + + Seldon::parse_comma_separated_list( str, callback ); + + REQUIRE_THAT( dbl_vec, Catch::Matchers::RangeEquals( dbl_vec_expected ) ); + REQUIRE_THAT( int_vec, Catch::Matchers::RangeEquals( int_vec_expected ) ); +} \ No newline at end of file From 9739b4f30d4fb84353183dffe5c42bcae6aaa794 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Wed, 20 Mar 2024 19:12:50 +0000 Subject: [PATCH 15/24] Deffuant: Vector model (WIP) Started to implement the Deffuant model with vectors. To minimize code duplication we refactored the `ModelDeffuant` class in the following way: - We introduced `DeffuantModelAbstract` which is a template class and implement the behaviour that is common to both the basic Deffuant model and the vector deffuant model - The basic deffuant model is now a specialization of `DeffuantModelAbstract`, where all the behaviour specific to the basic Deffuant model is implemented via template specialization and explicit instantiation in a separate .cpp source code file - The same was done with the vector Deffuant model, but it is not fully implemented yet. To simplify the notation they are availabe to the rest of the code via: `using DeffuantModel = DeffuantModelAbstract;` `using DeffuantModelVector = DeffuantModelAbstract;` Co-authored-by: Amrita Goswami --- include/agents/discrete_vector_agent.hpp | 59 ++++++++++++++++ include/models/DeffuantModel.hpp | 88 ++++++++++++++++++++++-- meson.build | 1 + src/models/DeffuantModel.cpp | 83 +++------------------- src/models/DeffuantModelVector.cpp | 34 +++++++++ 5 files changed, 186 insertions(+), 79 deletions(-) create mode 100644 include/agents/discrete_vector_agent.hpp create mode 100644 src/models/DeffuantModelVector.cpp diff --git a/include/agents/discrete_vector_agent.hpp b/include/agents/discrete_vector_agent.hpp new file mode 100644 index 0000000..20f63be --- /dev/null +++ b/include/agents/discrete_vector_agent.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "agent.hpp" +#include "agent_io.hpp" +#include "util/misc.hpp" +#include +#include +#include + +namespace Seldon +{ + +struct DiscreteVectorAgentData +{ + std::vector opinion{}; +}; + +using DiscreteVectorAgent = Agent; + +template<> +inline std::string agent_to_string( const DiscreteVectorAgent & agent ) +{ + if( agent.data.opinion.empty() ) + return ""; + + auto res = fmt::format( "{}", agent.data.opinion[0] ); + for( size_t i = 1; i < agent.data.opinion.size(); i++ ) + { + res += fmt::format( ", {}", agent.data.opinion[i] ); + } + return res; +} + +template<> +inline std::string opinion_to_string( const DiscreteVectorAgent & agent ) +{ + return agent_to_string( agent ); +} + +template<> +inline DiscreteVectorAgent agent_from_string( const std::string & str ) +{ + DiscreteVectorAgent res{}; + + auto callback = [&]( int idx_list [[maybe_unused]], const auto & substring ) { + res.data.opinion.push_back( std::stoi( substring ) ); + }; + + parse_comma_separated_list( str, callback ); + return res; +}; + +// template<> +// inline std::vector agent_to_string_column_names() +// { +// return { "opinion", "activity", "reluctance" }; +// } + +} // namespace Seldon \ No newline at end of file diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index 4e88975..3a34a82 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -1,39 +1,113 @@ #pragma once +#include "agents/discrete_vector_agent.hpp" #include "agents/simple_agent.hpp" #include "model.hpp" #include "network.hpp" +#include "util/math.hpp" #include #include + +#include "network_generation.hpp" #include #include #include + #include namespace Seldon { -class DeffuantModel : public Model +template +class DeffuantModelAbstract : public Model { public: - using AgentT = SimpleAgent; + using AgentT = AgentT_; using NetworkT = Network; double homophily_threshold = 0.2; // d in paper double mu = 0.5; // convergence parameter bool use_network = false; // for the basic Deffuant model - DeffuantModel( NetworkT & network, std::mt19937 & gen, bool use_network ); + DeffuantModelAbstract( NetworkT & network, std::mt19937 & gen, bool use_network ) + : Model(), use_network( use_network ), network( network ), gen( gen ) + { + // Generate the network as a square lattice if use_network is true + if( use_network ) + { + size_t n_edge = std::sqrt( network.n_agents() ); + if( n_edge * n_edge != network.n_agents() ) + { + throw std::runtime_error( "Number of agents is not a square number." ); + } + network = NetworkGeneration::generate_square_lattice( n_edge ); + } + initialize_agents(); + } + + std::vector select_interacting_agent_pair() + { + auto interacting_agents = std::vector(); + // If the basic model is being used, then search from all possible agents + if( !use_network ) + { + + // Pick any two agents to interact, randomly, without repetition + draw_unique_k_from_n( std::nullopt, 2, network.n_agents(), interacting_agents, gen ); + + return interacting_agents; + } + else + { + // First select an agent randomly + auto dist = std::uniform_int_distribution( 0, network.n_agents() - 1 ); + auto agent1_idx = dist( gen ); + interacting_agents.push_back( agent1_idx ); + + // Choose a neighbour randomly from the neighbour list of agent1_idx + auto neighbours = network.get_neighbours( agent1_idx ); + auto n_neighbours = neighbours.size(); + auto dist_n = std::uniform_int_distribution( 0, n_neighbours - 1 ); + auto index_in_neigh = dist_n( gen ); // Index inside neighbours list + auto agent2_idx = neighbours[index_in_neigh]; + interacting_agents.push_back( agent2_idx ); + + return interacting_agents; + } + } + + void iteration() override + { + Model::iteration(); // Update n_iterations - void iteration() override; + // Although the model defines each iteration as choosing *one* + // pair of agents, we will define each iteration as sampling + // n_agents pairs (similar to the time unit in evolution plots in the paper) + for( size_t i = 0; i < network.n_agents(); i++ ) + { + + auto interacting_agents = select_interacting_agent_pair(); + + auto & agent1 = network.agents[interacting_agents[0]]; + auto & agent2 = network.agents[interacting_agents[1]]; + + update_rule( agent1, agent2 ); + } + } + + // template + void update_rule( AgentT & agent1, AgentT & agent2 ); + void initialize_agents(); + + // void iteration() override; // bool finished() override; private: NetworkT & network; std::mt19937 & gen; // reference to simulation Mersenne-Twister engine - - // Select interacting agents - std::vector select_interacting_agent_pair(); }; +using DeffuantModel = DeffuantModelAbstract; +using DeffuantModelVector = DeffuantModelAbstract; + } // namespace Seldon \ No newline at end of file diff --git a/meson.build b/meson.build index d7a9bec..e7017c3 100644 --- a/meson.build +++ b/meson.build @@ -11,6 +11,7 @@ sources_seldon = [ 'src/models/DeGroot.cpp', 'src/models/ActivityDrivenModel.cpp', 'src/models/DeffuantModel.cpp', + 'src/models/DeffuantModelVector.cpp', 'src/util/tomlplusplus.cpp', ] diff --git a/src/models/DeffuantModel.cpp b/src/models/DeffuantModel.cpp index bc66cc6..976ca42 100644 --- a/src/models/DeffuantModel.cpp +++ b/src/models/DeffuantModel.cpp @@ -1,94 +1,33 @@ #include "models/DeffuantModel.hpp" -#include "network.hpp" -#include "network_generation.hpp" -#include "util/math.hpp" #include #include -#include -#include -#include #include namespace Seldon { -DeffuantModel::DeffuantModel( NetworkT & network, std::mt19937 & gen, bool use_network ) - : Model(), use_network( use_network ), network( network ), gen( gen ) +template<> +void DeffuantModelAbstract::initialize_agents() { - // Generate the network as a square lattice if use_network is true - if( use_network ) - { - size_t n_edge = std::sqrt( network.n_agents() ); - if( n_edge * n_edge != network.n_agents() ) - { - throw std::runtime_error( "Number of agents is not a square number." ); - } - network = NetworkGeneration::generate_square_lattice( n_edge ); - } - for( size_t i = 0; i < network.agents.size(); i++ ) { network.agents[i].data.opinion = double( i ) / double( network.agents.size() ); } } -std::vector DeffuantModel::select_interacting_agent_pair() +template<> +void DeffuantModelAbstract::update_rule( AgentT & agent1, AgentT & agent2 ) { - - auto interacting_agents = std::vector(); - - // If the basic model is being used, then search from all possible agents - if( !use_network ) + // Update rule + auto opinion_diff = agent1.data.opinion - agent2.data.opinion; + // Only if less than homophily_threshold + if( std::abs( opinion_diff ) < homophily_threshold ) { - - // Pick any two agents to interact, randomly, without repetition - draw_unique_k_from_n( std::nullopt, 2, network.n_agents(), interacting_agents, gen ); - - return interacting_agents; - } - else - { - // First select an agent randomly - auto dist = std::uniform_int_distribution( 0, network.n_agents() - 1 ); - auto agent1_idx = dist( gen ); - interacting_agents.push_back( agent1_idx ); - - // Choose a neighbour randomly from the neighbour list of agent1_idx - auto neighbours = network.get_neighbours( agent1_idx ); - auto n_neighbours = neighbours.size(); - auto dist_n = std::uniform_int_distribution( 0, n_neighbours - 1 ); - auto index_in_neigh = dist_n( gen ); // Index inside neighbours list - auto agent2_idx = neighbours[index_in_neigh]; - interacting_agents.push_back( agent2_idx ); - - return interacting_agents; + agent1.data.opinion -= mu * opinion_diff; + agent2.data.opinion += mu * opinion_diff; } } -void DeffuantModel::iteration() -{ - Model::iteration(); // Update n_iterations - - // Although the model defines each iteration as choosing *one* - // pair of agents, we will define each iteration as sampling - // n_agents pairs (similar to the time unit in evolution plots in the paper) - for( size_t i = 0; i < network.n_agents(); i++ ) - { - - auto interacting_agents = select_interacting_agent_pair(); +template class DeffuantModelAbstract; - auto & agent1 = network.agents[interacting_agents[0]]; - auto & agent2 = network.agents[interacting_agents[1]]; - - // Update rule - auto opinion_diff = agent1.data.opinion - agent2.data.opinion; - - // Only if less than homophily_threshold - if( std::abs( opinion_diff ) < homophily_threshold ) - { - agent1.data.opinion -= mu * opinion_diff; - agent2.data.opinion += mu * opinion_diff; - } - } -} } // namespace Seldon \ No newline at end of file diff --git a/src/models/DeffuantModelVector.cpp b/src/models/DeffuantModelVector.cpp new file mode 100644 index 0000000..8c98334 --- /dev/null +++ b/src/models/DeffuantModelVector.cpp @@ -0,0 +1,34 @@ +#include "agents/discrete_vector_agent.hpp" +#include "models/DeffuantModel.hpp" +#include +#include +#include + +namespace Seldon +{ + +template<> +void DeffuantModelAbstract::initialize_agents() +{ + for( size_t i = 0; i < network.agents.size(); i++ ) + { + // network.agents[i].data.opinion = double( i ) / double( network.agents.size() ); + } +} + +template<> +void DeffuantModelAbstract::update_rule( AgentT & agent1, AgentT & agent2 ) +{ + // Update rule + // auto opinion_diff = agent1.data.opinion - agent2.data.opinion; + // Only if less than homophily_threshold + // if( std::abs( opinion_diff ) < homophily_threshold ) + // { + // agent1.data.opinion -= mu * opinion_diff; + // agent2.data.opinion += mu * opinion_diff; + // } +} + +template class DeffuantModelAbstract; + +} // namespace Seldon \ No newline at end of file From c19efbc57c7336ecd40c412a26ce99eb3b4a5165 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Thu, 21 Mar 2024 19:20:54 +0000 Subject: [PATCH 16/24] Util: Hamming distance Implemented the Hamming distance, needed for the vector Deffuant model. Co-authored-by: Amrita Goswami --- include/util/math.hpp | 19 +++++++++++++++++++ include/util/misc.hpp | 2 +- test/test_util.cpp | 14 ++++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/include/util/math.hpp b/include/util/math.hpp index 53520fd..e6041fc 100644 --- a/include/util/math.hpp +++ b/include/util/math.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -174,4 +176,21 @@ class truncated_normal_distribution } }; +template +int hamming_distance( std::span v1, std::span v2 ) +{ + if( v1.size() != v2.size() ) + { + throw std::runtime_error( "v1 and v2 need to have the same size" ); + } + + int distance = 0; + for( size_t i = 0; i < v2.size(); i++ ) + { + if( v1[i] != v2[i] ) + distance++; + } + return distance; +} + } // namespace Seldon \ No newline at end of file diff --git a/include/util/misc.hpp b/include/util/misc.hpp index 9c48cad..d9bc5b6 100644 --- a/include/util/misc.hpp +++ b/include/util/misc.hpp @@ -33,7 +33,7 @@ inline std::string get_file_contents( const std::string & filename ) /* Executes `callback` for each substring in a comma separated list. -If the input is "a_d, b, 1", it would call the callback function like +If the input is "a_d, b, 1", it would call the callback function like so: callback(0, "a d") callback(1, " b") diff --git a/test/test_util.cpp b/test/test_util.cpp index bf72fec..a351fe2 100644 --- a/test/test_util.cpp +++ b/test/test_util.cpp @@ -1,4 +1,5 @@ #include "catch2/matchers/catch_matchers.hpp" +#include "util/math.hpp" #include "util/misc.hpp" #include @@ -15,8 +16,7 @@ TEST_CASE( "Test parse_comma_separated_list", "[util_parse_list]" ) std::vector dbl_vec_expected = { -2.0, 13.0 }; std::vector int_vec_expected = { 12, 10 }; - auto callback = [&]( int idx_list, std::string & substr ) - { + auto callback = [&]( int idx_list, std::string & substr ) { if( idx_list == 0 || idx_list == 3 ) { int_vec.push_back( std::stoi( substr ) ); @@ -31,4 +31,14 @@ TEST_CASE( "Test parse_comma_separated_list", "[util_parse_list]" ) REQUIRE_THAT( dbl_vec, Catch::Matchers::RangeEquals( dbl_vec_expected ) ); REQUIRE_THAT( int_vec, Catch::Matchers::RangeEquals( int_vec_expected ) ); +} + +TEST_CASE( "Test Hamming distance", "[util_hamming_dist]" ) +{ + std::vector v1 = { 1, 1, 1, 0, 1 }; + std::vector v2 = { 0, 1, 1, 0, 0 }; + + auto dist = Seldon::hamming_distance( std::span( v1 ), std::span( v2 ) ); + + REQUIRE( dist == 2 ); } \ No newline at end of file From 42a1837ecfa46fb74c2b6aaf07b91686e2363a00 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Thu, 21 Mar 2024 19:22:51 +0000 Subject: [PATCH 17/24] Deffuant: Implemented the update rule in the vector model Also hacked in the generation of the initial agents by hardcoding the dimensionality <--- TODO Co-authored-by: Amrita Goswami --- include/agents/discrete_vector_agent.hpp | 5 +-- src/models/DeffuantModelVector.cpp | 48 ++++++++++++++++++++---- test/test_util.cpp | 3 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/include/agents/discrete_vector_agent.hpp b/include/agents/discrete_vector_agent.hpp index 20f63be..01c72e0 100644 --- a/include/agents/discrete_vector_agent.hpp +++ b/include/agents/discrete_vector_agent.hpp @@ -42,9 +42,8 @@ inline DiscreteVectorAgent agent_from_string( const std::st { DiscreteVectorAgent res{}; - auto callback = [&]( int idx_list [[maybe_unused]], const auto & substring ) { - res.data.opinion.push_back( std::stoi( substring ) ); - }; + auto callback = [&]( int idx_list [[maybe_unused]], const auto & substring ) + { res.data.opinion.push_back( std::stoi( substring ) ); }; parse_comma_separated_list( str, callback ); return res; diff --git a/src/models/DeffuantModelVector.cpp b/src/models/DeffuantModelVector.cpp index 8c98334..3912632 100644 --- a/src/models/DeffuantModelVector.cpp +++ b/src/models/DeffuantModelVector.cpp @@ -1,7 +1,9 @@ +#include "agent.hpp" #include "agents/discrete_vector_agent.hpp" #include "models/DeffuantModel.hpp" -#include +#include "util/math.hpp" #include +#include #include namespace Seldon @@ -10,23 +12,53 @@ namespace Seldon template<> void DeffuantModelAbstract::initialize_agents() { + std::uniform_int_distribution dist( 0, 1 ); + int dim = 5; + for( size_t i = 0; i < network.agents.size(); i++ ) { - // network.agents[i].data.opinion = double( i ) / double( network.agents.size() ); + auto & opinion = network.agents[i].data.opinion; + opinion.resize( dim ); + for( auto & o : opinion ) + { + o = dist( gen ); + } } } template<> void DeffuantModelAbstract::update_rule( AgentT & agent1, AgentT & agent2 ) { + size_t dim = agent1.data.opinion.size(); + auto & opinion1 = agent1.data.opinion; + auto & opinion2 = agent2.data.opinion; + + auto distance = hamming_distance( std::span( opinion1 ), std::span( opinion2 ) ); + + std::uniform_int_distribution<> dist_pair( 0, 1 ); + std::uniform_real_distribution<> dist_convince( 0, 1 ); + // Update rule - // auto opinion_diff = agent1.data.opinion - agent2.data.opinion; // Only if less than homophily_threshold - // if( std::abs( opinion_diff ) < homophily_threshold ) - // { - // agent1.data.opinion -= mu * opinion_diff; - // agent2.data.opinion += mu * opinion_diff; - // } + if( distance < homophily_threshold ) + { + for( size_t idx_opinion = 0; idx_opinion < dim; idx_opinion++ ) + { + if( opinion1[idx_opinion] != opinion2[idx_opinion] ) + { + // randomly select one of the + auto idx_selected = dist_pair( gen ); + if( idx_selected == 0 && mu < dist_convince( gen ) ) + { + opinion1[idx_opinion] = opinion2[idx_opinion]; + } + else + { + opinion2[idx_opinion] = opinion1[idx_opinion]; + } + } + } + } } template class DeffuantModelAbstract; diff --git a/test/test_util.cpp b/test/test_util.cpp index a351fe2..c1ed1ac 100644 --- a/test/test_util.cpp +++ b/test/test_util.cpp @@ -16,7 +16,8 @@ TEST_CASE( "Test parse_comma_separated_list", "[util_parse_list]" ) std::vector dbl_vec_expected = { -2.0, 13.0 }; std::vector int_vec_expected = { 12, 10 }; - auto callback = [&]( int idx_list, std::string & substr ) { + auto callback = [&]( int idx_list, std::string & substr ) + { if( idx_list == 0 || idx_list == 3 ) { int_vec.push_back( std::stoi( substr ) ); From 1505bdcd365aad053bf93bc59a70b75347bbc3eb Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Thu, 21 Mar 2024 20:22:30 +0000 Subject: [PATCH 18/24] Similation: Simplified network generation The generation of the initial network is now simpler and doesnt need model specific branches anymore. The tradeoff was that the meanfield AD model must now create the fully connected network it requires itself. Co-authored-by: Amrita Goswami --- include/network.hpp | 7 ++++ include/simulation.hpp | 60 ++++++++++++++++-------------- src/models/ActivityDrivenModel.cpp | 8 ++++ 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/include/network.hpp b/include/network.hpp index 579ba22..8056b62 100644 --- a/include/network.hpp +++ b/include/network.hpp @@ -51,6 +51,13 @@ class Network { } + Network( std::vector agents ) + : agents( agents ), + neighbour_list( std::vector>( agents.size(), std::vector{} ) ), + weight_list( std::vector>( agents.size(), std::vector{} ) ) + { + } + Network( std::vector> && neighbour_list, std::vector> && weight_list, EdgeDirection direction ) diff --git a/include/simulation.hpp b/include/simulation.hpp index 2a15609..8d4b132 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -39,14 +39,9 @@ class Simulation : public SimulationInterface Config::OutputSettings output_settings; - Simulation( - const Config::SimulationOptions & options, const std::optional & cli_network_file, - const std::optional & cli_agent_file ) - : output_settings( options.output_settings ) + void + create_network( const Config::SimulationOptions & options, const std::optional & cli_network_file ) { - // Initialize the rng - gen = std::mt19937( options.rng_seed ); - // Construct the network std::optional file = cli_network_file; if( !file.has_value() ) // Check if toml file should be superceded by cli_network_file @@ -60,27 +55,12 @@ class Simulation : public SimulationInterface { int n_agents = options.network_settings.n_agents; auto n_connections = options.network_settings.n_connections; - - //@TODO figure this out - if( options.model == Config::Model::ActivityDrivenModel ) - { - auto model_settings = std::get( options.model_settings ); - if( model_settings.mean_weights ) - { - network = NetworkGeneration::generate_fully_connected( n_agents ); - } - else - { - network - = NetworkGeneration::generate_n_connections( n_agents, n_connections, true, gen ); - } - } - else - { - network = NetworkGeneration::generate_n_connections( n_agents, n_connections, true, gen ); - } + network = NetworkGeneration::generate_n_connections( n_agents, n_connections, true, gen ); } + } + void create_model( const Config::SimulationOptions & options, const std::optional & cli_agent_file ) + { if constexpr( std::is_same_v ) { if( options.model == Config::Model::DeGroot ) @@ -144,13 +124,23 @@ class Simulation : public SimulationInterface model->reluctance_mean = activitydriven_settings.reluctance_mean; model->reluctance_sigma = activitydriven_settings.reluctance_sigma; model->reluctance_eps = activitydriven_settings.reluctance_eps; - // bot + // Bot model->n_bots = activitydriven_settings.n_bots; model->bot_opinion = activitydriven_settings.bot_opinion; model->bot_m = activitydriven_settings.bot_m; model->bot_homophily = activitydriven_settings.bot_homophily; model->bot_activity = activitydriven_settings.bot_activity; model->get_agents_from_power_law(); + + // TODO: this is stupid and should be done in the constructor, but right now it cant since we set mean + // weights only later + if( model->mean_weights ) + { + auto agents_copy = network.agents; + network = NetworkGeneration::generate_fully_connected( network.n_agents() ); + network.agents = agents_copy; + } + return model; }(); @@ -161,6 +151,20 @@ class Simulation : public SimulationInterface } } + // void create_agents( const std::optional & cli_agent_file ) {} + + Simulation( + const Config::SimulationOptions & options, const std::optional & cli_network_file, + const std::optional & cli_agent_file ) + : output_settings( options.output_settings ) + { + // Initialize the rng + gen = std::mt19937( options.rng_seed ); + + create_network( options, cli_network_file ); + create_model( options, cli_agent_file ); + } + void run( const fs::path & output_dir_path ) override { auto n_output_agents = this->output_settings.n_output_agents; @@ -225,6 +229,6 @@ class Simulation : public SimulationInterface std::chrono::floor( total_time ) ); fmt::print( "=================================================================\n" ); } -}; +}; // namespace Seldon } // namespace Seldon \ No newline at end of file diff --git a/src/models/ActivityDrivenModel.cpp b/src/models/ActivityDrivenModel.cpp index c2f4a0a..891fda5 100644 --- a/src/models/ActivityDrivenModel.cpp +++ b/src/models/ActivityDrivenModel.cpp @@ -1,5 +1,6 @@ #include "models/ActivityDrivenModel.hpp" #include "network.hpp" +#include "network_generation.hpp" #include "util/math.hpp" #include #include @@ -14,6 +15,13 @@ ActivityDrivenModel::ActivityDrivenModel( NetworkT & network, std::mt19937 & gen contact_prob_list( std::vector>( network.n_agents() ) ), gen( gen ) { + + if( mean_weights ) + { + auto agents_copy = network.agents; + network = NetworkGeneration::generate_fully_connected( network.n_agents() ); + network.agents = agents_copy; + } } double ActivityDrivenModel::homophily_weight( size_t idx_contacter, size_t idx_contacted ) From b822f8ec5deffb6870bb665b31c3af010b336e13 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Thu, 21 Mar 2024 20:57:35 +0000 Subject: [PATCH 19/24] ActivityDrivenModel: Reworked constructor Now takes the `ActivityDrivenSettings` struct to construct. This also enabled making many member variables private. Also gave the `Model` base class a constructor that takes `max_iterations`. Co-authored-by: Amrita Goswami --- include/model.hpp | 3 +++ include/models/ActivityDrivenModel.hpp | 35 +++++++++++++------------- include/simulation.hpp | 35 +------------------------- src/models/ActivityDrivenModel.cpp | 30 ++++++++++++++++++---- 4 files changed, 46 insertions(+), 57 deletions(-) diff --git a/include/model.hpp b/include/model.hpp index e13f663..9402265 100644 --- a/include/model.hpp +++ b/include/model.hpp @@ -15,6 +15,9 @@ class Model std::optional max_iterations = std::nullopt; + Model() = default; + Model( std::optional max_iterations ) : max_iterations( max_iterations ){}; + virtual void initialize_iterations() { _n_iterations = 0; diff --git a/include/models/ActivityDrivenModel.hpp b/include/models/ActivityDrivenModel.hpp index 7459409..5641326 100644 --- a/include/models/ActivityDrivenModel.hpp +++ b/include/models/ActivityDrivenModel.hpp @@ -1,6 +1,8 @@ #pragma once #include "agents/activity_agent.hpp" +#include "config_parser.hpp" + #include "model.hpp" #include "network.hpp" #include @@ -19,12 +21,17 @@ class ActivityDrivenModel : public Model using AgentT = ActivityAgent; using NetworkT = Network; - ActivityDrivenModel( NetworkT & network, std::mt19937 & gen ); - - void get_agents_from_power_law(); // This needs to be called after eps and gamma have been set + ActivityDrivenModel( const Config::ActivityDrivenSettings & settings, NetworkT & network, std::mt19937 & gen ); void iteration() override; +private: + NetworkT & network; + std::vector> contact_prob_list; // Probability of choosing i in 1 to m rounds + // Random number generation + std::mt19937 & gen; // reference to simulation Mersenne-Twister engine + std::set> reciprocal_edge_buffer{}; + // Model-specific parameters double dt = 0.01; // Timestep for the integration of the coupled ODEs // Various free parameters @@ -41,39 +48,31 @@ class ActivityDrivenModel : public Model bool mean_activities = false; bool mean_weights = false; - double convergence_tol = 1e-12; // TODO: ?? - bool use_reluctances = false; double reluctance_mean{}; double reluctance_sigma{}; double reluctance_eps{}; double covariance_factor{}; - // bot @TODO: less hacky size_t n_bots = 0; // The first n_bots agents are bots std::vector bot_m = std::vector( 0 ); std::vector bot_activity = std::vector( 0 ); std::vector bot_opinion = std::vector( 0 ); std::vector bot_homophily = std::vector( 0 ); - [[nodiscard]] bool bot_present() const - { - return n_bots > 0; - } - -private: - NetworkT & network; - std::vector> contact_prob_list; // Probability of choosing i in 1 to m rounds - // Random number generation - std::mt19937 & gen; // reference to simulation Mersenne-Twister engine - std::set> reciprocal_edge_buffer{}; - // Buffers for RK4 integration std::vector k1_buffer{}; std::vector k2_buffer{}; std::vector k3_buffer{}; std::vector k4_buffer{}; + void get_agents_from_power_law(); + + [[nodiscard]] bool bot_present() const + { + return n_bots > 0; + } + template void get_euler_slopes( std::vector & k_buffer, Opinion_Callback opinion ) { diff --git a/include/simulation.hpp b/include/simulation.hpp index 8d4b132..0815e23 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -107,40 +107,7 @@ class Simulation : public SimulationInterface model = [&]() { - auto model = std::make_unique( network, gen ); - model->dt = activitydriven_settings.dt; - model->m = activitydriven_settings.m; - model->eps = activitydriven_settings.eps; - model->gamma = activitydriven_settings.gamma; - model->homophily = activitydriven_settings.homophily; - model->reciprocity = activitydriven_settings.reciprocity; - model->alpha = activitydriven_settings.alpha; - model->K = activitydriven_settings.K; - model->mean_activities = activitydriven_settings.mean_activities; - model->mean_weights = activitydriven_settings.mean_weights; - model->max_iterations = activitydriven_settings.max_iterations; - // Reluctance - model->use_reluctances = activitydriven_settings.use_reluctances; - model->reluctance_mean = activitydriven_settings.reluctance_mean; - model->reluctance_sigma = activitydriven_settings.reluctance_sigma; - model->reluctance_eps = activitydriven_settings.reluctance_eps; - // Bot - model->n_bots = activitydriven_settings.n_bots; - model->bot_opinion = activitydriven_settings.bot_opinion; - model->bot_m = activitydriven_settings.bot_m; - model->bot_homophily = activitydriven_settings.bot_homophily; - model->bot_activity = activitydriven_settings.bot_activity; - model->get_agents_from_power_law(); - - // TODO: this is stupid and should be done in the constructor, but right now it cant since we set mean - // weights only later - if( model->mean_weights ) - { - auto agents_copy = network.agents; - network = NetworkGeneration::generate_fully_connected( network.n_agents() ); - network.agents = agents_copy; - } - + auto model = std::make_unique( activitydriven_settings, network, gen ); return model; }(); diff --git a/src/models/ActivityDrivenModel.cpp b/src/models/ActivityDrivenModel.cpp index 891fda5..62417e2 100644 --- a/src/models/ActivityDrivenModel.cpp +++ b/src/models/ActivityDrivenModel.cpp @@ -9,12 +9,33 @@ namespace Seldon { -ActivityDrivenModel::ActivityDrivenModel( NetworkT & network, std::mt19937 & gen ) - : Model(), +ActivityDrivenModel::ActivityDrivenModel( + const Config::ActivityDrivenSettings & settings, NetworkT & network, std::mt19937 & gen ) + : Model( settings.max_iterations ), network( network ), contact_prob_list( std::vector>( network.n_agents() ) ), - gen( gen ) + gen( gen ), + dt( settings.dt ), + m( settings.m ), + eps( settings.eps ), + gamma( settings.gamma ), + alpha( settings.alpha ), + homophily( settings.homophily ), + reciprocity( settings.reciprocity ), + K( settings.K ), + mean_activities( settings.mean_activities ), + mean_weights( settings.mean_weights ), + use_reluctances( settings.use_reluctances ), + reluctance_mean( settings.reluctance_mean ), + reluctance_sigma( settings.reluctance_sigma ), + reluctance_eps( settings.reluctance_eps ), + n_bots( settings.n_bots ), + bot_m( settings.bot_m ), + bot_activity( settings.bot_activity ), + bot_opinion( settings.bot_opinion ), + bot_homophily( settings.bot_homophily ) { + get_agents_from_power_law(); if( mean_weights ) { @@ -160,8 +181,7 @@ void ActivityDrivenModel::update_network_mean() contact_prob_list[idx_agent] = weights; // set to zero } - auto probability_helper = []( double omega, size_t m ) - { + auto probability_helper = []( double omega, size_t m ) { double p = 0; for( size_t i = 1; i <= m; i++ ) p += ( std::pow( -omega, i + 1 ) + omega ) / ( omega + 1 ); From 2ab75f9de2537fcb23d4e8b25e778149e5ae1716 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Fri, 22 Mar 2024 12:48:20 +0000 Subject: [PATCH 20/24] DeGroot/Deffuant: Construct from settings Easier and clearer. Also more private members. Co-authored-by: Amrita Goswami --- include/model.hpp | 3 +-- include/models/DeGroot.hpp | 9 +++++---- include/models/DeffuantModel.hpp | 21 ++++++++++++--------- include/simulation.hpp | 10 +++------- src/models/ActivityDrivenModel.cpp | 3 ++- src/models/DeGroot.cpp | 8 ++++++-- test/test_deGroot.cpp | 15 ++++++++++----- 7 files changed, 39 insertions(+), 30 deletions(-) diff --git a/include/model.hpp b/include/model.hpp index 9402265..c184428 100644 --- a/include/model.hpp +++ b/include/model.hpp @@ -13,8 +13,6 @@ class Model public: using AgentT = AgentT_; - std::optional max_iterations = std::nullopt; - Model() = default; Model( std::optional max_iterations ) : max_iterations( max_iterations ){}; @@ -48,6 +46,7 @@ class Model virtual ~Model() = default; private: + std::optional max_iterations = std::nullopt; size_t _n_iterations{}; }; diff --git a/include/models/DeGroot.hpp b/include/models/DeGroot.hpp index 05d9c8d..f6c49ce 100644 --- a/include/models/DeGroot.hpp +++ b/include/models/DeGroot.hpp @@ -1,5 +1,6 @@ #pragma once #include "agents/simple_agent.hpp" +#include "config_parser.hpp" #include "model.hpp" #include "network.hpp" #include @@ -11,16 +12,16 @@ namespace Seldon class DeGrootModel : public Model { public: - using AgentT = SimpleAgent; - using NetworkT = Network; - double convergence_tol = 1e-12; + using AgentT = SimpleAgent; + using NetworkT = Network; - DeGrootModel( NetworkT & network ); + DeGrootModel( Config::DeGrootSettings settings, NetworkT & network ); void iteration() override; bool finished() override; private: + double convergence_tol{}; std::optional max_opinion_diff = std::nullopt; NetworkT & network; std::vector agents_current_copy; diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index 3a34a82..a6ae599 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -2,6 +2,7 @@ #include "agents/discrete_vector_agent.hpp" #include "agents/simple_agent.hpp" +#include "config_parser.hpp" #include "model.hpp" #include "network.hpp" #include "util/math.hpp" @@ -9,9 +10,6 @@ #include #include "network_generation.hpp" -#include -#include -#include #include @@ -25,12 +23,14 @@ class DeffuantModelAbstract : public Model using AgentT = AgentT_; using NetworkT = Network; - double homophily_threshold = 0.2; // d in paper - double mu = 0.5; // convergence parameter - bool use_network = false; // for the basic Deffuant model - - DeffuantModelAbstract( NetworkT & network, std::mt19937 & gen, bool use_network ) - : Model(), use_network( use_network ), network( network ), gen( gen ) + DeffuantModelAbstract( + const Config::DeffuantSettings & settings, NetworkT & network, std::mt19937 & gen, bool use_network ) + : Model( settings.max_iterations ), + homophily_threshold( settings.homophily_threshold ), + mu( settings.mu ), + use_network( settings.use_network ), + network( network ), + gen( gen ) { // Generate the network as a square lattice if use_network is true if( use_network ) @@ -103,6 +103,9 @@ class DeffuantModelAbstract : public Model // bool finished() override; private: + double homophily_threshold{}; // d in paper + double mu{}; // convergence parameter + bool use_network{}; // for the basic Deffuant model NetworkT & network; std::mt19937 & gen; // reference to simulation Mersenne-Twister engine }; diff --git a/include/simulation.hpp b/include/simulation.hpp index 0815e23..5e7c72b 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -70,9 +70,7 @@ class Simulation : public SimulationInterface // DeGroot specific parameters model = [&]() { - auto model = std::make_unique( network ); - model->max_iterations = degroot_settings.max_iterations; - model->convergence_tol = degroot_settings.convergence_tol; + auto model = std::make_unique( degroot_settings, network ); return model; }(); @@ -88,10 +86,8 @@ class Simulation : public SimulationInterface // Deffuant model specific parameters model = [&]() { - auto model = std::make_unique( network, gen, deffuant_settings.use_network ); - model->max_iterations = deffuant_settings.max_iterations; - model->homophily_threshold = deffuant_settings.homophily_threshold; - model->mu = deffuant_settings.mu; + auto model = std::make_unique( + deffuant_settings, network, gen, deffuant_settings.use_network ); return model; }(); diff --git a/src/models/ActivityDrivenModel.cpp b/src/models/ActivityDrivenModel.cpp index 62417e2..0a0ba06 100644 --- a/src/models/ActivityDrivenModel.cpp +++ b/src/models/ActivityDrivenModel.cpp @@ -181,7 +181,8 @@ void ActivityDrivenModel::update_network_mean() contact_prob_list[idx_agent] = weights; // set to zero } - auto probability_helper = []( double omega, size_t m ) { + auto probability_helper = []( double omega, size_t m ) + { double p = 0; for( size_t i = 1; i <= m; i++ ) p += ( std::pow( -omega, i + 1 ) + omega ) / ( omega + 1 ); diff --git a/src/models/DeGroot.cpp b/src/models/DeGroot.cpp index fae1391..1373f42 100644 --- a/src/models/DeGroot.cpp +++ b/src/models/DeGroot.cpp @@ -1,12 +1,16 @@ #include "models/DeGroot.hpp" +#include "config_parser.hpp" #include #include namespace Seldon { -DeGrootModel::DeGrootModel( NetworkT & network ) - : Model(), network( network ), agents_current_copy( network.agents ) +DeGrootModel::DeGrootModel( Config::DeGrootSettings settings, NetworkT & network ) + : Model( settings.max_iterations ), + convergence_tol( settings.convergence_tol ), + network( network ), + agents_current_copy( network.agents ) { // For a strongly connected network, the number of SCCs should be 1 // Print a warning if this is not true diff --git a/test/test_deGroot.cpp b/test/test_deGroot.cpp index 9c53577..862c9a4 100644 --- a/test/test_deGroot.cpp +++ b/test/test_deGroot.cpp @@ -23,11 +23,15 @@ TEST_CASE( "Test the DeGroot Model Symmetric", "[DeGroot]" ) { 0.2, 0.8 }, }; + auto settings = Config::DeGrootSettings(); + auto network = Network( std::move( neighbour_list ), std::move( weight_list ), Network::EdgeDirection::Incoming ); - auto model = DeGrootModel( network ); - model.convergence_tol = 1e-6; - model.max_iterations = 100; + settings.convergence_tol = 1e-6; + settings.max_iterations = 100; + + auto model = DeGrootModel( settings, network ); + network.agents[0].data.opinion = 0.0; network.agents[1].data.opinion = 1.0; @@ -36,10 +40,11 @@ TEST_CASE( "Test the DeGroot Model Symmetric", "[DeGroot]" ) model.iteration(); } - INFO( fmt::format( "N_iterations = {} (with convergence_tol {})\n", model.n_iterations(), model.convergence_tol ) ); + INFO( fmt::format( + "N_iterations = {} (with convergence_tol {})\n", model.n_iterations(), settings.convergence_tol ) ); for( size_t i = 0; i < n_agents; i++ ) { INFO( fmt::format( "Opinion {} = {}\n", i, network.agents[i].data.opinion ) ); - REQUIRE_THAT( network.agents[i].data.opinion, WithinAbs( 0.5, model.convergence_tol * 10.0 ) ); + REQUIRE_THAT( network.agents[i].data.opinion, WithinAbs( 0.5, settings.convergence_tol * 10.0 ) ); } } \ No newline at end of file From 27e319d0203d5a61fa8d3e21bac9d7e78f2c8e32 Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Fri, 22 Mar 2024 12:53:32 +0000 Subject: [PATCH 21/24] Simulation: Put agents_from_file outside of if Less code repetition, less confusing, less shitty Co-authored-by: Amrita Goswami --- include/simulation.hpp | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/include/simulation.hpp b/include/simulation.hpp index 5e7c72b..41fd4ea 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -73,11 +73,6 @@ class Simulation : public SimulationInterface auto model = std::make_unique( degroot_settings, network ); return model; }(); - - if( cli_agent_file.has_value() ) - { - network.agents = agents_from_file( cli_agent_file.value() ); - } } else if( options.model == Config::Model::DeffuantModel ) { @@ -90,11 +85,6 @@ class Simulation : public SimulationInterface deffuant_settings, network, gen, deffuant_settings.use_network ); return model; }(); - - if( cli_agent_file.has_value() ) - { - network.agents = agents_from_file( cli_agent_file.value() ); - } } } else if constexpr( std::is_same_v ) @@ -106,11 +96,11 @@ class Simulation : public SimulationInterface auto model = std::make_unique( activitydriven_settings, network, gen ); return model; }(); + } - if( cli_agent_file.has_value() ) - { - network.agents = agents_from_file( cli_agent_file.value() ); - } + if( cli_agent_file.has_value() ) + { + network.agents = agents_from_file( cli_agent_file.value() ); } } From 0014712761574d6ed0164bc98b21f5e84304e51f Mon Sep 17 00:00:00 2001 From: Moritz Sallermann Date: Fri, 22 Mar 2024 14:06:48 +0000 Subject: [PATCH 22/24] Simulation: Simplify creation of models Made a new header `model_factory` that takes care of the complexity. Co-authored-by: Amrita Goswami --- include/config_parser.hpp | 3 +- include/model_factory.hpp | 80 ++++++++++++++++++++++++++ include/models/ActivityDrivenModel.hpp | 2 +- include/models/DeffuantModel.hpp | 4 +- include/simulation.hpp | 43 +++----------- 5 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 include/model_factory.hpp diff --git a/include/config_parser.hpp b/include/config_parser.hpp index 5efe3ca..de66820 100644 --- a/include/config_parser.hpp +++ b/include/config_parser.hpp @@ -87,11 +87,12 @@ struct InitialNetworkSettings struct SimulationOptions { + using ModelVariantT = std::variant; Model model; std::string model_string; int rng_seed = std::random_device()(); OutputSettings output_settings; - std::variant model_settings; + ModelVariantT model_settings; InitialNetworkSettings network_settings; }; diff --git a/include/model_factory.hpp b/include/model_factory.hpp new file mode 100644 index 0000000..954a96f --- /dev/null +++ b/include/model_factory.hpp @@ -0,0 +1,80 @@ +#include "config_parser.hpp" +#include "model.hpp" +#include "models/ActivityDrivenModel.hpp" +#include "models/DeGroot.hpp" +#include "models/DeffuantModel.hpp" +#include "network.hpp" +#include +#include +#include +#include + +namespace Seldon::ModelFactory +{ + +using ModelVariantT = Config::SimulationOptions::ModelVariantT; + +template +auto check_agent_type( FuncT func ) +{ + if constexpr( std::is_same_v ) + { + return func(); + } + else + { + throw std::runtime_error( "Incompatible agent and model type!" ); + return std::unique_ptr>{}; + } +} + +template +inline auto create_model_degroot( Network & network, const ModelVariantT & model_settings ) +{ + if constexpr( std::is_same_v ) + { + auto degroot_settings = std::get( model_settings ); + auto model = std::make_unique( degroot_settings, network ); + return model; + } + else + { + throw std::runtime_error( "Incompatible agent and model type!" ); + return std::unique_ptr>{}; + } +} + +template +inline auto +create_model_activity_driven( Network & network, const ModelVariantT & model_settings, std::mt19937 & gen ) +{ + if constexpr( std::is_same_v ) + { + auto activitydriven_settings = std::get( model_settings ); + auto model = std::make_unique( activitydriven_settings, network, gen ); + return model; + } + else + { + throw std::runtime_error( "Incompatible agent and model type!" ); + return std::unique_ptr>{}; + } +} + +template +inline auto create_model_deffuant( Network & network, const ModelVariantT & model_settings, std::mt19937 & gen ) +{ + if constexpr( std::is_same_v ) + { + auto deffuant_settings = std::get( model_settings ); + auto model = std::make_unique( deffuant_settings, network, gen ); + return model; + } + else + { + throw std::runtime_error( "Incompatible agent and model type!" ); + return std::unique_ptr>{}; + } +} + +} // namespace Seldon::ModelFactory \ No newline at end of file diff --git a/include/models/ActivityDrivenModel.hpp b/include/models/ActivityDrivenModel.hpp index 5641326..06d830f 100644 --- a/include/models/ActivityDrivenModel.hpp +++ b/include/models/ActivityDrivenModel.hpp @@ -33,7 +33,7 @@ class ActivityDrivenModel : public Model std::set> reciprocal_edge_buffer{}; // Model-specific parameters - double dt = 0.01; // Timestep for the integration of the coupled ODEs + double dt{}; // Timestep for the integration of the coupled ODEs // Various free parameters int m{}; // Number of agents contacted, when the agent is active double eps{}; // Minimum activity epsilon; a_i belongs to [epsilon,1] diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index a6ae599..f5fad8b 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -23,8 +23,7 @@ class DeffuantModelAbstract : public Model using AgentT = AgentT_; using NetworkT = Network; - DeffuantModelAbstract( - const Config::DeffuantSettings & settings, NetworkT & network, std::mt19937 & gen, bool use_network ) + DeffuantModelAbstract( const Config::DeffuantSettings & settings, NetworkT & network, std::mt19937 & gen ) : Model( settings.max_iterations ), homophily_threshold( settings.homophily_threshold ), mu( settings.mu ), @@ -42,6 +41,7 @@ class DeffuantModelAbstract : public Model } network = NetworkGeneration::generate_square_lattice( n_edge ); } + initialize_agents(); } diff --git a/include/simulation.hpp b/include/simulation.hpp index 41fd4ea..44ac5dc 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -2,6 +2,7 @@ #include "config_parser.hpp" #include "fmt/core.h" +#include "model_factory.hpp" #include "network.hpp" #include #include @@ -61,41 +62,17 @@ class Simulation : public SimulationInterface void create_model( const Config::SimulationOptions & options, const std::optional & cli_agent_file ) { - if constexpr( std::is_same_v ) + if( options.model == Config::Model::DeGroot ) { - if( options.model == Config::Model::DeGroot ) - { - auto degroot_settings = std::get( options.model_settings ); - - // DeGroot specific parameters - model = [&]() - { - auto model = std::make_unique( degroot_settings, network ); - return model; - }(); - } - else if( options.model == Config::Model::DeffuantModel ) - { - auto deffuant_settings = std::get( options.model_settings ); - - // Deffuant model specific parameters - model = [&]() - { - auto model = std::make_unique( - deffuant_settings, network, gen, deffuant_settings.use_network ); - return model; - }(); - } + model = ModelFactory::create_model_degroot( network, options.model_settings ); } - else if constexpr( std::is_same_v ) + else if( options.model == Config::Model::ActivityDrivenModel ) { - auto activitydriven_settings = std::get( options.model_settings ); - - model = [&]() - { - auto model = std::make_unique( activitydriven_settings, network, gen ); - return model; - }(); + model = ModelFactory::create_model_activity_driven( network, options.model_settings, gen ); + } + else if( options.model == Config::Model::DeffuantModel ) + { + model = ModelFactory::create_model_deffuant( network, options.model_settings, gen ); } if( cli_agent_file.has_value() ) @@ -104,8 +81,6 @@ class Simulation : public SimulationInterface } } - // void create_agents( const std::optional & cli_agent_file ) {} - Simulation( const Config::SimulationOptions & options, const std::optional & cli_network_file, const std::optional & cli_agent_file ) From 8940c15d8ef0f90b61040142e375e8a1b63f5735 Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Fri, 22 Mar 2024 15:04:13 +0000 Subject: [PATCH 23/24] Deffuant: Move initialize_agents from constructor We now call initialize_agents for the Deffuant model out of the constructor, so that we can pass it the dimensions for the DeffuantVector model Co-authored-by: Moritz Sallermann --- examples/DeffuantVector/conf.toml | 24 ++++++++++++++++++++++++ include/connectivity.hpp | 2 +- include/model_factory.hpp | 18 ++++++++++++++++++ include/models/DeffuantModel.hpp | 2 -- test/test_network.cpp | 2 +- 5 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 examples/DeffuantVector/conf.toml diff --git a/examples/DeffuantVector/conf.toml b/examples/DeffuantVector/conf.toml new file mode 100644 index 0000000..604a40e --- /dev/null +++ b/examples/DeffuantVector/conf.toml @@ -0,0 +1,24 @@ +[simulation] +model = "Deffuant" +# rng_seed = 120 # Leaving this empty will pick a random seed + +[io] +# n_output_network = 20 # Write the network every 20 iterations +n_output_agents = 1 # Write the opinions of agents after every iteration +print_progress = true # Print the iteration time ; if not set, then does not prints +output_initial = true # Print the initial opinions and network file from step 0. If not set, this is true by default. +start_output = 1 # Start writing out opinions and/or network files from this iteration. If not set, this is 1. + +[model] +max_iterations = 1000 # If not set, max iterations is infinite + +[Deffuant] +homophily_threshold = 0.2 # d in the paper; agents interact if difference in opinion is less than this value +mu = 0.5 # convergence parameter; similar to social interaction strength K (0,0.5] +use_network = false # If true, will use a square lattice Will throw if sqrt(n_agents) is not an integer +binary_vector = true # If true, this will be the multi-dimensional binary vector Deffuant model +dim = 5 # For the multi-dimensional binary vector Deffuant model, define the number of dimensions in each opinion vector + +[network] +number_of_agents = 1000 +connections_per_agent = 0 diff --git a/include/connectivity.hpp b/include/connectivity.hpp index a9d0159..1af9c8d 100644 --- a/include/connectivity.hpp +++ b/include/connectivity.hpp @@ -75,7 +75,7 @@ class TarjanConnectivityAlgo { lowest[v] = std::min( lowest[v], num[u] ); } // u not processed - } // u has been visited + } // u has been visited } // Now v has been processed diff --git a/include/model_factory.hpp b/include/model_factory.hpp index 954a96f..f2b72db 100644 --- a/include/model_factory.hpp +++ b/include/model_factory.hpp @@ -77,4 +77,22 @@ inline auto create_model_deffuant( Network & network, const ModelVariant } } +template +inline auto +create_model_deffuant_vector( Network & network, const ModelVariantT & model_settings, std::mt19937 & gen ) +{ + if constexpr( std::is_same_v ) + { + auto deffuant_settings = std::get( model_settings ); + auto model = std::make_unique( deffuant_settings, network, gen ); + model.initialize_agents(); + return model; + } + else + { + throw std::runtime_error( "Incompatible agent and model type!" ); + return std::unique_ptr>{}; + } +} + } // namespace Seldon::ModelFactory \ No newline at end of file diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index f5fad8b..51667d4 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -41,8 +41,6 @@ class DeffuantModelAbstract : public Model } network = NetworkGeneration::generate_square_lattice( n_edge ); } - - initialize_agents(); } std::vector select_interacting_agent_pair() diff --git a/test/test_network.cpp b/test/test_network.cpp index f1d2bfa..57239e7 100644 --- a/test/test_network.cpp +++ b/test/test_network.cpp @@ -112,7 +112,7 @@ TEST_CASE( "Testing the network class" ) auto weight = buffer_w[i_neighbour]; std::tuple edge{ neighbour, i_agent, weight - }; // Note that i_agent and neighbour are flipped compared to before + }; // 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 } From 514215cb94c55b902eb463016cf82611074f199a Mon Sep 17 00:00:00 2001 From: Amrita Goswami Date: Fri, 22 Mar 2024 15:48:57 +0000 Subject: [PATCH 24/24] DeffuantVectorModel: Removed hardcoded dim The user can now enter the dimensions for the DeffuantVectorModel. Additionally, ConfigParser also checks to make sure that the user-defined dim=1, if the basic Deffuant model is being used. We also added a test for the DeffuantVectorModel. Co-authored-by: Moritz Sallermann --- include/config_parser.hpp | 9 +++-- include/connectivity.hpp | 2 +- include/model_factory.hpp | 3 +- include/models/DeffuantModel.hpp | 2 +- include/simulation.hpp | 10 +++++- src/config_parser.cpp | 14 ++++++++ src/models/DeffuantModel.cpp | 2 +- src/models/DeffuantModelVector.cpp | 3 +- test/res/deffuant_vector_2agents.toml | 24 +++++++++++++ test/test_deffuant.cpp | 49 +++++++++++++++++++++++++++ test/test_network.cpp | 2 +- 11 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 test/res/deffuant_vector_2agents.toml diff --git a/include/config_parser.hpp b/include/config_parser.hpp index de66820..208e27d 100644 --- a/include/config_parser.hpp +++ b/include/config_parser.hpp @@ -48,9 +48,12 @@ struct DeffuantSettings { std::optional max_iterations = std::nullopt; double homophily_threshold - = 0.2; // d in the paper; agents interact if difference in opinion is less than this value - double mu = 0.5; // convergence parameter; similar to social interaction strength K (0,0.5] - bool use_network = false; + = 0.2; // d in the paper; agents interact if difference in opinion is less than this value + double mu = 0.5; // convergence parameter; similar to social interaction strength K (0,0.5] + bool use_network = false; // For using a square lattice network + bool use_binary_vector = false; // For the multi-dimensional DeffuantModelVector; by default set to false + size_t dim + = 1; // The size of the opinions vector. This is used for the multi-dimensional DeffuantModelVector model. }; struct ActivityDrivenSettings diff --git a/include/connectivity.hpp b/include/connectivity.hpp index 1af9c8d..a9d0159 100644 --- a/include/connectivity.hpp +++ b/include/connectivity.hpp @@ -75,7 +75,7 @@ class TarjanConnectivityAlgo { lowest[v] = std::min( lowest[v], num[u] ); } // u not processed - } // u has been visited + } // u has been visited } // Now v has been processed diff --git a/include/model_factory.hpp b/include/model_factory.hpp index f2b72db..d5864ac 100644 --- a/include/model_factory.hpp +++ b/include/model_factory.hpp @@ -68,6 +68,7 @@ inline auto create_model_deffuant( Network & network, const ModelVariant { auto deffuant_settings = std::get( model_settings ); auto model = std::make_unique( deffuant_settings, network, gen ); + model->initialize_agents( deffuant_settings.dim ); return model; } else @@ -85,7 +86,7 @@ create_model_deffuant_vector( Network & network, const ModelVariantT & m { auto deffuant_settings = std::get( model_settings ); auto model = std::make_unique( deffuant_settings, network, gen ); - model.initialize_agents(); + model->initialize_agents( deffuant_settings.dim ); return model; } else diff --git a/include/models/DeffuantModel.hpp b/include/models/DeffuantModel.hpp index 51667d4..7f90baf 100644 --- a/include/models/DeffuantModel.hpp +++ b/include/models/DeffuantModel.hpp @@ -95,7 +95,7 @@ class DeffuantModelAbstract : public Model // template void update_rule( AgentT & agent1, AgentT & agent2 ); - void initialize_agents(); + void initialize_agents( size_t dim [[maybe_unused]] ); // void iteration() override; // bool finished() override; diff --git a/include/simulation.hpp b/include/simulation.hpp index 44ac5dc..4eca2fc 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -72,7 +72,15 @@ class Simulation : public SimulationInterface } else if( options.model == Config::Model::DeffuantModel ) { - model = ModelFactory::create_model_deffuant( network, options.model_settings, gen ); + auto deffuant_settings = std::get( options.model_settings ); + if( deffuant_settings.use_binary_vector ) + { + model = ModelFactory::create_model_deffuant_vector( network, options.model_settings, gen ); + } + else + { + model = ModelFactory::create_model_deffuant( network, options.model_settings, gen ); + } } if( cli_agent_file.has_value() ) diff --git a/src/config_parser.cpp b/src/config_parser.cpp index b872eb8..b65d83a 100644 --- a/src/config_parser.cpp +++ b/src/config_parser.cpp @@ -78,6 +78,9 @@ SimulationOptions parse_config_file( std::string_view config_file_path ) set_if_specified( model_settings.homophily_threshold, tbl[options.model_string]["homophily_threshold"] ); set_if_specified( model_settings.mu, tbl[options.model_string]["mu"] ); set_if_specified( model_settings.use_network, tbl[options.model_string]["use_network"] ); + // Options for the DeffuantModelVector model + set_if_specified( model_settings.use_binary_vector, tbl[options.model_string]["binary_vector"] ); + set_if_specified( model_settings.dim, tbl[options.model_string]["dim"] ); options.model_settings = model_settings; } else if( options.model == Model::ActivityDrivenModel ) @@ -215,6 +218,15 @@ void validate_settings( const SimulationOptions & options ) auto model_settings = std::get( options.model_settings ); check( name_and_var( model_settings.homophily_threshold ), g_zero ); check( name_and_var( model_settings.mu ), []( auto x ) { return x >= 0 && x <= 1; } ); + // DeffuantModelVector settings + check( name_and_var( model_settings.dim ), g_zero ); + // @TODO: maybe make this check nicer? + if( !model_settings.use_binary_vector ) + { + const std::string basic_deff_msg + = "The basic Deffuant model has not been implemented with multiple dimensions"; + check( name_and_var( model_settings.dim ), []( auto x ) { return x == 1; }, basic_deff_msg ); + } } } @@ -272,6 +284,8 @@ void print_settings( const SimulationOptions & options ) fmt::print( " homophily_threshold {}\n", model_settings.homophily_threshold ); fmt::print( " mu {}\n", model_settings.mu ); fmt::print( " use_network {}\n", model_settings.use_network ); + fmt::print( " use_binary_vector {}\n", model_settings.use_binary_vector ); + fmt::print( " dim {}\n", model_settings.dim ); } fmt::print( "[Network]\n" ); diff --git a/src/models/DeffuantModel.cpp b/src/models/DeffuantModel.cpp index 976ca42..8b01c2d 100644 --- a/src/models/DeffuantModel.cpp +++ b/src/models/DeffuantModel.cpp @@ -7,7 +7,7 @@ namespace Seldon { template<> -void DeffuantModelAbstract::initialize_agents() +void DeffuantModelAbstract::initialize_agents( size_t ) { for( size_t i = 0; i < network.agents.size(); i++ ) { diff --git a/src/models/DeffuantModelVector.cpp b/src/models/DeffuantModelVector.cpp index 3912632..a203494 100644 --- a/src/models/DeffuantModelVector.cpp +++ b/src/models/DeffuantModelVector.cpp @@ -10,10 +10,9 @@ namespace Seldon { template<> -void DeffuantModelAbstract::initialize_agents() +void DeffuantModelAbstract::initialize_agents( size_t dim ) { std::uniform_int_distribution dist( 0, 1 ); - int dim = 5; for( size_t i = 0; i < network.agents.size(); i++ ) { diff --git a/test/res/deffuant_vector_2agents.toml b/test/res/deffuant_vector_2agents.toml new file mode 100644 index 0000000..fe203f1 --- /dev/null +++ b/test/res/deffuant_vector_2agents.toml @@ -0,0 +1,24 @@ +[simulation] +model = "Deffuant" +# rng_seed = 120 # Leaving this empty will pick a random seed + +[io] +# n_output_network = 20 # Write the network every 20 iterations +# n_output_agents = 1 # Write the opinions of agents after every iteration +# print_progress = false # Print the iteration time ; if not set, then does not prints +# output_initial = true # Print the initial opinions and network file from step 0. If not set, this is true by default. +# start_output = 1 # Start writing out opinions and/or network files from this iteration. If not set, this is 1. + +[model] +max_iterations = 10 # If not set, max iterations is infinite + +[Deffuant] +homophily_threshold = 2 # d in the paper; agents interact if difference in opinion is less than this value +mu = 0.5 # convergence parameter; similar to social interaction strength K (0,0.5] +use_network = false # If true, will use a square lattice Will throw if sqrt(n_agents) is not an integer +binary_vector = true # If true, this will be the multi-dimensional binary vector Deffuant model +dim = 3 # For the multi-dimensional binary vector Deffuant model, define the number of dimensions in each opinion vector + +[network] +number_of_agents = 2 +connections_per_agent = 0 diff --git a/test/test_deffuant.cpp b/test/test_deffuant.cpp index 0504467..5818fbf 100644 --- a/test/test_deffuant.cpp +++ b/test/test_deffuant.cpp @@ -1,5 +1,7 @@ #include "catch2/matchers/catch_matchers.hpp" +#include "catch2/matchers/catch_matchers_range_equals.hpp" #include "models/DeffuantModel.hpp" +#include #include #include #include @@ -112,4 +114,51 @@ TEST_CASE( "Test the lattice deffuant model for 16x16 agents", "[deffuantLattice { REQUIRE_THAT( simulation.network.agents[idx_agent].data.opinion, WithinRel( -avg_opinion ) ); } +} + +TEST_CASE( + "Test the multi-dimensional Deffuant vector model, with 3-dimensional binary opinions, for two agents", + "[deffuantVectorTwoAgents]" ) +{ + using namespace Seldon; + using namespace Catch::Matchers; + using AgentT = DeffuantModelVector::AgentT; + + auto proj_root_path = fs::current_path(); + auto input_file = proj_root_path / fs::path( "test/res/deffuant_vector_2agents.toml" ); + + auto options = Config::parse_config_file( input_file.string() ); + + auto simulation = Simulation( options, std::nullopt, std::nullopt ); + + // We need an output path for Simulation, but we won't write anything out there + fs::path output_dir_path = proj_root_path / fs::path( "test/output_deffuant_vector" ); + + // references to agent opinion + std::vector & agent1_opinion = simulation.network.agents[0].data.opinion; + std::vector & agent2_opinion = simulation.network.agents[1].data.opinion; + + // agents are too far apart, we dont expect any change with the iterations + auto agent1_init = std::vector{ 0, 1, 0 }; + auto agent2_init = std::vector{ 1, 0, 1 }; + + agent1_opinion = agent1_init; + agent2_opinion = agent2_init; + + simulation.run( output_dir_path ); + + REQUIRE_THAT( agent1_opinion, Catch::Matchers::RangeEquals( agent1_init ) ); + REQUIRE_THAT( agent2_opinion, Catch::Matchers::RangeEquals( agent2_init ) ); + + // agents are close enough, they should converge + // dim-1 or 2 opinions should be the same + agent1_init = std::vector{ 0, 1, 1 }; + agent2_init = std::vector{ 1, 1, 1 }; + + agent1_opinion = agent1_init; + agent2_opinion = agent2_init; + + simulation.run( output_dir_path ); + + REQUIRE_THAT( agent1_opinion, Catch::Matchers::RangeEquals( agent2_opinion ) ); } \ No newline at end of file diff --git a/test/test_network.cpp b/test/test_network.cpp index 57239e7..f1d2bfa 100644 --- a/test/test_network.cpp +++ b/test/test_network.cpp @@ -112,7 +112,7 @@ TEST_CASE( "Testing the network class" ) auto weight = buffer_w[i_neighbour]; std::tuple edge{ neighbour, i_agent, weight - }; // Note that i_agent and neighbour are flipped compared to before + }; // 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 }