From 5437dfd078f4a7857d5f2680700c6e8b2e6891c2 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 08:57:04 +0800 Subject: [PATCH 01/12] Speed up `matrix.sum` --- src/pyscipopt/matrix.pxi | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 0548fbd10..570bbb752 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -18,11 +18,14 @@ def _is_number(e): class MatrixExpr(np.ndarray): def sum(self, **kwargs): """ - Based on `numpy.ndarray.sum`, but returns a scalar if the result is a single value. - This is useful for matrix expressions where the sum might reduce to a single value. + Based on `numpy.ndarray.sum`, but returns a scalar if `axis=None`. + This is useful for matrix expressions to compare with a matrix or a scalar. """ - res = super().sum(**kwargs) - return res if res.size > 1 else res.item() + + if kwargs.get("axis") is None: + # Speed up `.sum()` #1070 + return quicksum(self.flat) + return super().sum(**kwargs) def __le__(self, other: Union[float, int, Variable, np.ndarray, 'MatrixExpr']) -> np.ndarray: From 061eaac703bce5ef99a8249bae4772a15afb8b5f Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 08:57:45 +0800 Subject: [PATCH 02/12] Add test for sum with axis in matrix variable Added a test to verify that summing a matrix variable with axis=0 returns a MatrixExpr with the expected shape. This improves coverage for matrix sum operations with axis specified. --- tests/test_matrix_variable.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 0308bb694..a488f5aaa 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -170,6 +170,10 @@ def test_expr_from_matrix_vars(): def test_matrix_sum_argument(): m = Model() + # Return a array when axis isn't None + res = m.addMatrixVar((3, 1)).sum(axis=0) + assert isinstance(res, MatrixExpr) and res.shape == (1, 1) + # compare the result of summing 2d array to a scalar with a scalar x = m.addMatrixVar((2, 3), "x", "I", ub=4) m.addMatrixCons(x.sum() == 24) From 5204d782444bf6b68465972f2c0f4717cc7edd98 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 09:16:25 +0800 Subject: [PATCH 03/12] Add matrix sum performance test Introduced a new test to compare performance of matrix sum versus element-wise sum. Refactored imports for clarity and consistency. Renamed performance test for better description. --- tests/test_matrix_variable.py | 47 ++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index a488f5aaa..a4915b55a 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -1,11 +1,24 @@ -import pdb -import pprint -import pytest -from pyscipopt import Model, Variable, log, exp, cos, sin, sqrt -from pyscipopt import Expr, MatrixExpr, MatrixVariable, MatrixExprCons, MatrixConstraint, ExprCons from time import time import numpy as np +import pytest + +from pyscipopt import ( + Expr, + ExprCons, + MatrixConstraint, + MatrixExpr, + MatrixExprCons, + MatrixVariable, + Model, + Variable, + cos, + exp, + log, + quicksum, + sin, + sqrt, +) def test_catching_errors(): @@ -196,6 +209,28 @@ def test_matrix_sum_argument(): assert (m.getVal(x) == np.full((2, 3), 4)).all().all() assert (m.getVal(y) == np.full((2, 4), 3)).all().all() + +def test_sum_performance(): + n = 200 + start_orig = time() + + m = Model() + x = {} + for i in range(n): + for j in range(n): + x[(i, j)] = m.addVar(vtype="C", obj=1) + quicksum(x[i, j] for i in range(n) for j in range(n)) + end_orig = start_matrix = time() + + m = Model() + m.addMatrixVar((n, n), vtype="C", obj=1).sum() + end_matrix = time() + + matrix_time = end_matrix - start_matrix + orig_time = end_orig - start_orig + assert m.isGT(orig_time, matrix_time) + + def test_add_cons_matrixVar(): m = Model() matrix_variable = m.addMatrixVar(shape=(3, 3), vtype="B", name="A", obj=1) @@ -343,7 +378,7 @@ def test_MatrixVariable_attributes(): assert x.varMayRound().tolist() == [[True, True], [True, True]] @pytest.mark.skip(reason="Performance test") -def test_performance(): +def test_add_cons_performance(): start_orig = time() m = Model() x = {} From c425ebcfc56bd5189667c3cba35679af5a5fdbab Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 09:40:46 +0800 Subject: [PATCH 04/12] Use a more large size data --- tests/test_matrix_variable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index a4915b55a..63d4c0132 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -211,19 +211,19 @@ def test_matrix_sum_argument(): def test_sum_performance(): - n = 200 + m, n = 1000, 100 start_orig = time() m = Model() x = {} - for i in range(n): + for i in range(m): for j in range(n): x[(i, j)] = m.addVar(vtype="C", obj=1) - quicksum(x[i, j] for i in range(n) for j in range(n)) + quicksum(x[i, j] for i in range(m) for j in range(n)) end_orig = start_matrix = time() m = Model() - m.addMatrixVar((n, n), vtype="C", obj=1).sum() + m.addMatrixVar((m, n), vtype="C", obj=1).sum() end_matrix = time() matrix_time = end_matrix - start_matrix From 7b799b094d9a8f6ce52a9a4c12277bc59cb3fd37 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 09:47:17 +0800 Subject: [PATCH 05/12] Fix variable naming in test_sum_performance Renamed the Model instance from 'm' to 'model' to avoid confusion with the integer variable 'm' and improve code clarity in the test_sum_performance function. --- tests/test_matrix_variable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 63d4c0132..fccc47c73 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -214,16 +214,16 @@ def test_sum_performance(): m, n = 1000, 100 start_orig = time() - m = Model() + model = Model() x = {} for i in range(m): for j in range(n): - x[(i, j)] = m.addVar(vtype="C", obj=1) + x[(i, j)] = model.addVar(vtype="C", obj=1) quicksum(x[i, j] for i in range(m) for j in range(n)) end_orig = start_matrix = time() - m = Model() - m.addMatrixVar((m, n), vtype="C", obj=1).sum() + model = Model() + model.addMatrixVar((m, n), vtype="C", obj=1).sum() end_matrix = time() matrix_time = end_matrix - start_matrix From 929fc1a7266a7708b1340550987f3a8d139c877a Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 09:54:28 +0800 Subject: [PATCH 06/12] Fix variable name in performance test assertion Replaces incorrect usage of 'm' with 'model' in the assertion within test_sum_performance to ensure the correct object is referenced. --- tests/test_matrix_variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index fccc47c73..d1de9537f 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -228,7 +228,7 @@ def test_sum_performance(): matrix_time = end_matrix - start_matrix orig_time = end_orig - start_orig - assert m.isGT(orig_time, matrix_time) + assert model.isGT(orig_time, matrix_time) def test_add_cons_matrixVar(): From 6fb8c0d8d4db9445c577d654925112c9b80446be Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 9 Oct 2025 10:18:02 +0800 Subject: [PATCH 07/12] Fix expected shape in matrix sum test Updated the assertion in test_matrix_sum_argument to expect shape (1,) instead of (1, 1) when summing along axis 0. This aligns the test with the actual output of the sum operation. --- tests/test_matrix_variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index d1de9537f..7777ef1f0 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -185,7 +185,7 @@ def test_matrix_sum_argument(): # Return a array when axis isn't None res = m.addMatrixVar((3, 1)).sum(axis=0) - assert isinstance(res, MatrixExpr) and res.shape == (1, 1) + assert isinstance(res, MatrixExpr) and res.shape == (1,) # compare the result of summing 2d array to a scalar with a scalar x = m.addMatrixVar((2, 3), "x", "I", ub=4) From 44fd2ffdee92142a70455a880643db75415e1246 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 10 Oct 2025 08:55:01 +0800 Subject: [PATCH 08/12] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5e09fce..927ac641f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added ### Fixed ### Changed +- Speed up MatrixVariable.sum(axis=None) via quicksum ### Removed ## v5.6.0 - 2025.08.26 From f6ae036a784333876f0b9a7d2f60e3016bf8e9aa Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 10 Oct 2025 09:06:10 +0800 Subject: [PATCH 09/12] Adjust performance test assertion in matrix variable tests Modified the assertion in test_sum_performance to compare orig_time + 1 with matrix_time instead of orig_time. This may address timing precision or test flakiness. --- tests/test_matrix_variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 4ad67fc30..238d0b762 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -228,7 +228,7 @@ def test_sum_performance(): matrix_time = end_matrix - start_matrix orig_time = end_orig - start_orig - assert model.isGT(orig_time, matrix_time) + assert model.isGT(orig_time + 1, matrix_time) def test_add_cons_matrixVar(): From 9e52c453c262fd06e64be96a300cfb8c23a0d92c Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 10 Oct 2025 17:43:03 +0800 Subject: [PATCH 10/12] Try a bigger data size --- tests/test_matrix_variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 238d0b762..93659495e 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -211,7 +211,7 @@ def test_matrix_sum_argument(): def test_sum_performance(): - m, n = 1000, 100 + m, n = 2000, 1000 start_orig = time() model = Model() From 5a8cb87c237b6c0ce081e9c2fd9ea3228d39f27a Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 11 Oct 2025 08:51:03 +0800 Subject: [PATCH 11/12] Compare `np.sum` and `quicksum` --- tests/test_matrix_variable.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 93659495e..5097d6a2e 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -211,24 +211,21 @@ def test_matrix_sum_argument(): def test_sum_performance(): - m, n = 2000, 1000 - start_orig = time() - + n = 500 model = Model() - x = {} - for i in range(m): - for j in range(n): - x[(i, j)] = model.addVar(vtype="C", obj=1) - quicksum(x[i, j] for i in range(m) for j in range(n)) - end_orig = start_matrix = time() + x = model.addMatrixVar((n, n)) - model = Model() - model.addMatrixVar((m, n), vtype="C", obj=1).sum() + # Original sum via `np.sum` + start_orig = time() + np.sum(x) + end_orig = time() + + # Optimized sum via `quicksum` + start_matrix = time() + x.sum() end_matrix = time() - matrix_time = end_matrix - start_matrix - orig_time = end_orig - start_orig - assert model.isGT(orig_time + 1, matrix_time) + assert model.isGT(end_orig - start_orig, end_matrix - start_matrix) def test_add_cons_matrixVar(): From 6c95c94cc0518612d2c52cfa748ce3572409b3e0 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 18 Oct 2025 12:12:06 +0800 Subject: [PATCH 12/12] Try a bigger size --- tests/test_matrix_variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index ebe77857d..04f9d0fcc 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -211,7 +211,7 @@ def test_matrix_sum_argument(): def test_sum_performance(): - n = 500 + n = 1000 model = Model() x = model.addMatrixVar((n, n))