diff --git a/examples/Project.toml b/examples/Project.toml index 454baa8..119fdab 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -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" diff --git a/examples/max-cov.jl b/examples/max-cov.jl new file mode 100644 index 0000000..5eb31cb --- /dev/null +++ b/examples/max-cov.jl @@ -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) diff --git a/src/Game/Player.jl b/src/Game/Player.jl index 154de24..c6245f5 100644 --- a/src/Game/Player.jl +++ b/src/Game/Player.jl @@ -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) diff --git a/src/SGM/PolymatrixGame/Polymatrix.jl b/src/SGM/PolymatrixGame/Polymatrix.jl index 21c3fb8..9ed6180 100644 --- a/src/SGM/PolymatrixGame/Polymatrix.jl +++ b/src/SGM/PolymatrixGame/Polymatrix.jl @@ -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." @@ -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]))