Skip to content

Commit 2d70c2a

Browse files
committed
Squashed commit of the following:
commit a25d065a0618b8be6c78c6303a7318b5557a78c5 Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 27 22:06:39 2025 +0000 refactor NormalGames.jl dependency into SearchNE.jl commit 1c24319e18b41ae5e5b20a1b5f5a56d6964fb06b Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 27 21:57:11 2025 +0000 restrict polymatrix approach to players with bilateral payoffs commit e4aa3c7ac1e6f7d78fd5a234159f871eb70b02da Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 27 21:45:55 2025 +0000 simplify payoff functions commit 44ba2d5c8d972aace19516a6fc97e59951dfaa09 Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 27 21:45:37 2025 +0000 fix expected total payoff for a player commit 1ea9395277f35d9eb7f320cf616f59f45edc28ca Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 27 21:38:22 2025 +0000 define type for bilateral payoff functions and add documentation commit fc5171a Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Mon Feb 24 23:26:04 2025 +0000 create payoff directory; correct abstractness in function arguments
1 parent 6d8f466 commit 2d70c2a

File tree

8 files changed

+114
-100
lines changed

8 files changed

+114
-100
lines changed

src/Game/Payoff.jl

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/Game/Payoff/BilateralPayoff.jl

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
2+
"""
3+
A bilateral payoff can be computed as the sum of bilateral interactions between the players.
4+
5+
If all players have bilateral payoffs, an equilibrium for the sampled game can be solved
6+
using its polymatrix representation, which yields an efficient formulation for the
7+
optimization problem.
8+
9+
It is expected that each bilateral payoff type implements the `bilateral_payoff` function.
10+
The function must have the following signature:
11+
```julia
12+
function bilateral_payoff(Πp::MyBilateralPayoff, p::Integer, xp::Vector{<:Union{Real,VariableRef}}, k::Integer, xk::Vector{<:Real})
13+
[...]
14+
end
15+
```
16+
Note that the `xp` argument can also be a vector of JuMP variable references. This is
17+
because the payoff functions are also used to build JuMP expressions, e.g., in
18+
`best_response` (see `src/Game/Player.jl`).
19+
20+
"""
21+
abstract type AbstractBilateralPayoff <: AbstractPayoff end
22+
23+
"Payoff function of player `p` with quadratic bilateral (pairwise) interactions."
24+
struct QuadraticPayoff <: AbstractBilateralPayoff
25+
cp::Vector{Float64}
26+
"`x[k]' * Qp[k] * x[p]` is the payoff component for player `p` with respect to the strategy of player `k`."
27+
Qp::Vector{Matrix{Float64}}
28+
function QuadraticPayoff(cp::Vector{<:Real}, Qp::Vector{<:Matrix{<:Real}})
29+
if !all(Qpk->size(Qpk,2)==length(cp), Qp)
30+
error("All Qp matrices must have the same number of columns as elements in cp (= dimension of p's strategy space).")
31+
end
32+
return new(cp, Qp)
33+
end
34+
end
35+
function QuadraticPayoff(cp::Real, Qp::Vector{<:Real})
36+
# constructor for simpler, unidimensional cases
37+
return QuadraticPayoff([cp], [qpk * ones(1,1) for qpk in Qp])
38+
end
39+
40+
"Compute each component of the payoff of player `p` with respect to player `k`."
41+
function bilateral_payoff(Πp::QuadraticPayoff, p::Integer, xp::Vector{<:Union{Real,VariableRef}}, k::Integer, xk::Vector{<:Real})
42+
if p == k
43+
return Πp.cp' * xp - 0.5 * xp' * Πp.Qp[p] * xp
44+
else
45+
return xk' * Πp.Qp[k] * xp
46+
end
47+
end
48+
49+
50+
# Utils
51+
52+
function bilateral_payoff(Πp::AbstractBilateralPayoff, p::Integer, xp::Vector{<:Union{Real,VariableRef}}, k::Integer, σk::DiscreteMixedStrategy)
53+
return expected_value(xk -> bilateral_payoff(Πp, p, xp, k, xk), σk)
54+
end
55+
function bilateral_payoff(Πp::AbstractBilateralPayoff, p::Integer, σp::DiscreteMixedStrategy, k::Integer, σk::DiscreteMixedStrategy)
56+
return expected_value(xp -> bilateral_payoff(Πp, p, xp, k, σk), σp)
57+
end
58+
59+
"Compute the payoff of player `p` given strategies x."
60+
function payoff(Πp::AbstractBilateralPayoff, x::Vector{<:Vector{<:Union{Real,VariableRef}}}, p::Integer)
61+
return sum([bilateral_payoff(Πp, p, x[p], k, x[k]) for k in 1:length(x)])
62+
end

