Skip to content

Commit 9fdc253

Browse files
committed
Keep developing benchmarks
1 parent 6b59bf8 commit 9fdc253

File tree

11 files changed

+253
-7
lines changed

11 files changed

+253
-7
lines changed

Snakefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@ def generate_all_runs():
2222
return runs
2323

2424
rule all:
25+
input:
26+
"benchmarks/results.csv"
27+
28+
rule collect_benchmarks:
2529
input:
2630
[f"benchmarks/{problem}/results/{library}_{solver}_{size}.tsv"
2731
for problem, size, library, solver in generate_all_runs()]
32+
output:
33+
"benchmarks/results.csv"
34+
script:
35+
"benchmarks/collect_benchmarks.py"
2836

2937
rule run_benchmark:
3038
benchmark:
3139
repeat("benchmarks/{problem}/results/{library}_{solver}_{size}.tsv", config["repeat"])
3240
shell:
33-
"python benchmarks/run_benchmarks {wildcards.problem} --library {wildcards.library} --solver {wildcards.solver} --size {wildcards.size}"
41+
"python benchmarks/run_benchmark.py {wildcards.problem} --library {wildcards.library} --solver {wildcards.solver} --size {wildcards.size}"

benchmarks/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*/data/**
2+
*/results/*.tsv
3+
*.csv

benchmarks/collect_benchmarks.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pathlib import Path
2+
3+
import polars as pl
4+
from energy_model.scripts.util import mock_snakemake
5+
6+
7+
def collect_benchmarks(input_files):
8+
dfs = []
9+
for input_file in input_files:
10+
input_file = Path(input_file)
11+
problem = input_file.parent.parent.name
12+
name = input_file.name.partition(".")[0].split("_")
13+
library = name[0]
14+
solver = name[1]
15+
size = name[2]
16+
dfs.append(
17+
pl.read_csv(input_file, separator="\t")
18+
.mean()
19+
.with_columns(
20+
problem=pl.lit(problem),
21+
library=pl.lit(library),
22+
solver=pl.lit(solver),
23+
size=pl.lit(size),
24+
)
25+
)
26+
return pl.concat(dfs)
27+
28+
29+
if __name__ == "__main__":
30+
if "snakemake" not in globals() or hasattr(snakemake, "mock"): # noqa: F821
31+
snakemake = mock_snakemake("collect_benchmarks")
32+
33+
result = collect_benchmarks(snakemake.input)
34+
result.write_csv(snakemake.output[0])

benchmarks/config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
repeat: 3
1+
repeat: 1
22
problems:
33
facility_problem:
44
size:
@@ -20,4 +20,4 @@ libraries:
2020
# - gurobipy
2121
# - jump
2222
# - linopy
23-
# - pyoptinterface
23+
- pyoptinterface

benchmarks/energy_model/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Facility Location Problem
2+
3+
This is the exact same problem as described in the original [JuMP paper](https://epubs.siam.org/doi/10.1137/15M1020575) (see Section 8.2). It is re-explained here for clarity.
4+
5+
## Problem description
6+
7+
The purpose of this problem is to choose the location of $F$ facilities such that the maximum distance between any customer and its nearest facility is minimized. The $C^2$ Customers are evenly spread out over a square grid (see image below). Facilities can be placed anywhere in the grid.
8+
9+
TODO add image
10+
11+
12+
## Problem formulation
13+
14+
To simplify the formulation, let us scale our square grid such that its axes span go from $0$ to $1$.
15+
16+
We define a variable $y_{f,ax}$ which is the location of every facility ($f$) for each axis ($ax$) (either x-axis or y-axis).
17+
18+
$$ 0 \leq y_{f,ax} \leq 1$$
19+
20+
Next, we define the distance between every facility-customer pair, $s_{i,j,f}$.
21+
$$r^x_{i,j,f} = (i / N - y_{f,1}) \qquad \forall i, j, f$$
22+
$$r^y_{i,j,f} = (j / N - y_{f,2}) \qquad \forall i,j,f$$
23+
24+
$$s_{i,j,f} ^2 = (r_{i,j,f}^x) ^ 2 + (r^y_{i,j,f}) ^2 \qquad \forall i,j,f$$
25+
$$ s_{i,j,f} \geq 0 \qquad \forall i,j,f$$
26+
27+
We'd like to minimize the maximum distance between any facility and its nearest neighbor $d$.
28+
29+
$$\text{min} \quad d$$
30+
31+
At first, we might try to simply define $d$ as follows:
32+
33+
$$ d^{max} \geq s_{i,j,f} \qquad \forall i,j,f$$
34+
35+
However this is not quite right. We only wish to ensure the maximum distance is bounded by the distances between every customer and its _nearest_ facility. To do this, suppose we had a binary variable, $z_{i,j,f}$ that equals $1$ for customer-facility pairs that are relevant (when the facility is the nearest to the customer) and $0$ otherwise. We can now rewrite the above constraint as follows to fix our issue.
36+
37+
$$ d^{max} \geq s_{i,j,f} - \sqrt{2} (1 - z_{i,j,f}) \qquad \forall i,j,f $$
38+
39+
Why does this work? When $z_{i,j,f} = 0$ the right hand side is necessarily zero or less (since $\sqrt{2}$ is the largest possible distance in a 1-by-1 square) and the constraint is thus non-binding as desired.
40+
41+
Finally, how do we define this magical $z_{i,j,f}$ variable? We simply ensure that exactly one customer-facility pair exists for each customer. The optimization will automatically choose the nearest facility if it matters since it wishes to minimize the objective.
42+
43+
$$\sum_{f} z_{i,j,f} = 1 \qquad \forall i,j$$
44+

benchmarks/facility_problem/bm_pyoframe.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def solve(solver, size=None, G=None, F=None, is_benchmarking=True, use_var_names
3434
model.x_axis, model.y_axis, model.facilities, vtype="binary"
3535
)
3636
model.con_only_one_closest = model.is_closest.sum("f") == 1
37-
3837
model.dist_x = Variable(model.x_axis, model.facilities)
3938
model.dist_y = Variable(model.y_axis, model.facilities)
4039
model.con_dist_x = model.dist_x == customer_position_x.to_expr().add_dim(
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright (c) 2022: Miles Lubin and contributors
2+
#
3+
# Use of this source code is governed by an MIT-style license that can be found
4+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
5+
# See https://github.com/jump-dev/JuMPPaperBenchmarks
6+
7+
import pyomo.environ as pyo
8+
from pyomo.opt import SolverFactory
9+
10+
11+
def solve(solver, size, is_benchmarking=True):
12+
if type(size) == int:
13+
size = (size, size)
14+
G, F = size
15+
model = pyo.ConcreteModel()
16+
model.G = G
17+
model.F = F
18+
model.Grid = pyo.RangeSet(0, model.G)
19+
model.Facs = pyo.RangeSet(1, model.F)
20+
model.Dims = pyo.RangeSet(1, 2)
21+
model.y = pyo.Var(model.Facs, model.Dims, bounds=(0.0, 1.0))
22+
model.s = pyo.Var(model.Grid, model.Grid, model.Facs, bounds=(0.0, None))
23+
model.z = pyo.Var(model.Grid, model.Grid, model.Facs, within=pyo.Binary)
24+
model.r = pyo.Var(model.Grid, model.Grid, model.Facs, model.Dims)
25+
model.d = pyo.Var()
26+
model.obj = pyo.Objective(expr=1.0 * model.d)
27+
28+
def assmt_rule(mod, i, j):
29+
return sum([mod.z[i, j, f] for f in mod.Facs]) == 1
30+
31+
model.assmt = pyo.Constraint(model.Grid, model.Grid, rule=assmt_rule)
32+
M = 2 * 1.414
33+
34+
def quadrhs_rule(mod, i, j, f):
35+
return mod.s[i, j, f] == mod.d + M * (1 - mod.z[i, j, f])
36+
37+
model.quadrhs = pyo.Constraint(
38+
model.Grid, model.Grid, model.Facs, rule=quadrhs_rule
39+
)
40+
41+
def quaddistk1_rule(mod, i, j, f):
42+
return mod.r[i, j, f, 1] == (1.0 * i) / mod.G - mod.y[f, 1]
43+
44+
model.quaddistk1 = pyo.Constraint(
45+
model.Grid, model.Grid, model.Facs, rule=quaddistk1_rule
46+
)
47+
48+
def quaddistk2_rule(mod, i, j, f):
49+
return mod.r[i, j, f, 2] == (1.0 * j) / mod.G - mod.y[f, 2]
50+
51+
model.quaddistk2 = pyo.Constraint(
52+
model.Grid, model.Grid, model.Facs, rule=quaddistk2_rule
53+
)
54+
55+
def quaddist_rule(mod, i, j, f):
56+
return mod.r[i, j, f, 1] ** 2 + mod.r[i, j, f, 2] ** 2 <= mod.s[i, j, f] ** 2
57+
58+
model.quaddist = pyo.Constraint(
59+
model.Grid, model.Grid, model.Facs, rule=quaddist_rule
60+
)
61+
opt = SolverFactory(solver)
62+
if is_benchmarking:
63+
opt.options["timelimit"] = 0.0
64+
opt.options["presolve"] = False
65+
try:
66+
if solver == "gurobi_persistent":
67+
opt.set_instance(model)
68+
opt.solve(tee=True)
69+
else:
70+
opt.solve(model, tee=True)
71+
except ValueError as e:
72+
if "bad status: aborted" not in str(e):
73+
raise e
74+
return model
75+
76+
77+
if __name__ == "__main__":
78+
solve("gurobi", 5)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright (c) 2023: Yue Yang
2+
# https://github.com/metab0t/PyOptInterface_benchmark
3+
#
4+
# Use of this source code is governed by an MIT-style license that can be found
5+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
6+
7+
import pyoptinterface as poi
8+
from pyoptinterface import gurobi, copt
9+
import numpy as np
10+
11+
12+
def solve(solver, size):
13+
model_constructor = {
14+
"gurobi": gurobi.Model,
15+
"copt": copt.Model,
16+
}.get(solver, None)
17+
if model_constructor is None:
18+
raise ValueError(f"Unknown solver {solver}")
19+
20+
m = model_constructor()
21+
if type(size) == int:
22+
size = (size, size)
23+
G, F = size
24+
# Create variables
25+
y = add_ndarray_variable(m, (F, 2), lb=0.0, ub=1.0)
26+
s = add_ndarray_variable(m, (G + 1, G + 1, F), lb=0.0)
27+
z = add_ndarray_variable(m, (G + 1, G + 1, F), domain=poi.VariableDomain.Binary)
28+
r = add_ndarray_variable(m, (G + 1, G + 1, F, 2))
29+
d = m.add_variable()
30+
31+
# Set objective
32+
m.set_objective(d * 1.0)
33+
34+
# Add constraints
35+
for i in range(G + 1):
36+
for j in range(G + 1):
37+
expr = poi.quicksum(z[i, j, :])
38+
m.add_linear_constraint(expr, poi.Eq, 1.0)
39+
40+
M = 2 * 1.414
41+
for i in range(G + 1):
42+
for j in range(G + 1):
43+
for f in range(F):
44+
expr = s[i, j, f] - d - M * (1 - z[i, j, f])
45+
m.add_linear_constraint(expr, poi.Eq, 0.0)
46+
expr = r[i, j, f, 0] - i / G + y[f, 0]
47+
m.add_linear_constraint(expr, poi.Eq, 0.0)
48+
expr = r[i, j, f, 1] - j / G + y[f, 1]
49+
m.add_linear_constraint(expr, poi.Eq, 0.0)
50+
m.add_second_order_cone_constraint(
51+
[s[i, j, f], r[i, j, f, 0], r[i, j, f, 1]]
52+
)
53+
54+
# Optimize model
55+
m.set_model_attribute(poi.ModelAttribute.Silent, True)
56+
m.set_model_attribute(poi.ModelAttribute.TimeLimitSec, 0.0)
57+
# close presolve for gurobi and open for copt
58+
solver_name = m.get_model_attribute(poi.ModelAttribute.SolverName)
59+
if solver_name.lower() == "gurobi":
60+
m.set_raw_parameter("Presolve", 0)
61+
elif solver_name.lower() == "copt":
62+
m.set_raw_parameter("Presolve", 1)
63+
m.optimize()
64+
65+
66+
def add_ndarray_variable(m, shape, **kwargs):
67+
array = np.empty(shape, dtype=object)
68+
array_flat = array.flat
69+
for i in range(array.size):
70+
array_flat[i] = m.add_variable(**kwargs)
71+
return array
72+
73+
74+
if __name__ == "__main__":
75+
solve("gurobi", 5)

benchmarks/run_benchmark.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ def get_problems() -> Collection[str]:
2020
prog="run_benchmark", description="Run a benchmark with a given solver."
2121
)
2222
parser.add_argument("problem", choices=get_problems())
23-
parser.add_argument("--solver", choices=SUPPORTED_SOLVERS)
23+
parser.add_argument("--solver", choices=[s.name for s in SUPPORTED_SOLVERS])
2424
parser.add_argument("--size", type=int)
2525
parser.add_argument("--library")
2626
args = parser.parse_args()
2727

2828
# import solve function dynamically
29-
solve_func = importlib.import_module(f"benchmarks.bm_{args.library.lower()}").solve
29+
solve_func = importlib.import_module(
30+
f"{args.problem}.bm_{args.library.lower()}"
31+
).solve
3032

3133
solve_func(solver=args.solver, size=args.size)

0 commit comments

Comments
 (0)