diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 37977ea28b..e35b3ac10a 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -61,6 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 0eaa7000ad..12951b7662 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -61,6 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index fb23f887a7..73c1c9d91c 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -61,6 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index 501729acd9..e82c318b27 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -61,6 +61,7 @@ dependencies: - requests - rmm==26.2.*,>=0.0.0a0 - scikit-build-core>=0.11.0 +- scipy>=1.14.1 - sphinx - sphinx-copybutton - sphinx-design diff --git a/conda/recipes/cuopt/recipe.yaml b/conda/recipes/cuopt/recipe.yaml index 3408c67158..51da0e6f2d 100644 --- a/conda/recipes/cuopt/recipe.yaml +++ b/conda/recipes/cuopt/recipe.yaml @@ -82,6 +82,7 @@ requirements: - pylibraft =${{ minor_version }} - python - rmm =${{ minor_version }} + - scipy >=1.14.1 # Needed by Numba for CUDA support - cuda-nvcc-impl # TODO: Add nvjitlink here diff --git a/dependencies.yaml b/dependencies.yaml index 7dc6b94908..bb8a909cdb 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -350,6 +350,7 @@ dependencies: - numba>=0.60.0 - &pandas pandas>=2.0 - &pyyaml pyyaml>=6.0.0 + - scipy>=1.14.1 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py index b08e96f3be..b1286d461e 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/expressions_constraints_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# 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. + """ Working with Expressions and Constraints Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py index 39225b4158..87faddc922 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/incumbent_solutions_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# 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. + """ Working with Incumbent Solutions Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py index 4684f336f6..dfae94f073 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/pdlp_warmstart_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# 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. + """ Working with PDLP Warmstart Data Example @@ -90,7 +77,7 @@ def main(): print(f"Objective value = {problem.ObjValue}") # Get the warmstart data - warmstart_data = problem.get_pdlp_warm_start_data() + warmstart_data = problem.getWarmstartData() print( f"\nWarmstart data extracted (primal solution size: " f"{len(warmstart_data.current_primal_solution)})" diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py index 8a0422d44e..3febce3d1d 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/production_planning_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# 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. + """ Production Planning Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py new file mode 100644 index 0000000000..f822ead9fb --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/qp_matrix_example.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +""" +Quadratic Programming Matrix Example +==================================== + +.. note:: + The QP solver is currently in beta. + +This example demonstrates how to formulate and solve a +Quadratic Programming (QP) problem represented in a matrix format +using the cuOpt Python API. + +Problem: + minimize 0.01 * p1^2 + 0.02 * p2^2 + 0.015 * p3^2 + 8 * p1 + 6 * p2 + 7 * p3 + subject to p1 + p2 + p3 = 150 + 10 <= p1 <= 100 + 10 <= p2 <= 80 + 10 <= p3 <= 90 + +This is a convex QP that minimizes the cost of power generation and dispatch +while satisfying capacity and demand. +""" + +from cuopt.linear_programming.problem import ( + MINIMIZE, + Problem, + QuadraticExpression, +) + + +def main(): + # Create a new optimization problem + prob = Problem("QP Power Dispatch") + + # Add variables with lower and upper bounds + p1 = prob.addVariable(lb=10, ub=100) + p2 = prob.addVariable(lb=10, ub=80) + p3 = prob.addVariable(lb=10, ub=90) + + # Add demand constraint: p1 + p2 + p3 = 150 + prob.addConstraint(p1 + p2 + p3 == 150, name="demand") + + # Create matrix for quadratic terms: 0.01 p1^2 + 0.02 p2^2 + 0.015 p3^2 + matrix = [[0.01, 0.0, 0.0], [0.0, 0.02, 0.0], [0.0, 0.0, 0.015]] + quad_matrix = QuadraticExpression(matrix, prob.getVariables()) + + # Set objective using matrix representation + quad_obj = quad_matrix + 8 * p1 + 6 * p2 + 7 * p3 + prob.setObjective(quad_obj, sense=MINIMIZE) + + # Solve the problem + prob.solve() + + # Print results + print(f"Optimal solution found in {prob.SolveTime:.2f} seconds") + print(f"p1 = {p1.Value}") + print(f"p2 = {p2.Value}") + print(f"p3 = {p3.Value}") + print(f"Minimized cost = {prob.ObjValue}") + + +if __name__ == "__main__": + main() diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py index 646681967f..b85e4d89d7 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_lp_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# 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. + """ Simple Linear Programming Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py index 52df65904c..d951a8abcd 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_milp_example.py @@ -1,19 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# 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. + """ Mixed Integer Linear Programming Example diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py index ec70b0bdcb..d9ff455e83 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/simple_qp_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py new file mode 100644 index 0000000000..286626ed33 --- /dev/null +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/examples/solution_example.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Linear Programming Solution Example + +This example demonstrates how to: +- Create a linear programming problem +- Solve the problem and retrieve result details + +Problem: + Minimize: 3x + 2y + 5z + Subject to: + x + y + z = 4 + 2x + y + z = 5 + x, y, z >= 0 + +Expected Output: + Optimal solution found in 0.02 seconds + Objective: 9.0 + x = 1.0, ReducedCost = 0.0 + y = 3.0, ReducedCost = 0.0 + z = 0.0, ReducedCost = 2.999999858578644 + c1 DualValue = 1.0000000592359144 + c2 DualValue = 1.0000000821854418 +""" + +from cuopt.linear_programming.problem import Problem, MINIMIZE + + +def main(): + """Run the simple LP example.""" + problem = Problem("min_dual_rc") + + # Add Variables + x = problem.addVariable(lb=0.0, name="x") + y = problem.addVariable(lb=0.0, name="y") + z = problem.addVariable(lb=0.0, name="z") + + # Add Constraints (equalities) + problem.addConstraint(x + y + z == 4.0, name="c1") + problem.addConstraint(2.0 * x + y + z == 5.0, name="c2") + + # Set Objective (minimize) + problem.setObjective(3.0 * x + 2.0 * y + 5.0 * z, sense=MINIMIZE) + + # Solve + problem.solve() + + # Check solution status + if problem.Status.name == "Optimal": + print(f"Optimal solution found in {problem.SolveTime:.2f} seconds") + # Get Primal + print("Objective:", problem.ObjValue) + for v in problem.getVariables(): + print( + f"{v.VariableName} = {v.Value}, ReducedCost = {v.ReducedCost}" + ) + # Get Duals + for c in problem.getConstraints(): + print(f"{c.ConstraintName} DualValue = {c.DualValue}") + else: + print(f"Problem status: {problem.Status.name}") + + +if __name__ == "__main__": + main() diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst index cf87fa3814..3a4f603d46 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst @@ -7,7 +7,7 @@ LP, QP and MILP API Reference .. autoclass:: cuopt.linear_programming.problem.Problem :members: :undoc-members: - :exclude-members: reset_solved_values, populate_solution, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP + :exclude-members: reset_solved_values, populate_solution, dict_to_object, NumNZs, NumVariables, NumConstraints, IsMIP, get_incumbent_values, get_pdlp_warm_start_data, getQcsr .. autoclass:: cuopt.linear_programming.problem.Variable :members: diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst index bb6f428acd..76dfb7d74d 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-examples.rst @@ -111,6 +111,46 @@ The response is as follows: z = 99.99999999999999 Objective value = 399.99999999999994 +Working with Quadratic objective matrix +--------------------------------------- + +:download:`qp_matrix_example.py ` + +.. literalinclude:: examples/qp_matrix_example.py + :language: python + :linenos: + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.16 seconds + p1 = 30.770728122083014 + p2 = 65.38350784293876 + p3 = 53.84576403497824 + Minimized cost = 1153.8461538953868 + +Inspecting the Problem Solution +------------------------------- + +:download:`solution_example.py ` + +.. literalinclude:: examples/solution_example.py + :language: python + :linenos: + +The response is as follows: + +.. code-block:: text + + Optimal solution found in 0.02 seconds + Objective: 9.0 + x = 1.0, ReducedCost = 0.0 + y = 3.0, ReducedCost = 0.0 + z = 0.0, ReducedCost = 2.999999858578644 + c1 DualValue = 1.0000000592359144 + c2 DualValue = 1.0000000821854418 + Working with Incumbent Solutions -------------------------------- diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 4259753dcb..baf9716191 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 @@ -7,10 +7,12 @@ import cuopt_mps_parser import numpy as np +from scipy.sparse import coo_matrix 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 warnings class VType(str, Enum): @@ -234,7 +236,9 @@ def __mul__(self, other): case int() | float(): return LinearExpression([self], [float(other)], 0.0) case Variable(): - return QuadraticExpression([self], [other], [1.0], [], [], 0.0) + return QuadraticExpression( + qvars1=[self], qvars2=[other], qcoefficients=[1.0] + ) case LinearExpression(): qvars1 = [self] * len(other.vars) qvars2 = other.vars @@ -242,7 +246,11 @@ def __mul__(self, other): vars = [self] coeffs = [other.constant] return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, 0.0 + qvars1=qvars1, + qvars2=qvars2, + qcoefficients=qcoeffs, + vars=vars, + coefficients=coeffs, ) case _: raise ValueError( @@ -301,12 +309,33 @@ class QuadraticExpression: Parameters ---------- + qmatrix : List[List[float]] or 2D numpy array. + Matrix containing quadratic coefficient matrix terms. + Should be a square matrix with shape as (num_vars, num_vars). + qvars : List[Variable] + List of variables denoting the rows and cols in qmatrix. It is + a mandatory field when providing qmatrix. + qvars should be in the order of variables added to the problem + and can be obtained using problem.getVariables(). The length + of qvars should be equal to length of row/col in qmatrix. qvars1 : List[Variable] List of first variables for quadratic terms. + This should be used if adding quadratic terms in triplet (i, j, x) + format where i is the row variable, j is the column variable and + x is the corresponding coefficient. qvars1 contains all i variables + representing the row. qvars2 : List[Variable] List of second variables for quadratic terms. + This should be used if adding quadratic terms in triplet (i, j, x) + format where i is the row variable, j is the column variable and + x is the corresponding coefficient. qvars2 contains all j variables + representing the column. qcoefficients : List[float] List of coefficients for the quadratic terms. + This should be used if adding quadratic terms in triplet (i, j, x) + format where i is the row variable, j is the column variable and + x is the corresponding coefficient. qcoefficients contains all x + values representing coefficients for (i,j) vars : List[Variable] List of Variables for linear terms. coefficients : List[float] @@ -318,16 +347,38 @@ class QuadraticExpression: -------- >>> x = problem.addVariable() >>> y = problem.addVariable() - >>> # Create x^2 + 2*x*y + 3*x + 4 - >>> quad_expr = QuadraticExpression( - ... [x, x], [x, y], [1.0, 2.0], - ... [x], [3.0], 4.0 + >>> # Create objective x^2 + 2*x*y + 3*x + 4 using matrix + >>> quad_matrix = QuadraticExpression( + ... qmatrix=[[1.0, 2.0], [0.0, 0.0]], + ... qvars=[x, y] ... ) + >>> quad_obj_using_matrix = quad_matrix + 3*x + 4 + >>> # Create objective x^2 + 2*x*y + 3*x + 4 using expression + >>> quad_obj_using_expr = x*x + 2*x*y + 3*x + 4 """ def __init__( - self, qvars1, qvars2, qcoefficients, vars, coefficients, constant + self, + qmatrix=None, + qvars=[], + qvars1=[], + qvars2=[], + qcoefficients=[], + vars=[], + coefficients=[], + constant=0.0, ): + self.qmatrix = None + self.qvars = qvars + if qmatrix is not None: + self.qmatrix = coo_matrix(qmatrix) + mshape = self.qmatrix.shape + if mshape[0] != mshape[1]: + raise ValueError("qmatrix should be a square matrix") + if len(qvars) != mshape[0]: + raise ValueError( + "qvars length mismatch. Should match with qmatrix length. Please check docs for more details." + ) self.qvars1 = qvars1 self.qvars2 = qvars2 self.qcoefficients = qcoefficients @@ -340,31 +391,48 @@ def getVariables(self): Returns all the quadractic variables in the expression as list of tuples containing Variable 1 and Variable 2 for each term. """ - return list(zip(self.qvars1, self.qvars2)) + qvars1 = self.qvars1 + qvars2 = self.qvars2 + if self.qmatrix is not None: + qvars1 = self.qvars1 + [self.qvars[i] for i in self.qmatrix.row] + qvars2 = self.qvars2 + [self.qvars[i] for i in self.qmatrix.col] + return list(zip(qvars1, qvars2)) def getVariable1(self, i): """ Gets first Variable at ith index in the quadratic term. """ - return self.qvars1[i] + qvars1 = self.qvars1 + if self.qmatrix is not None: + qvars1 = self.qvars1 + [self.qvars[r] for r in self.qmatrix.row] + return qvars1[i] def getVariable2(self, i): """ Gets second Variable at ith index in the quadratic term. """ - return self.qvars2[i] + qvars2 = self.qvars2 + if self.qmatrix is not None: + qvars2 = self.qvars2 + [self.qvars[c] for c in self.qmatrix.col] + return qvars2[i] def getCoefficients(self): """ Returns all the coefficients of the quadratic term. """ - return self.qcoefficients + qcoefficients = self.qcoefficients + if self.qmatrix is not None: + qcoefficients = self.qcoefficients + self.qmatrix.data.tolist() + return qcoefficients def getCoefficient(self, i): """ Gets the coefficient of the quadratic term at ith index. """ - return self.qcoefficients[i] + qcoefficients = self.qcoefficients + if self.qmatrix is not None: + qcoefficients = self.qcoefficients + self.qmatrix.data.tolist() + return qcoefficients[i] def getLinearExpression(self): """ @@ -381,12 +449,21 @@ def getValue(self): value = 0.0 for i, var in enumerate(self.vars): value += var.Value * self.coefficients[i] - for i, (var1, var2) in enumerate(self.getVariables()): + for i, var1 in enumerate(self.qvars1): + var2 = self.qvars2[i] value += var1.Value * var2.Value * self.qcoefficients[i] + if self.qmatrix is not None: + rows_idx = self.qmatrix.row + cols_idx = self.qmatrix.col + data = self.qmatrix.data + for i, row_idx in enumerate(rows_idx): + var1 = self.qvars[row_idx] + var2 = self.qvars[cols_idx[i]] + value += var1.Value * var2.Value * data[i] return value + self.constant def __len__(self): - return len(self.vars) + len(self.qvars1) + return len(self.vars) + len(self.qvars1) + len(self.qvars) def __iadd__(self, other): # Compute expr1 += expr2 @@ -407,13 +484,20 @@ def __iadd__(self, other): self.constant += other.constant return self case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants + # Linear expression terms self.vars.extend(other.vars) self.coefficients.extend(other.coefficients) self.constant += other.constant + # Quadratic expression terms self.qvars2.extend(other.qvars2) self.qvars1.extend(other.qvars1) self.qcoefficients.extend(other.qcoefficients) + # Quadratic matrix terms + if self.qmatrix is not None and other.qmatrix is not None: + self.qmatrix += other.qmatrix + elif other.qmatrix is not None: + self.qmatrix = other.qmatrix + self.qvars = other.qvars return self case _: raise ValueError( @@ -427,6 +511,8 @@ def __add__(self, other): case int() | float(): # Update just the constant value return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -439,6 +525,8 @@ def __add__(self, other): vars = self.vars + [other] coeffs = self.coefficients + [1.0] return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -452,6 +540,8 @@ def __add__(self, other): coeffs = self.coefficients + other.coefficients constant = self.constant + other.constant return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -460,15 +550,31 @@ def __add__(self, other): constant, ) case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants - qvars1 = self.qvars1 + other.qvars1 - qvars2 = self.qvars2 + other.qvars2 - qcoeffs = self.qcoefficients + other.qcoefficients + # Linear expression terms vars = self.vars + other.vars coeffs = self.coefficients + other.coefficients constant = self.constant + other.constant + # Quadratic expression terms + qvars1 = self.qvars1 + other.qvars1 + qvars2 = self.qvars2 + other.qvars2 + qcoeffs = self.qcoefficients + other.qcoefficients + # Quadratic matrix terms + qmatrix = self.qmatrix + qvars = self.qvars + if self.qmatrix is not None and other.qmatrix is not None: + qmatrix = self.qmatrix + other.qmatrix + elif other.qmatrix is not None: + qmatrix = other.qmatrix + qvars = other.qvars return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qmatrix, + qvars, + qvars1, + qvars2, + qcoeffs, + vars, + coeffs, + constant, ) case _: raise ValueError( @@ -499,15 +605,22 @@ def __isub__(self, other): self.constant -= other.constant return self case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants + # Linear expression terms self.vars.extend(other.vars) for coeff in other.coefficients: self.coefficients.append(-coeff) self.constant -= other.constant + # Quadratic expression terms self.qvars2.extend(other.qvars2) self.qvars1.extend(other.qvars1) for qcoeff in other.qcoefficients: self.qcoefficients.append(-qcoeff) + # Quadratic matrix terms + if self.qmatrix is not None and other.qmatrix is not None: + self.qmatrix -= other.qmatrix + elif other.qmatrix is not None: + self.qmatrix = -other.qmatrix + self.qvars = other.qvars return self case _: raise ValueError( @@ -521,6 +634,8 @@ def __sub__(self, other): case int() | float(): # Update just the constant value return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -533,6 +648,8 @@ def __sub__(self, other): vars = self.vars + [other] coeffs = self.coefficients + [-1.0] return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -550,6 +667,8 @@ def __sub__(self, other): coeffs.append(-1.0 * i) constant = self.constant - other.constant return QuadraticExpression( + self.qmatrix, + self.qvars, self.qvars1, self.qvars2, self.qcoefficients, @@ -558,7 +677,7 @@ def __sub__(self, other): constant, ) case QuadraticExpression(): - # Append all quadratic variables, coefficients and constants + # Linear expression terms vars = self.vars + other.vars coeffs = [] for i in self.coefficients: @@ -566,6 +685,7 @@ def __sub__(self, other): for i in other.coefficients: coeffs.append(-1.0 * i) constant = self.constant - other.constant + # Quadratic expression terms qvars1 = self.qvars1 + other.qvars1 qvars2 = self.qvars2 + other.qvars2 qcoeffs = [] @@ -573,8 +693,23 @@ def __sub__(self, other): qcoeffs.append(i) for i in other.qcoefficients: qcoeffs.append(-1.0 * i) + # Quadratic matrix terms + qmatrix = self.qmatrix + qvars = self.qvars + if self.qmatrix is not None and other.qmatrix is not None: + qmatrix = self.qmatrix - other.qmatrix + elif other.qmatrix is not None: + qmatrix = -other.qmatrix + qvars = other.qvars return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qmatrix, + qvars, + qvars1, + qvars2, + qcoeffs, + vars, + coeffs, + constant, ) case _: raise ValueError( @@ -597,6 +732,8 @@ def __imul__(self, other): self.qcoefficients = [ qcoeff * float(other) for qcoeff in self.qcoefficients ] + if self.qmatrix is not None: + self.qmatrix *= float(other) return self case _: raise ValueError( @@ -613,7 +750,12 @@ def __mul__(self, other): qcoeff * float(other) for qcoeff in self.qcoefficients ] constant = self.constant * float(other) + qmatrix = None + if self.qmatrix is not None: + qmatrix = self.qmatrix * float(other) return QuadraticExpression( + qmatrix, + self.qvars, self.qvars1, self.qvars2, qcoeffs, @@ -641,6 +783,8 @@ def __itruediv__(self, other): coeff / float(other) for coeff in self.qcoefficients ] self.constant = self.constant / float(other) + if self.qmatrix is not None: + self.qmatrix = self.qmatrix / float(other) return self case _: raise ValueError( @@ -657,7 +801,12 @@ def __truediv__(self, other): qcoeff / float(other) for qcoeff in self.qcoefficients ] constant = self.constant / float(other) + qmatrix = None + if self.qmatrix is not None: + qmatrix = self.qmatrix / float(other) return QuadraticExpression( + qmatrix, + self.qvars, self.qvars1, self.qvars2, qcoeffs, @@ -899,7 +1048,12 @@ def __imul__(self, other): ] constant = self.constant * other.constant return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qvars1=qvars1, + qvars2=qvars2, + qcoefficients=qcoeffs, + vars=vars, + coefficients=coeffs, + constant=constant, ) case _: raise ValueError( @@ -914,6 +1068,8 @@ def __mul__(self, other): coeffs = [coeff * float(other) for coeff in self.coefficients] constant = self.constant * float(other) return LinearExpression(self.vars, coeffs, constant) + case Variable(): + return other * self case LinearExpression(): qvars1, qvars2, qcoeffs = [], [], [] for i in range(len(self.vars)): @@ -929,10 +1085,18 @@ def __mul__(self, other): ] constant = self.constant * other.constant return QuadraticExpression( - qvars1, qvars2, qcoeffs, vars, coeffs, constant + qvars1=qvars1, + qvars2=qvars2, + qcoefficients=qcoeffs, + vars=vars, + coefficients=coeffs, + constant=constant, + ) + case _: + raise ValueError( + "Can't multiply type %s by LinearExpresson" + % type(other).__name__ ) - case Variable(): - return other * self def __rmul__(self, other): return self * other @@ -1140,7 +1304,6 @@ def __init__(self, model_name=""): self.ObjSense = MINIMIZE self.ObjConstant = 0.0 self.Status = -1 - self.ObjValue = float("nan") self.warmstart_data = None self.model = None @@ -1148,8 +1311,7 @@ def __init__(self, model_name=""): self.rhs = None self.row_sense = None self.constraint_csr_matrix = None - self.objective_qcsr_matrix = None - self.objective_qcoo_matrix = [], [], [] + self.objective_qmatrix = None self.lower_bound = None self.upper_bound = None self.var_type = None @@ -1275,11 +1437,11 @@ def _to_data_model(self): dm.set_row_types(np.array(self.row_sense, dtype="S1")) dm.set_objective_coefficients(self.objective) dm.set_objective_offset(self.ObjConstant) - if self.getQcsr(): + if self.objective_qmatrix is not None: dm.set_quadratic_objective_matrix( - np.array(self.objective_qcsr_matrix["values"]), - np.array(self.objective_qcsr_matrix["column_indices"]), - np.array(self.objective_qcsr_matrix["row_pointers"]), + self.objective_qmatrix.data, + self.objective_qmatrix.indices, + self.objective_qmatrix.indptr, ) dm.set_variable_lower_bounds(self.lower_bound) dm.set_variable_upper_bounds(self.upper_bound) @@ -1310,9 +1472,7 @@ def reset_solved_values(self): self.model = None self.constraint_csr_matrix = None - self.objective_qcoo_matrix = [], [], [] - self.objective_qcsr_matrix = None - self.ObjValue = float("nan") + self.objective_qmatrix = None self.warmstart_data = None self.solved = False @@ -1480,14 +1640,21 @@ def setObjective(self, expr, sense=MINIMIZE): sum_coeff ) self.ObjConstant = expr.constant - self.objective_qcoo_matrix = ( - expr.qvars1, - expr.qvars2, - expr.qcoefficients, + qrows = [var.getIndex() for var in expr.qvars1] + qcols = [var.getIndex() for var in expr.qvars2] + self.objective_qmatrix = coo_matrix( + ( + np.array(expr.qcoefficients), + (np.array(qrows), np.array(qcols)), + ), + shape=(self.NumVariables, self.NumVariables), ) + if expr.qmatrix is not None: + self.objective_qmatrix += expr.qmatrix + self.objective_qmatrix = self.objective_qmatrix.tocsr() case _: raise ValueError( - "Objective must be a LinearExpression or a constant" + "Objective must be a Variable, Expression or a constant" ) def updateObjective(self, coeffs=[], constant=None, sense=None): @@ -1524,7 +1691,7 @@ def updateObjective(self, coeffs=[], constant=None, sense=None): if sense: self.ObjSense = sense - def get_incumbent_values(self, solution, vars): + def getIncumbentValues(self, solution, vars): """ Extract incumbent values of the vars from a problem solution. """ @@ -1533,7 +1700,15 @@ def get_incumbent_values(self, solution, vars): values.append(solution[var.index]) return values - def get_pdlp_warm_start_data(self): + def get_incumbent_values(self, solution, vars): + warnings.warn( + "This function is deprecated and will be removed." + "Please use getIncumbentValues instead.", + DeprecationWarning, + ) + return self.getIncumbentValues(solution, vars) + + def getWarmstartData(self): """ Note: Applicable to only LP. Allows to retrieve the warm start data from the PDLP solver @@ -1545,13 +1720,21 @@ def get_pdlp_warm_start_data(self): -------- >>> problem = problem.Problem.readMPS("LP.mps") >>> problem.solve() - >>> warmstart_data = problem.get_pdlp_warm_start_data() + >>> warmstart_data = problem.getWarmstartData() >>> settings.set_pdlp_warm_start_data(warmstart_data) >>> updated_problem = problem.Problem.readMPS("updated_LP.mps") >>> updated_problem.solve(settings) """ return self.warmstart_data + def get_pdlp_warm_start_data(self): + warnings.warn( + "This function is deprecated and will be removed." + "Please use getWarmstartData instead.", + DeprecationWarning, + ) + return self.getWarmstartData() + def getObjective(self): """ Get the Objective expression of the problem. @@ -1643,16 +1826,21 @@ def Obj(self): coeffs = [] for var in self.vars: coeffs.append(var.getObjectiveCoefficient()) - if not self.objective_qcoo_matrix: + if self.objective_qmatrix is None: return LinearExpression(self.vars, coeffs, self.ObjConstant) else: return QuadraticExpression( - *self.objective_qcoo_matrix, - self.vars, - coeffs, - self.ObjConstant, + qmatrix=self.objective_qmatrix, + qvars=self.vars, + vars=self.vars, + coefficients=coeffs, + constant=self.ObjConstant, ) + @property + def ObjValue(self): + return self.Obj.getValue() + def getCSR(self): """ Computes and returns the CSR representation of the @@ -1670,32 +1858,27 @@ def getCSR(self): self.constraint_csr_matrix = csr_dict return self.dict_to_object(csr_dict) - def getQcsr(self): - if self.objective_qcsr_matrix is not None: - return self.dict_to_object(self.objective_qcsr_matrix) - vars1, vars2, coeffs = self.objective_qcoo_matrix - if not vars1: + def getQCSR(self): + """ + Computes and returns the CSR matrix representation of the + quadratic objective. + """ + if self.objective_qmatrix is None: return None - Qdict = {} - Qcsr_dict = {"row_pointers": [0], "column_indices": [], "values": []} - for i, var1 in enumerate(vars1): - if var1.index not in Qdict: - Qdict[var1.index] = {} - row_dict = Qdict[var1.index] - var2 = vars2[i] - coeff = coeffs[i] - row_dict[var2.index] = ( - row_dict[var2.index] + coeff - if var2.index in row_dict - else coeff - ) - for i in range(0, self.NumVariables): - if i in Qdict: - Qcsr_dict["column_indices"].extend(list(Qdict[i].keys())) - Qcsr_dict["values"].extend(list(Qdict[i].values())) - Qcsr_dict["row_pointers"].append(len(Qcsr_dict["column_indices"])) - self.objective_qcsr_matrix = Qcsr_dict - return self.dict_to_object(Qcsr_dict) + qcsr_matrix = { + "row_pointers": self.objective_qmatrix.indptr, + "column_indices": self.objective_qmatrix.indices, + "values": self.objective_qmatrix.data, + } + return self.dict_to_object(qcsr_matrix) + + def getQcsr(self): + warnings.warn( + "This function is deprecated and will be removed." + "Please use getQCSR instead.", + DeprecationWarning, + ) + return self.getQCSR() def relax(self): """ @@ -1725,7 +1908,6 @@ def populate_solution(self, solution): else: IsMIP = True self.SolutionStats = self.dict_to_object(solution.get_milp_stats()) - primal_sol = solution.get_primal_solution() reduced_cost = solution.get_reduced_cost() if len(primal_sol) > 0: @@ -1744,7 +1926,6 @@ def populate_solution(self, solution): if dual_sol is not None and len(dual_sol) > 0: constr.DualValue = dual_sol[i] constr.Slack = constr.compute_slack() - self.ObjValue = self.Obj.getValue() self.solved = True def solve(self, settings=solver_settings.SolverSettings()): @@ -1765,9 +1946,7 @@ def solve(self, settings=solver_settings.SolverSettings()): """ if self.model is None: self._to_data_model() - # Call Solver solution = solver.Solve(self.model, settings) - # Post Solve self.populate_solution(solution) diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 85a944e2e8..91565d7267 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum, auto @@ -230,7 +230,7 @@ def set_pdlp_warm_start_data(self, pdlp_warm_start_data): ---------- pdlp_warm_start_data : PDLPWarmStartData PDLP warm start data obtained from a previous solve. - Refer :py:meth:`cuopt.linear_programming.problem.Problem.get_pdlp_warm_start_data` # noqa + Refer :py:meth:`cuopt.linear_programming.problem.Problem.getWarmstartData` # noqa Notes ----- 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 06ed937033..fa5e274e16 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import math @@ -20,6 +20,7 @@ Problem, VType, sense, + QuadraticExpression, ) from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_AUGMENTED, @@ -748,15 +749,15 @@ def test_quadratic_expression_and_matrix(): # Test Quadratic Matrix problem.setObjective(expr9) - Qcsr = problem.getQcsr() + Qcsr = problem.getQCSR() exp_row_ptrs = [0, 0, 3, 6] exp_col_inds = [0, 1, 2, 0, 1, 2] exp_vals = [21, -7, 7, 3, -1, 1] - assert Qcsr.row_pointers == exp_row_ptrs - assert Qcsr.column_indices == exp_col_inds - assert Qcsr.values == exp_vals + assert list(Qcsr.row_pointers) == exp_row_ptrs + assert list(Qcsr.column_indices) == exp_col_inds + assert list(Qcsr.values) == exp_vals def test_quadratic_objective_1(): @@ -813,3 +814,117 @@ def test_quadratic_objective_2(): assert x2.getValue() == pytest.approx(0.0000000, abs=0.000001) assert x3.getValue() == pytest.approx(0.1092896, abs=1e-3) assert problem.ObjValue == pytest.approx(-0.284153, abs=1e-3) + + +def test_quadratic_matrix_1(): + problem = Problem() + x1 = problem.addVariable(lb=1, name="x1") + x2 = problem.addVariable(lb=1, name="x2") + x3 = problem.addVariable(lb=2.0, name="x3") + x4 = problem.addVariable(lb=1, name="x4") + + # Constraints + problem.addConstraint(x1 + x2 + x3 + x4 <= 10, "c1") + problem.addConstraint(2 * x1 - x2 + x4 >= 5, "c2") + + # Quadratic objective + # Minimize 2 x1^2 + 3 x2^2 + x3^2 + 4 x4^2 + 1.5 x1 x2 - 2 x3 x4 - 4 x1 + x2 + 3 x3 + 5 + + quad_matrix = [[2, 1.5, 0, 0], [0, 3, 0, 0], [0, 0, 1, -2], [0, 0, 0, 4]] + lin_terms = x2 + 3 * x3 - 4 * x1 + 5 + quad_expr = ( + 2 * x1 * x1 + + 3 * x2 * x2 + + 1 * x3 * x3 + + 4 * x4 * x4 + + 1.5 * x1 * x2 + - 2 * x3 * x4 + - 4 * x1 + + 1 * x2 + + 3 * x3 + + 5 + ) + + # Break down obj into multiple expressions + lin_mix_1 = x2 + 3 * x3 + lin_mix_2 = 4 * x1 + quad_mix_1 = [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 2]] + quad_mix_2 = 3 * x2 * x2 + quad_mix_3 = [[1, 1.5, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 2]] + quad_mix_4 = 2 * x3 * x4 - 5 + + # Expected Solution + obj_value_exp = 25.25 + x1_exp = 2.5 + x2_exp = 1 + x3_exp = 2 + x4_exp = 1 + + # Solve 1 + problem.setObjective(quad_expr) + problem.solve() + assert problem.ObjValue == pytest.approx(obj_value_exp, abs=1e-3) + + # Solve 2 + quad_obj = QuadraticExpression(quad_matrix, problem.getVariables()) + problem.setObjective(quad_obj + lin_terms) + problem.solve() + assert problem.ObjValue == pytest.approx(obj_value_exp, abs=1e-3) + + # Solve 3 + vars = problem.getVariables() + qmatrix1 = QuadraticExpression(quad_mix_1, vars) + qmatrix3 = QuadraticExpression(quad_mix_3, vars) + quad_obj = ( + lin_mix_1 + qmatrix1 + quad_mix_2 + qmatrix3 - quad_mix_4 - lin_mix_2 + ) + problem.setObjective(quad_obj) + problem.solve() + assert problem.ObjValue == pytest.approx(obj_value_exp, abs=1e-3) + + # Verify accessor functions + q_vars = quad_obj.getVariables() + q_coeffs = quad_obj.getCoefficients() + lin_expr = quad_obj.getLinearExpression() + obj_value = 0.0 + for i, (var1, var2) in enumerate(q_vars): + obj_value += var1.Value * var2.Value * q_coeffs[i] + obj_value += lin_expr.getValue() + assert obj_value == pytest.approx(obj_value_exp, abs=1e-3) + assert quad_obj.getValue() == pytest.approx(obj_value_exp, abs=1e-3) + assert x1.Value == pytest.approx(x1_exp, abs=1e-3) + assert x2.Value == pytest.approx(x2_exp, abs=1e-3) + assert x3.Value == pytest.approx(x3_exp, abs=1e-3) + assert x4.Value == pytest.approx(x4_exp, abs=1e-3) + + +def test_quadratic_matrix_2(): + # Minimize 4 x1^2 + 2 x2^2 + 3 x3^2 + 1.5 x1 x3 - 2 x1 + 0.5 x2 - x3 + 4 + # subject to x1 + 2*x2 + x3 <= 3 + # x1 >= 0 + # x2 >= 0 + # x3 >= 0 + + problem = Problem() + x1 = problem.addVariable(lb=0, name="x") + x2 = problem.addVariable(lb=0, name="y") + x3 = problem.addVariable(lb=0, name="z") + + problem.addConstraint(x1 + 2 * x2 + x3 <= 3) + + Q = [[4, 0, 1.5], [0, 2, 0], [0, 0, 3]] + quad_expr = QuadraticExpression(qmatrix=Q, qvars=problem.getVariables()) + quad_expr1 = quad_expr + 4 # Quad_matrix add constant + quad_expr2 = quad_expr1 - x3 # Quad_matrix sub variable + quad_expr2 -= 2 * x1 # Quad_matrix isub lin_expr + quad_expr2 += 0.5 * x2 # Quad_matrix iadd lin_expr + + problem.setObjective(quad_expr2) + + problem.solve() + + assert problem.Status.name == "Optimal" + assert x1.getValue() == pytest.approx(0.2295081, abs=1e-3) + assert x2.getValue() == pytest.approx(0.0000000, abs=1e-3) + assert x3.getValue() == pytest.approx(0.1092896, abs=1e-3) + assert problem.ObjValue == pytest.approx(3.715847, abs=1e-3) diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 05ca7ffa8f..cbd86a3766 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "pyyaml>=6.0.0", "rapids-logger==0.2.*,>=0.0.0a0", "rmm==26.2.*,>=0.0.0a0", + "scipy>=1.14.1", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Intended Audience :: Developers",