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

Add support enumeration #26

Merged
merged 8 commits into from
Aug 21, 2017
Merged

Conversation

shizejin
Copy link
Member

@shizejin shizejin commented Mar 13, 2017

closes #18

@oyamad
Here I don't quite understand the three warnings of ::Any in the body of type-related diagnosing.

julia> using Games

julia> g = NormalFormGame(Player([3 3; 2 5; 0 6]), Player([3 2 3; 2 6 1]))
3×2 NormalFormGame{2,Int64}

julia> @code_warntype support_enumeration(g)
Variables:
  #self#::Games.#support_enumeration
  g::Games.NormalFormGame{2,Int64}
  task::Task
  #15::Games.##15#16{Games.NormalFormGame{2,Int64}}
  NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1}
  #temp#::Void
  action::Tuple{Array{Float64,1},Array{Float64,1}}
  itemT::Tuple{Array{Float64,1},Array{Float64,1}}

Body:
  begin 
      #15::Games.##15#16{Games.NormalFormGame{2,Int64}} = $(Expr(:new, Games.##15#16{Games.NormalFormGame{2,Int64}}, :(g)))
      SSAValue(0) = #15::Games.##15#16{Games.NormalFormGame{2,Int64}}
      # meta: location boot.jl Type 297
      SSAValue(4) = 0
      # meta: pop location
      task::Task = (Core.ccall)(:jl_new_task,(Core.apply_type)(Core.Ref,Core.Task)::Type{Ref{Task}},(Core.svec)(Core.Any,Core.Int)::SimpleVector,SSAValue(0),0,SSAValue(4),0)::Task # line 36:
      NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1} = (Core.ccall)(:jl_alloc_array_1d,(Core.apply_type)(Core.Array,Tuple{Array{Float64,1},Array{Float64,1}},1)::Type{Array{Tuple{Array{Float64,1},Array{Float64,1}},1}},(Core.svec)(Core.Any,Core.Int)::SimpleVector,Array{Tuple{Array{Float64,1},Array{Float64,1}},1},0,0,0)::Array{Tuple{Array{Float64,1},Array{Float64,1}},1} # line 37:
      SSAValue(1) = task::Task
      #temp#::Void = Base.nothing
      12: 
      # meta: location task.jl done 274
      SSAValue(6) = $(Expr(:invoke, LambdaInfo for consume(::Task), :(Base.consume), SSAValue(1)))
      (Core.setfield!)(SSAValue(1),:result,SSAValue(6))::Any
      # meta: pop location
      unless (Base.box)(Base.Bool,(Base.not_int)((Base.box)(Base.Bool,(Base.or_int)(((Core.getfield)(SSAValue(1),:state)::Symbol === :done)::Bool,((Core.getfield)(SSAValue(1),:state)::Symbol === :failed)::Bool)))) goto 34
      SSAValue(9) = (Core.getfield)(SSAValue(1),:result)::Any
      SSAValue(10) = Base.nothing
      SSAValue(3) = SSAValue(9)
      action::Tuple{Array{Float64,1},Array{Float64,1}} = (Core.typeassert)((Base.convert)(Tuple{Array{Float64,1},Array{Float64,1}},SSAValue(3))::Any,Tuple{Array{Float64,1},Array{Float64,1}})::Tuple{Array{Float64,1},Array{Float64,1}}
      #temp#::Void = SSAValue(10) # line 38:
      # meta: location array.jl push! 480
      SSAValue(7) = (Base.box)(UInt64,(Base.check_top_bit)(1))
      (Core.ccall)(:jl_array_grow_end,Base.Void,(Core.svec)(Base.Any,Base.UInt)::SimpleVector,NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1},0,SSAValue(7),0)::Void # line 481:
      SSAValue(8) = (Base.arraylen)(NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1})::Int64
      (Base.arrayset)(NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1},action::Tuple{Array{Float64,1},Array{Float64,1}},SSAValue(8))::Array{Tuple{Array{Float64,1},Array{Float64,1}},1}
      # meta: pop location
      NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1}
      32: 
      goto 12
      34:  # line 41:
      return NEs::Array{Tuple{Array{Float64,1},Array{Float64,1}},1}
  end::Array{Tuple{Array{Float64,1},Array{Float64,1}},1}

throw(ArgumentError("Implemented only for 2-player games"))
end

