Skip to content

Commit fc11e08

Browse files
External variables as parameters; evaluate NonlinearExpr through recursive replace
Parameters
2 parents 6461c29 + 255e99b commit fc11e08

File tree

10 files changed

+485
-174
lines changed

10 files changed

+485
-174
lines changed

src/Game/Assignment.jl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
const AssignmentDict = Dict{VariableRef,Float64}
3+
4+
"""
5+
Create a dictionary of variable assignments (JuMP-style) from a pure strategy.
6+
"""
7+
Assignment(player::Player, x::PureStrategy) = AssignmentDict(zip(all_variables(player), x))
8+
Assignment(x::Profile{PureStrategy})::AssignmentDict = merge(collect(Assignment(p, x_p) for (p, x_p) in x)...)
9+
export Assignment
10+
11+
"Replace the variables in an expression with their assigned values."
12+
function replace(expr::AbstractJuMPScalar, assignment::AssignmentDict)
13+
_recursive_replace(expr::Number) = expr
14+
function _recursive_replace(expr::AbstractJuMPScalar)
15+
if expr isa VariableRef
16+
return get(assignment, expr, expr)
17+
elseif expr isa AffExpr
18+
# TODO: this behavior of value is unintended and may lead to problems in the future
19+
return value(v -> get(assignment, v, v), expr)
20+
elseif expr isa QuadExpr
21+
return value(v -> get(assignment, v, v), expr)
22+
elseif expr isa NonlinearExpr
23+
replaced_expr = NonlinearExpr(expr.head, Vector{Any}(map(_recursive_replace, expr.args)))
24+
# If there are no nonlinear arguments, we can try to simplify the resulting expression
25+
if !any(arg isa NonlinearExpr for arg in replaced_expr.args)
26+
# TODO: the following is to be replaced by JuMP.simplify once https://github.com/jump-dev/JuMP.jl/pull/4047 gets merged
27+
g = MOI.Nonlinear.SymbolicAD.simplify(moi_function(replaced_expr))
28+
29+
# TODO: this owner model assignment is really ugly. maybe the owner model should be an argument
30+
terms_in_replaced_expr = filter(v -> v isa JuMP.AbstractJuMPScalar, replaced_expr.args)
31+
owner = nothing
32+
for term in terms_in_replaced_expr
33+
term_owner = owner_model(term)
34+
if ~isnothing(term_owner)
35+
owner = term_owner
36+
break
37+
end
38+
end
39+
40+
if isnothing(owner)
41+
# Explicitly handle the case where g is not a JuMP expression
42+
if g isa Number
43+
replaced_expr = g
44+
else
45+
error("replace: Unable to convert simplified expression to a number or JuMP expression. Got: $(typeof(g))")
46+
end
47+
else
48+
# update owner model
49+
replaced_expr = jump_function(owner, g)
50+
end
51+
end
52+
return replaced_expr
53+
else
54+
return expr
55+
end
56+
end
57+
58+
return _recursive_replace(expr)
59+
end
60+
61+
"Translate variable references of the assignment to internal references."
62+
function _internalize_assignment(player::Player, assignment::AssignmentDict)
63+
internal_assignment = AssignmentDict()
64+
for (v_ref, v_val) in assignment
65+
if v_ref all_variables(player.X)
66+
internal_assignment[v_ref] = v_val
67+
elseif v_ref keys(player._param_dict)
68+
internal_assignment[player._param_dict[v_ref]] = v_val
69+
end
70+
end
71+
72+
return internal_assignment
73+
end

src/Game/BestResponse.jl

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

22
"Compute `player`'s best response to the mixed strategy profile `σ_others`."
33
function best_response(player::Player, σ_others::Profile{DiscreteMixedStrategy})
4-
vars_player = all_variables(player.X)
4+
@assert player ̸ keys(σ_others) "Player must not be in the profile of others."
55

6-
obj = payoff(player, vars_player, σ_others)
6+
obj = expected_value(x_others -> replace_in_payoff(player, Assignment(x_others)), σ_others)
77

88
# I don't know why, but it was raising an error without changing the sense to feasibility first
99
set_objective_sense(player.X, JuMP.MOI.FEASIBILITY_SENSE)
@@ -12,32 +12,8 @@ function best_response(player::Player, σ_others::Profile{DiscreteMixedStrategy}
1212
set_silent(player.X)
1313
optimize!(player.X)
1414

15-
return value.(vars_player)
15+
return value.(all_variables(player))
1616
end
17+
1718
"Compute `player`'s best response to the pure strategy profile `x_others`."
1819
best_response(player::Player, x_others::Profile{PureStrategy}) = best_response(player, convert(Profile{DiscreteMixedStrategy}, x_others))
19-
# function best_response(player::Player{<:AbstractBilateralPayoff}, σ::Vector{DiscreteMixedStrategy})
20-
# error("best_response for player with bilateral payoff not implemented yet") # TODO
21-
22-
# xp = all_variables(player.X)
23-
24-
# # TODO: No idea why this doesn't work
25-
# # @objective(model, Max, sum([IPG.bilateral_payoff(Πp, p, xp, k, σ[k]) for k in 1:m]))
26-
27-
# obj = AffExpr()
28-
# for k in eachindex(σ)
29-
# if k == player.p
30-
# obj += IPG.bilateral_payoff(player.Π, xp)
31-
# else
32-
# obj += IPG.bilateral_payoff(player.Π, xp, σ[k], k)
33-
# end
34-
# end
35-
# # I don't know why, but it was raising an error without changing the sense to feasibility first
36-
# set_objective_sense(player.X, JuMP.MOI.FEASIBILITY_SENSE)
37-
# @objective(player.X, JuMP.MOI.MAX_SENSE, obj)
38-
39-
# set_silent(player.X)
40-
# optimize!(player.X)
41-
42-
# return value.(xp)
43-
# end

src/Game/Game.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
include("Strategies.jl")
33
include("Player.jl")
44
include("Profile.jl")
5+
include("Assignment.jl")
56
include("Payoff.jl")
67
include("BestResponse.jl")

src/Game/Payoff.jl

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,32 @@
11

2-
"""
3-
Create a dictionary of variable assignments (JuMP-style) from a pure strategy.
4-
"""
5-
function build_var_assignments(player::Player, x::Vector{<:Any})
6-
variable_assignments = Dict{VariableRef, Any}()
7-
for (v, val) in zip(all_variables(player.X), x)
8-
variable_assignments[v] = val
9-
end
10-
return variable_assignments
2+
function replace_in_payoff(player::Player, assignment::AssignmentDict)
3+
internal_assignment = _internalize_assignment(player, assignment)
4+
return replace(player.Π, internal_assignment)
115
end
126

137
"""
148
Get the payoff map for `player` given the pure strategy profile `x_others`.
159
The payoff map is a function that takes the player's strategy and returns the payoff.
1610
"""
1711
function get_payoff_map(player::Player, x_others::Profile{PureStrategy})
18-
others_var_assignments = [build_var_assignments(other, x_other)
19-
for (other, x_other) in x_others]
20-
var_assignments = merge(others_var_assignments...)
21-
22-
function payoff_map(x_player::Vector{<:Any})
23-
complete_var_assignments = merge(var_assignments, build_var_assignments(player, x_player))
12+
assignment_others = Assignment(x_others)
13+
internal_assignment_others = _internalize_assignment(player, assignment_others)
2414

25-
return value(v -> complete_var_assignments[v], player.Π)
15+
function payoff_map(x_player::Vector{Float64})
16+
complete_assignment = merge(internal_assignment_others, Assignment(player, x_player))
17+
return value(v -> complete_assignment[v], player.Π)
2618
end
2719

2820
return payoff_map
2921
end
3022

3123
"Evaluate the player's payoff when she plays `x_player` and the others play `x_others`."
32-
function payoff(player::Player, x_player::Vector{<:Any}, x_others::Profile{PureStrategy})
24+
function payoff(player::Player, x_player::PureStrategy, x_others::Profile{PureStrategy})
3325
return get_payoff_map(player, x_others)(x_player)
3426
end
3527

3628
"Expected payoff of a pure strategy (`x_player`) against a mixed profile (`σ_others`)."
37-
function payoff(player::Player, x_player::Vector{<:Any}, σ_others::Profile{DiscreteMixedStrategy})
29+
function payoff(player::Player, x_player::PureStrategy, σ_others::Profile{DiscreteMixedStrategy})
3830
return expected_value(x_others -> payoff(player, x_player, x_others), σ_others)
3931
end
4032

src/Game/Player.jl

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,70 @@
11
using JuMP, JSON3
22

3+
const VarToParamDict = Dict{VariableRef,VariableRef}
4+
35
"A player in an IPG."
46
mutable struct Player
57
"Strategy space."
68
X::Model
79
# TODO: using value(...) to manipulate expressions does not work for NonlinearExpr, see https://github.com/jump-dev/JuMP.jl/issues/4044 for an appropriate solution (another huge refactor))
810
"Payoff expression."
911
Π::AbstractJuMPScalar
12+
_param_dict::VarToParamDict
1013
end
11-
function Player()
12-
return Player(Model(), AffExpr(0))
14+
Player() = Player(Model(), AffExpr(NaN), VarToParamDict())
15+
Player() = Player(Model(), AffExpr(0.0), VarToParamDict())
16+
Player(X::Model) = Player(X, AffExpr(0.0), VarToParamDict())
17+
function Player(X::Model, Π::AbstractJuMPScalar)
18+
player = Player(X)
19+
set_payoff!(player, Π)
20+
return player
1321
end
1422
export Player
1523

24+
JuMP.all_variables(p::Player) = filter(v -> !is_parameter(v), all_variables(p.X))
25+
26+
"Maps external variables to internal parameters. Creates a new parameter if it does not exist."
27+
function _maybe_create_parameter_for_external_var(player::Player, var::VariableRef)::VariableRef
28+
var all_variables(player.X) && return var
29+
30+
if !haskey(player._param_dict, var)
31+
# create anonymous parameter with the same name as the variable
32+
param = @variable(player.X, base_name=name(var), set=Parameter(1))
33+
34+
player._param_dict[var] = param
35+
end
36+
37+
return player._param_dict[var]
38+
end
39+
1640
function set_payoff!(player::Player, payoff::AbstractJuMPScalar)
17-
player.Π = payoff
41+
_recursive_internalize_expr(expr::Number) = expr
42+
function _recursive_internalize_expr(expr::AbstractJuMPScalar)::AbstractJuMPScalar
43+
if expr isa VariableRef
44+
return _maybe_create_parameter_for_external_var(player, expr)
45+
elseif expr isa AffExpr
46+
internal_terms = typeof(expr.terms)(
47+
_maybe_create_parameter_for_external_var(player, var) => coeff
48+
for (var, coeff) in expr.terms
49+
)
50+
return AffExpr(expr.constant, internal_terms)
51+
elseif expr isa QuadExpr
52+
internal_terms = typeof(expr.terms)(
53+
UnorderedPair{VariableRef}(
54+
_maybe_create_parameter_for_external_var(player, vars.a),
55+
_maybe_create_parameter_for_external_var(player, vars.b)
56+
) => coeff
57+
for (vars, coeff) in expr.terms
58+
)
59+
return QuadExpr(_recursive_internalize_expr(expr.aff), internal_terms)
60+
elseif expr isa NonlinearExpr
61+
return NonlinearExpr(expr.head, Vector{Any}(map(_recursive_internalize_expr, expr.args)))
62+
else
63+
error("Unsupported expression type: $(typeof(expr))")
64+
end
65+
end
66+
67+
player.Π = _recursive_internalize_expr(payoff)
1868
end
1969
function set_payoff!(player::Player, payoff::Real)
2070
player.Π = AffExpr(payoff)
@@ -39,7 +89,7 @@ function find_feasible_pure_strategy(player::Player)::PureStrategy
3989
set_silent(player.X)
4090
optimize!(player.X)
4191

42-
return value.(all_variables(player.X))
92+
return value.(all_variables(player))
4393
end
4494

4595
"Solve the feasibility problem of all players, returning a feasible profile."

src/SGM/DeviationReaction.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ function find_deviation_best_response(players::Vector{Player}, σ::Profile{Discr
55

66
for p in player_order
77
player = players[p]
8-
new_x_p = best_response(player, σ)
98
σ_others = others(σ, player)
109

10+
new_x_p = best_response(player, σ_others)
11+
1112
payoff_improvement = payoff(player, new_x_p, σ_others) - payoff(player, σ)
1213
if payoff_improvement > dev_tol
1314
return payoff_improvement, player, new_x_p

src/SGM/Initialization.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ empty_S_X(players::Vector{Player}) = Dict{Player, Vector{PureStrategy}}(p => Vec
55
function initialize_strategies_feasibility(players::Vector{Player})
66
S_X = empty_S_X(players)
77
for player in players
8-
xp_init = start_value.(all_variables(player.X))
8+
xp_init = start_value.(all_variables(player))
99

1010
if nothing in xp_init
1111
# TODO: if `initial_sol` is just a partial solution, I could fix its values
@@ -24,13 +24,13 @@ function initialize_strategies_player_alone(players::Vector{Player})
2424
S_X = empty_S_X(players)
2525

2626
# profile that simulates players being alone (all others play 0)
27-
x_others_dummy = Profile{PureStrategy}(player => zeros(length(all_variables(player.X))) for player in players)
27+
x_dummy = Profile{PureStrategy}(player => zeros(length(all_variables(player))) for player in players)
2828

2929
for player in players
30-
xp_init = start_value.(all_variables(player.X))
30+
xp_init = start_value.(all_variables(player))
3131

3232
if nothing in xp_init
33-
xp_init = best_response(player, x_others_dummy)
33+
xp_init = best_response(player, others(x_dummy, player))
3434
end
3535

3636
push!(S_X[player], xp_init)

src/SGM/PolymatrixGame/Polymatrix.jl

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,60 @@ using LinearAlgebra
33

44
const Polymatrix = Dict{Tuple{Player, Player}, Matrix{Float64}}
55

6-
"Compute the component of the payoff that doesn't depend on other players."
7-
function compute_self_payoff(p::Player, x_p::PureStrategy)
8-
var_assignments_p = build_var_assignments(p, x_p)
96

10-
self_linear_payoff = sum(get(p.Π.aff.terms, v, 0) * var_assignments_p[v] for v in all_variables(p.X))
7+
function compute_self_payoff::AffExpr, v_bar::AssignmentDict)::Float64
118
# note the get() may be necessary as there may not be terms for all variables
12-
self_affine_payoff = p.Π.aff.constant + self_linear_payoff
9+
self_linear_payoff = sum(get.terms, ref, 0) * val for (ref, val) in v_bar)
10+
11+
# constant is here by convention (inherited from NormalGames.jl)
12+
return Π.constant + self_linear_payoff
13+
end
14+
15+
function compute_self_payoff::QuadExpr, v_bar::AssignmentDict)::Float64
1316
# TODO: maybe Dict{VariableRef, Number} should be the standard for assignments
14-
self_quad_payoff = sum(get(p.Π.terms, UnorderedPair(v,v), 0) * var_assignments_p[v]^2 for v in all_variables(p.X))
17+
self_quad_payoff = sum(get.terms, UnorderedPair(ref,ref), 0) * val^2 for (ref, val) in v_bar)
18+
19+
return self_quad_payoff + compute_self_payoff.aff, v_bar)
20+
end
21+
22+
"Compute the component of the payoff that doesn't depend on other players."
23+
function compute_self_payoff(p::Player, x_p::PureStrategy)
24+
v_bar = Assignment(p, x_p)
1525

