Skip to content

Commit 54e03cb

Browse files
committed
Squashed commit of the following:
commit 398321e Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 20 22:57:56 2025 +0000 add tests for the new player write/read methods commit 6a26ab4 Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 20 22:55:10 2025 +0000 found a bug in JuMP mof reading with start values commit 8d6f9ca Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 20 22:12:03 2025 +0000 implement player serialization and storage commit 30262b8 Author: Bruno Machado Pacheco <mpacheco.bruno@gmail.com> Date: Thu Feb 20 21:26:34 2025 +0000 refactor player as a parametric type of its payoff
1 parent 8eb19f5 commit 54e03cb

File tree

8 files changed

+107
-27
lines changed

8 files changed

+107
-27
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ version = "0.1.0"
55

66
[deps]
77
IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
8+
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
89
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
910
NormalGames = "4dc104ef-1e0c-4a09-93ea-14ddc8e86204"
1011

@@ -13,6 +14,7 @@ NormalGames = {path = "Normal-form-games"}
1314

1415
[compat]
1516
IterTools = "1.10.0"
17+
JSON3 = "1.14.1"
1618
JuMP = "1.23.6"
1719
Test = "1.11.0"
1820

src/Game/Player.jl

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
1-
using JuMP
1+
using JuMP, JSON3
22

33
abstract type AbstractPlayer end
44

55
"A player in an IPG."
6-
struct Player <: AbstractPlayer
6+
struct Player{Payoff<:AbstractPayoff} <: AbstractPlayer
77
"Strategy space."
88
Xp::Model
99
"Payoff function."
10-
Πp::AbstractPayoff
10+
Πp::Payoff
1111
"Player's index."
1212
p::Integer # TODO: this could be Any, to allow for more general collections, e.g, string names
1313
# TODO: maybe I could just index everything relevant as a Dict{Player, T}?
1414
end
1515
"Initialize player with empty strategy space."
16-
function Player(Πp::AbstractPayoff, p::Integer)
17-
return Player(Model(), Πp, p)
16+
function Player(Πp::Payoff, p::Integer) where Payoff <: AbstractPayoff
17+
return Player{Payoff}(Model(), Πp, p)
1818
end
1919

2020
"Check whether an optimizer has already been set for player."
21-
function has_optimizer(player::Player)
21+
function has_optimizer(player::AbstractPlayer)
2222
return ~(backend(player.Xp).state == JuMP.MOIU.NO_OPTIMIZER)
2323
end
2424

2525
"Define the optimizer for player."
26-
function set_optimizer(player::Player, optimizer_factory)
26+
function set_optimizer(player::AbstractPlayer, optimizer_factory)
2727
JuMP.set_optimizer(player.Xp, optimizer_factory)
2828
end
2929

3030
"Compute the utility that `player_p` receives from `player_k` when they play, resp., `xp` and `xk`."
31-
function bilateral_payoff(player_p::Player, xp::Vector{<:Union{Real,VariableRef}}, player_k::Player, xk::Vector{<:Real})
31+
function bilateral_payoff(player_p::Player{QuadraticPayoff}, xp::Vector{<:Union{Real,VariableRef}}, player_k::Player{QuadraticPayoff}, xk::Vector{<:Real})
3232
return bilateral_payoff(player_p.Πp, player_p.p, xp, player_k.p, xk)
3333
end
3434
"Compute the utility that `player_p` receives from `player_k` when they play, resp., `xp` and `σk`."
35-
function bilateral_payoff(player_p::Player, xp::Vector{<:Union{Real,VariableRef}}, player_k::Player, σk::DiscreteMixedStrategy)
35+
function bilateral_payoff(player_p::Player{QuadraticPayoff}, xp::Vector{<:Union{Real,VariableRef}}, player_k::Player{QuadraticPayoff}, σk::DiscreteMixedStrategy)
3636
return expected_value(xk -> bilateral_payoff(player_p.Πp, player_p.p, xp, player_k.p, xk), σk)
3737
end
3838

3939
"Compute the payoff of player `player` given pure strategy profile `x`."
40-
function payoff(player::Player, x::Vector{<:Vector{<:Real}})
40+
function payoff(player::AbstractPlayer, x::Vector{<:Vector{<:Real}})
4141
return payoff(player.Πp, x, player.p)
4242
end
4343
"Compute the payoff of player `player` given mixed strategy profile `σ`."
44-
function payoff(player::Player, σ::Vector{DiscreteMixedStrategy})
44+
function payoff(player::AbstractPlayer, σ::Vector{DiscreteMixedStrategy})
4545
_payoff = x -> payoff(player, x)
4646
return expected_value(_payoff, σ)
4747
end
4848

