diff --git a/AUTHORS.md b/AUTHORS.md
index d82ccb599fe9..08b2823f0ad4 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -103,6 +103,7 @@ JonathanSmith1936
Joseph Signorelli
Joshua A. Kelly
Josy P. Pullockara
+Kajetan R. Gulaj
Kedar Naik
Kürşat Yurt
LaSerpe
diff --git a/Common/include/containers/CPyWrapperMatrixView.hpp b/Common/include/containers/CPyWrapperMatrixView.hpp
index 739773de4923..effcae95b05c 100644
--- a/Common/include/containers/CPyWrapperMatrixView.hpp
+++ b/Common/include/containers/CPyWrapperMatrixView.hpp
@@ -92,7 +92,7 @@ class CPyWrapperMatrixView {
/*--- Define the functions required by the interface macro. ---*/
inline const su2double& Access(unsigned long row, unsigned long col) const {
- if (row > rows_ || col > cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMatrixView");
+ if (row >= rows_ || col >= cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMatrixView");
return data_[row * cols_ + col];
}
inline su2double& Access(unsigned long row, unsigned long col) {
@@ -133,7 +133,7 @@ class CPyWrapperMarkerMatrixView {
/*--- Define the functions required by the interface macro. ---*/
inline const su2double& Access(unsigned long row, unsigned long col) const {
- if (row > rows_ || col > cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMarkerMatrixView");
+ if (row >= rows_ || col >= cols_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapperMarkerMatrixView");
return data_[vertices_[row]->GetNode() * cols_ + col];
}
inline su2double& Access(unsigned long row, unsigned long col) {
@@ -183,7 +183,8 @@ class CPyWrapper3DMatrixView {
/*--- Define the functions required by the interface macro. ---*/
inline const su2double& Access(unsigned long row, unsigned long col, unsigned long dim) const {
- if (row > rows_ || col > cols_ || dim > dims_) SU2_MPI::Error(name_ + " out of bounds", "CPyWrapper3DMatrixView");
+ if (row >= rows_ || col >= cols_ || dim >= dims_)
+ SU2_MPI::Error(name_ + " out of bounds", "CPyWrapper3DMatrixView");
return data_[row * (cols_ * dims_) + col * dims_ + dim];
}
inline su2double& Access(unsigned long row, unsigned long col, unsigned long dim) {
diff --git a/TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg b/TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg
new file mode 100644
index 000000000000..531d6c681125
--- /dev/null
+++ b/TestCases/py_wrapper/matrix_view_bounds/heat_rectangle.cfg
@@ -0,0 +1,36 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% SU2 configuration file %
+% Case description: Bounds checks of the Python wrapper matrix views %
+% File Version 8.5.0 "Harrier" %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+SOLVER= HEAT_EQUATION
+INC_NONDIM= DIMENSIONAL
+
+FREESTREAM_TEMPERATURE= 300.0
+MATERIAL_DENSITY= 1000.0
+SPECIFIC_HEAT_CP= 500.0
+THERMAL_CONDUCTIVITY_CONSTANT= 10.0
+
+MARKER_ISOTHERMAL= ( x_minus, 310.0, x_plus, 300.0 )
+MARKER_HEATFLUX= ( y_minus, 0.0, y_plus, 0.0 )
+MARKER_MONITORING= ( x_minus )
+
+NUM_METHOD_GRAD= GREEN_GAUSS
+CFL_NUMBER= 10.0
+CFL_ADAPT= NO
+
+LINEAR_SOLVER= FGMRES
+LINEAR_SOLVER_PREC= ILU
+LINEAR_SOLVER_ERROR= 1E-6
+LINEAR_SOLVER_ITER= 10
+
+MESH_FORMAT= RECTANGLE
+MESH_BOX_SIZE= ( 9, 5, 0 )
+MESH_BOX_LENGTH= ( 0.1, 0.05, 0 )
+
+OUTPUT_FILES= NONE
+SCREEN_OUTPUT= INNER_ITER, RMS_RES, MAX_RES, TOTAL_HEATFLUX
+
+ITER= 11
+CONV_RESIDUAL_MINVAL= -12
diff --git a/TestCases/py_wrapper/matrix_view_bounds/run.py b/TestCases/py_wrapper/matrix_view_bounds/run.py
new file mode 100755
index 000000000000..2f12106ab1f9
--- /dev/null
+++ b/TestCases/py_wrapper/matrix_view_bounds/run.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+
+## \file run.py
+# \brief Bounds checks of the Python wrapper matrix views.
+# \version 8.5.0 "Harrier"
+#
+# SU2 Project Website: https://su2code.github.io
+#
+# The SU2 Project is maintained by the SU2 Foundation
+# (http://su2foundation.org)
+#
+# Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md)
+#
+# SU2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# SU2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SU2. If not, see .
+
+import subprocess
+import sys
+import pysu2
+
+def Views(driver):
+ """
+ One view of each class: CPyWrapperMatrixView, CPyWrapperMarkerMatrixView, CPyWrapper3DMatrixView.
+ """
+ iSolver = driver.GetSolverIndices()['HEAT']
+ iMarker = driver.GetMarkerIndices()['x_plus']
+ return {'Solution': driver.Solution(iSolver),
+ 'MarkerSolution': driver.MarkerSolution(iSolver, iMarker),
+ 'Gradient': driver.Gradient(iSolver)}
+
+def main():
+ """
+ Run the heat solver, then check that the views accept the last valid index and
+ stop with an error at the first index past the end, in every dimension.
+ Usage: run.py [ ], the second form is the child process.
+ """
+ cfg = sys.argv[1]
+
+ if len(sys.argv) == 4:
+ # Child process: access the view with the first index past the end of one dimension.
+ driver = pysu2.CSinglezoneDriver(cfg, 1, 0)
+ view = Views(driver)[sys.argv[2]]
+ iDim = int(sys.argv[3])
+ index = [0] * len(view.Shape())
+ index[iDim] = view.Shape()[iDim]
+ print(f'{sys.argv[2]}{tuple(index)} returned {view(*index)}')
+ return
+
+ # SU2_MPI::Error ends the process, so each access past the end runs in a child process,
+ # which must stop with the "out of bounds" error.
+ failed = []
+ for name, nDim in (('Solution', 2), ('MarkerSolution', 2), ('Gradient', 3)):
+ for iDim in range(nDim):
+ child = subprocess.run([sys.executable, __file__, cfg, name, str(iDim)],
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
+ if child.returncode == 0 or 'out of bounds' not in child.stdout:
+ returned = [line for line in child.stdout.splitlines() if ' returned ' in line]
+ failed.append(f'{name}, index past the end of dimension {iDim}: exit code {child.returncode}, '
+ f'no out of bounds error. {" ".join(returned)}')
+
+ driver = pysu2.CSinglezoneDriver(cfg, 1, 0)
+ driver.StartSolver()
+
+ # The last valid index of each view returns the value set through it.
+ for name, view in Views(driver).items():
+ index = [n - 1 for n in view.Shape()]
+ value = view(*index) + 1.0
+ view.Set(*index, value)
+ if view(*index) != value:
+ failed.append(f'{name}{tuple(index)} returned {view(*index)} instead of {value}')
+
+ driver.Finalize()
+
+ for message in failed:
+ print(f'FAILED: {message}')
+ if failed:
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/TestCases/serial_regression.py b/TestCases/serial_regression.py
index e1a826cbf170..e1c1f2bdaa6e 100755
--- a/TestCases/serial_regression.py
+++ b/TestCases/serial_regression.py
@@ -1760,6 +1760,19 @@ def main():
test_list.append(pywrapper_custom_inlet)
pass_list.append(pywrapper_custom_inlet.run_test(args.tsan, args.asan))
+ # Bounds checks of the matrix views
+ pywrapper_matrix_view_bounds = TestCase('pywrapper_matrix_view_bounds')
+ pywrapper_matrix_view_bounds.cfg_dir = "py_wrapper/matrix_view_bounds"
+ pywrapper_matrix_view_bounds.cfg_file = "heat_rectangle.cfg"
+ pywrapper_matrix_view_bounds.test_iter = 10
+ pywrapper_matrix_view_bounds.test_vals = [-5.122599, -4.820574, 127.760000]
+ pywrapper_matrix_view_bounds.command = TestCase.Command(exec = "python", param = "run.py")
+ pywrapper_matrix_view_bounds.timeout = 1600
+ pywrapper_matrix_view_bounds.tol = 0.00001
+ pywrapper_matrix_view_bounds.enabled_with_asan = False
+ test_list.append(pywrapper_matrix_view_bounds)
+ pass_list.append(pywrapper_matrix_view_bounds.run_test(args.tsan, args.asan))
+
# Tests summary
print('==================================================================')
print('Summary of the serial tests')