src/Game/Payoff/Payoff.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using JuMP
2+
3+
abstract type AbstractPayoff end
4+
5+
include("BilateralPayoff.jl")
6+
7+
8+
# Utils
9+
10+
"Compute expected payoff for player `p` given mixed strategy profile `σ`."
11+
function payoff(Πp::AbstractPayoff, σ::Vector{DiscreteMixedStrategy}, p::Integer)
12+
return expected_value(x -> payoff(Πp, x, p), σ)
13+
end

src/Game/Player.jl

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,14 @@ function set_optimizer(player::AbstractPlayer, optimizer_factory)
2727
JuMP.set_optimizer(player.Xp, optimizer_factory)
2828
end
2929

30-
"Compute the utility that `player_p` receives from `player_k` when they play, resp., `xp` and `xk`."
31-
function bilateral_payoff(player_p::Player{QuadraticPayoff}, xp::Vector{<:Union{Real,VariableRef}}, player_k::Player{QuadraticPayoff}, xk::Vector{<:Real})
32-
return bilateral_payoff(player_p.Πp, player_p.p, xp, player_k.p, xk)
33-
end
34-
"Compute the utility that `player_p` receives from `player_k` when they play, resp., `xp` and `σk`."
35-
function bilateral_payoff(player_p::Player{QuadraticPayoff}, xp::Vector{<:Union{Real,VariableRef}}, player_k::Player{QuadraticPayoff}, σk::DiscreteMixedStrategy)
36-
return expected_value(xk -> bilateral_payoff(player_p.Πp, player_p.p, xp, player_k.p, xk), σk)
37-
end
38-
39-
"Compute the payoff of player `player` given pure strategy profile `x`."
40-
function payoff(player::AbstractPlayer, x::Vector{<:Vector{<:Real}})
41-
return payoff(player.Πp, x, player.p)
42-
end
43-
"Compute the payoff of player `player` given mixed strategy profile `σ`."
44-
function payoff(player::AbstractPlayer, σ::Vector{DiscreteMixedStrategy})
45-
_payoff = x -> payoff(player, x)
46-
return expected_value(_payoff, σ)
47-
end
48-
4930
"Compute `player`'s best response to the mixed strategy profile `σp`."
50-
function best_response(player::Player{QuadraticPayoff}, σ::Vector{DiscreteMixedStrategy})
31+
function best_response(player::Player{<:AbstractPayoff}, σ::Vector{DiscreteMixedStrategy})
5132
xp = all_variables(player.Xp)
5233

5334
# TODO: No idea why this doesn't work
5435
# @objective(model, Max, sum([IPG.bilateral_payoff(Πp, p, xp, k, σ[k]) for k in 1:m]))
5536

56-
obj = QuadExpr()
37+
obj = AffExpr()
5738
for k in 1:length(σ)
5839
obj += IPG.bilateral_payoff(player.Πp, player.p, xp, k, σ[k])
5940
end

src/IPG.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
module IPG
22

3-
using NormalGames, JuMP
3+
using JuMP
44

55
include("Game/Strategies.jl")
6-
include("Game/Payoff.jl")
6+
include("Game/Payoff/Payoff.jl")
77
include("Game/Player.jl")
88
include("SGM/SGM.jl")
99

