From 2ca27944577705ad19df1dd029e4a3f0e25829ef Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 11 Jun 2025 13:19:36 -0500 Subject: [PATCH] GurobiModel: Capture static_var_obj_coeffs_quad --- miplearn/problems/maxcut.py | 10 +++-- miplearn/solvers/gurobi.py | 7 ++++ tests/problems/test_maxcut.py | 73 ++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/miplearn/problems/maxcut.py b/miplearn/problems/maxcut.py index 8ac8408..9ebb928 100644 --- a/miplearn/problems/maxcut.py +++ b/miplearn/problems/maxcut.py @@ -80,6 +80,7 @@ class MaxCutGenerator: def _generate_graph(self) -> Graph: return nx.generators.random_graphs.binomial_graph(self.n.rvs(), self.p.rvs()) + def build_maxcut_model_gurobipy( data: Union[str, MaxCutData], params: Optional[dict[str, Any]] = None, @@ -97,13 +98,16 @@ def build_maxcut_model_gurobipy( x = model.addVars(nodes, vtype=gp.GRB.BINARY, name="x") # Add the objective function - model.setObjective(quicksum( - - data.weights[i] * x[e[0]] * (1 - x[e[1]]) for (i, e) in enumerate(edges) - )) + model.setObjective( + quicksum( + -data.weights[i] * x[e[0]] * (1 - x[e[1]]) for (i, e) in enumerate(edges) + ) + ) model.update() return GurobiModel(model) + def _maxcut_read(data: Union[str, MaxCutData]) -> MaxCutData: if isinstance(data, str): data = read_pkl_gz(data) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 7a1540f..5f51056 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -264,6 +264,13 @@ class GurobiModel(AbstractModel): h5.put_array( h5_field, np.array(self.inner.getAttr(gp_field, gp_vars), dtype=float) ) + obj = self.inner.getObjective() + if isinstance(obj, gp.QuadExpr): + nvars = len(self.inner.getVars()) + obj_q = np.zeros((nvars, nvars)) + for i in range(obj.size()): + obj_q[obj.getVar1(i).index, obj.getVar2(i).index] = obj.getCoeff(i) + h5.put_array("static_var_obj_coeffs_quad", obj_q) def _extract_after_load_constrs(self, h5: H5File) -> None: gp_constrs = self.inner.getConstrs() diff --git a/tests/problems/test_maxcut.py b/tests/problems/test_maxcut.py index 5a418b3..83974d3 100644 --- a/tests/problems/test_maxcut.py +++ b/tests/problems/test_maxcut.py @@ -1,17 +1,22 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. + import random +from tempfile import TemporaryDirectory import numpy as np +from scipy.stats import randint, uniform +from miplearn.h5 import H5File from miplearn.problems.maxcut import MaxCutGenerator, build_maxcut_model_gurobipy -from scipy.stats import randint, uniform + def _set_seed(): random.seed(42) np.random.seed(42) + def test_maxcut_generator_not_fixed() -> None: _set_seed() gen = MaxCutGenerator( @@ -22,12 +27,20 @@ def test_maxcut_generator_not_fixed() -> None: data = gen.generate(3) assert len(data) == 3 assert list(data[0].graph.nodes()) == [0, 1, 2, 3, 4] - assert list(data[0].graph.edges()) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)] + assert list(data[0].graph.edges()) == [ + (0, 2), + (0, 3), + (0, 4), + (2, 3), + (2, 4), + (3, 4), + ] assert data[0].weights.tolist() == [-1, 1, -1, -1, -1, 1] assert list(data[1].graph.nodes()) == [0, 1, 2, 3, 4] assert list(data[1].graph.edges()) == [(0, 1), (0, 3), (0, 4), (1, 4), (3, 4)] assert data[1].weights.tolist() == [-1, -1, -1, 1, -1] + def test_maxcut_generator_fixed() -> None: random.seed(42) np.random.seed(42) @@ -39,19 +52,67 @@ def test_maxcut_generator_fixed() -> None: data = gen.generate(3) assert len(data) == 3 assert list(data[0].graph.nodes()) == [0, 1, 2, 3, 4] - assert list(data[0].graph.edges()) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)] + assert list(data[0].graph.edges()) == [ + (0, 2), + (0, 3), + (0, 4), + (2, 3), + (2, 4), + (3, 4), + ] assert data[0].weights.tolist() == [-1, 1, -1, -1, -1, 1] assert list(data[1].graph.nodes()) == [0, 1, 2, 3, 4] - assert list(data[1].graph.edges()) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)] + assert list(data[1].graph.edges()) == [ + (0, 2), + (0, 3), + (0, 4), + (2, 3), + (2, 4), + (3, 4), + ] assert data[1].weights.tolist() == [-1, -1, -1, 1, -1, -1] + def test_maxcut_model(): _set_seed() data = MaxCutGenerator( - n=randint(low=20, high=21), + n=randint(low=10, high=11), p=uniform(loc=0.5, scale=0.0), fix_graph=True, ).generate(1)[0] model = build_maxcut_model_gurobipy(data) + + with TemporaryDirectory() as tempdir: + with H5File(f"{tempdir}/data.h5", "w") as h5: + model.extract_after_load(h5) + obj_lin = h5.get_array("static_var_obj_coeffs") + assert obj_lin is not None + assert obj_lin.tolist() == [ + 3.0, + 1.0, + 3.0, + 1.0, + -1.0, + 0.0, + -1.0, + 0.0, + -1.0, + 0.0, + ] + obj_quad = h5.get_array("static_var_obj_coeffs_quad") + assert obj_quad is not None + assert obj_quad.tolist() == [ + [0.0, 0.0, -1.0, 1.0, -1.0, 0.0, 0.0, 0.0, -1.0, -1.0], + [0.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, -1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 1.0, -1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, -1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + ] + model.optimize() - assert model.inner.ObjVal == -26 + assert model.inner.ObjVal == -4