// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // balance_sparse.hpp // // Equilibration extension for SparseMatrix (= sparse version of LAPACK dgeequ). // // Useful as a preconditioning step when solving ill-conditioned sparse systems // with iterative solvers (CG / BiCGSTAB / GMRES / MINRES / FGMRES). It reuses // the same EquilibrateInfo + equilibrate_rhs + unequilibrate_solution as the // dense equilibrate_matrix. // // Recommended pattern: // auto [A_eq, info] = make_equilibrated_sparse(A); // auto b_eq = b; // equilibrate_rhs(b_eq, info); // auto result = bicgstab_logged(A_eq, b_eq, criteria, log); // unequilibrate_solution(result.x, info); // // Symmetric systems (CG / MINRES) require D_l = D_r so that D_l A D_r stays // symmetric. This implementation scales the left and right sides independently // (= non-symmetric), so it is not suitable for symmetric systems. For those, // use jacobi_preconditioner (= the existing D = 1/sqrt(diag(A))). #ifndef SANGI_BALANCE_SPARSE_HPP #define SANGI_BALANCE_SPARSE_HPP #include #include #include #include #include #include #include namespace sangi { namespace algorithms { /** * @brief Independent left/right scaling of a sparse matrix * (sparse equivalent of LAPACK dgeequ + dlaqge) * * @param A in const sparse matrix * @return {A_eq, info} pair. A_eq = D_l A D_r, with the sparsity pattern * preserved (an entry is dropped only if it becomes effectively zero). * * @note Algorithm: * 1. Take max |A(i,j)| over each row, then row_scale[i] = 1 / row_max[i] * 2. Take max |A(i,j)| over each column after row scaling, then * col_scale[j] = 1 / col_max[j] * 3. Build a new SparseMatrix with A_eq(i,j) = row_scale[i] * A(i,j) * col_scale[j] * * @note Use with an iterative solver: * auto [A_eq, info] = make_equilibrated_sparse(A); * equilibrate_rhs(b, info); // b โ† D_l b * auto result = solver(A_eq, b, ...); * unequilibrate_solution(result.x, info); // x โ† D_r y */ template std::pair, EquilibrateInfo> make_equilibrated_sparse(const SparseMatrix& A) { using IT = IndexType; const IT m = A.rows(); const IT n = A.cols(); EquilibrateInfo info; info.row_scale.assign(m, T(1)); info.col_scale.assign(n, T(1)); // Empty matrix โ†’ no-op SparseMatrix A_eq(m, n, SparseStorageFormat::COO); if (m == 0 || n == 0) return {std::move(A_eq), info}; // SparseMatrix exposes only CSC accessors (CSR is not public). // Scan all nnz in CSC order, accumulating row_max, and resolve // col_max one column at a time. const auto& csc_vals = A.csc_values(); const auto& csc_rows = A.csc_row_indices(); const auto& csc_ptr = A.csc_col_ptr(); std::vector row_max(static_cast(m), T(0)); T amax = T(0); for (IT j = 0; j < n; ++j) { const std::size_t s = static_cast(csc_ptr[j]); const std::size_t e = static_cast(csc_ptr[j + 1]); for (std::size_t k = s; k < e; ++k) { const IT i = csc_rows[k]; const T v = std::abs(csc_vals[k]); if (v > row_max[i]) row_max[i] = v; if (v > amax) amax = v; } } info.amax = amax; if (amax == T(0)) return {std::move(A_eq), info}; // row_scale[i] = 1 / row_max[i] T r_max = T(0), r_min = std::numeric_limits::max(); for (IT i = 0; i < m; ++i) { if (row_max[i] > T(0)) { info.row_scale[i] = T(1) / row_max[i]; if (row_max[i] > r_max) r_max = row_max[i]; if (row_max[i] < r_min) r_min = row_max[i]; } } info.row_cond = (r_max > T(0)) ? r_min / r_max : T(1); info.row_equilibrated = true; // Compute col_max after row scaling (= max_i |row_scale[i] ยท A(i, j)|). T c_max = T(0), c_min = std::numeric_limits::max(); for (IT j = 0; j < n; ++j) { T col_amax = T(0); const std::size_t s = static_cast(csc_ptr[j]); const std::size_t e = static_cast(csc_ptr[j + 1]); for (std::size_t k = s; k < e; ++k) { const IT i = csc_rows[k]; const T v = std::abs(csc_vals[k]) * info.row_scale[i]; if (v > col_amax) col_amax = v; } if (col_amax > T(0)) { info.col_scale[j] = T(1) / col_amax; if (col_amax > c_max) c_max = col_amax; if (col_amax < c_min) c_min = col_amax; } } info.col_cond = (c_max > T(0)) ? c_min / c_max : T(1); info.col_equilibrated = true; // Build the new sparse matrix: A_eq(i, j) = row_scale[i] * A(i, j) * col_scale[j] for (IT j = 0; j < n; ++j) { const T cs = info.col_scale[j]; const std::size_t s = static_cast(csc_ptr[j]); const std::size_t e = static_cast(csc_ptr[j + 1]); for (std::size_t k = s; k < e; ++k) { const IT i = csc_rows[k]; const T new_val = info.row_scale[i] * csc_vals[k] * cs; A_eq.set_coeff(i, j, new_val); } } return {std::move(A_eq), info}; } } // namespace algorithms } // namespace sangi #endif // SANGI_BALANCE_SPARSE_HPP