src/SGM/DeviationReaction.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function find_deviation_best_response(players::Vector{<:AbstractPlayer}, σ::Vec
1111

1212
new_σ = copy(σ)
1313
new_σ[p] = DiscreteMixedStrategy([1], [new_x_p])
14-
payoff_improvement = payoff(player, new_σ) - payoff(player, σ)
14+
payoff_improvement = payoff(player.Πp, new_σ, player.p) - payoff(player.Πp, σ, player.p)
1515
if payoff_improvement > dev_tol
1616
return payoff_improvement, p, new_x_p
1717
end

src/SGM/SampledGame/SampledGame.jl

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
"Compute polymatrix for normal form game from sample of strategies."
3-
function get_polymatrix(players::Vector{<:AbstractPlayer}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
3+
function get_polymatrix(players::Vector{<:Player{<:AbstractBilateralPayoff}}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
44
polymatrix = Dict{Tuple{Integer, Integer}, Matrix{Float64}}()
55

66
# compute utility of each player `p` using strategy `i_p` against player `k` using strategy `i_k`
@@ -15,8 +15,8 @@ function get_polymatrix(players::Vector{<:AbstractPlayer}, S_X::Vector{<:Vector{
1515
for i_p in 1:length(S_X[p])
1616
for i_k in 1:length(S_X[k])
1717
polymatrix[p,k][i_p,i_k] = (
18-
IPG.bilateral_payoff(players[p], S_X[p][i_p], players[k], S_X[k][i_k])
19-
+ IPG.bilateral_payoff(players[p], S_X[p][i_p], players[p], S_X[p][i_p])
18+
IPG.bilateral_payoff(players[p].Πp, p, S_X[p][i_p], k, S_X[k][i_k])
19+
+ IPG.bilateral_payoff(players[p].Πp, p, S_X[p][i_p], p, S_X[p][i_p])
2020
)
2121
end
2222
end
@@ -27,59 +27,49 @@ function get_polymatrix(players::Vector{<:AbstractPlayer}, S_X::Vector{<:Vector{
2727
return polymatrix
2828
end
2929

30-
"Wrapper for NormalGames.NormalGame that includes the sample of strategies."
30+
"Normal-form representation of the sampled game."
3131
mutable struct SampledGame
3232
S_X::Vector{Vector{Vector{Float64}}} # sample of strategies (finite subset of the strategy space X)
33-
normal_game::NormalGames.NormalGame
33+
polymatrix::Dict{Tuple{Int, Int}, Matrix{Float64}}
3434
end
35-
function SampledGame(players::Vector{<:AbstractPlayer}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
36-
payoff_polymatrix = get_polymatrix(players, S_X)
37-
normal_game = NormalGames.NormalGame(length(players), length.(S_X), payoff_polymatrix)
38-
39-
return SampledGame(S_X, normal_game)
35+
function SampledGame(players::Vector{<:Player{<:AbstractBilateralPayoff}}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
36+
return SampledGame(S_X, get_polymatrix(players, S_X))
4037
end
4138

42-
function add_new_strategy!(sampled_game::SampledGame, players::Vector{<:AbstractPlayer}, new_xp::Vector{<:Real}, p::Integer)
39+
function add_new_strategy!(sg::SampledGame, players::Vector{<:Player{<:AbstractBilateralPayoff}}, new_xp::Vector{<:Real}, p::Integer)
4340
# first part is easy, just add the new strategy to the set
44-
push!(sampled_game.S_X[p], new_xp)
45-
46-
n = sampled_game.normal_game.n
47-
strat = sampled_game.normal_game.strat
48-
polymatrix = sampled_game.normal_game.polymatrix
49-
50-
strat[p] += 1
41+
push!(sg.S_X[p], new_xp)
42+
strat = length.(sg.S_X)
5143

5244
# now we need to update the normal game (polymatrix)
53-
for (p1, p2) in keys(polymatrix)
45+
for (p1, p2) in keys(sg.polymatrix)
5446
if p1 == p2 == p
5547
# add new row to polymatrix to store the utilities wrt the new strategy
56-
polymatrix[p,p] = zeros(strat[p], strat[p])
48+
sg.polymatrix[p,p] = zeros(strat[p], strat[p])
5749
elseif (p1 != p) & (p2 == p)
5850
# add new column to polymatrix to store the utilities wrt the new strategy
59-
polymatrix[p1,p] = hcat(polymatrix[p1,p], zeros(strat[p1], 1))
51+
sg.polymatrix[p1,p] = hcat(sg.polymatrix[p1,p], zeros(strat[p1], 1))
6052

6153
for i in 1:strat[p1]
6254
# compute utility of player `p1` using strategy `i` against the new strategy of player `p`
63-
polymatrix[p1,p][i,end] = (
64-
IPG.bilateral_payoff(players[p1], sampled_game.S_X[p1][i], players[p], new_xp)
65-
+ IPG.bilateral_payoff(players[p1], sampled_game.S_X[p1][i], players[p1], sampled_game.S_X[p1][i])
55+
sg.polymatrix[p1,p][i,end] = (
56+
IPG.bilateral_payoff(players[p1].Πp, p1, sg.S_X[p1][i], p, new_xp)
57+
+ IPG.bilateral_payoff(players[p1].Πp, p1, sg.S_X[p1][i], p1, sg.S_X[p1][i])
6658
)
6759
end
6860
elseif (p1 == p) & (p2 != p)
6961
# add new row to polymatrix to store the utilities wrt the new strategy
70-
polymatrix[p,p2] = vcat(polymatrix[p,p2], zeros(1, strat[p2]))
62+
sg.polymatrix[p,p2] = vcat(sg.polymatrix[p,p2], zeros(1, strat[p2]))
7163

7264
for i in 1:strat[p2]
7365
# compute utility of player `p1` using strategy `i` against the new strategy of player `p`
74-
polymatrix[p,p2][end,i] = (
75-
IPG.bilateral_payoff(players[p], new_xp, players[p2], sampled_game.S_X[p2][i])
76-
+ IPG.bilateral_payoff(players[p], new_xp, players[p], new_xp)
66+
sg.polymatrix[p,p2][end,i] = (
67+
IPG.bilateral_payoff(players[p].Πp, p, new_xp, p2, sg.S_X[p2][i])
68+
+ IPG.bilateral_payoff(players[p].Πp, p, new_xp, p, new_xp)
7769
)
7870
end
7971
end
8072
end
81-
82-
sampled_game.normal_game = NormalGames.NormalGame(n, strat, polymatrix)
8373
end
8474

8575
include("SearchNE.jl")

src/SGM/SampledGame/SearchNE.jl

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
using NormalGames
12

23
"Compute a (mixed) nash equilibrium for the sampled game using PNS."
34
function solve_PNS(sampled_game::SampledGame, optimizer_factory)::Vector{DiscreteMixedStrategy}
4-
_, _, NE_mixed = NormalGames.NashEquilibriaPNS(sampled_game.normal_game, optimizer_factory, false, false, false)
5+
normal_game = NormalGames.NormalGame(
6+
length(sampled_game.S_X),
7+
length.(sampled_game.S_X),
8+
sampled_game.polymatrix
9+
)
10+
11+
_, _, NE_mixed = NormalGames.NashEquilibriaPNS(normal_game, optimizer_factory, false, false, false)
512

613
# each element in NE_mixed is a mixed NE, represented as a vector of probabilities in
714
# the same shape as S_X
@@ -17,15 +24,19 @@ end
1724
function solve_Sandholm1(sampled_game::SampledGame, optimizer_factory)::Vector{DiscreteMixedStrategy}
1825
# the method doesn't support polymatrices with negative entries, so a quick
1926
# preprocessing is performed
20-
polymatrix = copy(sampled_game.normal_game.polymatrix)
27+
polymatrix = copy(sampled_game.polymatrix)
2128
offset = 0
2229
for k in keys(polymatrix)
2330
offset = min(offset, minimum(polymatrix[k]))
2431
end
2532
for k in keys(polymatrix)
2633
polymatrix[k] = polymatrix[k] .- offset
2734
end
28-
normal_game = NormalGames.NormalGame(sampled_game.normal_game.n, sampled_game.normal_game.strat, polymatrix)
35+
normal_game = NormalGames.NormalGame(
36+
length(sampled_game.S_X),
37+
length.(sampled_game.S_X),
38+
polymatrix
39+
)
2940

3041
_, _, NE_mixed, _, _, _, _, _ = NormalGames.NashEquilibria2(normal_game, optimizer_factory)
3142

0 commit comments

Comments
 (0)