Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vertex enumeration #155

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Structure
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Order: Base Types and Methods, Game Generators, Computing Nash Equilibria, Repeated Games
Base Types and Methods: normal_form_game
Game Generators: random, generators/bimatrix_generators
Computing Nash Equilibria: pure_nash, support_enumeration, lrsnash
Computing Nash Equilibria: pure_nash, support_enumeration, lrsnash, vertex_enumeration
Repeated Games: repeated_game_util, repeated_game
6 changes: 5 additions & 1 deletion src/GameTheory.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ include("pure_nash.jl")
include("repeated_game.jl")
include("random.jl")
include("support_enumeration.jl")
include("vertex_enumeration.jl")
include("util.jl")
include("generators/Generators.jl")

Expand Down Expand Up @@ -108,6 +109,9 @@ export
support_enumeration, support_enumeration_task,

# LRS
lrsnash
lrsnash,

# Vertex Enumeration
vertex_enumeration

end # module
206 changes: 206 additions & 0 deletions src/vertex_enumeration.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
function nonnegativeorthant_hrep(dim::Int)
h = Vector{HalfSpace{Float64, Vector{Float64}}}()
for i in 1:dim
e_i = zeros(dim)
e_i[i] = -1.
push!(h, HalfSpace(e_i, 0.))
end
H = h[1]
for i in 2:lastindex(h)
H = H ∩ h[i]
end
H
end
nonnegativeorthant(dim::Int) = polyhedron(nonnegative_hrep(dim))


function br_envelope_hrep(player::Player)
A = player.payoff_array
h = Vector{HalfSpace{Float64, Vector{Float64}}}()
for i in 1:num_actions(player)
A_i = A[i,:]
push!(h, HalfSpace(A_i, 1.))
end
H = h[1]
for i in 2:lastindex(h)
H = H ∩ h[i]
end
H
end


function bestresponsepolyhedra(g::NormalFormGame)
nnorthantP = nonnegativeorthant_hrep(num_actions(g.players[1])) # x_i ≥ 0 for all i=1,…,m
nnorthantQ = nonnegativeorthant_hrep(num_actions(g.players[2])) # x_i ≥ 0 for all i=m+1,…,m+n
brenvelopeP = br_envelope_hrep(g.players[2]) # B'x ≤ 1
brenvelopeQ = br_envelope_hrep(g.players[1]) # Ax ≤ 1
P = brenvelopeP ∩ nnorthantP # best response polyhedron for Player 1
Q = nnorthantQ ∩ brenvelopeQ # best response polyhedron for Player 2
polyhedron(P), polyhedron(Q)
end


function hlabels(P::HRepresentation)
Phindices = Polyhedra.Index{Float64, HalfSpace{Float64, Vector{Float64}}}[]
for pi in eachindex(halfspaces(P))
push!(Phindices, pi)
end
Phindices
end
hlabels(P::DefaultPolyhedron) = hlabels(hrep(P))
hlabels(P::VRepresentation) = hlabels(doubledescription(P))


#function vlabels(P::VRepresentation)
# Pvindices = Polyhedra.Index{Float64, Vector{Float64}}[]
# for pi in eachindex(points(P))
# push!(Pvindices, pi)
# end
# Pvindices
#end
# vlabels(P::DefaultPolyhedron) = vlabels(vrep(P))
# vlabels(P::HRepresentation) = vlabels(doubledescription(P))


label_to_integer(idx::Polyhedra.Index{T, S}) where {T, S} = idx.value
# integer_to_vlabel(n::Int) = Polyhedra.Index{Float64, Vector{Float64}}(n)
# integer_to_hlabel(n::Int) = Polyhedra.Index{Float64, HalfSpace{Float64, Vector{Float64}}}(n)


function labelmap(P::DefaultPolyhedron)
labelmaps = []
vpoints = [x for x in points(P)]
for pi in eachindex(points(P))
push!(labelmaps, (vpoints[label_to_integer(pi)], incidenthalfspaceindices(P, pi)))
end
Dict(labelmaps)
end


