From 35e63606d496c65167dc26b33b9bf9f210c46485 Mon Sep 17 00:00:00 2001 From: Harry Foster Date: Mon, 29 Jun 2026 10:49:09 +0100 Subject: [PATCH 1/4] add pariwise_interaction_fitness_function and test --- src/ludics/fitness_functions.py | 32 ++++++++++++++++++++++++++++++ tests/test_fitness_functions.py | 35 ++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/ludics/fitness_functions.py b/src/ludics/fitness_functions.py index 13301b1..1262aec 100644 --- a/src/ludics/fitness_functions.py +++ b/src/ludics/fitness_functions.py @@ -86,3 +86,35 @@ def general_four_state_fitness_function(state, **kwargs): return np.array( [sym.Function(f"f_{i + 1}")(state_symbol) for i, j in enumerate(state)] ) + +def pairwise_interaction_fitness_function(state,a,b,c,d): + """ + Returns the fitness of players in a symmetric pairwise interaction game. + The payoff matrix for the row player is given by: + + | | C | D | + |---|-----|-----| + | C | a_i | b_i | + | D | c_i | d_i | + + And a player's fitness is given by the mean payoff they receive when + interacting in a 2 player game with a random other member of the + population. + + Parameters: + ----------- + state: numpy.array, the ordered set of actions each player takes. 1 is a + cooperator, 0 is a defector, + + a,b,c,d: numpy.array, the entries to go into the payoff matrix. Entry i is the + respective payoff for player i's payoff matrix + + Returns: + --------- + numpy.array: an ordered array of each player's fitness + """ + N = len(state) + number_of_cooperators = np.sum(state) + cooperator_payoffs = np.array([((number_of_cooperators-1) * a[i] + (N - number_of_cooperators) * b[i])/(N-1) for i in range(N)]) + defector_payoffs = np.array([((number_of_cooperators) * c[i] + (N - number_of_cooperators-1) * d[i])/(N-1) for i in range(N)]) + return np.array([cooperator_payoffs[player] if action == 1 else defector_payoffs[player] for player, action in enumerate(state)]) \ No newline at end of file diff --git a/tests/test_fitness_functions.py b/tests/test_fitness_functions.py index 08c048b..47bd06b 100644 --- a/tests/test_fitness_functions.py +++ b/tests/test_fitness_functions.py @@ -243,4 +243,37 @@ def test_public_goods_game_fitness_function_for_homoogeneous_contribution_and_he expected_return = np.array([0,1,2]) - np.testing.assert_allclose(actual_return, expected_return) \ No newline at end of file + np.testing.assert_allclose(actual_return, expected_return) + +def test_pairwise_interaction_fitness_function_for_symmetric_prisoners_dilemma(): + """ + Tests that pairwise_interaction_fitness_function returns the correct + value for a symmetric, standard prisoner's dilemma""" + + state = np.array([0,1,1,0,1]) + a = np.array([3 for _ in state]) + b = np.array([0 for _ in state]) + c = np.array([5 for _ in state]) + d = np.array([1 for _ in state]) + + actual_value = ludics.fitness_functions.pairwise_interaction_fitness_function(state=state,a=a,b=b,c=c,d=d) + expected_value = np.array([4, 6/4, 6/4, 4, 6/4]) + + np.testing.assert_allclose(actual_value, expected_value) + +def test_pairwise_interaction_fitness_function_for_asymmetric_game(): + """ + Tests that pairwise_interaction_fitness_function returns the correct value for + an asymmetric game""" + + state = np.array([1,0,1,0]) + a = np.array([1,2,3,4]) + b = np.array([2,4,6,8]) + c = np.array([5,4,3,2]) + d = np.array([2,2,2,1]) + + actual_value = ludics.fitness_functions.pairwise_interaction_fitness_function(state=state,a=a,b=b,c=c,d=d) + expected_value = np.array([5/3, 10/3, 5, 5/3]) + + np.testing.assert_allclose(actual_value, expected_value) + \ No newline at end of file From 7f9b6f5455cb3f2db24106e45f927668bbad5742 Mon Sep 17 00:00:00 2001 From: Harry Foster Date: Mon, 29 Jun 2026 10:49:37 +0100 Subject: [PATCH 2/4] save test_fitness_functions --- tests/test_fitness_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fitness_functions.py b/tests/test_fitness_functions.py index 47bd06b..724324f 100644 --- a/tests/test_fitness_functions.py +++ b/tests/test_fitness_functions.py @@ -273,6 +273,7 @@ def test_pairwise_interaction_fitness_function_for_asymmetric_game(): d = np.array([2,2,2,1]) actual_value = ludics.fitness_functions.pairwise_interaction_fitness_function(state=state,a=a,b=b,c=c,d=d) + expected_value = np.array([5/3, 10/3, 5, 5/3]) np.testing.assert_allclose(actual_value, expected_value) From 894fd94791c72de15481cb723feccf2ec4b7a27f Mon Sep 17 00:00:00 2001 From: Harry Foster Date: Mon, 29 Jun 2026 10:55:10 +0100 Subject: [PATCH 3/4] Allow float parameters --- src/ludics/fitness_functions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ludics/fitness_functions.py b/src/ludics/fitness_functions.py index 1262aec..571fe7d 100644 --- a/src/ludics/fitness_functions.py +++ b/src/ludics/fitness_functions.py @@ -115,6 +115,6 @@ def pairwise_interaction_fitness_function(state,a,b,c,d): """ N = len(state) number_of_cooperators = np.sum(state) - cooperator_payoffs = np.array([((number_of_cooperators-1) * a[i] + (N - number_of_cooperators) * b[i])/(N-1) for i in range(N)]) - defector_payoffs = np.array([((number_of_cooperators) * c[i] + (N - number_of_cooperators-1) * d[i])/(N-1) for i in range(N)]) - return np.array([cooperator_payoffs[player] if action == 1 else defector_payoffs[player] for player, action in enumerate(state)]) \ No newline at end of file + cooperator_payoffs = ((number_of_cooperators-1) * a + (N - number_of_cooperators) * b)/(N-1) + defector_payoffs = ((number_of_cooperators) * c + (N - number_of_cooperators-1) * d)/(N-1) + return cooperator_payoffs * state + defector_payoffs * (1 - state) \ No newline at end of file From 1796cd85128317700b63cd476aaf68237cb9c41b Mon Sep 17 00:00:00 2001 From: Harry Foster Date: Mon, 29 Jun 2026 10:58:27 +0100 Subject: [PATCH 4/4] Add test and update docstring --- src/ludics/fitness_functions.py | 4 ++-- tests/test_fitness_functions.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/ludics/fitness_functions.py b/src/ludics/fitness_functions.py index 571fe7d..3c210fe 100644 --- a/src/ludics/fitness_functions.py +++ b/src/ludics/fitness_functions.py @@ -106,8 +106,8 @@ def pairwise_interaction_fitness_function(state,a,b,c,d): state: numpy.array, the ordered set of actions each player takes. 1 is a cooperator, 0 is a defector, - a,b,c,d: numpy.array, the entries to go into the payoff matrix. Entry i is the - respective payoff for player i's payoff matrix + a,b,c,d: float numpy.array, the entries to go into the payoff matrix. Entry i is the + respective payoff for player i's payoff matrix. Float values indicate a symmetric game Returns: --------- diff --git a/tests/test_fitness_functions.py b/tests/test_fitness_functions.py index 724324f..62915ac 100644 --- a/tests/test_fitness_functions.py +++ b/tests/test_fitness_functions.py @@ -273,8 +273,24 @@ def test_pairwise_interaction_fitness_function_for_asymmetric_game(): d = np.array([2,2,2,1]) actual_value = ludics.fitness_functions.pairwise_interaction_fitness_function(state=state,a=a,b=b,c=c,d=d) - + expected_value = np.array([5/3, 10/3, 5, 5/3]) np.testing.assert_allclose(actual_value, expected_value) - \ No newline at end of file + +def test_pairwise_interaction_fitness_function_for_mixed_parameters(): + """ + Tests that pairwise_interaction_fitness_function returns the correct value for + an asymmetric game where two parameters are symmetric""" + + state = np.array([1,0,0,1]) + a = np.array([1,2,3,4]) + b = np.array([2,4,6,4]) + c = 5 + d = 2 + + actual_value = ludics.fitness_functions.pairwise_interaction_fitness_function(state=state,a=a,b=b,c=c,d=d) + + expected_value = np.array([5/3, 4, 4, 4]) + + np.testing.assert_allclose(actual_value, expected_value) \ No newline at end of file