return _support_enumeration_gen(g.players[1].payoff_array,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return a Task?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think return a Task would be better. I'll change this.

@shizejin shizejin changed the title Add support enumeration [WIP] Add support enumeration Mar 16, 2017
@shizejin shizejin changed the title [WIP] Add support enumeration WIP: Add support enumeration Mar 16, 2017
"""
support_enumeration_gen(g::NormalFormGame)

Generator version of `support_enumeration`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Generator" is a Python terminology. What is it called in Julia? @sglyon @cc7768

Copy link
Member Author

@shizejin shizejin Mar 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- Tasks seem to be the right comparison.

# Returns
* `::Task`: runnable task for generating Nash equilibria.
"""
function support_enumeration_gen(g::NormalFormGame)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the function name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also thinking about this, but would support_enumeration_task be fine?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that will be clear enough.

@shizejin shizejin changed the title WIP: Add support enumeration Add support enumeration Mar 19, 2017
minus 1 such pairs. This should thus be used only for small games.

# Arguments
* `g::NormalFormGame`: NormalFormGame instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line.


# Returns
* `::Vector{Tuple{Vector{Float64},Vector{Float64}}}`: Mixed-action
Nash equilibria that are found.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct indentation.

* `Tuple{Vector{Float64},Vector{Float64}}`: Tuple of Nash equilibrium
mixed actions.
"""
function _support_enumeration_task{T<:Real}(payoff_matrix1::Matrix{T},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be named _support_enumeration_producer or something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think producer is fine.

N = length(g.nums_actions)
if N != 2
throw(ArgumentError("Implemented only for 2-player games"))
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach is to implement the method only for 2-player games, by

function support_enumeration_task(g::NormalFormGame{2})

Which is more "Julian"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @oyamad's suggestion is more Julian because it leave the door open to implement this routine for games with a different number of players.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. That sounds very cool. I will update it.

* `payoff_matrix2::Matrix{T}`: Payoff matrix of player 2.

# Produces
* `Tuple{Vector{Float64},Vector{Float64}}`: Tuple of Nash equilibrium
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we return (or produce) Tuple{Vector{Rational{Int}},Vector{Rational{Int}}} is T is Rational?

(What if T<:Integer?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for Rational it is easy to do this.

If T<:Integer and I do

julia> A = [1 2; 7 5]
2×2 Array{Int64,2}:
 1  2
 7  5

julia> b = [1, 1]
2-element Array{Int64,1}:
 1
 1

julia> A \ b
2-element Array{Float64,1}:
 -0.333333
  0.666667

julia> sol = Vector{Rational{Int}}(2)
2-element Array{Rational{Int64},1}:
 4658402128//4591878432
 4591878432//4771073360

julia> A_ldiv_B!(sol, factorize(A), b)
2-element Array{Rational{Int64},1}:
 -1501199875790165//4503599627370496
  6004799503160661//9007199254740992

I believe it's just transform Float output into Rational.

How about we create A and b as Rational if T<:Integer, and return the Rational solution?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define A as a matrix of Rationals.

Copy link
Member Author

@shizejin shizejin Mar 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if we define A as matrix of Rationals, then the solution will also be a vector of Rationals. For now I am keeping A and b consistent with T of payoff matrixs. Do you think it would be better to set A and b to be matrix and vector of Rational if the payoff matrixs is of T<:Integer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think so. Keep the eltype of A and b to be T.

For the eltype of sol, try the following:

S = typeof(zero(T)/one(T))
sol = Vector{S}(k+1)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contrary to what is written in the manual, the following doesn't work:

julia> A = [1 2; 7 5//1]
2×2 Array{Rational{Int64},2}:
 1//1  2//1
 7//1  5//1

julia> b = [1, 1//1]
2-element Array{Rational{Int64},1}:
 1//1
 1//1

julia> A_ldiv_B!(factorize(A), b)
2-element Array{Rational{Int64},1}:
 -1//3
  2//3

julia> b
2-element Array{Rational{Int64},1}:
 1//1
 1//1

where the solution should have been contained in b. The same problem occurs with A_ldiv_B!(sol, factorize(A), b) with preallocated sol.

Do you see what is happening? @sglyon @cc7768

A[1:end-1, 1:end-1] = payoff_matrix[own_supp, :][:, opp_supp]
sol = Vector{Float64}(k+1)
try
sol[:] = A \ b
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use A_ldiv_B!, not to allocate a new array every time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean overwriting b, as it is not used later?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am talking about sol.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't A_ldiv_B! also need we first allocate sol in advance? Or do you want to allocate sol just once out of the function _indiff_mixed_action!? I am not quite sure what you mean by "every time".

If you are just worrying about the problem we talked in last meeting, What about just

    A[1:end-1, 1:end-1] = payoff_matrix[own_supp, :][:, opp_supp]
    try
        global sol
        sol = A \ b

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am thinking of allocating sol just once in the loop of _support_enumeration_task and passing it to _indiff_mixed_action!, similarly to A and b.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a great improvement in efficiency.


# Returns
* `:::Vector{Int}`: View of `a`.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be helpful if an "Examples" section is added as in the Python version.

Player([3 2 3; 2 6 1]))
NEs = [([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])]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the indentation.

@oyamad
Copy link
Member

oyamad commented Mar 21, 2017

Regarding #26 (comment), if the eltype of b is not Float, then this method is called and it seems that b itself is not modified.

Anyway, the following should work:

        b = A_ldiv_B!(A, b)

where b is assigned [0, ..., 0, 1] (with type S) just before try and A is LU-factorized by lufact!(A) in the for loop.

@cc7768
Copy link
Member

cc7768 commented Mar 21, 2017

@oyamad This is interesting -- I'm a little surprised it doesn't work for anything but floats. This might be related to the end of this discussion


Compute mixed-action Nash equilibria with equal support size for a
2-player normal form game by support enumeration. For a
non-degenerate game input, these are all Nash equilibria.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question about English:
Should it be better to say "these are all the Nash equilibria" ("the" added) or "all the Nash equilibria are enumerated"? @cc7768 @sglyon
(The same applies to the Python version.)

(In a degenerated game, there may be equilibria that the routine misses to return.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "these are all the Nash equilibria" sounds better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed about adding the "the"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you folks! Corrected the Python version.

@shizejin
Copy link
Member Author

shizejin commented Mar 30, 2017

About #26 (Comment), sometimes lufact!(A) raises InexactError if the eltype of A is Int, as the reason here.

An example:

julia> A = [1 4; 2 3]
2×2 Array{Int64,2}:
 1  4
 2  3

julia> lufact!(A)
ERROR: InexactError()
 in generic_lufact!(::Array{Int64,2}, ::Type{Val{true}}) at ./linalg/lu.jl:55
 in lufact!(::Array{Int64,2}) at ./linalg/lu.jl:22

How about we also set the eltype of A to be S? @oyamad

About the time of LU-factorization of A, as the value of A changes for each time we call _indiff_mixed_action!, maybe it should also be done before try (the same with b)?

@oyamad
Copy link
Member

oyamad commented Mar 31, 2017

Of course, LU factorization involves division, and an int divided by an int is not an int in general. Yes, S = typeof(zero(T)/one(T)) should be the resulting eltype.

m = size(payoff_matrix, 1)
k = length(own_supp)

A[1:end-1, 1:end-1] = payoff_matrix[own_supp, :][:, opp_supp]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't payoff_matrix[own_supp, opp_supp] work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it works.

"""
function _indiff_mixed_action!{T<:Real}(A::Matrix{T}, b::Vector{T},
out::Vector{T},
payoff_matrix::Matrix{T},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the eltype of payoff_matrix need to be the same as that of A?
(I guess the values of payoff_matrix would be converted when assigned to A.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would be automatically converted to be the same with the eltype of A.

I am doing this because the original eltype of payoff_matrix is not going to be used anymore, so just drop it might be more concise. And rather than automatically converting the element of payoff_matrix a lot of time, doing it once here might be more efficient, as I thought.

S = typeof(zero(T)/one(T))
if S != T
payoff_matrices = NTuple{2, Matrix{S}}(payoff_matrices)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this part is necessary.

actions = (Vector{S}(k), Vector{S}(k))
A = Matrix{S}(k+1, k+1)
b = Vector{S}(k+1)
while supps[1][end] < nums_actions[1]+1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess supps[1][end] <= nums_actions[1] will be clearer.


if any(b[1:end-1] .<= 0)
return false
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be any difference if replaced with

for i in 1:k
    b[i] <= zero(T) && return false
end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is better. I thought the for loop will make it inefficient, but the timing results show that your version is even quicker.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about

if findfirst(x -> x<=zero(T), b) != 0
    return false
end

According to the source code of findfirst, it is doing exactly what your code does, and I am wondering if it would be better to use the function that is available in Base.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop will be at least as fast as using the findfirst method with a closure mainly because I've seen type inference issues in cases like this. Under the hood, closures are implemented as new Julia types where the enclosed values become fields of the type. Sometimes type inference doesn't do a perfect job at inferring what the types of these fields should be, slowing the code down by multiple orders of magnitude in some cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explanation in details! I keep with the for loop in the new commitment.

A[1:end-1, end] = -one(T)
A[end, 1:end-1] = one(T)
A[end, end] = zero(T)
b[1:end-1] = zeros(T, k)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to be consistent by just zero(T).

@oyamad
Copy link
Member

oyamad commented Apr 2, 2017

Test cases with Int payoffs and Rational payoffs should be added.

@oyamad
Copy link
Member

oyamad commented May 31, 2017

FYI, the Python version has been revised in QuantEcon/QuantEcon.py#311.

@oyamad
Copy link
Member

oyamad commented Aug 21, 2017

@shizejin Good job, thanks.

@oyamad oyamad merged commit 2986211 into QuantEcon:master Aug 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support enumeration
4 participants