16-
return self_affine_payoff + self_quad_payoff
26+
return compute_self_payoff(p.Π, v_bar)
27+
end
28+
29+
"Compute player p's payoff component from some *other* player playing v_bar_k."
30+
function compute_others_payoff::AffExpr, v_bar_k::AssignmentDict)::Float64
31+
# TODO: identical to compute_self_payoff(::AffExpr, ::AssignmentDict), except for the constant. I could refactor
32+
return sum( # terms of the form q_j * xk_j, where xk_j belongs to player k
33+
get.terms, ref, 0) * val for (ref, val) in v_bar_k
34+
)
35+
end
36+
37+
"Compute player p's payoff component from her playing v_bar_p and some *other* player playing v_bar_k."
38+
function compute_bilateral_payoff::QuadExpr, v_bar_p::AssignmentDict, v_bar_k::AssignmentDict)::Float64
39+
mixed_components = sum( # terms of the form q_ij * xp_i * xk_j, where xp_i belongs to player p and xk_j belongs to player k
40+
get.terms, UnorderedPair(ref_i,ref_j), 0) * val_i * val_j
41+
for (ref_i, val_i) in v_bar_p
42+
for (ref_j, val_j) in v_bar_k
43+
)
44+
other_components = sum( # terms of the form q_ij * xk_i * xk_j, where xk_i,xk_j belong to player k
45+
get.terms, UnorderedPair(ref_i,ref_j), 0) * val_i * val_j
46+
for (ref_i, val_i) in v_bar_k
47+
for (ref_j, val_j) in v_bar_k
48+
)
49+
other_components = other_components / 2 # I'm iterating over al possible unordered pairs twice!
50+
51+
return mixed_components + other_components + compute_others_payoff.aff, v_bar_k)
1752
end
1853

1954
function compute_bilateral_payoff(p::Player, x_p::PureStrategy, k::Player, x_k::PureStrategy)
20-
var_assignments_k = build_var_assignments(k, x_k)
21-
var_assignments_p = build_var_assignments(p, x_p) # TODO: this could be cached.
2255
# In fact, +1 for having Dict{VariableRef, Number} as the standard for assignments
56+
v_bar_p = Assignment(p, x_p) # TODO: this could be cached.
57+
v_bar_k = _internalize_assignment(p, Assignment(k, x_k))
2358

24-
return sum(
25-
get(p.Π.terms, UnorderedPair(vp,vk), 0) * var_assignments_p[vp] * var_assignments_k[vk]
26-
for vp in all_variables(p.X), vk in all_variables(k.X)
27-
)
59+
return compute_bilateral_payoff(p.Π, v_bar_p, v_bar_k)
2860
end
2961

3062
"Compute polymatrix for normal form game from sample of strategies."

src/SGM/SGM.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ function SGM(players::Vector{Player}, optimizer_factory; max_iter=100, dev_tol=1
2020
# the strategy space of each player or, in case there is none, a feasibility problem is
2121
# solved
2222

23-
# TODO: I should use S_X for the sampled game, rather than the strategies
2423
S_X = initialize_strategies(players)
2524
sampled_game = PolymatrixSampledGame(players, S_X)
2625
verbose && println("Game initialized with strategies: ", S_X)

0 commit comments

Comments
 (0)