From 657fd1817dd9c7b5ed00d5603bbf56af4acb13bf Mon Sep 17 00:00:00 2001 From: Yung-Yu Chen Date: Fri, 15 May 2026 07:25:24 +0800 Subject: [PATCH 1/3] Add `SimpleArray::as_span()` and `as_mdspan()` Expose C++23 view types over the contiguous buffer: - `as_span()` returns `std::span` over the contiguous body. - `as_mdspan()` returns `std::mdspan>` built from `shape()`. Ghost indices are not subtracted. The view spans the full underlying storage. Add `gtests/test_nopython_mdspan.cpp` covering 1D-4D shapes, ghost arrays for each rank, and the `as_span()` linear view. --- cpp/modmesh/buffer/SimpleArray.hpp | 32 +++ gtests/CMakeLists.txt | 1 + gtests/test_nopython_mdspan.cpp | 404 +++++++++++++++++++++++++++++ 3 files changed, 437 insertions(+) create mode 100644 gtests/test_nopython_mdspan.cpp diff --git a/cpp/modmesh/buffer/SimpleArray.hpp b/cpp/modmesh/buffer/SimpleArray.hpp index a282e9bfc..e7a2ff433 100644 --- a/cpp/modmesh/buffer/SimpleArray.hpp +++ b/cpp/modmesh/buffer/SimpleArray.hpp @@ -38,11 +38,14 @@ #include #include +#include #include #include #include #include +#include #include +#include #include #ifdef _MSC_VER @@ -1946,6 +1949,35 @@ class SimpleArray template value_type * vptr(Args... args) { return m_body + buffer_offset(m_stride, args...); } + std::span as_span() { return std::span(data(), size()); } + std::span as_span() const { return std::span(data(), size()); } + + template + std::mdspan> as_mdspan() + { + if (ndim() != N) + { + throw std::out_of_range( + std::format("SimpleArray::as_mdspan: rank {} does not match ndim() {}", N, ndim())); + } + std::array exts; + for (size_t i = 0; i < N; ++i) { exts[i] = shape(i); } + return std::mdspan>(data(), exts); + } + + template + std::mdspan> as_mdspan() const + { + if (ndim() != N) + { + throw std::out_of_range( + std::format("SimpleArray::as_mdspan: rank {} does not match ndim() {}", N, ndim())); + } + std::array exts; + for (size_t i = 0; i < N; ++i) { exts[i] = shape(i); } + return std::mdspan>(data(), exts); + } + /* Backdoor */ value_type const & data(size_t it) const { return data()[it]; } value_type & data(size_t it) { return data()[it]; } diff --git a/gtests/CMakeLists.txt b/gtests/CMakeLists.txt index 4fb7d3696..2ade2370a 100644 --- a/gtests/CMakeLists.txt +++ b/gtests/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable( test_nopython_transform.cpp test_nopython_rtree.cpp test_nopython_formatter.cpp + test_nopython_mdspan.cpp ${MODMESH_TOGGLE_SOURCES} ${MODMESH_BUFFER_SOURCES} ${MODMESH_SERIALIZATION_SOURCES} diff --git a/gtests/test_nopython_mdspan.cpp b/gtests/test_nopython_mdspan.cpp new file mode 100644 index 000000000..6510bcf85 --- /dev/null +++ b/gtests/test_nopython_mdspan.cpp @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2026, Yung-Yu Chen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include + +#ifdef Py_PYTHON_H +#error "Python.h should not be included." +#endif + +TEST(SimpleArray, mdspan_1d) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{6}); + for (size_t i = 0; i < 6; ++i) { arr(i) = static_cast(i); } + + auto ms = arr.as_mdspan<1>(); + EXPECT_EQ(ms.extent(0), 6u); + for (size_t i = 0; i < 6; ++i) { EXPECT_EQ(ms[i], arr(i)); } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 6u); + for (size_t i = 0; i < 6; ++i) { EXPECT_EQ(sp[i], arr(i)); } + + // Write through mdspan is visible via span and the underlying SimpleArray. + ms[3] = 99.0; + EXPECT_EQ(arr(3), 99.0); + EXPECT_EQ(sp[3], 99.0); +} + +TEST(SimpleArray, mdspan_1d_const) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{6}, 5.0); + const auto & carr = arr; + + auto ms = carr.as_mdspan<1>(); + static_assert(std::is_same_v); + EXPECT_EQ(ms.extent(0), 6u); + for (size_t i = 0; i < 6; ++i) { EXPECT_EQ(ms[i], 5.0); } + + auto sp = carr.as_span(); + static_assert(std::is_same_v); + EXPECT_EQ(sp.size(), 6u); + for (size_t i = 0; i < 6; ++i) { EXPECT_EQ(sp[i], 5.0); } +} + +TEST(SimpleArray, mdspan_1d_ghost) +{ + namespace mm = modmesh; + + // 1D array: 5 total elements, 1 ghost at the front. + mm::SimpleArray arr(mm::small_vector{5}); + arr.set_nghost(1); + for (size_t i = 0; i < 5; ++i) { arr.data(i) = static_cast(i); } + + // Both views span all 5 elements via data(). + auto ms = arr.as_mdspan<1>(); + EXPECT_EQ(ms.extent(0), 5u); + for (size_t i = 0; i < 5; ++i) { EXPECT_EQ(ms[i], arr.data(i)); } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 5u); + for (size_t i = 0; i < 5; ++i) { EXPECT_EQ(sp[i], arr.data(i)); } +} + +TEST(SimpleArray, mdspan_2d) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{3, 4}); + for (size_t i = 0; i < 3; ++i) + { + for (size_t j = 0; j < 4; ++j) + { + arr(i, j) = static_cast(i * 4 + j); + } + } + + auto ms = arr.as_mdspan<2>(); + EXPECT_EQ(ms.extent(0), 3u); + EXPECT_EQ(ms.extent(1), 4u); + for (size_t i = 0; i < 3; ++i) + { + for (size_t j = 0; j < 4; ++j) + { + EXPECT_EQ((ms[i, j]), arr(i, j)); + } + } + + // Linear view over the C-contiguous buffer; sp[i*4+j] == arr(i, j). + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 12u); + for (size_t i = 0; i < 3; ++i) + { + for (size_t j = 0; j < 4; ++j) + { + EXPECT_EQ(sp[i * 4 + j], arr(i, j)); + } + } + + // Write through mdspan is visible via span and the underlying SimpleArray. + ms[1, 2] = 99.0; + EXPECT_EQ(arr(1, 2), 99.0); + EXPECT_EQ(sp[1 * 4 + 2], 99.0); +} + +TEST(SimpleArray, mdspan_2d_const) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{2, 3}, 7.0); + const auto & carr = arr; + + auto ms = carr.as_mdspan<2>(); + static_assert(std::is_same_v); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + EXPECT_EQ((ms[i, j]), 7.0); + } + } + + auto sp = carr.as_span(); + static_assert(std::is_same_v); + EXPECT_EQ(sp.size(), 6u); + for (size_t i = 0; i < 6; ++i) { EXPECT_EQ(sp[i], 7.0); } +} + +TEST(SimpleArray, mdspan_2d_ghost) +{ + namespace mm = modmesh; + + // shape {5, 4}: 5 rows (1 ghost + 4 body), 4 columns. + mm::SimpleArray arr(mm::small_vector{5, 4}); + arr.set_nghost(1); + for (size_t idx = 0; idx < arr.size(); ++idx) { arr.data(idx) = static_cast(idx); } + + auto ms = arr.as_mdspan<2>(); + EXPECT_EQ(ms.extent(0), 5u); + EXPECT_EQ(ms.extent(1), 4u); + + // ms origin is data(), so ms[i, j] == data()[i * 4 + j]. + for (size_t i = 0; i < 5; ++i) + { + for (size_t j = 0; j < 4; ++j) + { + EXPECT_EQ((ms[i, j]), arr.data(i * 4 + j)); + } + } + + // Body rows start at ms row 1 (== nghost); ms[i+1, j] == arr(i, j). + for (size_t i = 0; i < 4; ++i) + { + for (size_t j = 0; j < 4; ++j) + { + EXPECT_EQ((ms[i + 1, j]), arr(i, j)); + } + } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 20u); + for (size_t k = 0; k < sp.size(); ++k) { EXPECT_EQ(sp[k], arr.data(k)); } +} + +TEST(SimpleArray, mdspan_3d) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{2, 3, 4}); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 4; ++k) + { + arr(i, j, k) = static_cast((i * 3 + j) * 4 + k); + } + } + } + + auto ms = arr.as_mdspan<3>(); + EXPECT_EQ(ms.extent(0), 2u); + EXPECT_EQ(ms.extent(1), 3u); + EXPECT_EQ(ms.extent(2), 4u); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 4; ++k) + { + EXPECT_EQ((ms[i, j, k]), arr(i, j, k)); + } + } + } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 24u); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 4; ++k) + { + EXPECT_EQ(sp[(i * 3 + j) * 4 + k], arr(i, j, k)); + } + } + } + + ms[1, 2, 3] = 99.0; + EXPECT_EQ(arr(1, 2, 3), 99.0); + EXPECT_EQ(sp[(1 * 3 + 2) * 4 + 3], 99.0); +} + +TEST(SimpleArray, mdspan_3d_ghost) +{ + namespace mm = modmesh; + + // shape {4, 3, 2}: 4 slices (2 ghost + 2 body), 3 rows, 2 columns. + mm::SimpleArray arr(mm::small_vector{4, 3, 2}); + arr.set_nghost(2); + for (size_t idx = 0; idx < arr.size(); ++idx) { arr.data(idx) = static_cast(idx); } + + auto ms = arr.as_mdspan<3>(); + EXPECT_EQ(ms.extent(0), 4u); + EXPECT_EQ(ms.extent(1), 3u); + EXPECT_EQ(ms.extent(2), 2u); + + // ms origin is data(), so ms[i, j, k] == data()[(i * 3 + j) * 2 + k]. + for (size_t i = 0; i < 4; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 2; ++k) + { + EXPECT_EQ((ms[i, j, k]), arr.data((i * 3 + j) * 2 + k)); + } + } + } + + // Body slices start at ms index 2 (== nghost); ms[i+2, j, k] == arr(i, j, k). + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 2; ++k) + { + EXPECT_EQ((ms[i + 2, j, k]), arr(i, j, k)); + } + } + } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 24u); + for (size_t k = 0; k < sp.size(); ++k) { EXPECT_EQ(sp[k], arr.data(k)); } +} + +TEST(SimpleArray, mdspan_4d) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{2, 3, 4, 5}); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 4; ++k) + { + for (size_t l = 0; l < 5; ++l) + { + arr(i, j, k, l) = static_cast(((i * 3 + j) * 4 + k) * 5 + l); + } + } + } + } + + auto ms = arr.as_mdspan<4>(); + EXPECT_EQ(ms.extent(0), 2u); + EXPECT_EQ(ms.extent(1), 3u); + EXPECT_EQ(ms.extent(2), 4u); + EXPECT_EQ(ms.extent(3), 5u); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 4; ++k) + { + for (size_t l = 0; l < 5; ++l) + { + EXPECT_EQ((ms[i, j, k, l]), arr(i, j, k, l)); + } + } + } + } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 120u); + for (size_t i = 0; i < 2; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 4; ++k) + { + for (size_t l = 0; l < 5; ++l) + { + EXPECT_EQ(sp[((i * 3 + j) * 4 + k) * 5 + l], arr(i, j, k, l)); + } + } + } + } + + ms[1, 2, 3, 4] = 99.0; + EXPECT_EQ(arr(1, 2, 3, 4), 99.0); + EXPECT_EQ(sp[((1 * 3 + 2) * 4 + 3) * 5 + 4], 99.0); +} + +TEST(SimpleArray, mdspan_4d_ghost) +{ + namespace mm = modmesh; + + // shape {4, 3, 2, 2}: 4 slices (1 ghost + 3 body), 3 rows, 2 columns, 2 depth. + mm::SimpleArray arr(mm::small_vector{4, 3, 2, 2}); + arr.set_nghost(1); + for (size_t idx = 0; idx < arr.size(); ++idx) { arr.data(idx) = static_cast(idx); } + + auto ms = arr.as_mdspan<4>(); + EXPECT_EQ(ms.extent(0), 4u); + EXPECT_EQ(ms.extent(1), 3u); + EXPECT_EQ(ms.extent(2), 2u); + EXPECT_EQ(ms.extent(3), 2u); + + // ms origin is data(), so ms[i, j, k, l] == data()[((i * 3 + j) * 2 + k) * 2 + l]. + for (size_t i = 0; i < 4; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 2; ++k) + { + for (size_t l = 0; l < 2; ++l) + { + EXPECT_EQ((ms[i, j, k, l]), arr.data(((i * 3 + j) * 2 + k) * 2 + l)); + } + } + } + } + + // Body slices start at ms index 1 (== nghost); ms[i+1, j, k, l] == arr(i, j, k, l). + for (size_t i = 0; i < 3; ++i) + { + for (size_t j = 0; j < 3; ++j) + { + for (size_t k = 0; k < 2; ++k) + { + for (size_t l = 0; l < 2; ++l) + { + EXPECT_EQ((ms[i + 1, j, k, l]), arr(i, j, k, l)); + } + } + } + } + + auto sp = arr.as_span(); + EXPECT_EQ(sp.size(), 48u); + for (size_t k = 0; k < sp.size(); ++k) { EXPECT_EQ(sp[k], arr.data(k)); } +} + +TEST(SimpleArray, mdspan_rank_mismatch) +{ + namespace mm = modmesh; + + mm::SimpleArray arr(mm::small_vector{3, 4}); + EXPECT_THROW(arr.as_mdspan<3>(), std::out_of_range); +} + +// vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: From 5672eb0e67d433710d81bd5e3c0425e6858d8741 Mon Sep 17 00:00:00 2001 From: Yung-Yu Chen Date: Fri, 15 May 2026 07:25:24 +0800 Subject: [PATCH 2/3] Upgrade Ubuntu CI gcc-14 to gcc-16 for gcc-14 and gcc-15 from ppa:ubuntu-toolchain-r/test does not ship in libstdc++-15-dev. --- .github/actions/setup_linux/action.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup_linux/action.yml b/.github/actions/setup_linux/action.yml index 7f1aa6dd2..0ded6749c 100644 --- a/.github/actions/setup_linux/action.yml +++ b/.github/actions/setup_linux/action.yml @@ -89,16 +89,17 @@ runs: sudo ln -fs "$(which clang-tidy-22)" "/usr/local/bin/clang-tidy" echo "::endgroup::" - - name: Install and configure gcc-14 + - name: Install and configure gcc-16 shell: bash run: | - echo "::group::Install and configure gcc-14" + echo "::group::Install and configure gcc-16" + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test sudo apt-get -qqy update - sudo apt-get -qy install gcc-14 g++-14 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 - sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-14 100 - sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-14 100 + sudo apt-get -qy install gcc-16 g++-16 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-16 100 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-16 100 + sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-16 100 + sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-16 100 echo "::endgroup::" - name: install qt From 324c47215283869d22070f74bd964b726d35add6 Mon Sep 17 00:00:00 2001 From: Yung-Yu Chen Date: Fri, 15 May 2026 07:25:24 +0800 Subject: [PATCH 3/3] Require C-contiguous in `SimpleArray::as_span/as_mdspan` Both views assume linear row-major storage of prod(shape) elements starting at data(). Reject a non-C-contiguous array (e.g., padded or strided view) with std::runtime_error. Add `SimpleArray.mdspan_non_contiguous` to cover the new guard. --- cpp/modmesh/buffer/SimpleArray.hpp | 28 +++++++++++++++++++++++++--- gtests/test_nopython_mdspan.cpp | 20 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/cpp/modmesh/buffer/SimpleArray.hpp b/cpp/modmesh/buffer/SimpleArray.hpp index e7a2ff433..89f7d8f0f 100644 --- a/cpp/modmesh/buffer/SimpleArray.hpp +++ b/cpp/modmesh/buffer/SimpleArray.hpp @@ -1949,8 +1949,22 @@ class SimpleArray template value_type * vptr(Args... args) { return m_body + buffer_offset(m_stride, args...); } - std::span as_span() { return std::span(data(), size()); } - std::span as_span() const { return std::span(data(), size()); } + std::span as_span() + { + if (!is_c_contiguous()) + { + throw std::runtime_error("SimpleArray::as_span: array is not C-contiguous"); + } + return std::span(data(), size()); + } + std::span as_span() const + { + if (!is_c_contiguous()) + { + throw std::runtime_error("SimpleArray::as_span: array is not C-contiguous"); + } + return std::span(data(), size()); + } template std::mdspan> as_mdspan() @@ -1960,6 +1974,10 @@ class SimpleArray throw std::out_of_range( std::format("SimpleArray::as_mdspan: rank {} does not match ndim() {}", N, ndim())); } + if (!is_c_contiguous()) + { + throw std::runtime_error("SimpleArray::as_mdspan: array is not C-contiguous"); + } std::array exts; for (size_t i = 0; i < N; ++i) { exts[i] = shape(i); } return std::mdspan>(data(), exts); @@ -1973,6 +1991,10 @@ class SimpleArray throw std::out_of_range( std::format("SimpleArray::as_mdspan: rank {} does not match ndim() {}", N, ndim())); } + if (!is_c_contiguous()) + { + throw std::runtime_error("SimpleArray::as_mdspan: array is not C-contiguous"); + } std::array exts; for (size_t i = 0; i < N; ++i) { exts[i] = shape(i); } return std::mdspan>(data(), exts); @@ -2455,4 +2477,4 @@ class SimpleArrayPlex } /* end namespace modmesh */ -/* vim: set et ts=4 sw=4: */ +// vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: diff --git a/gtests/test_nopython_mdspan.cpp b/gtests/test_nopython_mdspan.cpp index 6510bcf85..bf1b09c07 100644 --- a/gtests/test_nopython_mdspan.cpp +++ b/gtests/test_nopython_mdspan.cpp @@ -401,4 +401,24 @@ TEST(SimpleArray, mdspan_rank_mismatch) EXPECT_THROW(arr.as_mdspan<3>(), std::out_of_range); } +TEST(SimpleArray, mdspan_non_contiguous) +{ + namespace mm = modmesh; + + // Build a 3x4 view whose stride differs from the row-major layout, so the + // array is neither C- nor F-contiguous over the underlying buffer. + mm::small_vector shape{3, 4}; + mm::small_vector stride{8, 1}; + auto buffer = mm::ConcreteBuffer::construct(3 * 8 * sizeof(double)); + mm::SimpleArray arr(shape, stride, buffer); + EXPECT_FALSE(arr.is_c_contiguous()); + + EXPECT_THROW(arr.as_span(), std::runtime_error); + EXPECT_THROW(arr.as_mdspan<2>(), std::runtime_error); + + const auto & carr = arr; + EXPECT_THROW(carr.as_span(), std::runtime_error); + EXPECT_THROW(carr.as_mdspan<2>(), std::runtime_error); +} + // vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: