diff --git a/.github/workflows/cmake-test.yml b/.github/workflows/cmake-test.yml index 20c58f9b..d736a952 100644 --- a/.github/workflows/cmake-test.yml +++ b/.github/workflows/cmake-test.yml @@ -182,7 +182,11 @@ jobs: if [ "${{ matrix.meshfields }}" = "ON" ]; then meshfields_lib="${{ runner.temp }}/build-meshFields/install/lib:" fi - echo "LD_LIBRARY_PATH=${{ runner.temp }}/build-kokkos/install/lib:${{ runner.temp }}/build-omega_h/install/lib:${meshfields_lib}${{ runner.temp }}/build-redev/install/lib:${{ runner.temp }}/build-ADIOS2/install/lib:${{ runner.temp }}/build-perfstubs/install/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV + petsc_lib="" + if [ "${{ matrix.petsc }}" = "ON" ]; then + petsc_lib="${{ runner.temp }}/petsc/ubuntu-kokkos/lib:" + fi + echo "LD_LIBRARY_PATH=${{ runner.temp }}/build-kokkos/install/lib:${{ runner.temp }}/build-omega_h/install/lib:${meshfields_lib}${petsc_lib}${{ runner.temp }}/build-redev/install/lib:${{ runner.temp }}/build-ADIOS2/install/lib:${{ runner.temp }}/build-perfstubs/install/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - name: clone petsc if: matrix.petsc == 'ON' diff --git a/examples/python-api-example/example_conservative_transfer.py b/examples/python-api-example/example_conservative_transfer.py new file mode 100644 index 00000000..d6e6fd54 --- /dev/null +++ b/examples/python-api-example/example_conservative_transfer.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +""" +Conservative field transfer between two Omega_h meshes. + +This example: + 1. Builds a source Omega_h 2D simplex mesh, writes it to disk, and reads it + back (demonstrating the mesh read path). + 2. Evaluates an analytic function onto an order-1 (linear) Lagrange field on + the source mesh. + 3. Builds, writes, and reads back a *different* target Omega_h mesh that + carries its own order-1 Lagrange field. + 4. Transfers the source field onto the target field with both the Monte + Carlo (control-variate) and the mesh-intersection conservative projection + methods, and reports the L2 / max errors and the conservation error + (preservation of the integral) for each. + 5. Demonstrates the mesh-intersection projection across element orders as + well: P1 -> P0 (projection to cell averages) and P0 -> P1, confirming the + integral is conserved in both directions. + +The mesh-intersection conservative projection supports scalar Lagrange spaces +of order 0 (piecewise constant, one DOF per element) or order 1 (piecewise +linear, one DOF per vertex) on 2D simplex meshes; source and target orders may +differ. The Monte Carlo operator requires order-1 spaces. +""" + +import argparse +import os +import gc +import tempfile + +import numpy as np +import pcms + + +# Smooth analytic field sampled onto the source mesh. +def source_function(x, y): + return np.sin(np.pi * x) * np.sin(np.pi * y) + 0.5 * x + 0.25 * y + + +def build_and_roundtrip_mesh(world, lib, nx, ny, path): + """Build a 2D box mesh, write it to disk, and read it back.""" + mesh = pcms.build_box( + world, + pcms.Family.SIMPLEX, + 1.0, 1.0, 0.0, # unit square (z = 0 -> 2D) + nx, ny, 0, + False, + ) + pcms.write_mesh_binary(path, mesh) + del mesh + gc.collect() + return pcms.read_mesh_binary(path, lib) + + +def integral_of_field(mesh, field): + """Integral of an order-1 nodal field via element-averaged quadrature.""" + values = field.get_dof_holder_data() + elem_verts = mesh.ask_elem_verts().reshape(-1, 3) + areas = mesh.ask_sizes() # element areas for a 2D simplex mesh + elem_avg = values[elem_verts].mean(axis=1) + return float(np.sum(areas * elem_avg)) + + +def integral_of_p0_field(mesh, field): + """Integral of an order-0 field: sum of per-element value * element area.""" + values = field.get_dof_holder_data() # one value per element + areas = mesh.ask_sizes() + return float(np.sum(areas * values)) + + +def evaluate_function_onto_field(space, func): + """Create a field on `space` and fill it from `func` at the DOF holders.""" + field = space.create_field() + coords = field.get_dof_holder_coordinates() + field.set_dof_holder_data(func(coords[:, 0], coords[:, 1])) + return field + + +def report(name, target_field, target_mesh, reference_coords, + reference_values, source_integral, integrate=integral_of_field): + values = target_field.get_dof_holder_data() + err = values - reference_values + l2 = np.sqrt(np.mean(err ** 2)) + linf = np.max(np.abs(err)) + target_integral = integrate(target_mesh, target_field) + cons = abs(target_integral - source_integral) + print(f" {name}") + print(f" L2 error vs analytic : {l2:.3e}") + print(f" max error vs analytic : {linf:.3e}") + print(f" target integral : {target_integral:.6f}") + print(f" conservation error |dI| : {cons:.3e}") + + +def _triangulation(mesh, vertex_coords): + """matplotlib Triangulation from an Omega_h 2D simplex mesh.""" + import matplotlib.tri as mtri + tris = mesh.ask_elem_verts().reshape(-1, 3) + return mtri.Triangulation(vertex_coords[:, 0], vertex_coords[:, 1], tris) + + +def _draw_field(ax, triang, field, order, title, vmin, vmax): + values = field.get_dof_holder_data() + if order == 1: + # Nodal (per-vertex) values -> smooth (Gouraud) shading. + art = ax.tripcolor(triang, values, shading="gouraud", + vmin=vmin, vmax=vmax) + else: + # Cell (per-element) values -> flat shading. + art = ax.tripcolor(triang, facecolors=values, shading="flat", + vmin=vmin, vmax=vmax) + ax.set_title(title, fontsize=9) + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + return art + + +def make_plots(out_path, source_mesh, target_mesh, source_vertex_coords, + target_vertex_coords, panels): + """Render before/after field panels to `out_path` (PNG). + + `panels` is a list of (mesh_key, field, order, title) where mesh_key is + "source" or "target". Returns True on success, False if matplotlib is + unavailable. + """ + try: + import matplotlib + matplotlib.use("Agg") # file output, no display needed + import matplotlib.pyplot as plt + except ImportError: + print("\n[plot] matplotlib not available; skipping plots. " + "Install matplotlib to enable --plot.") + return False + + triangs = { + "source": _triangulation(source_mesh, source_vertex_coords), + "target": _triangulation(target_mesh, target_vertex_coords), + } + + # Shared color scale across panels so fields are directly comparable. + all_vals = np.concatenate([f.get_dof_holder_data() for _, f, _, _ in panels]) + vmin, vmax = float(all_vals.min()), float(all_vals.max()) + + ncols = 3 + nrows = (len(panels) + ncols - 1) // ncols + fig, axes = plt.subplots(nrows, ncols, figsize=(4 * ncols, 3.6 * nrows), + squeeze=False) + art = None + for idx, (mesh_key, field, order, title) in enumerate(panels): + ax = axes[idx // ncols][idx % ncols] + art = _draw_field(ax, triangs[mesh_key], field, order, title, vmin, vmax) + for idx in range(len(panels), nrows * ncols): + axes[idx // ncols][idx % ncols].axis("off") + + fig.colorbar(art, ax=axes, shrink=0.85, label="field value") + fig.suptitle("Conservative projection: before / after", fontsize=12) + fig.savefig(out_path, dpi=130, bbox_inches="tight") + plt.close(fig) + print(f"\n[plot] wrote {out_path}") + return True + + +def main(plot_path=None): + lib = pcms.OmegaHLibrary() + world = lib.world() + + work_dir = tempfile.mkdtemp(prefix="pcms_conservative_transfer_") + try: + print("=" * 64) + print("Conservative field transfer: MonteCarlo and mesh intersection") + print("=" * 64) + + # 1. Source mesh (read from disk) + linear Lagrange field. + source_path = os.path.join(work_dir, "source.osh") + source_mesh = build_and_roundtrip_mesh(world, lib, 12, 12, source_path) + print(f"\nSource mesh : {source_mesh.nverts()} verts, " + f"{source_mesh.nelems()} elems (read from {source_path})") + + # The conservative-projection operators require the native Omega_h + # Lagrange backend (not the default MeshFields layout). + source_space = pcms.LagrangeFunctionSpace.from_mesh( + source_mesh, 1, 1, pcms.CoordinateSystem.Cartesian, + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, + ) + source_field = evaluate_function_onto_field(source_space, + source_function) + source_integral = integral_of_field(source_mesh, source_field) + print(f"Source field: order-1 Lagrange, " + f"integral = {source_integral:.6f}") + + # 2. Target mesh (read from disk) with its own linear Lagrange field. + target_path = os.path.join(work_dir, "target.osh") + target_mesh = build_and_roundtrip_mesh(world, lib, 17, 17, target_path) + print(f"\nTarget mesh : {target_mesh.nverts()} verts, " + f"{target_mesh.nelems()} elems (read from {target_path})") + + target_space = pcms.LagrangeFunctionSpace.from_mesh( + target_mesh, 1, 1, pcms.CoordinateSystem.Cartesian, + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, + ) + + # Analytic reference values at the target DOF holders. + target_template = target_space.create_field() + target_coords = target_template.get_dof_holder_coordinates() + reference_values = source_function(target_coords[:, 0], + target_coords[:, 1]) + + print("\nTransferring source field to target field:") + + # 3a. Mesh-intersection conservative projection. + mi_target = target_space.create_field() + mesh_intersection = pcms.OmegaHConservativeProjection( + source_space, target_space + ) + mesh_intersection.apply(source_field, mi_target) + report("mesh intersection (exact quadrature)", mi_target, target_mesh, + target_coords, reference_values, source_integral) + + # 3b. Monte Carlo (control-variate) conservative projection. + mc_target = target_space.create_field() + monte_carlo = pcms.OmegaHControlVariateProjection( + source_space, + target_space, + samples_per_element=64, + sampling=pcms.MonteCarloSampling.UniformRandom, + seed=8675309, + ) + monte_carlo.apply(source_field, mc_target) + report("Monte Carlo (control variate, UniformRandom)", mc_target, target_mesh, + target_coords, reference_values, source_integral) + + # 4. Mixed-order mesh-intersection projections. The same operator works + # for any combination of order-0 (piecewise constant) and order-1 + # (piecewise linear) Lagrange spaces; the integral is conserved in + # every case. + print("\nMixed-order mesh-intersection projections:") + + # 4a. P1 source -> P0 target (project the linear field to cell averages). + target_p0_space = pcms.LagrangeFunctionSpace.from_mesh( + target_mesh, 0, 1, pcms.CoordinateSystem.Cartesian, + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, + ) + p1_to_p0 = target_p0_space.create_field() + # Analytic reference at the P0 DOF holders (element centroids). + p0_coords = p1_to_p0.get_dof_holder_coordinates() + p0_reference = source_function(p0_coords[:, 0], p0_coords[:, 1]) + proj_p1_p0 = pcms.OmegaHConservativeProjection( + source_space, target_p0_space + ) + proj_p1_p0.apply(source_field, p1_to_p0) + report("P1 -> P0 (cell averages)", p1_to_p0, target_mesh, + p0_coords, p0_reference, source_integral, + integrate=integral_of_p0_field) + + # 4b. P0 source -> P1 target. Build a P0 source field by sampling the + # analytic function at the source element centroids. + source_p0_space = pcms.LagrangeFunctionSpace.from_mesh( + source_mesh, 0, 1, pcms.CoordinateSystem.Cartesian, + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, + ) + source_p0_field = evaluate_function_onto_field(source_p0_space, + source_function) + source_p0_integral = integral_of_p0_field(source_mesh, source_p0_field) + p0_to_p1 = target_space.create_field() + proj_p0_p1 = pcms.OmegaHConservativeProjection( + source_p0_space, target_space + ) + proj_p0_p1.apply(source_p0_field, p0_to_p1) + report("P0 -> P1", p0_to_p1, target_mesh, + target_coords, reference_values, source_p0_integral) + + print("\nāœ“ Field transfer completed for P1<->P1, P1->P0, and P0->P1.") + + # 5. Optional before/after visualization. + if plot_path is not None: + source_vertex_coords = source_field.get_dof_holder_coordinates() + make_plots( + plot_path, source_mesh, target_mesh, + source_vertex_coords, target_coords, + panels=[ + ("source", source_field, 1, "source (P1)"), + ("target", mi_target, 1, "P1 -> P1 (mesh intersection)"), + ("target", p1_to_p0, 0, "P1 -> P0 (cell averages)"), + ("source", source_p0_field, 0, "source (P0)"), + ("target", p0_to_p1, 1, "P0 -> P1"), + ], + ) + + finally: + import shutil + shutil.rmtree(work_dir, ignore_errors=True) + #del world + #del lib + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--plot", nargs="?", const="conservative_transfer.png", default=None, + metavar="PATH", + help="render before/after field panels to PATH (default " + "conservative_transfer.png) using matplotlib, if installed", + ) + args = parser.parse_args() + main(plot_path=args.plot) diff --git a/src/pcms/capi/client.cpp b/src/pcms/capi/client.cpp index 3d320094..485e3c12 100644 --- a/src/pcms/capi/client.cpp +++ b/src/pcms/capi/client.cpp @@ -1,5 +1,6 @@ #include "client.h" #include "pcms.h" +#include "pcms/field/field_factory.h" #include "pcms/field/function_space/xgc.h" #include "pcms/field/data/xgc.h" #include "pcms/field/layout/xgc.h" @@ -23,11 +24,11 @@ namespace detail template struct XGCFieldRegistration { - XGCFunctionSpace function_space; + XGCFieldFactory function_space; MPI_Comm plane_comm; Rank1View data; - XGCFieldRegistration(XGCFunctionSpace fs, MPI_Comm comm, + XGCFieldRegistration(XGCFieldFactory fs, MPI_Comm comm, Rank1View d) : function_space(std::move(fs)), plane_comm(comm), data(d) { @@ -37,10 +38,15 @@ struct XGCFieldRegistration struct DummyFieldRegistration {}; -class EmptyFunctionSpace : public FunctionSpace +class EmptyFieldFactory : public FieldFactory { public: - EmptyFunctionSpace() : layout_(std::make_shared()) {} + explicit EmptyFieldFactory(std::string layout_name = "") + { + auto layout = std::make_shared(); + layout->SetName(std::move(layout_name)); + layout_ = layout; + } [[nodiscard]] std::shared_ptr GetLayout() const noexcept override @@ -48,11 +54,6 @@ class EmptyFunctionSpace : public FunctionSpace return layout_; } - [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override - { - return CoordinateSystem::Cartesian; - } - protected: [[nodiscard]] FieldVariant CreateFieldImpl( Type value_type, FieldMetadata metadata) const override @@ -75,12 +76,12 @@ class EmptyFunctionSpace : public FunctionSpace PCMS_ALWAYS_ASSERT(fd != nullptr); if (dynamic_cast*>(fd.get()) == nullptr) { throw pcms_error( - "EmptyFunctionSpace::CreateField: requires SimpleFieldData"); + "EmptyFieldFactory::CreateField: requires SimpleFieldData"); } if (fd->GetDOFHolderDataHost().size() != detail::ExpectedFlatFieldDataSize(*layout_)) { throw pcms_error( - "EmptyFunctionSpace::CreateField: field data size does not match " + "EmptyFieldFactory::CreateField: field data size does not match " "layout"); } return WrapField(layout_, std::forward(fd)); @@ -88,12 +89,6 @@ class EmptyFunctionSpace : public FunctionSpace std::move(data)); } - [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( - Type /*value_type*/, const EvaluationRequest& /*request*/) const override - { - throw pcms_error("EmptyFunctionSpace does not support point evaluation"); - } - private: std::shared_ptr layout_; }; @@ -130,29 +125,31 @@ ClientState::HandleVariant RegisterField( Application& app, std::string name, const detail::XGCFieldRegistration& registration, bool participates) { + // The layout's coupling identity is the field name + // not the arbitrary adapter name given at creation. + registration.function_space.SetLayoutName(name); auto field = registration.function_space.template CreateField( - std::make_unique>( - registration.function_space.GetXGCLayout(), FieldMetadata{}, - registration.data)); - app.AddLayout(name, registration.function_space.GetLayout(), participates); + std::move(name), std::make_unique>( + registration.function_space.GetXGCLayout(), + FieldMetadata{}, registration.data)); std::unique_ptr> serializer = std::make_unique>(registration.plane_comm, participates); - return ClientState::HandleVariant{app.AddField( - std::move(name), std::move(field), std::move(serializer), participates)}; + return ClientState::HandleVariant{ + app.AddField(std::move(field), std::move(serializer), participates)}; } inline ClientState::HandleVariant RegisterField( Application& app, std::string name, const detail::DummyFieldRegistration&, bool participates) { - auto function_space = detail::EmptyFunctionSpace{}; - app.AddLayout(name, function_space.GetLayout(), participates); - auto field = function_space.CreateField(FieldMetadata{}); + auto function_space = detail::EmptyFieldFactory{name}; + auto field = + function_space.CreateField(std::move(name), FieldMetadata{}); std::unique_ptr> serializer = std::make_unique>(); - return ClientState::HandleVariant{app.AddField( - std::move(name), std::move(field), std::move(serializer), participates)}; + return ClientState::HandleVariant{ + app.AddField(std::move(field), std::move(serializer), participates)}; } } // namespace pcms @@ -251,7 +248,7 @@ void pcms_create_xgc_field_adapter_t( { PCMS_ALWAYS_ASSERT((size > 0) ? (data != nullptr) : true); auto function_space = - pcms::XGCFunctionSpace(reverse_classification, in_overlap, size); + pcms::XGCFieldFactory(reverse_classification, in_overlap, size); pcms::Rank1View data_view( reinterpret_cast(data), size); field_adapter.emplace>( diff --git a/src/pcms/coupler/coupler.cpp b/src/pcms/coupler/coupler.cpp index a0c83e7d..91a1bf80 100644 --- a/src/pcms/coupler/coupler.cpp +++ b/src/pcms/coupler/coupler.cpp @@ -3,32 +3,23 @@ namespace pcms { -FieldLayoutCommunicator& Application::GetLayoutCommunicator( - const FieldLayout& layout) +FieldLayoutCommunicator& Application::GetOrCreateLayoutCommunicator( + const FieldLayout& layout, bool participates) { PCMS_FUNCTION_TIMER; - auto it = field_layout_communicators_.find(&layout); + const std::string& name = layout.GetName(); + if (name.empty()) { + throw pcms_error( + "Application: the field's layout has no name; name it at construction " + "(From*(..., layout_name)) before registering it with a coupler"); + } + + // Fields whose layouts share a name share one GID-exchange communicator. + auto it = field_layout_communicators_.find(name); if (it != field_layout_communicators_.end()) { return *it->second; - } else { - throw pcms_error("Field added with unregistered layout. Call AddLayout() " - "before AddField()."); } -} -const FieldLayout& Application::AddLayout( - std::string name, std::shared_ptr layout, - bool participates) -{ - return AddLayout(std::move(name), std::move(layout), - std::make_unique(), - participates); -} - -const FieldLayout& Application::AddLayout( - std::string name, std::shared_ptr layout, - std::unique_ptr planner, bool participates) -{ MPI_Comm mpi_comm_subset = MPI_COMM_NULL; bool own_mpi_comm = false; PCMS_ALWAYS_ASSERT((mpi_comm_ == MPI_COMM_NULL) ? (!participates) : true); @@ -39,21 +30,19 @@ const FieldLayout& Application::AddLayout( &mpi_comm_subset); own_mpi_comm = true; } - layouts_.push_back(std::move(layout)); - const FieldLayout& layout_ref = *layouts_.back(); - // Check if there's an overlap mask for this layout const OverlapMask* overlap_mask = nullptr; auto mask_it = layout_overlap_masks_.find(name); if (mask_it != layout_overlap_masks_.end()) { overlap_mask = mask_it->second.get(); } - field_layout_communicators_.emplace( - &layout_ref, std::make_unique( - name, mpi_comm_subset, redev_, channel_, layout_ref, - std::move(planner), own_mpi_comm, overlap_mask)); - return layout_ref; + auto [it2, inserted] = field_layout_communicators_.emplace( + name, std::make_unique( + name, mpi_comm_subset, redev_, channel_, layout, + std::make_unique(), own_mpi_comm, + overlap_mask)); + return *it2->second; } void Application::SetLayoutOverlapMask( diff --git a/src/pcms/coupler/coupler.hpp b/src/pcms/coupler/coupler.hpp index d2d12cb1..a129fa04 100644 --- a/src/pcms/coupler/coupler.hpp +++ b/src/pcms/coupler/coupler.hpp @@ -11,12 +11,16 @@ #include "pcms/utility/assert.h" #include "pcms/utility/common.h" #include "pcms/utility/profile.h" +#include "pcms/transfer/transfer_operator.hpp" +#include #include +#include namespace pcms { class Application; +class Coupler; template class FieldHandle @@ -31,11 +35,101 @@ class FieldHandle void Receive(redev::Mode mode = redev::Mode::Synchronous) const; [[nodiscard]] Field& GetField() const; + [[nodiscard]] const std::string& GetName() const noexcept { return name_; } + private: Application* app_; std::string name_; }; +// FunctionHandle is a FieldHandle that additionally carries the function space, +// so it can be evaluated / used to build transfer operators. Only AddFunction +// produces one; AddTransfer accepts FunctionHandle (not FieldHandle), which +// makes passing a comm-only field to a transfer a compile error. +template +class FunctionHandle : public FieldHandle +{ +public: + FunctionHandle(Application* app, std::string name, + std::shared_ptr space) + : FieldHandle(app, std::move(name)), space_(std::move(space)) + { + } + + [[nodiscard]] const FunctionSpace& GetSpace() const noexcept + { + return *space_; + } + [[nodiscard]] std::shared_ptr GetSpacePtr() + const noexcept + { + return space_; + } + +private: + std::shared_ptr space_; +}; + +// Transfer is the non-templated base for a runnable transfer, owned by the +// Coupler. Run() takes no typed arguments (the operator's source/target fields +// are bound in the concrete BoundTransfer), so it erases the value type T and +// lets the coupler hold a heterogeneous set of transfers uniformly. +class Transfer +{ +public: + virtual void Run() const = 0; + virtual ~Transfer() noexcept = default; +}; + +// BoundTransfer binds a built transfer operator to the source/target functions +// it connects. Owned by the Coupler (as a Transfer) and referenced through a +// TransferHandle. The operator is held by shared_ptr so a single built operator +// can back several BoundTransfers -- one per field pair that lives on the same +// (source, target) function-space pair -- amortizing the expensive build. +// Run() is the cheap per-step Apply. +template +class BoundTransfer : public Transfer +{ +public: + BoundTransfer(FunctionHandle source, FunctionHandle target, + std::shared_ptr> op) + : source_(std::move(source)), + target_(std::move(target)), + operator_(std::move(op)) + { + } + + void Run() const override + { + PCMS_FUNCTION_TIMER; + operator_->Apply(source_.GetField(), target_.GetField()); + } + +private: + FunctionHandle source_; + FunctionHandle target_; + std::shared_ptr> operator_; +}; + +// Lightweight, copyable reference to a coupler-owned transfer (mirrors +// FieldHandle). Identified by name; Run() dispatches to the owning Coupler. +class TransferHandle +{ +public: + void Run() const; + [[nodiscard]] const std::string& GetName() const noexcept { return name_; } + +private: + TransferHandle(Coupler* coupler, std::string name) + : coupler_(coupler), name_(std::move(name)) + { + } + friend class Coupler; + + Coupler* coupler_; + std::string name_; +}; + class Application { public: @@ -50,27 +144,33 @@ class Application PCMS_FUNCTION_TIMER; } - const FieldLayout& AddLayout(std::string name, - std::shared_ptr layout, - bool participates = true); - const FieldLayout& AddLayout(std::string name, - std::shared_ptr layout, - std::unique_ptr planner, - bool participates = true); - - // Set the overlap mask for a specific layout by name + // Set the overlap mask for a field's layout by name. Must be set before the + // AddField/AddFunction that first creates that layout's communicator. void SetLayoutOverlapMask(const std::string& layout_name, std::unique_ptr overlap_mask); + // Register a comm-only field, identified by its own name. The layout's + // communicator is created on first use and shared by any later + // field/function whose layout has the same name. template - FieldHandle AddField(std::string name, Field&& field, - bool participates = true); + FieldHandle AddField(Field&& field, bool participates = true); template - FieldHandle AddField(std::string name, Field&& field, + FieldHandle AddField(Field&& field, std::unique_ptr> serializer, bool participates = true); + // Register a transferable field: like AddField but retains the function space + // (via the stored Function) and returns a FunctionHandle usable in transfers. + template + FunctionHandle AddFunction(Function&& function, + bool participates = true); + + template + FunctionHandle AddFunction(Function&& function, + std::unique_ptr> serializer, + bool participates = true); + void SendField(const std::string& name, redev::Mode mode = redev::Mode::Synchronous) { @@ -144,20 +244,29 @@ class Application [[nodiscard]] Field& GetField(const std::string& name); private: - FieldLayoutCommunicator& GetLayoutCommunicator(const FieldLayout& layout); + // Returns the communicator for `layout`, creating it (MPI split + overlap + // mask lookup + planner) on first use and reusing it for any later + // field/function whose layout has the same name. + FieldLayoutCommunicator& GetOrCreateLayoutCommunicator( + const FieldLayout& layout, bool participates); + + template + void RegisterFieldCommunicator(Field& field_obj, + std::unique_ptr> serializer, + bool participates); MPI_Comm mpi_comm_; redev::Redev& redev_; redev::Channel channel_; - std::vector> layouts_; std::map fields_; + std::map functions_; std::vector> owned_field_layout_communicators_; // map is used rather than unordered_map because we give pointers to the // internal data and rehash of unordered_map can cause pointer invalidation. // map is less cache friendly, but pointers are not invalidated. std::map field_communicators_; - std::map> + std::map> field_layout_communicators_; std::map> layout_overlap_masks_; }; @@ -204,12 +313,55 @@ class Coupler return redev_.GetPartition(); } + // Register a named transfer that applies an already-built operator to a field + // pair. The name identifies the transfer for the returned handle, + // GetTransfer, and RunTransfer, and must be unique among this coupler's + // transfers. + // + // The operator is the currency: build it once -- via a method recipe + // (method::X{}.Build(src_space, tgt_space), see + // pcms/transfer/transfer_method.hpp) or by constructing a TransferOperator + // directly -- and bind it here. Because operators are shared, one operator + // can back several transfers whose fields share the same (source, target) + // function-space pair (e.g. displacement and velocity on one space), reusing + // its cached localization/assembly. The caller must pass fields whose spaces + // match the ones the operator was built for. + template + TransferHandle AddTransfer(std::string name, + std::shared_ptr> op, + FunctionHandle source, + FunctionHandle target); + + // One-shot convenience: build an operator from a method recipe and bind it to + // this single named field pair. NOTE: each call builds a fresh operator; to + // reuse one operator across several field pairs on the same space pair, build + // it once and use the overload above. + template + TransferHandle AddTransfer(std::string name, FunctionHandle source, + FunctionHandle target, const Method& method); + + // Retrieve a handle to a previously added transfer by name. + [[nodiscard]] TransferHandle GetTransfer(const std::string& name); + + // Run a previously added transfer by name. + void RunTransfer(const std::string& name) { FindTransfer(name).Run(); } + private: + Transfer& FindTransfer(const std::string& name) + { + auto it = transfers_.find(name); + if (it == transfers_.end()) { + throw pcms_error("Coupler: no transfer named '" + name + "'"); + } + return *it->second; + } + std::string name_; MPI_Comm mpi_comm_; redev::Redev redev_; // gather and scatter operations have reference to internal fields std::map applications_; + std::map> transfers_; }; } // namespace pcms @@ -238,47 +390,157 @@ pcms::Field& pcms::FieldHandle::GetField() const template pcms::Field& pcms::Application::GetField(const std::string& name) { - auto* field = std::get_if>(&detail::find_or_error(name, fields_)); - if (field == nullptr) { - throw pcms_error("Field stored with different type than requested"); + auto field_it = fields_.find(name); + if (field_it != fields_.end()) { + auto* field = std::get_if>(&field_it->second); + if (field == nullptr) { + throw pcms_error("Field stored with different type than requested"); + } + return *field; + } + auto fn_it = functions_.find(name); + if (fn_it != functions_.end()) { + auto* function = std::get_if>(&fn_it->second); + if (function == nullptr) { + throw pcms_error("Field stored with different type than requested"); + } + return *function; } - return *field; + throw pcms_error("Field '" + name + "' not found"); } template -pcms::FieldHandle pcms::Application::AddField(std::string name, - Field&& field, +void pcms::Application::RegisterFieldCommunicator( + Field& field_obj, std::unique_ptr> serializer, + bool participates) +{ + const std::string& name = field_obj.GetName(); + FieldLayoutCommunicator& layout_communicator = + GetOrCreateLayoutCommunicator(field_obj.GetLayout(), participates); + FieldCommunicator2Ptr field_communicator = + std::make_unique>(name, layout_communicator, field_obj, + std::move(serializer)); + auto [it, inserted] = + field_communicators_.emplace(name, std::move(field_communicator)); + if (!inserted) { + throw pcms_error("Field with this name already exists"); + } +} + +template +pcms::FieldHandle pcms::Application::AddField(Field&& field, bool participates) { - return AddField(std::move(name), std::move(field), - std::make_unique>(), participates); + return AddField(std::move(field), std::make_unique>(), + participates); } template pcms::FieldHandle pcms::Application::AddField( - std::string name, Field&& field, - std::unique_ptr> serializer, bool participates) + Field&& field, std::unique_ptr> serializer, + bool participates) { PCMS_FUNCTION_TIMER; - (void)participates; + std::string name = field.GetName(); + if (name.empty()) { + throw pcms_error("Application::AddField: field has no name"); + } auto [field_it, field_inserted] = fields_.emplace(name, std::move(field)); if (!field_inserted) { throw pcms_error("Field with this name already exists"); } - auto& field_obj = std::get>(field_it->second); - const FieldLayout& layout = field_obj.GetLayout(); - FieldLayoutCommunicator& layout_communicator = GetLayoutCommunicator(layout); - FieldCommunicator2Ptr field_communicator = - std::make_unique>(name, layout_communicator, field_obj, - std::move(serializer)); - - auto [it, inserted] = - field_communicators_.emplace(name, std::move(field_communicator)); - if (!inserted) { + try { + RegisterFieldCommunicator(std::get>(field_it->second), + std::move(serializer), participates); + } catch (...) { fields_.erase(field_it); - throw pcms_error("Field with this name already exists"); + throw; } return FieldHandle{this, std::move(name)}; } +template +pcms::FunctionHandle pcms::Application::AddFunction(Function&& function, + bool participates) +{ + return AddFunction(std::move(function), + std::make_unique>(), participates); +} + +template +pcms::FunctionHandle pcms::Application::AddFunction( + Function&& function, std::unique_ptr> serializer, + bool participates) +{ + PCMS_FUNCTION_TIMER; + std::string name = function.GetName(); + if (name.empty()) { + throw pcms_error("Application::AddFunction: function has no name"); + } + auto space = function.GetSpacePtr(); + auto [fn_it, fn_inserted] = functions_.emplace(name, std::move(function)); + if (!fn_inserted) { + throw pcms_error("Field with this name already exists"); + } + try { + RegisterFieldCommunicator(std::get>(fn_it->second), + std::move(serializer), participates); + } catch (...) { + functions_.erase(fn_it); + throw; + } + return FunctionHandle{this, std::move(name), std::move(space)}; +} + +inline void pcms::TransferHandle::Run() const +{ + PCMS_ALWAYS_ASSERT(coupler_ != nullptr); + coupler_->RunTransfer(name_); +} + +inline pcms::TransferHandle pcms::Coupler::GetTransfer(const std::string& name) +{ + FindTransfer(name); // throws if absent + return TransferHandle{this, name}; +} + +template +pcms::TransferHandle pcms::Coupler::AddTransfer( + std::string name, std::shared_ptr> op, + FunctionHandle source, FunctionHandle target) +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(op != nullptr); + if (name.empty()) { + throw pcms_error("Coupler::AddTransfer: transfer name must not be empty"); + } + if (&source.GetSpace() != &op->SourceSpace() || + &target.GetSpace() != &op->TargetSpace()) { + throw pcms_error("Coupler::AddTransfer: transfer '" + name + + "' binds a field whose function space differs from the " + "one its operator was built for"); + } + auto [it, inserted] = transfers_.emplace( + name, std::make_unique>(std::move(source), + std::move(target), std::move(op))); + if (!inserted) { + throw pcms_error("Coupler::AddTransfer: a transfer named '" + name + + "' already exists"); + } + return TransferHandle{this, std::move(name)}; +} + +template +pcms::TransferHandle pcms::Coupler::AddTransfer(std::string name, + FunctionHandle source, + FunctionHandle target, + const Method& method) +{ + PCMS_FUNCTION_TIMER; + std::shared_ptr> op = + method.Build(source.GetSpace(), target.GetSpace()); + return AddTransfer(std::move(name), std::move(op), std::move(source), + std::move(target)); +} + #endif // COUPLER2_H_ diff --git a/src/pcms/coupler/field_communicator.hpp b/src/pcms/coupler/field_communicator.hpp index b1874507..fa731487 100644 --- a/src/pcms/coupler/field_communicator.hpp +++ b/src/pcms/coupler/field_communicator.hpp @@ -34,7 +34,8 @@ class FieldCommunicator field_(field), serializer_(std::move(serializer)) { - PCMS_ALWAYS_ASSERT(&layout_comm.GetLayout() == &field.GetLayout()); + PCMS_ALWAYS_ASSERT(layout_comm.GetLayout().GetName() == + field.GetLayout().GetName()); // The exchange plan is per DOF holder; the field payload carries // num_components values per holder, so the buffer is scaled accordingly. comm_buffer_.resize( diff --git a/src/pcms/coupler/field_exchange_planner.cpp b/src/pcms/coupler/field_exchange_planner.cpp index 4bdcd574..fb37ce29 100644 --- a/src/pcms/coupler/field_exchange_planner.cpp +++ b/src/pcms/coupler/field_exchange_planner.cpp @@ -1,5 +1,6 @@ #include "pcms/coupler/field_exchange_planner.h" #include "partition.h" +#include "pcms/field/gid_permutation.hpp" #include "pcms/utility/assert.h" #include "pcms/utility/inclusive_scan.h" #include "pcms/utility/profile.h" @@ -45,11 +46,13 @@ static int GetMeshEntityDim(LO local_index, const EntOffsetsArray& ent_offsets) static size_t GetMessageBlockIndex( LO permutation_entry, Rank1View offsets) { - auto begin = offsets.data_handle(); - auto end = begin + offsets.size(); - auto it = std::upper_bound(begin, end, permutation_entry); - PCMS_ALWAYS_ASSERT(it != begin); - return static_cast(std::distance(begin, it - 1)); + for (size_t i = 0; i < offsets.size() - 1; ++i) { + if (permutation_entry >= offsets(i) && permutation_entry < offsets(i + 1)) { + return i; + } + } + PCMS_ALWAYS_ASSERT(false); + return 0; } static OutMsg ConstructOutMessage(const ReversePartitionMap2& reverse_partition) @@ -61,8 +64,7 @@ static OutMsg ConstructOutMessage(const ReversePartitionMap2& reverse_partition) out.dest.reserve(reverse_partition.size()); for (const auto& rank : reverse_partition) { out.dest.push_back(rank.first); - counts.push_back(rank.second.indices.size() + - rank.second.ent_offsets.size()); + counts.push_back(rank.second.indices.size()); } out.offset.resize(counts.size() + 1); out.offset[0] = 0; @@ -73,14 +75,14 @@ static OutMsg ConstructOutMessage(const ReversePartitionMap2& reverse_partition) static redev::LOs ConstructPermutation( const ReversePartitionMap2& reverse_partition, size_t num_entries, - int* length) + int& length) { PCMS_FUNCTION_TIMER; - redev::LOs permutation(num_entries); + // Holders that do not participate in this exchange (non-owned, or owned but + // outside the overlap region) stay unmapped; serialization skips them. + redev::LOs permutation = MakeUnmappedPermutation(num_entries); LO entry = 0; for (const auto& rank : reverse_partition) { - entry += ent_offsets_len; - for (unsigned e = 0; e < rank.second.ent_offsets.size() - 1; ++e) { const int start = rank.second.ent_offsets[e]; const int end = rank.second.ent_offsets[e + 1]; @@ -92,53 +94,59 @@ static redev::LOs ConstructPermutation( } } } - *length = entry; + length = entry; return permutation; } +// Parses a received GID message -- a concatenation of per-sender +// [entity-offset header | GIDs] blocks -- into the permutation mapping each +// local holder to the compact (header-excluded) buffer index of its GID. This +// owns the on-the-wire message format; the GID-to-holder matching is delegated +// to pcms::AppendGidPermutation. Holders absent from the message are left +// unmapped. Returns the total payload length (GID count excluding headers) via +// payload_length. static redev::LOs ConstructPermutation( GlobalIDView local_gids, GlobalIDView received_msg, - const EntOffsetsArray& ent_offsets) + const EntOffsetsArray& ent_offsets, int& payload_length) { PCMS_FUNCTION_TIMER; - std::array, 4> gid_to_buffer_index; + GidToIndexMaps gid_to_buffer_index; size_t offset = 0; + LO payload_offset = 0; while (true) { - GlobalIDView received_offsets( - received_msg.data_handle() + offset, ent_offsets_len); - int length = received_offsets[received_offsets.size() - 1]; - GlobalIDView received_gids( - received_msg.data_handle() + offset + ent_offsets_len, length); + // Guard the header read before dereferencing its last entry (the payload + // length), so a truncated message aborts rather than reading out of bounds. + PCMS_ALWAYS_ASSERT(offset + ent_offsets_len <= received_msg.size()); + int message_length = received_msg(offset + ent_offsets_len - 1); - PCMS_ALWAYS_ASSERT(offset + ent_offsets_len + length - 1 < + PCMS_ALWAYS_ASSERT(offset + ent_offsets_len + message_length - 1 < received_msg.size()); - for (size_t e = 0; e < received_offsets.size() - 1; ++e) { - size_t start = received_offsets[e]; - size_t end = received_offsets[e + 1]; + for (size_t e = 0; e < ent_offsets_len - 1; ++e) { + size_t start = received_msg(offset + e); + size_t end = received_msg(offset + e + 1); for (size_t i = start; i < end; ++i) { - gid_to_buffer_index[e][received_gids[i]] = offset + ent_offsets_len + i; + const GO gid = received_msg(offset + ent_offsets_len + i); + gid_to_buffer_index[e][gid] = payload_offset + i; } } - offset += length + ent_offsets_len; + payload_offset += message_length; + offset += message_length + ent_offsets_len; if (offset >= received_msg.size()) break; } redev::LOs permutation; permutation.reserve(local_gids.size()); - for (size_t e = 0; e < ent_offsets.size() - 1; ++e) { - const auto start = ent_offsets[e]; - const auto end = ent_offsets[e + 1]; - - for (size_t i = start; i < end; ++i) - permutation.push_back(gid_to_buffer_index[e][local_gids[i]]); - } + AppendGidPermutation(local_gids, ent_offsets, gid_to_buffer_index, + permutation, + /*allow_missing=*/true); REDEV_ALWAYS_ASSERT(permutation.size() == local_gids.size()); + payload_length = payload_offset; return permutation; } @@ -158,8 +166,11 @@ static OutMsg ConstructOutMessage(int rank, int nproc, totInMsgs - in.srcRanks[(nAppProcs - 1) * nproc + rank]; OutMsg out; for (size_t i = 0; i < nAppProcs; ++i) { - if (senderDeg[i] > 0) + if (senderDeg[i] > 0) { + REDEV_ALWAYS_ASSERT(senderDeg[i] > ent_offsets_len); + senderDeg[i] -= ent_offsets_len; out.dest.push_back(i); + } } redev::GO sum = 0; for (auto deg : senderDeg) { @@ -203,7 +214,7 @@ static ReversePartitionMap2 BuildReversePartitionMap( auto overlap_mask_view = overlap_mask.GetMask(layout); for (LO local_index = 0; local_index < n; ++local_index) { - if (!owned[local_index]) + if (!owned(local_index)) continue; if (!overlap_mask_view[local_index]) @@ -250,7 +261,7 @@ ExchangePlan GenericFieldExchangePlanner::BuildExchangePlan( int length = 0; plan.permutation = - ConstructPermutation(reverse_partition, gids.size(), &length); + ConstructPermutation(reverse_partition, gids.size(), length); plan.msg_size = static_cast(length); return plan; @@ -268,8 +279,10 @@ ExchangePlan GenericFieldExchangePlanner::BuildReceivePlan( auto out_msg = ConstructOutMessage(rank, nproc, in_message_layout); plan.dest_ranks = std::move(out_msg.dest); plan.offsets = std::move(out_msg.offset); - plan.permutation = ConstructPermutation(gids, received_gids, ent_offsets); - plan.msg_size = received_gids.size(); + int length = 0; + plan.permutation = + ConstructPermutation(gids, received_gids, ent_offsets, length); + plan.msg_size = static_cast(length); return plan; } @@ -278,7 +291,9 @@ void GenericFieldExchangePlanner::FillGidMessage( Rank1View gid_message) const { PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(static_cast(gid_message.size()) == plan.msg_size); + const size_t header_size = plan.dest_ranks.size() * ent_offsets_len; + PCMS_ALWAYS_ASSERT(static_cast(gid_message.size()) == + plan.msg_size + header_size); auto gids = layout.GetGidsHost(); auto owned = layout.GetOwnedHost(); @@ -294,9 +309,15 @@ void GenericFieldExchangePlanner::FillGidMessage( continue; LO perm_index = plan.permutation[local_index]; - gid_message[perm_index] = gids[local_index]; - + // Owned holders outside the overlap region carry the sentinel and have no + // slot in the message. + if (perm_index < 0) + continue; auto block_index = GetMessageBlockIndex(perm_index, offsets); + const auto gid_index = + perm_index + static_cast((block_index + 1) * ent_offsets_len); + gid_message(gid_index) = gids(local_index); + int mesh_ent_dim = GetMeshEntityDim(local_index, ent_offsets); for (size_t e = static_cast(mesh_ent_dim) + 1; e < ent_offsets_len; ++e) { @@ -306,9 +327,10 @@ void GenericFieldExchangePlanner::FillGidMessage( for (size_t block_index = 0; block_index < per_rank_offsets.size(); ++block_index) { - auto header_offset = static_cast(plan.offsets[block_index]); + auto header_offset = static_cast(plan.offsets[block_index]) + + block_index * ent_offsets_len; for (size_t e = 0; e < ent_offsets_len; ++e) { - gid_message[header_offset + e] = + gid_message(header_offset + e) = static_cast(per_rank_offsets[block_index][e]); } } diff --git a/src/pcms/coupler/field_exchange_planner.h b/src/pcms/coupler/field_exchange_planner.h index 4ea2fa54..5e5ae29f 100644 --- a/src/pcms/coupler/field_exchange_planner.h +++ b/src/pcms/coupler/field_exchange_planner.h @@ -13,6 +13,8 @@ namespace pcms struct ExchangePlan { redev::LOs dest_ranks; + // Field-payload offsets and permutation. GID-message headers are inserted + // only while exchanging the layout and are not represented in this plan. redev::LOs offsets; std::vector permutation; size_t msg_size = 0; diff --git a/src/pcms/coupler/field_layout_communicator.cpp b/src/pcms/coupler/field_layout_communicator.cpp index 713ba98c..00dabfb7 100644 --- a/src/pcms/coupler/field_layout_communicator.cpp +++ b/src/pcms/coupler/field_layout_communicator.cpp @@ -74,8 +74,13 @@ void FieldLayoutCommunicator::UpdateLayout() // overlap_mask_ is always non-null (created in constructor if not provided) plan_ = planner_->BuildExchangePlan( layout_, redev::Partition{redev_.GetPartition()}, overlap_mask_.get()); - gid_comm_.SetOutMessageLayout(plan_.dest_ranks, plan_.offsets); - std::vector gid_message(plan_.msg_size); + redev::LOs gid_offsets(plan_.offsets.size()); + for (size_t i = 0; i < plan_.offsets.size(); ++i) { + gid_offsets[i] = plan_.offsets[i] + i * ent_offsets_len; + } + gid_comm_.SetOutMessageLayout(plan_.dest_ranks, gid_offsets); + std::vector gid_message(plan_.msg_size + + plan_.dest_ranks.size() * ent_offsets_len); planner_->FillGidMessage( layout_, plan_, Rank1View(gid_message.data(), gid_message.size())); diff --git a/src/pcms/coupler/field_serializer.h b/src/pcms/coupler/field_serializer.h index 87246456..1838fb98 100644 --- a/src/pcms/coupler/field_serializer.h +++ b/src/pcms/coupler/field_serializer.h @@ -28,7 +28,9 @@ class FieldSerializer const LO num_dof = static_cast(data.extent(0)); const LO num_comp = static_cast(data.extent(1)); for (LO i = 0; i < num_dof; ++i) { - if (owned[i]) { + // A negative permutation entry marks a holder outside the exchange + // (non-owned, or owned but outside the overlap region); it has no slot. + if (owned[i] && permutation[i] >= 0) { for (LO c = 0; c < num_comp; ++c) { buffer[permutation[i] * num_comp + c] = data(i, c); } @@ -48,7 +50,10 @@ class FieldSerializer Kokkos::View sorted("sorted", layout.OwnedSize()); auto owned = layout.GetOwnedHost(); for (LO i = 0; i < num_dof; ++i) { - if (owned[i]) { + // A negative permutation entry marks a holder outside the exchange (owned + // but outside the overlap region); no data was received for it, so its + // zero-initialized `sorted` slot is left as-is. + if (owned[i] && permutation[i] >= 0) { for (LO c = 0; c < num_comp; ++c) { sorted[i * num_comp + c] = buffer[permutation[i] * num_comp + c]; } diff --git a/src/pcms/coupler/serializer/xgc.h b/src/pcms/coupler/serializer/xgc.h index ce09cf74..ec16d116 100644 --- a/src/pcms/coupler/serializer/xgc.h +++ b/src/pcms/coupler/serializer/xgc.h @@ -41,7 +41,9 @@ class XGCFieldSerializer : public FieldSerializer const LO num_dof = static_cast(data.extent(0)); const LO num_comp = static_cast(data.extent(1)); for (LO i = 0; i < num_dof; ++i) { - if (owned[i]) { + // A negative permutation entry marks a holder outside the exchange + // (non-owned, or owned but outside the overlap region); it has no slot. + if (owned[i] && permutation[i] >= 0) { for (LO c = 0; c < num_comp; ++c) { buffer[permutation[i] * num_comp + c] = data(i, c); } @@ -74,7 +76,10 @@ class XGCFieldSerializer : public FieldSerializer } if (rank_participates_) { for (LO i = 0; i < num_dof; ++i) { - if (owned[i]) { + // A negative permutation entry marks a holder outside the exchange + // (owned but outside the overlap region); no data was received for it, + // so it keeps its current value (pre-filled above). + if (owned[i] && permutation[i] >= 0) { for (LO c = 0; c < num_comp; ++c) { full_data[i * num_comp + c] = buffer[permutation[i] * num_comp + c]; } diff --git a/src/pcms/field/CMakeLists.txt b/src/pcms/field/CMakeLists.txt index 5d37a858..fc15c3a6 100644 --- a/src/pcms/field/CMakeLists.txt +++ b/src/pcms/field/CMakeLists.txt @@ -3,7 +3,9 @@ find_package(KokkosKernels REQUIRED) set(PCMS_FIELD_HEADERS field.h coordinate_system.h + element_dispatch.h evaluation_request.h + gid_permutation.hpp field_layout.h field_metadata.h layout/empty.h @@ -17,6 +19,8 @@ set(PCMS_FIELD_HEADERS data/point_cloud.h ) set(PCMS_FIELD_SOURCES + field_layout.cpp + gid_permutation.cpp function_space/polynomial_reconstruction.cpp layout/empty.cpp layout/point_cloud.cpp diff --git a/src/pcms/field/element_dispatch.h b/src/pcms/field/element_dispatch.h new file mode 100644 index 00000000..67ac1fad --- /dev/null +++ b/src/pcms/field/element_dispatch.h @@ -0,0 +1,37 @@ +#ifndef PCMS_FIELD_ELEMENT_DISPATCH_H +#define PCMS_FIELD_ELEMENT_DISPATCH_H + +#include "pcms/utility/assert.h" +#include +#include + +namespace pcms::detail +{ + +// Runtime -> compile-time bridge for finite-element order. +// +// Turns a runtime Lagrange order into a compile-time std::integral_constant and +// invokes the generic callable `f` with it, so the callee can instantiate +// order-templated kernels (shape functions, DOF-per-element counts) without +// scattering `if (order == ...)` branches through device code. This is the same +// dispatch shape as MakeMeshFieldBackend's switch, factored out so both the +// transfer integrators and the MeshField evaluator backend can share it. +// +// Supported orders: 0 (one DOF per element) and 1 (one DOF per vertex). Add a +// case here when a higher-order element (P2, ...) lands; every consumer picks +// it up at once. `f` must return the same type for every supported order. +template +auto DispatchByOrder(int order, F&& f) +{ + switch (order) { + case 0: return f(std::integral_constant{}); + case 1: return f(std::integral_constant{}); + default: + throw pcms_error("DispatchByOrder: unsupported element order " + + std::to_string(order)); + } +} + +} // namespace pcms::detail + +#endif // PCMS_FIELD_ELEMENT_DISPATCH_H diff --git a/src/pcms/field/eqdsk_field.h b/src/pcms/field/eqdsk_field.h index bb4188c8..05e8308d 100644 --- a/src/pcms/field/eqdsk_field.h +++ b/src/pcms/field/eqdsk_field.h @@ -12,7 +12,7 @@ namespace pcms struct EQDSKField { - SplineFunctionSpace space; + std::shared_ptr space; Field field; }; @@ -20,7 +20,7 @@ struct EQDSKField struct EQDSKFieldWithData { EQDSKData data; - SplineFunctionSpace space; + std::shared_ptr space; Field field; }; @@ -43,7 +43,7 @@ inline EQDSKField MakeEQDSKField(const EQDSKData& eqdsk_data) { auto space = SplineFunctionSpace::FromUniformGrid( eqdsk_data.grid, CoordinateSystem::Cartesian); - auto field = space.CreateField(); + Field field = space->CreateFunction(); field.GetData().SetDOFHolderData(Rank2View( eqdsk_data.PSIZR.data(), eqdsk_data.PSIZR.extent(0), 1)); return {std::move(space), std::move(field)}; diff --git a/src/pcms/field/field.h b/src/pcms/field/field.h index 433717d0..6da4261d 100644 --- a/src/pcms/field/field.h +++ b/src/pcms/field/field.h @@ -2,12 +2,13 @@ #define PCMS_COUPLING_FIELD_H #include "field_data.h" -#include "field_evaluator_factory.h" #include "field_layout.h" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" #include "pcms/utility/types.h" #include +#include +#include #include namespace pcms @@ -15,12 +16,13 @@ namespace pcms class FunctionSpace; -// Field is a composed per-field object: it owns coefficient data and holds -// a shared reference to the evaluator factory so the function space stays alive -// as long as the field does. +// Field is the (layout + data) bundle: it pairs coefficient data with the +// FieldLayout that gives the DOF-holder ordering its meaning. It is the +// currency of the communication path and of TransferOperator::Apply; it is NOT +// evaluatable on its own (evaluation semantics live on FunctionSpace). // -// Fields are typically created via LagrangeFunctionSpace::CreateField(). -// They are move-only (unique_ptr member). +// Fields are created via FunctionSpace::CreateField(). They are move-only +// (unique_ptr member). template class Field { @@ -29,12 +31,16 @@ class Field Field& operator=(Field&&) = default; Field(const Field&) = delete; Field& operator=(const Field&) = delete; + ~Field() = default; FieldData& GetData() noexcept { return *data_; } const FieldData& GetData() const noexcept { return *data_; } const FieldLayout& GetLayout() const { return *layout_; } + // Optional identifier for this field. Empty by default; set at creation. + [[nodiscard]] const std::string& GetName() const noexcept { return name_; } + Rank2View GetDOFHolderDataHost() const { return data_->GetDOFHolderDataHost(); @@ -55,32 +61,69 @@ class Field data_->SetDOFHolderData(v); } -private: - class CtorKey - { - CtorKey() = default; - friend class FunctionSpace; - }; - - Field(CtorKey, std::shared_ptr layout, - std::shared_ptr> evaluator_factory, +protected: + Field(std::string name, std::shared_ptr layout, std::unique_ptr> data) - : layout_(std::move(layout)), - evaluator_factory_(std::move(evaluator_factory)), - data_(std::move(data)) + : name_(std::move(name)), layout_(std::move(layout)), data_(std::move(data)) { } + // FieldFactory constructs Fields (via WrapField); FunctionSpace constructs + // Fields the same way and moves a Field's name, layout and data out to build + // a Function (via CreateFunction). Both stamp the field name. + friend class FieldFactory; friend class FunctionSpace; +private: + std::string name_; std::shared_ptr layout_; - std::shared_ptr> evaluator_factory_; std::unique_ptr> data_; }; +// Function is a Field that additionally knows its FunctionSpace, so it can +// be evaluated and used to build transfer operators. It is-a Field so the +// communication path (FieldCommunicator holds Field&) and +// TransferOperator::Apply(const Field&) bind a Function slice-free. It adds no +// virtuals: moving a Function into a Field slot (e.g. AddField(Field&&)) +// intentionally slices off the space reference, leaving a valid comm-only +// Field. +template +class Function : public Field +{ +public: + Function(Function&&) = default; + Function& operator=(Function&&) = default; + Function(const Function&) = delete; + Function& operator=(const Function&) = delete; + + const FunctionSpace& GetSpace() const noexcept { return *space_; } + std::shared_ptr GetSpacePtr() const noexcept + { + return space_; + } + +protected: + Function(std::string name, std::shared_ptr layout, + std::unique_ptr> data, + std::shared_ptr space) + : Field(std::move(name), std::move(layout), std::move(data)), + space_(std::move(space)) + { + } + + friend class FunctionSpace; + +private: + std::shared_ptr space_; +}; + using FieldVariant = std::variant, Field, Field, Field, Field>; +using FunctionVariant = + std::variant, Function, Function, + Function, Function>; + } // namespace pcms #endif // PCMS_COUPLING_FIELD_H diff --git a/src/pcms/field/field_factory.h b/src/pcms/field/field_factory.h new file mode 100644 index 00000000..7903c72e --- /dev/null +++ b/src/pcms/field/field_factory.h @@ -0,0 +1,102 @@ +#ifndef PCMS_FIELD_FACTORY_H +#define PCMS_FIELD_FACTORY_H + +#include "field.h" +#include "field_data.h" +#include "field_layout.h" +#include "field_metadata.h" +#include "pcms/utility/types.h" +#include +#include + +namespace pcms +{ + +namespace detail +{ + +inline size_t ExpectedFlatFieldDataSize(const FieldLayout& layout) +{ + return static_cast(layout.GetNumOwnedDofHolder()) * + static_cast(layout.GetNumComponents()); +} + +} // namespace detail + +// Compile-time gate: true only for the five supported field value types. +template +inline constexpr bool is_supported_field_type_v = + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v; + +// FieldFactory constructs Field bundles ({layout, data}) with no evaluation +// capability and no shared-ownership requirement. Comm-only backends (e.g. +// XGCFieldFactory) derive it directly. FunctionSpace is a separate abstraction +// (not a FieldFactory) that additionally supports evaluation and produces +// Functions; a FieldFactory can never be passed where a FunctionSpace is +// required, which is the compile-time form of "comm-only cannot be evaluated". +class FieldFactory +{ +public: + virtual std::shared_ptr GetLayout() const noexcept = 0; + + virtual ~FieldFactory() noexcept = default; + + // Create a new named field with freshly allocated data. The name is optional + // (empty by default) and identifies the field to consumers such as a coupler. + template + [[nodiscard]] Field CreateField(std::string name = "", + FieldMetadata metadata = {}) const; + + // Expert API: wrap externally constructed field data into a Field. The + // concrete factory validates backend-specific field-data type and storage + // size compatibility. + template + [[nodiscard]] Field CreateField(std::string name, + std::unique_ptr> data) const; + +protected: + template + static Field WrapField(std::shared_ptr layout, + std::unique_ptr> data) + { + return Field(std::string{}, std::move(layout), std::move(data)); + } + + virtual FieldVariant CreateFieldImpl(Type value_type, + FieldMetadata metadata) const = 0; + + virtual FieldVariant CreateFieldImpl(FieldDataVariant data) const = 0; +}; + +template +Field FieldFactory::CreateField(std::string name, + FieldMetadata metadata) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + Field field = + std::get>(CreateFieldImpl(TypeEnumFromType(), metadata)); + field.name_ = std::move(name); + return field; +} + +template +Field FieldFactory::CreateField(std::string name, + std::unique_ptr> data) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + if (!data) { + throw pcms_error("FieldFactory::CreateField: data must not be null"); + } + Field field = + std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); + field.name_ = std::move(name); + return field; +} + +} // namespace pcms + +#endif // PCMS_FIELD_FACTORY_H diff --git a/src/pcms/field/field_layout.cpp b/src/pcms/field/field_layout.cpp new file mode 100644 index 00000000..6e785f9e --- /dev/null +++ b/src/pcms/field/field_layout.cpp @@ -0,0 +1,38 @@ +#include "pcms/field/field_layout.h" +#include "pcms/field/gid_permutation.hpp" +#include "pcms/utility/assert.h" + +namespace pcms +{ + +void FieldLayout::BuildGlobalToLocalPermutation() +{ + const auto permutation = + BuildSortedGidPermutation(GetGidsHost(), GetEntOffsets()); + global_to_local_host_ = Kokkos::View( + "global_to_local_host", permutation.size()); + for (size_t i = 0; i < permutation.size(); ++i) + global_to_local_host_(i) = permutation[i]; + + global_to_local_ = + Kokkos::View("global_to_local", permutation.size()); + Kokkos::deep_copy(global_to_local_, global_to_local_host_); +} + +Kokkos::View +FieldLayout::GetGlobalToLocalPermutationHost() const +{ + // A non-empty view means BuildGlobalToLocalPermutation() ran in the derived + // layout's constructor; an empty one means the layout never built it. + PCMS_ALWAYS_ASSERT(global_to_local_host_.size() > 0); + return global_to_local_host_; +} + +Kokkos::View +FieldLayout::GetGlobalToLocalPermutation() const +{ + PCMS_ALWAYS_ASSERT(global_to_local_.size() > 0); + return global_to_local_; +} + +} // namespace pcms diff --git a/src/pcms/field/field_layout.h b/src/pcms/field/field_layout.h index 03aef658..b4551e4c 100644 --- a/src/pcms/field/field_layout.h +++ b/src/pcms/field/field_layout.h @@ -2,6 +2,7 @@ #define PCMS_FIELD_LAYOUT_H #include #include +#include #include #include "pcms/discretization/discretization.h" #include "pcms/utility/types.h" @@ -19,6 +20,12 @@ using ReversePartitionMap = std::map>; class FieldLayout { public: + // Optional identifier for this layout. Set once at construction (via a From* + // factory) and empty by default. Consumers that need to identify a layout by + // a stable name use it; it carries no meaning within the field layer itself. + [[nodiscard]] const std::string& GetName() const noexcept { return name_; } + void SetName(std::string name) { name_ = std::move(name); } + virtual std::shared_ptr GetDiscretization() const noexcept = 0; @@ -40,14 +47,20 @@ class FieldLayout virtual Rank1View GetOwnedHost() const = 0; virtual GlobalIDView GetGidsHost() const = 0; + // Maps each local DOF holder to its contiguous active index, ordered by GID + // within each entity block. Components are not included in the permutation. + // For local GIDs [102, 7, 41, 19], the permutation is [3, 0, 2, 1]. Thus + // holder i, component c is read from values(permutation(i), c), or from + // flat_values[permutation(i) * num_components + c]. + Kokkos::View GetGlobalToLocalPermutationHost() + const; + Kokkos::View GetGlobalToLocalPermutation() + const; + // returns true if the field layout is distributed // if the field layout is distributed, the owned and global dofs are the same [[nodiscard]] virtual bool IsDistributed() const = 0; - // This class should construct the permutation arrays that are needed - // for serialization / deserialization - // - virtual EntOffsetsArray GetEntOffsets() const = 0; virtual CoordinateView GetDOFHolderCoordinates() const = 0; @@ -61,6 +74,19 @@ class FieldLayout GetDOFHolderClassificationIdsHost() const = 0; virtual ~FieldLayout() noexcept = default; + +protected: + // Builds the global-to-local permutation from GetGidsHost()/GetEntOffsets(). + // Derived layouts that support GetGlobalToLocalPermutation() must call this + // from their constructor, once their GIDs and entity offsets are available; + // building eagerly keeps first access free of the data race that lazy + // construction of the mutable cache would introduce. + void BuildGlobalToLocalPermutation(); + +private: + std::string name_; + Kokkos::View global_to_local_host_; + Kokkos::View global_to_local_; }; } // namespace pcms diff --git a/src/pcms/field/function_space.h b/src/pcms/field/function_space.h index a5e6f867..26ef4fad 100644 --- a/src/pcms/field/function_space.h +++ b/src/pcms/field/function_space.h @@ -5,7 +5,7 @@ #include "evaluation_request.h" #include "field.h" #include "field_data.h" -#include "field_evaluator_factory.h" +#include "field_factory.h" #include "field_layout.h" #include "field_metadata.h" #include "out_of_bounds_policy.h" @@ -20,33 +20,18 @@ namespace pcms { -namespace detail -{ - -inline size_t ExpectedFlatFieldDataSize(const FieldLayout& layout) -{ - return static_cast(layout.GetNumOwnedDofHolder()) * - static_cast(layout.GetNumComponents()); -} - -} // namespace detail - -// Compile-time gate: true only for the five supported field value types. -template -inline constexpr bool is_supported_field_type_v = - std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v || - std::is_same_v; - // FunctionSpace is an abstract interface representing an evaluatable field -// space: layout, evaluation rules, and coordinate interpretation. +// space: layout, evaluation rules, and coordinate interpretation. It produces +// Functions (a Field bound to this space). // // Concrete implementations (e.g. LagrangeFunctionSpace) provide backends for -// specific discretizations or mesh types. -// -// FunctionSpace is used as the parameter type for operation objects such as -// Interpolator, so that operations are not coupled to a specific backend. -class FunctionSpace +// specific discretizations or mesh types, and their From* factories return a +// shared_ptr. A Function co-owns its space via shared_ptr (obtained through +// enable_shared_from_this), which is why concrete spaces are always +// shared-owned; a stack-allocated FunctionSpace is not constructible (the +// backend constructors are private and reachable only via the shared_ptr +// From* factories). +class FunctionSpace : public std::enable_shared_from_this { public: virtual std::shared_ptr GetDiscretization() @@ -61,17 +46,16 @@ class FunctionSpace virtual ~FunctionSpace() noexcept = default; - // Create a new field with freshly allocated data for this function space. - // Compile-time error for unsupported T; runtime error for T unsupported by - // the concrete backend. template - [[nodiscard]] Field CreateField(FieldMetadata metadata = {}) const; + [[nodiscard]] Function CreateFunction(std::string name = "", + FieldMetadata metadata = {}) const; - // Expert API: wrap externally constructed field data into a Field for this - // function space. The concrete function space validates backend-specific - // field-data type and storage size compatibility. + // Expert API: wrap externally constructed field data into a Function for this + // space. The concrete space validates backend-specific field-data type and + // storage size compatibility. template - [[nodiscard]] Field CreateField(std::unique_ptr> data) const; + [[nodiscard]] Function CreateFunction( + std::string name, std::unique_ptr> data) const; // Create a point evaluator for the given evaluation request. // Compile-time error for unsupported T; runtime error for T or capability @@ -89,12 +73,19 @@ class FunctionSpace protected: template static Field WrapField(std::shared_ptr layout, - std::unique_ptr> data, - std::shared_ptr> - evaluator_factory = nullptr) + std::unique_ptr> data) + { + return Field(std::string{}, std::move(layout), std::move(data)); + } + + template + static Function WrapFunction(std::string name, + std::shared_ptr layout, + std::unique_ptr> data, + std::shared_ptr space) { - return Field(typename Field::CtorKey{}, std::move(layout), - std::move(evaluator_factory), std::move(data)); + return Function(std::move(name), std::move(layout), std::move(data), + std::move(space)); } virtual FieldVariant CreateFieldImpl(Type value_type, @@ -107,22 +98,30 @@ class FunctionSpace }; template -Field FunctionSpace::CreateField(FieldMetadata metadata) const +Function FunctionSpace::CreateFunction(std::string name, + FieldMetadata metadata) const { static_assert(is_supported_field_type_v, "T is not a supported field type"); - return std::get>(CreateFieldImpl(TypeEnumFromType(), metadata)); + Field field = + std::get>(CreateFieldImpl(TypeEnumFromType(), metadata)); + return WrapFunction(std::move(name), std::move(field.layout_), + std::move(field.data_), shared_from_this()); } template -Field FunctionSpace::CreateField(std::unique_ptr> data) const +Function FunctionSpace::CreateFunction( + std::string name, std::unique_ptr> data) const { static_assert(is_supported_field_type_v, "T is not a supported field type"); if (!data) { - throw pcms_error("FunctionSpace::CreateField: data must not be null"); + throw pcms_error("FunctionSpace::CreateFunction: data must not be null"); } - return std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); + Field field = + std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); + return WrapFunction(std::move(name), std::move(field.layout_), + std::move(field.data_), shared_from_this()); } template diff --git a/src/pcms/field/function_space/lagrange.cpp b/src/pcms/field/function_space/lagrange.cpp index 8328e266..8d15a6ec 100644 --- a/src/pcms/field/function_space/lagrange.cpp +++ b/src/pcms/field/function_space/lagrange.cpp @@ -54,7 +54,7 @@ void ValidateLagrangeWrappedFieldData(const FieldLayout& layout, } // namespace LagrangeFunctionSpace::LagrangeFunctionSpace( - std::shared_ptr layout, + Key, std::shared_ptr layout, std::function create_field_data_fn, std::shared_ptr> evaluator_factory) noexcept : layout_(std::move(layout)), @@ -63,10 +63,10 @@ LagrangeFunctionSpace::LagrangeFunctionSpace( { } -LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( +std::shared_ptr LagrangeFunctionSpace::FromMesh( Omega_h::Mesh& mesh, int order, int num_components, CoordinateSystem coordinate_system, std::string global_id_name, - Backend backend) + Backend backend, std::string layout_name) { // https://github.com/SCOREC/meshFields/issues/88 if (backend == Backend::MeshFields && order == 0) { @@ -89,10 +89,11 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( auto mesh_layout = std::make_shared( mesh, nodes_per_dim, num_components, coordinate_system, std::move(global_id_name)); + mesh_layout->SetName(layout_name); auto eval_factory = std::make_shared>(mesh_layout); - return LagrangeFunctionSpace( - mesh_layout, + return std::make_shared( + Key{}, mesh_layout, [mesh_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { if (t == Type::Float) { if constexpr (std::is_same_v || @@ -126,10 +127,11 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( } auto layout = std::make_shared( mesh, order, num_components, coordinate_system, std::move(global_id_name)); + layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(layout); - return LagrangeFunctionSpace( - layout, + return std::make_shared( + Key{}, layout, [layout](Type t, FieldMetadata metadata) -> FieldDataVariant { return apply_to_type(t, [&](auto tag) -> FieldDataVariant { using T = typename decltype(tag)::type; @@ -143,10 +145,10 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( std::move(eval_factory)); } -LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( +std::shared_ptr LagrangeFunctionSpace::FromMesh( Omega_h::Mesh& mesh, int order, int num_components, CoordinateSystem coordinate_system, Omega_h::Read owned_mask, - std::string global_id_name, Backend backend) + std::string global_id_name, Backend backend, std::string layout_name) { if (backend == Backend::MeshFields) { throw pcms_error( @@ -161,10 +163,11 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( auto layout = std::make_shared( mesh, order, num_components, coordinate_system, std::move(owned_mask), std::move(global_id_name)); + layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(layout); - return LagrangeFunctionSpace( - layout, + return std::make_shared( + Key{}, layout, [layout](Type t, FieldMetadata metadata) -> FieldDataVariant { return apply_to_type(t, [&](auto tag) -> FieldDataVariant { using T = typename decltype(tag)::type; @@ -178,9 +181,9 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( std::move(eval_factory)); } -LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( +std::shared_ptr LagrangeFunctionSpace::FromUniformGrid( const UniformGrid<2>& grid, int num_components, - CoordinateSystem coordinate_system, int order) + CoordinateSystem coordinate_system, int order, std::string layout_name) { if (order != 0 && order != 1) { throw std::invalid_argument("LagrangeFunctionSpace::FromUniformGrid: only " @@ -188,10 +191,11 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( } auto ug_layout = std::make_shared>( grid, num_components, coordinate_system, order); + ug_layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(ug_layout); - return LagrangeFunctionSpace( - ug_layout, + return std::make_shared( + Key{}, ug_layout, [ug_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { return apply_to_type(t, [&](auto tag) -> FieldDataVariant { using T = typename decltype(tag)::type; @@ -205,9 +209,9 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( std::move(eval_factory)); } -LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( +std::shared_ptr LagrangeFunctionSpace::FromUniformGrid( const UniformGrid<3>& grid, int num_components, - CoordinateSystem coordinate_system, int order) + CoordinateSystem coordinate_system, int order, std::string layout_name) { if (order != 0 && order != 1) { throw std::invalid_argument("LagrangeFunctionSpace::FromUniformGrid: only " @@ -215,10 +219,11 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( } auto ug_layout = std::make_shared>( grid, num_components, coordinate_system, order); + ug_layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(ug_layout); - return LagrangeFunctionSpace( - ug_layout, + return std::make_shared( + Key{}, ug_layout, [ug_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { return apply_to_type(t, [&](auto tag) -> FieldDataVariant { using T = typename decltype(tag)::type; @@ -251,8 +256,7 @@ FieldVariant LagrangeFunctionSpace::CreateFieldImpl( [this](auto&& fd) -> FieldVariant { using FD = std::decay_t; using T = typename FD::element_type::value_type; - return WrapField(layout_, std::forward(fd), - evaluator_factory_); + return WrapField(layout_, std::forward(fd)); }, std::move(field_data)); } @@ -269,8 +273,7 @@ FieldVariant LagrangeFunctionSpace::CreateFieldImpl(FieldDataVariant data) const "LagrangeFunctionSpace: int8_t is not a supported field type"); } else { ValidateLagrangeWrappedFieldData(*layout_, *fd); - return WrapField(layout_, std::forward(fd), - evaluator_factory_); + return WrapField(layout_, std::forward(fd)); } }, std::move(data)); diff --git a/src/pcms/field/function_space/lagrange.h b/src/pcms/field/function_space/lagrange.h index 54b2f695..d729d628 100644 --- a/src/pcms/field/function_space/lagrange.h +++ b/src/pcms/field/function_space/lagrange.h @@ -27,7 +27,19 @@ namespace pcms class LagrangeFunctionSpace : public FunctionSpace { + // Passkey: only members (the From* factories) can create a Key, so only they + // can construct a space, while std::make_shared can still call the ctor. + struct Key + { + explicit Key() = default; + }; + public: + LagrangeFunctionSpace( + Key, std::shared_ptr layout, + std::function create_field_data_fn, + std::shared_ptr> evaluator_factory) noexcept; + enum class Backend { MeshFields, @@ -41,25 +53,28 @@ class LagrangeFunctionSpace : public FunctionSpace #endif // Unstructured mesh — dispatches to MeshFields or native Omega_h backend - [[nodiscard]] static LagrangeFunctionSpace FromMesh( + [[nodiscard]] static std::shared_ptr FromMesh( Omega_h::Mesh& mesh, int order, int num_components, CoordinateSystem coordinate_system, std::string global_id_name = "global", - Backend backend = DefaultBackend); + Backend backend = DefaultBackend, std::string layout_name = ""); - [[nodiscard]] static LagrangeFunctionSpace FromMesh( + [[nodiscard]] static std::shared_ptr FromMesh( Omega_h::Mesh& mesh, int order, int num_components, CoordinateSystem coordinate_system, Omega_h::Read owned_mask, - std::string global_id_name = "global", Backend backend = DefaultBackend); + std::string global_id_name = "global", Backend backend = DefaultBackend, + std::string layout_name = ""); // Structured uniform grid — order-1 H1-conforming nodal field on a regular // grid - [[nodiscard]] static LagrangeFunctionSpace FromUniformGrid( + [[nodiscard]] static std::shared_ptr FromUniformGrid( const UniformGrid<2>& grid, int num_components, - CoordinateSystem coordinate_system, int order = 1); + CoordinateSystem coordinate_system, int order = 1, + std::string layout_name = ""); - [[nodiscard]] static LagrangeFunctionSpace FromUniformGrid( + [[nodiscard]] static std::shared_ptr FromUniformGrid( const UniformGrid<3>& grid, int num_components, - CoordinateSystem coordinate_system, int order = 1); + CoordinateSystem coordinate_system, int order = 1, + std::string layout_name = ""); [[nodiscard]] std::shared_ptr GetLayout() const noexcept override; @@ -77,11 +92,6 @@ class LagrangeFunctionSpace : public FunctionSpace Type value_type, const EvaluationRequest& request) const override; private: - explicit LagrangeFunctionSpace( - std::shared_ptr layout, - std::function create_field_data_fn, - std::shared_ptr> evaluator_factory) noexcept; - std::shared_ptr layout_; std::function create_field_data_fn_; std::shared_ptr> evaluator_factory_; diff --git a/src/pcms/field/function_space/polynomial_reconstruction.cpp b/src/pcms/field/function_space/polynomial_reconstruction.cpp index 060b9c24..397e5f7c 100644 --- a/src/pcms/field/function_space/polynomial_reconstruction.cpp +++ b/src/pcms/field/function_space/polynomial_reconstruction.cpp @@ -17,13 +17,13 @@ namespace pcms { PolynomialReconstructionFunctionSpace::PolynomialReconstructionFunctionSpace( - std::shared_ptr layout, + Key, std::shared_ptr layout, std::shared_ptr> evaluator_factory) noexcept : layout_(std::move(layout)), evaluator_factory_(std::move(evaluator_factory)) { } -PolynomialReconstructionFunctionSpace +std::shared_ptr PolynomialReconstructionFunctionSpace::Create( Rank2View coords, CoordinateSystem coordinate_system, MLSOptions options) @@ -40,11 +40,11 @@ PolynomialReconstructionFunctionSpace::Create( std::make_shared(pc_layout, options); auto eval_factory = std::make_shared( pc_layout, localization, options); - return PolynomialReconstructionFunctionSpace(pc_layout, - std::move(eval_factory)); + return std::make_shared( + Key{}, pc_layout, std::move(eval_factory)); } -PolynomialReconstructionFunctionSpace +std::shared_ptr PolynomialReconstructionFunctionSpace::FromMesh( Omega_h::Mesh& mesh, int source_entity_dim, CoordinateSystem coordinate_system, MLSOptions options) @@ -66,8 +66,8 @@ PolynomialReconstructionFunctionSpace::FromMesh( mesh, source_entity_dim, options); auto eval_factory = std::make_shared( mesh_layout, localization, options); - return PolynomialReconstructionFunctionSpace(mesh_layout, - std::move(eval_factory)); + return std::make_shared( + Key{}, mesh_layout, std::move(eval_factory)); } std::shared_ptr @@ -93,8 +93,7 @@ FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( "supported"); } else { return WrapField( - layout_, std::make_unique>(layout_, metadata), - evaluator_factory_); + layout_, std::make_unique>(layout_, metadata)); } }); } @@ -120,7 +119,7 @@ FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( "PolynomialReconstructionFunctionSpace::CreateField: field data size " "does not match layout"); } - return WrapField(layout_, std::move(fd), evaluator_factory_); + return WrapField(layout_, std::move(fd)); } PointEvaluatorVariant diff --git a/src/pcms/field/function_space/polynomial_reconstruction.hpp b/src/pcms/field/function_space/polynomial_reconstruction.hpp index 0222c282..890f61a7 100644 --- a/src/pcms/field/function_space/polynomial_reconstruction.hpp +++ b/src/pcms/field/function_space/polynomial_reconstruction.hpp @@ -23,14 +23,25 @@ class FieldEvaluatorFactory; class PolynomialReconstructionFunctionSpace : public FunctionSpace { + // Passkey: only members (the Create/FromMesh factories) can create a Key, so + // only they can construct a space, while std::make_shared can call the ctor. + struct Key + { + explicit Key() = default; + }; + public: - [[nodiscard]] static PolynomialReconstructionFunctionSpace Create( - Rank2View coords, CoordinateSystem coordinate_system, - MLSOptions options = {}); + PolynomialReconstructionFunctionSpace( + Key, std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept; + + [[nodiscard]] static std::shared_ptr + Create(Rank2View coords, + CoordinateSystem coordinate_system, MLSOptions options = {}); - [[nodiscard]] static PolynomialReconstructionFunctionSpace FromMesh( - Omega_h::Mesh& mesh, int source_entity_dim, - CoordinateSystem coordinate_system, MLSOptions options = {}); + [[nodiscard]] static std::shared_ptr + FromMesh(Omega_h::Mesh& mesh, int source_entity_dim, + CoordinateSystem coordinate_system, MLSOptions options = {}); [[nodiscard]] std::shared_ptr GetLayout() const noexcept override; @@ -48,10 +59,6 @@ class PolynomialReconstructionFunctionSpace : public FunctionSpace Type value_type, const EvaluationRequest& request) const override; private: - explicit PolynomialReconstructionFunctionSpace( - std::shared_ptr layout, - std::shared_ptr> evaluator_factory) noexcept; - std::shared_ptr layout_; std::shared_ptr> evaluator_factory_; }; diff --git a/src/pcms/field/function_space/spline.cpp b/src/pcms/field/function_space/spline.cpp index f4530fc8..c746a8d2 100644 --- a/src/pcms/field/function_space/spline.cpp +++ b/src/pcms/field/function_space/spline.cpp @@ -11,20 +11,21 @@ namespace pcms { SplineFunctionSpace::SplineFunctionSpace( - std::shared_ptr layout, + Key, std::shared_ptr layout, std::shared_ptr> evaluator_factory) noexcept : layout_(std::move(layout)), evaluator_factory_(std::move(evaluator_factory)) { } -SplineFunctionSpace SplineFunctionSpace::FromUniformGrid( +std::shared_ptr SplineFunctionSpace::FromUniformGrid( const UniformGrid<2>& grid, CoordinateSystem coordinate_system) { auto layout = std::make_shared>(grid, 1, coordinate_system, 1); auto evaluator_factory = std::make_shared(layout); - return SplineFunctionSpace(layout, std::move(evaluator_factory)); + return std::make_shared(Key{}, layout, + std::move(evaluator_factory)); } std::shared_ptr SplineFunctionSpace::GetLayout() @@ -47,8 +48,7 @@ FieldVariant SplineFunctionSpace::CreateFieldImpl(Type value_type, throw pcms_error("SplineFunctionSpace: only double (Real) is supported"); } else { return WrapField( - layout_, std::make_unique>(layout_, metadata), - evaluator_factory_); + layout_, std::make_unique>(layout_, metadata)); } }); } @@ -70,7 +70,7 @@ FieldVariant SplineFunctionSpace::CreateFieldImpl(FieldDataVariant data) const "SplineFunctionSpace::CreateField: field data size does not match " "layout"); } - return WrapField(layout_, std::move(fd), evaluator_factory_); + return WrapField(layout_, std::move(fd)); } PointEvaluatorVariant SplineFunctionSpace::CreatePointEvaluatorImpl( diff --git a/src/pcms/field/function_space/spline.h b/src/pcms/field/function_space/spline.h index 15c7d712..f8e467d7 100644 --- a/src/pcms/field/function_space/spline.h +++ b/src/pcms/field/function_space/spline.h @@ -21,8 +21,19 @@ namespace pcms class SplineFunctionSpace : public FunctionSpace { + // Passkey: only members (the From* factories) can create a Key, so only they + // can construct a space, while std::make_shared can still call the ctor. + struct Key + { + explicit Key() = default; + }; + public: - [[nodiscard]] static SplineFunctionSpace FromUniformGrid( + SplineFunctionSpace( + Key, std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept; + + [[nodiscard]] static std::shared_ptr FromUniformGrid( const UniformGrid<2>& grid, CoordinateSystem coordinate_system); [[nodiscard]] std::shared_ptr GetLayout() @@ -41,10 +52,6 @@ class SplineFunctionSpace : public FunctionSpace Type value_type, const EvaluationRequest& request) const override; private: - explicit SplineFunctionSpace( - std::shared_ptr layout, - std::shared_ptr> evaluator_factory) noexcept; - std::shared_ptr layout_; std::shared_ptr> evaluator_factory_; }; diff --git a/src/pcms/field/function_space/xgc.h b/src/pcms/field/function_space/xgc.h index 164abc7a..cbea70b4 100644 --- a/src/pcms/field/function_space/xgc.h +++ b/src/pcms/field/function_space/xgc.h @@ -1,9 +1,9 @@ -#ifndef PCMS_XGC_FUNCTION_SPACE_H -#define PCMS_XGC_FUNCTION_SPACE_H +#ifndef PCMS_XGC_FIELD_FACTORY_H +#define PCMS_XGC_FIELD_FACTORY_H #include "pcms/field/data/xgc.h" #include "pcms/field/field.h" -#include "pcms/field/function_space.h" +#include "pcms/field/field_factory.h" #include "pcms/field/field_metadata.h" #include "pcms/utility/common.h" #include @@ -12,15 +12,22 @@ namespace pcms { -class XGCFunctionSpace : public FunctionSpace +// XGC is a comm-only backend +class XGCFieldFactory : public FieldFactory { public: - XGCFunctionSpace(const ReverseClassificationVertex& reverse_classification, - std::function in_overlap, - LO num_plane_nodes) + XGCFieldFactory(const ReverseClassificationVertex& reverse_classification, + std::function in_overlap, + LO num_plane_nodes, std::string layout_name = "") : layout_(std::make_shared( reverse_classification, std::move(in_overlap), num_plane_nodes)) { + layout_->SetName(std::move(layout_name)); + } + + void SetLayoutName(std::string name) const + { + layout_->SetName(std::move(name)); } [[nodiscard]] std::shared_ptr GetLayout() @@ -29,9 +36,10 @@ class XGCFunctionSpace : public FunctionSpace return layout_; } - [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override + [[nodiscard]] std::shared_ptr GetXGCLayout() + const noexcept { - return CoordinateSystem::XGC; + return layout_; } protected: @@ -55,12 +63,12 @@ class XGCFunctionSpace : public FunctionSpace PCMS_ALWAYS_ASSERT(fd != nullptr); if (dynamic_cast*>(fd.get()) == nullptr) { throw pcms_error( - "XGCFunctionSpace::CreateField: requires XGCFieldData"); + "XGCFieldFactory::CreateField: requires XGCFieldData"); } if (fd->GetDOFHolderDataHost().size() != static_cast(layout_->GetFullDataSize())) { throw pcms_error( - "XGCFunctionSpace::CreateField: field data size does not match " + "XGCFieldFactory::CreateField: field data size does not match " "layout"); } return WrapField(layout_, std::move(fd)); @@ -68,23 +76,10 @@ class XGCFunctionSpace : public FunctionSpace std::move(data)); } - [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( - Type /*value_type*/, const EvaluationRequest& /*request*/) const override - { - throw pcms_error("XGCFunctionSpace does not support point evaluation"); - } - -public: - [[nodiscard]] std::shared_ptr GetXGCLayout() - const noexcept - { - return layout_; - } - private: - std::shared_ptr layout_; + std::shared_ptr layout_; }; } // namespace pcms -#endif // PCMS_XGC_FUNCTION_SPACE_H +#endif // PCMS_XGC_FIELD_FACTORY_H diff --git a/src/pcms/field/gid_permutation.cpp b/src/pcms/field/gid_permutation.cpp new file mode 100644 index 00000000..475216f8 --- /dev/null +++ b/src/pcms/field/gid_permutation.cpp @@ -0,0 +1,70 @@ +#include "pcms/field/gid_permutation.hpp" +#include "pcms/utility/assert.h" +#include + +namespace pcms +{ + +namespace +{ + +// Sentinel permutation entry for a local holder that does not participate in an +// exchange (a non-owned/ghost holder, or an owned holder outside the coupled +// overlap region whose GID is never transmitted). It is deliberately negative +// so serialization/deserialization can skip it, and any accidental use as a +// buffer index is an obvious out-of-bounds access rather than a silent read or +// write of holder 0. +constexpr LO UnmappedPermutationIndex = -1; + +} // namespace + +std::vector MakeUnmappedPermutation(std::size_t size) +{ + return std::vector(size, UnmappedPermutationIndex); +} + +void AppendGidPermutation(GlobalIDView local_gids, + const EntOffsetsArray& ent_offsets, + const GidToIndexMaps& gid_to_index, + std::vector& permutation, bool allow_missing) +{ + for (size_t e = 0; e < ent_offsets.size() - 1; ++e) { + for (size_t i = ent_offsets[e]; i < ent_offsets[e + 1]; ++i) { + const auto entry = gid_to_index[e].find(local_gids(i)); + if (entry != gid_to_index[e].end()) { + permutation.push_back(entry->second); + continue; + } + PCMS_ALWAYS_ASSERT(allow_missing); + permutation.push_back(UnmappedPermutationIndex); + } + } +} + +std::vector BuildSortedGidPermutation(GlobalIDView gids, + const EntOffsetsArray& ent_offsets) +{ + GidToIndexMaps gid_to_index; + LO next_index = 0; + for (size_t e = 0; e < ent_offsets.size() - 1; ++e) { + std::vector sorted_gids; + sorted_gids.reserve(ent_offsets[e + 1] - ent_offsets[e]); + for (size_t i = ent_offsets[e]; i < ent_offsets[e + 1]; ++i) { + sorted_gids.push_back(gids(i)); + } + std::sort(sorted_gids.begin(), sorted_gids.end()); + PCMS_ALWAYS_ASSERT( + std::adjacent_find(sorted_gids.begin(), sorted_gids.end()) == + sorted_gids.end()); + for (const GO gid : sorted_gids) { + gid_to_index[e].emplace(gid, next_index++); + } + } + std::vector permutation; + permutation.reserve(gids.size()); + AppendGidPermutation(gids, ent_offsets, gid_to_index, permutation, + /*allow_missing=*/false); + return permutation; +} + +} // namespace pcms diff --git a/src/pcms/field/gid_permutation.hpp b/src/pcms/field/gid_permutation.hpp new file mode 100644 index 00000000..0118fd31 --- /dev/null +++ b/src/pcms/field/gid_permutation.hpp @@ -0,0 +1,45 @@ +#ifndef PCMS_FIELD_GID_PERMUTATION_HPP +#define PCMS_FIELD_GID_PERMUTATION_HPP + +#include "pcms/field/field_layout.h" +#include +#include +#include +#include + +namespace pcms +{ + +// Maps a GID to its position in an exchange buffer, grouped by entity-dimension +// block: index e holds the GIDs classified to mesh entity dimension e. +using GidToIndexMaps = std::array, ent_offsets_len - 1>; + +// Returns a permutation of `size` entries with every local holder initially +// unmapped. Unmapped entries hold a negative sentinel so that serialization +// skips them and any accidental use as a buffer index is an obvious +// out-of-bounds access rather than a silent read or write of holder 0. Callers +// overwrite the entries for holders that participate in the exchange. +std::vector MakeUnmappedPermutation(std::size_t size); + +// Appends, in entity-offset (local holder) order, the position each local GID +// maps to according to gid_to_index. When allow_missing is true, a GID absent +// from gid_to_index is left unmapped (negative sentinel): a local layout may +// contain ghost holders and holders outside the overlap region, which are never +// exchanged and which (de)serialization skips. When allow_missing is false -- +// e.g. the sorted permutation below, whose maps are built from these very GIDs +// -- a missing GID is a hard error. +void AppendGidPermutation(GlobalIDView local_gids, + const EntOffsetsArray& ent_offsets, + const GidToIndexMaps& gid_to_index, + std::vector& permutation, + bool allow_missing = false); + +// Builds a permutation that maps each local holder to a contiguous index +// ordered by GID within each entity block. Every GID must be unique within its +// block. +std::vector BuildSortedGidPermutation(GlobalIDView gids, + const EntOffsetsArray& ent_offsets); + +} // namespace pcms + +#endif // PCMS_FIELD_GID_PERMUTATION_HPP diff --git a/src/pcms/field/layout/omega_h_lagrange.cpp b/src/pcms/field/layout/omega_h_lagrange.cpp index a2749a6e..a325140f 100644 --- a/src/pcms/field/layout/omega_h_lagrange.cpp +++ b/src/pcms/field/layout/omega_h_lagrange.cpp @@ -146,6 +146,8 @@ OmegaHLagrangeLayout::OmegaHLagrangeLayout(Omega_h::Mesh& mesh, int order, classification_ids_host_(i) = static_cast(class_ids_host[i]); } discretization_ = std::make_shared(mesh_); + + BuildGlobalToLocalPermutation(); } OmegaHLagrangeLayout::OmegaHLagrangeLayout( @@ -189,6 +191,8 @@ OmegaHLagrangeLayout::OmegaHLagrangeLayout( classification_ids_host_(i) = static_cast(class_ids_host[i]); } discretization_ = std::make_shared(mesh_); + + BuildGlobalToLocalPermutation(); } std::shared_ptr OmegaHLagrangeLayout::GetDiscretization() @@ -223,6 +227,11 @@ GlobalIDView OmegaHLagrangeLayout::GetGidsHost() const return GlobalIDView(gids_host_.data(), gids_host_.size()); } +GlobalIDView OmegaHLagrangeLayout::GetGids() const +{ + return GlobalIDView(gids_.data(), gids_.size()); +} + CoordinateView OmegaHLagrangeLayout::GetDOFHolderCoordinates() const { diff --git a/src/pcms/field/layout/omega_h_lagrange.h b/src/pcms/field/layout/omega_h_lagrange.h index f750b47a..68154979 100644 --- a/src/pcms/field/layout/omega_h_lagrange.h +++ b/src/pcms/field/layout/omega_h_lagrange.h @@ -35,6 +35,7 @@ class OmegaHLagrangeLayout : public FieldLayout Rank1View GetOwnedHost() const override; GlobalIDView GetGidsHost() const override; + GlobalIDView GetGids() const; CoordinateView GetDOFHolderCoordinates() const override; [[nodiscard]] bool IsDistributed() const override; diff --git a/src/pcms/field/uniform_grid_binary_field.h b/src/pcms/field/uniform_grid_binary_field.h index 6a71de7a..bce42439 100644 --- a/src/pcms/field/uniform_grid_binary_field.h +++ b/src/pcms/field/uniform_grid_binary_field.h @@ -39,9 +39,9 @@ CreateUniformGridBinaryField(Omega_h::Mesh& mesh, const UniformGrid& grid) auto function_space = LagrangeFunctionSpace::FromUniformGrid( grid, 1, CoordinateSystem::Cartesian); auto layout = std::dynamic_pointer_cast>( - function_space.GetLayout()); + function_space->GetLayout()); PCMS_ALWAYS_ASSERT(layout != nullptr); - auto field = function_space.template CreateField(FieldMetadata{}); + Field field = function_space->template CreateFunction(""); auto coord_view = layout->GetDOFHolderCoordinates(); auto coords = coord_view.GetValues(); diff --git a/src/pcms/localization/adj_search.cpp b/src/pcms/localization/adj_search.cpp index 51a48838..b15925d7 100644 --- a/src/pcms/localization/adj_search.cpp +++ b/src/pcms/localization/adj_search.cpp @@ -208,9 +208,12 @@ static void adjBasedSearchFromPoints( Omega_h::Real cutoffDistance = radii2[id]; Omega_h::LO source_cell_id = source_cell_ids[id]; - OMEGA_H_CHECK_PRINTF(source_cell_id >= 0, - "ERROR: Source cell id not found for target %d\n", - id); + // Targets outside the source mesh are flagged with an invalid cell id + // (see locate_target_cells). Skip them: no supports, no memory access. + if (source_cell_id < 0) { + nSupports[id] = 0; + return; + } const Omega_h::LO num_verts_in_dim = dim + 1; Omega_h::LO start_ptr = source_cell_id * num_verts_in_dim; @@ -382,6 +385,10 @@ SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, true); } else { pcms::printInfo("INFO: Adaptive radius search... \n"); + // Cap the adaptation so targets that can never reach the required support + // count (e.g. points outside the source mesh, which are skipped and always + // report 0 supports) do not spin the loop forever. + const int max_radius_adjust_loops = 50; int r_adjust_loop = 0; while (true) { nSupports = Omega_h::Write( @@ -410,6 +417,14 @@ SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, break; } + if (r_adjust_loop >= max_radius_adjust_loops) { + pcms::printInfo( + "WARNING: radius adaptation hit the %d-loop cap; some targets remain " + "outside the [%d, %d] support range\n", + max_radius_adjust_loops, min_req_support, 3 * min_req_support); + break; + } + adapt_radii(min_req_support, max_allowed_support, nvertices_target, radii2, nSupports); } @@ -471,6 +486,9 @@ SupportResults searchNeighbors(Omega_h::Mesh& mesh, search.adjBasedSearch(supports_ptr, nSupports, supports_idx, radii2, true); } else { pcms::printInfo("INFO: Adaptive radius search... \n"); + // Cap the adaptation so targets that can never reach the required support + // count do not spin the loop forever (see searchNeighborsFromPoints). + const int max_radius_adjust_loops = 50; int r_adjust_loop = 0; while (true) { // until the number of minimum support is met const auto max_radius = Omega_h::get_max(Omega_h::read(radii2)); @@ -498,6 +516,14 @@ SupportResults searchNeighbors(Omega_h::Mesh& mesh, break; } + if (r_adjust_loop >= max_radius_adjust_loops) { + pcms::printInfo( + "WARNING: radius adaptation hit the %d-loop cap; some targets remain " + "outside the [%d, %d] support range\n", + max_radius_adjust_loops, min_support, 3 * min_support); + break; + } + adapt_radii(min_support, 3 * min_support, radii2.size(), radii2, nSupports); } diff --git a/src/pcms/pythonapi/CMakeLists.txt b/src/pcms/pythonapi/CMakeLists.txt index bb2c316b..f2c569cd 100644 --- a/src/pcms/pythonapi/CMakeLists.txt +++ b/src/pcms/pythonapi/CMakeLists.txt @@ -17,6 +17,14 @@ pybind11_add_module(pcms target_compile_features(pcms PUBLIC cxx_std_20) target_link_libraries(pcms PRIVATE Kokkos::kokkos pybind11::module MPI::MPI_C pcms::core pcms::transfer) +# The conservative/Monte Carlo projection bindings include transfer headers +# that transitively pull in PETSc (petscvec.h). PETSc is linked PRIVATE into +# pcms_transfer, so its include directories do not propagate; link it here so +# the bindings compile when PETSc + MeshFields are enabled. +if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) + target_link_libraries(pcms PRIVATE PETSc::PETSc meshfields::meshfields) +endif() + # if we are building as a python package from the pyproject.toml # scikit-build-core will set this variable if(SKBUILD) diff --git a/src/pcms/pythonapi/bind_field_base.cpp b/src/pcms/pythonapi/bind_field_base.cpp index ee4bff09..eca1c98a 100644 --- a/src/pcms/pythonapi/bind_field_base.cpp +++ b/src/pcms/pythonapi/bind_field_base.cpp @@ -8,6 +8,7 @@ #include "pcms/field/field_layout.h" #include "pcms/field/field_metadata.h" #include "pcms/field/function_space.h" +#include "pcms/discretization/discretization/omega_h.hpp" #include "pcms/field/data/simple.h" #include "pcms/field/layout/uniform_grid.h" #include "pcms/field/uniform_grid_binary_field.h" @@ -223,7 +224,12 @@ void bind_create_field_module(py::module& m) .def( "set_dof_holder_data", - [](Field& self, py::array_t arr) { + // c_style|forcecast makes pybind materialize a contiguous copy of the + // input, so non-contiguous arrays (e.g. a column slice like data[:, i]) + // are read correctly rather than by walking flat memory with the wrong + // stride. + [](Field& self, + py::array_t arr) { auto buf = arr.request(); if (buf.ndim != 1) { throw std::runtime_error("DOF holder data must be a 1D array"); @@ -280,10 +286,12 @@ void bind_create_field_module(py::module& m) }, "DOF holder coordinates as a 2D numpy array (num_dof_holders Ɨ dim)"); - py::class_(m, "FunctionSpace") + py::class_>(m, "FunctionSpace") .def( "create_field", - [](const FunctionSpace& self) { return self.CreateField(); }, + [](const FunctionSpace& self) -> Field { + return self.CreateFunction(); + }, "Create a Field for this function space.") .def( "create_point_evaluator", @@ -293,19 +301,52 @@ void bind_create_field_module(py::module& m) py::arg("request"), "Create a reusable point evaluator from an EvaluationRequest.") .def("get_coordinate_system", &FunctionSpace::GetCoordinateSystem, - "Get the coordinate system for this function space"); + "Get the coordinate system for this function space") + .def( + "mesh", + [](const FunctionSpace& self) -> Omega_h::Mesh& { + auto disc = std::dynamic_pointer_cast( + self.GetDiscretization()); + if (!disc) { + throw std::runtime_error( + "FunctionSpace::mesh: this function space is not backed by an " + "Omega_h mesh"); + } + return disc->GetMesh(); + }, + py::return_value_policy::reference, + "The Omega_h mesh this function space is defined on (Omega_h backend " + "only)."); // Bind LagrangeFunctionSpace as a concrete FunctionSpace subtype. - py::class_(m, "LagrangeFunctionSpace") + py::class_> + lagrange_space(m, "LagrangeFunctionSpace"); + + // Backend selecting the underlying field layout. The conservative-projection + // transfer operators require the native Omega_h backend. + py::enum_(lagrange_space, "Backend") + .value("MeshFields", LagrangeFunctionSpace::Backend::MeshFields, + "MeshFields-backed layout (default when MeshFields is enabled)") + .value("OmegaH", LagrangeFunctionSpace::Backend::OmegaH, + "Native Omega_h Lagrange layout (required by the conservative and " + "Monte Carlo projection transfer operators)") + .export_values(); + + lagrange_space .def_static( "from_mesh", [](Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system) { - return LagrangeFunctionSpace::FromMesh(mesh, order, num_components, - coordinate_system); + CoordinateSystem coordinate_system, std::string global_id_name, + LagrangeFunctionSpace::Backend backend) { + return LagrangeFunctionSpace::FromMesh( + mesh, order, num_components, coordinate_system, + std::move(global_id_name), backend); }, py::arg("mesh"), py::arg("order"), py::arg("num_components") = 1, py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("global_id_name") = "global", + py::arg("backend") = LagrangeFunctionSpace::DefaultBackend, "Create a LagrangeFunctionSpace from an Omega_h mesh") .def_static( @@ -348,7 +389,8 @@ void bind_create_field_module(py::module& m) // Bind PolynomialReconstructionFunctionSpace as a concrete FunctionSpace // subtype. - py::class_( + py::class_>( m, "PolynomialReconstructionFunctionSpace") .def_static( "from_coords", diff --git a/src/pcms/pythonapi/bind_transfer_field.cpp b/src/pcms/pythonapi/bind_transfer_field.cpp index 599b31f3..cc599bb0 100644 --- a/src/pcms/pythonapi/bind_transfer_field.cpp +++ b/src/pcms/pythonapi/bind_transfer_field.cpp @@ -1,6 +1,7 @@ #include #include #include +#include "pcms/configuration.h" #include "pcms/transfer/copy.h" #include "pcms/field/field.h" #include "pcms/field/function_space.h" @@ -9,6 +10,12 @@ #include "pcms/field/out_of_bounds_policy.h" #include "numpy_array_transform.h" #include "pcms/utility/types.h" +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) +#include "pcms/transfer/omega_h_conservative_projection.hpp" +#include "pcms/transfer/omega_h_control_variate_projection.hpp" +#include "pcms/transfer/omega_h_mc_rhs_integrator.hpp" +#include +#endif namespace py = pybind11; @@ -70,6 +77,62 @@ void bind_transfer_field_module(py::module& m) Field& target) { self.Apply(source, target); }, py::arg("source"), py::arg("target"), "Copy source field data to target field (same layout required)."); + +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) + // Conservative L2 (Galerkin) projection between order-1 Lagrange spaces on + // Omega_h 2D simplex meshes. The RHS is integrated exactly over the + // intersection of the source and target meshes. + py::class_(m, "OmegaHConservativeProjection") + .def(py::init([](const FunctionSpace& source_space, + const FunctionSpace& target_space) { + return std::make_unique(source_space, + target_space); + }), + py::arg("source_space"), py::arg("target_space"), + "Construct a mesh-intersection conservative projection. Mesh " + "intersection, quadrature setup, and mass-matrix factorization happen " + "here and are cached; call apply() repeatedly.") + .def( + "apply", + [](const OmegaHConservativeProjection& self, const Field& source, + Field& target) { self.Apply(source, target); }, + py::arg("source"), py::arg("target"), + "Conservatively project the source field onto the target space using " + "exact mesh-intersection quadrature."); + + // Sampling strategy for the Monte Carlo RHS integrator. + py::enum_(m, "MonteCarloSampling") + .value("UniformRandom", MonteCarloSampling::UniformRandom, + "Independent pseudo-random uniform draws per element") + .export_values(); + + // Variance-reduced Monte Carlo Galerkin projection. The source field is + // first interpolated onto the target space as a control variate; only the + // residual is integrated stochastically, so sampling noise is small (and + // exactly zero when the source already lives in the target space). + py::class_(m, + "OmegaHControlVariateProjection") + .def(py::init([](const FunctionSpace& source_space, + const FunctionSpace& target_space, int samples_per_element, + MonteCarloSampling sampling, std::uint64_t seed) { + return std::make_unique( + source_space, target_space, samples_per_element, sampling, seed); + }), + py::arg("source_space"), py::arg("target_space"), + py::arg("samples_per_element"), + py::arg("sampling") = MonteCarloSampling::UniformRandom, + py::arg("seed") = std::uint64_t(8675309), + "Construct a Monte Carlo (control-variate) conservative projection. " + "Sample-point generation, the control-variate interpolator, and the " + "mass-matrix factorization are cached; call apply() repeatedly.") + .def( + "apply", + [](const OmegaHControlVariateProjection& self, const Field& source, + Field& target) { self.Apply(source, target); }, + py::arg("source"), py::arg("target"), + "Project the source field onto the target space using Monte Carlo " + "integration with control-variate variance reduction."); +#endif } } // namespace pcms diff --git a/src/pcms/pythonapi/pythonapi.cpp b/src/pcms/pythonapi/pythonapi.cpp index 3e9e253e..e3491377 100644 --- a/src/pcms/pythonapi/pythonapi.cpp +++ b/src/pcms/pythonapi/pythonapi.cpp @@ -1,5 +1,9 @@ #include +#include "pcms/configuration.h" +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) +#include +#endif namespace py = pybind11; @@ -33,6 +37,26 @@ void bind_mesh_utilities_module(py::module& m); } // namespace pcms PYBIND11_MODULE(pcms, m) { +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) + // The conservative/Monte Carlo projection solvers build PETSc objects on + // PETSC_COMM_WORLD, so PETSc must be initialized before any of them are + // constructed. Do it once at import time (PetscInitialize brings up MPI if it + // is not already running) so callers never manage PETSc state by hand. + // + // PETSc is intentionally NOT finalized via atexit: PETSc's Kokkos-backed + // objects must be torn down before Kokkos::finalize, but Kokkos is owned by + // OmegaHLibrary and an atexit hook would run after that library is destroyed. + // Leaving PETSc initialized until the process exits avoids that ordering trap + // and is harmless (the OS reclaims everything on exit). + { + PetscBool petsc_initialized = PETSC_FALSE; + PetscInitialized(&petsc_initialized); + if (!petsc_initialized) { + PetscInitializeNoArguments(); + } + } +#endif + // Bind fundamental types first (coordinate systems, etc.) pcms::bind_coordinate_system_module(m); pcms::bind_coordinate_module(m); diff --git a/src/pcms/transfer/CMakeLists.txt b/src/pcms/transfer/CMakeLists.txt index 216b5759..dddd25a2 100644 --- a/src/pcms/transfer/CMakeLists.txt +++ b/src/pcms/transfer/CMakeLists.txt @@ -7,6 +7,7 @@ set(PCMS_FIELD_TRANSFER_HEADERS interpolator.h copy.h transfer_operator.hpp + integration_point_set.hpp ) @@ -17,25 +18,29 @@ set(PCMS_FIELD_TRANSFER_SOURCES if(PCMS_ENABLE_MESHFIELDS) list(APPEND PCMS_FIELD_TRANSFER_HEADERS - load_vector_integrator.hpp mass_matrix_integrator.hpp) - list(APPEND PCMS_FIELD_TRANSFER_SOURCES - load_vector_integrator.cpp) endif() if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) list(APPEND PCMS_FIELD_TRANSFER_HEADERS - calculate_load_vector.hpp - calculate_mass_matrix.hpp + linear_form_integrator.hpp + bilinear_form_integrator.hpp conservative_projection_solver.hpp omega_h_conservative_projection.hpp + omega_h_intersection_rhs_integrator.hpp + omega_h_mass_integrator.hpp + omega_h_mc_rhs_integrator.hpp + omega_h_control_variate_projection.hpp coo_assembly_utils.hpp - petsc_utils.hpp) + petsc_utils.hpp + omega_h_form_integrator_utils.hpp) list(APPEND PCMS_FIELD_TRANSFER_SOURCES - calculate_load_vector.cpp - calculate_mass_matrix.cpp conservative_projection_solver.cpp omega_h_conservative_projection.cpp + omega_h_intersection_rhs_integrator.cpp + omega_h_mass_integrator.cpp + omega_h_mc_rhs_integrator.cpp + omega_h_control_variate_projection.cpp petsc_utils.cpp coo_assembly_utils.cpp) endif() @@ -60,7 +65,7 @@ target_link_libraries(pcms_transfer PUBLIC Omega_h::omega_h) if(PCMS_ENABLE_MESHFIELDS) - target_link_libraries(pcms_transfer PUBLIC meshfields::meshfields) + target_link_libraries(pcms_transfer PRIVATE meshfields::meshfields) endif() if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) diff --git a/src/pcms/transfer/bilinear_form_integrator.hpp b/src/pcms/transfer/bilinear_form_integrator.hpp new file mode 100644 index 00000000..060667bd --- /dev/null +++ b/src/pcms/transfer/bilinear_form_integrator.hpp @@ -0,0 +1,22 @@ +#ifndef PCMS_TRANSFER_BILINEAR_FORM_INTEGRATOR_H +#define PCMS_TRANSFER_BILINEAR_FORM_INTEGRATOR_H + +#include + +namespace pcms +{ + +class BilinearFormIntegrator +{ +public: + // Returns the internally-assembled matrix. The integrator owns the matrix + // and its lifetime must exceed any KSP that references it (PETSc's reference + // counting keeps the matrix alive until the KSP is destroyed). + virtual Mat GetMatrix() const noexcept = 0; + + virtual ~BilinearFormIntegrator() noexcept = default; +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_BILINEAR_FORM_INTEGRATOR_H diff --git a/src/pcms/transfer/calculate_load_vector.cpp b/src/pcms/transfer/calculate_load_vector.cpp deleted file mode 100644 index f351a4ef..00000000 --- a/src/pcms/transfer/calculate_load_vector.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "pcms/transfer/calculate_load_vector.hpp" -#include "pcms/transfer/coo_assembly_utils.hpp" -#include "pcms/transfer/load_vector_integrator.hpp" -#include "pcms/transfer/petsc_utils.hpp" - -namespace pcms -{ -PetscErrorCode calculateLoadVector(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& source_values, - Vec* loadVec_out) -{ - - PetscFunctionBeginUser; - PetscInt nnz = 0; - PetscInt* coo_i = nullptr; - PetscCall(build_linear_triangle_coo_rows(target_mesh, &coo_i, &nnz)); - PetscScalar* coo_vals = nullptr; - PetscCall(PetscMalloc1(nnz, &coo_vals)); - - // Fill COO values - auto elmLoadVector = - buildLoadVector(target_mesh, source_mesh, intersection, source_values); - - auto hostElmLoadVector = Kokkos::create_mirror_view(elmLoadVector); - Kokkos::deep_copy(hostElmLoadVector, elmLoadVector); - PetscCheck(static_cast(hostElmLoadVector.extent(0)) == nnz, - PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, - "Element load vector size (%d) does not match COO nnz (%d)", - static_cast(hostElmLoadVector.extent(0)), nnz); - - for (PetscInt e = 0; e < nnz; ++e) { - coo_vals[e] = hostElmLoadVector(e); - } - - // create vector with preallocated COO structure - Vec vec; - PetscCall(createSeqVec(PETSC_COMM_WORLD, target_mesh.nverts(), &vec)); - PetscCall(VecSetPreallocationCOO(vec, nnz, coo_i)); - PetscCall(VecSetValuesCOO(vec, coo_vals, ADD_VALUES)); - PetscCall(PetscFree(coo_i)); - PetscCall(PetscFree(coo_vals)); - - if (target_mesh.nelems() < 10) { - PetscCall(VecView(vec, PETSC_VIEWER_STDOUT_WORLD)); - } - - *loadVec_out = vec; - PetscFunctionReturn(PETSC_SUCCESS); -} -} // namespace pcms diff --git a/src/pcms/transfer/calculate_load_vector.hpp b/src/pcms/transfer/calculate_load_vector.hpp deleted file mode 100644 index b1968338..00000000 --- a/src/pcms/transfer/calculate_load_vector.hpp +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @file calculate_load_vector.hpp - * @brief Routines for assembling global load vector in conservative field - * projection. - * - * Provides functionality to compute and assemble the global load vector - * used in Galerkin-based conservative field transfer between non-conforming - * meshes. - * - */ - -#ifndef PCMS_TRANSFER_CALCULATE_LOAD_VECTOR_HPP -#define PCMS_TRANSFER_CALCULATE_LOAD_VECTOR_HPP -#include -#include -#include - -/** - * @brief Assembles the global load vector. - * - * This function computes the unassembled local load vector contributions for - * each triangular element in the target mesh using `buildLoadVector()` and then - * assembles them into a global PETSc vector in COO format. - * - * - * @param target_mesh The target Omega_h mesh to which the scalar field is being - * projected. - * @param source_mesh The source Omega_h mesh containing the original scalar - * field values. - * @param intersection Precomputed intersection data for each target element. - * Includes the number and indices of intersecting source - * elements. - * @param source_values Nodal scalar field values defined on the source mesh. - * @param[out] loadVec_out Pointer to a PETSc Vec where the assembled load - * vector will be stored. - * - * @return PetscErrorCode Returns PETSC_SUCCESS if successful, or an appropriate - * PETSc error code otherwise. - * - * @note - * - Works for 2D linear triangular elements. - * - Uses COO-style preallocation and insertion into the PETSc vector. - * - Internally calls `buildLoadVector()` to compute per-element contributions. - * - The resulting vector is used as the right-hand side (RHS) in a projection - * solve. - * - * @see buildLoadVector,IntersectionResults - */ - -namespace pcms -{ -// FIXME use PCMS error handling rather than returning a PETSC error code -PetscErrorCode calculateLoadVector(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& source_values, - Vec* loadVec_out); -} // namespace pcms -#endif // PCMS_TRANSFER_CALCULATE_LOAD_VECTOR_HPP diff --git a/src/pcms/transfer/calculate_mass_matrix.cpp b/src/pcms/transfer/calculate_mass_matrix.cpp deleted file mode 100644 index 2b54e4c6..00000000 --- a/src/pcms/transfer/calculate_mass_matrix.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "pcms/transfer/calculate_mass_matrix.hpp" -#include "pcms/transfer/coo_assembly_utils.hpp" -#include "pcms/transfer/mass_matrix_integrator.hpp" -#include "pcms/transfer/petsc_utils.hpp" -#include "pcms/utility/memory_spaces.h" - -namespace pcms -{ -/** - * @brief Creates a PETSc matrix based on mesh connectivity - * - * This function creates a sparse matrix with the proper sparsity pattern - * according to the mesh connectivity. The matrix size corresponds to the - * number of vertices in the mesh. - * - * @param mesh The Omega_h mesh to create the matrix from - * @param[out] A Pointer to the PETSc matrix to be created - * @return PetscErrorCode PETSc error code (PETSC_SUCCESS if successful) - */ -static PetscErrorCode create_linear_triangle_coo_matrix(Omega_h::Mesh& mesh, - Mat* A) -{ - PetscInt* coo_rows = nullptr; - PetscInt* coo_cols = nullptr; - PetscInt matSize = 0; - PetscFunctionBeginUser; - PetscCall(createSeqAIJMat(PETSC_COMM_WORLD, mesh.nverts(), mesh.nverts(), 0, - nullptr, A)); - PetscCall( - build_linear_triangle_coo_rows_cols(mesh, &coo_rows, &coo_cols, &matSize)); - PetscCall(MatSetPreallocationCOO(*A, matSize, coo_rows, coo_cols)); - PetscCall(PetscFree2(coo_rows, coo_cols)); - PetscFunctionReturn(PETSC_SUCCESS); -} -PetscErrorCode calculateMassMatrix(Omega_h::Mesh& mesh, Mat* mass_out) -{ - PetscFunctionBeginUser; - - MeshField::OmegahMeshField - omf(mesh); - - const auto ShapeOrder = 1; - auto coordField = omf.getCoordField(); - const auto [shp, map] = - MeshField::Omegah::getTriangleElement(mesh); - MeshField::FieldElement coordFe(mesh.nelems(), coordField, shp, map); - - auto elmMassMatrix = buildMassMatrix(mesh, coordFe); - const PetscInt expected_nnz = 9 * mesh.nelems(); - PetscCheck(static_cast(elmMassMatrix.extent(0)) == expected_nnz, - PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, - "Element mass data size (%d) does not match expected linear COO " - "entries (%d)", - static_cast(elmMassMatrix.extent(0)), expected_nnz); - - Mat mass; - PetscCall(create_linear_triangle_coo_matrix(mesh, &mass)); - PetscCall(MatZeroEntries(mass)); - PetscCall( - MatSetValuesCOO(mass, elmMassMatrix.data(), - INSERT_VALUES)); // FIXME fails here on gpu, calls into host - // implementation... AFAIK, petsc checks - // the type of the input array of values to - // decide which backend to use... - // - if (mesh.nelems() < 10) { - PetscCall(MatView(mass, PETSC_VIEWER_STDOUT_WORLD)); - } - - *mass_out = mass; - PetscFunctionReturn(PETSC_SUCCESS); -} -} // namespace pcms diff --git a/src/pcms/transfer/calculate_mass_matrix.hpp b/src/pcms/transfer/calculate_mass_matrix.hpp deleted file mode 100644 index 0889fdc8..00000000 --- a/src/pcms/transfer/calculate_mass_matrix.hpp +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file calculateMassMatrix.hpp - * @brief Functions for calculating mass matrices on finite element meshes - * @author [Cameron Smith] - * @date April, 2025 - * - * This file contains functions for creating and computing mass matrices - * for finite element calculations using Omega_h mesh structures and PETSc. - */ - -#ifndef PCMS_TRANSFER_CALCULATE_MASS_MATRIX_HPP -#define PCMS_TRANSFER_CALCULATE_MASS_MATRIX_HPP - -#include -#include - -namespace pcms -{ -/** - * @brief Calculates the mass matrix for a given mesh - * - * This function constructs a mass matrix based on the provided mesh using - * a finite element approach. It creates coordinate field elements, builds - * the mass matrix using the massMatrixIntegrator, and sets up the PETSc matrix - * with appropriate values. - * - * @param mesh The Omega_h mesh to calculate the mass matrix for - * @param[out] mass_out Pointer to the resulting mass matrix - * @return PetscErrorCode PETSc error code (PETSC_SUCCESS if successful) - */ - -PetscErrorCode calculateMassMatrix(Omega_h::Mesh& mesh, Mat* mass_out); -} // namespace pcms -#endif // PCMS_TRANSFER_CALCULATE_MASS_MATRIX_HPP diff --git a/src/pcms/transfer/conservative_projection_solver.cpp b/src/pcms/transfer/conservative_projection_solver.cpp index 148454f8..e15555e3 100644 --- a/src/pcms/transfer/conservative_projection_solver.cpp +++ b/src/pcms/transfer/conservative_projection_solver.cpp @@ -1,69 +1,12 @@ #include #include "pcms/transfer/conservative_projection_solver.hpp" +#include "pcms/utility/arrays.h" #include "pcms/transfer/petsc_utils.hpp" -#include "pcms/transfer/calculate_load_vector.hpp" -#include "pcms/transfer/calculate_mass_matrix.hpp" +#include namespace pcms { -/** - * @brief Solves a linear system Ax = b using PETSc's KSP solvers - * - * Uses PETSc's Krylov Subspace solvers to find x in Ax = b. - * The solver can be configured through PETSc runtime options. - * - * @param A The system matrix - * @param b The right-hand side vector - * @return Vec Solution vector x - */ -static Vec solveLinearSystem(Mat A, Vec b) -{ - PetscInt m, n; - PetscErrorCode ierr; - - ierr = MatGetSize(A, &m, &n); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - Vec x; - ierr = createSeqVec(PETSC_COMM_WORLD, n, &x); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - KSP ksp; - ierr = KSPCreate(PETSC_COMM_WORLD, &ksp); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = KSPSetOperators(ksp, A, A); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = KSPSetComputeSingularValues(ksp, PETSC_TRUE); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = KSPSetFromOptions(ksp); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = KSPSetUp(ksp); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = KSPSolve(ksp, b, x); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - /// compute and print condition number estimate - PetscReal smax = 0.0, smin = 0.0; - ierr = KSPComputeExtremeSingularValues(ksp, &smax, &smin); - if (!ierr && smin > 0.0) { - PetscPrintf(PETSC_COMM_WORLD, - "Estimated condition number of matrix A: %.6e\n", smax / smin); - } else { - PetscPrintf(PETSC_COMM_WORLD, - "Condition number estimate unavailable (smin <= 0 or error)\n"); - } - - ierr = KSPDestroy(&ksp); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - - return x; -} static Omega_h::Reals vecToOmegaHReals(Vec vec) { @@ -86,59 +29,71 @@ static Omega_h::Reals vecToOmegaHReals(Vec vec) return Omega_h::Reals(values_host); } -Omega_h::Reals solveGalerkinProjection(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& source_values) -{ - if ((PetscInt)source_values.size() != - source_mesh.coords().size() / source_mesh.dim()) { - std::cerr << "ERROR: source_values size (" << source_values.size() - << ") doesn't match expected size (" - << source_mesh.coords().size() / source_mesh.dim() << ")" - << std::endl; - throw std::runtime_error("source_values length mismatch"); - } +// --------------------------------------------------------------------------- +// GalerkinProjectionSolver +// --------------------------------------------------------------------------- - Mat mass; - PetscErrorCode ierr = calculateMassMatrix(target_mesh, &mass); - CHKERRABORT(PETSC_COMM_WORLD, ierr); +GalerkinProjectionSolver::GalerkinProjectionSolver( + const BilinearFormIntegrator& mass_integrator, + LinearFormIntegrator& rhs_integrator) + : rhs_integrator_(&rhs_integrator) +{ + Mat A = mass_integrator.GetMatrix(); - Vec vec; - ierr = calculateLoadVector(target_mesh, source_mesh, intersection, - source_values, &vec); + PetscInt m = 0, n = 0; + PetscErrorCode ierr = MatGetSize(A, &m, &n); CHKERRABORT(PETSC_COMM_WORLD, ierr); + nverts_ = m; - Vec x = solveLinearSystem(mass, vec); - auto solution_vector = vecToOmegaHReals(x); + const std::size_t num_pts = + rhs_integrator_->GetIntegrationPoints().NumPoints(); + sampled_values_ = + Kokkos::View("rhs_sampled", num_pts, 1); - ierr = VecDestroy(&x); + ierr = KSPCreate(PETSC_COMM_WORLD, &ksp_); CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = MatDestroy(&mass); + ierr = KSPSetOperators(ksp_, A, A); CHKERRABORT(PETSC_COMM_WORLD, ierr); - - ierr = VecDestroy(&vec); + ierr = KSPSetFromOptions(ksp_); CHKERRABORT(PETSC_COMM_WORLD, ierr); + ierr = KSPSetUp(ksp_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); +} - return solution_vector; +GalerkinProjectionSolver::~GalerkinProjectionSolver() +{ + if (ksp_) { + KSPDestroy(&ksp_); + } + // mat_ is owned by the BilinearFormIntegrator; KSP holds its own reference. +} + +Omega_h::Reals GalerkinProjectionSolver::Solve( + const PointEvaluator& evaluator, const Field& source_field) const +{ + evaluator.Evaluate(source_field, MakeRank2View(sampled_values_)); + return Solve(MakeConstRank2View(sampled_values_)); } -Omega_h::Reals rhsVectorMI(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& source_values) + +Omega_h::Reals GalerkinProjectionSolver::Solve( + Rank2View sampled_values) const { - Vec vec; - PetscErrorCode ierr; - ierr = calculateLoadVector(target_mesh, source_mesh, intersection, - source_values, &vec); + rhs_integrator_->Assemble(sampled_values); + Vec rhs_vector = rhs_integrator_->GetVector(); + + Vec solution = nullptr; + PetscErrorCode ierr = createSeqVec(PETSC_COMM_WORLD, nverts_, &solution); CHKERRABORT(PETSC_COMM_WORLD, ierr); - auto rhsvector = vecToOmegaHReals(vec); + ierr = KSPSolve(ksp_, rhs_vector, solution); + CHKERRABORT(PETSC_COMM_WORLD, ierr); - ierr = VecDestroy(&vec); + auto result = vecToOmegaHReals(solution); + + ierr = VecDestroy(&solution); CHKERRABORT(PETSC_COMM_WORLD, ierr); - return rhsvector; + return result; } + } // namespace pcms diff --git a/src/pcms/transfer/conservative_projection_solver.hpp b/src/pcms/transfer/conservative_projection_solver.hpp index d3e46a37..f0f2df0e 100644 --- a/src/pcms/transfer/conservative_projection_solver.hpp +++ b/src/pcms/transfer/conservative_projection_solver.hpp @@ -1,72 +1,72 @@ -/** - * @file conservative_projection_solver.hpp - * @brief Solves the conservative projection of scalar fields between - * non-matching meshes. - * - * Provides the main interface to perform Galerkin projection of scalar fields - * from a source mesh to a target mesh using conservative transfer using a - * supermesh generated from mesh intersections. - * - * The solver computes the right-hand side (load vector), assembles the mass - * matrix, and solves the resulting linear system to obtain projected nodal - * values. - * - */ - #ifndef PCMS_TRANSFER_CONSERVATIVE_PROJECTION_SOLVER_HPP #define PCMS_TRANSFER_CONSERVATIVE_PROJECTION_SOLVER_HPP +#include #include -#include +#include -#include +#include +#include +#include +#include +#include namespace pcms { -/** - * @brief Solves a conservative galerkin projection problem to transfer scalar - * field values onto a target mesh. - * - * This function assembles and solves a linear system of the form: - * \f[ - * M \cdot x = f - * \f] - * where: - * - \f$M\f$ is the mass matrix on the target mesh (based on P1 finite - * elements), - * - \f$f\f$ is the load vector computed on the supermesh, - * - \f$x\f$ is the unknown nodal field on the target mesh (solution). - * - * The method computes the conservative field transfer between two non-matching - * meshes using mesh intersections (supermesh). - * - * ### Algorithm Steps: - * 1. Compute and assemble mass matrix and load vector - * 2. Solve the linear system using PETSc. - * 3. Return the solution as a nodal field on the target mesh. - * - * @param target_mesh The Omega_h mesh where the field is projected. - * @param source_mesh The Omega_h mesh containing the original field data. - * @param intersection Precomputed intersection information between source and - * target meshes. - * @param source_values Nodal scalar field values on the source mesh. - * - * @return A vector of nodal values on the target mesh after projection - * (Omega_h::Reals). - * - * - */ +// Cached Galerkin projection solver for M*x = f. +// +// The mass matrix is obtained from mass_integrator (which owns and assembled +// it) and the KSP is set up (preconditioned/factored) at construction. The RHS +// integrator is also bound at construction: its integration points fix the +// size of the cached sample buffer, which is allocated once and reused. Each +// Solve() call only fills that buffer, assembles a fresh RHS vector, and calls +// KSPSolve, reusing the cached factorization. +// +// Binding one RHS integrator per solver matches every current consumer (each +// builds M and the integrator together and reuses them in lockstep) and lets +// the sample buffer be sized exactly once. Reusing a factored M with a +// different integrator is intentionally not supported; if that need arises, +// add a setter that rebinds the integrator and resizes the buffer. +// +// Non-copyable and non-movable because it owns PETSc handles. +class GalerkinProjectionSolver +{ +public: + // Retrieves the assembled matrix from mass_integrator, sets up the KSP, and + // sizes the sample buffer from rhs_integrator's integration points. PETSc + // reference-counts the matrix, so mass_integrator need not outlive this + // solver, but rhs_integrator must (it is assembled into on every Solve). + GalerkinProjectionSolver(const BilinearFormIntegrator& mass_integrator, + LinearFormIntegrator& rhs_integrator); + ~GalerkinProjectionSolver(); + + GalerkinProjectionSolver(const GalerkinProjectionSolver&) = delete; + GalerkinProjectionSolver& operator=(const GalerkinProjectionSolver&) = delete; + + // Evaluates source_field at the bound integrator's integration points via + // evaluator into the cached buffer, assembles the RHS, solves M*x = f using + // the cached KSP, and returns nodal values on the target mesh. + // + // May be called repeatedly with different source fields, provided the target + // mesh (and therefore M) is unchanged. + Omega_h::Reals Solve(const PointEvaluator& evaluator, + const Field& source_field) const; + + // Assembles the RHS from values already sampled at the bound integrator's + // integration points (shape [num_points][num_components]) and solves + // M*x = f using the cached KSP. Used by workflows that pre-process the + // sampled values (e.g. control-variate residuals) before assembly. + Omega_h::Reals Solve( + Rank2View sampled_values) const; -Omega_h::Reals solveGalerkinProjection(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& source_values); +private: + PetscInt nverts_; + KSP ksp_ = nullptr; + LinearFormIntegrator* rhs_integrator_; + mutable Kokkos::View sampled_values_; +}; -Omega_h::Reals rhsVectorMI(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& source_values); } // namespace pcms #endif // PCMS_TRANSFER_CONSERVATIVE_PROJECTION_SOLVER_HPP diff --git a/src/pcms/transfer/copy.h b/src/pcms/transfer/copy.h index f650fed5..3ab4fa80 100644 --- a/src/pcms/transfer/copy.h +++ b/src/pcms/transfer/copy.h @@ -39,6 +39,7 @@ class Copy : public TransferOperator { public: Copy(const FunctionSpace& source_space, const FunctionSpace& target_space) + : TransferOperator(source_space, target_space) { auto source_layout = source_space.GetLayout(); auto target_layout = target_space.GetLayout(); diff --git a/src/pcms/transfer/integration_point_set.hpp b/src/pcms/transfer/integration_point_set.hpp new file mode 100644 index 00000000..5cd3c7fe --- /dev/null +++ b/src/pcms/transfer/integration_point_set.hpp @@ -0,0 +1,57 @@ +#ifndef PCMS_TRANSFER_INTEGRATION_POINT_SET_H +#define PCMS_TRANSFER_INTEGRATION_POINT_SET_H + +#include "pcms/field/coordinate_system.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/types.h" + +#include +#include +#include + +namespace pcms +{ + +// An ordered collection of global coordinates at which a source field must +// be evaluated for integration. The ordering is significant: sampled values +// passed back to a LinearFormIntegrator must use the same point order as +// stored here. +// +// This type intentionally does not expose quadrature weights, basis values, +// entity ids, or any other assembly metadata. Those remain private to the +// concrete integrator implementation. It owns the coordinate buffer; callers +// receive a non-owning CoordinateView from GetCoordinates(). +template +class IntegrationPointSet +{ +public: + IntegrationPointSet() = default; + + IntegrationPointSet(CoordinateSystem coordinate_system, + Kokkos::View coords) + : coordinate_system_(coordinate_system), coords_(std::move(coords)) + { + } + + // Global coordinates of all integration points, shape [num_points][dim]. + CoordinateView GetCoordinates() const + { + return CoordinateView(coordinate_system_, + MakeConstRank2View(coords_)); + } + + // Number of integration points. + [[nodiscard]] + std::size_t NumPoints() const + { + return coords_.extent(0); + } + +private: + CoordinateSystem coordinate_system_ = CoordinateSystem::Cartesian; + Kokkos::View coords_; +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_INTEGRATION_POINT_SET_H diff --git a/src/pcms/transfer/interpolator.h b/src/pcms/transfer/interpolator.h index a8b5b07e..e5c1a218 100644 --- a/src/pcms/transfer/interpolator.h +++ b/src/pcms/transfer/interpolator.h @@ -12,7 +12,7 @@ #include "pcms/utility/types.h" #include #include -#include +#include "pcms/transfer/transfer_operator.hpp" namespace pcms { @@ -36,7 +36,8 @@ class Interpolator : public TransferOperator // Expensive: localizes target DOF coords into source mesh. Called once. Interpolator(const FunctionSpace& source_space, const FunctionSpace& target_space, OutOfBoundsPolicy policy = {}) - : num_points_(static_cast( + : TransferOperator(source_space, target_space), + num_points_(static_cast( target_space.GetLayout()->GetDOFHolderCoordinates().GetValues().extent( 0))), n_comp_(target_space.GetLayout()->GetNumComponents()), diff --git a/src/pcms/transfer/linear_form_integrator.hpp b/src/pcms/transfer/linear_form_integrator.hpp new file mode 100644 index 00000000..38835a8a --- /dev/null +++ b/src/pcms/transfer/linear_form_integrator.hpp @@ -0,0 +1,34 @@ +#ifndef PCMS_TRANSFER_LINEAR_FORM_INTEGRATOR_H +#define PCMS_TRANSFER_LINEAR_FORM_INTEGRATOR_H + +#include "pcms/transfer/integration_point_set.hpp" +#include "pcms/utility/arrays.h" +#include + +namespace pcms +{ + +class LinearFormIntegrator +{ +public: + // Returns the ordered integration points. The index ordering must match the + // first dimension of sampled_values passed to Assemble. + virtual const IntegrationPointSet& GetIntegrationPoints() + const noexcept = 0; + + // Returns the internally-owned vector. The integrator owns the vector; + // callers receive a non-owning handle. Valid after construction. + virtual Vec GetVector() const noexcept = 0; + + // Zeros the owned vector, scatter-adds weighted contributions from the + // sampled source-field values, and finalizes assembly. + // sampled_values shape: [num_integration_points][num_components] + virtual void Assemble( + Rank2View sampled_values) = 0; + + virtual ~LinearFormIntegrator() noexcept = default; +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_LINEAR_FORM_INTEGRATOR_H diff --git a/src/pcms/transfer/load_vector_integrator.cpp b/src/pcms/transfer/load_vector_integrator.cpp deleted file mode 100644 index 221e3905..00000000 --- a/src/pcms/transfer/load_vector_integrator.cpp +++ /dev/null @@ -1,430 +0,0 @@ -#include "pcms/transfer/load_vector_integrator.hpp" - -namespace pcms -{ - -/** - * @brief Converts barycentric coordinates to global (physical) coordinates. - * - * Given barycentric coordinates within a 2D triangle and the coordinates of - * the triangle's vertices, this function computes the corresponding global - * position. - * - * @param barycentric_coord The barycentric coordinates \f$(\lambda_1, - * \lambda_2, \lambda_3)\f$ of the point. - * @param verts_coord The coordinates of the triangle's three vertices in global - * space. - * @return The 2D global coordinates corresponding to the given barycentric - * position. - */ - -[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<2> global_from_barycentric( - const MeshField::Vector3& barycentric_coord, - const Omega_h::Few, 3>& verts_coord) -{ - Omega_h::Vector<2> real_coords = {0.0, 0.0}; - - for (int i = 0; i < 3; ++i) { - real_coords[0] += barycentric_coord[i] * verts_coord[i][0]; - real_coords[1] += barycentric_coord[i] * verts_coord[i][1]; - } - return real_coords; -} - -/** - * @brief Computes the barycentric coordinates of a 2D point with respect to a - * triangle. - * - * Given a point in global (x, y) coordinates and the coordinates of the three - * vertices of a triangle, this function evaluates the barycentric coordinates - * \f$(\lambda_1, \lambda_2, \lambda_3)\f$ of the point with respect to that - * triangle. - * - * @param point The 2D global coordinates of the point to evaluate (in - * Omega_h::Vector<2> format). - * @param verts_coord The vertex coordinates of the triangle (in r3d::Vector<2> - * format). - * @return A vector of three barycentric coordinates corresponding to the input - * point. - * - */ - -[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<3> evaluate_barycentric( - const Omega_h::Vector<2>& point, - const r3d::Few, 3>& verts_coord) -{ - Omega_h::Few, 3> omegah_vector; - for (int i = 0; i < 3; ++i) { - omegah_vector[i][0] = verts_coord[i][0]; - omegah_vector[i][1] = verts_coord[i][1]; - } - - auto barycentric_coordinate = - Omega_h::barycentric_from_global<2, 2>(point, omegah_vector); - - return barycentric_coordinate; -} -/** - * @brief Evaluates the value of a linear function at a given point using - * barycentric coordinates. - * - * This function computes the interpolated value of a nodal scalar field over a - * triangle, using barycentric coordinates within the specified element. - * - * @param nodal_values The global array of nodal field values. - * @param faces2nodes The element-to-node connectivity array. - * @param bary_coords The barycentric coordinates of the evaluation point within - * the triangle. - * @param elm_id The ID of the triangle element being evaluated. - * @return The interpolated function value at the given point. - * - * @note This function assumes linear (3-node) triangular element. - */ - -[[nodiscard]] OMEGA_H_INLINE double evaluate_function_value( - const Omega_h::Reals& nodal_values, const Omega_h::LOs& faces2nodes, - const Omega_h::Vector<3>& bary_coords, const int elm_id) -{ - - double value = 0; - const auto elm_verts = Omega_h::gather_verts<3>(faces2nodes, elm_id); - for (int i = 0; i < 3; ++i) { - int nid = elm_verts[i]; - value += nodal_values[nid] * bary_coords[i]; - } - - return value; -} - -/** - * @brief Deduplicate, reorder, and orient a 2D polygon produced by r3d. - * - * This function performs all necessary cleanup and reconstruction of a polygon - * returned by `r3d::intersect_simplices()`, which may contain: - * - duplicated vertices, - * - unordered vertex lists, - * - invalid or inconsistent neighbor links (`pnbrs`), - * - negative orientation (CW instead of CCW). - * - * The cleanup proceeds with the following stages: - * - * **(1) Geometric deduplication:** - * Vertices whose coordinates are equal within a tolerance `tol` are collapsed - * into a single unique vertex. A compacted vertex list is built. - * - * **(2) CCW vertex reordering:** - * The remaining unique vertices are sorted by their polar angle around the - * polygon centroid. This yields a globally consistent counter-clockwise (CCW) - * - * - * **(3) Rebuilding neighbor links:** - * After sorting, each vertex's two neighbors (`pnbrs[0]` and `pnbrs[1]`) are - * reassigned to form a closed CCW cycle: - * - * pnbrs[0] = previous vertex in CCW order - * pnbrs[1] = next vertex in CCW order - * - * - * - * @param poly The polygon to clean, and reorder. - * - * @param tol Tolerance for geometric duplicate detection (default: 1e-12). - * - * @return The number of unique, CCW-ordered vertices remaining in the polygon. - * - * @see r3d::Polytope - * @see r3d::measure - * @see r3d::intersect_simplices - */ - -[[nodiscard]] OMEGA_H_INLINE int remove_duplicate_vertices_and_fix_links( - r3d::Polytope<2>& poly, const double tol = 1e-12) -{ - - const int old_n = poly.nverts; - int new_n = 0; - - // geometric deupliactes filtering - for (int i = 0; i < old_n; ++i) { - const auto& pi = poly.verts[i].pos; - bool dup = false; - for (int j = 0; j < new_n; ++j) { - const auto& pj = poly.verts[j].pos; - if (Kokkos::fabs(pi[0] - pj[0]) < tol && - Kokkos::fabs(pi[1] - pj[1]) < tol) { - dup = true; - break; - } - } - if (!dup) { - poly.verts[new_n] = poly.verts[i]; - ++new_n; - } - } - poly.nverts = new_n; - - // CCW reorder verts by angle about centroid - if (new_n >= 3) { - // centroid - double cx = 0.0; - double cy = 0.0; - for (int i = 0; i < new_n; ++i) { - cx += poly.verts[i].pos[0]; - cy += poly.verts[i].pos[1]; - } - cx /= new_n; - cy /= new_n; - - // angle sort - int order[r3d::MaxVerts<2>::value]; - for (int i = 0; i < new_n; ++i) { - order[i] = i; - } - - for (int i = 0; i < new_n - 1; ++i) { - for (int j = i + 1; j < new_n; ++j) { - const double a1 = Kokkos::atan2(poly.verts[order[i]].pos[1] - cy, - poly.verts[order[i]].pos[0] - cx); - const double a2 = Kokkos::atan2(poly.verts[order[j]].pos[1] - cy, - poly.verts[order[j]].pos[0] - cx); - if (a1 > a2) { - int t = order[i]; - order[i] = order[j]; - order[j] = t; - } - } - } - - r3d::Vertex<2> tmp[r3d::MaxVerts<2>::value]; - for (int i = 0; i < new_n; ++i) { - tmp[i] = poly.verts[order[i]]; - } - - for (int i = 0; i < new_n; ++i) { - poly.verts[i] = tmp[i]; - } - - // set circular neighbors consistent with verts[] order - for (int i = 0; i < new_n; ++i) { - const int prev = (i - 1 + new_n) % new_n; - const int next = (i + 1) % new_n; - poly.verts[i].pnbrs[0] = prev; // CCW prev - poly.verts[i].pnbrs[1] = next; // CCW next - } - - // ensure positive orientation (if measure is still negative, reverse) - double area = r3d::measure(poly); - if (area < 0.0) { - // reverse verts and relink - for (int i = 0; i < new_n / 2; ++i) { - r3d::Vertex<2> t = poly.verts[i]; - poly.verts[i] = poly.verts[new_n - 1 - i]; - poly.verts[new_n - 1 - i] = t; - } - for (int i = 0; i < new_n; ++i) { - const int prev = (i - 1 + new_n) % new_n; - const int next = (i + 1) % new_n; - poly.verts[i].pnbrs[0] = prev; - poly.verts[i].pnbrs[1] = next; - } - } - } else { - // clear links - for (int i = 0; i < new_n; ++i) { - poly.verts[i].pnbrs[0] = -1; - poly.verts[i].pnbrs[1] = -1; - } - } - - return new_n; -} - -template -OMEGA_H_INLINE void for_each_intersection_subtriangle( - const int elm, const IntersectionResults& intersection, - const Omega_h::Reals& tgt_coords, const Omega_h::Reals& src_coords, - const Omega_h::LOs& tgt_faces2nodes, const Omega_h::LOs& src_faces2nodes, - TriangleOp&& op) -{ - auto tgt_elm_vert_coords = - get_vert_coords_of_elem(tgt_coords, tgt_faces2nodes, elm); - const int start = intersection.tgt2src_offsets[elm]; - const int end = intersection.tgt2src_offsets[elm + 1]; - - for (int i = start; i < end; ++i) { - const int current_src_elm = intersection.tgt2src_indices[i]; - auto src_elm_vert_coords = - get_vert_coords_of_elem(src_coords, src_faces2nodes, current_src_elm); - r3d::Polytope<2> poly; - r3d::intersect_simplices(poly, tgt_elm_vert_coords, src_elm_vert_coords); - auto nverts = remove_duplicate_vertices_and_fix_links(poly, 1e-12); - ; - auto poly_area = r3d::measure(poly); - - for (int j = 1; j < nverts - 1; ++j) { - // build triangle from poly.verts[0], poly.verts[j], - // poly.verts[j+1] - auto& p0 = poly.verts[0].pos; - auto& p1 = poly.verts[j].pos; - auto& p2 = poly.verts[j + 1].pos; - - Omega_h::Few, 3> tri_coords; - tri_coords[0] = {p0[0], p0[1]}; - tri_coords[1] = {p1[0], p1[1]}; - tri_coords[2] = {p2[0], p2[1]}; - - Omega_h::Few, 2> basis; - basis[0] = tri_coords[1] - tri_coords[0]; - basis[1] = tri_coords[2] - tri_coords[0]; - - Omega_h::Real area = - Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); - - const double EPS_AREA = abs_tol + rel_tol * poly_area; - if (area <= EPS_AREA) - continue; // drops duplicates and colinear/degenerates - - op(tri_coords, tgt_elm_vert_coords, src_elm_vert_coords, current_src_elm, - area); - } - } -} - -Kokkos::View buildLoadVector( - Omega_h::Mesh& target_mesh, Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, const Omega_h::Reals& source_values) -{ - - const auto& tgt_coords = target_mesh.coords(); - const auto& src_coords = source_mesh.coords(); - const auto& tgt_faces2nodes = - target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - const auto& src_faces2nodes = - source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - - IntegrationData<2> integrationPoints; - int npts = integrationPoints.size(); - - // TODO: Make it generalised; hardcoded for liner 2D - Kokkos::View elmLoadVector( - "elmLoadVector", static_cast(target_mesh.nelems()) * 3); - Kokkos::parallel_for( - "calculate load vector", target_mesh.nelems(), - KOKKOS_LAMBDA(const int& elm) { - Omega_h::Vector<3> part_integration = {0.0, 0.0, 0.0}; - for_each_intersection_subtriangle( - elm, intersection, tgt_coords, src_coords, tgt_faces2nodes, - src_faces2nodes, - [&](const Omega_h::Few, 3>& tri_coords, - const r3d::Few, 3>& tgt_elm_vert_coords, - const r3d::Few, 3>& src_elm_vert_coords, - const int current_src_elm, const Omega_h::Real area) { - for (int ip = 0; ip < npts; ++ip) { - auto bary = integrationPoints.bary_coords(ip); - auto weight = integrationPoints.weights(ip); - - // convert barycentric to real coords in triangle - auto real_coords = global_from_barycentric(bary, tri_coords); - - // evaluate shape function (barycentric wrt target for linear) - auto shape_fn = - evaluate_barycentric(real_coords, tgt_elm_vert_coords); - - // evaluate function at point (barycentric wrt source for linear) - auto src_bary = - evaluate_barycentric(real_coords, src_elm_vert_coords); - auto fval = evaluate_function_value(source_values, src_faces2nodes, - src_bary, current_src_elm); - - // integration - for (int k = 0; k < 3; ++k) { - part_integration[k] += shape_fn[k] * fval * weight * 2 * area; - } - } - }); - - for (int j = 0; j < 3; ++j) { - elmLoadVector(elm * 3 + j) = part_integration[j]; - } - }); - - return elmLoadVector; -} -Errors evaluate_proj_and_cons_errors(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& target_values, - const Omega_h::Reals& source_values) -{ - - const auto& tgt_coords = target_mesh.coords(); - const auto& src_coords = source_mesh.coords(); - const auto& tgt_faces2nodes = - target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - const auto& src_faces2nodes = - source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - - IntegrationData<2> integrationPoints; - int npts = integrationPoints.size(); - - constexpr double EPS_DEN = 1e-30; - - Kokkos::View accum("accum", 4); - Kokkos::deep_copy(accum, 0.0); - - Kokkos::parallel_for( - "evaluate relative errors", target_mesh.nelems(), - KOKKOS_LAMBDA(const int& elm) { - double N2 = 0.0, D2 = 0.0, C = 0.0, QD = 0.0; - for_each_intersection_subtriangle( - elm, intersection, tgt_coords, src_coords, tgt_faces2nodes, - src_faces2nodes, - [&](const Omega_h::Few, 3>& tri_coords, - const r3d::Few, 3>& tgt_elm_vert_coords, - const r3d::Few, 3>& src_elm_vert_coords, - const int current_src_elm, const Omega_h::Real area) { - for (int ip = 0; ip < npts; ++ip) { - auto bary = integrationPoints.bary_coords(ip); - auto weight = integrationPoints.weights(ip); - - // convert barycentric to real coords in triangle - auto real_coords = global_from_barycentric(bary, tri_coords); - auto tgt_bary = - evaluate_barycentric(real_coords, tgt_elm_vert_coords); - - // evaluate shape function (barycentric wrt target for linear) - auto tgtVal = evaluate_function_value( - target_values, tgt_faces2nodes, tgt_bary, elm); - - // evaluate function at point (barycentric wrt source for linear) - auto src_bary = - evaluate_barycentric(real_coords, src_elm_vert_coords); - auto srcVal = evaluate_function_value( - source_values, src_faces2nodes, src_bary, current_src_elm); - - // integration - auto diff = srcVal - tgtVal; - auto w = 2 * weight * area; - N2 += diff * diff * w; - D2 += srcVal * srcVal * w; - C += diff * w; - QD += srcVal * w; - } - }); - - Kokkos::atomic_add(&accum(0), N2); - Kokkos::atomic_add(&accum(1), D2); - Kokkos::atomic_add(&accum(2), C); - Kokkos::atomic_add(&accum(3), QD); - }); - - auto h_accum = Kokkos::create_mirror(accum); - Kokkos::deep_copy(h_accum, accum); - const double proj_err = - Kokkos::sqrt(h_accum(0)) / Kokkos::max(Kokkos::sqrt(h_accum(1)), EPS_DEN); - const double cons_err = - Kokkos::fabs(h_accum(2)) / Kokkos::max(Kokkos::fabs(h_accum(3)), EPS_DEN); - - return Errors{.proj_err = proj_err, .cons_err = cons_err}; -} -} // namespace pcms diff --git a/src/pcms/transfer/load_vector_integrator.hpp b/src/pcms/transfer/load_vector_integrator.hpp deleted file mode 100644 index 4e1d2fad..00000000 --- a/src/pcms/transfer/load_vector_integrator.hpp +++ /dev/null @@ -1,220 +0,0 @@ -/** - * @file load_vector_integrator.hpp - * @brief Functions for computing load vectors in conservative field projection. - * - * This file implements routines to compute the element-wise load vector - * (right-hand side) contributions used in Galerkin projection of scalar - * fields from a source mesh to a target mesh. - * - * The integration is performed over polygonal intersections (supermesh) between - * source and target elements using barycentric quadrature. The resulting values - * represent unassembled local contributions that can later be combined into a - * global load vector. - * - * @note - * - Assumes 2D linear triangular meshes. - * - Intersection data is provided via the `IntersectionResults` structure. - */ -#ifndef PCMS_TRANSFER_LOAD_VECTOR_INTEGRATOR_HPP -#define PCMS_TRANSFER_LOAD_VECTOR_INTEGRATOR_HPP - -#include -#include -#include -#include -#include -#include - -namespace pcms -{ -/** - * @brief Computes the load vector for each target element in the conservative - * field transfer. - * - * This routine is used for constructing the right-hand side (RHS) of the - * conservative field transfer formulation, projecting field quantities from the - * source mesh to the target mesh. - * - * The underlying algorithm computes contributions to the load vector - * using geometric intersection data between source and target elements. - * - * @note Currently this method works for a two-dimensional linear triangles. - */ - -/** - * @brief Provides barycentric integration points and weights for a triangle - * element. - * - * This templated struct stores the barycentric coordinates - * and quadrature weights for performing numerical integration over a reference - * triangle. It is used for integrating functions over elements in the - * conservative field transfer. - * - * @tparam order The quadrature order (number of integration points and - * polynomial accuracy). - */ -template -struct IntegrationData -{ - // Barycentric coordinates of integration points - Kokkos::View bary_coords; - - // Quadrature weights associated with each integration point - Kokkos::View weights; - - /** - * @brief Constructs the integration data for a given quadrature order - * - * Initializes barycentric coordinates and weights using - * MeshField's predefined triangle quadrature rules. - */ - IntegrationData() - { - auto ip_vec = MeshField::getIntegrationPoints(order); - std::size_t num_ip = ip_vec.size(); - - bary_coords = Kokkos::View("bary_coords", num_ip); - weights = Kokkos::View("weights", num_ip); - - auto bary_coords_host = Kokkos::create_mirror_view(bary_coords); - auto weights_host = Kokkos::create_mirror_view(weights); - - for (std::size_t i = 0; i < num_ip; ++i) { - bary_coords_host(i) = ip_vec[i].param; - weights_host(i) = ip_vec[i].weight; - } - - Kokkos::deep_copy(bary_coords, bary_coords_host); - Kokkos::deep_copy(weights, weights_host); - } - - /** - * @brief Returns the number of integration points - * - * @return Number of integration points for the selected order. - */ - - int size() const { return bary_coords.extent(0); } -}; - -/** - * @brief Computes the per-element RHS load vectors for conservative field - * projection from source to target mesh. - * - * This function computes local (element-wise) right-hand side (RHS) - * contributions for the Galerkin projection of a scalar field from the source - * mesh to the target mesh. It integrates over the polygonal intersection - * regions between each target element and its intersecting source elements - * using barycentric quadrature. - * - * The output is a flat array containing unassembled load vector contributions - * at the nodes of each target triangle. - * - * @param target_mesh The target mesh object receiving the projected scalar - * field. - * @param source_mesh The source mesh object containing the original scalar - * field values. - * @param intersection Precomputed intersection data for each target element. - * Includes the number and indices of intersecting source - * elements. - * @param source_values Scalar field values defined at the nodes of the source - * mesh. - * - * @return A Kokkos view containing per-element load vectors. - * Each triangle contributes 3 values (one per node), so the view has - * size 3 Ɨ (number of target elements). - * - * @note - * - This function assumes 2D linear triangular elements. - * - Degenerate or near-zero-area intersection polygons are skipped. - * - Each polygon is triangulated using a fan structure and integrated using - * barycentric quadrature rules. - * - The returned vector must be assembled into a global RHS vector in a later - * step. - * - * @see evaluate_barycentric, evaluate_function_value, global_from_barycentric - * @see IntersectionResults - */ - -Kokkos::View buildLoadVector( - Omega_h::Mesh& target_mesh, Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, const Omega_h::Reals& source_values); -/// Holds projection and conservation error metrics returned by -/// evaluate_pro_and_cons_errors(). -struct Errors -{ - double proj_err; ///< L2 projection error computed on the supermesh. - double cons_err; ///< Relative conservation error over the supermesh. -}; - -/** - * @brief Computes projection and conservation errors over the supermesh for - * scalar field transfer. - * - * This function quantifies the accuracy and conservation properties of - * conservative field transfer between nonconforming meshes using - * supermesh-based integration. It returns a struct containing two error - * metrics: - * - * - **Projection Error (`proj_err`)** — Measures the L2 norm of the difference - * between the projected source field and the target field over the - * supermesh.This reflects how accurately the field has been projected. - * - * - **Conservation Error (`cons_err`)** — Relative difference in the integrated - * field values between source and target representations. This captures - * conservation loss across the transfer. - * - * ### Mathematical Definitions: - * \f[ - * \text{proj\_err} = \frac{ \| q_D - q_T \|_{L_2(\Omega_S)} } - * { \| q_D \|_{L_2(\Omega_S)} }, \quad - * \text{cons\_err} = \frac{ \left| \int_{\Omega_S} q_D - \int_{\Omega_S} q_T - * \right| } { \left| \int_{\Omega_S} q_D \right| } - * \f] - * - * where: - * - \f$q_D\f$ is the scalar field defined on the source mesh (mesh from where - * the field is defined), - * - \f$q_T\f$ is the projected field on the target mesh (mesh to where the - * field is projected), - * - \f$\Omega_S\f$ is the supermesh formed by polygonal intersections of source - * and target elements. - * - * Integration is performed by triangulating each intersection region and - * applying barycentric quadrature. Degenerate or near-zero-area triangles are - * skipped based on area tolerance. - * - * - * @param target_mesh The target mesh object receiving the projected scalar - * field. - * @param source_mesh The source mesh object containing the original scalar - * field values. - * @param intersection Precomputed intersection data for each target element. - * Includes the number and indices of intersecting source - * elements. - * @param target_values Nodal scalar field values evaluated on the target mesh - * using galerkin projection. - * @param source_values Scalar field values defined at the nodes of the source - * mesh. - * - * - * @return A struct containing: - * - `proj_err`: Exact L2 projection error over the supermesh. - * - `cons_err`: Relative conservation error over the supermesh. - * - * @note - * - Assumes 2D linear (P1) triangular elements. - * - Ideal for validating conservative transfer schemes or testing projection - * fidelity. - * - * @see IntersectionResults, buildLoadVector - */ - -Errors evaluate_proj_and_cons_errors(Omega_h::Mesh& target_mesh, - Omega_h::Mesh& source_mesh, - const IntersectionResults& intersection, - const Omega_h::Reals& target_values, - const Omega_h::Reals& source_values); -} // namespace pcms - -#endif // PCMS_TRANSFER_LOAD_VECTOR_INTEGRATOR_HPP diff --git a/src/pcms/transfer/mass_matrix_integrator.hpp b/src/pcms/transfer/mass_matrix_integrator.hpp index 1a2b4b59..7d36c975 100644 --- a/src/pcms/transfer/mass_matrix_integrator.hpp +++ b/src/pcms/transfer/mass_matrix_integrator.hpp @@ -31,7 +31,7 @@ class MassMatrixIntegrator : public MeshField::Integrator } void atPoints(Kokkos::View p, Kokkos::View w, - Kokkos::View dV) + Kokkos::View dV) override { // std::cerr << "MassMatrixIntegrator::atPoints(...)\n"; const size_t numPtsPerElem = p.extent(0) / mesh.nelems(); @@ -77,8 +77,8 @@ class MassMatrixIntegrator : public MeshField::Integrator }; template -Kokkos::View buildMassMatrix(Omega_h::Mesh& mesh, - FieldElement& coordFe) +Kokkos::View buildElementMassMatrix(Omega_h::Mesh& mesh, + FieldElement& coordFe) { MassMatrixIntegrator mmi(mesh, coordFe); mmi.process(coordFe); diff --git a/src/pcms/transfer/monte_carlo_sampling.hpp b/src/pcms/transfer/monte_carlo_sampling.hpp new file mode 100644 index 00000000..0e5738e9 --- /dev/null +++ b/src/pcms/transfer/monte_carlo_sampling.hpp @@ -0,0 +1,15 @@ +#ifndef PCMS_TRANSFER_MONTE_CARLO_SAMPLING_HPP +#define PCMS_TRANSFER_MONTE_CARLO_SAMPLING_HPP + +namespace pcms +{ + +// Strategy for generating sample points on each target element. +enum class MonteCarloSampling +{ + UniformRandom, // independent uniform draws per element (XorShift64 pool) +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_MONTE_CARLO_SAMPLING_HPP diff --git a/src/pcms/transfer/omega_h_conservative_projection.cpp b/src/pcms/transfer/omega_h_conservative_projection.cpp index c7ae7e65..539d739c 100644 --- a/src/pcms/transfer/omega_h_conservative_projection.cpp +++ b/src/pcms/transfer/omega_h_conservative_projection.cpp @@ -1,6 +1,8 @@ #include "pcms/transfer/omega_h_conservative_projection.hpp" +#include "pcms/transfer/conservative_projection_solver.hpp" +#include "pcms/transfer/omega_h_intersection_rhs_integrator.hpp" +#include "pcms/transfer/omega_h_mass_integrator.hpp" #include "pcms/utility/arrays.h" -#include "pcms/utility/assert.h" #include #include @@ -10,28 +12,6 @@ namespace pcms namespace { -void CheckSupportedLayout( - const FunctionSpace& space, - const std::shared_ptr& layout, const char* role) -{ - if (layout == nullptr) { - throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + - " space must use OmegaHLagrangeLayout"); - } - if (layout->GetOrder() != 1) { - throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + - " space must be order-1"); - } - if (layout->GetNumComponents() != 1) { - throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + - " space must have exactly one component"); - } - if (space.GetCoordinateSystem() != CoordinateSystem::Cartesian) { - throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + - " space must use Cartesian coordinates"); - } -} - void CheckApplyCompatible(const Field& source, const Field& target, const OmegaHLagrangeLayout& source_layout, const OmegaHLagrangeLayout& target_layout) @@ -58,45 +38,60 @@ void CheckApplyCompatible(const Field& source, const Field& target, } } -Omega_h::Reals MakeOmegaHReals(Rank1View values) -{ - Omega_h::HostWrite values_host(values.size()); - for (size_t i = 0; i < values.size(); ++i) { - values_host[i] = values[i]; - } - return Omega_h::Reals(values_host); -} - } // namespace OmegaHConservativeProjection::OmegaHConservativeProjection( const FunctionSpace& source_space, const FunctionSpace& target_space) - : source_layout_(std::dynamic_pointer_cast( + : TransferOperator(source_space, target_space), + source_layout_(std::dynamic_pointer_cast( source_space.GetLayout())), target_layout_(std::dynamic_pointer_cast( target_space.GetLayout())) { - CheckSupportedLayout(source_space, source_layout_, "source"); - CheckSupportedLayout(target_space, target_layout_, "target"); + rhs_integrator_ = std::make_unique( + source_layout_, source_space.GetCoordinateSystem(), target_layout_, + target_space.GetCoordinateSystem()); - intersections_ = - intersectTargets(source_layout_->GetMesh(), target_layout_->GetMesh()); + evaluator_ = + source_space.CreatePointEvaluator(EvaluationRequest::FromCoordinates( + rhs_integrator_->GetIntegrationPoints().GetCoordinates())); + + // Mass integrator is only needed to build the solver; PETSc reference-counts + // the matrix so it remains alive inside the KSP after this scope ends. + OmegaHMassIntegrator mass_integrator(target_layout_, + target_space.GetCoordinateSystem()); + solver_ = std::make_unique(mass_integrator, + *rhs_integrator_); + target_values_ = Kokkos::View( + "conservative_projection_target_values", + target_layout_->GetNumOwnedDofHolder(), target_layout_->GetNumComponents()); } +// Defined here so that GalerkinProjectionSolver (forward-declared in the +// header) is a complete type when unique_ptr's destructor is instantiated. +OmegaHConservativeProjection::~OmegaHConservativeProjection() = default; + void OmegaHConservativeProjection::Apply(const Field& source, Field& target) const { CheckApplyCompatible(source, target, *source_layout_, *target_layout_); - const auto source_values = - MakeOmegaHReals(FlattenToRank1View(source.GetDOFHolderDataHost())); - const auto target_values = solveGalerkinProjection( - target_layout_->GetMesh(), source_layout_->GetMesh(), intersections_, - source_values); - auto target_values_h = Omega_h::HostRead(target_values); - target.SetDOFHolderDataHost(Rank2View( - target_values_h.data(), target_layout_->GetNumOwnedDofHolder(), - target_layout_->GetNumComponents())); + const auto solution = solver_->Solve(*evaluator_, source); + const auto global_to_local = target_layout_->GetGlobalToLocalPermutation(); + const int num_dof_holders = target_layout_->GetNumOwnedDofHolder(); + const int num_components = target_layout_->GetNumComponents(); + auto target_values = target_values_; + Kokkos::parallel_for( + "conservative_projection_scatter_solution", + Kokkos::RangePolicy(0, num_dof_holders), + KOKKOS_LAMBDA(int i) { + for (int c = 0; c < num_components; ++c) { + target_values(i, c) = solution[global_to_local(i) * num_components + c]; + } + }); + Kokkos::fence(); + + target.SetDOFHolderData(MakeConstRank2View(target_values_)); } } // namespace pcms diff --git a/src/pcms/transfer/omega_h_conservative_projection.hpp b/src/pcms/transfer/omega_h_conservative_projection.hpp index 386cb260..18b7a2cb 100644 --- a/src/pcms/transfer/omega_h_conservative_projection.hpp +++ b/src/pcms/transfer/omega_h_conservative_projection.hpp @@ -3,30 +3,49 @@ #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" -#include "pcms/transfer/conservative_projection_solver.hpp" +#include "pcms/field/point_evaluator.h" #include "pcms/transfer/transfer_operator.hpp" +#include #include namespace pcms { +class GalerkinProjectionSolver; +class OmegaHIntersectionRHSIntegrator; + // Conservative Galerkin projection between Omega_h order-1 Lagrange spaces. // -// Construction caches the source-target mesh intersections. Apply() assembles -// the projection system for the current source coefficients, solves it, and -// writes the projected scalar field into the target Field. +// Construction (one-time cost): +// - Computes mesh intersections and quadrature data +// (OmegaHIntersectionRHSIntegrator). +// - Localizes integration points in the source mesh (PointEvaluator). +// - Assembles the target mass matrix and factors it via KSP +// (GalerkinProjectionSolver). +// +// Apply() (per-call cost): +// - Evaluates the source field at the fixed integration points. +// - Assembles a fresh RHS vector. +// - Calls KSPSolve reusing the cached factorization. +// No mesh intersection, no matrix assembly, no refactorization per call. class OmegaHConservativeProjection : public TransferOperator { public: OmegaHConservativeProjection(const FunctionSpace& source_space, const FunctionSpace& target_space); + // Defined in the .cpp where GalerkinProjectionSolver is a complete type. + ~OmegaHConservativeProjection() override; + void Apply(const Field& source, Field& target) const override; private: std::shared_ptr source_layout_; std::shared_ptr target_layout_; - IntersectionResults intersections_; + std::unique_ptr rhs_integrator_; + std::unique_ptr> evaluator_; + std::unique_ptr solver_; + mutable Kokkos::View target_values_; }; } // namespace pcms diff --git a/src/pcms/transfer/omega_h_control_variate_projection.cpp b/src/pcms/transfer/omega_h_control_variate_projection.cpp new file mode 100644 index 00000000..2e5c5ed1 --- /dev/null +++ b/src/pcms/transfer/omega_h_control_variate_projection.cpp @@ -0,0 +1,86 @@ +#include "pcms/transfer/omega_h_control_variate_projection.hpp" +#include "pcms/transfer/conservative_projection_solver.hpp" +#include "pcms/transfer/omega_h_mass_integrator.hpp" +#include "pcms/transfer/omega_h_mc_rhs_integrator.hpp" +#include "pcms/utility/arrays.h" +#include +#include + +namespace pcms +{ + +OmegaHControlVariateProjection::OmegaHControlVariateProjection( + const FunctionSpace& source_space, const FunctionSpace& target_space, + int samples_per_element, MonteCarloSampling sampling, uint64_t seed) + : TransferOperator(source_space, target_space), + target_layout_(std::dynamic_pointer_cast( + target_space.GetLayout())), + rhs_integrator_(std::make_unique( + target_layout_, target_space.GetCoordinateSystem(), samples_per_element, + sampling, seed)), + interpolator_(source_space, target_space), + control_variate_(target_space.CreateFunction()) +{ + const auto sample_coords = + rhs_integrator_->GetIntegrationPoints().GetCoordinates(); + source_at_samples_ = source_space.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(sample_coords)); + control_variate_at_samples_ = target_space.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(sample_coords)); + + // Mass integrator is only needed to build the solver; PETSc reference-counts + // the matrix so it remains alive inside the KSP after this scope ends. + OmegaHMassIntegrator mass_integrator(target_layout_, + target_space.GetCoordinateSystem()); + solver_ = std::make_unique(mass_integrator, + *rhs_integrator_); +} + +// Defined here so that GalerkinProjectionSolver (forward-declared in the +// header) is a complete type when unique_ptr's destructor is instantiated. +OmegaHControlVariateProjection::~OmegaHControlVariateProjection() = default; + +void OmegaHControlVariateProjection::Apply(const Field& source, + Field& target) const +{ + // 1. Control variate: interpolate the source field onto the target space. + interpolator_.Apply(source, control_variate_); + + // 2. Sample the source field and the control variate at the fixed Monte + // Carlo sample points; the stochastic RHS integrates the residual. + const std::size_t num_samples = + rhs_integrator_->GetIntegrationPoints().NumPoints(); + Kokkos::View f_samples("cv_f_samples", num_samples, + 1); + Kokkos::View residual("cv_residual", num_samples, + 1); + source_at_samples_->Evaluate(source, MakeRank2View(f_samples)); + control_variate_at_samples_->Evaluate(control_variate_, + MakeRank2View(residual)); + Kokkos::parallel_for( + "cv_residual", Kokkos::RangePolicy(0, num_samples), + KOKKOS_LAMBDA(int i) { + residual(i, 0) = f_samples(i, 0) - residual(i, 0); + }); + + // 3. Project the residual: M * delta = r. + const auto delta = solver_->Solve(MakeConstRank2View(residual)); + + // 4. x = g + delta. delta is indexed by active PETSc row, while the field is + // indexed by local DOF holder. + const auto g_nodal = control_variate_.GetDOFHolderData(); + const auto global_to_local = target_layout_->GetGlobalToLocalPermutation(); + const int nverts = static_cast(g_nodal.extent(0)); + + Kokkos::View result("cv_result", nverts, 1); + Kokkos::parallel_for( + "cv_add_correction", Kokkos::RangePolicy(0, nverts), + KOKKOS_LAMBDA(int i) { + result(i, 0) = g_nodal(i, 0) + delta[global_to_local(i)]; + }); + Kokkos::fence(); + + target.SetDOFHolderData(MakeConstRank2View(result)); +} + +} // namespace pcms diff --git a/src/pcms/transfer/omega_h_control_variate_projection.hpp b/src/pcms/transfer/omega_h_control_variate_projection.hpp new file mode 100644 index 00000000..4196ea5d --- /dev/null +++ b/src/pcms/transfer/omega_h_control_variate_projection.hpp @@ -0,0 +1,65 @@ +#ifndef PCMS_TRANSFER_OMEGA_H_CONTROL_VARIATE_PROJECTION_HPP +#define PCMS_TRANSFER_OMEGA_H_CONTROL_VARIATE_PROJECTION_HPP + +#include "pcms/field/field.h" +#include "pcms/field/function_space.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/transfer/interpolator.h" +#include "pcms/transfer/monte_carlo_sampling.hpp" +#include "pcms/transfer/transfer_operator.hpp" +#include +#include + +namespace pcms +{ + +class GalerkinProjectionSolver; +class OmegaHMonteCarloRHSIntegrator; + +// Variance-reduced Monte Carlo Galerkin projection using a control variate. +// +// The control variate g is the interpolation of the source field onto the +// target space (Interpolator: evaluate the source field at the target nodal +// locations, interpolate with the target basis). Since g lives in the target +// space, its L2 projection is g itself, so only the residual f - g needs the +// stochastic RHS: +// M x = M g + \int phi (f - g) => x = g + M^{-1} r, +// r_j ~= (|T| / N) * sum_i phi_j(x_i) (f(x_i) - g(x_i)) +// Because g approximates f, the sampled residual has far smaller variance +// than sampling f directly (and is exactly zero when f is in the target +// space). +// +// Apply() per-call cost: one source-field evaluation at the target nodes +// (interpolation), one source-field and one target-field evaluation at the +// fixed sample points, RHS assembly, and a KSPSolve with the cached +// factorization. +class OmegaHControlVariateProjection : public TransferOperator +{ +public: + OmegaHControlVariateProjection(const FunctionSpace& source_space, + const FunctionSpace& target_space, + int samples_per_element, + MonteCarloSampling sampling, + uint64_t seed = 8675309); + + // Defined in the .cpp where GalerkinProjectionSolver is a complete type. + ~OmegaHControlVariateProjection() override; + + void Apply(const Field& source, Field& target) const override; + +private: + std::shared_ptr target_layout_; + std::unique_ptr rhs_integrator_; + Interpolator interpolator_; + // Scratch field on the target space holding the interpolated control + // variate; overwritten on every Apply. + mutable Field control_variate_; + std::unique_ptr> source_at_samples_; + std::unique_ptr> control_variate_at_samples_; + std::unique_ptr solver_; +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_OMEGA_H_CONTROL_VARIATE_PROJECTION_HPP diff --git a/src/pcms/transfer/omega_h_form_integrator_utils.hpp b/src/pcms/transfer/omega_h_form_integrator_utils.hpp new file mode 100644 index 00000000..23f5e35d --- /dev/null +++ b/src/pcms/transfer/omega_h_form_integrator_utils.hpp @@ -0,0 +1,341 @@ +#ifndef PCMS_TRANSFER_OMEGA_H_FORM_INTEGRATOR_UTILS_HPP +#define PCMS_TRANSFER_OMEGA_H_FORM_INTEGRATOR_UTILS_HPP + +#include "pcms/field/function_space.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/transfer/mesh_intersection.hpp" +#include "pcms/utility/assert.h" +#include +#include +#include +#include + +namespace pcms::detail +{ + +// Shared checks for a scalar Cartesian Lagrange space on a 2D simplex mesh, +// independent of order. Order is validated separately by the callers below. +inline void CheckOmegaHScalarSimplex2DLayout( + CoordinateSystem coordinate_system, + const std::shared_ptr& layout, + const char* context, const char* role) +{ + if (layout == nullptr) { + throw pcms_error(std::string(context) + ": " + role + + " space must use OmegaHLagrangeLayout"); + } + if (layout->GetNumComponents() != 1) { + throw pcms_error(std::string(context) + ": " + role + + " space must have exactly one component"); + } + if (coordinate_system != CoordinateSystem::Cartesian) { + throw pcms_error(std::string(context) + ": " + role + + " space must use Cartesian coordinates"); + } + const Omega_h::Mesh& mesh = layout->GetMesh(); + if (mesh.dim() != 2) { + throw pcms_error(std::string(context) + ": " + role + " mesh must be 2D"); + } + if (mesh.family() != OMEGA_H_SIMPLEX) { + throw pcms_error(std::string(context) + ": " + role + + " mesh must be a simplex (triangle) mesh"); + } +} + +// Strict order-1 check (used where only P1 is supported, e.g. Monte-Carlo RHS). +inline void CheckOmegaHScalarP1Layout( + CoordinateSystem coordinate_system, + const std::shared_ptr& layout, + const char* context, const char* role) +{ + CheckOmegaHScalarSimplex2DLayout(coordinate_system, layout, context, role); + if (layout->GetOrder() != 1) { + throw pcms_error(std::string(context) + ": " + role + + " space must be order-1"); + } +} + +// Conservative-projection check: any supported Lagrange order (P0 or P1). The +// intersection integrator handles source and target orders independently, so +// this replaces the strict P1 requirement on those paths. +inline void CheckOmegaHScalarLagrangeLayout( + CoordinateSystem coordinate_system, + const std::shared_ptr& layout, + const char* context, const char* role) +{ + CheckOmegaHScalarSimplex2DLayout(coordinate_system, layout, context, role); + const int order = layout->GetOrder(); + if (order != 0 && order != 1) { + throw pcms_error(std::string(context) + ": " + role + + " space must be order-0 or order-1"); + } +} + +[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<2> GlobalFromBarycentric( + const MeshField::Vector3& barycentric_coord, + const Omega_h::Few, 3>& verts_coord) +{ + Omega_h::Vector<2> real_coords = {0.0, 0.0}; + for (int i = 0; i < 3; ++i) { + real_coords[0] += barycentric_coord[i] * verts_coord[i][0]; + real_coords[1] += barycentric_coord[i] * verts_coord[i][1]; + } + return real_coords; +} + +[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<3> EvaluateBarycentric( + const Omega_h::Vector<2>& point, + const r3d::Few, 3>& verts_coord) +{ + Omega_h::Few, 3> omegah_vector; + for (int i = 0; i < 3; ++i) { + omegah_vector[i][0] = verts_coord[i][0]; + omegah_vector[i][1] = verts_coord[i][1]; + } + return Omega_h::barycentric_from_global<2, 2>(point, omegah_vector); +} + +[[nodiscard]] OMEGA_H_INLINE int RemoveDuplicateVerticesAndFixLinks( + r3d::Polytope<2>& poly, const double tol = 1e-12) +{ + const int old_n = poly.nverts; + int new_n = 0; + + for (int i = 0; i < old_n; ++i) { + const auto& pi = poly.verts[i].pos; + bool dup = false; + for (int j = 0; j < new_n; ++j) { + const auto& pj = poly.verts[j].pos; + if (Kokkos::fabs(pi[0] - pj[0]) < tol && + Kokkos::fabs(pi[1] - pj[1]) < tol) { + dup = true; + break; + } + } + if (!dup) { + poly.verts[new_n] = poly.verts[i]; + ++new_n; + } + } + poly.nverts = new_n; + + if (new_n >= 3) { + double cx = 0.0; + double cy = 0.0; + for (int i = 0; i < new_n; ++i) { + cx += poly.verts[i].pos[0]; + cy += poly.verts[i].pos[1]; + } + cx /= new_n; + cy /= new_n; + + int order[r3d::MaxVerts<2>::value]; + for (int i = 0; i < new_n; ++i) { + order[i] = i; + } + + for (int i = 0; i < new_n - 1; ++i) { + for (int j = i + 1; j < new_n; ++j) { + const double a1 = Kokkos::atan2(poly.verts[order[i]].pos[1] - cy, + poly.verts[order[i]].pos[0] - cx); + const double a2 = Kokkos::atan2(poly.verts[order[j]].pos[1] - cy, + poly.verts[order[j]].pos[0] - cx); + if (a1 > a2) { + int t = order[i]; + order[i] = order[j]; + order[j] = t; + } + } + } + + r3d::Vertex<2> tmp[r3d::MaxVerts<2>::value]; + for (int i = 0; i < new_n; ++i) { + tmp[i] = poly.verts[order[i]]; + } + + for (int i = 0; i < new_n; ++i) { + poly.verts[i] = tmp[i]; + } + + for (int i = 0; i < new_n; ++i) { + const int prev = (i - 1 + new_n) % new_n; + const int next = (i + 1) % new_n; + poly.verts[i].pnbrs[0] = prev; + poly.verts[i].pnbrs[1] = next; + } + + double area = r3d::measure(poly); + if (area < 0.0) { + for (int i = 0; i < new_n / 2; ++i) { + r3d::Vertex<2> t = poly.verts[i]; + poly.verts[i] = poly.verts[new_n - 1 - i]; + poly.verts[new_n - 1 - i] = t; + } + for (int i = 0; i < new_n; ++i) { + const int prev = (i - 1 + new_n) % new_n; + const int next = (i + 1) % new_n; + poly.verts[i].pnbrs[0] = prev; + poly.verts[i].pnbrs[1] = next; + } + } + } else { + for (int i = 0; i < new_n; ++i) { + poly.verts[i].pnbrs[0] = -1; + poly.verts[i].pnbrs[1] = -1; + } + } + + return new_n; +} + +template +OMEGA_H_INLINE void ForEachIntersectionSubtriangle( + const int elm, const IntersectionResults& intersection, + const Omega_h::Reals& tgt_coords, const Omega_h::Reals& src_coords, + const Omega_h::LOs& tgt_faces2nodes, const Omega_h::LOs& src_faces2nodes, + TriangleOp&& op) +{ + auto tgt_elm_vert_coords = + get_vert_coords_of_elem(tgt_coords, tgt_faces2nodes, elm); + const int start = intersection.tgt2src_offsets[elm]; + const int end = intersection.tgt2src_offsets[elm + 1]; + + for (int i = start; i < end; ++i) { + const int current_src_elm = intersection.tgt2src_indices[i]; + auto src_elm_vert_coords = + get_vert_coords_of_elem(src_coords, src_faces2nodes, current_src_elm); + r3d::Polytope<2> poly; + r3d::intersect_simplices(poly, tgt_elm_vert_coords, src_elm_vert_coords); + auto nverts = RemoveDuplicateVerticesAndFixLinks(poly, 1e-12); + auto poly_area = r3d::measure(poly); + + for (int j = 1; j < nverts - 1; ++j) { + auto& p0 = poly.verts[0].pos; + auto& p1 = poly.verts[j].pos; + auto& p2 = poly.verts[j + 1].pos; + + Omega_h::Few, 3> tri_coords; + tri_coords[0] = {p0[0], p0[1]}; + tri_coords[1] = {p1[0], p1[1]}; + tri_coords[2] = {p2[0], p2[1]}; + + Omega_h::Few, 2> basis; + basis[0] = tri_coords[1] - tri_coords[0]; + basis[1] = tri_coords[2] - tri_coords[0]; + + Omega_h::Real area = + Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + + const double eps_area = abs_tol + rel_tol * poly_area; + if (area <= eps_area) { + continue; + } + + op(tri_coords, tgt_elm_vert_coords, src_elm_vert_coords, current_src_elm, + area); + } + } +} + +// Barycentric integration points and weights for a reference triangle, taken +// from MeshField's predefined triangle quadrature rules and staged on device +// for use in element integration kernels. +// +// MeshField::getIntegrationPoints returns a host std::vector, which cannot be +// dereferenced inside a device kernel, so the (tiny) rule is copied into device +// Kokkos views once at construction. +// +// The quadrature order is a runtime argument because the required polynomial +// accuracy depends on the source and target element orders (degree = +// source_order + target_order), which are only known at construction. +struct IntegrationData +{ + Kokkos::View bary_coords; // barycentric coordinates + Kokkos::View weights; // quadrature weights + + explicit IntegrationData(int order) + { + auto ip_vec = MeshField::getIntegrationPoints(order); + const std::size_t num_ip = ip_vec.size(); + + bary_coords = Kokkos::View("bary_coords", num_ip); + weights = Kokkos::View("weights", num_ip); + + auto bary_coords_host = Kokkos::create_mirror_view(bary_coords); + auto weights_host = Kokkos::create_mirror_view(weights); + for (std::size_t i = 0; i < num_ip; ++i) { + bary_coords_host(i) = ip_vec[i].param; + weights_host(i) = ip_vec[i].weight; + } + Kokkos::deep_copy(bary_coords, bary_coords_host); + Kokkos::deep_copy(weights, weights_host); + } + + int size() const { return bary_coords.extent(0); } +}; + +// Target Lagrange basis on a triangle, parameterized by element order, for the +// conservative-projection RHS assembly. Order 0 is a single element-constant +// DOF; order 1 is the three vertex (barycentric) DOFs. Higher orders slot in as +// additional specializations, kept in lock-step with element_dispatch.h. +// +// Each specialization provides, for a target element `elm` with local vertex +// ids `verts` and vertex coordinates `tgt_verts`: +// ndof number of local target DOFs +// Index(...) active PETSc row for local dof k +// Values(pt, ...) basis values at the (global) integration point pt +template +struct TargetTriBasis; + +template <> +struct TargetTriBasis<0> +{ + static constexpr int ndof = 1; + + template + KOKKOS_INLINE_FUNCTION static LO Index(const Permutation& permutation, + int elm, + const Omega_h::Few&, + int /*k*/) + { + return permutation(elm); + } + + KOKKOS_INLINE_FUNCTION static void Values( + const Omega_h::Vector<2>&, const Omega_h::Few, 3>&, + Omega_h::Real out[ndof]) + { + out[0] = 1.0; + } +}; + +template <> +struct TargetTriBasis<1> +{ + static constexpr int ndof = 3; + + template + KOKKOS_INLINE_FUNCTION static LO Index( + const Permutation& permutation, int /*elm*/, + const Omega_h::Few& verts, int k) + { + return permutation(verts[k]); + } + + // P1 basis functions are the barycentric coordinates of the target element + // evaluated at the (global) integration point. + KOKKOS_INLINE_FUNCTION static void Values( + const Omega_h::Vector<2>& pt, + const Omega_h::Few, 3>& tgt_verts, + Omega_h::Real out[ndof]) + { + const auto bary = Omega_h::barycentric_from_global<2, 2>(pt, tgt_verts); + out[0] = bary[0]; + out[1] = bary[1]; + out[2] = bary[2]; + } +}; + +} // namespace pcms::detail + +#endif // PCMS_TRANSFER_OMEGA_H_FORM_INTEGRATOR_UTILS_HPP diff --git a/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp b/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp new file mode 100644 index 00000000..1cd0e9b3 --- /dev/null +++ b/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp @@ -0,0 +1,275 @@ +#include "pcms/transfer/omega_h_intersection_rhs_integrator.hpp" +#include "pcms/field/element_dispatch.h" +#include "pcms/transfer/omega_h_form_integrator_utils.hpp" +#include "pcms/transfer/petsc_utils.hpp" +#include "pcms/utility/assert.h" +#include +#include +#include +#include + +namespace pcms +{ + +namespace +{ + +// Results of the two-pass intersection kernel; the constructor moves these into +// the integrator's members. +struct Data +{ + Kokkos::View coords; + Kokkos::View node_gids; + Kokkos::View coeffs; + int ndof_per_elem = 0; + PetscInt num_target_dofs = 0; +}; + +template +Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, + const OmegaHLagrangeLayout& target_layout, int quad_order); + +Data BuildData(const std::shared_ptr& source_layout, + CoordinateSystem source_coordinate_system, + const std::shared_ptr& target_layout, + CoordinateSystem target_coordinate_system) +{ + detail::CheckOmegaHScalarLagrangeLayout( + source_coordinate_system, source_layout, "OmegaHIntersectionRHSIntegrator", + "source"); + detail::CheckOmegaHScalarLagrangeLayout( + target_coordinate_system, target_layout, "OmegaHIntersectionRHSIntegrator", + "target"); + + // The integrand f_src * phi_target has polynomial degree source_order + + // target_order on each intersection subtriangle; integrate it exactly (with a + // 1-point floor so a P0->P0 pair still gets a valid rule). + const int quad_order = + std::max(1, source_layout->GetOrder() + target_layout->GetOrder()); + + return detail::DispatchByOrder(target_layout->GetOrder(), [&](auto order_c) { + constexpr int TgtOrder = decltype(order_c)::value; + return BuildDataImpl(*source_layout, *target_layout, quad_order); + }); +} + +template +Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, + const OmegaHLagrangeLayout& target_layout, int quad_order) +{ + using Basis = detail::TargetTriBasis; + constexpr int ndof = Basis::ndof; + + Omega_h::Mesh& source_mesh = source_layout.GetMesh(); + Omega_h::Mesh& target_mesh = target_layout.GetMesh(); + + const auto intersections = intersectTargets(source_mesh, target_mesh); + + const auto& tgt_coords = target_mesh.coords(); + const auto& tgt_faces2nodes = + target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& src_coords = source_mesh.coords(); + const auto& src_faces2nodes = + source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + + detail::IntegrationData ip_data(quad_order); + const int npts = ip_data.size(); + auto bary_coords = ip_data.bary_coords; // device view + auto weights = ip_data.weights; // device view + + const auto global_to_local = target_layout.GetGlobalToLocalPermutation(); + + const Omega_h::LOs tgt2src_offsets = intersections.tgt2src_offsets; + const Omega_h::LOs tgt2src_indices = intersections.tgt2src_indices; + const int nelems = target_mesh.nelems(); + + // Pass 1: count integration points per target element. + Omega_h::Write ip_counts(nelems, 0, "rhs_ip_counts"); + Kokkos::parallel_for( + "rhs_count", nelems, KOKKOS_LAMBDA(int elm) { + int count = 0; + detail::ForEachIntersectionSubtriangle( + elm, {tgt2src_offsets, tgt2src_indices}, tgt_coords, src_coords, + tgt_faces2nodes, src_faces2nodes, + [&](const Omega_h::Few, 3>&, + const r3d::Few, 3>&, + const r3d::Few, 3>&, int, + Omega_h::Real) { count += npts; }); + ip_counts[elm] = count; + }); + Kokkos::fence(); + + const auto ip_offsets = Omega_h::offset_scan( + Omega_h::Read(ip_counts), "rhs_ip_offsets"); + const int num_pts = static_cast(ip_offsets.last()); + + // Pass 2: fill coords, node_gids, and coeffs on device. node_gids/coeffs hold + // ndof (target DOFs per element) entries per integration point. + Kokkos::View coords("rhs_coords", num_pts, 2); + Kokkos::View node_gids( + "rhs_node_gids", static_cast(num_pts) * ndof); + Kokkos::View coeffs( + "rhs_coeffs", static_cast(num_pts) * ndof); + + Kokkos::parallel_for( + "rhs_fill", nelems, KOKKOS_LAMBDA(int elm) { + const auto tgt_verts = Omega_h::gather_verts<3>(tgt_faces2nodes, elm); + const Omega_h::Matrix<2, 3> tgt_vert_mat = + Omega_h::gather_vectors<3, 2>(tgt_coords, tgt_verts); + Omega_h::Few, 3> tgt_omh; + for (int i = 0; i < 3; ++i) + tgt_omh[i] = {tgt_vert_mat[i][0], tgt_vert_mat[i][1]}; + + int ip_local = 0; + const int offset = ip_offsets[elm]; + + detail::ForEachIntersectionSubtriangle( + elm, {tgt2src_offsets, tgt2src_indices}, tgt_coords, src_coords, + tgt_faces2nodes, src_faces2nodes, + [&](const Omega_h::Few, 3>& tri, + const r3d::Few, 3>&, + const r3d::Few, 3>&, int, Omega_h::Real area) { + for (int ip_idx = 0; ip_idx < npts; ++ip_idx) { + const auto bary = bary_coords(ip_idx); + const double w = weights(ip_idx); + const auto pt = detail::GlobalFromBarycentric(bary, tri); + + Omega_h::Real basis[ndof]; + Basis::Values(pt, tgt_omh, basis); + + const int global_ip = offset + ip_local; + coords(global_ip, 0) = pt[0]; + coords(global_ip, 1) = pt[1]; + for (int k = 0; k < ndof; ++k) { + node_gids(global_ip * ndof + k) = static_cast( + Basis::Index(global_to_local, elm, tgt_verts, k)); + coeffs(global_ip * ndof + k) = basis[k] * w * 2.0 * area; + } + ++ip_local; + } + }); + }); + Kokkos::fence(); + + Data d; + d.coords = std::move(coords); + d.node_gids = std::move(node_gids); + d.coeffs = std::move(coeffs); + d.ndof_per_elem = ndof; + d.num_target_dofs = + static_cast(target_layout.GetNumOwnedDofHolder()); + return d; +} + +} // namespace + +// --------------------------------------------------------------------------- +// OmegaHIntersectionRHSIntegrator constructors +// --------------------------------------------------------------------------- + +OmegaHIntersectionRHSIntegrator::OmegaHIntersectionRHSIntegrator( + const FunctionSpace& source_space, const FunctionSpace& target_space) + : OmegaHIntersectionRHSIntegrator( + std::dynamic_pointer_cast( + source_space.GetLayout()), + source_space.GetCoordinateSystem(), + std::dynamic_pointer_cast( + target_space.GetLayout()), + target_space.GetCoordinateSystem()) +{ +} + +OmegaHIntersectionRHSIntegrator::OmegaHIntersectionRHSIntegrator( + std::shared_ptr source_layout, + CoordinateSystem source_coordinate_system, + std::shared_ptr target_layout, + CoordinateSystem target_coordinate_system) +{ + Data data = BuildData(source_layout, source_coordinate_system, target_layout, + target_coordinate_system); + point_set_ = IntegrationPointSet( + CoordinateSystem::Cartesian, std::move(data.coords)); + node_gids_ = std::move(data.node_gids); + coeffs_ = std::move(data.coeffs); + ndof_per_elem_ = data.ndof_per_elem; + + const PetscInt nnz = static_cast(node_gids_.extent(0)); + PetscErrorCode ierr = + createSeqVec(PETSC_COMM_WORLD, data.num_target_dofs, &vec_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + // VecSetPreallocationCOO takes the COO indices on the host + // TODO: ask Todd/PETSc folks if there is a better way to do this for GPU + // support + auto node_gids_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, node_gids_); + ierr = VecSetPreallocationCOO(vec_, nnz, node_gids_host.data()); + CHKERRABORT(PETSC_COMM_WORLD, ierr); +} + +OmegaHIntersectionRHSIntegrator::~OmegaHIntersectionRHSIntegrator() +{ + if (vec_) { + VecDestroy(&vec_); + } +} + +// --------------------------------------------------------------------------- +// OmegaHIntersectionRHSIntegrator public interface +// --------------------------------------------------------------------------- + +const IntegrationPointSet& +OmegaHIntersectionRHSIntegrator::GetIntegrationPoints() const noexcept +{ + return point_set_; +} + +Vec OmegaHIntersectionRHSIntegrator::GetVector() const noexcept +{ + return vec_; +} + +void OmegaHIntersectionRHSIntegrator::Assemble( + Rank2View sampled_values) +{ + const int ndof = ndof_per_elem_; + const std::size_t num_pts = + static_cast(node_gids_.extent(0)) / ndof; + PCMS_ALWAYS_ASSERT(static_cast(sampled_values.extent(0)) == + num_pts); + PCMS_ALWAYS_ASSERT(sampled_values.extent(1) >= 1); + + PetscErrorCode ierr = VecZeroEntries(vec_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + auto sv = Kokkos::View>( + sampled_values.data_handle(), sampled_values.extent(0), + sampled_values.extent(1)); + Kokkos::View coo_vals("rhs_coo_vals", + num_pts * ndof); + auto coeffs = coeffs_; + Kokkos::parallel_for( + "rhs_coo_vals", static_cast(num_pts), KOKKOS_LAMBDA(int i) { + const PetscScalar f = static_cast(sv(i, 0)); + for (int j = 0; j < ndof; ++j) { + coo_vals(i * ndof + j) = + static_cast(coeffs(i * ndof + j)) * f; + } + }); + + ierr = VecSetValuesCOO(vec_, coo_vals.data(), ADD_VALUES); + CHKERRABORT(PETSC_COMM_WORLD, ierr); +} + +// --------------------------------------------------------------------------- +// Builder +// --------------------------------------------------------------------------- + +std::unique_ptr BuildOmegaHConservativeRHSIntegrator( + const FunctionSpace& source_space, const FunctionSpace& target_space) +{ + return std::make_unique(source_space, + target_space); +} + +} // namespace pcms diff --git a/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp b/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp new file mode 100644 index 00000000..f6a3a9d5 --- /dev/null +++ b/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp @@ -0,0 +1,68 @@ +#ifndef PCMS_TRANSFER_OMEGA_H_INTERSECTION_RHS_INTEGRATOR_HPP +#define PCMS_TRANSFER_OMEGA_H_INTERSECTION_RHS_INTEGRATOR_HPP + +#include "pcms/field/function_space.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/transfer/integration_point_set.hpp" +#include "pcms/transfer/linear_form_integrator.hpp" +#include "pcms/transfer/mesh_intersection.hpp" +#include +#include + +namespace pcms +{ + +// Conservative-projection RHS integrator for Lagrange spaces on Omega_h 2D +// simplex meshes. Source and target orders are handled independently: the +// source enters only through pointwise samples (any supported order), while the +// target order selects the basis and DOF layout (P0: one element-constant DOF; +// P1: three vertex DOFs). The quadrature order is source_order + target_order. +// +// Construction validates both spaces, computes mesh intersections, and runs a +// two-pass device kernel (count then fill) to build the quadrature data and +// allocate the owned RHS vector sized to the target DOF count. +// +// Assemble(sampled_values): +// Zeros the owned vector, then for each integration point i and local target +// DOF j: +// contribution = stored_coeff[i][j] * sampled_values(i, 0) +// Scatter-adds via VecSetValuesCOO with ADD_VALUES. +// GetVector() returns the assembled vector (non-owning handle). +class OmegaHIntersectionRHSIntegrator : public LinearFormIntegrator +{ +public: + OmegaHIntersectionRHSIntegrator(const FunctionSpace& source_space, + const FunctionSpace& target_space); + OmegaHIntersectionRHSIntegrator( + std::shared_ptr source_layout, + CoordinateSystem source_coordinate_system, + std::shared_ptr target_layout, + CoordinateSystem target_coordinate_system); + ~OmegaHIntersectionRHSIntegrator(); + + Vec GetVector() const noexcept override; + + const IntegrationPointSet& GetIntegrationPoints() + const noexcept override; + + void Assemble( + Rank2View sampled_values) override; + +private: + Vec vec_ = nullptr; + IntegrationPointSet point_set_; + Kokkos::View + node_gids_; // COO idx, num_pts*ndof + Kokkos::View coeffs_; // weights, num_pts*ndof + int ndof_per_elem_ = 0; // target DOFs/element +}; + +// Builds an OmegaHIntersectionRHSIntegrator for conservative L2 projection. +// Both spaces must use OmegaHLagrangeLayout, scalar, Cartesian, order-0 or +// order-1 (source and target orders may differ). +std::unique_ptr BuildOmegaHConservativeRHSIntegrator( + const FunctionSpace& source_space, const FunctionSpace& target_space); + +} // namespace pcms + +#endif // PCMS_TRANSFER_OMEGA_H_INTERSECTION_RHS_INTEGRATOR_HPP diff --git a/src/pcms/transfer/omega_h_mass_integrator.cpp b/src/pcms/transfer/omega_h_mass_integrator.cpp new file mode 100644 index 00000000..29cd90e2 --- /dev/null +++ b/src/pcms/transfer/omega_h_mass_integrator.cpp @@ -0,0 +1,173 @@ +#include "pcms/transfer/omega_h_mass_integrator.hpp" +#include "pcms/transfer/mass_matrix_integrator.hpp" +#include "pcms/transfer/omega_h_form_integrator_utils.hpp" +#include "pcms/transfer/petsc_utils.hpp" +#include "pcms/utility/assert.h" +#include "pcms/utility/memory_spaces.h" +#include +#include +#include +#include +#include +#include + +namespace pcms +{ + +namespace +{ + +// Fills the diagonal COO entries of a P0 (piecewise-constant) mass matrix: one +// entry per element on its own DOF, valued at the element area. +void FillP0MassCoo( + int nelems, const Omega_h::Reals& coords, const Omega_h::LOs& faces2nodes, + const Kokkos::View& global_to_local, + const Kokkos::View& coo_rows, + const Kokkos::View& coo_cols, + const Kokkos::View& vals) +{ + Kokkos::parallel_for( + "mass_p0_diag", nelems, KOKKOS_LAMBDA(int e) { + const auto verts = Omega_h::gather_verts<3>(faces2nodes, e); + const Omega_h::Matrix<2, 3> vm = + Omega_h::gather_vectors<3, 2>(coords, verts); + Omega_h::Few, 2> basis; + basis[0] = vm[1] - vm[0]; + basis[1] = vm[2] - vm[0]; + const Omega_h::Real area = + Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + const PetscInt g = static_cast(global_to_local(e)); + coo_rows(e) = g; + coo_cols(e) = g; + vals(e) = static_cast(area); + }); +} + +// Fills the 3x3-block COO sparsity pattern of a P1 (linear) mass matrix: each +// element contributes a dense block coupling its three vertex DOFs. +void FillP1MassCooPattern( + int nelems, const Omega_h::LOs& faces2nodes, + const Kokkos::View& global_to_local, + const Kokkos::View& coo_rows, + const Kokkos::View& coo_cols) +{ + Kokkos::parallel_for( + "mass_coo_pattern", nelems, KOKKOS_LAMBDA(int e) { + const auto verts = Omega_h::gather_verts<3>(faces2nodes, e); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + const int idx = e * 9 + i * 3 + j; + coo_rows(idx) = static_cast(global_to_local(verts[i])); + coo_cols(idx) = static_cast(global_to_local(verts[j])); + } + } + }); +} + +} // namespace + +OmegaHMassIntegrator::OmegaHMassIntegrator(const FunctionSpace& target_space) + : OmegaHMassIntegrator(std::dynamic_pointer_cast( + target_space.GetLayout()), + target_space.GetCoordinateSystem()) +{ +} + +OmegaHMassIntegrator::OmegaHMassIntegrator( + std::shared_ptr target_layout, + CoordinateSystem coordinate_system) +{ + detail::CheckOmegaHScalarLagrangeLayout(coordinate_system, target_layout, + "OmegaHMassIntegrator", "target"); + + Omega_h::Mesh& mesh = target_layout->GetMesh(); + const auto global_to_local = target_layout->GetGlobalToLocalPermutation(); + const PetscInt num_dofs = + static_cast(target_layout->GetNumOwnedDofHolder()); + const int nelems = mesh.nelems(); + + if (target_layout->GetOrder() == 0) { + // P0 target: piecewise-constant basis functions have disjoint support, so + // the mass matrix is diagonal with M_ee = area(e). One COO entry per + // element on its own (element-id) diagonal. + const auto& coords = mesh.coords(); + const auto& faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + + const PetscInt nnz = static_cast(nelems); + Kokkos::View coo_rows("mass_coo_rows", nnz); + Kokkos::View coo_cols("mass_coo_cols", nnz); + Kokkos::View vals("mass_vals", nnz); + FillP0MassCoo(nelems, coords, faces2nodes, global_to_local, coo_rows, + coo_cols, vals); + + PetscErrorCode ierr = + createSeqAIJMat(PETSC_COMM_WORLD, num_dofs, num_dofs, 0, nullptr, &mat_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + auto coo_rows_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_rows); + auto coo_cols_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_cols); + ierr = MatSetPreallocationCOO(mat_, nnz, coo_rows_host.data(), + coo_cols_host.data()); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + ierr = MatSetValuesCOO(mat_, vals.data(), INSERT_VALUES); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + return; + } + + // P1 target: consistent mass matrix assembled from MeshField per-element 3x3 + // blocks. (Higher MeshField orders extend this branch via + // getTriangleElement.) + MeshField::OmegahMeshField + omf(mesh); + auto coordField = omf.getCoordField(); + const auto [shp, map] = MeshField::Omegah::getTriangleElement<1>(mesh); + MeshField::FieldElement coordFe(mesh.nelems(), coordField, shp, map); + auto elm_mass_dev = buildElementMassMatrix(mesh, coordFe); + + // Build COO sparsity pattern on device: each element contributes a 3x3 block. + const auto& faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + + const PetscInt nnz = static_cast(nelems) * 9; + Kokkos::View coo_rows("mass_coo_rows", nnz); + Kokkos::View coo_cols("mass_coo_cols", nnz); + FillP1MassCooPattern(nelems, faces2nodes, global_to_local, coo_rows, + coo_cols); + + // Create sparse matrix, preallocate with COO pattern, then bulk-set values. + // elm_mass_dev is in the same element-major order as coo_rows/coo_cols, so + // it can be passed directly to MatSetValuesCOO — no host copy needed. + PetscErrorCode ierr = + createSeqAIJMat(PETSC_COMM_WORLD, num_dofs, num_dofs, 0, nullptr, &mat_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + auto coo_rows_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_rows); + auto coo_cols_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_cols); + ierr = MatSetPreallocationCOO(mat_, nnz, coo_rows_host.data(), + coo_cols_host.data()); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + ierr = MatSetValuesCOO(mat_, elm_mass_dev.data(), INSERT_VALUES); + CHKERRABORT(PETSC_COMM_WORLD, ierr); +} + +OmegaHMassIntegrator::~OmegaHMassIntegrator() +{ + if (mat_) { + MatDestroy(&mat_); + } +} + +Mat OmegaHMassIntegrator::GetMatrix() const noexcept +{ + return mat_; +} + +std::unique_ptr BuildOmegaHMassIntegrator( + const FunctionSpace& target_space) +{ + return std::make_unique(target_space); +} + +} // namespace pcms diff --git a/src/pcms/transfer/omega_h_mass_integrator.hpp b/src/pcms/transfer/omega_h_mass_integrator.hpp new file mode 100644 index 00000000..54cc7a66 --- /dev/null +++ b/src/pcms/transfer/omega_h_mass_integrator.hpp @@ -0,0 +1,45 @@ +#ifndef PCMS_TRANSFER_OMEGA_H_MASS_INTEGRATOR_HPP +#define PCMS_TRANSFER_OMEGA_H_MASS_INTEGRATOR_HPP + +#include "pcms/field/function_space.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/transfer/bilinear_form_integrator.hpp" +#include +#include + +namespace pcms +{ + +// Mass-matrix bilinear form integrator for Lagrange spaces on Omega_h 2D +// simplex meshes. Supports order-0 (diagonal, M_ee = area(e)) and order-1 +// (consistent 3x3 element blocks) target spaces. +// +// The matrix is assembled once at construction into a PETSc sparse AIJ matrix. +// Sparsity is derived directly from the element node GIDs — no separate COO +// utility is needed. The integrator owns the matrix; callers receive a +// non-owning handle via GetMatrix(). PETSc reference-counts the matrix, so a +// KSP that calls KSPSetOperators(GetMatrix()) safely outlives this object. +class OmegaHMassIntegrator : public BilinearFormIntegrator +{ +public: + explicit OmegaHMassIntegrator(const FunctionSpace& target_space); + OmegaHMassIntegrator( + std::shared_ptr target_layout, + CoordinateSystem coordinate_system); + ~OmegaHMassIntegrator(); + + Mat GetMatrix() const noexcept override; + +private: + Mat mat_ = nullptr; +}; + +// Builds an OmegaHMassIntegrator for the given target space. +// target_space must use OmegaHLagrangeLayout, scalar, Cartesian, order-0 or +// order-1. +std::unique_ptr BuildOmegaHMassIntegrator( + const FunctionSpace& target_space); + +} // namespace pcms + +#endif // PCMS_TRANSFER_OMEGA_H_MASS_INTEGRATOR_HPP diff --git a/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp b/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp new file mode 100644 index 00000000..d3b7ea9c --- /dev/null +++ b/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp @@ -0,0 +1,204 @@ +#include "pcms/transfer/omega_h_mc_rhs_integrator.hpp" +#include "pcms/transfer/omega_h_form_integrator_utils.hpp" +#include "pcms/transfer/petsc_utils.hpp" +#include "pcms/utility/assert.h" +#include +#include +#include + +namespace pcms +{ + +namespace +{ + +// Maps a uniform point on the unit square to uniform barycentric coordinates +// Shape Distributions (ACM Transactions on Graphics, Vol. 21, No. 4, October +// 2002.) page 814 Eq 1 +KOKKOS_INLINE_FUNCTION Omega_h::Vector<3> UniformTriangleBarycentric(Real u, + Real v) +{ + const Real s = Kokkos::sqrt(u); + return {1.0 - s, s * (1.0 - v), s * v}; +} + +// Fills coords, node_gids, and coeffs for all samples of one target element. +// unit_sample_at(s, u, v) provides the s-th unit-square sample. +template +KOKKOS_INLINE_FUNCTION void FillElementSamples( + const int elm, const int samples_per_element, + const Omega_h::Reals& mesh_coords, const Omega_h::LOs& faces2nodes, + const Kokkos::View& global_to_local, + const Kokkos::View& coords, + const Kokkos::View& node_gids, + const Kokkos::View& coeffs, + const UnitSampleAt& unit_sample_at) +{ + const auto verts = Omega_h::gather_verts<3>(faces2nodes, elm); + const auto vert_coords = Omega_h::gather_vectors<3, 2>(mesh_coords, verts); + Omega_h::Few, 2> basis; + basis[0] = vert_coords[1] - vert_coords[0]; + basis[1] = vert_coords[2] - vert_coords[0]; + const Real area = Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + const Real weight = area / samples_per_element; + + for (int s = 0; s < samples_per_element; ++s) { + Real u = 0.0; + Real v = 0.0; + unit_sample_at(s, u, v); + const auto bary = UniformTriangleBarycentric(u, v); + + const int i = elm * samples_per_element + s; + Real x = 0.0; + Real y = 0.0; + for (int k = 0; k < 3; ++k) { + x += bary[k] * vert_coords[k][0]; + y += bary[k] * vert_coords[k][1]; + node_gids(i * 3 + k) = static_cast(global_to_local(verts[k])); + coeffs(i * 3 + k) = bary[k] * weight; + } + coords(i, 0) = x; + coords(i, 1) = y; + } +} + +// Samples samples_per_element from a uniform random distribution over the +// target element, writing their coordinates, node GIDs, and coefficients. +void FillElementSamples( + int nelems, int samples_per_element, const Omega_h::Reals& mesh_coords, + const Omega_h::LOs& faces2nodes, + const Kokkos::View& global_to_local, + const Kokkos::View& coords, + const Kokkos::View& node_gids, + const Kokkos::View& coeffs, uint64_t seed) +{ + Kokkos::Random_XorShift64_Pool pool(seed); + Kokkos::parallel_for( + "mc_rhs_fill_random", Kokkos::RangePolicy(0, nelems), + KOKKOS_LAMBDA(int elm) { + auto gen = pool.get_state(); + FillElementSamples(elm, samples_per_element, mesh_coords, faces2nodes, + global_to_local, coords, node_gids, coeffs, + [&](int /*s*/, Real& u, Real& v) { + u = gen.drand(); + v = gen.drand(); + }); + pool.free_state(gen); + }); + Kokkos::fence(); +} + +} // namespace + +OmegaHMonteCarloRHSIntegrator::OmegaHMonteCarloRHSIntegrator( + const FunctionSpace& target_space, int samples_per_element, + MonteCarloSampling sampling, uint64_t seed) + : OmegaHMonteCarloRHSIntegrator( + std::dynamic_pointer_cast( + target_space.GetLayout()), + target_space.GetCoordinateSystem(), samples_per_element, sampling, seed) +{ +} + +OmegaHMonteCarloRHSIntegrator::OmegaHMonteCarloRHSIntegrator( + std::shared_ptr target_layout, + CoordinateSystem target_coordinate_system, int samples_per_element, + MonteCarloSampling /*sampling*/, uint64_t seed) +{ + detail::CheckOmegaHScalarP1Layout(target_coordinate_system, target_layout, + "OmegaHMonteCarloRHSIntegrator", "target"); + if (samples_per_element <= 0) { + throw pcms_error( + "OmegaHMonteCarloRHSIntegrator: samples_per_element must be positive"); + } + + Omega_h::Mesh& mesh = target_layout->GetMesh(); + + const int nelems = mesh.nelems(); + const int num_samples = nelems * samples_per_element; + const auto mesh_coords = mesh.coords(); + const auto faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto global_to_local = target_layout->GetGlobalToLocalPermutation(); + + Kokkos::View coords("mc_rhs_coords", num_samples, + 2); + Kokkos::View node_gids( + "mc_rhs_node_gids", static_cast(num_samples) * 3); + Kokkos::View coeffs( + "mc_rhs_coeffs", static_cast(num_samples) * 3); + + FillElementSamples(nelems, samples_per_element, mesh_coords, faces2nodes, + global_to_local, coords, node_gids, coeffs, seed); + + point_set_ = IntegrationPointSet( + CoordinateSystem::Cartesian, std::move(coords)); + node_gids_ = std::move(node_gids); + coeffs_ = std::move(coeffs); + + const PetscInt nnz = static_cast(node_gids_.extent(0)); + PetscErrorCode ierr = + createSeqVec(PETSC_COMM_WORLD, static_cast(mesh.nverts()), &vec_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + // VecSetPreallocationCOO takes the COO indices on the host + // TODO: ask Todd/PETSc folks if there is a better way to do this for GPU + // support + auto node_gids_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, node_gids_); + ierr = VecSetPreallocationCOO(vec_, nnz, node_gids_host.data()); + CHKERRABORT(PETSC_COMM_WORLD, ierr); +} + +OmegaHMonteCarloRHSIntegrator::~OmegaHMonteCarloRHSIntegrator() +{ + if (vec_) { + VecDestroy(&vec_); + } +} + +// --------------------------------------------------------------------------- +// OmegaHMonteCarloRHSIntegrator public interface +// --------------------------------------------------------------------------- + +const IntegrationPointSet& +OmegaHMonteCarloRHSIntegrator::GetIntegrationPoints() const noexcept +{ + return point_set_; +} + +Vec OmegaHMonteCarloRHSIntegrator::GetVector() const noexcept +{ + return vec_; +} + +void OmegaHMonteCarloRHSIntegrator::Assemble( + Rank2View sampled_values) +{ + const std::size_t num_samples = + static_cast(node_gids_.extent(0) / 3); + PCMS_ALWAYS_ASSERT(static_cast(sampled_values.extent(0)) == + num_samples); + PCMS_ALWAYS_ASSERT(sampled_values.extent(1) >= 1); + + PetscErrorCode ierr = VecZeroEntries(vec_); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + auto sv = Kokkos::View>( + sampled_values.data_handle(), sampled_values.extent(0), + sampled_values.extent(1)); + Kokkos::View coo_vals("mc_rhs_coo_vals", + num_samples * 3); + auto coeffs = coeffs_; + Kokkos::parallel_for( + "mc_rhs_coo_vals", static_cast(num_samples), KOKKOS_LAMBDA(int i) { + const PetscScalar f = static_cast(sv(i, 0)); + for (int j = 0; j < 3; ++j) { + coo_vals(i * 3 + j) = static_cast(coeffs(i * 3 + j)) * f; + } + }); + + ierr = VecSetValuesCOO(vec_, coo_vals.data(), ADD_VALUES); + CHKERRABORT(PETSC_COMM_WORLD, ierr); +} + +} // namespace pcms diff --git a/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp b/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp new file mode 100644 index 00000000..68fad801 --- /dev/null +++ b/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp @@ -0,0 +1,63 @@ +#ifndef PCMS_TRANSFER_OMEGA_H_MC_RHS_INTEGRATOR_HPP +#define PCMS_TRANSFER_OMEGA_H_MC_RHS_INTEGRATOR_HPP + +#include "pcms/field/function_space.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/transfer/integration_point_set.hpp" +#include "pcms/transfer/linear_form_integrator.hpp" +#include "pcms/transfer/monte_carlo_sampling.hpp" +#include +#include +#include + +namespace pcms +{ + +// Monte Carlo RHS integrator for conservative L2 projection onto order-1 +// Lagrange spaces on Omega_h 2D simplex meshes. +// +// Instead of intersection-based quadrature, each load-vector entry +// b_j = \int phi_j f dx +// is estimated per target element T from a uniform sample x_1..x_N over T: +// b_j ~= (|T| / N) * sum_i phi_j(x_i) f(x_i) +// Unit-square samples (pseudo-random) are mapped to each triangle +// with the area-preserving sqrt parametrization. Sample locations are fixed +// at construction and exposed through GetIntegrationPoints(); Assemble() +// consumes source-field values sampled there, exactly like +// OmegaHIntersectionRHSIntegrator, so this drops into +// GalerkinProjectionSolver::Solve. +class OmegaHMonteCarloRHSIntegrator : public LinearFormIntegrator +{ +public: + OmegaHMonteCarloRHSIntegrator(const FunctionSpace& target_space, + int samples_per_element, + MonteCarloSampling sampling, + uint64_t seed = 8675309); + OmegaHMonteCarloRHSIntegrator( + std::shared_ptr target_layout, + CoordinateSystem target_coordinate_system, int samples_per_element, + MonteCarloSampling sampling, uint64_t seed = 8675309); + ~OmegaHMonteCarloRHSIntegrator(); + + OmegaHMonteCarloRHSIntegrator(const OmegaHMonteCarloRHSIntegrator&) = delete; + OmegaHMonteCarloRHSIntegrator& operator=( + const OmegaHMonteCarloRHSIntegrator&) = delete; + + Vec GetVector() const noexcept override; + + const IntegrationPointSet& GetIntegrationPoints() + const noexcept override; + + void Assemble( + Rank2View sampled_values) override; + +private: + Vec vec_ = nullptr; + IntegrationPointSet point_set_; + Kokkos::View node_gids_; // COO indices + Kokkos::View coeffs_; // basis * |T| / N +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_OMEGA_H_MC_RHS_INTEGRATOR_HPP diff --git a/src/pcms/transfer/transfer_method.hpp b/src/pcms/transfer/transfer_method.hpp new file mode 100644 index 00000000..c63706b7 --- /dev/null +++ b/src/pcms/transfer/transfer_method.hpp @@ -0,0 +1,97 @@ +#ifndef PCMS_TRANSFER_METHOD_H +#define PCMS_TRANSFER_METHOD_H + +#include "pcms/transfer/transfer_operator.hpp" +#include "pcms/transfer/copy.h" +#include "pcms/transfer/interpolator.h" +#include "pcms/transfer/omega_h_conservative_projection.hpp" +#include "pcms/transfer/omega_h_control_variate_projection.hpp" +#include "pcms/transfer/monte_carlo_sampling.hpp" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/utility/types.h" +#include +#include + +namespace pcms +{ + +class FunctionSpace; + +// A "transfer method" is a parameter-carrying recipe: it owns the +// method-specific parameters, and its +// +// std::unique_ptr> Build(const FunctionSpace& source, +// const FunctionSpace& target) +// const +// +// member turns a space pair into a transfer operator. Selecting or comparing +// methods is then a one-line change at the AddTransfer call site, e.g. +// AddTransfer(src, tgt, +// method::ConservativeMonteCarlo{.samples_per_element = 64}); +// +// Methods are aggregates (no polymorphic base) so their parameters can be set +// with designated initializers; they are matched by AddTransfer as a +// duck-typed concept rather than a base class. The value type T is enforced +// through Build's return type: a Real-only method will not compile against +// FunctionHandle, and templated methods work for any supported T. +// +// The recipes live in `pcms::method` so their short names (e.g. `Copy`) do not +// collide with the transfer-operator types they wrap. +namespace method +{ + +// Identity copy; source and target must share a layout. Any supported T. +template +struct Copy +{ + std::unique_ptr> Build(const FunctionSpace& source, + const FunctionSpace& target) const + { + return std::make_unique>(source, target); + } +}; + +// Point interpolation: evaluate the source field at the target DOF sites. Any +// supported T. +template +struct Interpolation +{ + OutOfBoundsPolicy policy{}; + + std::unique_ptr> Build(const FunctionSpace& source, + const FunctionSpace& target) const + { + return std::make_unique>(source, target, policy); + } +}; + +// Conservative Galerkin (L2) projection via mesh intersection. Real only. +struct ConservativeIntersection +{ + std::unique_ptr> Build( + const FunctionSpace& source, const FunctionSpace& target) const + { + return std::make_unique(source, target); + } +}; + +// Conservative projection with a Monte-Carlo / control-variate RHS. Real only. +struct ConservativeMonteCarlo +{ + int samples_per_element = 32; + MonteCarloSampling sampling = MonteCarloSampling::UniformRandom; + std::uint64_t seed = 8675309; + + std::unique_ptr> Build( + const FunctionSpace& source, const FunctionSpace& target) const + { + return std::make_unique( + source, target, samples_per_element, sampling, seed); + } +}; + +} // namespace method + +} // namespace pcms + +#endif // PCMS_TRANSFER_METHOD_H diff --git a/src/pcms/transfer/transfer_operator.hpp b/src/pcms/transfer/transfer_operator.hpp index adf36c1d..016bfc1c 100644 --- a/src/pcms/transfer/transfer_operator.hpp +++ b/src/pcms/transfer/transfer_operator.hpp @@ -6,13 +6,41 @@ namespace pcms template class Field; +class FunctionSpace; +// A TransferOperator maps a source field to a target field -- both discrete +// fields on function spaces. That space-to-space contract is what distinguishes +// a transfer from a PointEvaluator (field-on-a-space -> values at arbitrary +// coordinates), which is the layer to reach for when the target is not a field +// on a space. Every operator is therefore built from a (source, target) space +// pair, recorded here so callers can check a field's space against the one the +// operator was built for before applying it. template class TransferOperator { public: virtual void Apply(const Field& source, Field& target) const = 0; virtual ~TransferOperator() noexcept = default; + + [[nodiscard]] const FunctionSpace& SourceSpace() const noexcept + { + return *source_space_; + } + [[nodiscard]] const FunctionSpace& TargetSpace() const noexcept + { + return *target_space_; + } + +protected: + TransferOperator(const FunctionSpace& source_space, + const FunctionSpace& target_space) noexcept + : source_space_(&source_space), target_space_(&target_space) + { + } + +private: + const FunctionSpace* source_space_; + const FunctionSpace* target_space_; }; } // namespace pcms diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1c603c45..0d1ae784 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -214,6 +214,47 @@ if(PCMS_ENABLE_OMEGA_H) ${d3d1p} ignored) endif() + if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) + add_executable(coupler_transfer test_coupler_transfer.cpp) + target_link_libraries(coupler_transfer PUBLIC pcms::core test_support) + if(HOST_NPROC GREATER_EQUAL 4) + tri_mpi_test( + TESTNAME + test_coupler_transfer_4p + TIMEOUT + 60 + NAME1 + rdv + EXE1 + ./coupler_transfer + PROCS1 + 2 + ARGS1 + -1 + ${d3d1p} + ${d3d2p_cpn} + NAME2 + source + EXE2 + ./coupler_transfer + PROCS2 + 1 + ARGS2 + 0 + ${d3d1p} + ignored + NAME3 + target + EXE3 + ./coupler_transfer + PROCS3 + 1 + ARGS3 + 1 + ${d3d1p} + ignored) + endif() + endif() if(HOST_NPROC GREATER_EQUAL 26) tri_mpi_test( TESTNAME @@ -385,6 +426,7 @@ if(Catch2_FOUND) test_field_evaluation.cpp test_field_interpolation.cpp test_field_copy.cpp + test_field_exchange_planner.cpp test_point_search.cpp test_mls_basis.cpp test_rbf_interp.cpp @@ -404,11 +446,14 @@ if(Catch2_FOUND) if(PCMS_ENABLE_MESHFIELDS) list(APPEND PCMS_UNIT_TEST_SOURCES - test_load_vector.cpp) + test_omega_h_form_integrator_utils.cpp) endif() if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) list(APPEND PCMS_UNIT_TEST_SOURCES + test_omega_h_intersection_rhs_integrator.cpp + test_omega_h_mass_integrator.cpp + test_omega_h_mc_rhs_integrator.cpp test_mesh_intersection_field_transfer.cpp) endif() diff --git a/test/field_test_utils.h b/test/field_test_utils.h index 3fbb624f..5aaac78e 100644 --- a/test/field_test_utils.h +++ b/test/field_test_utils.h @@ -282,8 +282,8 @@ void CheckEvaluation(const Factory& factory, const Field& field, double abs_tol = 1e-10) { auto device_coords = - CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); - auto evaluator = factory.template CreatePointEvaluator( + CreateDeviceCoordinateView(pts, factory->GetCoordinateSystem()); + auto evaluator = factory->template CreatePointEvaluator( EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); CheckEvaluation(*evaluator, field, pts, func, abs_tol); } @@ -314,9 +314,9 @@ void CheckFillMode(const Factory& factory, const Field& field, Real fill_value, const std::vector& outside_pts) { auto device_coords = - CreateDeviceCoordinateView(outside_pts, factory.GetCoordinateSystem()); + CreateDeviceCoordinateView(outside_pts, factory->GetCoordinateSystem()); OutOfBoundsPolicy policy{OutOfBoundsMode::FILL, fill_value}; - auto evaluator = factory.template CreatePointEvaluator( + auto evaluator = factory->template CreatePointEvaluator( EvaluationRequest::FromCoordinates(device_coords.coordinate_view, policy)); CheckFillMode(*evaluator, field, fill_value, outside_pts); } @@ -334,9 +334,9 @@ void CheckEvaluationWithFill(const Factory& factory, const Field& field, REQUIRE(static_cast(is_inside.size()) == n); auto device_coords = - CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); + CreateDeviceCoordinateView(pts, factory->GetCoordinateSystem()); OutOfBoundsPolicy policy{OutOfBoundsMode::FILL, fill_value}; - auto evaluator = factory.template CreatePointEvaluator( + auto evaluator = factory->template CreatePointEvaluator( EvaluationRequest::FromCoordinates(device_coords.coordinate_view, policy)); Kokkos::View out_device("out_device", n); diff --git a/test/test_coupler_transfer.cpp b/test/test_coupler_transfer.cpp new file mode 100644 index 00000000..bd4252f9 --- /dev/null +++ b/test/test_coupler_transfer.cpp @@ -0,0 +1,231 @@ +#include +#include +#include +#include +#include +#include +#include +#include "test_support.h" +#include "pcms/coupler/coupler.hpp" +#include "pcms/transfer/transfer_method.hpp" +#include "pcms/field/function_space/lagrange.h" + +using pcms::Real; +namespace ts = test_support; + +static constexpr bool done = true; +static constexpr int COMM_ROUNDS = 4; + +namespace +{ + +std::shared_ptr MakeSpace(Omega_h::Mesh& mesh) +{ + // Both ends name the layout "field" so their GID exchanges connect. + return pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "field"); +} + +// Fill a field with scale*gid at each dof holder. The scale lets two fields on +// the same space carry distinct data through one shared transfer operator. +void SetFieldToGids(const pcms::FieldLayout& layout, + pcms::FieldData* field, Real scale = 1.0) +{ + auto gids = layout.GetGidsHost(); + const auto n = layout.GetNumOwnedDofHolder(); + Omega_h::HostWrite values(n); + Kokkos::parallel_for( + "set_gids", + Kokkos::RangePolicy(0, n), + [=](int i) { values[i] = scale * gids[i]; }); + field->SetDOFHolderDataHost( + pcms::Rank2View(values.data(), n, 1)); +} + +bool FieldEqualsGids(const pcms::FieldLayout& layout, + pcms::FieldData* field, int rank, Real scale, + const char* label) +{ + auto gids = layout.GetGidsHost(); + auto owned = layout.GetOwnedHost(); + auto got = pcms::FlattenToRank1View(field->GetDOFHolderDataHost()); + const auto n = layout.GetNumOwnedDofHolder(); + + int expected = 0, matched = 0; + Kokkos::parallel_reduce( + "count_owned", + Kokkos::RangePolicy(0, + owned.size()), + KOKKOS_LAMBDA(int i, int& s) { s += owned[i] != 0; }, expected); + Kokkos::parallel_reduce( + "count_match", + Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(int i, int& s) { + // Identity interpolation of an arbitrary per-vertex GID field: in exact + // arithmetic each target vertex recovers its source value, but the + // point-in-element search leaves a tiny (~1e-7) barycentric perturbation + // that, scaled by the large unordered GID differences, can nudge a value + // just below its integer. Round rather than truncate to compare. + if (owned[i]) + s += std::llround(got[i]) == std::llround(scale * gids[i]); + }, + matched); + + const bool ok = matched == expected; + std::cerr << "Rank " << rank << " - target field '" << label + << "' validation " << (ok ? "PASSED" : "FAILED") << " (" << matched + << "/" << expected << ")\n"; + return ok; +} + +} // namespace + +// -------------------------------------------------------------------------- +// The coupling server: receives a field from the source app, transfers it onto +// the target app's space, and sends it on. This is the whole coupler-integrated +// transfer story. +// -------------------------------------------------------------------------- +void transfer_server(MPI_Comm comm, Omega_h::Mesh& mesh, + std::string_view cpn_file) +{ + pcms::Coupler cpl("coupler_transfer", comm, /*isServer=*/true, + redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + auto* source_app = cpl.AddApplication("coupler_transfer_source"); + auto* target_app = cpl.AddApplication("coupler_transfer_target"); + + auto source_space = MakeSpace(mesh); + auto target_space = MakeSpace(mesh); + + // Two fields (think displacement and velocity) that live on the same source + // and target spaces. Each pair is registered from the same space objects, so + // both share a function space -- and therefore one transfer operator can + // serve both. + auto source = + source_app->AddFunction(source_space->CreateFunction("field")); + auto target = + target_app->AddFunction(target_space->CreateFunction("field")); + auto source2 = + source_app->AddFunction(source_space->CreateFunction("field2")); + auto target2 = + target_app->AddFunction(target_space->CreateFunction("field2")); + + // Build the interpolation operator ONCE from the shared (source, target) + // space pair -- this is the expensive localization step -- then bind that one + // operator to each field pair. transfer and transfer2 reuse the same cached + // localization; there is no second Build(). + std::shared_ptr> interp = + pcms::method::Interpolation{}.Build(source.GetSpace(), + target.GetSpace()); + // Named transfers: the returned handle and the name are interchangeable ways + // to drive them (see the two Run styles below). + auto transfer = cpl.AddTransfer("field", interp, source, target); + cpl.AddTransfer("field2", interp, source2, target2); + + do { + for (int i = 0; i < COMM_ROUNDS; ++i) { + source_app->ReceivePhase([&] { + source.Receive(); + source2.Receive(); + }); + transfer.Run(); // via handle: field source -> target + cpl.RunTransfer("field2"); // via name: field2 source -> target + target_app->SendPhase([&] { + target.Send(); + target2.Send(); + }); + } + } while (!done); +} + +void transfer_source_client(MPI_Comm comm, Omega_h::Mesh& mesh) +{ + pcms::Coupler coupler("coupler_transfer", comm, false, {}); + auto* app = coupler.AddApplication("coupler_transfer_source"); + auto factory = MakeSpace(mesh); + + auto field = factory->CreateFunction("field"); + SetFieldToGids(*factory->GetLayout(), &field.GetData()); + app->AddField(std::move(field)); + + // A second field on the same space, with a distinct scale so it cannot be + // confused with the first as it flows through the shared operator. + auto field2 = factory->CreateFunction("field2"); + SetFieldToGids(*factory->GetLayout(), &field2.GetData(), 3.0); + app->AddField(std::move(field2)); + + do { + for (int i = 0; i < COMM_ROUNDS; ++i) { + app->SendPhase([&] { + app->SendField("field"); + app->SendField("field2"); + }); + } + } while (!done); +} + +void transfer_target_client(MPI_Comm comm, Omega_h::Mesh& mesh) +{ + int rank; + MPI_Comm_rank(comm, &rank); + pcms::Coupler coupler("coupler_transfer", comm, false, {}); + auto* app = coupler.AddApplication("coupler_transfer_target"); + auto factory = MakeSpace(mesh); + + auto field = factory->CreateFunction("field"); + auto* field_ptr = &field.GetData(); + app->AddField(std::move(field)); + + auto field2 = factory->CreateFunction("field2"); + auto* field2_ptr = &field2.GetData(); + app->AddField(std::move(field2)); + + do { + for (int i = 0; i < COMM_ROUNDS; ++i) { + app->ReceivePhase([&] { + app->ReceiveField("field"); + app->ReceiveField("field2"); + }); + // Identity interpolation on the same mesh: each target must receive the + // exact field the source sent. The two fields carry different scales, so + // if the shared operator crossed their data the mismatch would show here. + if (!FieldEqualsGids(*factory->GetLayout(), field_ptr, rank, 1.0, + "field") || + !FieldEqualsGids(*factory->GetLayout(), field2_ptr, rank, 3.0, + "field2")) { + exit(EXIT_FAILURE); + } + } + } while (!done); +} + +int main(int argc, char** argv) +{ + try { + auto lib = Omega_h::Library(&argc, &argv); + const int rank = lib.world()->rank(); + if (argc != 4) { + if (!rank) { + std::cerr + << "Usage: " << argv[0] + << " /path/to/mesh /path/to/partition.cpn\n"; + } + exit(EXIT_FAILURE); + } + const auto clientId = atoi(argv[1]); + REDEV_ALWAYS_ASSERT(clientId >= -1 && clientId <= 1); + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(argv[2], lib.world(), &mesh); + MPI_Comm mpi_comm = lib.world()->get_impl(); + switch (clientId) { + case -1: transfer_server(mpi_comm, mesh, argv[3]); break; + case 0: transfer_source_client(mpi_comm, mesh); break; + case 1: transfer_target_client(mpi_comm, mesh); break; + default: break; // unreachable: clientId asserted to [-1, 1] above + } + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } +} diff --git a/test/test_eqdsk.cpp b/test/test_eqdsk.cpp index 40150574..d546fbbe 100644 --- a/test/test_eqdsk.cpp +++ b/test/test_eqdsk.cpp @@ -182,10 +182,10 @@ TEST_CASE("EQDSKData with SplineFunctionSpace") { // auto spline_space = pcms::SplineFunctionSpace::FromEQDSK(eqdsk_data); - // auto psi_field = spline_space.CreateField(); + // auto psi_field = spline_space->CreateFunction(); auto [spline_space, psi_field] = pcms::MakeEQDSKField(eqdsk_data); - auto layout = spline_space.GetLayout(); + auto layout = spline_space->GetLayout(); const int layout_size = layout->OwnedSize(); const int psizr_size = static_cast(eqdsk_data.PSIZR.extent(0)); @@ -230,7 +230,7 @@ TEST_CASE("EQDSKData with SplineFunctionSpace") auto eval_request = pcms::EvaluationRequest::FromCoordinates(coord_view); auto evaluator = - spline_space.CreatePointEvaluator(eval_request); + spline_space->CreatePointEvaluator(eval_request); auto eval_results_1d = Kokkos::View( "eval_results", num_eval_points); diff --git a/test/test_field_communication.cpp b/test/test_field_communication.cpp index 83b8bc5b..dcee3252 100644 --- a/test/test_field_communication.cpp +++ b/test/test_field_communication.cpp @@ -78,15 +78,12 @@ static void test_shared_layout(Omega_h::Library& lib, redev::Partition{partition}); auto* app = cpl.AddApplication("shared_layout"); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); - app->AddLayout("shared", layout); + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "shared"); + auto f1 = app->AddField(factory->CreateFunction("field_a")); PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == 1); - auto f1 = app->AddField("field_a", factory.CreateField()); - PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == - 1); // still 1 after adding field - auto f2 = app->AddField("field_b", factory.CreateField()); - PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == 1); // still 1 + auto f2 = app->AddField(factory->CreateFunction("field_b")); + PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == 1); // shared layout app->ReceivePhase([&]() { f1.Receive(); f2.Receive(); @@ -100,11 +97,10 @@ static void test_shared_layout(Omega_h::Library& lib, redev::Partition{}); auto* app = cpl.AddApplication("shared_layout"); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); - app->AddLayout("shared", layout); - auto f1 = app->AddField("field_a", factory.CreateField()); - auto f2 = app->AddField("field_b", factory.CreateField()); + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "shared"); + auto f1 = app->AddField(factory->CreateFunction("field_a")); + auto f2 = app->AddField(factory->CreateFunction("field_b")); app->SendPhase([&]() { f1.Send(); f2.Send(); @@ -125,7 +121,7 @@ void client1(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, order, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); auto gids = layout->GetGidsHost(); const auto n = layout->GetNumOwnedDofHolder(); Omega_h::HostWrite ids(n); @@ -134,7 +130,7 @@ void client1(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, "id gid", Kokkos::RangePolicy(0, n), [=](int i) { ids[i] = gids[i]; }); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); field.SetDOFHolderDataHost( pcms::Rank2View(ids.data(), n, 1)); @@ -157,11 +153,11 @@ void client2(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, order, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); auto gids = layout->GetGidsHost(); const auto n = layout->GetNumOwnedDofHolder(); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::FieldLayoutCommunicator layout_comm(comm_name + "2", comm, rdv, channel, *layout); pcms::FieldCommunicator field_comm(layout_comm.GetName(), @@ -217,14 +213,14 @@ void server(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, order, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); const auto n = layout->GetNumOwnedDofHolder(); Omega_h::HostWrite ids(n); Kokkos::parallel_for( "id 0", Kokkos::RangePolicy(0, n), [=](int i) { ids[i] = 0; }); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::FieldLayoutCommunicator layout_comm1(comm_name + "1", comm, rdv, channel1, *layout); pcms::FieldLayoutCommunicator layout_comm2(comm_name + "2", comm, rdv, diff --git a/test/test_field_copy.cpp b/test/test_field_copy.cpp index 9906bbbc..46a1b94e 100644 --- a/test/test_field_copy.cpp +++ b/test/test_field_copy.cpp @@ -22,20 +22,20 @@ void test_copy(Omega_h::CommPtr world, int dim, int order, int num_components) auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, order, num_components, pcms::CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); int ndata = layout->GetNumOwnedDofHolder() * num_components; Omega_h::HostWrite ids(ndata); Kokkos::parallel_for( Kokkos::RangePolicy(0, ndata), [=](int i) { ids[i] = i; }); - auto original = factory.CreateField(pcms::FieldMetadata{}); + auto original = factory->CreateFunction(); original.SetDOFHolderDataHost( pcms::Rank2View( ids.data(), layout->GetNumOwnedDofHolder(), num_components)); - auto copied = factory.CreateField(pcms::FieldMetadata{}); - pcms::Copy copy(factory, factory); + auto copied = factory->CreateFunction(); + pcms::Copy copy(*factory, *factory); copy.Apply(original, copied); auto copied_array = pcms::FlattenToRank1View(copied.GetDOFHolderDataHost()); diff --git a/test/test_field_evaluation.cpp b/test/test_field_evaluation.cpp index c4a29378..ff238e11 100644 --- a/test/test_field_evaluation.cpp +++ b/test/test_field_evaluation.cpp @@ -30,10 +30,10 @@ TEST_CASE("evaluate linear 2d omega_h_field") 100, 0, false); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), + field.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); pcms::test::CheckEvaluation( factory, field, pcms::test::StandardEvalCoords2D(), @@ -75,7 +75,7 @@ TEST_CASE("evaluate quadratic 2d meshfields_field") }); Omega_h::HostWrite test_f_host(test_f); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); field.GetData().SetDOFHolderDataHost( pcms::Rank2View(test_f_host.data(), test_f_host.size(), 1)); diff --git a/test/test_field_exchange_planner.cpp b/test/test_field_exchange_planner.cpp new file mode 100644 index 00000000..44678e47 --- /dev/null +++ b/test/test_field_exchange_planner.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include + +TEST_CASE("Field exchange plans exclude GID message headers", + "[field_exchange]") +{ + Kokkos::View coords("coords", 4, 2); + pcms::PointCloudLayout layout(2, coords, pcms::CoordinateSystem::Cartesian); + + // Two incoming messages. Each contains a five-entry entity-offset header + // followed by two vertex GIDs. + Kokkos::View received( + "received_gids", 2 * static_cast(pcms::ent_offsets_len + 2)); + const pcms::GO message[] = { + 0, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 3, 1, + }; + for (size_t i = 0; i < received.size(); ++i) + received(i) = message[i]; + + redev::InMessageLayout in; + in.srcRanks = {0, 7}; + in.offset = {0, 14}; + + pcms::GenericFieldExchangePlanner planner; + const auto plan = planner.BuildReceivePlan( + layout, + pcms::GlobalIDView(received.data(), received.size()), + 0, 1, in); + + REQUIRE(plan.dest_ranks == redev::LOs{0, 1}); + REQUIRE(plan.offsets == redev::LOs{0, 2, 4}); + REQUIRE(plan.permutation == std::vector{1, 3, 0, 2}); + REQUIRE(plan.msg_size == 4); +} + +TEST_CASE("GID messages insert headers around compact field payloads", + "[field_exchange]") +{ + Kokkos::View coords("coords", 4, 2); + pcms::PointCloudLayout layout(2, coords, pcms::CoordinateSystem::Cartesian); + + pcms::ExchangePlan plan; + plan.dest_ranks = {0, 1}; + plan.offsets = {0, 2, 4}; + plan.permutation = {0, 2, 1, 3}; + plan.msg_size = 4; + + Kokkos::View message( + "gid_message", + plan.msg_size + 2 * static_cast(pcms::ent_offsets_len)); + pcms::GenericFieldExchangePlanner planner; + planner.FillGidMessage(layout, plan, + pcms::Rank1View( + message.data(), message.size())); + + const pcms::GO expected[] = { + 0, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 1, 3, + }; + for (size_t i = 0; i < message.size(); ++i) { + CAPTURE(i); + REQUIRE(message(i) == expected[i]); + } +} diff --git a/test/test_field_interpolation.cpp b/test/test_field_interpolation.cpp index fdada727..bc88ab97 100644 --- a/test/test_field_interpolation.cpp +++ b/test/test_field_interpolation.cpp @@ -27,12 +27,12 @@ TEST_CASE("interpolate linear 2d omega_h_field") Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); - auto interpolated = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); + auto interpolated = factory->CreateFunction(); pcms::test::SetField( field, OMEGA_H_LAMBDA(Real x, Real y) { return -0.3 * x + 0.5 * y; }); - pcms::Interpolator interp(factory, factory); + pcms::Interpolator interp(*factory, *factory); interp.Apply(field, interpolated); auto interpolated_dof = pcms::FlattenToRank1View(interpolated.GetDOFHolderDataHost()); @@ -55,7 +55,7 @@ TEST_CASE("interpolate quadratic 2d meshfields_field") auto factory2 = pcms::LagrangeFunctionSpace::FromMesh( mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto layout = factory2.GetLayout(); + auto layout = factory2->GetLayout(); const auto nverts = mesh.nents(0); const auto nedges = mesh.nents(1); auto mesh_coords = mesh.coords(); @@ -80,12 +80,12 @@ TEST_CASE("interpolate quadratic 2d meshfields_field") }); Omega_h::HostWrite test_f_host(test_f); - auto field = factory2.CreateField(pcms::FieldMetadata{}); - auto interpolated = factory2.CreateField(pcms::FieldMetadata{}); + auto field = factory2->CreateFunction(); + auto interpolated = factory2->CreateFunction(); field.SetDOFHolderDataHost(pcms::Rank2View( test_f_host.data(), test_f_host.size(), 1)); - pcms::Interpolator interp(factory2, factory2); + pcms::Interpolator interp(*factory2, *factory2); interp.Apply(field, interpolated); auto interpolated_dof = @@ -126,13 +126,13 @@ TEST_CASE("Interpolator: construct once, apply twice with different data") auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto source = factory.CreateField(pcms::FieldMetadata{}); - auto target = factory.CreateField(pcms::FieldMetadata{}); + auto source = factory->CreateFunction(); + auto target = factory->CreateFunction(); // Construct Interpolator once — localization happens here. // Because source and target share the same layout (same factory), // the target DOF holder coordinates are the source mesh node coordinates. - pcms::Interpolator interp(factory, factory); + pcms::Interpolator interp(*factory, *factory); // First application: linear function f(x,y) = -0.3*x + 0.5*y pcms::test::SetField( diff --git a/test/test_load_vector.cpp b/test/test_load_vector.cpp deleted file mode 100644 index 1a38df50..00000000 --- a/test/test_load_vector.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -TEST_CASE("Load vector computation on intersected regions", "[load_vector]") -{ - - Omega_h::Library lib; - - // 2D coordinates (x,y) : 4 vertices - Omega_h::Reals coords({ - 0.0, 0.0, // v0 - 1.0, 0.0, // v1 - 1.0, 1.0, // v2 - 0.0, 1.0 // v3 - }); - - // Target Mesh with two triangles - // Two triangles, CCW - // T0: (v0,v1,v3) = (0,1,3) - // T1: (v1,v2,v3) = (1,2,3) - Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); - - Omega_h::Mesh target_mesh(&lib); - Omega_h::build_from_elems_and_coords(&target_mesh, OMEGA_H_SIMPLEX, 2, - ev2v_target, coords); - - // Source Mesh with two triangles - // Two triangles, CCW - // T0: (v0,v1,v3) = (0,1,2) - // T1: (v1,v2,v3) = (0,2,3) - Omega_h::LOs ev2v_source({0, 1, 2, 0, 2, 3}); - - Omega_h::Mesh source_mesh(&lib); - Omega_h::build_from_elems_and_coords(&source_mesh, OMEGA_H_SIMPLEX, 2, - ev2v_source, coords); - - REQUIRE(source_mesh.dim() == 2); - REQUIRE(target_mesh.dim() == 2); - - int num_tgt_elems = target_mesh.nelems(); - - auto intersection = pcms::intersectTargets(source_mesh, target_mesh); - SECTION("check localization routine for coincident cases") - { - Kokkos::View points("test_points", 3); - auto points_h = Kokkos::create_mirror_view(points); - points_h(0, 0) = 0.0; - points_h(0, 1) = 0.0; - points_h(1, 0) = 1.0; - points_h(1, 1) = 0.0; - points_h(2, 0) = 1.0; - points_h(2, 1) = 1.0; - - Kokkos::deep_copy(points, points_h); - pcms::GridPointSearch2D search_cell(target_mesh, 20, 20); - - auto d_results = search_cell(points); - - auto h_results = Kokkos::create_mirror_view(d_results); - - Kokkos::deep_copy(h_results, d_results); - - REQUIRE(h_results.extent(0) == 3); - - for (int i = 0; i < h_results.extent(0); ++i) { - auto r = h_results(i); - std::cout << "tri_id = " << r.element_id << "\n"; - std::cout << "dim = " << static_cast(r.dimensionality) << "\n"; - // std::cout << "parametric = " << r.parametric_coords << "\n"; - INFO("tri_id : " << r.element_id); - REQUIRE(r.element_id >= 0); - } - } - - SECTION("Basic shape and consistency of result") - { - - // Fill dummy values at each vertex of source - Omega_h::Write values(source_mesh.nverts(), 1.0); - - auto load_vector = - pcms::buildLoadVector(target_mesh, source_mesh, intersection, values); - - auto load_vector_host = Kokkos::create_mirror(load_vector); - Kokkos::deep_copy(load_vector_host, load_vector); - - REQUIRE(static_cast(load_vector_host.extent(0)) == num_tgt_elems * 3); - - for (int i = 0; i < load_vector_host.extent(0); ++i) - REQUIRE(load_vector_host(i) >= - 0.0); // Since function is 1.0 and everything is positive - } - - SECTION("Integration is zero if source values are zero") - { - Omega_h::Write zero_field(source_mesh.nverts(), 0.0); - - auto load_vector = - pcms::buildLoadVector(target_mesh, source_mesh, intersection, zero_field); - - auto load_vector_host = Kokkos::create_mirror_view(load_vector); - Kokkos::deep_copy(load_vector_host, load_vector); - - for (int i = 0; i < load_vector_host.extent(0); ++i) { - REQUIRE(load_vector_host(i) == Catch::Approx(0.0)); - } - } - - SECTION("load vector computed after the intersection of simple target and " - "source elements") - { - // the source elements are triangle1 (0,0), (1,0) & (1,1) and triangle2 - // (0,0), (1,1) & (0,1) the target elements are triangle1 (0,0), (1,0) & - // (0,1) and triangle2 (1,0), (1,1) & (0,1) - - Omega_h::Write constant_field(source_mesh.nverts(), 2.0); - - auto load_vector = pcms::buildLoadVector(target_mesh, source_mesh, - intersection, constant_field); - - auto load_vector_host = Kokkos::create_mirror_view(load_vector); - Kokkos::deep_copy(load_vector_host, load_vector); - - double expected_load_vector[6] = {0.333333, 0.333333, 0.333333, - 0.333333, 0.333333, 0.333333}; - double tolerance = 1e-6; - for (int i = 0; i < load_vector_host.extent(0); ++i) { - - CAPTURE(i, expected_load_vector[i], load_vector_host[i], tolerance); - CHECK_THAT(expected_load_vector[i], - Catch::Matchers::WithinAbs(load_vector_host[i], tolerance)); - } - } -} diff --git a/test/test_mesh_intersection_field_transfer.cpp b/test/test_mesh_intersection_field_transfer.cpp index bd34af54..4afa0f43 100644 --- a/test/test_mesh_intersection_field_transfer.cpp +++ b/test/test_mesh_intersection_field_transfer.cpp @@ -5,223 +5,483 @@ #include #include -#include -#include #include +#include #include #include "field_test_utils.h" +#include +#include + namespace { -double integrate_linear_field(Omega_h::Mesh& mesh, const Omega_h::Reals& u) +// Builds a unit-square 2D simplex mesh from the given element connectivity and +// adds the geometric classification tags required to build an Omega_h-backed +// Lagrange function space. +Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, const Omega_h::LOs& ev2v) +{ + const Omega_h::Reals coords({ + 0.0, 0.0, // v0 + 1.0, 0.0, // v1 + 1.0, 1.0, // v2 + 0.0, 1.0 // v3 + }); + Omega_h::Mesh mesh(&lib); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); + for (Omega_h::Int dim = 0; dim <= 2; ++dim) { + mesh.add_tag( + dim, "class_dim", 1, + Omega_h::Read(mesh.nents(dim), Omega_h::I8(dim))); + mesh.add_tag( + dim, "class_id", 1, + Omega_h::Read(mesh.nents(dim), Omega_h::ClassId(0))); + } + return mesh; +} + +std::shared_ptr MakeP1Space( + Omega_h::Mesh& mesh, const std::string& global_id_name = "global") { + return pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, global_id_name, + pcms::LagrangeFunctionSpace::Backend::OmegaH); +} + +std::shared_ptr MakeP0Space(Omega_h::Mesh& mesh) +{ + return pcms::LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); +} + +void AddReorderedVertexGlobalIds(Omega_h::Mesh& mesh) +{ + mesh.add_tag( + 0, "reordered_global", 1, + Omega_h::Read({2, 0, 3, 1}, "reordered_global")); +} + +void AddSparseVertexGlobalIds(Omega_h::Mesh& mesh) +{ + mesh.add_tag( + 0, "sparse_global", 1, + Omega_h::Read({102, 7, 41, 19}, "sparse_global")); +} + +// Integral over the mesh of an order-0 (piecewise-constant) field whose values +// are stored in element order. Exact by construction. +double IntegrateP0Field(Omega_h::Mesh& mesh, + const pcms::Field& field) +{ + const auto values = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); const auto elem_areas = Omega_h::measure_elements_real(&mesh); - const auto elem_verts = mesh.ask_elem_verts(); - const auto nverts = mesh.nverts(); + const auto elem_areas_h = Omega_h::HostRead(elem_areas); + + double integral = 0.0; + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + integral += elem_areas_h[e] * values[e]; + } + return integral; +} - REQUIRE(static_cast(u.size()) == nverts); +// Per-element centroid coordinates in element order. +std::vector> ElementCentroids(Omega_h::Mesh& mesh) +{ + const auto coords_h = Omega_h::HostRead(mesh.coords()); + const auto elem_verts_h = + Omega_h::HostRead(mesh.ask_elem_verts()); + std::vector> centroids(mesh.nelems()); + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + double cx = 0.0, cy = 0.0; + for (int k = 0; k < 3; ++k) { + const Omega_h::LO v = elem_verts_h[3 * e + k]; + cx += coords_h[2 * v + 0]; + cy += coords_h[2 * v + 1]; + } + centroids[e] = {cx / 3.0, cy / 3.0}; + } + return centroids; +} +// Integral over the mesh of an order-1 nodal field whose values are stored in +// vertex order (as produced by Field::GetDOFHolderDataHost for the +// Omega_h backend), computed exactly via per-element vertex averaging. +double IntegrateP1Field(Omega_h::Mesh& mesh, + const pcms::Field& field) +{ + const auto values = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + const auto elem_areas = Omega_h::measure_elements_real(&mesh); + const auto elem_verts = mesh.ask_elem_verts(); const auto elem_areas_h = Omega_h::HostRead(elem_areas); const auto elem_verts_h = Omega_h::HostRead(elem_verts); - const auto u_h = Omega_h::HostRead(u); double integral = 0.0; for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { const Omega_h::LO v0 = elem_verts_h[3 * e + 0]; const Omega_h::LO v1 = elem_verts_h[3 * e + 1]; const Omega_h::LO v2 = elem_verts_h[3 * e + 2]; - - const double area = elem_areas_h[e]; - const double avg = (u_h[v0] + u_h[v1] + u_h[v2]) / 3.0; - integral += area * avg; + const double avg = (values[v0] + values[v1] + values[v2]) / 3.0; + integral += elem_areas_h[e] * avg; } return integral; } -Omega_h::Reals make_omega_h_reals( - pcms::Rank1View values) -{ - Omega_h::HostWrite values_h(values.size()); - for (size_t i = 0; i < values.size(); ++i) { - values_h[i] = values[i]; - } - return Omega_h::Reals(values_h); -} - } // namespace -TEST_CASE("mesh intersection linear/constant conservation", +// Fields that already live in the target order-1 space (constants and affine +// functions) must be reproduced exactly by the L2 projection, and the integral +// must be conserved. These properties hold without any reference solver, so +// they pin down correctness on their own. +TEST_CASE("OmegaHConservativeProjection reproduces constant and linear fields", "[transfer][mesh_intersection]") { Omega_h::Library lib; - Omega_h::Reals coords({ - 0.0, 0.0, // v0 - 1.0, 0.0, // v1 - 1.0, 1.0, // v2 - 0.0, 1.0 // v3 - }); + // Source and target triangulate the same square along opposite diagonals. + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); - // Source mesh: diagonal (v0-v2) - Omega_h::LOs ev2v_source({0, 1, 2, 0, 2, 3}); - Omega_h::Mesh source_mesh(&lib); - Omega_h::build_from_elems_and_coords(&source_mesh, OMEGA_H_SIMPLEX, 2, - ev2v_source, coords); + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); - // Target mesh: opposite diagonal (v1-v3) - Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); - Omega_h::Mesh target_mesh(&lib); - Omega_h::build_from_elems_and_coords(&target_mesh, OMEGA_H_SIMPLEX, 2, - ev2v_target, coords); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); - const auto src_coords = source_mesh.coords(); - - auto intersections = pcms::intersectTargets(source_mesh, target_mesh); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); SECTION("constant field is preserved and conserved") { const double c = 2.0; - Omega_h::Write source_const(source_mesh.nverts()); - Omega_h::parallel_for( - source_const.size(), OMEGA_H_LAMBDA(int i) { source_const[i] = c; }); + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return c; }); - auto projected = pcms::solveGalerkinProjection(target_mesh, source_mesh, - intersections, source_const); - auto projected_h = Omega_h::HostRead(projected); + projection.Apply(source, target); - REQUIRE(static_cast(projected.size()) == target_mesh.nverts()); + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + REQUIRE(static_cast(target_values.size()) == + target_mesh.nverts()); for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { - REQUIRE(projected_h[i] == Catch::Approx(c).margin(1e-10)); + REQUIRE(target_values[i] == Catch::Approx(c).margin(1e-10)); } - - const double source_integral = - integrate_linear_field(source_mesh, source_const); - const double target_integral = - integrate_linear_field(target_mesh, projected); - REQUIRE(target_integral == Catch::Approx(source_integral).margin(1e-10)); + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-10)); } - SECTION("linear field is reproduced on target vertices") + SECTION("linear field is reproduced on target vertices and conserved") { - Omega_h::Write source_linear(source_mesh.nverts()); - Omega_h::parallel_for( - source_mesh.nverts(), OMEGA_H_LAMBDA(int i) { - const double x = src_coords[2 * i + 0]; - const double y = src_coords[2 * i + 1]; - source_linear[i] = x + y; - }); - - auto projected = pcms::solveGalerkinProjection( - target_mesh, source_mesh, intersections, source_linear); - auto projected_h = Omega_h::HostRead(projected); + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + y; }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); const auto tgt_coords_h = Omega_h::HostRead(target_mesh.coords()); - for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { const double expected = tgt_coords_h[2 * i + 0] + tgt_coords_h[2 * i + 1]; - REQUIRE(projected_h[i] == Catch::Approx(expected).margin(1e-9)); + REQUIRE(target_values[i] == Catch::Approx(expected).margin(1e-9)); } + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-9)); } } -TEST_CASE("OmegaHConservativeProjection matches conservative projection solver", +// For a field that does not live in the target space the projection is not an +// interpolation, but conservation of the integral is the defining property of +// the conservative (Galerkin) projection and must still hold exactly. This +// also exercises a second Apply with a different source field to confirm the +// cached integrators/evaluator/factorization are reused without stale state. +TEST_CASE("OmegaHConservativeProjection conserves the integral", "[transfer][mesh_intersection]") { Omega_h::Library lib; - Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); - - Omega_h::LOs ev2v_source({0, 1, 2, 0, 2, 3}); - Omega_h::Mesh source_mesh(&lib); - Omega_h::build_from_elems_and_coords(&source_mesh, OMEGA_H_SIMPLEX, 2, - ev2v_source, coords); - - Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); - Omega_h::Mesh target_mesh(&lib); - Omega_h::build_from_elems_and_coords(&target_mesh, OMEGA_H_SIMPLEX, 2, - ev2v_target, coords); - - // Add classification for all dimensions - // Vertices (dim 0) - source_mesh.add_tag( - 0, "class_dim", 1, - Omega_h::Read(source_mesh.nverts(), Omega_h::I8(0))); - source_mesh.add_tag( - 0, "class_id", 1, - Omega_h::Read(source_mesh.nverts(), Omega_h::ClassId(0))); - - // Edges (dim 1) - source_mesh.add_tag( - 1, "class_dim", 1, - Omega_h::Read(source_mesh.nedges(), Omega_h::I8(1))); - source_mesh.add_tag( - 1, "class_id", 1, - Omega_h::Read(source_mesh.nedges(), Omega_h::ClassId(0))); - - // Faces (dim 2) - source_mesh.add_tag( - 2, "class_dim", 1, - Omega_h::Read(source_mesh.nelems(), Omega_h::I8(2))); - source_mesh.add_tag( - 2, "class_id", 1, - Omega_h::Read(source_mesh.nelems(), Omega_h::ClassId(0))); - - // Add classification for all dimensions - // Vertices (dim 0) - target_mesh.add_tag( - 0, "class_dim", 1, - Omega_h::Read(target_mesh.nverts(), Omega_h::I8(0))); - target_mesh.add_tag( - 0, "class_id", 1, - Omega_h::Read(target_mesh.nverts(), Omega_h::ClassId(0))); - - // Edges (dim 1) - target_mesh.add_tag( - 1, "class_dim", 1, - Omega_h::Read(target_mesh.nedges(), Omega_h::I8(1))); - target_mesh.add_tag( - 1, "class_id", 1, - Omega_h::Read(target_mesh.nedges(), Omega_h::ClassId(0))); - - // Faces (dim 2) - target_mesh.add_tag( - 2, "class_dim", 1, - Omega_h::Read(target_mesh.nelems(), Omega_h::I8(2))); - target_mesh.add_tag( - 2, "class_id", 1, - Omega_h::Read(target_mesh.nelems(), Omega_h::ClassId(0))); - - auto source_space = pcms::LagrangeFunctionSpace::FromMesh( - source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto target_space = pcms::LagrangeFunctionSpace::FromMesh( - target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); pcms::test::SetField( source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x * x + x * y + 0.5 * y * y; }); + projection.Apply(source, target); + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-12)); + + // Second Apply with a different source field — verifies cached state is + // reused correctly and is not stale. + pcms::test::SetField( + source, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return 2.0 * x - y + 0.5; }); + projection.Apply(source, target); + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-12)); +} + +TEST_CASE("OmegaHConservativeProjection writes reordered target GIDs in local " + "DOF order", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + AddReorderedVertexGlobalIds(target_mesh); + + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh, "reordered_global"); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); + projection.Apply(source, target); + + const auto target_values = target.GetDOFHolderDataHost(); + const auto target_coords = + target_space->GetLayout()->GetDOFHolderCoordinates().GetValues(); + const auto target_coords_h = pcms::test::CopyCoordinatesToHost( + target_coords, target_mesh.nverts(), target_mesh.dim()); + REQUIRE(static_cast(target_values.extent(0)) == + target_mesh.nverts()); + REQUIRE(target_values.extent(1) == 1); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = target_coords_h(i, 0) + 2.0 * target_coords_h(i, 1); + CAPTURE(i); + REQUIRE(target_values(i, 0) == Catch::Approx(expected).margin(1e-9)); + } +} + +TEST_CASE("OmegaHConservativeProjection maps sparse target GIDs to active " + "PETSc rows", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; - const auto intersections = pcms::intersectTargets(source_mesh, target_mesh); - const auto expected = - pcms::solveGalerkinProjection(target_mesh, source_mesh, intersections, - make_omega_h_reals(pcms::FlattenToRank1View( - source.GetDOFHolderDataHost()))); - const auto expected_h = Omega_h::HostRead(expected); + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + AddSparseVertexGlobalIds(target_mesh); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh, "sparse_global"); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); projection.Apply(source, target); - const auto target_values = - pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); - REQUIRE(static_cast(target_values.size()) == + const auto target_values = target.GetDOFHolderDataHost(); + const auto target_coords = + target_space->GetLayout()->GetDOFHolderCoordinates().GetValues(); + const auto target_coords_h = pcms::test::CopyCoordinatesToHost( + target_coords, target_mesh.nverts(), target_mesh.dim()); + REQUIRE(static_cast(target_values.extent(0)) == target_mesh.nverts()); + REQUIRE(target_values.extent(1) == 1); for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { - REQUIRE(target_values[i] == Catch::Approx(expected_h[i]).margin(1e-12)); + const double expected = target_coords_h(i, 0) + 2.0 * target_coords_h(i, 1); + CAPTURE(i); + REQUIRE(target_values(i, 0) == Catch::Approx(expected).margin(1e-9)); + } +} + +// P0 (piecewise-constant) source -> P1 target. A constant lives in P1, so it is +// reproduced exactly; a non-constant P0 source is not reproduced but its +// integral must still be conserved. +TEST_CASE("OmegaHConservativeProjection P0 source to P1 target", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + + auto source_space = MakeP0Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); + + SECTION("constant field is preserved and conserved") + { + const double c = 3.5; + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return c; }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + REQUIRE(static_cast(target_values.size()) == + target_mesh.nverts()); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + REQUIRE(target_values[i] == Catch::Approx(c).margin(1e-10)); + } + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP0Field(source_mesh, source)).margin(1e-10)); + } + + SECTION("non-constant P0 source conserves the integral") + { + pcms::test::SetField( + source, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + + projection.Apply(source, target); + + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP0Field(source_mesh, source)).margin(1e-10)); + } +} + +// P1 source -> P0 (piecewise-constant) target. A constant is reproduced +// exactly; a linear source projects to its cell average, which for a linear +// function equals the value at each element centroid. The integral is conserved +// in every case. +TEST_CASE("OmegaHConservativeProjection P1 source to P0 target", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP0Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); + + SECTION("constant field is preserved and conserved") + { + const double c = -1.25; + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return c; }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + REQUIRE(static_cast(target_values.size()) == + target_mesh.nelems()); + for (Omega_h::LO e = 0; e < target_mesh.nelems(); ++e) { + REQUIRE(target_values[e] == Catch::Approx(c).margin(1e-10)); + } + REQUIRE(IntegrateP0Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-10)); + } + + SECTION("linear field projects to cell average and conserves the integral") + { + const auto f = [](double x, double y) { return x + 2.0 * y; }; + pcms::test::SetField( + source, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + const auto centroids = ElementCentroids(target_mesh); + REQUIRE(static_cast(target_values.size()) == centroids.size()); + for (std::size_t e = 0; e < centroids.size(); ++e) { + const double expected = f(centroids[e][0], centroids[e][1]); + REQUIRE(target_values[e] == Catch::Approx(expected).margin(1e-9)); + } + REQUIRE(IntegrateP0Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-9)); + } +} + +// Exercises the pcms::method::* recipe factory: each recipe's Build() must +// produce a working transfer operator equivalent to constructing it directly, +// and parameter-carrying recipes must forward their parameters. +TEST_CASE("TransferMethod recipes build working operators", + "[transfer][method]") +{ + Omega_h::Library lib; + Omega_h::Mesh source_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + y; }); + + const auto tgt_coords_h = + Omega_h::HostRead(target_mesh.coords()); + auto check_reproduces_linear = [&]() { + const auto values = pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = tgt_coords_h[2 * i + 0] + tgt_coords_h[2 * i + 1]; + REQUIRE(values[i] == Catch::Approx(expected).margin(1e-9)); + } + }; + + SECTION("ConservativeIntersection reproduces a linear field") + { + auto op = pcms::method::ConservativeIntersection{}.Build(*source_space, + *target_space); + op->Apply(source, target); + check_reproduces_linear(); + } + + SECTION("Interpolation reproduces a linear field") + { + auto op = pcms::method::Interpolation{}.Build(*source_space, + *target_space); + op->Apply(source, target); + check_reproduces_linear(); + } + + SECTION("ConservativeMonteCarlo forwards params and conserves the integral") + { + // A linear field lives in the target P1 space, so the control-variate + // residual is zero and the projection is exact for any sample count. + auto op = + pcms::method::ConservativeMonteCarlo{.samples_per_element = 16}.Build( + *source_space, *target_space); + op->Apply(source, target); + check_reproduces_linear(); + REQUIRE(IntegrateP1Field(target_mesh, target) == + Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-9)); } } diff --git a/test/test_omega_h_field2_outofbounds.cpp b/test/test_omega_h_field2_outofbounds.cpp index 5e4c3813..cf748e1f 100644 --- a/test/test_omega_h_field2_outofbounds.cpp +++ b/test/test_omega_h_field2_outofbounds.cpp @@ -21,9 +21,9 @@ TEST_CASE("omega_h_field2 out of bounds FILL mode") Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), + field.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }); Real fill_value = -999.0; diff --git a/test/test_omega_h_form_integrator_utils.cpp b/test/test_omega_h_form_integrator_utils.cpp new file mode 100644 index 00000000..91cedac7 --- /dev/null +++ b/test/test_omega_h_form_integrator_utils.cpp @@ -0,0 +1,80 @@ +#include +#include +#include + +namespace +{ + +r3d::Polytope<2> MakePoly(int nverts, + std::initializer_list> pts) +{ + r3d::Polytope<2> poly; + poly.nverts = nverts; + int i = 0; + for (auto [x, y] : pts) { + poly.verts[i].pos[0] = x; + poly.verts[i].pos[1] = y; + poly.verts[i].pnbrs[0] = -1; + poly.verts[i].pnbrs[1] = -1; + ++i; + } + return poly; +} + +} // namespace + +TEST_CASE( + "RemoveDuplicateVerticesAndFixLinks: no duplicates leaves triangle intact", + "[form_integrator_utils]") +{ + // A clean CCW triangle — nothing to remove, area should equal 0.5. + auto poly = MakePoly(3, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}}); + const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); + REQUIRE(n == 3); + REQUIRE(poly.nverts == 3); + // The function sets correct pnbrs, so r3d::measure is valid after the call. + const double area = r3d::measure(poly); + REQUIRE(area == Catch::Approx(0.5).epsilon(1e-12)); +} + +TEST_CASE("RemoveDuplicateVerticesAndFixLinks: one duplicate vertex is removed", + "[form_integrator_utils]") +{ + // 4-vertex polygon; last vertex duplicates the first within the default tol. + // After dedup: 3 unique vertices, area of the underlying triangle preserved. + auto poly = MakePoly(4, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}, {5e-13, 0.0}}); + const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); + REQUIRE(n == 3); + REQUIRE(poly.nverts == 3); + const double area = r3d::measure(poly); + REQUIRE(area == Catch::Approx(0.5).epsilon(1e-10)); +} + +TEST_CASE( + "RemoveDuplicateVerticesAndFixLinks: polygon becomes degenerate after dedup", + "[form_integrator_utils]") +{ + // 3 vertices where two positions are identical — only 2 unique remain. + // The function must mark the polygon degenerate: all pnbrs set to -1. + auto poly = MakePoly(3, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}); + const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); + REQUIRE(n == 2); + REQUIRE(poly.nverts == 2); + for (int i = 0; i < 2; ++i) { + CAPTURE(i); + CHECK(poly.verts[i].pnbrs[0] == -1); + CHECK(poly.verts[i].pnbrs[1] == -1); + } +} + +TEST_CASE( + "RemoveDuplicateVerticesAndFixLinks: near-tolerance vertex is retained", + "[form_integrator_utils]") +{ + // Vertex at (0, 2e-12) differs from (0, 0) by 2e-12 in y, which exceeds the + // default tol=1e-12, so it must NOT be treated as a duplicate. + auto poly = MakePoly(3, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 2e-12}}); + const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); + REQUIRE(n == 3); + REQUIRE(poly.nverts == 3); +} diff --git a/test/test_omega_h_intersection_rhs_integrator.cpp b/test/test_omega_h_intersection_rhs_integrator.cpp new file mode 100644 index 00000000..fcbb3aed --- /dev/null +++ b/test/test_omega_h_intersection_rhs_integrator.cpp @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +namespace +{ + +// Build a unit-square 2D simplex mesh. +// diagonal=0: T0=(0,1,3), T1=(1,2,3) +// diagonal=1: T0=(0,1,2), T1=(0,2,3) +Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, int diagonal) +{ + const Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); + Omega_h::LOs ev2v = (diagonal == 0) ? Omega_h::LOs({0, 1, 3, 1, 2, 3}) + : Omega_h::LOs({0, 1, 2, 0, 2, 3}); + Omega_h::Mesh mesh(&lib); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); + mesh.add_tag( + 0, "class_dim", 1, + Omega_h::Read(mesh.nverts(), Omega_h::I8(0))); + mesh.add_tag( + 0, "class_id", 1, + Omega_h::Read(mesh.nverts(), Omega_h::ClassId(0))); + mesh.add_tag( + 1, "class_dim", 1, + Omega_h::Read(mesh.nedges(), Omega_h::I8(1))); + mesh.add_tag( + 1, "class_id", 1, + Omega_h::Read(mesh.nedges(), Omega_h::ClassId(0))); + mesh.add_tag( + 2, "class_dim", 1, + Omega_h::Read(mesh.nelems(), Omega_h::I8(2))); + mesh.add_tag( + 2, "class_id", 1, + Omega_h::Read(mesh.nelems(), Omega_h::ClassId(0))); + return mesh; +} + +// Independent reference for the assembled conservative load vector +// b_j = \int phi_j^target f dx +// when f is exactly representable in the target order-1 space (constant or +// affine): the integral then equals (M g)_j, where M is the target consistent +// mass matrix and g_k = f(target node k). Assembled here from per-element mass +// matrices on the target mesh alone — no intersection or source-mesh machinery +// — so it cross-checks the integrator instead of restating its computation. +// +// The result is keyed by global DOF id to match the PETSc row indexing of the +// integrator's assembled vector. +std::unordered_map ExpectedLoadByGid( + Omega_h::Mesh& target_mesh, const pcms::OmegaHLagrangeLayout& target_layout, + const std::vector& g_by_vert) +{ + const auto coords_h = Omega_h::HostRead(target_mesh.coords()); + const auto faces2nodes_h = Omega_h::HostRead( + target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b); + const auto& gids = target_layout.GetGidsHost(); + + std::unordered_map b; + for (int e = 0; e < target_mesh.nelems(); ++e) { + const int v[3] = {faces2nodes_h[3 * e + 0], faces2nodes_h[3 * e + 1], + faces2nodes_h[3 * e + 2]}; + const double x0 = coords_h[2 * v[0]], y0 = coords_h[2 * v[0] + 1]; + const double x1 = coords_h[2 * v[1]], y1 = coords_h[2 * v[1] + 1]; + const double x2 = coords_h[2 * v[2]], y2 = coords_h[2 * v[2] + 1]; + const double area = + 0.5 * std::abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)); + + // Consistent P1 element mass matrix M^e = (area/12) [[2,1,1],[1,2,1], + // [1,1,2]], so (M^e g)_j = (area/12) (g_j + sum_k g_k). + const double g_sum = g_by_vert[v[0]] + g_by_vert[v[1]] + g_by_vert[v[2]]; + for (int j = 0; j < 3; ++j) { + const double bj = (area / 12.0) * (g_by_vert[v[j]] + g_sum); + b[static_cast(gids[v[j]])] += static_cast(bj); + } + } + return b; +} + +// Assembles the integrator's load vector for source_field and compares it, +// per global DOF id, against the supplied expected values. +void CheckAssembledLoadMatches( + const std::shared_ptr& source_space, + pcms::LinearFormIntegrator& integrator, + const pcms::Field& source_field, + const std::unordered_map& expected) +{ + const auto& pts = integrator.GetIntegrationPoints(); + const std::size_t npts = pts.GetCoordinates().GetValues().extent(0); + + auto evaluator = source_space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(pts.GetCoordinates())); + Kokkos::View sampled("sampled", npts, + 1); + evaluator->Evaluate(source_field, pcms::MakeRank2View(sampled)); + + integrator.Assemble(pcms::MakeConstRank2View(sampled)); + Vec vec = integrator.GetVector(); + + const PetscScalar* vec_array = nullptr; + VecGetArrayRead(vec, &vec_array); + for (const auto& [gid, ref_val] : expected) { + const pcms::Real got = static_cast(vec_array[gid]); + CAPTURE(gid, ref_val, got); + CHECK(got == Catch::Approx(ref_val).epsilon(1e-10)); + } + VecRestoreArrayRead(vec, &vec_array); + // vec is owned by integrator; no VecDestroy here. +} + +} // namespace + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +TEST_CASE("OmegaHIntersectionRHSIntegrator: integration points lie inside " + "target domain", + "[rhs_integrator]") +{ + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto integrator = + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + + const auto raw_coords = + integrator->GetIntegrationPoints().GetCoordinates().GetValues(); + auto raw_coords_view = Kokkos::View>( + raw_coords.data_handle(), raw_coords.extent(0), raw_coords.extent(1)); + auto raw_coords_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, raw_coords_view); + const std::size_t n = raw_coords_host.extent(0); + + REQUIRE(n > 0); + for (std::size_t i = 0; i < n; ++i) { + CAPTURE(i, raw_coords_host(i, 0), raw_coords_host(i, 1)); + CHECK(raw_coords_host(i, 0) >= -1e-12); + CHECK(raw_coords_host(i, 0) <= 1.0 + 1e-12); + CHECK(raw_coords_host(i, 1) >= -1e-12); + CHECK(raw_coords_host(i, 1) <= 1.0 + 1e-12); + } +} + +TEST_CASE("OmegaHIntersectionRHSIntegrator: zero source field gives zero " + "assembled vector", + "[rhs_integrator]") +{ + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto source_field = source_space->CreateFunction(); + // Default-constructed field has zero data. + + auto integrator = + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + const auto& pts = integrator->GetIntegrationPoints(); + const std::size_t npts = pts.GetCoordinates().GetValues().extent(0); + + auto evaluator = source_space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(pts.GetCoordinates())); + + Kokkos::View sampled("sampled", npts, + 1); + evaluator->Evaluate(source_field, pcms::MakeRank2View(sampled)); + + integrator->Assemble(pcms::MakeConstRank2View(sampled)); + Vec vec = integrator->GetVector(); + + PetscReal norm = 0.0; + VecNorm(vec, NORM_INFINITY, &norm); + INFO("vector inf-norm=" << norm); + CHECK(static_cast(norm) == Catch::Approx(0.0).margin(1e-14)); + // vec is owned by integrator; no VecDestroy here. +} + +TEST_CASE("OmegaHIntersectionRHSIntegrator: constant field matches " + "consistent-mass load", + "[rhs_integrator]") +{ + // For a constant source field the assembled load vector must equal the + // target consistent mass matrix applied to the constant nodal vector. + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + const double c = 2.0; + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return c; }); + + const auto target_layout = + std::dynamic_pointer_cast( + target_space->GetLayout()); + const std::vector g(target_mesh.nverts(), c); + const auto expected = ExpectedLoadByGid(target_mesh, *target_layout, g); + + auto integrator = + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + CheckAssembledLoadMatches(source_space, *integrator, source_field, expected); +} + +TEST_CASE( + "OmegaHIntersectionRHSIntegrator: linear field matches consistent-mass load", + "[rhs_integrator]") +{ + // f(x,y) = x + y exercises non-trivial quadrature paths that a constant + // field cannot catch (a constant integrates exactly under any rule, while a + // linear field requires at least 1st-order accuracy). The field is affine, + // hence exactly representable in the target P1 space, so the assembled load + // vector must equal the target consistent mass matrix applied to the nodal + // values of x + y. + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + y; }); + + const auto target_layout = + std::dynamic_pointer_cast( + target_space->GetLayout()); + const auto tgt_coords_h = + Omega_h::HostRead(target_mesh.coords()); + std::vector g(target_mesh.nverts()); + for (int i = 0; i < target_mesh.nverts(); ++i) { + g[i] = tgt_coords_h[2 * i + 0] + tgt_coords_h[2 * i + 1]; + } + const auto expected = ExpectedLoadByGid(target_mesh, *target_layout, g); + + auto integrator = + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + CheckAssembledLoadMatches(source_space, *integrator, source_field, expected); +} + +TEST_CASE("OmegaHIntersectionRHSIntegrator: rejects invalid layouts", + "[rhs_integrator]") +{ + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + + SECTION("multi-component source space throws") + { + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 2, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS( + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); + } + + SECTION("multi-component target space throws") + { + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 2, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS( + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); + } + + SECTION("non-Cartesian source coordinate system throws") + { + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cylindrical, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS( + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); + } + + SECTION("non-Cartesian target coordinate system throws") + { + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cylindrical, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS( + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); + } +} diff --git a/test/test_omega_h_lagrange_field.cpp b/test/test_omega_h_lagrange_field.cpp index 259a0165..c17d45b1 100644 --- a/test/test_omega_h_lagrange_field.cpp +++ b/test/test_omega_h_lagrange_field.cpp @@ -115,8 +115,8 @@ TEST_CASE("OmegaHLagrangeLayout layout sharing") auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto f1 = factory.CreateField(pcms::FieldMetadata{}); - auto f2 = factory.CreateField(pcms::FieldMetadata{}); + auto f1 = factory->CreateFunction(); + auto f2 = factory->CreateFunction(); REQUIRE(&f1.GetLayout() == &f2.GetLayout()); } @@ -129,9 +129,9 @@ TEST_CASE("OmegaHLagrangeField order-1: set/get DOF data round-trip") auto mesh = MakeBox2D(lib.world()); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); - int n = factory.GetLayout()->GetNumOwnedDofHolder(); + int n = factory->GetLayout()->GetNumOwnedDofHolder(); std::vector data(n); for (int i = 0; i < n; ++i) data[i] = static_cast(i); @@ -151,10 +151,10 @@ TEST_CASE("OmegaHLagrangeField order-1: linear function evaluation") auto mesh = MakeBox2D(lib.world(), 20, 20); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), + field.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); pcms::test::CheckEvaluation( factory, field, pcms::test::StandardEvalCoords2D(), @@ -169,10 +169,10 @@ TEST_CASE("MeshFieldsAdapter order-1: linear function evaluation (shared util)") auto mesh = MakeBox2D(lib.world(), 20, 20); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), + field.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); pcms::test::CheckEvaluation( factory, field, pcms::test::StandardEvalCoords2D(), @@ -185,10 +185,10 @@ TEST_CASE("OmegaHLagrangeField order-1: out-of-bounds FILL mode") auto mesh = MakeBox2D(lib.world()); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), + field.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); Real fill_value = -999.0; @@ -202,12 +202,12 @@ TEST_CASE("OmegaHLagrangeField order-1: serialize / deserialize round-trip") auto mesh = MakeBox2D(lib.world()); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), + field.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); - pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); + pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); } TEST_CASE( @@ -221,9 +221,9 @@ TEST_CASE( auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, nc, pcms::CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); - const int n = factory.GetLayout()->GetNumOwnedDofHolder(); + 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) @@ -231,7 +231,7 @@ TEST_CASE( field.GetData().SetDOFHolderDataHost( pcms::Rank2View(data.data(), n, nc)); - pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); + pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); } // ---- Order-0 field tests ---------------------------------------------------- @@ -242,9 +242,9 @@ TEST_CASE("OmegaHLagrangeField order-0: set/get DOF data round-trip") auto mesh = MakeBox2D(lib.world()); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 0, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); - int n = factory.GetLayout()->GetNumOwnedDofHolder(); + int n = factory->GetLayout()->GetNumOwnedDofHolder(); std::vector data(n, 3.14); pcms::Rank2View view(data.data(), n, 1); field.GetData().SetDOFHolderDataHost(view); @@ -261,7 +261,7 @@ TEST_CASE("OmegaHLagrangeField order-0: constant field evaluation") auto mesh = MakeBox2D(lib.world(), 10, 10); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 0, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); const Real kValue = 42.0; int nelems = mesh.nelems(); @@ -273,8 +273,8 @@ TEST_CASE("OmegaHLagrangeField order-0: constant field evaluation") auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; auto device_coords = - pcms::test::CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); - auto evaluator = factory.CreatePointEvaluator( + pcms::test::CreateDeviceCoordinateView(pts, factory->GetCoordinateSystem()); + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View eval_device("eval", n); @@ -296,7 +296,7 @@ TEST_CASE("OmegaHLagrangeField order-0: out-of-bounds FILL mode") auto mesh = MakeBox2D(lib.world()); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 0, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); int nelems = mesh.nelems(); std::vector data(nelems, 1.0); @@ -315,7 +315,7 @@ TEST_CASE("OmegaHLagrangeField order-0: serialize / deserialize round-trip") auto mesh = MakeBox2D(lib.world()); auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 0, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); int nelems = mesh.nelems(); std::vector data(nelems); @@ -325,7 +325,7 @@ TEST_CASE("OmegaHLagrangeField order-0: serialize / deserialize round-trip") 1); field.GetData().SetDOFHolderDataHost(view); - pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); + pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); } // ---- Layout sharing communicator contract ----------------------------------- @@ -353,7 +353,7 @@ TEST_CASE("OmegaHLagrangeField: field valid after layout destruction") { auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - field.emplace(factory.CreateField(pcms::FieldMetadata{})); + field.emplace(factory->CreateFunction()); } // factory goes out of scope; field keeps layout alive pcms::test::SetField( diff --git a/test/test_omega_h_mass_integrator.cpp b/test/test_omega_h_mass_integrator.cpp new file mode 100644 index 00000000..f77fd85f --- /dev/null +++ b/test/test_omega_h_mass_integrator.cpp @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +namespace +{ + +Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib) +{ + const Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); + Omega_h::LOs ev2v({0, 1, 3, 1, 2, 3}); + Omega_h::Mesh mesh(&lib); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); + mesh.add_tag( + 0, "class_dim", 1, + Omega_h::Read(mesh.nverts(), Omega_h::I8(0))); + mesh.add_tag( + 0, "class_id", 1, + Omega_h::Read(mesh.nverts(), Omega_h::ClassId(0))); + mesh.add_tag( + 1, "class_dim", 1, + Omega_h::Read(mesh.nedges(), Omega_h::I8(1))); + mesh.add_tag( + 1, "class_id", 1, + Omega_h::Read(mesh.nedges(), Omega_h::ClassId(0))); + mesh.add_tag( + 2, "class_dim", 1, + Omega_h::Read(mesh.nelems(), Omega_h::I8(2))); + mesh.add_tag( + 2, "class_id", 1, + Omega_h::Read(mesh.nelems(), Omega_h::ClassId(0))); + return mesh; +} + +std::map, pcms::Real> BuildReferenceMassMap( + Omega_h::Mesh& mesh, const pcms::FunctionSpace& space) +{ + using namespace pcms; + + MeshField::OmegahMeshField + omf(mesh); + auto coordField = omf.getCoordField(); + const auto [shp, map] = MeshField::Omegah::getTriangleElement<1>(mesh); + MeshField::FieldElement coordFe(mesh.nelems(), coordField, shp, map); + auto elm_mass_dev = buildElementMassMatrix(mesh, coordFe); + auto elm_mass_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, elm_mass_dev); + + const auto layout = + std::dynamic_pointer_cast(space.GetLayout()); + REQUIRE(layout != nullptr); + const auto& faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& gids = layout->GetGidsHost(); + + std::map, Real> ref; + for (int e = 0; e < mesh.nelems(); ++e) { + const auto verts = Omega_h::gather_verts<3>(faces2nodes, e); + for (int i = 0; i < 3; ++i) { + const GO row = static_cast(gids[verts[i]]); + for (int j = 0; j < 3; ++j) { + const GO col = static_cast(gids[verts[j]]); + ref[{row, col}] += elm_mass_host(e * 9 + i * 3 + j); + } + } + } + return ref; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +TEST_CASE( + "OmegaHMassIntegrator: assembled matrix matches buildElementMassMatrix", + "[mass_integrator]") +{ + Omega_h::Library lib; + auto mesh = BuildUnitSquare(lib); + + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + const auto ref = BuildReferenceMassMap(mesh, *space); + + auto integrator = pcms::BuildOmegaHMassIntegrator(*space); + Mat mat = integrator->GetMatrix(); + + for (const auto& [key, ref_val] : ref) { + PetscInt r = static_cast(key.first); + PetscInt c = static_cast(key.second); + PetscScalar got = 0.0; + MatGetValues(mat, 1, &r, 1, &c, &got); + CAPTURE(key.first, key.second, ref_val, got); + CHECK(static_cast(got) == + Catch::Approx(ref_val).epsilon(1e-12)); + } + // mat is owned by integrator; no MatDestroy here. +} + +TEST_CASE("OmegaHMassIntegrator: row sums match lumped mass", + "[mass_integrator]") +{ + // For a consistent mass matrix M, the row sums equal the lumped mass at + // each node: row_sum(i) = integral(N_i dx). For a regular mesh of unit + // area with uniform nodal distribution the sum over all nodes equals 1. + Omega_h::Library lib; + auto mesh = BuildUnitSquare(lib); + + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + const auto layout = + std::dynamic_pointer_cast( + space->GetLayout()); + const auto& gids = layout->GetGidsHost(); + const int nverts = mesh.nverts(); + + auto integrator = pcms::BuildOmegaHMassIntegrator(*space); + Mat mat = integrator->GetMatrix(); + + // Sum all row sums. + pcms::Real grand_total = 0.0; + for (int v = 0; v < nverts; ++v) { + const pcms::GO row = static_cast(gids[v]); + pcms::Real row_sum = 0.0; + for (int u = 0; u < nverts; ++u) { + const pcms::GO col = static_cast(gids[u]); + PetscInt r = static_cast(row); + PetscInt c = static_cast(col); + PetscScalar val = 0.0; + MatGetValues(mat, 1, &r, 1, &c, &val); + row_sum += static_cast(val); + } + CAPTURE(v, row, row_sum); + CHECK(row_sum > 0.0); // each row sum must be positive + grand_total += row_sum; + } + + // Total == area of domain == 1. + CHECK(grand_total == Catch::Approx(1.0).epsilon(1e-10)); + // mat is owned by integrator; no MatDestroy here. +} + +TEST_CASE("OmegaHMassIntegrator: rejects invalid layouts", "[mass_integrator]") +{ + Omega_h::Library lib; + auto mesh = BuildUnitSquare(lib); + + SECTION("multi-component space throws") + { + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 2, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(*space)); + } + + SECTION("non-Cartesian coordinate system throws") + { + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cylindrical, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(*space)); + } +} diff --git a/test/test_omega_h_mc_rhs_integrator.cpp b/test/test_omega_h_mc_rhs_integrator.cpp new file mode 100644 index 00000000..eabe5404 --- /dev/null +++ b/test/test_omega_h_mc_rhs_integrator.cpp @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" +#include +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +namespace +{ + +// Build a unit-square 2D simplex mesh. +// diagonal=0: T0=(0,1,3), T1=(1,2,3) +// diagonal=1: T0=(0,1,2), T1=(0,2,3) +Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, int diagonal) +{ + const Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); + Omega_h::LOs ev2v = (diagonal == 0) ? Omega_h::LOs({0, 1, 3, 1, 2, 3}) + : Omega_h::LOs({0, 1, 2, 0, 2, 3}); + Omega_h::Mesh mesh(&lib); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); + mesh.add_tag( + 0, "class_dim", 1, + Omega_h::Read(mesh.nverts(), Omega_h::I8(0))); + mesh.add_tag( + 0, "class_id", 1, + Omega_h::Read(mesh.nverts(), Omega_h::ClassId(0))); + mesh.add_tag( + 1, "class_dim", 1, + Omega_h::Read(mesh.nedges(), Omega_h::I8(1))); + mesh.add_tag( + 1, "class_id", 1, + Omega_h::Read(mesh.nedges(), Omega_h::ClassId(0))); + mesh.add_tag( + 2, "class_dim", 1, + Omega_h::Read(mesh.nelems(), Omega_h::I8(2))); + mesh.add_tag( + 2, "class_id", 1, + Omega_h::Read(mesh.nelems(), Omega_h::ClassId(0))); + return mesh; +} + +std::shared_ptr MakeP1Space(Omega_h::Mesh& mesh) +{ + return pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); +} + +// Evaluates source_field at the integrator's sample points and assembles. +void EvaluateAndAssemble( + pcms::LinearFormIntegrator& integrator, + const std::shared_ptr& source_space, + const pcms::Field& source_field) +{ + const auto& pts = integrator.GetIntegrationPoints(); + const std::size_t npts = pts.GetCoordinates().GetValues().extent(0); + auto evaluator = source_space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(pts.GetCoordinates())); + Kokkos::View sampled("sampled", npts, + 1); + evaluator->Evaluate(source_field, pcms::MakeRank2View(sampled)); + integrator.Assemble(pcms::MakeConstRank2View(sampled)); +} + +} // namespace + +// --------------------------------------------------------------------------- +// Monte Carlo RHS integrator +// --------------------------------------------------------------------------- + +TEST_CASE("OmegaHMonteCarloRHSIntegrator: sample points lie inside the domain", + "[mc_rhs_integrator]") +{ + Omega_h::Library lib; + auto target_mesh = BuildUnitSquare(lib, 0); + auto target_space = MakeP1Space(target_mesh); + + const int samples_per_element = 16; + for (const auto sampling : {pcms::MonteCarloSampling::UniformRandom}) { + pcms::OmegaHMonteCarloRHSIntegrator integrator( + *target_space, samples_per_element, sampling); + const auto raw_coords = + integrator.GetIntegrationPoints().GetCoordinates().GetValues(); + auto coords_view = Kokkos::View>( + raw_coords.data_handle(), raw_coords.extent(0), raw_coords.extent(1)); + auto coords_h = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coords_view); + + REQUIRE( + coords_h.extent(0) == + static_cast(target_mesh.nelems() * samples_per_element)); + for (std::size_t i = 0; i < coords_h.extent(0); ++i) { + CAPTURE(i, coords_h(i, 0), coords_h(i, 1)); + CHECK(coords_h(i, 0) >= -1e-12); + CHECK(coords_h(i, 0) <= 1.0 + 1e-12); + CHECK(coords_h(i, 1) >= -1e-12); + CHECK(coords_h(i, 1) <= 1.0 + 1e-12); + } + } +} + +TEST_CASE("OmegaHMonteCarloRHSIntegrator: constant field integrates exactly", + "[mc_rhs_integrator]") +{ + // For f = c the estimator sums to c * |domain| for any sample placement, + // because the P1 basis functions partition unity at every sample point. + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); + + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return 2.0; }); + + for (const auto sampling : {pcms::MonteCarloSampling::UniformRandom}) { + pcms::OmegaHMonteCarloRHSIntegrator integrator(*target_space, 8, sampling); + EvaluateAndAssemble(integrator, source_space, source_field); + + PetscScalar sum = 0.0; + VecSum(integrator.GetVector(), &sum); + INFO("sampling=" << "UniformRandom"); + CHECK(static_cast(sum) == Catch::Approx(2.0).margin(1e-12)); + } +} + +// --------------------------------------------------------------------------- +// Control variate projection +// --------------------------------------------------------------------------- + +TEST_CASE("OmegaHControlVariateProjection: exact for fields in the target " + "space even with few samples", + "[mc_rhs_integrator][control_variate]") +{ + // For a globally linear field the control variate (target-space interpolant + // of the source field) equals the source field, so the sampled residual is + // identically zero and the projection is exact regardless of sample count. + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); + + auto source_field = source_space->CreateFunction(); + auto target_field = target_space->CreateFunction(); + pcms::test::SetField( + source_field, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return 3.0 * x - y + 0.25; }); + + pcms::OmegaHControlVariateProjection projection( + *source_space, *target_space, /*samples_per_element=*/4, + pcms::MonteCarloSampling::UniformRandom); + projection.Apply(source_field, target_field); + + const auto values = + pcms::FlattenToRank1View(target_field.GetDOFHolderDataHost()); + const auto coords_h = Omega_h::HostRead(target_mesh.coords()); + REQUIRE(static_cast(values.size()) == target_mesh.nverts()); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = + 3.0 * coords_h[2 * i + 0] - coords_h[2 * i + 1] + 0.25; + CAPTURE(i, expected, values[i]); + CHECK(values[i] == Catch::Approx(expected).margin(1e-9)); + } +} + +TEST_CASE("OmegaHControlVariateProjection: reduces error vs plain Monte Carlo", + "[mc_rhs_integrator][control_variate]") +{ + // For a non-linear field the control variate does not cancel f exactly, but + // the residual is much smaller than f, so with identical sample counts and + // seeds the control-variate projection must be closer to the exact + // (intersection-quadrature) projection than the plain Monte Carlo one. + Omega_h::Library lib; + auto source_mesh = BuildUnitSquare(lib, 1); + auto target_mesh = BuildUnitSquare(lib, 0); + auto source_space = MakeP1Space(source_mesh); + auto target_space = MakeP1Space(target_mesh); + + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return x * x + x * y + 0.5 * y * y; + }); + + // Reference: exact mesh-intersection quadrature projection. + auto reference_field = target_space->CreateFunction(); + pcms::OmegaHConservativeProjection reference_projection(*source_space, + *target_space); + reference_projection.Apply(source_field, reference_field); + const auto reference = + pcms::FlattenToRank1View(reference_field.GetDOFHolderDataHost()); + + const int samples_per_element = 64; + const uint64_t seed = 20240611; + + // Plain Monte Carlo projection. + pcms::OmegaHMonteCarloRHSIntegrator mc_integrator( + *target_space, samples_per_element, pcms::MonteCarloSampling::UniformRandom, + seed); + auto evaluator = source_space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates( + mc_integrator.GetIntegrationPoints().GetCoordinates())); + pcms::OmegaHMassIntegrator mass_integrator(*target_space); + pcms::GalerkinProjectionSolver solver(mass_integrator, mc_integrator); + const auto mc_projected = solver.Solve(*evaluator, source_field); + const auto mc_projected_h = Omega_h::HostRead(mc_projected); + + // Control-variate projection with the same sampling configuration. + auto cv_field = target_space->CreateFunction(); + pcms::OmegaHControlVariateProjection cv_projection( + *source_space, *target_space, samples_per_element, + pcms::MonteCarloSampling::UniformRandom, seed); + cv_projection.Apply(source_field, cv_field); + const auto cv_values = + pcms::FlattenToRank1View(cv_field.GetDOFHolderDataHost()); + + double mc_error = 0.0; + double cv_error = 0.0; + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + mc_error = std::max(mc_error, std::abs(mc_projected_h[i] - reference[i])); + cv_error = std::max(cv_error, std::abs(cv_values[i] - reference[i])); + } + CAPTURE(mc_error, cv_error); + CHECK(cv_error < mc_error); +} diff --git a/test/test_point_evaluator.cpp b/test/test_point_evaluator.cpp index fa0fc2f5..dcc57087 100644 --- a/test/test_point_evaluator.cpp +++ b/test/test_point_evaluator.cpp @@ -31,16 +31,16 @@ TEST_CASE("PointEvaluator: OmegaH order-1 linear evaluation") mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), + field_data.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = factory.CreatePointEvaluator( + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); pcms::test::CheckEvaluation( *evaluator, field_data, pts, @@ -61,15 +61,15 @@ TEST_CASE("PointEvaluator: same evaluator reused for two FieldData objects") mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_a = factory.CreateField(); - auto field_b = factory.CreateField(); + auto field_a = factory->CreateFunction(); + auto field_b = factory->CreateFunction(); // field_a: linear_f; field_b: constant 42 pcms::test::SetField( - field_a.GetData(), *factory.GetLayout(), + field_a.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); pcms::test::SetField( - field_b.GetData(), *factory.GetLayout(), + field_b.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); auto pts = pcms::test::StandardEvalCoords2D(); @@ -78,7 +78,7 @@ TEST_CASE("PointEvaluator: same evaluator reused for two FieldData objects") pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); // Create the PointEvaluator once - auto evaluator = factory.CreatePointEvaluator( + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View out_a_device("out_a", n); @@ -122,9 +122,9 @@ TEST_CASE("PointEvaluator: OmegaH order-1 out-of-bounds fill") mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), + field_data.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); // Points clearly outside [0,1]^2 @@ -133,9 +133,9 @@ TEST_CASE("PointEvaluator: OmegaH order-1 out-of-bounds fill") auto device_coords = pcms::test::CreateDeviceCoordinateView( outside_pts, CoordinateSystem::Cartesian); pcms::OutOfBoundsPolicy policy{pcms::OutOfBoundsMode::FILL, -999.0}; - auto evaluator = - factory.CreatePointEvaluator(pcms::EvaluationRequest::FromCoordinates( - device_coords.coordinate_view, policy)); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view, + policy)); pcms::test::CheckFillMode(*evaluator, field_data, -999.0, outside_pts); } @@ -155,15 +155,15 @@ TEST_CASE("PointEvaluator: UniformGrid order-1 linear evaluation") auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, CoordinateSystem::Cartesian, 1); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), + field_data.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = factory.CreatePointEvaluator( + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); pcms::test::CheckEvaluation( *evaluator, field_data, pts, @@ -181,15 +181,15 @@ TEST_CASE("PointEvaluator: SplineFunctionSpace uniform-grid evaluation") auto factory = pcms::SplineFunctionSpace::FromUniformGrid( grid, CoordinateSystem::Cartesian); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), + field_data.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = factory.CreatePointEvaluator( + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); pcms::test::CheckEvaluation( *evaluator, field_data, pts, @@ -209,7 +209,7 @@ TEST_CASE("FieldLayout: metadata queries") auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); auto coords = layout->GetDOFHolderCoordinates(); REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); @@ -230,9 +230,9 @@ TEST_CASE("FieldData: layout metadata queries") auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); - auto coords = factory.GetLayout()->GetDOFHolderCoordinates(); + auto coords = factory->GetLayout()->GetDOFHolderCoordinates(); REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); REQUIRE(coords.GetValues().extent(0) > 0); REQUIRE(coords.GetValues().extent(1) == 2); @@ -251,9 +251,9 @@ TEST_CASE("SimpleFieldData: set and get DOF holder data round-trip") auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); - auto& layout = *factory.GetLayout(); + auto& layout = *factory->GetLayout(); int n = layout.GetNumOwnedDofHolder(); REQUIRE(n > 0); @@ -288,7 +288,7 @@ TEST_CASE("FieldLayout: MeshFields metadata queries") mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); auto coords = layout->GetDOFHolderCoordinates(); REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); REQUIRE(coords.GetValues().extent(0) > 0); @@ -305,16 +305,16 @@ TEST_CASE("PointEvaluator: MeshFields order-1 linear evaluation") mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), + field_data.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = factory.CreatePointEvaluator( + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); pcms::test::CheckEvaluation( *evaluator, field_data, pts, @@ -331,9 +331,9 @@ TEST_CASE("PointEvaluator: MeshFields out-of-bounds fill") mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), + field_data.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); const std::vector outside_pts = {-0.5, 0.5, 1.5, 0.5, @@ -341,9 +341,9 @@ TEST_CASE("PointEvaluator: MeshFields out-of-bounds fill") auto device_coords = pcms::test::CreateDeviceCoordinateView( outside_pts, CoordinateSystem::Cartesian); pcms::OutOfBoundsPolicy policy{pcms::OutOfBoundsMode::FILL, -999.0}; - auto evaluator = - factory.CreatePointEvaluator(pcms::EvaluationRequest::FromCoordinates( - device_coords.coordinate_view, policy)); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view, + policy)); pcms::test::CheckFillMode(*evaluator, field_data, -999.0, outside_pts); } @@ -358,13 +358,13 @@ TEST_CASE( mesh, 1, 1, CoordinateSystem::Cartesian, "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto field_a = factory.CreateField(); - auto field_b = factory.CreateField(); + auto field_a = factory->CreateFunction(); + auto field_b = factory->CreateFunction(); pcms::test::SetField( - field_a.GetData(), *factory.GetLayout(), + field_a.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); pcms::test::SetField( - field_b.GetData(), *factory.GetLayout(), + field_b.GetData(), *factory->GetLayout(), OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); auto pts = pcms::test::StandardEvalCoords2D(); @@ -372,7 +372,7 @@ TEST_CASE( auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = factory.CreatePointEvaluator( + auto evaluator = factory->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View out_a_device("out_a", n); diff --git a/test/test_polynomial_reconstruction_function_space.cpp b/test/test_polynomial_reconstruction_function_space.cpp index 1f3b675a..0281c13f 100644 --- a/test/test_polynomial_reconstruction_function_space.cpp +++ b/test/test_polynomial_reconstruction_function_space.cpp @@ -37,7 +37,7 @@ TEST_CASE( auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); REQUIRE(layout->GetNumComponents() == 1); REQUIRE(layout->GetNumOwnedDofHolder() == 4); @@ -64,8 +64,8 @@ TEST_CASE("PolynomialReconstructionFunctionSpace fields share layout") auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - auto source = factory.CreateField(pcms::FieldMetadata{}); - auto target = factory.CreateField(pcms::FieldMetadata{}); + auto source = factory->CreateFunction(); + auto target = factory->CreateFunction(); REQUIRE(&source.GetLayout() == &target.GetLayout()); } @@ -78,7 +78,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field set/get DOF " auto field = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian) - .CreateField(pcms::FieldMetadata{}); + ->CreateFunction(); std::vector data{1.0, 2.0, 3.0, 4.0}; Rank2View data_view( @@ -100,14 +100,14 @@ TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field serialize / " auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + 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()); + pcms::test::CheckSerializeDeserialize(*factory->GetLayout(), field.GetData()); } TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " @@ -119,7 +119,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " auto field = [&]() { auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - return factory.CreateField(pcms::FieldMetadata{}); + return factory->CreateFunction(); }(); auto point_cloud_layout = @@ -148,15 +148,15 @@ TEST_CASE("Different layouts on the same mesh report SameEntities") auto lagrange = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, CoordinateSystem::Cartesian); - auto nodal_disc = nodal.GetLayout()->GetDiscretization(); - auto lagrange_disc = lagrange.GetLayout()->GetDiscretization(); + auto nodal_disc = nodal->GetLayout()->GetDiscretization(); + auto lagrange_disc = lagrange->GetLayout()->GetDiscretization(); REQUIRE(nodal_disc != nullptr); REQUIRE(lagrange_disc != nullptr); REQUIRE(nodal_disc->SameEntities(*lagrange_disc)); - REQUIRE(nodal.GetLayout()->GetNumOwnedDofHolder() == mesh.nfaces()); - REQUIRE(lagrange.GetLayout()->GetNumOwnedDofHolder() == mesh.nverts()); + REQUIRE(nodal->GetLayout()->GetNumOwnedDofHolder() == mesh.nfaces()); + REQUIRE(lagrange->GetLayout()->GetNumOwnedDofHolder() == mesh.nverts()); REQUIRE(nodal_disc->GetNumEntities(pcms::Face) == mesh.nfaces()); REQUIRE(lagrange_disc->GetNumEntities(pcms::Vertex) == mesh.nverts()); @@ -175,8 +175,8 @@ TEST_CASE("Layouts on different meshes do not report SameEntities") auto nodal_b = pcms::PolynomialReconstructionFunctionSpace::FromMesh( mesh_b, pcms::Vertex, CoordinateSystem::Cartesian); - auto disc_a = nodal_a.GetLayout()->GetDiscretization(); - auto disc_b = nodal_b.GetLayout()->GetDiscretization(); + auto disc_a = nodal_a->GetLayout()->GetDiscretization(); + auto disc_b = nodal_b->GetLayout()->GetDiscretization(); REQUIRE_FALSE(disc_a->SameEntities(*disc_b)); } @@ -196,8 +196,8 @@ TEST_CASE( auto standalone = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - auto mesh_disc = lagrange.GetLayout()->GetDiscretization(); - auto point_cloud_disc = standalone.GetLayout()->GetDiscretization(); + auto mesh_disc = lagrange->GetLayout()->GetDiscretization(); + auto point_cloud_disc = standalone->GetLayout()->GetDiscretization(); REQUIRE_FALSE(mesh_disc->SameEntities(*point_cloud_disc)); REQUIRE_FALSE(point_cloud_disc->SameEntities(*mesh_disc)); diff --git a/test/test_polynomial_reconstruction_mls_evaluation.cpp b/test/test_polynomial_reconstruction_mls_evaluation.cpp index 8b979e68..888a83b1 100644 --- a/test/test_polynomial_reconstruction_mls_evaluation.cpp +++ b/test/test_polynomial_reconstruction_mls_evaluation.cpp @@ -119,13 +119,13 @@ void CheckPolynomialReproduction(unsigned degree, auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian, SweepTestOptions(degree, basis)); - auto field = fs.CreateField(pcms::FieldMetadata{}); - pcms::test::SetField(field.GetData(), *fs.GetLayout(), func); + auto field = fs->CreateFunction(); + pcms::test::SetField(field.GetData(), *fs->GetLayout(), func); auto pts = QueryPoints(); auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); pcms::test::CheckEvaluation(*evaluator, field, pts, func, abs_tol); } @@ -170,22 +170,22 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: same PointEvaluator " auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian, DefaultTestOptions()); - auto field_a = fs.CreateField(pcms::FieldMetadata{}); - auto field_b = fs.CreateField(pcms::FieldMetadata{}); + auto field_a = fs->CreateFunction(); + auto field_b = fs->CreateFunction(); pcms::test::SetField( - field_a.GetData(), *fs.GetLayout(), + field_a.GetData(), *fs->GetLayout(), OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); const Real cval = 7.0; pcms::test::SetField( - field_b.GetData(), *fs.GetLayout(), + field_b.GetData(), *fs->GetLayout(), OMEGA_H_LAMBDA(Real, Real) { return cval; }); auto pts = QueryPoints(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View out_a_device("out_a", n); @@ -226,13 +226,13 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: Evaluate throws for " auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - auto field = fs.CreateField(pcms::FieldMetadata{}); + auto field = fs->CreateFunction(); auto pts = QueryPoints(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); // Two-component output — must throw @@ -258,16 +258,16 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: default MLSOptions — " // Use default options — no third argument auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian); - auto field = fs.CreateField(pcms::FieldMetadata{}); + auto field = fs->CreateFunction(); pcms::test::SetField( - field.GetData(), *fs.GetLayout(), + field.GetData(), *fs->GetLayout(), OMEGA_H_LAMBDA(Real, Real) { return Real(1.0); }); auto pts = QueryPoints(); int n = static_cast(pts.size()) / 2; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View out_device("out", n); @@ -296,7 +296,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " auto pts = QueryPoints(); auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - REQUIRE_THROWS(fs.CreatePointEvaluator( + REQUIRE_THROWS(fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view))); } @@ -313,7 +313,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " auto pts = QueryPoints(); auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cylindrical); - REQUIRE_THROWS(fs.CreatePointEvaluator( + REQUIRE_THROWS(fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view))); } @@ -332,7 +332,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: radius option is " auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian, opts); - auto field = fs.CreateField(pcms::FieldMetadata{}); + auto field = fs->CreateFunction(); std::vector dof_values{1.0, 5.0}; field.GetData().SetDOFHolderDataHost(Rank2View( dof_values.data(), static_cast(dof_values.size()), 1)); @@ -340,7 +340,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: radius option is " std::vector pts{0.0, 0.0}; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View out_device("out", 1); using LayoutPolicy = @@ -364,7 +364,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( coords_view, CoordinateSystem::Cartesian, DefaultTestOptions3D()); auto layout_coords_device = - fs.GetLayout()->GetDOFHolderCoordinates().GetValues(); + fs->GetLayout()->GetDOFHolderCoordinates().GetValues(); auto layout_coords = pcms::test::CopyCoordinatesToHost(layout_coords_device, 27, 3); REQUIRE(static_cast(layout_coords.extent(1)) == 3); @@ -374,7 +374,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " REQUIRE(layout_coords(i, 2) == Catch::Approx(src[3 * i + 2])); } - auto field = fs.CreateField(pcms::FieldMetadata{}); + auto field = fs->CreateFunction(); std::vector dof_values(27); for (int i = 0; i < 27; ++i) { const Real x = src[3 * i + 0]; @@ -388,7 +388,7 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " std::vector pts{0.5, 0.5, 0.25, 0.5, 0.5, 0.75}; auto device_coords = pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian, 3); - auto evaluator = fs.CreatePointEvaluator( + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); Kokkos::View out_device("out", 2); using LayoutPolicy = diff --git a/test/test_proxy_coupling.cpp b/test/test_proxy_coupling.cpp index 455fc183..6e166ad6 100644 --- a/test/test_proxy_coupling.cpp +++ b/test/test_proxy_coupling.cpp @@ -90,20 +90,20 @@ void xgc_delta_f(MPI_Comm comm, Omega_h::Mesh& mesh) pcms::Application* app = coupler.AddApplication("proxy_couple_xgc_delta_f"); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - app->AddLayout("gids", factory.GetLayout()); + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); - auto gids_field = factory.CreateField(pcms::FieldMetadata{}); - auto gids2_field = factory.CreateField(pcms::FieldMetadata{}); + auto gids_field = factory->CreateFunction("gids"); + auto gids2_field = factory->CreateFunction("gids2"); auto* gids_ptr = &gids_field.GetData(); auto* gids2_ptr = &gids2_field.GetData(); - initializeFieldWithGids(*factory.GetLayout(), gids_ptr, 1.0); - initializeFieldWithGids(*factory.GetLayout(), gids2_ptr, 2.0); + initializeFieldWithGids(*factory->GetLayout(), gids_ptr, 1.0); + initializeFieldWithGids(*factory->GetLayout(), gids2_ptr, 2.0); - app->AddField("gids", std::move(gids_field)); - app->AddField("gids2", std::move(gids2_field)); + app->AddField(std::move(gids_field)); + app->AddField(std::move(gids2_field)); do { for (int i = 0; i < COMM_ROUNDS; ++i) { @@ -114,7 +114,7 @@ void xgc_delta_f(MPI_Comm comm, Omega_h::Mesh& mesh) app->ReceiveField("gids"); //(Alt) df_gid_field->Receive(); app->EndReceivePhase(); - if (!validateField(*factory.GetLayout(), gids_ptr, "gids", rank, 1.0)) { + if (!validateField(*factory->GetLayout(), gids_ptr, "gids", rank, 1.0)) { std::cerr << "xgc_delta_f: Field validation failed at round " << i << std::endl; exit(EXIT_FAILURE); @@ -131,16 +131,16 @@ void xgc_total_f(MPI_Comm comm, Omega_h::Mesh& mesh) pcms::Application* app = coupler.AddApplication("proxy_couple_xgc_total_f"); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - app->AddLayout("gids", factory.GetLayout()); + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); - auto gids_field = factory.CreateField(pcms::FieldMetadata{}); + auto gids_field = factory->CreateFunction("gids"); auto* gids_ptr = &gids_field.GetData(); - initializeFieldWithGids(*factory.GetLayout(), gids_ptr, 10.0); + initializeFieldWithGids(*factory->GetLayout(), gids_ptr, 10.0); - app->AddField("gids", std::move(gids_field)); + app->AddField(std::move(gids_field)); do { for (int i = 0; i < COMM_ROUNDS; ++i) { @@ -151,7 +151,7 @@ void xgc_total_f(MPI_Comm comm, Omega_h::Mesh& mesh) app->ReceiveField("gids"); //(Alt) tf_gid_field->Receive(); app->EndReceivePhase(); - if (!validateField(*factory.GetLayout(), gids_ptr, "gids", rank, 10.0)) { + if (!validateField(*factory->GetLayout(), gids_ptr, "gids", rank, 10.0)) { std::cerr << "xgc_total_f: Field validation failed at round " << i << std::endl; exit(EXIT_FAILURE); @@ -173,27 +173,24 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) auto* total_f = cpl.AddApplication("proxy_couple_xgc_total_f"); auto* delta_f = cpl.AddApplication("proxy_couple_xgc_delta_f"); auto factory_total = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - total_f->AddLayout("gids", factory_total.GetLayout()); + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); auto factory_delta = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - delta_f->AddLayout("gids", factory_delta.GetLayout()); + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); // TODO, fields should have a transfer policy rather than parameters - auto total_gids_field = - factory_total.CreateField(pcms::FieldMetadata{}); - auto delta_gids_field = - factory_delta.CreateField(pcms::FieldMetadata{}); - auto delta_gids2_field = - factory_delta.CreateField(pcms::FieldMetadata{}); + auto total_gids_field = factory_total->CreateFunction("gids"); + auto delta_gids_field = factory_delta->CreateFunction("gids"); + auto delta_gids2_field = factory_delta->CreateFunction("gids2"); auto* total_gids_ptr = &total_gids_field.GetData(); auto* delta_gids_ptr = &delta_gids_field.GetData(); auto* delta_gids2_ptr = &delta_gids2_field.GetData(); - total_f->AddField("gids", std::move(total_gids_field)); - delta_f->AddField("gids", std::move(delta_gids_field)); - delta_f->AddField("gids2", std::move(delta_gids2_field)); + total_f->AddField(std::move(total_gids_field)); + delta_f->AddField(std::move(delta_gids_field)); + delta_f->AddField(std::move(delta_gids2_field)); do { for (int i = 0; i < COMM_ROUNDS; ++i) { @@ -201,7 +198,7 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) total_f->ReceiveField("gids"); total_f->EndReceivePhase(); - if (!validateField(*factory_total.GetLayout(), total_gids_ptr, "gids", + if (!validateField(*factory_total->GetLayout(), total_gids_ptr, "gids", rank, 10.0)) { std::cerr << "xgc_coupler: total_f field validation failed at round " << i << std::endl; @@ -212,7 +209,7 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) delta_f->ReceiveField("gids"); delta_f->EndReceivePhase(); - if (!validateField(*factory_delta.GetLayout(), delta_gids_ptr, "gids", + if (!validateField(*factory_delta->GetLayout(), delta_gids_ptr, "gids", rank, 1.0)) { std::cerr << "xgc_coupler: delta_f field validation failed at round " << i << std::endl; diff --git a/test/test_proxy_coupling_xgc_server.cpp b/test/test_proxy_coupling_xgc_server.cpp index 4a76b748..ada0b759 100644 --- a/test/test_proxy_coupling_xgc_server.cpp +++ b/test/test_proxy_coupling_xgc_server.cpp @@ -51,17 +51,17 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) // FIXME: The current C/Fortran proxy API couples layout registration to // field registration, so each XGC plane is registered as a separate layout // communicator even though the layouts are geometrically identical. - auto function_space = pcms::XGCFunctionSpace( - rc, ts::IsModelEntInOverlap{}, static_cast(mesh.nverts())); + auto function_space = + pcms::XGCFieldFactory(rc, ts::IsModelEntInOverlap{}, + static_cast(mesh.nverts()), ss.str()); auto field = function_space.CreateField( - std::make_unique>( - function_space.GetXGCLayout(), pcms::FieldMetadata{}, - make_array_view(data[i]))); - application->AddLayout(ss.str(), function_space.GetLayout()); + ss.str(), std::make_unique>( + function_space.GetXGCLayout(), pcms::FieldMetadata{}, + make_array_view(data[i]))); std::unique_ptr> serializer = std::make_unique>(comm); fields.push_back( - application->AddField(ss.str(), std::move(field), std::move(serializer))); + application->AddField(std::move(field), std::move(serializer))); } do { @@ -172,15 +172,14 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, // communicator even though the layouts are geometrically identical. auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian, numbering, - pcms::LagrangeFunctionSpace::Backend::OmegaH); - application->AddLayout(ss.str(), factory.GetLayout()); - auto field = - factory.CreateField(std::make_unique>( - factory.GetLayout(), pcms::FieldMetadata{})); + pcms::LagrangeFunctionSpace::Backend::OmegaH, ss.str()); + auto field = factory->CreateFunction( + ss.str(), std::make_unique>( + factory->GetLayout(), pcms::FieldMetadata{})); std::unique_ptr> serializer = std::make_unique>(); fields.push_back( - application->AddField(ss.str(), std::move(field), std::move(serializer))); + application->AddField(std::move(field), std::move(serializer))); } do { application->ReceivePhase([&]() { diff --git a/test/test_proxy_coupling_xgc_server_overlap.cpp b/test/test_proxy_coupling_xgc_server_overlap.cpp index 0161ee4e..4bfafc20 100644 --- a/test/test_proxy_coupling_xgc_server_overlap.cpp +++ b/test/test_proxy_coupling_xgc_server_overlap.cpp @@ -54,21 +54,20 @@ void xgc_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, application->SetLayoutOverlapMask(ss.str(), std::move(overlap_mask)); - auto function_space = pcms::XGCFunctionSpace( - rc, ts::IsModelEntInOverlap{}, static_cast(mesh.nverts())); - - application->AddLayout(ss.str(), function_space.GetLayout()); + auto function_space = + pcms::XGCFieldFactory(rc, ts::IsModelEntInOverlap{}, + static_cast(mesh.nverts()), ss.str()); auto field = function_space.CreateField( - std::make_unique>( - function_space.GetXGCLayout(), pcms::FieldMetadata{}, - make_array_view(data[i]))); + ss.str(), std::make_unique>( + function_space.GetXGCLayout(), pcms::FieldMetadata{}, + make_array_view(data[i]))); std::unique_ptr> serializer = std::make_unique>(comm); fields.push_back( - application->AddField(ss.str(), std::move(field), std::move(serializer))); + application->AddField(std::move(field), std::move(serializer))); } do { @@ -175,19 +174,17 @@ void omegah_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, auto factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian, numbering, - pcms::LagrangeFunctionSpace::Backend::OmegaH); - - application->AddLayout(ss.str(), factory.GetLayout()); + pcms::LagrangeFunctionSpace::Backend::OmegaH, ss.str()); - auto field = - factory.CreateField(std::make_unique>( - factory.GetLayout(), pcms::FieldMetadata{})); + auto field = factory->CreateFunction( + ss.str(), std::make_unique>( + factory->GetLayout(), pcms::FieldMetadata{})); std::unique_ptr> serializer = std::make_unique>(); fields.push_back( - application->AddField(ss.str(), std::move(field), std::move(serializer))); + application->AddField(std::move(field), std::move(serializer))); } do { diff --git a/test/test_uniform_grid_field.cpp b/test/test_uniform_grid_field.cpp index b179167d..632bd4db 100644 --- a/test/test_uniform_grid_field.cpp +++ b/test/test_uniform_grid_field.cpp @@ -109,7 +109,7 @@ TEST_CASE("UniformGrid field creation") auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + auto field = field_space->CreateFunction(); REQUIRE(field.GetDOFHolderDataHost().size() == static_cast(layout->OwnedSize())); } @@ -125,7 +125,7 @@ TEST_CASE("UniformGrid order-0 field creation and evaluation") grid, 1, pcms::CoordinateSystem::Cartesian, 0); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian, 0); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + auto field = field_space->CreateFunction(); pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); REQUIRE(layout->GetOrder() == 0); @@ -179,7 +179,7 @@ TEST_CASE("UniformGrid field data operations", "[uniform_grid_field]") grid, 1, pcms::CoordinateSystem::Cartesian); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + auto field = field_space->CreateFunction(); std::vector data(25); for (size_t i = 0; i < 25; ++i) @@ -206,7 +206,7 @@ TEST_CASE("UniformGrid field evaluation - piecewise constant") grid, 1, pcms::CoordinateSystem::Cartesian); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + auto field = field_space->CreateFunction(); pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); // Set vertex values for a 2x2 cell grid (3x3 = 9 vertices) @@ -269,7 +269,7 @@ TEST_CASE("UniformGrid field serialization") grid, 1, pcms::CoordinateSystem::Cartesian); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + auto field = field_space->CreateFunction(); std::vector data(16); for (size_t i = 0; i < 16; ++i) @@ -303,13 +303,13 @@ TEST_CASE("UniformGrid field copy") auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); field.SetDOFHolderDataHost( pcms::Rank2View( data.data(), static_cast(data.size()), 1)); - auto field2 = factory.CreateField(pcms::FieldMetadata{}); - pcms::Copy copy(factory, factory); + auto field2 = factory->CreateFunction(); + pcms::Copy copy(*factory, *factory); copy.Apply(field, field2); auto copied_data = pcms::FlattenToRank1View(field2.GetDOFHolderDataHost()); @@ -326,8 +326,7 @@ TEST_CASE("Transfer from OmegaH field to UniformGrid field") auto omega_h_factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto omega_h_field = - omega_h_factory.CreateField(pcms::FieldMetadata{}); + auto omega_h_field = omega_h_factory->CreateFunction(); pcms::test::SetField( omega_h_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); @@ -338,15 +337,15 @@ TEST_CASE("Transfer from OmegaH field to UniformGrid field") grid.divisions = {2, 2}; auto ug_factory = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto ug_field = ug_factory.CreateField(pcms::FieldMetadata{}); + auto ug_field = ug_factory->CreateFunction(); - pcms::Interpolator interp(omega_h_factory, ug_factory); + pcms::Interpolator interp(*omega_h_factory, *ug_factory); interp.Apply(omega_h_field, ug_field); auto transferred_data = pcms::FlattenToRank1View(ug_field.GetDOFHolderDataHost()); - auto ug_coords = ug_factory.GetLayout()->GetDOFHolderCoordinates(); - int num_ug_nodes = ug_factory.GetLayout()->GetNumOwnedDofHolder(); + auto ug_coords = ug_factory->GetLayout()->GetDOFHolderCoordinates(); + int num_ug_nodes = ug_factory->GetLayout()->GetNumOwnedDofHolder(); // set up_coords to host auto ug_coords_host = @@ -640,23 +639,22 @@ TEST_CASE("UniformGrid workflow") auto omega_h_factory = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto omega_h_field = - omega_h_factory.CreateField(pcms::FieldMetadata{}); + auto omega_h_field = omega_h_factory->CreateFunction(); pcms::test::SetField( omega_h_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); auto ug_factory = pcms::LagrangeFunctionSpace::FromUniformGrid( grid, 1, pcms::CoordinateSystem::Cartesian); - auto ug_field = ug_factory.CreateField(pcms::FieldMetadata{}); + auto ug_field = ug_factory->CreateFunction(); auto [mask_layout, mask_field] = pcms::CreateUniformGridBinaryField<2>(mesh, grid); - pcms::Interpolator interp(omega_h_factory, ug_factory); + pcms::Interpolator interp(*omega_h_factory, *ug_factory); interp.Apply(omega_h_field, ug_field); auto ug_coords_device_view = - ug_factory.GetLayout()->GetDOFHolderCoordinates().GetValues(); + ug_factory->GetLayout()->GetDOFHolderCoordinates().GetValues(); auto ug_coords_host_view = pcms::test::CopyCoordinatesToHost(ug_coords_device_view, 25, 2); diff --git a/test/test_xgc_field_data.cpp b/test/test_xgc_field_data.cpp index 234bd2d1..30a95163 100644 --- a/test/test_xgc_field_data.cpp +++ b/test/test_xgc_field_data.cpp @@ -104,29 +104,18 @@ TEST_CASE("XGC FieldData serializer preserves inactive entries") } } -TEST_CASE("XGCFunctionSpace creates fields and rejects evaluator access") +TEST_CASE("XGCFieldFactory creates fields and rejects evaluator access") { static constexpr int data_size = 16; auto rc = create_dummy_rc(data_size); - pcms::XGCFunctionSpace function_space(rc, in_overlap, data_size); + pcms::XGCFieldFactory function_space(rc, in_overlap, data_size); std::vector data(data_size); std::iota(data.begin(), data.end(), 0.0); auto field = function_space.CreateField( - std::make_unique>( - function_space.GetXGCLayout(), pcms::FieldMetadata{}, - pcms::make_array_view(data))); + "", std::make_unique>( + function_space.GetXGCLayout(), pcms::FieldMetadata{}, + pcms::make_array_view(data))); REQUIRE(&field.GetLayout() == function_space.GetLayout().get()); - REQUIRE(function_space.GetCoordinateSystem() == pcms::CoordinateSystem::XGC); - // XGC does not support point evaluation; CreatePointEvaluator throws. - using LayoutPolicy = - pcms::detail::default_layout_for_memory_space_t; - pcms::Rank2View - empty_coords{nullptr, 0, 2}; - REQUIRE_THROWS_AS(function_space.CreatePointEvaluator( - pcms::EvaluationRequest::FromCoordinates( - pcms::CoordinateView{ - pcms::CoordinateSystem::XGC, empty_coords})), - pcms::pcms_error); } diff --git a/test/xgc_n0_coupling_server.cpp b/test/xgc_n0_coupling_server.cpp index ddbac140..0484c810 100644 --- a/test/xgc_n0_coupling_server.cpp +++ b/test/xgc_n0_coupling_server.cpp @@ -43,16 +43,15 @@ struct RegisteredField [[nodiscard]] static RegisteredField AddField( pcms::Application* application, - const pcms::LagrangeFunctionSpace& function_space, const std::string& name, - const std::string& path, int plane) + const std::shared_ptr& function_space, + const std::string& name, const std::string& path, int plane) { PCMS_ALWAYS_ASSERT(application != nullptr); auto field_name = MakeFieldName(name, plane); - auto field = function_space.CreateField(pcms::FieldMetadata{}); + auto field = function_space->CreateFunction(path + field_name); std::unique_ptr> serializer = std::make_unique>(); - auto handle = application->AddField(path + field_name, std::move(field), - std::move(serializer)); + auto handle = application->AddField(std::move(field), std::move(serializer)); return {std::move(handle)}; } @@ -220,10 +219,7 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, }); auto function_space = pcms::LagrangeFunctionSpace::FromMesh( mesh, 1, 1, pcms::CoordinateSystem::Cartesian, is_overlap, numbering, - pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto layout = function_space.GetLayout(); - core->AddLayout("core_layout", layout); - edge->AddLayout("edge_layout", layout); + pcms::LagrangeFunctionSpace::Backend::OmegaH, "n0_layout"); auto time2 = std::chrono::steady_clock::now(); elapsed_seconds = time2 - time1; ts::timeMinMaxAvg(elapsed_seconds.count(), min, max, avg);