UpwindTransportSolver.hpp
Go to the documentation of this file.
1/*
2 Copyright 2026 Equinor ASA.
3
4 This file is part of the Open Porous Media project (OPM).
5
6 OPM is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 OPM is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with OPM. If not, see <http://www.gnu.org/licenses/>.
18*/
19
20#ifndef OPM_UPWIND_TRANSPORT_SOLVER_HEADER_INCLUDED
21#define OPM_UPWIND_TRANSPORT_SOLVER_HEADER_INCLUDED
22
23#include <opm/common/utility/SparseTable.hpp>
24
26
27#include <algorithm>
28#include <array>
29#include <cmath>
30#include <cstddef>
31#include <span>
32#include <stdexcept>
33#include <string>
34#include <utility>
35#include <vector>
36
55
56namespace Opm
57{
58
61{
64 int maxDirectSize = 200;
65
68 double relTolerance = 1e-4;
69
72 double absTolerance = 1e-20;
73
75 int maxIterations = 100;
76};
77
80{
83
85 int numLoops = 0;
86
89
92};
93
94namespace detail
95{
96
105 template <class Scalar>
106 void solveDenseSystem(std::vector<Scalar>& a,
107 std::vector<Scalar>& b,
108 std::size_t m,
109 std::size_t numRhs)
110 {
111 for (std::size_t col = 0; col < m; ++col) {
112 // Gaussian elimination with partial pivoting.
113 std::size_t pivot = col;
114 for (std::size_t row = col + 1; row < m; ++row) {
115 if (std::abs(a[row * m + col]) > std::abs(a[pivot * m + col])) {
116 pivot = row;
117 }
118 }
119 if (a[pivot * m + col] == Scalar {0}) {
120 throw std::runtime_error("Opm::solveReordered: singular matrix");
121 }
122 if (pivot != col) {
123 for (std::size_t k = 0; k < m; ++k) {
124 std::swap(a[pivot * m + k], a[col * m + k]);
125 }
126 for (std::size_t k = 0; k < numRhs; ++k) {
127 std::swap(b[pivot * numRhs + k], b[col * numRhs + k]);
128 }
129 }
130 for (std::size_t row = col + 1; row < m; ++row) {
131 const Scalar factor = a[row * m + col] / a[col * m + col];
132 if (factor == Scalar {0}) {
133 continue;
134 }
135 for (std::size_t k = col; k < m; ++k) {
136 a[row * m + k] -= factor * a[col * m + k];
137 }
138 for (std::size_t k = 0; k < numRhs; ++k) {
139 b[row * numRhs + k] -= factor * b[col * numRhs + k];
140 }
141 }
142 }
143 for (std::size_t col = m; col-- > 0;) {
144 for (std::size_t k = 0; k < numRhs; ++k) {
145 Scalar sum = b[col * numRhs + k];
146 for (std::size_t j = col + 1; j < m; ++j) {
147 sum -= a[col * m + j] * b[j * numRhs + k];
148 }
149 b[col * numRhs + k] = sum / a[col * m + col];
150 }
151 }
152 }
153
160 template <class Block>
161 bool isNonzeroBlock(const Block& block)
162 {
163 for (std::size_t a = 0; a < Block::rows; ++a) {
164 for (std::size_t b = 0; b < Block::cols; ++b) {
165 if (block[a][b] != 0) {
166 return true;
167 }
168 }
169 }
170 return false;
171 }
172
178 template <class Block>
179 bool isFiniteBlock(const Block& block)
180 {
181 for (std::size_t a = 0; a < Block::rows; ++a) {
182 for (std::size_t b = 0; b < Block::cols; ++b) {
183 if (!std::isfinite(block[a][b])) {
184 return false;
185 }
186 }
187 }
188 return true;
189 }
190
197 template <class Matrix, class Function>
198 void forEachNonzeroCoupling(const Matrix& matrix, Function&& f)
199 {
200 for (auto row = matrix.begin(); row != matrix.end(); ++row) {
201 for (auto entry = row->begin(); entry != row->end(); ++entry) {
202 if (entry.index() != row.index() && isNonzeroBlock(*entry)) {
203 f(row.index(), entry.index(), *entry);
204 }
205 }
206 }
207 }
208
221 template <class Matrix>
223 {
224 using Block = typename Matrix::block_type;
225 static_assert(Block::rows == Block::cols, "The matrix blocks must be square");
226
227 public:
228 using Scalar = typename Block::field_type;
229 static constexpr std::size_t blockSize = Block::rows;
230
238 explicit MatrixCoefficients(const Matrix& matrix)
239 {
240 if (matrix.N() != matrix.M()) {
241 throw std::invalid_argument("Opm::solveReordered: the matrix must be square");
242 }
243 diagonal_.reserve(matrix.N());
244 for (std::size_t r = 0; r < matrix.N(); ++r) {
245 const auto entry = matrix[r].find(r);
246 if (entry == matrix[r].end()) {
247 throw std::invalid_argument("Opm::solveReordered: row " + std::to_string(r)
248 + " has no diagonal block");
249 }
250 // Checked up front, since an infinite entry can give a finite solution, e.g.
251 // b / inf = 0 or after elimination in a directly solved loop
252 if (!isFiniteBlock(*entry)) {
253 throw std::runtime_error("Opm::solveReordered: NaN or infinite entry in the "
254 "diagonal block of row " + std::to_string(r));
255 }
256 diagonal_.push_back(&*entry);
257 }
258
259 std::vector<int> rowStart(matrix.N() + 1, 0);
260 std::vector<int> columns;
261 std::vector<const Block*> blocks;
263 matrix, [&](std::size_t r, std::size_t c, const Block& block) {
264 if (!isFiniteBlock(block)) {
265 throw std::runtime_error("Opm::solveReordered: NaN or infinite entry in "
266 "the block of row " + std::to_string(r)
267 + ", column " + std::to_string(c));
268 }
269 ++rowStart[r + 1];
270 columns.push_back(static_cast<int>(c));
271 blocks.push_back(&block);
272 });
273 for (std::size_t r = 0; r < matrix.N(); ++r) {
274 rowStart[r + 1] += rowStart[r];
275 }
276 graph_ = SparseTable<int>(std::move(columns), std::vector<int>(rowStart));
277 couplings_ = SparseTable<const Block*>(std::move(blocks), std::move(rowStart));
278 }
279
283 std::size_t numCells() const
284 {
285 return diagonal_.size();
286 }
287
293 const SparseTable<int>& graph() const
294 {
295 return graph_;
296 }
297
302 const Block& diagonal(std::size_t c) const
303 {
304 return *diagonal_[c];
305 }
306
312 template <class Function>
313 void forEachCoupling(std::size_t c, Function&& f) const
314 {
315 auto block = couplings_[static_cast<int>(c)].begin();
316 for (const int u : graph_[static_cast<int>(c)]) {
317 f(static_cast<std::size_t>(u), **block++);
318 }
319 }
320
321 private:
322 std::vector<const Block*> diagonal_;
323 SparseTable<int> graph_;
324 SparseTable<const Block*> couplings_;
325 };
326
327
333 template <class Coefficients, class Vectors>
335 {
336 using Scalar = typename Coefficients::Scalar;
337 static constexpr std::size_t N = Coefficients::blockSize;
338
339 public:
349 ReorderedSystem(const Coefficients& coefficients,
350 const Vectors& rhs,
351 Vectors& x,
352 const UpwindTransportSettings& settings,
353 const std::string& name)
354 : coefficients_(coefficients)
355 , rhs_(rhs)
356 , x_(x)
357 , settings_(settings)
358 , name_(name)
359 , numCells_(coefficients.numCells())
360 , numVectors_(rhs.size())
361 , localIndex_(numCells_, -1)
362 , cellSolution_(N * numVectors_)
363 , change_(N * numVectors_)
364 , scale_(N * numVectors_)
365 {
366 validate_();
367 }
368
378 {
379 const auto components = tarjan(coefficients_.graph());
380 UpwindTransportStatistics statistics;
381 statistics.numComponents = components.numComponents();
382 for (const auto cells : components.components()) {
383 const int size = static_cast<int>(cells.size());
384 statistics.largestComponent = std::max(statistics.largestComponent, size);
385 if (size == 1) {
386 solveCell_(cells);
387 } else {
388 ++statistics.numLoops;
389 if (size * static_cast<int>(N) <= settings_.maxDirectSize) {
390 solveDirectly_(cells);
391 } else {
392 const int sweeps = solveByGaussSeidel_(cells);
393 statistics.maxIterations = std::max(statistics.maxIterations, sweeps);
394 }
395 }
396 }
397 return statistics;
398 }
399
400 private:
404 void validate_() const
405 {
406 if (x_.size() != numVectors_) {
407 throw std::invalid_argument(name_ + ": sizes do not match");
408 }
409 for (std::size_t q = 0; q < numVectors_; ++q) {
410 if (static_cast<std::size_t>(rhs_[q].size()) != numCells_
411 || static_cast<std::size_t>(x_[q].size()) != numCells_) {
412 throw std::invalid_argument(name_ + ": wrong vector length");
413 }
414 }
415 }
416
426 void checkFinite_(Scalar value,
427 std::size_t q,
428 std::span<const int> cells,
429 bool useInLoop) const
430 {
431 if (!std::isfinite(value)) [[unlikely]] {
432 throw std::runtime_error(nonFiniteMessage_(q, cells, useInLoop));
433 }
434 }
435
445 std::string nonFiniteMessage_(std::size_t q,
446 std::span<const int> cells,
447 bool useInLoop) const
448 {
449 // A sorted copy, so that looking up the cells of a large loop stays cheap
450 std::vector<int> sorted(cells.begin(), cells.end());
451 std::ranges::sort(sorted);
452 const auto inCells = [&sorted](std::size_t u) {
453 return std::ranges::binary_search(sorted, static_cast<int>(u));
454 };
455 std::string cause;
456 for (const int cell : cells) {
457 const auto c = static_cast<std::size_t>(cell);
458 for (std::size_t a = 0; a < N && cause.empty(); ++a) {
459 if (!std::isfinite(rhs_[q][c][a])) {
460 cause = "the right-hand side in row " + std::to_string(c);
461 }
462 }
463 coefficients_.forEachCoupling(c, [&](std::size_t u, const auto&) {
464 if (!cause.empty()) {
465 return;
466 }
467 if (useInLoop || !inCells(u)) {
468 for (std::size_t b = 0; b < N; ++b) {
469 if (!std::isfinite(x_[q][u][b])) {
470 cause = "the value in row " + std::to_string(u)
471 + (inCells(u) ? ", an initial value of Gauss-Seidel" : "");
472 return;
473 }
474 }
475 }
476 });
477 if (!cause.empty()) {
478 break;
479 }
480 }
481 if (cause.empty()) {
482 cause = "an overflow in the solve";
483 }
484 return name_ + ": NaN or infinite solution for right-hand side " + std::to_string(q)
485 + " in a component with row " + std::to_string(cells.front()) + ", caused by "
486 + cause;
487 }
488
497 std::array<Scalar, N> rightHandSide_(std::size_t q, std::size_t c) const
498 {
499 std::array<Scalar, N> sum;
500 for (std::size_t a = 0; a < N; ++a) {
501 sum[a] = rhs_[q][c][a];
502 }
503 coefficients_.forEachCoupling(c, [&](std::size_t u, const auto& block) {
504 if (localIndex_[u] < 0) {
505 for (std::size_t a = 0; a < N; ++a) {
506 for (std::size_t b = 0; b < N; ++b) {
507 sum[a] -= block[a][b] * x_[q][u][b];
508 }
509 }
510 }
511 });
512 return sum;
513 }
514
521 void solveDiagonal_(std::size_t c)
522 {
523 if constexpr (N == 1) {
524 // One division
525 const Scalar diagonal = coefficients_.diagonal(c)[0][0];
526 if (diagonal == Scalar {0}) {
527 throw std::runtime_error(name_ + ": singular diagonal in row "
528 + std::to_string(c));
529 }
530 for (std::size_t q = 0; q < numVectors_; ++q) {
531 cellSolution_[q] = rightHandSide_(q, c)[0] / diagonal;
532 }
533 } else {
534 matrix_.resize(N * N);
535 const auto& diagonal = coefficients_.diagonal(c);
536 for (std::size_t a = 0; a < N; ++a) {
537 for (std::size_t b = 0; b < N; ++b) {
538 matrix_[a * N + b] = diagonal[a][b];
539 }
540 }
541 for (std::size_t q = 0; q < numVectors_; ++q) {
542 const auto sum = rightHandSide_(q, c);
543 for (std::size_t a = 0; a < N; ++a) {
544 cellSolution_[a * numVectors_ + q] = sum[a];
545 }
546 }
547 try {
548 solveDenseSystem(matrix_, cellSolution_, N, numVectors_);
549 } catch (const std::runtime_error&) {
550 throw std::runtime_error(name_ + ": singular diagonal block in row "
551 + std::to_string(c));
552 }
553 }
554 }
555
560 void solveCell_(std::span<const int> cells)
561 {
562 const auto c = static_cast<std::size_t>(cells[0]);
563 solveDiagonal_(c);
564 for (std::size_t q = 0; q < numVectors_; ++q) {
565 for (std::size_t a = 0; a < N; ++a) {
566 const Scalar value = cellSolution_[a * numVectors_ + q];
567 checkFinite_(value, q, cells, false);
568 x_[q][c][a] = value;
569 }
570 }
571 }
572
577 void solveDirectly_(std::span<const int> cells)
578 {
579 const std::size_t m = cells.size();
580 const std::size_t size = m * N;
581 matrix_.assign(size * size, Scalar {0});
582 solution_.assign(size * numVectors_, Scalar {0});
583 for (std::size_t i = 0; i < m; ++i) {
584 localIndex_[cells[i]] = static_cast<int>(i);
585 }
586 for (std::size_t i = 0; i < m; ++i) {
587 const auto c = static_cast<std::size_t>(cells[i]);
588 const auto& diagonal = coefficients_.diagonal(c);
589 for (std::size_t a = 0; a < N; ++a) {
590 for (std::size_t b = 0; b < N; ++b) {
591 matrix_[(i * N + a) * size + i * N + b] = diagonal[a][b];
592 }
593 }
594 coefficients_.forEachCoupling(c, [&](std::size_t u, const auto& block) {
595 if (localIndex_[u] >= 0) {
596 const auto j = static_cast<std::size_t>(localIndex_[u]);
597 for (std::size_t a = 0; a < N; ++a) {
598 for (std::size_t b = 0; b < N; ++b) {
599 matrix_[(i * N + a) * size + j * N + b] += block[a][b];
600 }
601 }
602 }
603 });
604 for (std::size_t q = 0; q < numVectors_; ++q) {
605 // Cells outside the loop are already solved.
606 const auto sum = rightHandSide_(q, c);
607 for (std::size_t a = 0; a < N; ++a) {
608 solution_[(i * N + a) * numVectors_ + q] = sum[a];
609 }
610 }
611 }
612 for (std::size_t i = 0; i < m; ++i) {
613 localIndex_[cells[i]] = -1;
614 }
615 try {
616 solveDenseSystem(matrix_, solution_, size, numVectors_);
617 } catch (const std::runtime_error&) {
618 throw std::runtime_error(name_ + ": singular matrix for a flow loop of "
619 + std::to_string(m) + " cells");
620 }
621 for (std::size_t i = 0; i < m; ++i) {
622 for (std::size_t q = 0; q < numVectors_; ++q) {
623 for (std::size_t a = 0; a < N; ++a) {
624 const Scalar value = solution_[(i * N + a) * numVectors_ + q];
625 checkFinite_(value, q, cells, false);
626 x_[q][static_cast<std::size_t>(cells[i])][a] = value;
627 }
628 }
629 }
630 }
631
638 int solveByGaussSeidel_(std::span<const int> cells)
639 {
640 for (int sweep = 1; sweep <= settings_.maxIterations; ++sweep) {
641 std::fill(change_.begin(), change_.end(), Scalar {0});
642 std::fill(scale_.begin(), scale_.end(), Scalar {0});
643 for (const int cell : cells) {
644 const auto c = static_cast<std::size_t>(cell);
645 solveDiagonal_(c);
646 for (std::size_t q = 0; q < numVectors_; ++q) {
647 for (std::size_t a = 0; a < N; ++a) {
648 auto& value = x_[q][c][a];
649 const Scalar updated = cellSolution_[a * numVectors_ + q];
650 checkFinite_(updated, q, cells, true);
651 const std::size_t k = q * N + a;
652 change_[k] = std::max(change_[k], std::abs(updated - value));
653 scale_[k] = std::max(scale_[k], std::abs(updated));
654 value = updated;
655 }
656 }
657 }
658 // Each block component converges on its own scale, so small components are not
659 // only converged relative to large ones. The absolute tolerance covers zero
660 bool converged = true;
661 for (std::size_t k = 0; k < change_.size(); ++k) {
662 converged = converged
663 && (change_[k] <= settings_.relTolerance * scale_[k]
664 || change_[k] <= settings_.absTolerance);
665 }
666 if (converged) {
667 return sweep;
668 }
669 }
670 throw std::runtime_error(name_ + ": no convergence after "
671 + std::to_string(settings_.maxIterations)
672 + " sweeps for a flow loop of " + std::to_string(cells.size())
673 + " cells");
674 }
675
676 const Coefficients& coefficients_;
677 const Vectors& rhs_;
678 Vectors& x_;
679 const UpwindTransportSettings& settings_;
680 std::string name_;
681 std::size_t numCells_;
682 std::size_t numVectors_;
683
684 // Work arrays, reused for every cell and flow loop
685 std::vector<int> localIndex_;
686 std::vector<Scalar> matrix_;
687 std::vector<Scalar> solution_;
688 std::vector<Scalar> cellSolution_;
689 std::vector<Scalar> change_;
690 std::vector<Scalar> scale_;
691 };
692
693} // namespace detail
694
714template <class Matrix, class Vectors>
715UpwindTransportStatistics
716solveReordered(const Matrix& matrix,
717 const Vectors& rhs,
718 Vectors& x,
719 const UpwindTransportSettings& settings = {})
720{
721 const detail::MatrixCoefficients<Matrix> coefficients(matrix);
722 return detail::ReorderedSystem<detail::MatrixCoefficients<Matrix>, Vectors>(
723 coefficients, rhs, x, settings, "Opm::solveReordered")
724 .solve();
725}
726
727} // namespace Opm
728
729#endif // OPM_UPWIND_TRANSPORT_SOLVER_HEADER_INCLUDED
The coefficients of solveReordered(), from a sparse block matrix.
Definition: UpwindTransportSolver.hpp:223
typename Block::field_type Scalar
Definition: UpwindTransportSolver.hpp:228
MatrixCoefficients(const Matrix &matrix)
Collect the diagonal blocks and the nonzero couplings of a matrix.
Definition: UpwindTransportSolver.hpp:238
void forEachCoupling(std::size_t c, Function &&f) const
Call a function for each cell that a cell depends on.
Definition: UpwindTransportSolver.hpp:313
static constexpr std::size_t blockSize
Definition: UpwindTransportSolver.hpp:229
const Block & diagonal(std::size_t c) const
The diagonal block of a cell.
Definition: UpwindTransportSolver.hpp:302
std::size_t numCells() const
The number of cells, i.e. block rows of the matrix.
Definition: UpwindTransportSolver.hpp:283
const SparseTable< int > & graph() const
The dependency graph, as input to Opm::tarjan().
Definition: UpwindTransportSolver.hpp:293
The system of solveReordered() and the work arrays for solving it.
Definition: UpwindTransportSolver.hpp:335
ReorderedSystem(const Coefficients &coefficients, const Vectors &rhs, Vectors &x, const UpwindTransportSettings &settings, const std::string &name)
Set up the system.
Definition: UpwindTransportSolver.hpp:349
UpwindTransportStatistics solve()
Solve the components in the order of Opm::tarjan().
Definition: UpwindTransportSolver.hpp:377
if(!linsolver_)
Definition: FlexibleSolver_impl.hpp:336
bool isFiniteBlock(const Block &block)
Whether all entries of a block are finite.
Definition: UpwindTransportSolver.hpp:179
void solveDenseSystem(std::vector< Scalar > &a, std::vector< Scalar > &b, std::size_t m, std::size_t numRhs)
Solve a dense system a x = b by Gaussian elimination with partial pivoting.
Definition: UpwindTransportSolver.hpp:106
void forEachNonzeroCoupling(const Matrix &matrix, Function &&f)
Call a function for every nonzero off-diagonal block of a sparse block matrix.
Definition: UpwindTransportSolver.hpp:198
bool isNonzeroBlock(const Block &block)
Whether any entry of a block is nonzero. NaN counts as nonzero, so that it is rejected by MatrixCoeff...
Definition: UpwindTransportSolver.hpp:161
Definition: blackoilbioeffectsmodules.hh:45
StronglyConnectedComponents tarjan(const Graph &graph)
Compute the strongly connected components of a directed graph with Tarjan's algorithm.
Definition: Tarjan.hpp:116
UpwindTransportStatistics solveReordered(const Matrix &matrix, const Vectors &rhs, Vectors &x, const UpwindTransportSettings &settings={})
Solve the block system A x_q = b_q described in the file comment.
Definition: UpwindTransportSolver.hpp:716
std::string to_string(const ConvergenceReport::ReservoirFailure::Type t)
Settings of solveReordered().
Definition: UpwindTransportSolver.hpp:61
double relTolerance
Definition: UpwindTransportSolver.hpp:68
int maxDirectSize
Definition: UpwindTransportSolver.hpp:64
int maxIterations
Most Gauss-Seidel sweeps over a flow loop before giving up.
Definition: UpwindTransportSolver.hpp:75
double absTolerance
Definition: UpwindTransportSolver.hpp:72
Statistics of a solveReordered() call.
Definition: UpwindTransportSolver.hpp:80
int largestComponent
Number of cells in the largest component.
Definition: UpwindTransportSolver.hpp:88
int numLoops
Number of components with more than one cell, i.e. flow loops.
Definition: UpwindTransportSolver.hpp:85
int maxIterations
Most sweeps used for any flow loop that was solved by iteration.
Definition: UpwindTransportSolver.hpp:91
int numComponents
Number of components, i.e. cells or flow loops.
Definition: UpwindTransportSolver.hpp:82