Skip to content

Commit 5a53af9

Browse files
Merge pull request #7 from brunompacheco/max-cov-example
Max cov example
2 parents f7c1265 + e63da71 commit 5a53af9

File tree

4 files changed

+150
-19
lines changed

4 files changed

+150
-19
lines changed

examples/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[deps]
2+
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
23
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
34
ExcelFiles = "89b67f3b-d1aa-5f6f-9ca4-282e8d98620d"
45
IPG = "2f50017d-522a-4a0a-b317-915d5df6b243"
56
NormalGames = "4dc104ef-1e0c-4a09-93ea-14ddc8e86204"
67
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
8+
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
79
SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f"
810
StringEncodings = "69024149-9ee7-55f6-a4c4-859efe599b68"
911

examples/max-cov.jl

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using CSV
2+
using DataFrames
3+
using PyCall
4+
5+
py"""
6+
import pickle
7+
8+
def load_pickle(fpath):
9+
with open(fpath, "rb") as f:
10+
data = pickle.load(f)
11+
return data
12+
"""
13+
14+
load_pickle = py"load_pickle"
15+
16+
# these parameters must match those in the csv file name, and the folder from which they are downloaded
17+
type_dataset = "multi"
18+
county_size = 5
19+
num_lakes_per_county = 50
20+
budget_ratio = 0.5
21+
22+
dirname = "EBMC_generated/$(type_dataset)_dataset/"
23+
fname = "$(county_size)_$(num_lakes_per_county)_$(budget_ratio).csv"
24+
df_edge = DataFrame(CSV.File(dirname * fname))
25+
26+
info_data = load_pickle(dirname * "info_data.pickle")
27+
28+
# === Unpack Experiment Settings === #
29+
# extract the value list for the (county_size, num_lakes_per_county, budget_ratio) key
30+
vals = info_data[(county_size, num_lakes_per_county, budget_ratio)]
31+
32+
# values come from Python (0-based originally) so use 1-based Julia indices
33+
counties = vals[1] # likely a sequence of county ids
34+
num_lakes_per_county = Int(vals[2]) # ensure it's an Int
35+
infestation_status = vals[3] # likely a Python dict
36+
county_budget = vals[4]
37+
38+
# determine infested lakes (any value > 0 in the nested dict)
39+
infested_lakes = String[]
40+
for (key, infestation_vals) in infestation_status
41+
if any(v -> v > 0, values(infestation_vals))
42+
push!(infested_lakes, string(key))
43+
end
44+
end
45+
46+
# === lakes and lake->county mapping === #
47+
lakes = unique(vcat(df_edge[:, :dow_origin], df_edge[:, :dow_destination]))
48+
lake_county = Dict(lake => lake[1:2] for lake in lakes) # first two chars like Python's [:2]
49+
50+
# === Compute Lake Weights === #
51+
w = Dict{String, Float64}(lake => 0.0 for lake in lakes)
52+
for row in eachrow(df_edge)
53+
if row[:bij] != 0
54+
ori = string(row[:dow_origin])
55+
dst = string(row[:dow_destination])
56+
w[ori] += row[:bij] * row[:weight]
57+
w[dst] += row[:bij] * row[:weight]
58+
end
59+
end
60+
61+
# === Set Model Parameters === #
62+
I = lakes
63+
64+
I_c = Dict(county => [i for i in I if i[1:2] == county] for county in counties)
65+
I_c_complement = Dict(county => [i for i in I if !(i in I_c[county])] for county in counties)
66+
67+
# build arc dictionaries n (weight) and t (bij)
68+
n = Dict{Tuple{Any,Any}, Float64}()
69+
t = Dict{Tuple{Any,Any}, Float64}()
70+
for row in eachrow(df_edge)
71+
arc = (row[:dow_origin], row[:dow_destination])
72+
n[arc] = row[:weight]
73+
t[arc] = row[:bij]
74+
end
75+
76+
arcs = collect(keys(n))
77+
78+
# arcs within, incoming to, and outgoing from each county
79+
arcs_c = Dict{Any, Vector{Tuple{Any,Any}}}()
80+
arcs_plus_c = Dict{Any, Vector{Tuple{Any,Any}}}()
81+
arcs_minus_c = Dict{Any, Vector{Tuple{Any,Any}}}()
82+
83+
for county in counties
84+
arcs_c[county] = [arc for arc in arcs if (arc[1][1:2] == county) && (arc[2][1:2] == county)]
85+
arcs_plus_c[county] = [arc for arc in arcs if (arc[2][1:2] == county) && (arc[1][1:2] != county)]
86+
arcs_minus_c[county] = [arc for arc in arcs if (arc[1][1:2] == county) && (arc[2][1:2] != county)]
87+
end
88+
89+
# === Define and Solve SELFISH Game using IPG.jl === #
90+
91+
using IPG, SCIP
92+
using IPG.JuMP: Containers
93+
94+
# define players
95+
players = [Player(name=county) for county in counties]
96+
97+
# add variables
98+
x_c = Dict(p => @variable(p.X, [I_c[p.name]], Bin, base_name="x_$(p.name)_") for p in players)
99+
y_c = Dict(p => @variable(p.X, [arcs_minus_c[p.name]], Bin, base_name="y_$(p.name)_") for p in players)
100+
101+
# concatenate x variables
102+
x = Containers.DenseAxisArray(vcat([x_c[p].data for p in players]...), vcat([x_c[p].axes[1] for p in players]...))
103+
104+
for p in players
105+
### add constraints
106+
# TODO: x[arc[i]] may be a variable from another player; need to translate the index
107+
# before using in @constraint. This is a limitation of our implementation, that I am
108+
# currently handling with the internalize_expr method. Ideally, we would have the macro
109+
# overwritten so that the internalization is automatic.
110+
@constraint(p.X, [arc in arcs_minus_c[p.name]], y_c[p][arc] <= IPG.internalize_expr(p, x[arc[1]] + x[arc[2]]))
111+
@constraint(p.X, sum(x_c[p]) <= county_budget[p.name])
112+
113+
### set payoff
114+
set_payoff!(p, sum(t[arc] * n[arc] * y_c[p][arc] for arc in arcs_minus_c[p.name]))
115+
end
116+
117+
Σ, payoff_improvements = SGM(players, SCIP.Optimizer, max_iter=10, verbose=true)

src/Game/Player.jl

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,34 +66,38 @@ function _maybe_create_parameter_for_external_var(player::Player, var::VariableR
6666
return player._param_dict[var]
6767
end
6868

69-
function set_payoff!(player::Player, payoff::AbstractJuMPScalar)
70-
_recursive_internalize_expr(expr::Number) = expr
71-
function _recursive_internalize_expr(expr::AbstractJuMPScalar)::AbstractJuMPScalar
72-
if expr isa VariableRef
73-
return _maybe_create_parameter_for_external_var(player, expr)
74-
elseif expr isa AffExpr
75-
internal_terms = typeof(expr.terms)(
69+
function internalize_expr(player::Player, expr::AbstractJuMPScalar)::AbstractJuMPScalar
70+
_recursive_internalize_expr(e::Number) = e
71+
function _recursive_internalize_expr(e::AbstractJuMPScalar)::AbstractJuMPScalar
72+
if e isa VariableRef
73+
return _maybe_create_parameter_for_external_var(player, e)
74+
elseif e isa AffExpr
75+
internal_terms = typeof(e.terms)(
7676
_maybe_create_parameter_for_external_var(player, var) => coeff
77-
for (var, coeff) in expr.terms
77+
for (var, coeff) in e.terms
7878
)
79-
return AffExpr(expr.constant, internal_terms)
80-
elseif expr isa QuadExpr
81-
internal_terms = typeof(expr.terms)(
79+
return AffExpr(e.constant, internal_terms)
80+
elseif e isa QuadExpr
81+
internal_terms = typeof(e.terms)(
8282
UnorderedPair{VariableRef}(
8383
_maybe_create_parameter_for_external_var(player, vars.a),
8484
_maybe_create_parameter_for_external_var(player, vars.b)
8585
) => coeff
86-
for (vars, coeff) in expr.terms
86+
for (vars, coeff) in e.terms
8787
)
88-
return QuadExpr(_recursive_internalize_expr(expr.aff), internal_terms)
89-
elseif expr isa NonlinearExpr
90-
return NonlinearExpr(expr.head, Vector{Any}(map(_recursive_internalize_expr, expr.args)))
88+
return QuadExpr(_recursive_internalize_expr(e.aff), internal_terms)
89+
elseif e isa NonlinearExpr
90+
return NonlinearExpr(e.head, Vector{Any}(map(_recursive_internalize_expr, e.args)))
9191
else
92-
error("Unsupported expression type: $(typeof(expr))")
92+
error("Unsupported expression type: $(typeof(e))")
9393
end
9494
end
9595

96-
player.Π = _recursive_internalize_expr(payoff)
96+
return _recursive_internalize_expr(expr)
97+
end
98+
99+
function set_payoff!(player::Player, payoff::AbstractJuMPScalar)
100+
player.Π = internalize_expr(player, payoff)
97101
end
98102
function set_payoff!(player::Player, payoff::Real)
99103
player.Π = AffExpr(payoff)

src/SGM/PolymatrixGame/Polymatrix.jl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,20 @@ function compute_bilateral_payoff(Π::QuadExpr, v_bar_p::AssignmentDict, v_bar_k
5050

5151
return mixed_components + other_components + compute_others_payoff(Π.aff, v_bar_k)
5252
end
53+
# for affine expressions, the bilateral payoff does not depend on the player's own strategy
54+
compute_bilateral_payoff(Π::AffExpr, v_bar_p::AssignmentDict, v_bar_k::AssignmentDict)::Float64 = compute_others_payoff(Π, v_bar_k)
5355

5456
function compute_bilateral_payoff(p::Player, x_p::PureStrategy, k::Player, x_k::PureStrategy)
5557
# In fact, +1 for having Dict{VariableRef, Number} as the standard for assignments
5658
v_bar_p = Assignment(p, x_p) # TODO: this could be cached.
5759
v_bar_k = _internalize_assignment(p, Assignment(k, x_k))
5860

59-
return compute_bilateral_payoff(p.Π, v_bar_p, v_bar_k)
61+
if length(v_bar_k) == 0
62+
# if there are no variables from player k in p's payoff, there's no influence
63+
return 0.0
64+
else
65+
return compute_bilateral_payoff(p.Π, v_bar_p, v_bar_k)
66+
end
6067
end
6168

6269
"Compute polymatrix for normal form game from sample of strategies."
@@ -74,7 +81,8 @@ function get_polymatrix_bilateral(players::Vector{Player}, S_X::Dict{Player, Vec
7481
# TODO: we could have the payoff type as a Player parameter, so that we can filter that out straight away
7582
polymatrix = Polymatrix()
7683

77-
# compute utility of each player `p` using strategy `i_p` against player `k` using strategy `i_k`
84+
# compute utility of each player `p` using the `i_p`-th strategy against player `k`
85+
# using the `i_k`-th strategy
7886
for p in players
7987
for k in players
8088
polymatrix[p,k] = zeros(length(S_X[p]), length(S_X[k]))

0 commit comments

Comments
 (0)