diff --git a/src/pcms/coupler/field_communicator.hpp b/src/pcms/coupler/field_communicator.hpp index fa731487..2ff250b5 100644 --- a/src/pcms/coupler/field_communicator.hpp +++ b/src/pcms/coupler/field_communicator.hpp @@ -51,8 +51,7 @@ class FieldCommunicator PCMS_FUNCTION_TIMER; PCMS_ALWAYS_ASSERT(layout_comm_.GetChannel().InSendCommunicationPhase()); auto buffer = make_array_view(comm_buffer_); - serializer_->Serialize(field_.GetData(), field_.GetLayout(), buffer, - layout_comm_.GetPermutationArray()); + serializer_->Serialize(field_, buffer, layout_comm_.GetPermutationArray()); comm_.Send(buffer.data_handle(), mode); } @@ -64,8 +63,7 @@ class FieldCommunicator // mode because we make an immediate call to deserialize after a call to // receive. auto data = comm_.Recv(redev::Mode::Synchronous); - serializer_->Deserialize(field_.GetData(), field_.GetLayout(), - make_const_array_view(data), + serializer_->Deserialize(field_, make_const_array_view(data), layout_comm_.GetPermutationArray()); } diff --git a/src/pcms/coupler/field_serializer.h b/src/pcms/coupler/field_serializer.h index 723929cf..f619aa5a 100644 --- a/src/pcms/coupler/field_serializer.h +++ b/src/pcms/coupler/field_serializer.h @@ -14,12 +14,12 @@ template class FieldSerializer { public: - virtual int Serialize(const FieldData& field, const FieldLayout& layout, + virtual int Serialize(const Field& field, Rank1View buffer, Rank1View permutation) const { auto data = field.GetDOFHolderDataHost(); - auto owned = layout.GetOwnedHost(); + auto owned = field.GetLayout().GetOwnedHost(); // The exchange plan is per DOF holder: owned[i] and permutation[i] are // indexed by holder. All num_components components of a holder share its // location, so they occupy one contiguous block permutation[i]*num_comp in @@ -41,10 +41,10 @@ class FieldSerializer } virtual void Deserialize( - FieldData& field, const FieldLayout& layout, - Rank1View buffer, + Field& field, Rank1View buffer, Rank1View permutation) const { + const auto& layout = field.GetLayout(); const LO num_dof = layout.GetNumOwnedDofHolder(); const LO num_comp = layout.GetNumComponents(); Kokkos::View sorted("sorted", layout.OwnedSize()); diff --git a/src/pcms/coupler/serializer/xgc.h b/src/pcms/coupler/serializer/xgc.h index e2658377..b5c62a12 100644 --- a/src/pcms/coupler/serializer/xgc.h +++ b/src/pcms/coupler/serializer/xgc.h @@ -20,23 +20,22 @@ class XGCFieldSerializer : public FieldSerializer { } - int Serialize(const FieldData& field, const FieldLayout& layout, - Rank1View buffer, + int Serialize(const Field& field, Rank1View buffer, Rank1View permutation) const override { if (!rank_participates_) { return 0; } - auto const* xgc_field = dynamic_cast*>(&field); + auto const* xgc_field = + dynamic_cast*>(&field.GetData()); if (!xgc_field) { throw pcms_error("XGCFieldSerializer::Serialize: incompatible FieldData"); } auto data = xgc_field->GetDOFHolderDataHost(); - auto owned = layout.GetOwnedHost(); - // Per-holder plan: owned[i]/permutation[i] index holders; a holder's - // num_components values form one contiguous block in the wire buffer. + // Per-holder plan: permutation[i] indexes holders; a holder's num_components + // values form one contiguous block in the wire buffer. if (buffer.size() > 0) { const LO num_dof = static_cast(data.extent(0)); const LO num_comp = static_cast(data.extent(1)); @@ -54,17 +53,17 @@ class XGCFieldSerializer : public FieldSerializer } void Deserialize( - FieldData& field, const FieldLayout& layout, - Rank1View buffer, + Field& field, Rank1View buffer, Rank1View permutation) const override { - auto* xgc_field = dynamic_cast*>(&field); + auto* xgc_field = dynamic_cast*>(&field.GetData()); if (!xgc_field) { throw pcms_error( "XGCFieldSerializer::Deserialize: incompatible FieldData"); } auto current = xgc_field->GetDOFHolderDataHost(); + const auto& layout = field.GetLayout(); const LO num_dof = static_cast(current.extent(0)); const LO num_comp = static_cast(current.extent(1)); std::vector full_data(current.size()); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9a7eb845..93e68a33 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -400,7 +400,8 @@ if(Catch2_FOUND) message( STATUS "Found Catch2: ${Catch2_DIR} (found version ${Catch2_VERSION})") set(PCMS_UNIT_TEST_SOURCES unit_test_main.cpp test_coordinate_transform.cpp - test_coordinate.cpp test_bounding_box.cpp) + test_coordinate.cpp test_bounding_box.cpp + test_field_serializer.cpp) if(PCMS_ENABLE_XGC) list(APPEND PCMS_UNIT_TEST_SOURCES test_xgc_reverse_classification.cpp test_xgc_field_data.cpp) diff --git a/test/field_test_utils.h b/test/field_test_utils.h index 2ddd2f45..60b488fe 100644 --- a/test/field_test_utils.h +++ b/test/field_test_utils.h @@ -10,7 +10,6 @@ #include "pcms/field/evaluation_request.h" #include "pcms/field/point_evaluator.h" #include "pcms/field/out_of_bounds_policy.h" -#include "pcms/coupler/field_serializer.h" #include "pcms/field/coordinate_system.h" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" @@ -282,39 +281,6 @@ inline void SetField(FieldData& field, const FieldLayout& layout, SetField(layout, field, func); } -// Check that serialize followed by deserialize round-trips the data. -// Uses an identity permutation so permutation[i] = i. -inline void CheckSerializeDeserialize(const FieldLayout& layout, - FieldData& field) -{ - auto data_before = FlattenToRank1View(field.GetDOFHolderDataHost()); - int n = static_cast(data_before.size()); - - std::vector buffer(n); - std::vector perm(n); - for (int i = 0; i < n; ++i) - perm[i] = i; - - Rank1View buf_view(buffer.data(), n); - Rank1View perm_view(perm.data(), n); - - FieldSerializer serializer; - serializer.Serialize(field, layout, buf_view, perm_view); - serializer.Deserialize( - field, layout, Rank1View(buf_view), perm_view); - - auto data_after = FlattenToRank1View(field.GetDOFHolderDataHost()); - REQUIRE(data_after.size() == data_before.size()); - for (int i = 0; i < n; ++i) { - REQUIRE(data_after[i] == Catch::Approx(data_before[i])); - } -} - -inline void CheckSerializeDeserialize(Field& field) -{ - CheckSerializeDeserialize(field.GetLayout(), field.GetData()); -} - // Helper structure to hold device coordinates with proper lifetime management struct DeviceCoordinates { diff --git a/test/test_field_serializer.cpp b/test/test_field_serializer.cpp new file mode 100644 index 00000000..10a29e74 --- /dev/null +++ b/test/test_field_serializer.cpp @@ -0,0 +1,239 @@ +#include +#include + +#include "pcms/configuration.h" +#include "pcms/coupler/field_serializer.h" +#include "pcms/utility/arrays.h" + +#include +#include +#include + +#ifdef PCMS_ENABLE_OMEGA_H +#include "field_test_utils.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/utility/uniform_grid.h" +#include +#endif + +#ifdef PCMS_ENABLE_XGC +#include "pcms/coupler/serializer/xgc.h" +#include "pcms/field/data/xgc.h" +#include "pcms/field/function_space/xgc.h" +#endif + +namespace +{ + +#ifdef PCMS_ENABLE_OMEGA_H +void CheckSerializeDeserialize(pcms::Field& field) +{ + auto data_before = field.GetDOFHolderDataHost(); + const auto num_dof = static_cast(data_before.extent(0)); + const auto num_components = static_cast(data_before.extent(1)); + const auto num_values = static_cast(data_before.size()); + + std::vector expected(num_values); + for (pcms::LO i = 0; i < num_dof; ++i) { + for (pcms::LO c = 0; c < num_components; ++c) { + expected[static_cast(i) * num_components + c] = data_before(i, c); + } + } + + std::vector buffer(num_values); + std::vector permutation(num_dof); + std::iota(permutation.begin(), permutation.end(), pcms::LO{0}); + + pcms::FieldSerializer serializer; + REQUIRE(serializer.Serialize(field, pcms::make_array_view(buffer), + pcms::make_const_array_view(permutation)) == + static_cast(num_values)); + + std::vector cleared(num_values, -1.0); + field.SetDOFHolderDataHost( + pcms::Rank2View( + cleared.data(), num_dof, num_components)); + serializer.Deserialize(field, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(permutation)); + + auto data_after = field.GetDOFHolderDataHost(); + REQUIRE(data_after.size() == expected.size()); + for (pcms::LO i = 0; i < num_dof; ++i) { + for (pcms::LO c = 0; c < num_components; ++c) { + REQUIRE( + data_after(i, c) == + Catch::Approx(expected[static_cast(i) * num_components + c])); + } + } +} +#endif + +#ifdef PCMS_ENABLE_XGC +pcms::ReverseClassificationVertex CreateDummyReverseClassification(int size) +{ + pcms::ReverseClassificationVertex rc; + for (int i = 0; i < size; ++i) { + rc.Insert(i % 4 == 0 ? pcms::DimID{0, 0} : pcms::DimID{0, 1}, i); + } + return rc; +} + +bool InXGCOverlap(int, int id) +{ + return id == 0; +} +#endif + +} // namespace + +#ifdef PCMS_ENABLE_XGC +TEST_CASE("XGCFieldSerializer preserves inactive field entries") +{ + static constexpr int data_size = 16; + auto rc = CreateDummyReverseClassification(data_size); + pcms::XGCFieldFactory factory(rc, InXGCOverlap, data_size); + + std::vector data(data_size); + std::iota(data.begin(), data.end(), 0.0); + const auto original = data; + auto field = factory.CreateField( + "", std::make_unique>( + factory.GetXGCLayout(), pcms::FieldMetadata{}, + pcms::make_array_view(data))); + pcms::XGCFieldSerializer serializer(MPI_COMM_SELF); + + auto owned = field.GetLayout().GetOwnedHost(); + std::vector permutation(data_size, -1); + int entry = 0; + for (int i = 0; i < data_size; ++i) { + if (owned[i]) { + permutation[i] = entry++; + } + } + const int num_owned = entry; + REQUIRE(num_owned == 4); + + std::vector buffer(num_owned, -1.0); + REQUIRE(serializer.Serialize(field, pcms::make_array_view(buffer), + pcms::make_const_array_view(permutation)) == + num_owned); + + for (int i = 0; i < data_size; ++i) { + if (owned[i]) { + REQUIRE(buffer[permutation[i]] == data[i]); + buffer[permutation[i]] += 100.0; + } else { + REQUIRE(permutation[i] == -1); + } + } + + serializer.Deserialize(field, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(permutation)); + + auto after = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + for (int i = 0; i < data_size; ++i) { + if (owned[i]) { + REQUIRE(after[i] == Catch::Approx(original[i] + 100.0)); + } else { + REQUIRE(after[i] == Catch::Approx(original[i])); + } + } +} +#endif + +#ifdef PCMS_ENABLE_OMEGA_H +TEST_CASE("FieldSerializer round-trips a polynomial-reconstruction field") +{ + std::vector coords{0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0}; + pcms::Rank2View coords_view(coords.data(), + 4, 2); + auto space = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, pcms::CoordinateSystem::Cartesian); + auto field = space->CreateFunction(); + + std::vector data{5.0, 6.0, 7.0, 8.0}; + field.SetDOFHolderDataHost( + pcms::Rank2View(data.data(), 4, + 1)); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips a uniform-grid field") +{ + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {10.0, 10.0}; + grid.divisions = {3, 3}; + auto space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field = space->CreateFunction(); + + std::vector data(16); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast(i * 10); + } + field.SetDOFHolderDataHost( + pcms::Rank2View(data.data(), 16, + 1)); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips an order-1 Omega_h field") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + auto space = pcms::test::MakeP1Space(mesh); + auto field = space->CreateFunction(); + + pcms::test::SetField( + field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return pcms::test::linear_f(x, y); + }); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips a multi-component Omega_h field") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + constexpr int num_components = 3; + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, num_components, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto field = space->CreateFunction(); + + const int num_dof = field.GetLayout().GetNumOwnedDofHolder(); + std::vector data(static_cast(num_dof) * num_components); + for (int i = 0; i < num_dof; ++i) { + for (int c = 0; c < num_components; ++c) { + data[static_cast(i) * num_components + c] = i + 0.25 * c; + } + } + field.SetDOFHolderDataHost( + pcms::Rank2View( + data.data(), num_dof, num_components)); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips an order-0 Omega_h field") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + auto space = pcms::test::MakeP0Space(mesh); + auto field = space->CreateFunction(); + + const int num_dof = mesh.nelems(); + std::vector data(num_dof); + std::iota(data.begin(), data.end(), pcms::Real{0}); + field.SetDOFHolderDataHost( + pcms::Rank2View(data.data(), + num_dof, 1)); + + CheckSerializeDeserialize(field); +} +#endif diff --git a/test/test_omega_h_lagrange_field.cpp b/test/test_omega_h_lagrange_field.cpp index 0dd5ffc7..4345fce2 100644 --- a/test/test_omega_h_lagrange_field.cpp +++ b/test/test_omega_h_lagrange_field.cpp @@ -196,44 +196,6 @@ TEST_CASE("OmegaHLagrangeField order-1: out-of-bounds FILL mode") pcms::test::CheckFillMode(factory, field, fill_value, outside); } -TEST_CASE("OmegaHLagrangeField order-1: serialize / deserialize round-trip") -{ - auto lib = Omega_h::Library{}; - auto mesh = MakeBox2D(lib.world()); - auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory->CreateFunction(); - - pcms::test::SetField( - field.GetData(), *factory->GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); - pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); -} - -TEST_CASE( - "OmegaHLagrangeField order-1: multi-component serialize / deserialize " - "round-trip") -{ - auto lib = Omega_h::Library{}; - auto mesh = MakeBox2D(lib.world()); - const int nc = 3; - // The MeshFields backend is scalar-only; multi-component needs OmegaH. - auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, nc, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field = factory->CreateFunction(); - - const int n = factory->GetLayout()->GetNumOwnedDofHolder(); - std::vector data(static_cast(n) * nc); - for (int i = 0; i < n; ++i) - for (int c = 0; c < nc; ++c) - data[static_cast(i) * nc + c] = i + 0.25 * c; - field.GetData().SetDOFHolderDataHost( - pcms::Rank2View(data.data(), n, nc)); - - pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); -} - // ---- Order-0 field tests ---------------------------------------------------- TEST_CASE("OmegaHLagrangeField order-0: set/get DOF data round-trip") @@ -294,25 +256,6 @@ TEST_CASE("OmegaHLagrangeField order-0: out-of-bounds FILL mode") pcms::test::CheckFillMode(factory, field, fill_value, outside); } -TEST_CASE("OmegaHLagrangeField order-0: serialize / deserialize round-trip") -{ - auto lib = Omega_h::Library{}; - auto mesh = MakeBox2D(lib.world()); - auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 0, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory->CreateFunction(); - - int nelems = mesh.nelems(); - std::vector data(nelems); - for (int i = 0; i < nelems; ++i) - data[i] = static_cast(i); - pcms::Rank2View view(data.data(), nelems, - 1); - field.GetData().SetDOFHolderDataHost(view); - - pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); -} - // ---- Layout sharing communicator contract ----------------------------------- // // BEHAVIORAL CONTRACT (requires MPI/redev — exercised by diff --git a/test/test_polynomial_reconstruction_function_space.cpp b/test/test_polynomial_reconstruction_function_space.cpp index 0281c13f..2c836af9 100644 --- a/test/test_polynomial_reconstruction_function_space.cpp +++ b/test/test_polynomial_reconstruction_function_space.cpp @@ -92,24 +92,6 @@ TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field set/get DOF " } } -TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field serialize / " - "deserialize round-trip") -{ - auto coords = MakeCoords2D(); - Rank2View coords_view(coords.data(), 4, 2); - - auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian); - auto field = factory->CreateFunction(); - - std::vector data{5.0, 6.0, 7.0, 8.0}; - Rank2View data_view( - data.data(), static_cast(data.size()), 1); - field.GetData().SetDOFHolderDataHost(data_view); - - pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); -} - TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " "after temporary factory destruction") { diff --git a/test/test_uniform_grid_field.cpp b/test/test_uniform_grid_field.cpp index 699ec26a..56cd2b0a 100644 --- a/test/test_uniform_grid_field.cpp +++ b/test/test_uniform_grid_field.cpp @@ -248,30 +248,6 @@ TEST_CASE("UniformGrid field evaluation - piecewise constant") REQUIRE(std::abs(results_host(3, 0) - 3.25) < 1e-10); } -TEST_CASE("UniformGrid field serialization") -{ - pcms::UniformGrid<2> grid; - grid.bot_left = {0.0, 0.0}; - grid.edge_length = {10.0, 10.0}; - grid.divisions = {3, 3}; - - auto layout = std::make_shared>( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space->CreateFunction(); - - std::vector data(16); - for (size_t i = 0; i < 16; ++i) - data[i] = static_cast(i * 10); - - field.SetDOFHolderDataHost( - pcms::Rank2View( - data.data(), static_cast(data.size()), 1)); - - pcms::test::CheckSerializeDeserialize(field); -} - TEST_CASE("UniformGrid field copy") { pcms::UniformGrid<2> grid; diff --git a/test/test_xgc_field_data.cpp b/test/test_xgc_field_data.cpp index b487b1e1..498d855a 100644 --- a/test/test_xgc_field_data.cpp +++ b/test/test_xgc_field_data.cpp @@ -1,9 +1,6 @@ #include -#include #include "pcms/field/data/xgc.h" #include "pcms/field/function_space/xgc.h" -#include "pcms/coupler/serializer/xgc.h" -#include #include namespace @@ -54,63 +51,6 @@ TEST_CASE("XGC FieldLayout marks overlap entries and gids") } } -TEST_CASE("XGC FieldData serializer preserves inactive entries") -{ - static constexpr int data_size = 16; - auto rc = create_dummy_rc(data_size); - auto layout = - std::make_shared(rc, in_overlap, data_size); - - std::vector data(data_size); - std::iota(data.begin(), data.end(), 0.0); - auto original = data; - pcms::XGCFieldData field(layout, pcms::FieldMetadata{}, - pcms::make_array_view(data)); - pcms::XGCFieldSerializer serializer(MPI_COMM_SELF); - - auto owned = layout->GetOwnedHost(); - - // Non-owned / non-participating holders must have negative permutation - // entries (matching the real FieldExchangePlanner output). Only the 4 - // owned holders get compacted non-negative indices. - std::vector permutation(data_size, -1); - int entry = 0; - for (int i = 0; i < data_size; ++i) { - if (owned[i]) { - permutation[i] = entry++; - } - } - const int num_owned = entry; - REQUIRE(num_owned == 4); - - std::vector buffer(num_owned, -1.0); - - REQUIRE(serializer.Serialize(field, *layout, pcms::make_array_view(buffer), - pcms::make_const_array_view(permutation)) == - num_owned); - - for (int i = 0; i < data_size; ++i) { - if (owned[i]) { - REQUIRE(buffer[permutation[i]] == data[i]); - buffer[permutation[i]] += 100.0; - } else { - REQUIRE(permutation[i] == -1); - } - } - - serializer.Deserialize(field, *layout, pcms::make_const_array_view(buffer), - pcms::make_const_array_view(permutation)); - - auto after = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); - for (int i = 0; i < data_size; ++i) { - if (owned[i]) { - REQUIRE(after[i] == Catch::Approx(original[i] + 100.0)); - } else { - REQUIRE(after[i] == Catch::Approx(original[i])); - } - } -} - TEST_CASE("XGCFieldFactory creates fields and rejects evaluator access") { static constexpr int data_size = 16;