4949
"Compute `player`'s best response to the mixed strategy profile `σp`."
50-
function best_response(player::Player, σ::Vector{DiscreteMixedStrategy})
50+
function best_response(player::Player{QuadraticPayoff}, σ::Vector{DiscreteMixedStrategy})
5151
xp = all_variables(player.Xp)
5252

5353
# TODO: No idea why this doesn't work
@@ -68,7 +68,7 @@ function best_response(player::Player, σ::Vector{DiscreteMixedStrategy})
6868
end
6969

7070
"Solve the feasibility problem for a player, returning a feasible strategy."
71-
function find_feasible_pure_strategy(player::Player)
71+
function find_feasible_pure_strategy(player::AbstractPlayer)
7272
@objective(player.Xp, JuMP.MOI.FEASIBILITY_SENSE, 0)
7373

7474
set_silent(player.Xp)
@@ -78,6 +78,47 @@ function find_feasible_pure_strategy(player::Player)
7878
end
7979

8080
"Solve the feasibility problem of all players, returning a feasible profile."
81-
function find_feasible_pure_profile(players::Vector{Player})
81+
function find_feasible_pure_profile(players::Vector{<:AbstractPlayer})
8282
return [find_feasible_pure_strategy(player) for player in players]
83-
end
83+
end
84+
85+
function save(player::Player{QuadraticPayoff}, filename::String)
86+
# we need to ensure that the file is stored as a json, so we can add the payoff information
87+
JuMP.write_to_file(player.Xp, filename; format = JuMP.MOI.FileFormats.FORMAT_MOF)
88+
89+
# TODO: this could be refactored as a payoff JSON-serialization method
90+
# see https://quinnj.github.io/JSON3.jl/stable/#Struct-API
91+
mof_json = JSON3.read(read(filename, String))
92+
mof_json = copy(mof_json) # JSON type is immutable
93+
94+
mof_json[:IPG__player_index] = player.p
95+
mof_json[:IPG__payoff] = Dict(
96+
:cp => player.Πp.cp,
97+
:Qp => player.Πp.Qp,
98+
# JSON3 cannot store matrices, it stores them as a flat vector
99+
:Qp_shapes => [size(Qpk) for Qpk in player.Πp.Qp],
100+
)
101+
102+
open(filename, "w") do file
103+
JSON3.write(file, mof_json)
104+
end
105+
end
106+
107+
function load(filename::String)::Player{QuadraticPayoff}
108+
Xp = JuMP.read_from_file(filename; format = JuMP.MOI.FileFormats.FORMAT_MOF)
109+
110+
# see https://github.com/jump-dev/JuMP.jl/issues/3946
111+
set_start_value.(all_variables(Xp), start_value.(all_variables(Xp)))
112+
113+
mof_json = JSON3.read(read(filename, String))
114+
115+
player_index = mof_json[:IPG__player_index]
116+
payoff_data = mof_json[:IPG__payoff]
117+
cp = copy(payoff_data[:cp])
118+
flat_Qp = copy(payoff_data[:Qp])
119+
Qp_shapes = copy(payoff_data[:Qp_shapes])
120+
121+
Qp = [reshape(flat_Qpk, Tuple(Qpk_shape)) for (flat_Qpk, Qpk_shape) in zip(flat_Qp, Qp_shapes)]
122+
123+
return Player{QuadraticPayoff}(Xp, QuadraticPayoff(cp, Qp), player_index)
124+
end

src/SGM/DeviationReaction.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
"Find a deviation from mixed profile `σ`."
3-
function find_deviation_best_response(players::Vector{Player}, σ::Vector{DiscreteMixedStrategy}; player_order=nothing, dev_tol=1e-3)::Tuple{Float64,Int64,Union{Nothing,Vector{Float64}}}
3+
function find_deviation_best_response(players::Vector{<:AbstractPlayer}, σ::Vector{DiscreteMixedStrategy}; player_order=nothing, dev_tol=1e-3)::Tuple{Float64,Int64,Union{Nothing,Vector{Float64}}}
44
if isnothing(player_order)
55
player_order = 1:length(players)
66
end

src/SGM/Initialization.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
function initialize_strategies_feasibility(players::Vector{Player})
2+
function initialize_strategies_feasibility(players::Vector{<:AbstractPlayer})
33
S_X = [Vector{Vector{Float64}}() for _ in players]
44
for player in players
55
xp_init = start_value.(all_variables(player.Xp))
@@ -16,7 +16,7 @@ function initialize_strategies_feasibility(players::Vector{Player})
1616
return S_X
1717
end
1818

19-
function initialize_strategies_player_alone(players::Vector{Player})
19+
function initialize_strategies_player_alone(players::Vector{<:AbstractPlayer})
2020
S_X = [Vector{Vector{Float64}}() for _ in players]
2121

