Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions examples/Project.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
ExcelFiles = "89b67f3b-d1aa-5f6f-9ca4-282e8d98620d"
IPG = "2f50017d-522a-4a0a-b317-915d5df6b243"
NormalGames = "4dc104ef-1e0c-4a09-93ea-14ddc8e86204"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f"
StringEncodings = "69024149-9ee7-55f6-a4c4-859efe599b68"

Expand Down
117 changes: 117 additions & 0 deletions examples/max-cov.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using CSV
using DataFrames
using PyCall

py"""
import pickle

def load_pickle(fpath):
with open(fpath, "rb") as f:
data = pickle.load(f)
return data
"""

load_pickle = py"load_pickle"

# these parameters must match those in the csv file name, and the folder from which they are downloaded
type_dataset = "multi"
county_size = 5
num_lakes_per_county = 50
budget_ratio = 0.5

dirname = "EBMC_generated/$(type_dataset)_dataset/"
fname = "$(county_size)_$(num_lakes_per_county)_$(budget_ratio).csv"
df_edge = DataFrame(CSV.File(dirname * fname))

info_data = load_pickle(dirname * "info_data.pickle")

# === Unpack Experiment Settings === #
# extract the value list for the (county_size, num_lakes_per_county, budget_ratio) key
vals = info_data[(county_size, num_lakes_per_county, budget_ratio)]

# values come from Python (0-based originally) so use 1-based Julia indices
counties = vals[1] # likely a sequence of county ids
num_lakes_per_county = Int(vals[2]) # ensure it's an Int
infestation_status = vals[3] # likely a Python dict
county_budget = vals[4]

# determine infested lakes (any value > 0 in the nested dict)
infested_lakes = String[]
for (key, infestation_vals) in infestation_status
if any(v -> v > 0, values(infestation_vals))
push!(infested_lakes, string(key))
end
end

# === lakes and lake->county mapping === #
lakes = unique(vcat(df_edge[:, :dow_origin], df_edge[:, :dow_destination]))
lake_county = Dict(lake => lake[1:2] for lake in lakes) # first two chars like Python's [:2]

# === Compute Lake Weights === #
w = Dict{String, Float64}(lake => 0.0 for lake in lakes)
for row in eachrow(df_edge)
if row[:bij] != 0
ori = string(row[:dow_origin])
dst = string(row[:dow_destination])
w[ori] += row[:bij] * row[:weight]
w[dst] += row[:bij] * row[:weight]
end
end

# === Set Model Parameters === #
I = lakes

I_c = Dict(county => [i for i in I if i[1:2] == county] for county in counties)
I_c_complement = Dict(county => [i for i in I if !(i in I_c[county])] for county in counties)

# build arc dictionaries n (weight) and t (bij)
n = Dict{Tuple{Any,Any}, Float64}()
t = Dict{Tuple{Any,Any}, Float64}()
for row in eachrow(df_edge)
arc = (row[:dow_origin], row[:dow_destination])
n[arc] = row[:weight]
t[arc] = row[:bij]
end

arcs = collect(keys(n))

# arcs within, incoming to, and outgoing from each county
arcs_c = Dict{Any, Vector{Tuple{Any,Any}}}()
arcs_plus_c = Dict{Any, Vector{Tuple{Any,Any}}}()
arcs_minus_c = Dict{Any, Vector{Tuple{Any,Any}}}()

for county in counties
arcs_c[county] = [arc for arc in arcs if (arc[1][1:2] == county) && (arc[2][1:2] == county)]
arcs_plus_c[county] = [arc for arc in arcs if (arc[2][1:2] == county) && (arc[1][1:2] != county)]
arcs_minus_c[county] = [arc for arc in arcs if (arc[1][1:2] == county) && (arc[2][1:2] != county)]
end

# === Define and Solve SELFISH Game using IPG.jl === #

using IPG, SCIP
using IPG.JuMP: Containers

# define players
players = [Player(name=county) for county in counties]

# add variables
x_c = Dict(p => @variable(p.X, [I_c[p.name]], Bin, base_name="x_$(p.name)_") for p in players)
y_c = Dict(p => @variable(p.X, [arcs_minus_c[p.name]], Bin, base_name="y_$(p.name)_") for p in players)

# concatenate x variables
x = Containers.DenseAxisArray(vcat([x_c[p].data for p in players]...), vcat([x_c[p].axes[1] for p in players]...))

