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')