From cf81943fa4c029e61bc85d7ef7dfa4b7e0430770 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Mon, 18 Aug 2025 20:07:28 -0700 Subject: [PATCH 01/13] add read MPS and relaxation to python API --- .../cuopt/cuopt/linear_programming/problem.py | 121 +++++++++++++++--- .../linear_programming/test_python_API.py | 50 ++++++++ 2 files changed, 156 insertions(+), 15 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 1a14e17cf..53ee8d5e7 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -16,10 +16,12 @@ from enum import Enum import numpy as np +import copy import cuopt.linear_programming.data_model as data_model import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings +import cuopt_mps_parser class VType(str, Enum): @@ -699,12 +701,11 @@ def __init__(self, model_name=""): self.Status = -1 self.ObjValue = float("nan") + self.model = None self.solved = False self.rhs = None self.row_sense = None - self.row_pointers = None - self.column_indicies = None - self.values = None + self.constraint_csr_matrix = None self.lower_bound = None self.upper_bound = None self.var_type = None @@ -714,6 +715,54 @@ def __init__(self, mdict): for key, value in mdict.items(): setattr(self, key, value) + def _from_data_model(self, dm): + obj_coeffs = dm.get_objective_coefficients() + num_vars = len(obj_coeffs) + sense = dm.get_sense() + if sense: + sense = MAXIMIZE + else: + sense = MINIMIZE + v_lb = dm.get_variable_lower_bounds() + v_ub = dm.get_variable_upper_bounds() + v_types = dm.get_variable_types() + v_names = dm.get_variable_names().tolist() + + # Add all Variables and Objective Coefficients + for i in range(num_vars): + v_name = "" + if v_names: + v_name = v_names[i] + self.addVariable(v_lb[i], v_ub[i], vtype=v_types[i], name=v_name) + vars = self.getVariables() + expr = LinearExpression(vars, obj_coeffs, 0.0) + self.setObjective(expr, sense) + + # Add all Constraints + c_lb = dm.get_constraint_lower_bounds() + c_ub = dm.get_constraint_upper_bounds() + c_b = dm.get_constraint_bounds() + offsets = dm.get_constraint_matrix_offsets() + indices = dm.get_constraint_matrix_indices() + values = dm.get_constraint_matrix_values() + + num_constrs = len(offsets)-1 + for i in range(num_constrs): + start = offsets[i] + end = offsets[i+1] + c_coeffs = values[start:end] + c_indices = indices[start:end] + c_vars = [vars[j] for j in c_indices] + expr = LinearExpression(c_vars, c_coeffs, 0.0) + if c_lb[i] == c_ub[i]: + self.addConstraint(expr == c_b[i]) + elif c_lb[i] == c_b[i]: + self.addConstraint(expr >= c_b[i]) + elif c_ub[i] == c_b[i]: + self.addConstraint(expr <= c_b[i]) + else: + raise Exception("Couldn't initialize constraints") + def reset_solved_values(self): # Resets all post solve values for var in self.vars: @@ -724,6 +773,8 @@ def reset_solved_values(self): constr.Slack = float("nan") constr.DualValue = float("nan") + self.model = None + self.constraint_csr_matrix = None self.ObjValue = float("nan") self.solved = False @@ -856,6 +907,17 @@ def getConstraints(self): """ return self.constrs + @classmethod + def readMPS(cls, mps_file): + """ + Initiliaze a problem from an MPS file. + """ + problem = cls() + data_model = cuopt_mps_parser.ParseMps(mps_file) + problem._from_data_model(data_model) + problem.model = data_model + return problem + @property def NumVariables(self): # Returns number of variables in the problem @@ -887,6 +949,8 @@ def getCSR(self): Computes and returns the CSR representation of the constraint matrix. """ + if self.constraint_csr_matrix is not None: + return self.dict_to_object(self.constraint_csr_matrix) csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} for constr in self.constrs: csr_dict["column_indices"].extend( @@ -894,6 +958,7 @@ def getCSR(self): ) csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) + self.constraint_csr_matrix = csr_dict return self.dict_to_object(csr_dict) def get_incumbent_values(self, solution, vars): @@ -905,6 +970,17 @@ def get_incumbent_values(self, solution, vars): values.append(solution[var.index]) return values + def relax(self): + """ + Relax a MIP problem into an LP problem and return the relaxed model. + """ + self.reset_solved_values() + relaxed_problem = copy.deepcopy(self) + vars = relaxed_problem.getVariables() + for v in vars: + v.VariableType = CONTINUOUS + return relaxed_problem + def post_solve(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() @@ -950,19 +1026,32 @@ def solve(self, settings=solver_settings.SolverSettings()): >>> problem.solve() """ + if self.model is not None: + # Call Solver + solution = solver.Solve(self.model, settings) + # Post Solve + self.post_solve(solution) + return + # iterate through the constraints and construct the constraint matrix n = len(self.vars) - self.row_pointers = [0] - self.column_indicies = [] - self.values = [] self.rhs = [] self.row_sense = [] - for constr in self.constrs: - self.column_indicies.extend(list(constr.vindex_coeff_dict.keys())) - self.values.extend(list(constr.vindex_coeff_dict.values())) - self.row_pointers.append(len(self.column_indicies)) - self.rhs.append(constr.RHS) - self.row_sense.append(constr.Sense) + + if self.constraint_csr_matrix is None: + csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} + for constr in self.constrs: + csr_dict["column_indices"].extend(list(constr.vindex_coeff_dict.keys())) + csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) + csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) + self.constraint_csr_matrix = csr_dict + + else: + for constr in self.constrs: + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) self.objective = np.zeros(n) self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) @@ -977,9 +1066,9 @@ def solve(self, settings=solver_settings.SolverSettings()): # Initialize datamodel dm = data_model.DataModel() dm.set_csr_constraint_matrix( - np.array(self.values), - np.array(self.column_indicies), - np.array(self.row_pointers), + np.array(self.constraint_csr_matrix["values"]), + np.array(self.constraint_csr_matrix["column_indices"]), + np.array(self.constraint_csr_matrix["row_pointers"]), ) if self.ObjSense == -1: dm.set_maximize(True) @@ -989,9 +1078,11 @@ def solve(self, settings=solver_settings.SolverSettings()): dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) dm.set_variable_types(self.var_type) + self.model = dm # Call Solver solution = solver.Solve(dm, settings) # Post Solve self.post_solve(solution) + diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 132920a86..e0dbda214 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -26,12 +26,15 @@ CONTINUOUS, INTEGER, MAXIMIZE, + MINIMIZE, CType, Problem, VType, sense, ) +from cuopt.linear_programming.solver.solver_parameters import CUOPT_USER_PROBLEM_FILE + def test_model(): @@ -265,6 +268,53 @@ def test_constraint_matrix(): assert rhs == exp_rhs +def test_read_write_mps_and_relaxation(): + + # Create MIP model + m = Problem("SMALLMIP") + + # Vars: continuous, nonnegative by default + x1 = m.addVariable(name="x1", lb=0.0, vtype=INTEGER) + x2 = m.addVariable(name="x2", lb=0.0, ub=4.0, vtype=INTEGER) + x3 = m.addVariable(name="x3", lb=0.0, ub=6.0, vtype=INTEGER) + x4 = m.addVariable(name="x4", lb=0.0, vtype=INTEGER) + x5 = m.addVariable(name="x5", lb=0.0, vtype=INTEGER) + + # Objective (minimize) + m.setObjective(2*x1 + 3*x2 + 0*x3 + 1*x4 + 4*x5, MINIMIZE) + + # Constraints (5 total) + m.addConstraint(x1 + x2 + x3 <= 10, name="c1") + m.addConstraint(2*x1 + x3 - x4 >= 3, name="c2") + m.addConstraint(x2 + 3*x5 == 7, name="c3") + m.addConstraint(x4 + x5 <= 8, name="c4") + m.addConstraint(x1 + x2 + x3 + x4 + x5 >= 5, name="c5") + + # Write MPS + ss = SolverSettings() + ss.set_parameter(CUOPT_USER_PROBLEM_FILE, "small_mip.mps") + m.solve(ss) + + # Read MPS and solve + prob = Problem.readMPS("small_mip.mps") + prob.solve() + + expected_values_mip = [0.0, 1.0, 3.0, 0.0, 2.0] + assert prob.Status.name == "Optimal" + for i, v in enumerate(prob.getVariables()): + assert v.getValue() == pytest.approx(expected_values_mip[i]) + + # Relax the Problem into LP and solve + lp_prob = prob.relax() + assert not lp_prob.IsMIP + lp_prob.solve() + + expected_values_lp = [0.0, 0.0, 3.0, 0.0, 2.333333] + assert lp_prob.Status.name == "Optimal" + for i, v in enumerate(lp_prob.getVariables()): + assert v.getValue() == pytest.approx(expected_values_lp[i]) + + def test_incumbent_solutions(): # Callback for incumbent solution From c42c96e107c5b981c2dac6489ce5d21bd042c672 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 19 Aug 2025 10:02:51 -0700 Subject: [PATCH 02/13] add objective offset to write mps --- cpp/src/mip/problem/problem.cu | 1 + cpp/src/mip/problem/problem.cuh | 1 + cpp/src/mip/problem/write_mps.cu | 3 +++ 3 files changed, 5 insertions(+) diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 888388ef1..ce518fbc7 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -142,6 +142,7 @@ problem_t::problem_t( var_names(problem_.get_variable_names()), row_names(problem_.get_row_names()), objective_name(problem_.get_objective_name()), + objective_offset(problem_.get_objective_offset()), lp_state(*this, problem_.get_handle_ptr()->get_stream()), fixing_helpers(n_constraints, n_variables, handle_ptr) { diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index cc29ce0e6..8764ca08e 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -263,6 +263,7 @@ class problem_t { std::vector row_names{}; /** name of the objective (only a single objective is currently allowed) */ std::string objective_name; + f_t objective_offset; bool is_scaled_{false}; bool preprocess_called{false}; // this LP state keeps the warm start data of some solution of diff --git a/cpp/src/mip/problem/write_mps.cu b/cpp/src/mip/problem/write_mps.cu index cca3cd5b1..e779d52ba 100644 --- a/cpp/src/mip/problem/write_mps.cu +++ b/cpp/src/mip/problem/write_mps.cu @@ -124,6 +124,9 @@ void problem_t::write_as_mps(const std::string& path) mps_file << " RHS1 " << row_name << " " << rhs << "\n"; } } + if (isfinite(objective_offset) && objective_offset != 0.0) { + mps_file << " RHS1 " << (objective_name.empty() ? "OBJ" : objective_name) << " " << -objective_offset << "\n"; + } // RANGES section if needed bool has_ranges = false; From 4af74126ec27f9ea5076c7336dc271661788ed2a Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 22 Aug 2025 16:56:14 -0400 Subject: [PATCH 03/13] set the objective offset in the datamodel in problem.py --- python/cuopt/cuopt/linear_programming/problem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 53ee8d5e7..b32822432 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -874,7 +874,7 @@ def setObjective(self, expr, sense=MINIMIZE): case int() | float(): for var in self.vars: var.setObjectiveCoefficient(0.0) - self.ObjCon = float(expr) + self.ObjConstant = float(expr) case Variable(): for var in self.vars: var.setObjectiveCoefficient(0.0) @@ -883,6 +883,7 @@ def setObjective(self, expr, sense=MINIMIZE): case LinearExpression(): for var, coeff in expr.zipVarCoefficients(): self.vars[var.getIndex()].setObjectiveCoefficient(coeff) + self.ObjConstant = expr.getConstant() case _: raise ValueError( "Objective must be a LinearExpression or a constant" @@ -1075,6 +1076,7 @@ def solve(self, settings=solver_settings.SolverSettings()): dm.set_constraint_bounds(np.array(self.rhs)) dm.set_row_types(np.array(self.row_sense, dtype="S1")) dm.set_objective_coefficients(self.objective) + dm.set_objective_offset(self.ObjConstant) dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) dm.set_variable_types(self.var_type) From b26f99f28c4ffd8a1f982ffd4e5cfafb187a5ac5 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 27 Aug 2025 21:40:07 -0700 Subject: [PATCH 04/13] add write mps --- .../cuopt/linear_programming/solve.hpp | 4 + .../utilities/cython_solve.hpp | 6 +- .../utilities/cython_solve.cu | 14 +++ cpp/src/mip/solve.cu | 14 ++- .../cuopt/linear_programming/__init__.py | 2 +- .../cuopt/cuopt/linear_programming/problem.py | 110 +++++++++--------- .../linear_programming/solver/__init__.py | 2 +- .../linear_programming/solver/solver.pxd | 4 + .../cuopt/linear_programming/solver/solver.py | 5 + .../solver/solver_wrapper.pyx | 9 ++ 10 files changed, 112 insertions(+), 58 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/solve.hpp b/cpp/include/cuopt/linear_programming/solve.hpp index 04ee5530c..60b062cb1 100644 --- a/cpp/include/cuopt/linear_programming/solve.hpp +++ b/cpp/include/cuopt/linear_programming/solve.hpp @@ -26,6 +26,7 @@ #include #include #include +#include namespace cuopt::linear_programming { @@ -116,4 +117,7 @@ optimization_problem_t mps_data_model_to_optimization_problem( raft::handle_t const* handle_ptr, const cuopt::mps_parser::mps_data_model_t& data_model); +template +void write_mps(optimization_problem_t& op_problem, + std::string); } // namespace cuopt::linear_programming diff --git a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp index eef185d0d..a74d8d8d8 100644 --- a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp @@ -24,7 +24,7 @@ #include #include #include - +#include #include #include #include @@ -102,6 +102,10 @@ struct solver_ret_t { // Wrapper for solve to expose the API to cython. +void write_mps(cuopt::mps_parser::data_model_view_t*, + std::string, + unsigned int flags = cudaStreamNonBlocking); + std::unique_ptr call_solve(cuopt::mps_parser::data_model_view_t*, linear_programming::solver_settings_t*, unsigned int flags = cudaStreamNonBlocking, diff --git a/cpp/src/linear_programming/utilities/cython_solve.cu b/cpp/src/linear_programming/utilities/cython_solve.cu index 1f9034e20..a1ec3c99f 100644 --- a/cpp/src/linear_programming/utilities/cython_solve.cu +++ b/cpp/src/linear_programming/utilities/cython_solve.cu @@ -109,6 +109,20 @@ data_model_to_optimization_problem( return op_problem; } +void write_mps(cuopt::mps_parser::data_model_view_t* data_model, + std::string user_problem_file, + unsigned int flags) +{ + cudaStream_t stream; + RAFT_CUDA_TRY(cudaStreamCreateWithFlags(&stream, flags)); + const raft::handle_t handle_{stream}; + + cuopt::linear_programming::solver_settings_t solver_settings; + auto op_problem = data_model_to_optimization_problem(data_model, &solver_settings, &handle_); + + cuopt::linear_programming::write_mps(op_problem, user_problem_file); +} + /** * @brief Wrapper for linear_programming to expose the API to cython * diff --git a/cpp/src/mip/solve.cu b/cpp/src/mip/solve.cu index 841770c4d..f2077c1b9 100644 --- a/cpp/src/mip/solve.cu +++ b/cpp/src/mip/solve.cu @@ -203,6 +203,14 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, } } +template +void write_mps(optimization_problem_t& op_problem, + std::string user_problem_file) +{ + detail::problem_t problem(op_problem); + problem.write_as_mps(user_problem_file); +} + template mip_solution_t solve_mip( raft::handle_t const* handle_ptr, @@ -221,7 +229,11 @@ mip_solution_t solve_mip( template mip_solution_t solve_mip( \ raft::handle_t const* handle_ptr, \ const cuopt::mps_parser::mps_data_model_t& mps_data_model, \ - mip_solver_settings_t const& settings); + mip_solver_settings_t const& settings); \ + \ + template void write_mps( \ + optimization_problem_t& op_problem, \ + std::string user_problem_file); #if MIP_INSTANTIATE_FLOAT INSTANTIATE(float) diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index 7941ad911..e781fac64 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -17,7 +17,7 @@ from cuopt.linear_programming.data_model import DataModel from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution -from cuopt.linear_programming.solver import BatchSolve, Solve +from cuopt.linear_programming.solver import BatchSolve, Solve, writeMPS from cuopt.linear_programming.solver_settings import ( PDLPSolverMode, SolverMethod, diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 53ee8d5e7..13e9b6b24 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -763,6 +763,54 @@ def _from_data_model(self, dm): else: raise Exception("Couldn't initialize constraints") + def _to_data_model(self): + # iterate through the constraints and construct the constraint matrix + n = len(self.vars) + self.rhs = [] + self.row_sense = [] + + if self.constraint_csr_matrix is None: + csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} + for constr in self.constrs: + csr_dict["column_indices"].extend(list(constr.vindex_coeff_dict.keys())) + csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) + csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) + self.constraint_csr_matrix = csr_dict + + else: + for constr in self.constrs: + self.rhs.append(constr.RHS) + self.row_sense.append(constr.Sense) + + self.objective = np.zeros(n) + self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) + self.var_type = np.empty(n, dtype="S1") + + for j in range(n): + self.objective[j] = self.vars[j].getObjectiveCoefficient() + self.var_type[j] = self.vars[j].getVariableType() + self.lower_bound[j] = self.vars[j].getLowerBound() + self.upper_bound[j] = self.vars[j].getUpperBound() + + # Initialize datamodel + dm = data_model.DataModel() + dm.set_csr_constraint_matrix( + np.array(self.constraint_csr_matrix["values"]), + np.array(self.constraint_csr_matrix["column_indices"]), + np.array(self.constraint_csr_matrix["row_pointers"]), + ) + if self.ObjSense == -1: + dm.set_maximize(True) + dm.set_constraint_bounds(np.array(self.rhs)) + dm.set_row_types(np.array(self.row_sense, dtype="S1")) + dm.set_objective_coefficients(self.objective) + dm.set_variable_lower_bounds(self.lower_bound) + dm.set_variable_upper_bounds(self.upper_bound) + dm.set_variable_types(self.var_type) + self.model = dm + def reset_solved_values(self): # Resets all post solve values for var in self.vars: @@ -918,6 +966,11 @@ def readMPS(cls, mps_file): problem.model = data_model return problem + def writeMPS(self, mps_file): + if self.model is None: + self._to_data_model() + solver.writeMPS(self.model, mps_file) + @property def NumVariables(self): # Returns number of variables in the problem @@ -1026,62 +1079,11 @@ def solve(self, settings=solver_settings.SolverSettings()): >>> problem.solve() """ - if self.model is not None: - # Call Solver - solution = solver.Solve(self.model, settings) - # Post Solve - self.post_solve(solution) - return - - # iterate through the constraints and construct the constraint matrix - n = len(self.vars) - self.rhs = [] - self.row_sense = [] - - if self.constraint_csr_matrix is None: - csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} - for constr in self.constrs: - csr_dict["column_indices"].extend(list(constr.vindex_coeff_dict.keys())) - csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) - csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) - self.rhs.append(constr.RHS) - self.row_sense.append(constr.Sense) - self.constraint_csr_matrix = csr_dict - - else: - for constr in self.constrs: - self.rhs.append(constr.RHS) - self.row_sense.append(constr.Sense) - - self.objective = np.zeros(n) - self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) - self.var_type = np.empty(n, dtype="S1") - - for j in range(n): - self.objective[j] = self.vars[j].getObjectiveCoefficient() - self.var_type[j] = self.vars[j].getVariableType() - self.lower_bound[j] = self.vars[j].getLowerBound() - self.upper_bound[j] = self.vars[j].getUpperBound() - - # Initialize datamodel - dm = data_model.DataModel() - dm.set_csr_constraint_matrix( - np.array(self.constraint_csr_matrix["values"]), - np.array(self.constraint_csr_matrix["column_indices"]), - np.array(self.constraint_csr_matrix["row_pointers"]), - ) - if self.ObjSense == -1: - dm.set_maximize(True) - dm.set_constraint_bounds(np.array(self.rhs)) - dm.set_row_types(np.array(self.row_sense, dtype="S1")) - dm.set_objective_coefficients(self.objective) - dm.set_variable_lower_bounds(self.lower_bound) - dm.set_variable_upper_bounds(self.upper_bound) - dm.set_variable_types(self.var_type) - self.model = dm + if self.model is None: + self._to_data_model() # Call Solver - solution = solver.Solve(dm, settings) + solution = solver.Solve(self.model, settings) # Post Solve self.post_solve(solution) diff --git a/python/cuopt/cuopt/linear_programming/solver/__init__.py b/python/cuopt/cuopt/linear_programming/solver/__init__.py index dbce0419b..92e37de4e 100644 --- a/python/cuopt/cuopt/linear_programming/solver/__init__.py +++ b/python/cuopt/cuopt/linear_programming/solver/__init__.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cuopt.linear_programming.solver.solver import BatchSolve, Solve +from cuopt.linear_programming.solver.solver import BatchSolve, Solve, writeMPS diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.pxd b/python/cuopt/cuopt/linear_programming/solver/solver.pxd index 255fcd764..3a7dbf56f 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.pxd +++ b/python/cuopt/cuopt/linear_programming/solver/solver.pxd @@ -191,6 +191,10 @@ cdef extern from "cuopt/linear_programming/utilities/cython_solve.hpp" namespace solver_settings_t[int, double]* solver_settings, ) except + + #cdef extern from "cuopt/linear_programming/utilities/cython_solve.hpp" namespace "cuopt::cython": # noqa + cdef void write_mps(data_model_view_t[int, double]* data_model, + string user_problem_file) except + + cdef pair[vector[unique_ptr[solver_ret_t]], double] call_batch_solve( # noqa vector[data_model_view_t[int, double] *] data_models, solver_settings_t[int, double]* solver_settings, diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 12921ae7c..0f07d436a 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -180,3 +180,8 @@ def BatchSolve(data_model_list, solver_settings=None): solver_settings = SolverSettings() return solver_wrapper.BatchSolve(data_model_list, solver_settings) + + +@catch_cuopt_exception +def writeMPS(data_model, user_problem_file): + return solver_wrapper.writeMPS(data_model, user_problem_file) diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 02782b8f9..614ec3f39 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -44,6 +44,7 @@ from cuopt.linear_programming.data_model.data_model_wrapper cimport DataModel from cuopt.linear_programming.solver.solver cimport ( call_batch_solve, call_solve, + write_mps, error_type_t, mip_termination_status_t, pdlp_solver_mode_t, @@ -661,6 +662,14 @@ cdef create_solution(unique_ptr[solver_ret_t] sol_ret_ptr, ) +def writeMPS(py_data_model_obj, user_problem_file): + cdef DataModel data_model_obj = py_data_model_obj + data_model_obj.variable_types = type_cast( + data_model_obj.variable_types, "S1", "variable_types" + ) + set_data_model_view(data_model_obj) + write_mps(data_model_obj.c_data_model_view.get(), user_problem_file.encode('utf-8')) + def Solve(py_data_model_obj, settings, mip=False): cdef DataModel data_model_obj = py_data_model_obj From 7a4fbde9a065e99f8c3ef30fa94e0c22d88e71f6 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 29 Aug 2025 05:52:59 -0700 Subject: [PATCH 05/13] move write_mps to libmps and take data_model_view as input --- .../optimization_problem.hpp | 7 + cpp/libmps_parser/CMakeLists.txt | 2 + .../include/mps_parser/data_model_view.hpp | 29 ++ .../include/mps_parser/mps_writer.hpp | 59 +++++ .../include/mps_parser/writer.hpp | 38 +++ cpp/libmps_parser/src/data_model_view.cpp | 25 ++ cpp/libmps_parser/src/mps_writer.cpp | 248 ++++++++++++++++++ cpp/libmps_parser/src/mps_writer.hpp | 93 +++++++ cpp/libmps_parser/src/writer.cpp | 36 +++ .../optimization_problem.cu | 84 +++++- cpp/src/linear_programming/solve.cu | 2 +- .../utilities/cython_solve.cu | 12 +- cpp/src/mip/CMakeLists.txt | 1 - cpp/src/mip/problem/problem.cuh | 2 - cpp/src/mip/problem/write_mps.cu | 179 ------------- cpp/src/mip/solve.cu | 16 +- 16 files changed, 627 insertions(+), 206 deletions(-) create mode 100644 cpp/libmps_parser/include/mps_parser/mps_writer.hpp create mode 100644 cpp/libmps_parser/include/mps_parser/writer.hpp create mode 100644 cpp/libmps_parser/src/mps_writer.cpp create mode 100644 cpp/libmps_parser/src/mps_writer.hpp create mode 100644 cpp/libmps_parser/src/writer.cpp delete mode 100644 cpp/src/mip/problem/write_mps.cu diff --git a/cpp/include/cuopt/linear_programming/optimization_problem.hpp b/cpp/include/cuopt/linear_programming/optimization_problem.hpp index 77686a337..683c6988e 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem.hpp @@ -302,6 +302,13 @@ class optimization_problem_t { */ void set_row_names(const std::vector& row_names); + /** + * @brief Write the problem to an MPS formatted file + * + * @param[in] mps_file_path Path to the MPS file to write + */ + void write_to_mps(const std::string& mps_file_path); + i_t get_n_variables() const; i_t get_n_constraints() const; i_t get_nnz() const; diff --git a/cpp/libmps_parser/CMakeLists.txt b/cpp/libmps_parser/CMakeLists.txt index 07b68b001..9c8fd14ee 100644 --- a/cpp/libmps_parser/CMakeLists.txt +++ b/cpp/libmps_parser/CMakeLists.txt @@ -66,7 +66,9 @@ add_library(mps_parser SHARED src/data_model_view.cpp src/mps_data_model.cpp src/mps_parser.cpp + src/mps_writer.cpp src/parser.cpp + src/writer.cpp src/utilities/cython_mps_parser.cpp ) diff --git a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp index 05f75f734..b1284defc 100644 --- a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp +++ b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp @@ -186,6 +186,20 @@ class data_model_view_t { * @param[in] problem_name Problem name value. */ void set_problem_name(const std::string& problem_name); + /** + * @brief Set the variables names. + * @note Setting before calling the solver is optional. + * + * @param[in] variable_names Variable names values. + */ + void set_variable_names(const std::vector& variables_names); + /** + * @brief Set the row names. + * @note Setting before calling the solver is optional. + * + * @param[in] row_names Row names value. + */ + void set_row_names(const std::vector& row_names); /** * @brief Set the constraints lower bounds. * @note Setting before calling the solver is optional if you set the row type, else it's @@ -326,6 +340,19 @@ class data_model_view_t { */ span get_initial_dual_solution() const noexcept; + /** + * @brief Get the variable names + * + * @return span + */ + const std::vector& get_variable_names() const noexcept; + /** + * @brief Get the row names + * + * @return span + */ + const std::vector& get_row_names() const noexcept; + /** * @brief Get the problem name * @@ -354,6 +381,8 @@ class data_model_view_t { span row_types_; std::string objective_name_; std::string problem_name_; + std::vector variable_names_; + std::vector row_names_; span constraint_lower_bounds_; span constraint_upper_bounds_; diff --git a/cpp/libmps_parser/include/mps_parser/mps_writer.hpp b/cpp/libmps_parser/include/mps_parser/mps_writer.hpp new file mode 100644 index 000000000..da919a68e --- /dev/null +++ b/cpp/libmps_parser/include/mps_parser/mps_writer.hpp @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace cuopt::mps_parser { + +/** + * @brief Main writer class for MPS files + * + * @tparam f_t data type of the weights and variables + * @tparam i_t data type of the indices + */ +template +class mps_writer_t { + public: + /** + * @brief Ctor. Takes a data model view as input and writes it out as a MPS formatted file + * + * @param[in] problem Data model view to write + * @param[in] file Path to the MPS file to write + */ + mps_writer_t(const data_model_view_t& problem); + + /** + * @brief Writes the problem to an MPS formatted file + * + * @param[in] mps_file_path Path to the MPS file to write + */ + void write(const std::string& mps_file_path); + + private: + const data_model_view_t& problem_; +}; // class mps_writer_t + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/writer.hpp b/cpp/libmps_parser/include/mps_parser/writer.hpp new file mode 100644 index 000000000..8f193af13 --- /dev/null +++ b/cpp/libmps_parser/include/mps_parser/writer.hpp @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +// TODO: we might want to eventually rename libmps_parser to libmps_io +// (or libcuopt_io if we want to support other hypothetical formats) +namespace cuopt::mps_parser { + +/** + * @brief Writes the problem to an MPS formatted file + * + * Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more + * details on both free and fixed MPS format. + * + * @param[in] problem The problem data model view to write + * @param[in] mps_file_path Path to the MPS file to write + */ +template +void write_mps(const data_model_view_t& problem, const std::string& mps_file_path); + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/src/data_model_view.cpp b/cpp/libmps_parser/src/data_model_view.cpp index 4bca8c056..8c81ba679 100644 --- a/cpp/libmps_parser/src/data_model_view.cpp +++ b/cpp/libmps_parser/src/data_model_view.cpp @@ -171,6 +171,19 @@ void data_model_view_t::set_problem_name(const std::string& problem_na problem_name_ = problem_name; } +template +void data_model_view_t::set_variable_names( + const std::vector& variables_names) +{ + variable_names_ = variables_names; +} + +template +void data_model_view_t::set_row_names(const std::vector& row_names) +{ + row_names_ = row_names; +} + template span data_model_view_t::get_constraint_matrix_values() const noexcept { @@ -279,6 +292,18 @@ bool data_model_view_t::get_sense() const noexcept return maximize_; } +template +const std::vector& data_model_view_t::get_variable_names() const noexcept +{ + return variable_names_; +} + +template +const std::vector& data_model_view_t::get_row_names() const noexcept +{ + return row_names_; +} + // NOTE: Explicitly instantiate all types here in order to avoid linker error template class data_model_view_t; diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp new file mode 100644 index 000000000..158663ffe --- /dev/null +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -0,0 +1,248 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace cuopt::mps_parser { + +template +mps_writer_t::mps_writer_t(const data_model_view_t& problem) : problem_(problem) +{ +} + +template +void mps_writer_t::write(const std::string& mps_file_path) +{ + std::ofstream mps_file(mps_file_path); + + mps_parser_expects(mps_file.is_open(), + error_type_t::ValidationError, + "Error creating output MPS file! Given path: %s", + mps_file_path.c_str()); + + i_t n_variables = problem_.get_variable_names().size(); + i_t n_constraints = problem_.get_constraint_upper_bounds().size(); + + std::vector objective_coefficients(problem_.get_objective_coefficients().size()); + std::vector constraint_lower_bounds(problem_.get_constraint_lower_bounds().size()); + std::vector constraint_upper_bounds(problem_.get_constraint_upper_bounds().size()); + std::vector variable_lower_bounds(problem_.get_variable_lower_bounds().size()); + std::vector variable_upper_bounds(problem_.get_variable_upper_bounds().size()); + std::vector variable_types(problem_.get_variable_types().size()); + std::vector row_types(problem_.get_row_types().size()); + std::vector constraint_matrix_offsets(problem_.get_constraint_matrix_offsets().size()); + std::vector constraint_matrix_indices(problem_.get_constraint_matrix_indices().size()); + std::vector constraint_matrix_values(problem_.get_constraint_matrix_values().size()); + + // data_model_view_t may contain device or host pointers(?). Handle both w/ a raft copy + cudaMemcpy(objective_coefficients.data(), + problem_.get_objective_coefficients().data(), + problem_.get_objective_coefficients().size() * sizeof(f_t), + cudaMemcpyDefault); + cudaMemcpy(constraint_lower_bounds.data(), + problem_.get_constraint_lower_bounds().data(), + problem_.get_constraint_lower_bounds().size() * sizeof(f_t), + cudaMemcpyDefault); + cudaMemcpy(constraint_upper_bounds.data(), + problem_.get_constraint_upper_bounds().data(), + problem_.get_constraint_upper_bounds().size() * sizeof(f_t), + cudaMemcpyDefault); + cudaMemcpy(variable_lower_bounds.data(), + problem_.get_variable_lower_bounds().data(), + problem_.get_variable_lower_bounds().size() * sizeof(f_t), + cudaMemcpyDefault); + cudaMemcpy(variable_upper_bounds.data(), + problem_.get_variable_upper_bounds().data(), + problem_.get_variable_upper_bounds().size() * sizeof(f_t), + cudaMemcpyDefault); + cudaMemcpy(variable_types.data(), + problem_.get_variable_types().data(), + problem_.get_variable_types().size() * sizeof(char), + cudaMemcpyDefault); + cudaMemcpy(row_types.data(), + problem_.get_row_types().data(), + problem_.get_row_types().size() * sizeof(char), + cudaMemcpyDefault); + cudaMemcpy(constraint_matrix_offsets.data(), + problem_.get_constraint_matrix_offsets().data(), + problem_.get_constraint_matrix_offsets().size() * sizeof(i_t), + cudaMemcpyDefault); + cudaMemcpy(constraint_matrix_indices.data(), + problem_.get_constraint_matrix_indices().data(), + problem_.get_constraint_matrix_indices().size() * sizeof(i_t), + cudaMemcpyDefault); + cudaMemcpy(constraint_matrix_values.data(), + problem_.get_constraint_matrix_values().data(), + problem_.get_constraint_matrix_values().size() * sizeof(f_t), + cudaMemcpyDefault); + + // save coefficients with full precision + mps_file << std::setprecision(std::numeric_limits::max_digits10); + + // NAME section + mps_file << "NAME " << problem_.get_problem_name() << "\n"; + + if (problem_.get_sense()) { mps_file << "OBJSENSE\n MAXIMIZE\n"; } + + // ROWS section + mps_file << "ROWS\n"; + mps_file << " N " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << "\n"; + for (size_t i = 0; i < (size_t)n_constraints; i++) { + std::string row_name = + i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); + + char type = 'L'; + if (constraint_lower_bounds[i] == constraint_upper_bounds[i]) + type = 'E'; + else if (std::isinf(constraint_upper_bounds[i])) + type = 'G'; + mps_file << " " << type << " " << row_name << "\n"; + } + + // COLUMNS section + mps_file << "COLUMNS\n"; + + // Keep a single integer section marker by going over constraints twice and writing out + // integral/nonintegral nonzeros ordered map + std::map>> integral_col_nnzs; + std::map>> continuous_col_nnzs; + for (size_t row_id = 0; row_id < (size_t)n_constraints; row_id++) { + for (size_t k = (size_t)constraint_matrix_offsets[row_id]; + k < (size_t)constraint_matrix_offsets[row_id + 1]; + k++) { + size_t var = (size_t)constraint_matrix_indices[k]; + if (variable_types[var] == 'I') { + integral_col_nnzs[var].emplace_back(row_id, constraint_matrix_values[k]); + } else { + continuous_col_nnzs[var].emplace_back(row_id, constraint_matrix_values[k]); + } + } + } + + for (size_t is_integral = 0; is_integral < 2; is_integral++) { + auto& col_map = is_integral ? integral_col_nnzs : continuous_col_nnzs; + if (is_integral) mps_file << " MARK0001 'MARKER' 'INTORG'\n"; + for (auto& [var_id, nnzs] : col_map) { + std::string col_name = var_id < problem_.get_variable_names().size() + ? problem_.get_variable_names()[var_id] + : "C" + std::to_string(var_id); + for (auto& nnz : nnzs) { + std::string row_name = nnz.first < problem_.get_row_names().size() + ? problem_.get_row_names()[nnz.first] + : "R" + std::to_string(nnz.first); + mps_file << " " << col_name << " " << row_name << " " << nnz.second << "\n"; + } + // Write objective coefficients + if (objective_coefficients[var_id] != 0.0) { + mps_file << " " << col_name << " " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << " " + << (problem_.get_sense() ? -objective_coefficients[var_id] + : objective_coefficients[var_id]) + << "\n"; + } + } + if (is_integral) mps_file << " MARK0001 'MARKER' 'INTEND'\n"; + } + + // RHS section + mps_file << "RHS\n"; + for (size_t i = 0; i < (size_t)n_constraints; i++) { + std::string row_name = + i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); + + f_t rhs; + if (std::isinf(constraint_lower_bounds[i])) { + rhs = constraint_upper_bounds[i]; + } else if (std::isinf(constraint_upper_bounds[i])) { + rhs = constraint_lower_bounds[i]; + } else { // RANGES, encode the lower bound + rhs = constraint_lower_bounds[i]; + } + + if (std::isfinite(rhs) && rhs != 0.0) { + mps_file << " RHS1 " << row_name << " " << rhs << "\n"; + } + } + if (std::isfinite(problem_.get_objective_offset()) && problem_.get_objective_offset() != 0.0) { + mps_file << " RHS1 " + << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) + << " " << -problem_.get_objective_offset() << "\n"; + } + + // RANGES section if needed + bool has_ranges = false; + for (size_t i = 0; i < (size_t)n_constraints; i++) { + if (constraint_lower_bounds[i] != -std::numeric_limits::infinity() && + constraint_upper_bounds[i] != std::numeric_limits::infinity() && + constraint_lower_bounds[i] != constraint_upper_bounds[i]) { + if (!has_ranges) { + mps_file << "RANGES\n"; + has_ranges = true; + } + std::string row_name = "R" + std::to_string(i); + mps_file << " RNG1 " << row_name << " " + << (constraint_upper_bounds[i] - constraint_lower_bounds[i]) << "\n"; + } + } + + // BOUNDS section + mps_file << "BOUNDS\n"; + for (size_t j = 0; j < (size_t)n_variables; j++) { + std::string col_name = j < problem_.get_variable_names().size() + ? problem_.get_variable_names()[j] + : "C" + std::to_string(j); + + if (variable_lower_bounds[j] == -std::numeric_limits::infinity() && + variable_upper_bounds[j] == std::numeric_limits::infinity()) { + mps_file << " FR BOUND1 " << col_name << "\n"; + } else { + if (variable_lower_bounds[j] != 0.0 || objective_coefficients[j] == 0.0 || + variable_types[j] != 'C') { + if (variable_lower_bounds[j] == -std::numeric_limits::infinity()) { + mps_file << " MI BOUND1 " << col_name << "\n"; + } else { + mps_file << " LO BOUND1 " << col_name << " " << variable_lower_bounds[j] << "\n"; + } + } + if (variable_upper_bounds[j] != std::numeric_limits::infinity()) { + mps_file << " UP BOUND1 " << col_name << " " << variable_upper_bounds[j] << "\n"; + } + } + } + + mps_file << "ENDATA\n"; + mps_file.close(); +} + +template class mps_writer_t; +template class mps_writer_t; + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/src/mps_writer.hpp b/cpp/libmps_parser/src/mps_writer.hpp new file mode 100644 index 000000000..2e0f19668 --- /dev/null +++ b/cpp/libmps_parser/src/mps_writer.hpp @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace cuopt::mps_parser { + +/** + * @brief Different possible types of 'ROWS' + */ +enum RowType { + Objective = 'N', + LesserThanOrEqual = 'L', + GreaterThanOrEqual = 'G', + Equality = 'E', +}; // enum RowType + +/** + * @brief Different possible types of 'BOUNDS' + */ +enum BoundType { + LowerBound, + UpperBound, + Fixed, + Free, + LowerBoundNegInf, + UpperBoundInf, + BinaryVariable, + LowerBoundIntegerVariable, + UpperBoundIntegerVariable, + SemiContiniousVariable, +}; // enum BoundType + +/** + * @brief Different possible types of 'OBJSENSE' + */ +enum ObjSenseType { + Minimize, + Maximize, +}; // enum ObjSenseType + +/** + * @brief Main writer class for MPS files + * + * @tparam f_t data type of the weights and variables + * @tparam i_t data type of the indices + */ +template +class mps_writer_t { + public: + /** + * @brief Ctor. Takes a data model view as input and writes it out as a MPS formatted file + * + * @param[in] problem Data model view to write + * @param[in] file Path to the MPS file to write + */ + mps_writer_t(const data_model_view_t& problem); + + /** + * @brief Writes the problem to an MPS formatted file + * + * @param[in] mps_file_path Path to the MPS file to write + */ + void write(const std::string& mps_file_path); + + private: + const data_model_view_t& problem_; +}; // class mps_writer_t + +} // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/src/writer.cpp b/cpp/libmps_parser/src/writer.cpp new file mode 100644 index 000000000..cf05653c3 --- /dev/null +++ b/cpp/libmps_parser/src/writer.cpp @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +namespace cuopt::mps_parser { + +template +void write_mps(const data_model_view_t& problem, const std::string& mps_file_path) +{ + mps_writer_t writer(problem); + writer.write(mps_file_path); +} + +template void write_mps(const data_model_view_t& problem, + const std::string& mps_file_path); +template void write_mps(const data_model_view_t& problem, + const std::string& mps_file_path); + +} // namespace cuopt::mps_parser diff --git a/cpp/src/linear_programming/optimization_problem.cu b/cpp/src/linear_programming/optimization_problem.cu index ad33eb9b4..84362aa23 100644 --- a/cpp/src/linear_programming/optimization_problem.cu +++ b/cpp/src/linear_programming/optimization_problem.cu @@ -16,10 +16,11 @@ */ #include -#include +#include #include #include +#include #include #include @@ -484,6 +485,87 @@ void optimization_problem_t::set_maximize(bool _maximize) maximize_ = _maximize; } +template +void optimization_problem_t::write_to_mps(const std::string& mps_file_path) +{ + cuopt::mps_parser::data_model_view_t data_model_view; + + // Set optimization sense + data_model_view.set_maximize(get_sense()); + + // Set constraint matrix in CSR format + if (get_nnz() != 0) { + data_model_view.set_csr_constraint_matrix(get_constraint_matrix_values().data(), + get_constraint_matrix_values().size(), + get_constraint_matrix_indices().data(), + get_constraint_matrix_indices().size(), + get_constraint_matrix_offsets().data(), + get_constraint_matrix_offsets().size()); + } + + // Set constraint bounds (RHS) + if (get_n_constraints() != 0) { + data_model_view.set_constraint_bounds(get_constraint_bounds().data(), + get_constraint_bounds().size()); + } + + // Set objective coefficients + if (get_n_variables() != 0) { + data_model_view.set_objective_coefficients(get_objective_coefficients().data(), + get_objective_coefficients().size()); + } + + // Set objective scaling and offset + data_model_view.set_objective_scaling_factor(get_objective_scaling_factor()); + data_model_view.set_objective_offset(get_objective_offset()); + + // Set variable bounds + if (get_n_variables() != 0) { + data_model_view.set_variable_lower_bounds(get_variable_lower_bounds().data(), + get_variable_lower_bounds().size()); + data_model_view.set_variable_upper_bounds(get_variable_upper_bounds().data(), + get_variable_upper_bounds().size()); + } + + // Set row types (constraint types) + if (get_row_types().size() != 0) { + data_model_view.set_row_types(get_row_types().data(), get_row_types().size()); + } + + // Set constraint bounds (lower and upper) + if (get_n_constraints() != 0) { + data_model_view.set_constraint_lower_bounds(get_constraint_lower_bounds().data(), + get_constraint_lower_bounds().size()); + data_model_view.set_constraint_upper_bounds(get_constraint_upper_bounds().data(), + get_constraint_upper_bounds().size()); + } + + // Create a temporary vector to hold the converted variable types + std::vector variable_types(get_n_variables()); + // Set variable types (convert from enum to char) + if (get_n_variables() != 0) { + auto enum_variable_types = cuopt::host_copy(get_variable_types()); + + // Convert enum types to char types + for (size_t i = 0; i < variable_types.size(); ++i) { + variable_types[i] = (enum_variable_types[i] == var_t::INTEGER) ? 'I' : 'C'; + } + + data_model_view.set_variable_types(variable_types.data(), variable_types.size()); + } + + // Set problem and variable names if available + if (!get_problem_name().empty()) { data_model_view.set_problem_name(get_problem_name()); } + + if (!get_objective_name().empty()) { data_model_view.set_objective_name(get_objective_name()); } + + if (!get_variable_names().empty()) { data_model_view.set_variable_names(get_variable_names()); } + + if (!get_row_names().empty()) { data_model_view.set_row_names(get_row_names()); } + + cuopt::mps_parser::write_mps(data_model_view, mps_file_path); +} + // NOTE: Explicitly instantiate all types here in order to avoid linker error #if MIP_INSTANTIATE_FLOAT template class optimization_problem_t; diff --git a/cpp/src/linear_programming/solve.cu b/cpp/src/linear_programming/solve.cu index c06985997..04a5e059c 100644 --- a/cpp/src/linear_programming/solve.cu +++ b/cpp/src/linear_programming/solve.cu @@ -590,7 +590,7 @@ optimization_problem_solution_t solve_lp(optimization_problem_t #include #include +#include +#include +#include #include #include @@ -113,14 +116,7 @@ void write_mps(cuopt::mps_parser::data_model_view_t* data_model, std::string user_problem_file, unsigned int flags) { - cudaStream_t stream; - RAFT_CUDA_TRY(cudaStreamCreateWithFlags(&stream, flags)); - const raft::handle_t handle_{stream}; - - cuopt::linear_programming::solver_settings_t solver_settings; - auto op_problem = data_model_to_optimization_problem(data_model, &solver_settings, &handle_); - - cuopt::linear_programming::write_mps(op_problem, user_problem_file); + cuopt::mps_parser::write_mps(*data_model, user_problem_file); } /** diff --git a/cpp/src/mip/CMakeLists.txt b/cpp/src/mip/CMakeLists.txt index 25c9a18bf..4d01c56db 100644 --- a/cpp/src/mip/CMakeLists.txt +++ b/cpp/src/mip/CMakeLists.txt @@ -17,7 +17,6 @@ set(MIP_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/problem/problem.cu) list(PREPEND MIP_SRC_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/problem/write_mps.cu ${CMAKE_CURRENT_SOURCE_DIR}/solve.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index 8764ca08e..1e9ce4f0c 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -111,8 +111,6 @@ class problem_t { void get_host_user_problem( cuopt::linear_programming::dual_simplex::user_problem_t& user_problem) const; - void write_as_mps(const std::string& path); - struct view_t { DI std::pair reverse_range_for_var(i_t v) const { diff --git a/cpp/src/mip/problem/write_mps.cu b/cpp/src/mip/problem/write_mps.cu deleted file mode 100644 index e779d52ba..000000000 --- a/cpp/src/mip/problem/write_mps.cu +++ /dev/null @@ -1,179 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights - * reserved. SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "problem.cuh" - -#include -#include -#include -#include - -#include - -#include "utilities/copy_helpers.hpp" - -namespace cuopt::linear_programming::detail { - -template -void problem_t::write_as_mps(const std::string& path) -{ - // Get host copies of device data - auto h_reverse_coefficients = cuopt::host_copy(reverse_coefficients, handle_ptr->get_stream()); - auto h_reverse_constraints = cuopt::host_copy(reverse_constraints, handle_ptr->get_stream()); - auto h_reverse_offsets = cuopt::host_copy(reverse_offsets, handle_ptr->get_stream()); - auto h_obj_coeffs = cuopt::host_copy(objective_coefficients, handle_ptr->get_stream()); - auto h_var_lb = cuopt::host_copy(variable_lower_bounds, handle_ptr->get_stream()); - auto h_var_ub = cuopt::host_copy(variable_upper_bounds, handle_ptr->get_stream()); - auto h_cstr_lb = cuopt::host_copy(constraint_lower_bounds, handle_ptr->get_stream()); - auto h_cstr_ub = cuopt::host_copy(constraint_upper_bounds, handle_ptr->get_stream()); - auto h_var_types = cuopt::host_copy(variable_types, handle_ptr->get_stream()); - - std::ofstream mps_file(path); - if (!mps_file.is_open()) { - CUOPT_LOG_ERROR("Could not open file %s for writing", path.c_str()); - return; - } - - // save coefficients with full precision - mps_file << std::setprecision(std::numeric_limits::max_digits10); - - // NAME section - mps_file << "NAME " << original_problem_ptr->get_problem_name() << "\n"; - - if (maximize) { mps_file << "OBJSENSE\n MAXIMIZE\n"; } - - // ROWS section - mps_file << "ROWS\n"; - mps_file << " N " << (objective_name.empty() ? "OBJ" : objective_name) << "\n"; - for (size_t i = 0; i < (size_t)n_constraints; i++) { - std::string row_name = i < row_names.size() ? row_names[i] : "R" + std::to_string(i); - - char type = 'L'; - if (h_cstr_lb[i] == h_cstr_ub[i]) - type = 'E'; - else if (std::isinf(h_cstr_ub[i])) - type = 'G'; - mps_file << " " << type << " " << row_name << "\n"; - } - - // COLUMNS section - mps_file << "COLUMNS\n"; - - bool in_integer_section = false; - for (size_t j = 0; j < (size_t)n_variables; j++) { - std::string col_name = j < var_names.size() ? var_names[j] : "C" + std::to_string(j); - - // Write integer marker if needed - if (h_var_types[j] != var_t::CONTINUOUS) { - if (!in_integer_section) { - mps_file << " MARK0001 'MARKER' 'INTORG'\n"; - in_integer_section = true; - } - } - - // Write objective coefficient if non-zero - if (h_obj_coeffs[j] != 0.0) { - mps_file << " " << col_name << " " << (objective_name.empty() ? "OBJ" : objective_name) - << " " << (maximize ? -h_obj_coeffs[j] : h_obj_coeffs[j]) << "\n"; - } - - // Write constraint coefficients - for (size_t k = (size_t)h_reverse_offsets[j]; k < (size_t)h_reverse_offsets[j + 1]; k++) { - size_t row = (size_t)h_reverse_constraints[k]; - std::string row_name = row < row_names.size() ? row_names[row] : "R" + std::to_string(row); - mps_file << " " << col_name << " " << row_name << " " << h_reverse_coefficients[k] << "\n"; - } - - if (h_var_types[j] != var_t::CONTINUOUS && in_integer_section) { - if (j == (size_t)n_variables - 1 || h_var_types[j + 1] == var_t::CONTINUOUS) { - mps_file << " MARK0001 'MARKER' 'INTEND'\n"; - in_integer_section = false; - } - } - } - - // RHS section - mps_file << "RHS\n"; - for (size_t i = 0; i < (size_t)n_constraints; i++) { - std::string row_name = i < row_names.size() ? row_names[i] : "R" + std::to_string(i); - - f_t rhs; - if (std::isinf(h_cstr_lb[i])) { - rhs = h_cstr_ub[i]; - } else if (std::isinf(h_cstr_ub[i])) { - rhs = h_cstr_lb[i]; - } else { // RANGES, encode the lower bound - rhs = h_cstr_lb[i]; - } - - if (isfinite(rhs) && rhs != 0.0) { - mps_file << " RHS1 " << row_name << " " << rhs << "\n"; - } - } - if (isfinite(objective_offset) && objective_offset != 0.0) { - mps_file << " RHS1 " << (objective_name.empty() ? "OBJ" : objective_name) << " " << -objective_offset << "\n"; - } - - // RANGES section if needed - bool has_ranges = false; - for (size_t i = 0; i < (size_t)n_constraints; i++) { - if (h_cstr_lb[i] != -std::numeric_limits::infinity() && - h_cstr_ub[i] != std::numeric_limits::infinity() && h_cstr_lb[i] != h_cstr_ub[i]) { - if (!has_ranges) { - mps_file << "RANGES\n"; - has_ranges = true; - } - std::string row_name = i < row_names.size() ? row_names[i] : "R" + std::to_string(i); - mps_file << " RNG1 " << row_name << " " << (h_cstr_ub[i] - h_cstr_lb[i]) << "\n"; - } - } - - // BOUNDS section - mps_file << "BOUNDS\n"; - for (size_t j = 0; j < (size_t)n_variables; j++) { - std::string col_name = j < var_names.size() ? var_names[j] : "C" + std::to_string(j); - - if (h_var_lb[j] == -std::numeric_limits::infinity() && - h_var_ub[j] == std::numeric_limits::infinity()) { - mps_file << " FR BOUND1 " << col_name << "\n"; - } else { - if (h_var_lb[j] != 0.0 || h_obj_coeffs[j] == 0.0 || h_var_types[j] != var_t::CONTINUOUS) { - if (h_var_lb[j] == -std::numeric_limits::infinity()) { - mps_file << " MI BOUND1 " << col_name << "\n"; - } else { - mps_file << " LO BOUND1 " << col_name << " " << h_var_lb[j] << "\n"; - } - } - if (h_var_ub[j] != std::numeric_limits::infinity()) { - mps_file << " UP BOUND1 " << col_name << " " << h_var_ub[j] << "\n"; - } - } - } - - mps_file << "ENDATA\n"; - mps_file.close(); -} - -#if MIP_INSTANTIATE_FLOAT -template void problem_t::write_as_mps(const std::string& path); -#endif - -#if MIP_INSTANTIATE_DOUBLE -template void problem_t::write_as_mps(const std::string& path); -#endif - -} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/solve.cu b/cpp/src/mip/solve.cu index f2077c1b9..e5c1890e2 100644 --- a/cpp/src/mip/solve.cu +++ b/cpp/src/mip/solve.cu @@ -178,7 +178,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, detail::problem_t problem(op_problem, settings.get_tolerances()); if (settings.user_problem_file != "") { CUOPT_LOG_INFO("Writing user problem to file: %s", settings.user_problem_file.c_str()); - problem.write_as_mps(settings.user_problem_file); + op_problem.write_to_mps(settings.user_problem_file); } // this is for PDLP, i think this should be part of pdlp solver @@ -203,14 +203,6 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, } } -template -void write_mps(optimization_problem_t& op_problem, - std::string user_problem_file) -{ - detail::problem_t problem(op_problem); - problem.write_as_mps(user_problem_file); -} - template mip_solution_t solve_mip( raft::handle_t const* handle_ptr, @@ -229,11 +221,7 @@ mip_solution_t solve_mip( template mip_solution_t solve_mip( \ raft::handle_t const* handle_ptr, \ const cuopt::mps_parser::mps_data_model_t& mps_data_model, \ - mip_solver_settings_t const& settings); \ - \ - template void write_mps( \ - optimization_problem_t& op_problem, \ - std::string user_problem_file); + mip_solver_settings_t const& settings); #if MIP_INSTANTIATE_FLOAT INSTANTIATE(float) From 519063ab392081ec1e42a4ebdf2211394fbc1045 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 4 Sep 2025 09:23:29 -0700 Subject: [PATCH 06/13] move write mps to data model from solver --- .../cuopt/linear_programming/solve.hpp | 3 - .../utilities/cython_solve.hpp | 4 - cpp/libmps_parser/src/mps_writer.cpp | 86 +++++----- .../utilities/cython_solve.cu | 7 - .../cuopt/linear_programming/__init__.py | 2 +- .../data_model/data_model.pxd | 11 +- .../data_model/data_model.py | 4 + .../data_model/data_model_wrapper.pyx | 149 +++++++++++++++++- .../cuopt/cuopt/linear_programming/problem.py | 2 +- .../linear_programming/solver/__init__.py | 2 +- .../linear_programming/solver/solver.pxd | 4 - .../cuopt/linear_programming/solver/solver.py | 5 - .../solver/solver_wrapper.pyx | 9 -- 13 files changed, 204 insertions(+), 84 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/solve.hpp b/cpp/include/cuopt/linear_programming/solve.hpp index 60b062cb1..a01126a7e 100644 --- a/cpp/include/cuopt/linear_programming/solve.hpp +++ b/cpp/include/cuopt/linear_programming/solve.hpp @@ -117,7 +117,4 @@ optimization_problem_t mps_data_model_to_optimization_problem( raft::handle_t const* handle_ptr, const cuopt::mps_parser::mps_data_model_t& data_model); -template -void write_mps(optimization_problem_t& op_problem, - std::string); } // namespace cuopt::linear_programming diff --git a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp index a74d8d8d8..15c70278c 100644 --- a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp @@ -102,10 +102,6 @@ struct solver_ret_t { // Wrapper for solve to expose the API to cython. -void write_mps(cuopt::mps_parser::data_model_view_t*, - std::string, - unsigned int flags = cudaStreamNonBlocking); - std::unique_ptr call_solve(cuopt::mps_parser::data_model_view_t*, linear_programming::solver_settings_t*, unsigned int flags = cudaStreamNonBlocking, diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index 158663ffe..7846442f2 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace cuopt::mps_parser { @@ -45,12 +46,17 @@ void mps_writer_t::write(const std::string& mps_file_path) "Error creating output MPS file! Given path: %s", mps_file_path.c_str()); - i_t n_variables = problem_.get_variable_names().size(); - i_t n_constraints = problem_.get_constraint_upper_bounds().size(); + i_t n_variables = problem_.get_variable_lower_bounds().size(); + i_t n_constraints; + if (problem_.get_constraint_bounds().size() > 0) + n_constraints = problem_.get_constraint_bounds().size(); + else + n_constraints = problem_.get_constraint_lower_bounds().size(); std::vector objective_coefficients(problem_.get_objective_coefficients().size()); - std::vector constraint_lower_bounds(problem_.get_constraint_lower_bounds().size()); - std::vector constraint_upper_bounds(problem_.get_constraint_upper_bounds().size()); + std::vector constraint_lower_bounds(n_constraints); + std::vector constraint_upper_bounds(n_constraints); + std::vector constraint_bounds(problem_.get_constraint_bounds().size()); std::vector variable_lower_bounds(problem_.get_variable_lower_bounds().size()); std::vector variable_upper_bounds(problem_.get_variable_upper_bounds().size()); std::vector variable_types(problem_.get_variable_types().size()); @@ -59,47 +65,32 @@ void mps_writer_t::write(const std::string& mps_file_path) std::vector constraint_matrix_indices(problem_.get_constraint_matrix_indices().size()); std::vector constraint_matrix_values(problem_.get_constraint_matrix_values().size()); - // data_model_view_t may contain device or host pointers(?). Handle both w/ a raft copy - cudaMemcpy(objective_coefficients.data(), - problem_.get_objective_coefficients().data(), - problem_.get_objective_coefficients().size() * sizeof(f_t), - cudaMemcpyDefault); - cudaMemcpy(constraint_lower_bounds.data(), - problem_.get_constraint_lower_bounds().data(), - problem_.get_constraint_lower_bounds().size() * sizeof(f_t), - cudaMemcpyDefault); - cudaMemcpy(constraint_upper_bounds.data(), - problem_.get_constraint_upper_bounds().data(), - problem_.get_constraint_upper_bounds().size() * sizeof(f_t), - cudaMemcpyDefault); - cudaMemcpy(variable_lower_bounds.data(), - problem_.get_variable_lower_bounds().data(), - problem_.get_variable_lower_bounds().size() * sizeof(f_t), - cudaMemcpyDefault); - cudaMemcpy(variable_upper_bounds.data(), - problem_.get_variable_upper_bounds().data(), - problem_.get_variable_upper_bounds().size() * sizeof(f_t), - cudaMemcpyDefault); - cudaMemcpy(variable_types.data(), - problem_.get_variable_types().data(), - problem_.get_variable_types().size() * sizeof(char), - cudaMemcpyDefault); - cudaMemcpy(row_types.data(), - problem_.get_row_types().data(), - problem_.get_row_types().size() * sizeof(char), - cudaMemcpyDefault); - cudaMemcpy(constraint_matrix_offsets.data(), - problem_.get_constraint_matrix_offsets().data(), - problem_.get_constraint_matrix_offsets().size() * sizeof(i_t), - cudaMemcpyDefault); - cudaMemcpy(constraint_matrix_indices.data(), - problem_.get_constraint_matrix_indices().data(), - problem_.get_constraint_matrix_indices().size() * sizeof(i_t), - cudaMemcpyDefault); - cudaMemcpy(constraint_matrix_values.data(), - problem_.get_constraint_matrix_values().data(), - problem_.get_constraint_matrix_values().size() * sizeof(f_t), - cudaMemcpyDefault); + std::copy(problem_.get_objective_coefficients().data(), problem_.get_objective_coefficients().data() + problem_.get_objective_coefficients().size(), objective_coefficients.data()); + std::copy(problem_.get_constraint_bounds().data(), problem_.get_constraint_bounds().data() + problem_.get_constraint_bounds().size(), constraint_bounds.data()); + std::copy(problem_.get_variable_lower_bounds().data(), problem_.get_variable_lower_bounds().data() + problem_.get_variable_lower_bounds().size(), variable_lower_bounds.data()); + std::copy(problem_.get_variable_upper_bounds().data(), problem_.get_variable_upper_bounds().data() + problem_.get_variable_upper_bounds().size(), variable_upper_bounds.data()); + std::copy(problem_.get_variable_types().data(), problem_.get_variable_types().data() + problem_.get_variable_types().size(), variable_types.data()); + std::copy(problem_.get_row_types().data(), problem_.get_row_types().data() + problem_.get_row_types().size(), row_types.data()); + std::copy(problem_.get_constraint_matrix_offsets().data(), problem_.get_constraint_matrix_offsets().data() + problem_.get_constraint_matrix_offsets().size(), constraint_matrix_offsets.data()); + std::copy(problem_.get_constraint_matrix_indices().data(), problem_.get_constraint_matrix_indices().data() + problem_.get_constraint_matrix_indices().size(), constraint_matrix_indices.data()); + std::copy(problem_.get_constraint_matrix_values().data(), problem_.get_constraint_matrix_values().data() + problem_.get_constraint_matrix_values().size(), constraint_matrix_values.data()); + + if (problem_.get_constraint_lower_bounds().size() == 0 || problem_.get_constraint_upper_bounds().size() == 0) { + for (size_t i = 0; i < (size_t)n_constraints; i++) { + constraint_lower_bounds[i] = constraint_bounds[i]; + constraint_upper_bounds[i] = constraint_bounds[i]; + if (row_types[i] == 'L') { + constraint_lower_bounds[i] = -std::numeric_limits::infinity(); + } + else if (row_types[i] == 'G') { + constraint_upper_bounds[i] = std::numeric_limits::infinity(); + } + } + } + else { + std::copy(problem_.get_constraint_lower_bounds().data(), problem_.get_constraint_lower_bounds().data() + problem_.get_constraint_lower_bounds().size(), constraint_lower_bounds.data()); + std::copy(problem_.get_constraint_upper_bounds().data(), problem_.get_constraint_upper_bounds().data() + problem_.get_constraint_upper_bounds().size(), constraint_upper_bounds.data()); + } // save coefficients with full precision mps_file << std::setprecision(std::numeric_limits::max_digits10); @@ -117,7 +108,6 @@ void mps_writer_t::write(const std::string& mps_file_path) for (size_t i = 0; i < (size_t)n_constraints; i++) { std::string row_name = i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); - char type = 'L'; if (constraint_lower_bounds[i] == constraint_upper_bounds[i]) type = 'E'; @@ -179,7 +169,9 @@ void mps_writer_t::write(const std::string& mps_file_path) i < problem_.get_row_names().size() ? problem_.get_row_names()[i] : "R" + std::to_string(i); f_t rhs; - if (std::isinf(constraint_lower_bounds[i])) { + if (constraint_bounds.size() > 0) + rhs = constraint_bounds[i]; + else if (std::isinf(constraint_lower_bounds[i])) { rhs = constraint_upper_bounds[i]; } else if (std::isinf(constraint_upper_bounds[i])) { rhs = constraint_lower_bounds[i]; diff --git a/cpp/src/linear_programming/utilities/cython_solve.cu b/cpp/src/linear_programming/utilities/cython_solve.cu index fbcc5eb26..2913501ac 100644 --- a/cpp/src/linear_programming/utilities/cython_solve.cu +++ b/cpp/src/linear_programming/utilities/cython_solve.cu @@ -112,13 +112,6 @@ data_model_to_optimization_problem( return op_problem; } -void write_mps(cuopt::mps_parser::data_model_view_t* data_model, - std::string user_problem_file, - unsigned int flags) -{ - cuopt::mps_parser::write_mps(*data_model, user_problem_file); -} - /** * @brief Wrapper for linear_programming to expose the API to cython * diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index e781fac64..7941ad911 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -17,7 +17,7 @@ from cuopt.linear_programming.data_model import DataModel from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution -from cuopt.linear_programming.solver import BatchSolve, Solve, writeMPS +from cuopt.linear_programming.solver import BatchSolve, Solve from cuopt.linear_programming.solver_settings import ( PDLPSolverMode, SolverMethod, diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index 8f5911bf0..39d4fcb00 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -20,7 +20,7 @@ # cython: language_level = 3 from libcpp cimport bool - +from libcpp.string cimport string cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" nogil: # noqa @@ -56,3 +56,12 @@ cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" i_t size) except + void set_row_types(const char* row_types, i_t size) except + void set_variable_types(const char* var_types, i_t size) except + + +cdef extern from "mps_parser/writer.hpp" namespace "cuopt::mps_parser" nogil: # noqa + + cdef void write_mps(const data_model_view_t[int, double] data_model, + const string user_problem_file) except + + +#cdef extern from "cuopt/linear_programming/utilities/cython_solve.hpp" namespace "cuopt::cython": # noqa +# cdef void write_mps(data_model_view_t[int, double]* data_model, +# string user_problem_file) except + diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index 0301b3603..fef02eb5b 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -603,3 +603,7 @@ def get_row_names(self): """ return super().get_row_names() + + @catch_cuopt_exception + def writeMPS(self, user_problem_file): + return super().writeMPS(user_problem_file) diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 11f3308bb..4f13b3354 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -19,12 +19,14 @@ # cython: embedsignature = True # cython: language_level = 3 -from .data_model cimport data_model_view_t +from .data_model cimport data_model_view_t, write_mps import warnings import numpy as np +import cudf +from libc.stdint cimport uintptr_t from libcpp.memory cimport unique_ptr from libcpp.utility cimport move @@ -42,6 +44,17 @@ def type_cast(np_obj, np_type, name): return np_obj +def get_data_ptr(array): + if isinstance(array, cudf.Series): + return array.__cuda_array_interface__['data'][0] + elif isinstance(array, np.ndarray): + return array.__array_interface__['data'][0] + else: + raise Exception( + "get_data_ptr must be called with cudf.Series or np.ndarray" + ) + + cdef class DataModel: def __init__(self): @@ -189,3 +202,137 @@ cdef class DataModel: def get_row_names(self): return self.row_names + + + def set_data_model_view(self): + cdef data_model_view_t[int, double]* c_data_model_view = ( + self.c_data_model_view.get() + ) + + # Set self.fields on the C++ side if set on the Python side + cdef uintptr_t c_A_values = ( + get_data_ptr(self.get_constraint_matrix_values()) + ) + cdef uintptr_t c_A_indices = ( + get_data_ptr(self.get_constraint_matrix_indices()) + ) + cdef uintptr_t c_A_offsets = ( + get_data_ptr(self.get_constraint_matrix_offsets()) + ) + if self.get_constraint_matrix_values().shape[0] != 0 and self.get_constraint_matrix_indices().shape[0] != 0 and self.get_constraint_matrix_offsets().shape[0] != 0: # noqa + c_data_model_view.set_csr_constraint_matrix( + c_A_values, + self.get_constraint_matrix_values().shape[0], + c_A_indices, + self.get_constraint_matrix_indices().shape[0], + c_A_offsets, + self.get_constraint_matrix_offsets().shape[0] + ) + + cdef uintptr_t c_b = ( + get_data_ptr(self.get_constraint_bounds()) + ) + if self.get_constraint_bounds().shape[0] != 0: + c_data_model_view.set_constraint_bounds( + c_b, + self.get_constraint_bounds().shape[0] + ) + + cdef uintptr_t c_c = ( + get_data_ptr(self.get_objective_coefficients()) + ) + if self.get_objective_coefficients().shape[0] != 0: + c_data_model_view.set_objective_coefficients( + c_c, + self.get_objective_coefficients().shape[0] + ) + + c_data_model_view.set_objective_scaling_factor( + self.get_objective_scaling_factor() + ) + c_data_model_view.set_objective_offset( + self.get_objective_offset() + ) + c_data_model_view.set_maximize( self.maximize) + + cdef uintptr_t c_variable_lower_bounds = ( + get_data_ptr(self.get_variable_lower_bounds()) + ) + if self.get_variable_lower_bounds().shape[0] != 0: + c_data_model_view.set_variable_lower_bounds( + c_variable_lower_bounds, + self.get_variable_lower_bounds().shape[0] + ) + + cdef uintptr_t c_variable_upper_bounds = ( + get_data_ptr(self.get_variable_upper_bounds()) + ) + if self.get_variable_upper_bounds().shape[0] != 0: + c_data_model_view.set_variable_upper_bounds( + c_variable_upper_bounds, + self.get_variable_upper_bounds().shape[0] + ) + cdef uintptr_t c_constraint_lower_bounds = ( + get_data_ptr(self.get_constraint_lower_bounds()) + ) + if self.get_constraint_lower_bounds().shape[0] != 0: + c_data_model_view.set_constraint_lower_bounds( + c_constraint_lower_bounds, + self.get_constraint_lower_bounds().shape[0] + ) + cdef uintptr_t c_constraint_upper_bounds = ( + get_data_ptr(self.get_constraint_upper_bounds()) + ) + if self.get_constraint_upper_bounds().shape[0] != 0: + c_data_model_view.set_constraint_upper_bounds( + c_constraint_upper_bounds, + self.get_constraint_upper_bounds().shape[0] + ) + cdef uintptr_t c_row_types = ( + get_data_ptr(self.get_ascii_row_types()) + ) + if self.get_ascii_row_types().shape[0] != 0: + c_data_model_view.set_row_types( + c_row_types, + self.get_ascii_row_types().shape[0] + ) + + cdef uintptr_t c_var_types = ( + get_data_ptr(self.get_variable_types()) + ) + if self.get_variable_types().shape[0] != 0: + c_data_model_view.set_variable_types( + c_var_types, + self.get_variable_types().shape[0] + ) + + # Set initial solution on the C++ side if set on the Python side + cdef uintptr_t c_initial_primal_solution = ( + get_data_ptr(self.get_initial_primal_solution()) + ) + if self.get_initial_primal_solution().shape[0] != 0: + c_data_model_view.set_initial_primal_solution( + c_initial_primal_solution, + self.get_initial_primal_solution().shape[0] + ) + cdef uintptr_t c_initial_dual_solution = ( + get_data_ptr(self.get_initial_dual_solution()) + ) + if self.get_initial_dual_solution().shape[0] != 0: + c_data_model_view.set_initial_dual_solution( + c_initial_dual_solution, + self.get_initial_dual_solution().shape[0] + ) + + def writeMPS(self, user_problem_file): + self.variable_types = type_cast( + self.variable_types, "S1", "variable_types" + ) + self.set_data_model_view() + cdef int a = 10 + cdef int* b = &a + #cdef data_model_view_t[int, double] abc + #cdef data_model_view_t[int, double] abc = self.c_data_model_view.get()[0] + #cdef data_model_view_t[int, double] abc = self.c_data_model_view.get() + + write_mps(self.c_data_model_view.get()[0], user_problem_file.encode('utf-8')) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 9589ce122..dab7111b0 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -972,7 +972,7 @@ def readMPS(cls, mps_file): def writeMPS(self, mps_file): if self.model is None: self._to_data_model() - solver.writeMPS(self.model, mps_file) + self.model.writeMPS(mps_file) @property def NumVariables(self): diff --git a/python/cuopt/cuopt/linear_programming/solver/__init__.py b/python/cuopt/cuopt/linear_programming/solver/__init__.py index 92e37de4e..dbce0419b 100644 --- a/python/cuopt/cuopt/linear_programming/solver/__init__.py +++ b/python/cuopt/cuopt/linear_programming/solver/__init__.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cuopt.linear_programming.solver.solver import BatchSolve, Solve, writeMPS +from cuopt.linear_programming.solver.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.pxd b/python/cuopt/cuopt/linear_programming/solver/solver.pxd index 3a7dbf56f..255fcd764 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.pxd +++ b/python/cuopt/cuopt/linear_programming/solver/solver.pxd @@ -191,10 +191,6 @@ cdef extern from "cuopt/linear_programming/utilities/cython_solve.hpp" namespace solver_settings_t[int, double]* solver_settings, ) except + - #cdef extern from "cuopt/linear_programming/utilities/cython_solve.hpp" namespace "cuopt::cython": # noqa - cdef void write_mps(data_model_view_t[int, double]* data_model, - string user_problem_file) except + - cdef pair[vector[unique_ptr[solver_ret_t]], double] call_batch_solve( # noqa vector[data_model_view_t[int, double] *] data_models, solver_settings_t[int, double]* solver_settings, diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 0f07d436a..12921ae7c 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -180,8 +180,3 @@ def BatchSolve(data_model_list, solver_settings=None): solver_settings = SolverSettings() return solver_wrapper.BatchSolve(data_model_list, solver_settings) - - -@catch_cuopt_exception -def writeMPS(data_model, user_problem_file): - return solver_wrapper.writeMPS(data_model, user_problem_file) diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 614ec3f39..2f0f4fa29 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -44,7 +44,6 @@ from cuopt.linear_programming.data_model.data_model_wrapper cimport DataModel from cuopt.linear_programming.solver.solver cimport ( call_batch_solve, call_solve, - write_mps, error_type_t, mip_termination_status_t, pdlp_solver_mode_t, @@ -661,14 +660,6 @@ cdef create_solution(unique_ptr[solver_ret_t] sol_ret_ptr, solved_by_pdlp=solved_by_pdlp, ) - -def writeMPS(py_data_model_obj, user_problem_file): - cdef DataModel data_model_obj = py_data_model_obj - data_model_obj.variable_types = type_cast( - data_model_obj.variable_types, "S1", "variable_types" - ) - set_data_model_view(data_model_obj) - write_mps(data_model_obj.c_data_model_view.get(), user_problem_file.encode('utf-8')) def Solve(py_data_model_obj, settings, mip=False): From 73b3b1f187009fe864601d48f4e7b8e8bc2c986b Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 4 Sep 2025 20:32:43 -0700 Subject: [PATCH 07/13] add problem name, row name and var name to API mps --- .../cuopt_mps_parser/parser.pxd | 2 + .../cuopt_mps_parser/parser_wrapper.pyx | 3 +- .../data_model/data_model.pxd | 7 ++++ .../data_model/data_model.py | 28 +++++++++++++ .../data_model/data_model_wrapper.pyx | 41 ++++++++++++++++--- .../cuopt/cuopt/linear_programming/problem.py | 16 ++++++-- .../linear_programming/test_python_API.py | 7 ++-- 7 files changed, 90 insertions(+), 14 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd index 3ab51cd7a..7a9950387 100644 --- a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser.pxd @@ -44,6 +44,8 @@ cdef extern from "mps_parser/mps_data_model.hpp" namespace "cuopt::mps_parser": vector[string] var_names_ vector[string] row_names_ vector[char] row_types_ + string objective_name_ + string problem_name_ cdef extern from "mps_parser/utilities/cython_mps_parser.hpp" namespace "cuopt::cython": # noqa diff --git a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx index 9b150a7eb..9a29cf8a8 100644 --- a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx @@ -41,7 +41,6 @@ def type_cast(np_obj, np_type, name): np_obj = np_obj.astype(np.dtype(np_type)) return np_obj - @catch_mps_parser_exception def ParseMps(mps_file_path, fixed_mps_formats): data_model = DataModel() @@ -129,5 +128,7 @@ def ParseMps(mps_file_path, fixed_mps_formats): data_model.set_row_types(row_types) data_model.set_variable_names(var_names_) data_model.set_row_names(row_names_) + data_model.set_objective_name(dm_ret.objective_name_.decode()) + data_model.set_problem_name(dm_ret.problem_name_.decode()) return data_model diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index 39d4fcb00..7bdec0aee 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -21,6 +21,8 @@ from libcpp cimport bool from libcpp.string cimport string +from libcpp.vector cimport vector + cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" nogil: # noqa @@ -56,6 +58,11 @@ cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" i_t size) except + void set_row_types(const char* row_types, i_t size) except + void set_variable_types(const char* var_types, i_t size) except + + void set_variable_names(const vector[string] variables_names) except + + void set_row_names(const vector[string] row_names) except + + void set_problem_name(const string problem_name) except + + void set_objective_name(const string objective_name) except + + cdef extern from "mps_parser/writer.hpp" namespace "cuopt::mps_parser" nogil: # noqa diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index fef02eb5b..d5e43302d 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -411,6 +411,20 @@ def set_row_names(self, row_names): """ super().set_row_names(row_names) + @catch_cuopt_exception + def set_objective_name(self, objective_name): + """ + Set the objective name as string. + """ + super().set_objective_name(objective_name) + + @catch_cuopt_exception + def set_problem_name(self, problem_name): + """ + Set the problem name as string. + """ + super().set_problem_name(problem_name) + @catch_cuopt_exception def set_initial_primal_solution(self, initial_primal_solution): """ @@ -604,6 +618,20 @@ def get_row_names(self): """ return super().get_row_names() + @catch_cuopt_exception + def get_objective_name(self): + """ + Get the objective name as string. + """ + return super().get_objective_name() + + @catch_cuopt_exception + def get_problem_name(self): + """ + Get the problem name as string. + """ + return super().get_problem_name() + @catch_cuopt_exception def writeMPS(self, user_problem_file): return super().writeMPS(user_problem_file) diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 4f13b3354..637ea369b 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -146,6 +146,12 @@ cdef class DataModel: def set_row_names(self, row_names): self.row_names = row_names + def set_objective_name(self, objective_name): + self.objective_name = objective_name + + def set_problem_name(self, problem_name): + self.problem_name = problem_name + def get_sense(self): return self.maximize @@ -203,6 +209,11 @@ cdef class DataModel: def get_row_names(self): return self.row_names + def get_objective_name(self): + return self.objective_name + + def get_problem_name(self): + return self.problem_name def set_data_model_view(self): cdef data_model_view_t[int, double]* c_data_model_view = ( @@ -306,6 +317,30 @@ cdef class DataModel: self.get_variable_types().shape[0] ) + cdef vector[string] c_var_names + for s in self.get_variable_names(): + c_var_names.push_back(s.encode()) + + if len(self.get_variable_names()) != 0: + c_data_model_view.set_variable_names( + c_var_names + ) + + cdef vector[string] c_row_names + for s in self.get_row_names(): + c_row_names.push_back(s.encode()) + + if len(self.get_row_names()) != 0: + c_data_model_view.set_row_names( + c_row_names + ) + + if self.get_problem_name(): + c_data_model_view.set_problem_name(self.get_problem_name().encode()) + + if self.get_objective_name(): + c_data_model_view.set_objective_name( self.get_objective_name().encode()) + # Set initial solution on the C++ side if set on the Python side cdef uintptr_t c_initial_primal_solution = ( get_data_ptr(self.get_initial_primal_solution()) @@ -329,10 +364,4 @@ cdef class DataModel: self.variable_types, "S1", "variable_types" ) self.set_data_model_view() - cdef int a = 10 - cdef int* b = &a - #cdef data_model_view_t[int, double] abc - #cdef data_model_view_t[int, double] abc = self.c_data_model_view.get()[0] - #cdef data_model_view_t[int, double] abc = self.c_data_model_view.get() - write_mps(self.c_data_model_view.get()[0], user_problem_file.encode('utf-8')) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index dab7111b0..c74f85efd 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -716,6 +716,7 @@ def __init__(self, mdict): setattr(self, key, value) def _from_data_model(self, dm): + self.Name = dm.get_problem_name() obj_coeffs = dm.get_objective_coefficients() obj_constant = dm.get_objective_offset() num_vars = len(obj_coeffs) @@ -743,6 +744,7 @@ def _from_data_model(self, dm): c_lb = dm.get_constraint_lower_bounds() c_ub = dm.get_constraint_upper_bounds() c_b = dm.get_constraint_bounds() + c_names = dm.get_row_names() offsets = dm.get_constraint_matrix_offsets() indices = dm.get_constraint_matrix_indices() values = dm.get_constraint_matrix_values() @@ -756,11 +758,11 @@ def _from_data_model(self, dm): c_vars = [vars[j] for j in c_indices] expr = LinearExpression(c_vars, c_coeffs, 0.0) if c_lb[i] == c_ub[i]: - self.addConstraint(expr == c_b[i]) + self.addConstraint(expr == c_b[i], name=c_names[i]) elif c_lb[i] == c_b[i]: - self.addConstraint(expr >= c_b[i]) + self.addConstraint(expr >= c_b[i], name=c_names[i]) elif c_ub[i] == c_b[i]: - self.addConstraint(expr <= c_b[i]) + self.addConstraint(expr <= c_b[i], name=c_names[i]) else: raise Exception("Couldn't initialize constraints") @@ -769,6 +771,7 @@ def _to_data_model(self): n = len(self.vars) self.rhs = [] self.row_sense = [] + self.row_names = [] if self.constraint_csr_matrix is None: csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} @@ -778,6 +781,7 @@ def _to_data_model(self): csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) self.rhs.append(constr.RHS) self.row_sense.append(constr.Sense) + self.row_names.append(constr.ConstraintName) self.constraint_csr_matrix = csr_dict else: @@ -788,12 +792,14 @@ def _to_data_model(self): self.objective = np.zeros(n) self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) self.var_type = np.empty(n, dtype="S1") + self.var_names = [] for j in range(n): self.objective[j] = self.vars[j].getObjectiveCoefficient() self.var_type[j] = self.vars[j].getVariableType() self.lower_bound[j] = self.vars[j].getLowerBound() self.upper_bound[j] = self.vars[j].getUpperBound() + self.var_names.append(self.vars[j].VariableName) # Initialize datamodel dm = data_model.DataModel() @@ -811,6 +817,10 @@ def _to_data_model(self): dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) dm.set_variable_types(self.var_type) + dm.set_variable_names(self.var_names) + dm.set_row_names(self.row_names) + dm.set_problem_name(self.Name) + self.model = dm def reset_solved_values(self): diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index e0dbda214..04e343527 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -281,7 +281,7 @@ def test_read_write_mps_and_relaxation(): x5 = m.addVariable(name="x5", lb=0.0, vtype=INTEGER) # Objective (minimize) - m.setObjective(2*x1 + 3*x2 + 0*x3 + 1*x4 + 4*x5, MINIMIZE) + m.setObjective(2*x1 + 3*x2 + x3 + 1*x4 + 4*x5, MINIMIZE) # Constraints (5 total) m.addConstraint(x1 + x2 + x3 <= 10, name="c1") @@ -291,12 +291,11 @@ def test_read_write_mps_and_relaxation(): m.addConstraint(x1 + x2 + x3 + x4 + x5 >= 5, name="c5") # Write MPS - ss = SolverSettings() - ss.set_parameter(CUOPT_USER_PROBLEM_FILE, "small_mip.mps") - m.solve(ss) + m.writeMPS("small_mip.mps") # Read MPS and solve prob = Problem.readMPS("small_mip.mps") + assert prob.IsMIP prob.solve() expected_values_mip = [0.0, 1.0, 3.0, 0.0, 2.0] From 0b570e300f4ce532f8a756170b92ea4e461b5760 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 4 Sep 2025 20:39:35 -0700 Subject: [PATCH 08/13] fix error on merge --- cpp/libmps_parser/src/data_model_view.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/libmps_parser/src/data_model_view.cpp b/cpp/libmps_parser/src/data_model_view.cpp index 40e047561..25558e37e 100644 --- a/cpp/libmps_parser/src/data_model_view.cpp +++ b/cpp/libmps_parser/src/data_model_view.cpp @@ -329,6 +329,7 @@ template const std::vector& data_model_view_t::get_row_names() const noexcept { return row_names_; +} // QPS-specific getter implementations template From 383a8b8f08a4685eb880d49dd7190222440f4597 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 5 Sep 2025 12:25:17 +0000 Subject: [PATCH 09/13] fix mps_writer not working w/ device vectors --- cpp/libmps_parser/src/mps_writer.cpp | 71 +++++++++----- cpp/libmps_parser/src/mps_writer.hpp | 93 ------------------- .../optimization_problem.cu | 49 ++++++---- 3 files changed, 79 insertions(+), 134 deletions(-) delete mode 100644 cpp/libmps_parser/src/mps_writer.hpp diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index 7846442f2..adf563fea 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -20,14 +20,12 @@ #include #include -#include - #include #include #include +#include #include #include -#include namespace cuopt::mps_parser { @@ -46,7 +44,7 @@ void mps_writer_t::write(const std::string& mps_file_path) "Error creating output MPS file! Given path: %s", mps_file_path.c_str()); - i_t n_variables = problem_.get_variable_lower_bounds().size(); + i_t n_variables = problem_.get_variable_lower_bounds().size(); i_t n_constraints; if (problem_.get_constraint_bounds().size() > 0) n_constraints = problem_.get_constraint_bounds().size(); @@ -65,31 +63,60 @@ void mps_writer_t::write(const std::string& mps_file_path) std::vector constraint_matrix_indices(problem_.get_constraint_matrix_indices().size()); std::vector constraint_matrix_values(problem_.get_constraint_matrix_values().size()); - std::copy(problem_.get_objective_coefficients().data(), problem_.get_objective_coefficients().data() + problem_.get_objective_coefficients().size(), objective_coefficients.data()); - std::copy(problem_.get_constraint_bounds().data(), problem_.get_constraint_bounds().data() + problem_.get_constraint_bounds().size(), constraint_bounds.data()); - std::copy(problem_.get_variable_lower_bounds().data(), problem_.get_variable_lower_bounds().data() + problem_.get_variable_lower_bounds().size(), variable_lower_bounds.data()); - std::copy(problem_.get_variable_upper_bounds().data(), problem_.get_variable_upper_bounds().data() + problem_.get_variable_upper_bounds().size(), variable_upper_bounds.data()); - std::copy(problem_.get_variable_types().data(), problem_.get_variable_types().data() + problem_.get_variable_types().size(), variable_types.data()); - std::copy(problem_.get_row_types().data(), problem_.get_row_types().data() + problem_.get_row_types().size(), row_types.data()); - std::copy(problem_.get_constraint_matrix_offsets().data(), problem_.get_constraint_matrix_offsets().data() + problem_.get_constraint_matrix_offsets().size(), constraint_matrix_offsets.data()); - std::copy(problem_.get_constraint_matrix_indices().data(), problem_.get_constraint_matrix_indices().data() + problem_.get_constraint_matrix_indices().size(), constraint_matrix_indices.data()); - std::copy(problem_.get_constraint_matrix_values().data(), problem_.get_constraint_matrix_values().data() + problem_.get_constraint_matrix_values().size(), constraint_matrix_values.data()); - - if (problem_.get_constraint_lower_bounds().size() == 0 || problem_.get_constraint_upper_bounds().size() == 0) { + std::copy( + problem_.get_objective_coefficients().data(), + problem_.get_objective_coefficients().data() + problem_.get_objective_coefficients().size(), + objective_coefficients.data()); + std::copy(problem_.get_constraint_bounds().data(), + problem_.get_constraint_bounds().data() + problem_.get_constraint_bounds().size(), + constraint_bounds.data()); + std::copy( + problem_.get_variable_lower_bounds().data(), + problem_.get_variable_lower_bounds().data() + problem_.get_variable_lower_bounds().size(), + variable_lower_bounds.data()); + std::copy( + problem_.get_variable_upper_bounds().data(), + problem_.get_variable_upper_bounds().data() + problem_.get_variable_upper_bounds().size(), + variable_upper_bounds.data()); + std::copy(problem_.get_variable_types().data(), + problem_.get_variable_types().data() + problem_.get_variable_types().size(), + variable_types.data()); + std::copy(problem_.get_row_types().data(), + problem_.get_row_types().data() + problem_.get_row_types().size(), + row_types.data()); + std::copy(problem_.get_constraint_matrix_offsets().data(), + problem_.get_constraint_matrix_offsets().data() + + problem_.get_constraint_matrix_offsets().size(), + constraint_matrix_offsets.data()); + std::copy(problem_.get_constraint_matrix_indices().data(), + problem_.get_constraint_matrix_indices().data() + + problem_.get_constraint_matrix_indices().size(), + constraint_matrix_indices.data()); + std::copy( + problem_.get_constraint_matrix_values().data(), + problem_.get_constraint_matrix_values().data() + problem_.get_constraint_matrix_values().size(), + constraint_matrix_values.data()); + + if (problem_.get_constraint_lower_bounds().size() == 0 || + problem_.get_constraint_upper_bounds().size() == 0) { for (size_t i = 0; i < (size_t)n_constraints; i++) { constraint_lower_bounds[i] = constraint_bounds[i]; constraint_upper_bounds[i] = constraint_bounds[i]; if (row_types[i] == 'L') { constraint_lower_bounds[i] = -std::numeric_limits::infinity(); - } - else if (row_types[i] == 'G') { - constraint_upper_bounds[i] = std::numeric_limits::infinity(); + } else if (row_types[i] == 'G') { + constraint_upper_bounds[i] = std::numeric_limits::infinity(); } } - } - else { - std::copy(problem_.get_constraint_lower_bounds().data(), problem_.get_constraint_lower_bounds().data() + problem_.get_constraint_lower_bounds().size(), constraint_lower_bounds.data()); - std::copy(problem_.get_constraint_upper_bounds().data(), problem_.get_constraint_upper_bounds().data() + problem_.get_constraint_upper_bounds().size(), constraint_upper_bounds.data()); + } else { + std::copy( + problem_.get_constraint_lower_bounds().data(), + problem_.get_constraint_lower_bounds().data() + problem_.get_constraint_lower_bounds().size(), + constraint_lower_bounds.data()); + std::copy( + problem_.get_constraint_upper_bounds().data(), + problem_.get_constraint_upper_bounds().data() + problem_.get_constraint_upper_bounds().size(), + constraint_upper_bounds.data()); } // save coefficients with full precision diff --git a/cpp/libmps_parser/src/mps_writer.hpp b/cpp/libmps_parser/src/mps_writer.hpp deleted file mode 100644 index 2e0f19668..000000000 --- a/cpp/libmps_parser/src/mps_writer.hpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights - * reserved. SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -namespace cuopt::mps_parser { - -/** - * @brief Different possible types of 'ROWS' - */ -enum RowType { - Objective = 'N', - LesserThanOrEqual = 'L', - GreaterThanOrEqual = 'G', - Equality = 'E', -}; // enum RowType - -/** - * @brief Different possible types of 'BOUNDS' - */ -enum BoundType { - LowerBound, - UpperBound, - Fixed, - Free, - LowerBoundNegInf, - UpperBoundInf, - BinaryVariable, - LowerBoundIntegerVariable, - UpperBoundIntegerVariable, - SemiContiniousVariable, -}; // enum BoundType - -/** - * @brief Different possible types of 'OBJSENSE' - */ -enum ObjSenseType { - Minimize, - Maximize, -}; // enum ObjSenseType - -/** - * @brief Main writer class for MPS files - * - * @tparam f_t data type of the weights and variables - * @tparam i_t data type of the indices - */ -template -class mps_writer_t { - public: - /** - * @brief Ctor. Takes a data model view as input and writes it out as a MPS formatted file - * - * @param[in] problem Data model view to write - * @param[in] file Path to the MPS file to write - */ - mps_writer_t(const data_model_view_t& problem); - - /** - * @brief Writes the problem to an MPS formatted file - * - * @param[in] mps_file_path Path to the MPS file to write - */ - void write(const std::string& mps_file_path); - - private: - const data_model_view_t& problem_; -}; // class mps_writer_t - -} // namespace cuopt::mps_parser diff --git a/cpp/src/linear_programming/optimization_problem.cu b/cpp/src/linear_programming/optimization_problem.cu index 8e9a73e11..a8d1b5bd0 100644 --- a/cpp/src/linear_programming/optimization_problem.cu +++ b/cpp/src/linear_programming/optimization_problem.cu @@ -499,26 +499,37 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ // Set optimization sense data_model_view.set_maximize(get_sense()); + // Copy to host + auto constraint_matrix_values = cuopt::host_copy(get_constraint_matrix_values()); + auto constraint_matrix_indices = cuopt::host_copy(get_constraint_matrix_indices()); + auto constraint_matrix_offsets = cuopt::host_copy(get_constraint_matrix_offsets()); + auto constraint_bounds = cuopt::host_copy(get_constraint_bounds()); + auto objective_coefficients = cuopt::host_copy(get_objective_coefficients()); + auto variable_lower_bounds = cuopt::host_copy(get_variable_lower_bounds()); + auto variable_upper_bounds = cuopt::host_copy(get_variable_upper_bounds()); + auto constraint_lower_bounds = cuopt::host_copy(get_constraint_lower_bounds()); + auto constraint_upper_bounds = cuopt::host_copy(get_constraint_upper_bounds()); + auto row_types = cuopt::host_copy(get_row_types()); + // Set constraint matrix in CSR format if (get_nnz() != 0) { - data_model_view.set_csr_constraint_matrix(get_constraint_matrix_values().data(), - get_constraint_matrix_values().size(), - get_constraint_matrix_indices().data(), - get_constraint_matrix_indices().size(), - get_constraint_matrix_offsets().data(), - get_constraint_matrix_offsets().size()); + data_model_view.set_csr_constraint_matrix(constraint_matrix_values.data(), + constraint_matrix_values.size(), + constraint_matrix_indices.data(), + constraint_matrix_indices.size(), + constraint_matrix_offsets.data(), + constraint_matrix_offsets.size()); } // Set constraint bounds (RHS) if (get_n_constraints() != 0) { - data_model_view.set_constraint_bounds(get_constraint_bounds().data(), - get_constraint_bounds().size()); + data_model_view.set_constraint_bounds(constraint_bounds.data(), constraint_bounds.size()); } // Set objective coefficients if (get_n_variables() != 0) { - data_model_view.set_objective_coefficients(get_objective_coefficients().data(), - get_objective_coefficients().size()); + data_model_view.set_objective_coefficients(objective_coefficients.data(), + objective_coefficients.size()); } // Set objective scaling and offset @@ -527,23 +538,23 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ // Set variable bounds if (get_n_variables() != 0) { - data_model_view.set_variable_lower_bounds(get_variable_lower_bounds().data(), - get_variable_lower_bounds().size()); - data_model_view.set_variable_upper_bounds(get_variable_upper_bounds().data(), - get_variable_upper_bounds().size()); + data_model_view.set_variable_lower_bounds(variable_lower_bounds.data(), + variable_lower_bounds.size()); + data_model_view.set_variable_upper_bounds(variable_upper_bounds.data(), + variable_upper_bounds.size()); } // Set row types (constraint types) if (get_row_types().size() != 0) { - data_model_view.set_row_types(get_row_types().data(), get_row_types().size()); + data_model_view.set_row_types(row_types.data(), row_types.size()); } // Set constraint bounds (lower and upper) if (get_n_constraints() != 0) { - data_model_view.set_constraint_lower_bounds(get_constraint_lower_bounds().data(), - get_constraint_lower_bounds().size()); - data_model_view.set_constraint_upper_bounds(get_constraint_upper_bounds().data(), - get_constraint_upper_bounds().size()); + data_model_view.set_constraint_lower_bounds(constraint_lower_bounds.data(), + constraint_lower_bounds.size()); + data_model_view.set_constraint_upper_bounds(constraint_upper_bounds.data(), + constraint_upper_bounds.size()); } // Create a temporary vector to hold the converted variable types From 94ec78993a8e52c7645ca6a3da19bb5a46640022 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 5 Sep 2025 14:56:53 -0700 Subject: [PATCH 10/13] fix solver_settings write mps --- .../optimization_problem.cu | 2 +- .../cuopt_mps_parser/parser_wrapper.pyx | 1 + .../data_model/data_model.pxd | 7 +- .../data_model/data_model_wrapper.pyx | 16 ++- .../cuopt/cuopt/linear_programming/problem.py | 27 ++-- .../solver/solver_wrapper.pyx | 134 +----------------- .../linear_programming/test_python_API.py | 14 +- 7 files changed, 46 insertions(+), 155 deletions(-) diff --git a/cpp/src/linear_programming/optimization_problem.cu b/cpp/src/linear_programming/optimization_problem.cu index a8d1b5bd0..0757a882e 100644 --- a/cpp/src/linear_programming/optimization_problem.cu +++ b/cpp/src/linear_programming/optimization_problem.cu @@ -550,7 +550,7 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ } // Set constraint bounds (lower and upper) - if (get_n_constraints() != 0) { + if (get_constraint_lower_bounds().size() != 0 && get_constraint_upper_bounds().size() != 0 ) { data_model_view.set_constraint_lower_bounds(constraint_lower_bounds.data(), constraint_lower_bounds.size()); data_model_view.set_constraint_upper_bounds(constraint_upper_bounds.data(), diff --git a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx index 9a29cf8a8..819c12ceb 100644 --- a/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/cuopt_mps_parser/parser_wrapper.pyx @@ -41,6 +41,7 @@ def type_cast(np_obj, np_type, name): np_obj = np_obj.astype(np.dtype(np_type)) return np_obj + @catch_mps_parser_exception def ParseMps(mps_file_path, fixed_mps_formats): data_model = DataModel() diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index 7bdec0aee..247d1c53b 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -66,9 +66,6 @@ cdef extern from "mps_parser/data_model_view.hpp" namespace "cuopt::mps_parser" cdef extern from "mps_parser/writer.hpp" namespace "cuopt::mps_parser" nogil: # noqa - cdef void write_mps(const data_model_view_t[int, double] data_model, + cdef void write_mps( + const data_model_view_t[int, double] data_model, const string user_problem_file) except + - -#cdef extern from "cuopt/linear_programming/utilities/cython_solve.hpp" namespace "cuopt::cython": # noqa -# cdef void write_mps(data_model_view_t[int, double]* data_model, -# string user_problem_file) except + diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 637ea369b..50641d331 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -24,6 +24,7 @@ from .data_model cimport data_model_view_t, write_mps import warnings import numpy as np + import cudf from libc.stdint cimport uintptr_t @@ -320,10 +321,10 @@ cdef class DataModel: cdef vector[string] c_var_names for s in self.get_variable_names(): c_var_names.push_back(s.encode()) - + if len(self.get_variable_names()) != 0: c_data_model_view.set_variable_names( - c_var_names + c_var_names ) cdef vector[string] c_row_names @@ -336,10 +337,14 @@ cdef class DataModel: ) if self.get_problem_name(): - c_data_model_view.set_problem_name(self.get_problem_name().encode()) + c_data_model_view.set_problem_name( + self.get_problem_name().encode() + ) if self.get_objective_name(): - c_data_model_view.set_objective_name( self.get_objective_name().encode()) + c_data_model_view.set_objective_name( + self.get_objective_name().encode() + ) # Set initial solution on the C++ side if set on the Python side cdef uintptr_t c_initial_primal_solution = ( @@ -364,4 +369,5 @@ cdef class DataModel: self.variable_types, "S1", "variable_types" ) self.set_data_model_view() - write_mps(self.c_data_model_view.get()[0], user_problem_file.encode('utf-8')) + write_mps(self.c_data_model_view.get()[0], + user_problem_file.encode('utf-8')) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index c74f85efd..1f4f0df8c 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -13,15 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy from enum import Enum +import cuopt_mps_parser import numpy as np -import copy import cuopt.linear_programming.data_model as data_model import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings -import cuopt_mps_parser class VType(str, Enum): @@ -749,10 +749,10 @@ def _from_data_model(self, dm): indices = dm.get_constraint_matrix_indices() values = dm.get_constraint_matrix_values() - num_constrs = len(offsets)-1 + num_constrs = len(offsets) - 1 for i in range(num_constrs): start = offsets[i] - end = offsets[i+1] + end = offsets[i + 1] c_coeffs = values[start:end] c_indices = indices[start:end] c_vars = [vars[j] for j in c_indices] @@ -774,11 +774,21 @@ def _to_data_model(self): self.row_names = [] if self.constraint_csr_matrix is None: - csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} + csr_dict = { + "row_pointers": [0], + "column_indices": [], + "values": [], + } for constr in self.constrs: - csr_dict["column_indices"].extend(list(constr.vindex_coeff_dict.keys())) - csr_dict["values"].extend(list(constr.vindex_coeff_dict.values())) - csr_dict["row_pointers"].append(len(csr_dict["column_indices"])) + csr_dict["column_indices"].extend( + list(constr.vindex_coeff_dict.keys()) + ) + csr_dict["values"].extend( + list(constr.vindex_coeff_dict.values()) + ) + csr_dict["row_pointers"].append( + len(csr_dict["column_indices"]) + ) self.rhs.append(constr.RHS) self.row_sense.append(constr.Sense) self.row_names.append(constr.ConstraintName) @@ -1100,4 +1110,3 @@ def solve(self, settings=solver_settings.SolverSettings()): # Post Solve self.post_solve(solution) - diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 2f0f4fa29..a468d57ae 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -154,127 +154,6 @@ def type_cast(cudf_obj, np_type, name): return cudf_obj -cdef set_data_model_view(DataModel data_model_obj): - cdef data_model_view_t[int, double]* c_data_model_view = ( - data_model_obj.c_data_model_view.get() - ) - - # Set data_model_obj fields on the C++ side if set on the Python side - cdef uintptr_t c_A_values = ( - get_data_ptr(data_model_obj.get_constraint_matrix_values()) - ) - cdef uintptr_t c_A_indices = ( - get_data_ptr(data_model_obj.get_constraint_matrix_indices()) - ) - cdef uintptr_t c_A_offsets = ( - get_data_ptr(data_model_obj.get_constraint_matrix_offsets()) - ) - if data_model_obj.get_constraint_matrix_values().shape[0] != 0 and data_model_obj.get_constraint_matrix_indices().shape[0] != 0 and data_model_obj.get_constraint_matrix_offsets().shape[0] != 0: # noqa - c_data_model_view.set_csr_constraint_matrix( - c_A_values, - data_model_obj.get_constraint_matrix_values().shape[0], - c_A_indices, - data_model_obj.get_constraint_matrix_indices().shape[0], - c_A_offsets, - data_model_obj.get_constraint_matrix_offsets().shape[0] - ) - - cdef uintptr_t c_b = ( - get_data_ptr(data_model_obj.get_constraint_bounds()) - ) - if data_model_obj.get_constraint_bounds().shape[0] != 0: - c_data_model_view.set_constraint_bounds( - c_b, - data_model_obj.get_constraint_bounds().shape[0] - ) - - cdef uintptr_t c_c = ( - get_data_ptr(data_model_obj.get_objective_coefficients()) - ) - if data_model_obj.get_objective_coefficients().shape[0] != 0: - c_data_model_view.set_objective_coefficients( - c_c, - data_model_obj.get_objective_coefficients().shape[0] - ) - - c_data_model_view.set_objective_scaling_factor( - data_model_obj.get_objective_scaling_factor() - ) - c_data_model_view.set_objective_offset( - data_model_obj.get_objective_offset() - ) - c_data_model_view.set_maximize( data_model_obj.maximize) - - cdef uintptr_t c_variable_lower_bounds = ( - get_data_ptr(data_model_obj.get_variable_lower_bounds()) - ) - if data_model_obj.get_variable_lower_bounds().shape[0] != 0: - c_data_model_view.set_variable_lower_bounds( - c_variable_lower_bounds, - data_model_obj.get_variable_lower_bounds().shape[0] - ) - - cdef uintptr_t c_variable_upper_bounds = ( - get_data_ptr(data_model_obj.get_variable_upper_bounds()) - ) - if data_model_obj.get_variable_upper_bounds().shape[0] != 0: - c_data_model_view.set_variable_upper_bounds( - c_variable_upper_bounds, - data_model_obj.get_variable_upper_bounds().shape[0] - ) - cdef uintptr_t c_constraint_lower_bounds = ( - get_data_ptr(data_model_obj.get_constraint_lower_bounds()) - ) - if data_model_obj.get_constraint_lower_bounds().shape[0] != 0: - c_data_model_view.set_constraint_lower_bounds( - c_constraint_lower_bounds, - data_model_obj.get_constraint_lower_bounds().shape[0] - ) - cdef uintptr_t c_constraint_upper_bounds = ( - get_data_ptr(data_model_obj.get_constraint_upper_bounds()) - ) - if data_model_obj.get_constraint_upper_bounds().shape[0] != 0: - c_data_model_view.set_constraint_upper_bounds( - c_constraint_upper_bounds, - data_model_obj.get_constraint_upper_bounds().shape[0] - ) - cdef uintptr_t c_row_types = ( - get_data_ptr(data_model_obj.get_ascii_row_types()) - ) - if data_model_obj.get_ascii_row_types().shape[0] != 0: - c_data_model_view.set_row_types( - c_row_types, - data_model_obj.get_ascii_row_types().shape[0] - ) - - cdef uintptr_t c_var_types = ( - get_data_ptr(data_model_obj.get_variable_types()) - ) - if data_model_obj.get_variable_types().shape[0] != 0: - c_data_model_view.set_variable_types( - c_var_types, - data_model_obj.get_variable_types().shape[0] - ) - - # Set initial solution on the C++ side if set on the Python side - cdef uintptr_t c_initial_primal_solution = ( - get_data_ptr(data_model_obj.get_initial_primal_solution()) - ) - if data_model_obj.get_initial_primal_solution().shape[0] != 0: - c_data_model_view.set_initial_primal_solution( - c_initial_primal_solution, - data_model_obj.get_initial_primal_solution().shape[0] - ) - cdef uintptr_t c_initial_dual_solution = ( - get_data_ptr(data_model_obj.get_initial_dual_solution()) - ) - if data_model_obj.get_initial_dual_solution().shape[0] != 0: - c_data_model_view.set_initial_dual_solution( - c_initial_dual_solution, - data_model_obj.get_initial_dual_solution().shape[0] - ) - - cdef set_solver_setting( unique_ptr[solver_settings_t[int, double]]& unique_solver_settings, settings, @@ -660,7 +539,7 @@ cdef create_solution(unique_ptr[solver_ret_t] sol_ret_ptr, solved_by_pdlp=solved_by_pdlp, ) - + def Solve(py_data_model_obj, settings, mip=False): cdef DataModel data_model_obj = py_data_model_obj @@ -675,7 +554,7 @@ def Solve(py_data_model_obj, settings, mip=False): set_solver_setting( unique_solver_settings, settings, data_model_obj, mip ) - set_data_model_view(data_model_obj) + data_model_obj.set_data_model_view() return create_solution(move(call_solve( data_model_obj.c_data_model_view.get(), @@ -683,8 +562,10 @@ def Solve(py_data_model_obj, settings, mip=False): )), data_model_obj) -cdef insert_vector(DataModel data_model_obj, - vector[data_model_view_t[int, double] *]& data_model_views): +cdef set_and_insert_vector( + DataModel data_model_obj, + vector[data_model_view_t[int, double] *]& data_model_views): + data_model_obj.set_data_model_view() data_model_views.push_back(data_model_obj.c_data_model_view.get()) @@ -699,8 +580,7 @@ def BatchSolve(py_data_model_list, settings): cdef vector[data_model_view_t[int, double] *] data_model_views for data_model_obj in py_data_model_list: - set_data_model_view(data_model_obj) - insert_vector(data_model_obj, data_model_views) + set_and_insert_vector(data_model_obj, data_model_views) cdef pair[ vector[unique_ptr[solver_ret_t]], diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 04e343527..62f1eeb18 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -33,8 +33,6 @@ sense, ) -from cuopt.linear_programming.solver.solver_parameters import CUOPT_USER_PROBLEM_FILE - def test_model(): @@ -281,13 +279,13 @@ def test_read_write_mps_and_relaxation(): x5 = m.addVariable(name="x5", lb=0.0, vtype=INTEGER) # Objective (minimize) - m.setObjective(2*x1 + 3*x2 + x3 + 1*x4 + 4*x5, MINIMIZE) + m.setObjective(2 * x1 + 3 * x2 + x3 + 1 * x4 + 4 * x5, MINIMIZE) # Constraints (5 total) m.addConstraint(x1 + x2 + x3 <= 10, name="c1") - m.addConstraint(2*x1 + x3 - x4 >= 3, name="c2") - m.addConstraint(x2 + 3*x5 == 7, name="c3") - m.addConstraint(x4 + x5 <= 8, name="c4") + m.addConstraint(2 * x1 + x3 - x4 >= 3, name="c2") + m.addConstraint(x2 + 3 * x5 == 7, name="c3") + m.addConstraint(x4 + x5 <= 8, name="c4") m.addConstraint(x1 + x2 + x3 + x4 + x5 >= 5, name="c5") # Write MPS @@ -298,7 +296,7 @@ def test_read_write_mps_and_relaxation(): assert prob.IsMIP prob.solve() - expected_values_mip = [0.0, 1.0, 3.0, 0.0, 2.0] + expected_values_mip = [1.0, 1.0, 1.0, 0.0, 2.0] assert prob.Status.name == "Optimal" for i, v in enumerate(prob.getVariables()): assert v.getValue() == pytest.approx(expected_values_mip[i]) @@ -308,7 +306,7 @@ def test_read_write_mps_and_relaxation(): assert not lp_prob.IsMIP lp_prob.solve() - expected_values_lp = [0.0, 0.0, 3.0, 0.0, 2.333333] + expected_values_lp = [0.33333333, 0.0, 2.33333333, 0.0, 2.33333333] assert lp_prob.Status.name == "Optimal" for i, v in enumerate(lp_prob.getVariables()): assert v.getValue() == pytest.approx(expected_values_lp[i]) From 4729c167bca68f7b8e07d85020f9c5f33e8980a6 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 9 Sep 2025 15:56:32 -0700 Subject: [PATCH 11/13] update test --- cpp/include/cuopt/linear_programming/solve.hpp | 2 +- .../cuopt/linear_programming/utilities/cython_solve.hpp | 2 +- cpp/src/linear_programming/optimization_problem.cu | 2 +- python/cuopt/cuopt/linear_programming/problem.py | 4 ++-- .../cuopt/cuopt/tests/linear_programming/test_python_API.py | 1 + 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/solve.hpp b/cpp/include/cuopt/linear_programming/solve.hpp index a01126a7e..11e8f9dcf 100644 --- a/cpp/include/cuopt/linear_programming/solve.hpp +++ b/cpp/include/cuopt/linear_programming/solve.hpp @@ -25,8 +25,8 @@ #include #include #include -#include #include +#include namespace cuopt::linear_programming { diff --git a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp index 15c70278c..46d672cb1 100644 --- a/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp @@ -22,10 +22,10 @@ #include #include #include +#include #include #include #include -#include #include #include diff --git a/cpp/src/linear_programming/optimization_problem.cu b/cpp/src/linear_programming/optimization_problem.cu index 0757a882e..5847c503b 100644 --- a/cpp/src/linear_programming/optimization_problem.cu +++ b/cpp/src/linear_programming/optimization_problem.cu @@ -550,7 +550,7 @@ void optimization_problem_t::write_to_mps(const std::string& mps_file_ } // Set constraint bounds (lower and upper) - if (get_constraint_lower_bounds().size() != 0 && get_constraint_upper_bounds().size() != 0 ) { + if (get_constraint_lower_bounds().size() != 0 && get_constraint_upper_bounds().size() != 0) { data_model_view.set_constraint_lower_bounds(constraint_lower_bounds.data(), constraint_lower_bounds.size()); data_model_view.set_constraint_upper_bounds(constraint_upper_bounds.data(), diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 1f4f0df8c..9ad77ca58 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1057,7 +1057,7 @@ def relax(self): v.VariableType = CONTINUOUS return relaxed_problem - def post_solve(self, solution): + def populate_solution(self, solution): self.Status = solution.get_termination_status() self.SolveTime = solution.get_solve_time() @@ -1109,4 +1109,4 @@ def solve(self, settings=solver_settings.SolverSettings()): solution = solver.Solve(self.model, settings) # Post Solve - self.post_solve(solution) + self.populate_solution(solution) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 62f1eeb18..1cc4993d2 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -293,6 +293,7 @@ def test_read_write_mps_and_relaxation(): # Read MPS and solve prob = Problem.readMPS("small_mip.mps") + assert prob.Name == "SMALLMIP" assert prob.IsMIP prob.solve() From 0189f3e0a133df1dceffd407933a028cb2a812f9 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 16 Sep 2025 08:39:37 -0700 Subject: [PATCH 12/13] fix cpp tests and docs --- cpp/libmps_parser/src/mps_writer.cpp | 3 +-- .../utilities/cython_solve.cu | 8 ++++++++ cpp/tests/mip/doc_example_test.cu | 17 +++++++++++------ docs/cuopt/source/conf.py | 1 + 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index adf563fea..b74aacd10 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -181,8 +181,7 @@ void mps_writer_t::write(const std::string& mps_file_path) mps_file << " " << col_name << " " << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) << " " - << (problem_.get_sense() ? -objective_coefficients[var_id] - : objective_coefficients[var_id]) + << objective_coefficients[var_id] << "\n"; } } diff --git a/cpp/src/linear_programming/utilities/cython_solve.cu b/cpp/src/linear_programming/utilities/cython_solve.cu index 2913501ac..2753e824a 100644 --- a/cpp/src/linear_programming/utilities/cython_solve.cu +++ b/cpp/src/linear_programming/utilities/cython_solve.cu @@ -109,6 +109,14 @@ data_model_to_optimization_problem( op_problem.set_variable_types(enum_variable_types.data(), enum_variable_types.size()); } + if (data_model->get_variable_names().size() != 0) { + op_problem.set_variable_names(data_model->get_variable_names()); + } + + if (data_model->get_row_names().size() != 0) { + op_problem.set_row_names(data_model->get_row_names()); + } + return op_problem; } diff --git a/cpp/tests/mip/doc_example_test.cu b/cpp/tests/mip/doc_example_test.cu index 3506eb00d..6b46e87e6 100644 --- a/cpp/tests/mip/doc_example_test.cu +++ b/cpp/tests/mip/doc_example_test.cu @@ -160,12 +160,17 @@ TEST(docs, user_problem_file) // Get solution values const auto& sol_values = solution.get_solution(); // x should be approximately 37 and integer - EXPECT_NEAR(37.0, sol_values.element(0, handle_.get_stream()), 0.1); - EXPECT_NEAR(std::round(sol_values.element(0, handle_.get_stream())), - sol_values.element(0, handle_.get_stream()), - settings.tolerances.integrality_tolerance); // Check x is integer - // y should be approximately 39.5 - EXPECT_NEAR(39.5, sol_values.element(1, handle_.get_stream()), 0.1); + for(int i=0; i Date: Tue, 16 Sep 2025 09:10:52 -0700 Subject: [PATCH 13/13] fix style --- cpp/libmps_parser/src/mps_writer.cpp | 4 +--- cpp/tests/mip/doc_example_test.cu | 7 +++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cpp/libmps_parser/src/mps_writer.cpp b/cpp/libmps_parser/src/mps_writer.cpp index b74aacd10..7eb1a4f42 100644 --- a/cpp/libmps_parser/src/mps_writer.cpp +++ b/cpp/libmps_parser/src/mps_writer.cpp @@ -180,9 +180,7 @@ void mps_writer_t::write(const std::string& mps_file_path) if (objective_coefficients[var_id] != 0.0) { mps_file << " " << col_name << " " << (problem_.get_objective_name().empty() ? "OBJ" : problem_.get_objective_name()) - << " " - << objective_coefficients[var_id] - << "\n"; + << " " << objective_coefficients[var_id] << "\n"; } } if (is_integral) mps_file << " MARK0001 'MARKER' 'INTEND'\n"; diff --git a/cpp/tests/mip/doc_example_test.cu b/cpp/tests/mip/doc_example_test.cu index 6b46e87e6..e154e8c69 100644 --- a/cpp/tests/mip/doc_example_test.cu +++ b/cpp/tests/mip/doc_example_test.cu @@ -160,14 +160,13 @@ TEST(docs, user_problem_file) // Get solution values const auto& sol_values = solution.get_solution(); // x should be approximately 37 and integer - for(int i=0; i