2222
# mixed profile that simulates players being alone (all others play 0)

src/SGM/PlayerOrder.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11

2-
function get_player_order_fixed(players::Vector{Player}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
2+
function get_player_order_fixed(players::Vector{<:AbstractPlayer}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
33
return keys(players)
44
end
55

6-
function get_player_order_fixed_ascending(players::Vector{Player}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
6+
function get_player_order_fixed_ascending(players::Vector{<:AbstractPlayer}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
77
return sort(keys(players))
88
end
99

10-
function get_player_order_fixed_descending(players::Vector{Player}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
10+
function get_player_order_fixed_descending(players::Vector{<:AbstractPlayer}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
1111
return reverse(sort(keys(players)))
1212
end
1313

14-
function get_player_order_random(players::Vector{Player}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
14+
function get_player_order_random(players::Vector{<:AbstractPlayer}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
1515
return shuffle(keys(players))
1616
end
1717

18-
function get_player_order_by_last_deviation(players::Vector{Player}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
18+
function get_player_order_by_last_deviation(players::Vector{<:AbstractPlayer}, iter::Integer, Σ_S::Vector{Vector{DiscreteMixedStrategy}}, payoff_improvements::Vector{<:Tuple{<:Integer,<:Real}})
1919
iterations_since_last_deviation = Dict(player.p => length(payoff_improvements) for player in players)
2020

2121
for i in 0:length(payoff_improvements)-1

src/SGM/SGM.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ include("PlayerOrder.jl")
44
include("DeviationReaction.jl")
55
include("Initialization.jl")
66

7-
function SGM(players::Vector{Player}, optimizer_factory; max_iter=100, dev_tol=1e-3)
7+
function SGM(players::Vector{<:AbstractPlayer}, optimizer_factory; max_iter=100, dev_tol=1e-3)
88
# set `optimizer_factory` the optimizer for each player that doesn't have one yet
99
for player in players
1010
# check whether an optimizer has already been set to player

src/SGM/SampledGame/SampledGame.jl

Lines changed: 3 additions & 3 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{Player}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
3+
function get_polymatrix(players::Vector{<:AbstractPlayer}, 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`
@@ -32,14 +32,14 @@ mutable struct SampledGame
3232
S_X::Vector{Vector{Vector{Float64}}} # sample of strategies (finite subset of the strategy space X)
3333
normal_game::NormalGames.NormalGame
3434
end
35-
function SampledGame(players::Vector{Player}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
35+
function SampledGame(players::Vector{<:AbstractPlayer}, S_X::Vector{<:Vector{<:Vector{<:Real}}})
3636
payoff_polymatrix = get_polymatrix(players, S_X)
3737
normal_game = NormalGames.NormalGame(length(players), length.(S_X), payoff_polymatrix)
3838

3939
return SampledGame(S_X, normal_game)
4040
end
4141

42-
function add_new_strategy!(sampled_game::SampledGame, players::Vector{Player}, new_xp::Vector{<:Real}, p::Integer)
42+
function add_new_strategy!(sampled_game::SampledGame, players::Vector{<:AbstractPlayer}, new_xp::Vector{<:Real}, p::Integer)
4343
# first part is easy, just add the new strategy to the set
4444
push!(sampled_game.S_X[p], new_xp)
4545

test/runtests.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,43 @@ using Test
1313
@test expected_value(sum, σp) == 1.0
1414
end
1515

16+
@testset "Player serialization" begin
17+
using JuMP, SCIP
18+
19+
X1 = Model()
20+
@variable(X1, x1, start=10.0)
21+
@constraint(X1, x1 >= 0)
22+
23+
player = Player(X1, QuadraticPayoff(0, [2, 1]), 1)
24+
25+
filename = "test_player.json"
26+
IPG.save(player, filename)
27+
loaded_player = IPG.load(filename)
28+
29+
@test loaded_player.p == player.p
30+
@test loaded_player.Πp.cp == player.Πp.cp
31+
@test loaded_player.Πp.Qp == player.Πp.Qp
32+
33+
# test strategy space
34+
X1 = player.Xp
35+
loaded_X1 = loaded_player.Xp
36+
37+
set_optimizer(X1, SCIP.Optimizer)
38+
set_optimizer(loaded_X1, SCIP.Optimizer)
39+
40+
set_silent(X1)
41+
optimize!(X1)
42+
43+
set_silent(loaded_X1)
44+
optimize!(loaded_X1)
45+
46+
@test value.(all_variables(X1)) == value.(all_variables(loaded_X1))
47+
@test objective_value(X1) == objective_value(loaded_X1)
48+
49+
# cleanup
50+
rm(filename)
51+
end
52+
1653
@testset "Example 5.3" begin
1754
using SCIP
1855

0 commit comments

Comments
 (0)