mutable struct LabeledPolyhedron{S<:Polyhedron, T<:Vector, U<:Vector, V<:Dict}
polyhedron::S
points::T
hlabels::U
labelmap::V
end


function LabeledPolyhedron(P::DefaultPolyhedron)
vpoints = [x for x in points(P)]
LabeledPolyhedron(P, vpoints, hlabels(P), labelmap(P))
end


struct LabeledBimatrixGame
game::NormalFormGame
P::LabeledPolyhedron
Q::LabeledPolyhedron
end


function LabeledBimatrixGame(g::NormalFormGame)
@assert num_players(g) == 2
samwycherley marked this conversation as resolved.
Show resolved Hide resolved
P, Q = bestresponsepolyhedra(g)
LabeledBimatrixGame(g, LabeledPolyhedron(P), LabeledPolyhedron(Q))
end


# unlabel(LP::LabeledPolyhedron) = LP.polyhedron
# unlabel(P::T) where T <: Union{Polyhedron, HRepresentation, VRepresentation} = P


get_num_hlabels(LP::LabeledPolyhedron, point) = length(LP.labelmap[point])


function is_nondegenerate(b::LabeledBimatrixGame)
m = num_actions(b.game.players[1])
n = num_actions(b.game.players[2])
all([(get_num_hlabels(b.P, point) ≤ m) for point in b.P.points]) && all([(get_num_hlabels(b.Q, idx) ≤ n) for idx in b.Q.points])
end


function is_nondegenerate(g::NormalFormGame)
if num_players(g) == 2
return is_nondegenerate(LabeledBimatrixGame(g))
else
error("Not a bimatrix game.")
end
end


#function droplabel!(LP::LabeledPolyhedron, point, label)
# LP.labelmap[point]
# if !(label in LP.labelmap[point])
# @warn("Label not found.")
# return nothing
# end
# deleteat!(LP.labelmap[point], findall(x->x==label, LP.labelmap[point])[1]) # note label is unique so findall() finds precisely 1 index
#end


#function droplabel(LP::LabeledPolyhedron, point, label)
# LP.labelmap[point]
# if !(label in LP.labelmap[point])
# @warn("Label not found.")
# return LP.labelmap[point]
# end
# deleteat(LP.labelmap[point], findall(x->x==label, LP.labelmap[point])[1]) # note label is unique so findall() finds precisely 1 index
#end


function dropvertex_pure!(LP::LabeledPolyhedron, point)
delete!(LP.labelmap, point)
deleteat!(LP.points, findall(x -> x == point, LP.points)[1])
end


function dropvertex!(LP::LabeledPolyhedron, point)
if length(findall(x -> x == point, LP.points)) == 0
dropvertex!(LP::LabeledPolyhedron, point, 1e-10)
else
dropvertex_pure!(LP, point)
end
end


function dropvertex!(LP::LabeledPolyhedron, point, tol) # handle precision errors up to tolerance
local_point = LP.points[findall(x -> norm(x - point) < tol, LP.points)[1]]
local_point
dropvertex_pure!(LP, local_point)
end


"""
vertex_enumeration(g::NormalFormGame)

Finds all Nash equilibria of a non-degenerate bimatrix game `g` via the vertex enumeration algorithm (Algorithm 3.5 in von Stengel (2007).)

# References
- B. von Stengel, "Equilibria Computation for Two-Player Games in Strategic and Extensive Form."
In N. Nisan, T. Roughgarden, E. Tardos and V. V. Vazirani (eds.), Algorithmic Game Theory, 2007.
"""
function vertex_enumeration(g::NormalFormGame)
if !(all(g.players[1].payoff_array .≥ 0) && all(g.players[2].payoff_array .≥ 0))
player1_transform = Player(g.players[1].payoff_array .- minimum(g.players[1].payoff_array))
player2_transform = Player(g.players[2].payoff_array .- minimum(g.players[2].payoff_array))
g = NormalFormGame(player1_transform, player2_transform) # positive affine transformation
end
b = LabeledBimatrixGame(g)
if !is_nondegenerate(b)
error("The vertex enumeration algorithm will not yield a solution for degenerate games.")
end
m, n = num_actions.(g.players)
NEs = Tuple[]
dropvertex!(b.P, zeros(m))
dropvertex!(b.Q, zeros(n))