for p in players
### add constraints
# TODO: x[arc[i]] may be a variable from another player; need to translate the index
# before using in @constraint. This is a limitation of our implementation, that I am
# currently handling with the internalize_expr method. Ideally, we would have the macro
# overwritten so that the internalization is automatic.
@constraint(p.X, [arc in arcs_minus_c[p.name]], y_c[p][arc] <= IPG.internalize_expr(p, x[arc[1]] + x[arc[2]]))
@constraint(p.X, sum(x_c[p]) <= county_budget[p.name])

### set payoff
set_payoff!(p, sum(t[arc] * n[arc] * y_c[p][arc] for arc in arcs_minus_c[p.name]))
end

Σ, payoff_improvements = SGM(players, SCIP.Optimizer, max_iter=10, verbose=true)
38 changes: 21 additions & 17 deletions src/Game/Player.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,34 +66,38 @@ function _maybe_create_parameter_for_external_var(player::Player, var::VariableR
return player._param_dict[var]
end

function set_payoff!(player::Player, payoff::AbstractJuMPScalar)
_recursive_internalize_expr(expr::Number) = expr
function _recursive_internalize_expr(expr::AbstractJuMPScalar)::AbstractJuMPScalar
if expr isa VariableRef
return _maybe_create_parameter_for_external_var(player, expr)
elseif expr isa AffExpr
internal_terms = typeof(expr.terms)(
function internalize_expr(player::Player, expr::AbstractJuMPScalar)::AbstractJuMPScalar
_recursive_internalize_expr(e::Number) = e
function _recursive_internalize_expr(e::AbstractJuMPScalar)::AbstractJuMPScalar
if e isa VariableRef
return _maybe_create_parameter_for_external_var(player, e)
elseif e isa AffExpr
internal_terms = typeof(e.terms)(
_maybe_create_parameter_for_external_var(player, var) => coeff
for (var, coeff) in expr.terms
for (var, coeff) in e.terms
)
return AffExpr(expr.constant, internal_terms)
elseif expr isa QuadExpr
internal_terms = typeof(expr.terms)(
return AffExpr(e.constant, internal_terms)
elseif e isa QuadExpr
internal_terms = typeof(e.terms)(
UnorderedPair{VariableRef}(
_maybe_create_parameter_for_external_var(player, vars.a),
_maybe_create_parameter_for_external_var(player, vars.b)
) => coeff
for (vars, coeff) in expr.terms
for (vars, coeff) in e.terms
)
return QuadExpr(_recursive_internalize_expr(expr.aff), internal_terms)
elseif expr isa NonlinearExpr
return NonlinearExpr(expr.head, Vector{Any}(map(_recursive_internalize_expr, expr.args)))
return QuadExpr(_recursive_internalize_expr(e.aff), internal_terms)
elseif e isa NonlinearExpr
return NonlinearExpr(e.head, Vector{Any}(map(_recursive_internalize_expr, e.args)))
else
error("Unsupported expression type: $(typeof(expr))")
error("Unsupported expression type: $(typeof(e))")
end
end

player.Π = _recursive_internalize_expr(payoff)
return _recursive_internalize_expr(expr)
end

function set_payoff!(player::Player, payoff::AbstractJuMPScalar)
player.Π = internalize_expr(player, payoff)
end
function set_payoff!(player::Player, payoff::Real)
player.Π = AffExpr(payoff)
Expand Down
12 changes: 10 additions & 2 deletions src/SGM/PolymatrixGame/Polymatrix.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,20 @@ function compute_bilateral_payoff(Π::QuadExpr, v_bar_p::AssignmentDict, v_bar_k

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

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

return compute_bilateral_payoff(p.Π, v_bar_p, v_bar_k)
if length(v_bar_k) == 0
# if there are no variables from player k in p's payoff, there's no influence
return 0.0
else
return compute_bilateral_payoff(p.Π, v_bar_p, v_bar_k)
end
end

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

# compute utility of each player `p` using strategy `i_p` against player `k` using strategy `i_k`
# compute utility of each player `p` using the `i_p`-th strategy against player `k`
# using the `i_k`-th strategy
for p in players
for k in players
polymatrix[p,k] = zeros(length(S_X[p]), length(S_X[k]))
Expand Down