From f683b490109aec17e91b01c912f20b73712fea81 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 00:48:07 +0000 Subject: [PATCH 1/8] WIP: maximum coverage game --- examples/Project.toml | 2 + examples/max-cov.jl | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 examples/max-cov.jl 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..13c7347 --- /dev/null +++ b/examples/max-cov.jl @@ -0,0 +1,108 @@ +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 = "single" +county_size = 2 +num_lakes_per_county = 50 +budget_ratio = 0.3 + +df_edge = DataFrame(CSV.File("examples/2_50_0.3.csv")) +info_data = load_pickle("examples/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 Game using IPG.jl === # + +using IPG, SCIP + +# 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], 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]...)) + +# add constraints +for p in players + # TODO: x[arc[i]] may be a variable from another player; need to translate the index + # before using in @constraint. This is a HUGE limitation of our implementation. I'll + # probably need to overwrite @constraint to handle this properly. For now, I will use + # the private internal methods to translate VariableRef's. + @constraint(p.X, [arc in arcs], y_c[p][arc] <= x[arc[1]] + x[arc[2]]) + @constraint(p.X, sum(x_c[p]) <= county_budget[p.name]) +end From 940a8132e5709aecb7c402bcca26fc66a7cb2339 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 19:00:57 +0000 Subject: [PATCH 2/8] refactor expression internalization outwards of payoff setting --- src/Game/Player.jl | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Game/Player.jl b/src/Game/Player.jl index 154de24..0b235a6 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) + player.Π = _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) From bf77a0800865a07ca62ee248951a5784cee71049 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 19:30:49 +0000 Subject: [PATCH 3/8] WIP: working for 2p games --- examples/max-cov.jl | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/max-cov.jl b/examples/max-cov.jl index 13c7347..ae43c9a 100644 --- a/examples/max-cov.jl +++ b/examples/max-cov.jl @@ -19,8 +19,11 @@ county_size = 2 num_lakes_per_county = 50 budget_ratio = 0.3 -df_edge = DataFrame(CSV.File("examples/2_50_0.3.csv")) -info_data = load_pickle("examples/info_data.pickle") +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 @@ -83,26 +86,32 @@ for county in counties arcs_minus_c[county] = [arc for arc in arcs if (arc[1][1:2] == county) && (arc[2][1:2] != county)] end -# === Define and Solve Game using IPG.jl === # +# === 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], Bin, base_name="y_$(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]...)) -# add constraints 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 HUGE limitation of our implementation. I'll - # probably need to overwrite @constraint to handle this properly. For now, I will use - # the private internal methods to translate VariableRef's. - @constraint(p.X, [arc in arcs], y_c[p][arc] <= x[arc[1]] + x[arc[2]]) + # 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) From 8f7c6f492139b0905adb65414162098ef020d31e Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 19:31:20 +0000 Subject: [PATCH 4/8] fix: bilateral payoff with affexpr was not auto casted to quadexpr --- src/SGM/PolymatrixGame/Polymatrix.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SGM/PolymatrixGame/Polymatrix.jl b/src/SGM/PolymatrixGame/Polymatrix.jl index 21c3fb8..ed127b9 100644 --- a/src/SGM/PolymatrixGame/Polymatrix.jl +++ b/src/SGM/PolymatrixGame/Polymatrix.jl @@ -50,6 +50,7 @@ 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 +compute_bilateral_payoff(Π::AffExpr, v_bar_p::AssignmentDict, v_bar_k::AssignmentDict)::Float64 = compute_bilateral_payoff(QuadExpr(Π), v_bar_p, 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 From fe59eb56710ee5ca2120a991fb2850ef02752e66 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 20:25:27 +0000 Subject: [PATCH 5/8] fix: computing bilateral payoff when x_other has no influence --- src/SGM/PolymatrixGame/Polymatrix.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/SGM/PolymatrixGame/Polymatrix.jl b/src/SGM/PolymatrixGame/Polymatrix.jl index ed127b9..8a37e10 100644 --- a/src/SGM/PolymatrixGame/Polymatrix.jl +++ b/src/SGM/PolymatrixGame/Polymatrix.jl @@ -50,14 +50,19 @@ 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 -compute_bilateral_payoff(Π::AffExpr, v_bar_p::AssignmentDict, v_bar_k::AssignmentDict)::Float64 = compute_bilateral_payoff(QuadExpr(Π), v_bar_p, v_bar_k) +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." @@ -75,7 +80,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])) From b62a1c0914522e8298e83975ecb25ed9c11314c8 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 20:26:15 +0000 Subject: [PATCH 6/8] maxcov works for larger problems --- examples/max-cov.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/max-cov.jl b/examples/max-cov.jl index ae43c9a..5eb31cb 100644 --- a/examples/max-cov.jl +++ b/examples/max-cov.jl @@ -14,10 +14,10 @@ def load_pickle(fpath): 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 = "single" -county_size = 2 +type_dataset = "multi" +county_size = 5 num_lakes_per_county = 50 -budget_ratio = 0.3 +budget_ratio = 0.5 dirname = "EBMC_generated/$(type_dataset)_dataset/" fname = "$(county_size)_$(num_lakes_per_county)_$(budget_ratio).csv" From 07f4dd4e3c2f3546d26bbd01e11755f8a7606044 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 21:00:43 +0000 Subject: [PATCH 7/8] explain why v_bar_p is ignored in compute_bilateral_payoff with AffExpr --- src/SGM/PolymatrixGame/Polymatrix.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SGM/PolymatrixGame/Polymatrix.jl b/src/SGM/PolymatrixGame/Polymatrix.jl index 8a37e10..9ed6180 100644 --- a/src/SGM/PolymatrixGame/Polymatrix.jl +++ b/src/SGM/PolymatrixGame/Polymatrix.jl @@ -50,6 +50,7 @@ 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) From e63da71a45a394e4f182551d29555612aa124c46 Mon Sep 17 00:00:00 2001 From: Bruno Machado Pacheco Date: Tue, 16 Sep 2025 21:04:06 +0000 Subject: [PATCH 8/8] fix: setting payoff in internalize_expr --- src/Game/Player.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Game/Player.jl b/src/Game/Player.jl index 0b235a6..c6245f5 100644 --- a/src/Game/Player.jl +++ b/src/Game/Player.jl @@ -93,7 +93,7 @@ function internalize_expr(player::Player, expr::AbstractJuMPScalar)::AbstractJuM end end - player.Π = _recursive_internalize_expr(expr) + return _recursive_internalize_expr(expr) end function set_payoff!(player::Player, payoff::AbstractJuMPScalar)