From 46c98b86d333abc5e6499ac0724e60ee6053a9cd Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 13 Nov 2023 08:35:50 -0500 Subject: [PATCH 1/3] working with removal --- ilpy/decl.pxd | 3 +- ilpy/impl/solvers/GurobiBackend.cpp | 7 ++++- ilpy/impl/solvers/GurobiBackend.h | 4 ++- ilpy/impl/solvers/ScipBackend.cpp | 20 ++++++++++++- ilpy/impl/solvers/ScipBackend.h | 4 ++- ilpy/impl/solvers/SolverBackend.h | 45 +++++++++++++++-------------- ilpy/wrapper.pyi | 1 + ilpy/wrapper.pyx | 14 ++++++++- tests/test_solvers.py | 34 ++++++++++++++++++++++ 9 files changed, 104 insertions(+), 28 deletions(-) diff --git a/ilpy/decl.pxd b/ilpy/decl.pxd index 48c15b2..4ccf95c 100644 --- a/ilpy/decl.pxd +++ b/ilpy/decl.pxd @@ -93,7 +93,8 @@ cdef extern from "impl/solvers/SolverBackend.h": ) except + void setObjective(Objective&) void setConstraints(Constraints&) - void addConstraint(Constraint&) + int addConstraint(Constraint&) + void removeConstraint(int) void setTimeout(double) void setOptimalityGap(double, bool) void setNumThreads(unsigned int) diff --git a/ilpy/impl/solvers/GurobiBackend.cpp b/ilpy/impl/solvers/GurobiBackend.cpp index dca105c..544b9e8 100644 --- a/ilpy/impl/solvers/GurobiBackend.cpp +++ b/ilpy/impl/solvers/GurobiBackend.cpp @@ -157,7 +157,7 @@ GurobiBackend::setConstraints(const Constraints &constraints) GRB_CHECK(GRBupdatemodel(_model)); } -void +intptr_t GurobiBackend::addConstraint(const Constraint& constraint) { // set the linear coefficients @@ -210,8 +210,13 @@ GurobiBackend::addConstraint(const Constraint& constraint) { delete[] qrows; delete[] qcols; delete[] qvals; + + return 0; } +void GurobiBackend::removeConstraint(intptr_t constraintId){ + +} bool GurobiBackend::solve(Solution& x, std::string& msg) { diff --git a/ilpy/impl/solvers/GurobiBackend.h b/ilpy/impl/solvers/GurobiBackend.h index 0e4c58f..31b1d46 100644 --- a/ilpy/impl/solvers/GurobiBackend.h +++ b/ilpy/impl/solvers/GurobiBackend.h @@ -53,7 +53,9 @@ class GurobiBackend : public SolverBackend { void setConstraints(const Constraints& constraints); - void addConstraint(const Constraint& constraint); + intptr_t addConstraint(const Constraint& constraint); + + void removeConstraint(intptr_t constraintId); void setTimeout(double timeout) { _timeout = timeout; } diff --git a/ilpy/impl/solvers/ScipBackend.cpp b/ilpy/impl/solvers/ScipBackend.cpp index 789c352..519f5da 100644 --- a/ilpy/impl/solvers/ScipBackend.cpp +++ b/ilpy/impl/solvers/ScipBackend.cpp @@ -137,7 +137,7 @@ ScipBackend::setConstraints(const Constraints& constraints) { } } -void +intptr_t ScipBackend::addConstraint(const Constraint& constraint) { // create a list of variables and their coefficients @@ -189,6 +189,24 @@ ScipBackend::addConstraint(const Constraint& constraint) { // we do not release the constraint here // so that we can remove the constraints later in freeConstraints() // SCIP_CALL_ABORT(SCIPreleaseCons(_scip, &c)); + + // Cast the pointer to the new SCIP constraint to intptr_t and return it + intptr_t x = reinterpret_cast(c); + printf("ScipBackend::addConstraint\n"); + printf("returning constraintId = %ld\n", x); + return x; +} + +void ScipBackend::removeConstraint(intptr_t constraintId){ + printf("ScipBackend::removeConstraint\n"); + printf("constraintId = %ld\n", constraintId); + + // Cast the intptr_t back to a SCIP_CONS pointer + SCIP_CONS *cons = reinterpret_cast(constraintId); + // Remove the constraint from the model + SCIP_CALL_ABORT(SCIPdelCons(_scip, cons)); + // Release the constraint + SCIP_CALL_ABORT(SCIPreleaseCons(_scip, &cons)); } void diff --git a/ilpy/impl/solvers/ScipBackend.h b/ilpy/impl/solvers/ScipBackend.h index c9f643f..5fd9b5e 100644 --- a/ilpy/impl/solvers/ScipBackend.h +++ b/ilpy/impl/solvers/ScipBackend.h @@ -52,7 +52,9 @@ class ScipBackend : public SolverBackend { void setConstraints(const Constraints& constraints); - void addConstraint(const Constraint& constraint); + intptr_t addConstraint(const Constraint& constraint); + + void removeConstraint(intptr_t constraintId); void setTimeout(double timeout); diff --git a/ilpy/impl/solvers/SolverBackend.h b/ilpy/impl/solvers/SolverBackend.h index 6a8e914..eb947da 100644 --- a/ilpy/impl/solvers/SolverBackend.h +++ b/ilpy/impl/solvers/SolverBackend.h @@ -6,10 +6,10 @@ #include "Solution.h" #include "VariableType.h" -class SolverBackend { +class SolverBackend +{ public: - virtual ~SolverBackend() {} /** @@ -20,48 +20,50 @@ class SolverBackend { * Binary). */ virtual void initialize( - unsigned int numVariables, - VariableType variableType) = 0; + unsigned int numVariables, + VariableType variableType) = 0; /** * Initialise the linear solver for the given type of variables. * * @param numVariables * The number of variables in the problem. - * + * * @param defaultVariableType - * The default type of the variables (Continuous, Integer, + * The default type of the variables (Continuous, Integer, * Binary). * * @param specialVariableTypes - * A map of variable numbers to variable types to override the + * A map of variable numbers to variable types to override the * default. */ virtual void initialize( - unsigned int numVariables, - VariableType defaultVariableType, - const std::map& specialVariableTypes) = 0; + unsigned int numVariables, + VariableType defaultVariableType, + const std::map &specialVariableTypes) = 0; /** * Set the objective. * * @param objective A linear objective. */ - virtual void setObjective(const Objective& objective) = 0; + virtual void setObjective(const Objective &objective) = 0; /** * Set the linear (in)equality constraints. * * @param constraints A set of linear constraints. */ - virtual void setConstraints(const Constraints& constraints) = 0; + virtual void setConstraints(const Constraints &constraints) = 0; /** * Add a single constraint. * * @param constraint A linear constraints. */ - virtual void addConstraint(const Constraint& constraint) = 0; + virtual intptr_t addConstraint(const Constraint &constraint) = 0; + + virtual void removeConstraint(intptr_t constraintId) = 0; /** * Set a timeout in seconds for subsequent solve calls. @@ -69,25 +71,25 @@ class SolverBackend { virtual void setTimeout(double timeout) = 0; /** - * Set the solver's optimality gap. The solver will terminate with an - * "optimal" solution as soon as the gap between the upper and lower bound + * Set the solver's optimality gap. The solver will terminate with an + * "optimal" solution as soon as the gap between the upper and lower bound * is less than the given value times the upper bound. * * @param gap * The optimality gap. * * @param absolute - * If set to true, a solution is considered optimal if the gap - * between the upper and lower bound is smaller than the given + * If set to true, a solution is considered optimal if the gap + * between the upper and lower bound is smaller than the given * value. */ - virtual void setOptimalityGap(double gap, bool absolute=false) = 0; + virtual void setOptimalityGap(double gap, bool absolute = false) = 0; /** * Set the number of threads the solver can use. * * @param numThreads - * The number of threads to use. Defaults to 0, which leaves the + * The number of threads to use. Defaults to 0, which leaves the * decision to the solver. */ virtual void setNumThreads(unsigned int numThreads) = 0; @@ -98,7 +100,7 @@ class SolverBackend { * @param verbose * If set to true, verbose logging is enabled. */ - virtual void setVerbose(bool verbose) = 0; + virtual void setVerbose(bool verbose) = 0; /** * Solve the problem. @@ -108,8 +110,7 @@ class SolverBackend { * @param message A status message from the solver. * @return true, if the optimal value was found. */ - virtual bool solve(Solution& solution, std::string& message) = 0; + virtual bool solve(Solution &solution, std::string &message) = 0; }; #endif // INFERENCE_SOLVER_BACKEND_H__ - diff --git a/ilpy/wrapper.pyi b/ilpy/wrapper.pyi index 88504fd..27ed120 100644 --- a/ilpy/wrapper.pyi +++ b/ilpy/wrapper.pyi @@ -117,6 +117,7 @@ class Solver: def set_objective(self, objective: Objective | Expression) -> None: ... def set_constraints(self, constraints: Constraints) -> None: ... def add_constraint(self, constraint: Constraint | Expression) -> None: ... + def remove_constraint(self, constraint: Constraint | Expression) -> None: ... def set_timeout(self, timeout: float) -> None: ... def set_optimality_gap(self, gap: float, absolute: bool = False) -> None: ... def set_num_threads(self, num_threads: int) -> None: ... diff --git a/ilpy/wrapper.pyx b/ilpy/wrapper.pyx index e13d356..809c90a 100644 --- a/ilpy/wrapper.pyx +++ b/ilpy/wrapper.pyx @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from libc.stdint cimport uint32_t from libcpp.memory cimport shared_ptr from libcpp.map cimport map as cppmap +from libc.stdint cimport uintptr_t from libcpp.string cimport string from cython.operator cimport dereference as deref from . cimport decl @@ -245,6 +246,7 @@ cdef class Solver: cdef shared_ptr[decl.SolverBackend] p cdef unsigned int num_variables + cdef dict _constraint_map def __cinit__( self, @@ -259,6 +261,7 @@ cdef class Solver: vtypes[k] = v self.p = factory.createSolverBackend(preference) self.num_variables = num_variables + self._constraint_map = {} deref(self.p).initialize(num_variables, default_variable_type, vtypes) def set_objective(self, objective: Objective | Expression): @@ -278,7 +281,16 @@ cdef class Solver: const = constraint.as_constraint() else: const = constraint - deref(self.p).addConstraint(const.p[0]) + backend_id = deref(self.p).addConstraint(const.p[0]) + self._constraint_map[id(constraint)] = backend_id + print("stored", backend_id) + + def remove_constraint(self, constraint: Constraint | Expression): + if id(constraint) not in self._constraint_map: + raise ValueError("Constraint not found") + backend_id = self._constraint_map[id(constraint)] + print("removing", backend_id) + deref(self.p).removeConstraint(backend_id) def set_timeout(self, timeout): deref(self.p).setTimeout(timeout) diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 589718c..6f7107a 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -228,3 +228,37 @@ def test_solve_twice(preference: ilpy.Preference) -> None: solver.set_constraints(c1) solution = solver.solve() assert list(solution) != [7, 3] + + +@pytest.mark.parametrize("preference", PREFS) +def test_unique_constraint(preference: ilpy.Preference) -> None: + solver = ilpy.Solver(2, ilpy.VariableType.Integer, preference=preference) + x = ilpy.Variable("x", index=0) + y = ilpy.Variable("y", index=1) + solver.add_constraint(x + y <= 4) + solver.add_constraint(2 * x + y >= 5) + solver.add_constraint(y - x >= 2) + solution = solver.solve() + assert list(solution) == [1, 3] + # assert solution.get_value() == solution[0] + 2 * solution[1] + + +@pytest.mark.parametrize("preference", [ilpy.Scip]) +def test_unique_constraint(preference: ilpy.Preference) -> None: + solver = ilpy.Solver(2, ilpy.VariableType.Integer, preference=preference) + x = ilpy.Variable("x", index=0) + y = ilpy.Variable("y", index=1) + solver.add_constraint(y == x + 1) + c = y == 3 - x + solver.add_constraint(c) + solver.set_objective((x + y).as_objective(ilpy.Minimize)) + solution = solver.solve() + assert list(solution) == [1, 2] + solver.remove_constraint(c) + solver.add_constraint(y == 5 - x) + solution = solver.solve() + assert list(solution) == [2, 3] + # assert list(solution) == [0, 0] # optimal solution is now infeasible + # solver.remove_constraint(c) + # solution = solver.solve() + # assert list(solution) == [2, 3] From 1038b0700cc5b458a77718655f7f57873cb58f11 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 13 Nov 2023 08:46:55 -0500 Subject: [PATCH 2/3] working tests --- ilpy/impl/solvers/ScipBackend.cpp | 8 +------ ilpy/wrapper.pyx | 2 -- tests/test_solvers.py | 38 ++++++++++++------------------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/ilpy/impl/solvers/ScipBackend.cpp b/ilpy/impl/solvers/ScipBackend.cpp index 519f5da..d3d5d92 100644 --- a/ilpy/impl/solvers/ScipBackend.cpp +++ b/ilpy/impl/solvers/ScipBackend.cpp @@ -191,16 +191,10 @@ ScipBackend::addConstraint(const Constraint& constraint) { // SCIP_CALL_ABORT(SCIPreleaseCons(_scip, &c)); // Cast the pointer to the new SCIP constraint to intptr_t and return it - intptr_t x = reinterpret_cast(c); - printf("ScipBackend::addConstraint\n"); - printf("returning constraintId = %ld\n", x); - return x; + return reinterpret_cast(c); } void ScipBackend::removeConstraint(intptr_t constraintId){ - printf("ScipBackend::removeConstraint\n"); - printf("constraintId = %ld\n", constraintId); - // Cast the intptr_t back to a SCIP_CONS pointer SCIP_CONS *cons = reinterpret_cast(constraintId); // Remove the constraint from the model diff --git a/ilpy/wrapper.pyx b/ilpy/wrapper.pyx index 809c90a..b2f5132 100644 --- a/ilpy/wrapper.pyx +++ b/ilpy/wrapper.pyx @@ -283,13 +283,11 @@ cdef class Solver: const = constraint backend_id = deref(self.p).addConstraint(const.p[0]) self._constraint_map[id(constraint)] = backend_id - print("stored", backend_id) def remove_constraint(self, constraint: Constraint | Expression): if id(constraint) not in self._constraint_map: raise ValueError("Constraint not found") backend_id = self._constraint_map[id(constraint)] - print("removing", backend_id) deref(self.p).removeConstraint(backend_id) def set_timeout(self, timeout): diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 6f7107a..06f5940 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -230,35 +230,27 @@ def test_solve_twice(preference: ilpy.Preference) -> None: assert list(solution) != [7, 3] -@pytest.mark.parametrize("preference", PREFS) -def test_unique_constraint(preference: ilpy.Preference) -> None: - solver = ilpy.Solver(2, ilpy.VariableType.Integer, preference=preference) - x = ilpy.Variable("x", index=0) - y = ilpy.Variable("y", index=1) - solver.add_constraint(x + y <= 4) - solver.add_constraint(2 * x + y >= 5) - solver.add_constraint(y - x >= 2) - solution = solver.solve() - assert list(solution) == [1, 3] - # assert solution.get_value() == solution[0] + 2 * solution[1] - - @pytest.mark.parametrize("preference", [ilpy.Scip]) -def test_unique_constraint(preference: ilpy.Preference) -> None: +def test_remove_constraint(preference: ilpy.Preference) -> None: solver = ilpy.Solver(2, ilpy.VariableType.Integer, preference=preference) x = ilpy.Variable("x", index=0) y = ilpy.Variable("y", index=1) - solver.add_constraint(y == x + 1) - c = y == 3 - x - solver.add_constraint(c) solver.set_objective((x + y).as_objective(ilpy.Minimize)) + c1 = y == x + 1 + c2 = y == 3 - x + c3 = y == 5 - x # incompatible with c2 + solver.add_constraint(c1) + solver.add_constraint(c2) solution = solver.solve() assert list(solution) == [1, 2] - solver.remove_constraint(c) - solver.add_constraint(y == 5 - x) + + # FIXME: if we add an unsatisfiable constraint and solve in the middle, + # it appears to leave the solver in an unrecoverable state. + # solver.add_constraint(c3) + # solution = solver.solve() + # assert list(solution) == [0, 0] # optimal solution is now infeasible + + solver.remove_constraint(c2) + solver.add_constraint(c3) solution = solver.solve() assert list(solution) == [2, 3] - # assert list(solution) == [0, 0] # optimal solution is now infeasible - # solver.remove_constraint(c) - # solution = solver.solve() - # assert list(solution) == [2, 3] From 599ac42d7045fe20ce3ed6be5cb0b37c0ec36d14 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 13 Nov 2023 08:55:07 -0500 Subject: [PATCH 3/3] add comment --- ilpy/impl/solvers/ScipBackend.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ilpy/impl/solvers/ScipBackend.cpp b/ilpy/impl/solvers/ScipBackend.cpp index d3d5d92..e47f500 100644 --- a/ilpy/impl/solvers/ScipBackend.cpp +++ b/ilpy/impl/solvers/ScipBackend.cpp @@ -201,6 +201,8 @@ void ScipBackend::removeConstraint(intptr_t constraintId){ SCIP_CALL_ABORT(SCIPdelCons(_scip, cons)); // Release the constraint SCIP_CALL_ABORT(SCIPreleaseCons(_scip, &cons)); + // TODO: remove from _constraints as well + } void