for x in b.P.points
for y in b.Q.points
if sort(label_to_integer.(vcat(b.P.labelmap[x], b.Q.labelmap[y]))) == Vector(1:m+n)
# i.e. (x, y) completely labeled, x ∈ P - {0}, y ∈ Q - {0}
push!(NEs, (x./(ones(1,m)*x),y./(ones(1,n)*y)))
end
end
end
NEs
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ include("test_normal_form_game.jl")
include("test_random.jl")
include("test_support_enumeration.jl")
include("test_lrsnash.jl")
include("test_vertex_enumeration.jl")


include("generators/runtests.jl")
62 changes: 62 additions & 0 deletions test/test_vertex_enumeration.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@testset "Testing nondegenerate bimatrix games" begin
@testset "Test 2 by 2 normal form game with 3 equilibria" begin
g = NormalFormGame(Player([1 0; 0 1]), Player([1 0; 0 1]))
NEs = [([1.0, 0.0], [1.0, 0.0]),
([0.0, 1.0], [0.0, 1.0]),
([0.5, 0.5], [0.5, 0.5])]
NEs_computed = @inferred(vertex_enumeration(g))
@test sort(NEs) == sort(NEs_computed)
end

@testset "Test 2 by 3 normal form game with 1 equilibrium" begin
g = NormalFormGame(Player([5 4 3; 4 1 2]), Player([5 1; 4 3; 3 2]))
NEs = [([1.0, 0.0], [1.0, 0.0, 0.0])]
NEs_computed = @inferred(vertex_enumeration(g))
@test NEs == NEs_computed
end

@testset "Test Matching Pennies" begin
MP = [1 -1; -1 1]
g = NormalFormGame(Player(MP), Player(-MP))
NEs = [([0.5, 0.5], [0.5, 0.5])]
NEs_computed = @inferred(vertex_enumeration(g))
@test NEs == NEs_computed
end
@testset "Test 3 by 2 normal form game where polyhedron features imprecision error" begin
g = NormalFormGame(Player([3 3; 2 5; 0 6]), Player([3 2 3; 2 6 1]))
NEs = sort([([1.0, 0.0, 0.0], [1.0, 0.0]),
([0.8, 0.2, 0.0], [2/3, 1/3]),
([0.0, 1/3, 2/3], [1/3, 2/3])])
NEs_computed = sort(@inferred(vertex_enumeration(g)))
NExs = []
NEys = []
for (x,y) in NEs
push!(NExs, x)
push!(NEys, y)
end
NExs_computed = []
NEys_computed = []
for (x,y) in NEs_computed
push!(NExs_computed, x)
push!(NEys_computed, y)
end
@test NExs ≈ NExs_computed && NEys ≈ NEys_computed
end
end


import GameTheory: is_nondegenerate

@testset "Testing degenerate game errors" begin
@testset "Test degenerate 2 by 2 normal form game throws error" begin
A = [1 1; 1 1]
g = NormalFormGame(Player(A), Player(A))
@test is_nondegenerate(g) == false
@test_throws ErrorException("The vertex enumeration algorithm will not yield a solution for degenerate games.") vertex_enumeration(g)
end
@testset "Test that 3 player game throws error" begin
g = random_game((2, 2, 2))
@test_throws AssertionError vertex_enumeration(g)
@test_throws ErrorException("Not a bimatrix game.") is_nondegenerate(g)
end
end