include/pops/runtime/program/external_riemann_brick.hpp Source FileΒΆ

adc_cpp: include/pops/runtime/program/external_riemann_brick.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
external_riemann_brick.hpp
Go to the documentation of this file.
1#pragma once
2
3// Static dispatch of an EXTERNAL C++ Riemann brick (Spec 3 section 21-22, criterion 20, ADC-463).
4//
5// `external_brick.hpp` owns the HOST IDENTITY catalog: `POPS_REGISTER_BRICK` records a brick's id +
6// requirements, `pops_brick_manifest()` exports them, and `pops.lib.load_cpp_library` surfaces a
7// requirement-carrying `riemann.User(id)` descriptor. This header owns the NUMERICAL half: how the
8// brick's flux is actually DISPATCHED into the finite-volume machinery without a per-cell string
9// lookup.
10//
11// The flux of an external brick is a `NumericalFlux` policy (numerics/fv/numerical_flux.hpp) living
12// in a SEPARATE `.so`, so it can never be a compile-time template parameter of the host's pre-built
13// `make_block` (whose `if (riem == "hllc") build_block<..., HLLCFlux>` ladder is closed over the
14// native fluxes). Instead the `.so` ITSELF performs the static instantiation: the
15// `POPS_DEFINE_EXTERNAL_RIEMANN_BRICK` macro emits an `extern "C"` entry point that calls
16// `build_block<Limiter, UserFlux>(...)` -- the user flux is a compile-time template parameter inside
17// the `.so`, fully inlined, exactly like a native flux's `build_block` leaf. The host dlopens the
18// `.so`, resolves that entry-point function pointer ONCE at install time, and calls it; the per-cell
19// kernel then runs the statically-instantiated `UserFlux` functor with NO string comparison on the
20// hot path. The only string is the limiter (a 4-way `if` resolved once per install, mirroring the
21// native AOT block in compiled_block_abi.hpp).
22//
23// ABI (flat double arrays, component-major c*n*n + j*n + i, like System::copy_state): identical to
24// the AOT compiled block (compiled_block_abi.hpp) so the host marshals an external brick the same
25// way it marshals a generated one. Only the flux is fixed at the `.so`'s compile time instead of
26// dispatched by string.
27
29
30#include <pops/runtime/builders/compiled/compiled_block_abi.hpp> // compiled_block::{make_grid,...}
31#include <pops/runtime/builders/block/block_builder.hpp> // build_block<Limiter, Flux>, block_n_ghost
32#include <pops/runtime/config/dispatch_tags.hpp> // validate_limiter
33#include <pops/numerics/fv/reconstruction.hpp> // NoSlope / Minmod / VanLeer / Weno5
34
35#include <pops/runtime/dynamic/dynlib.hpp> // portable dlopen<->LoadLibraryW (ADC-99)
36
37#include <stdexcept>
38#include <string>
39#include <vector>
40
41namespace pops::runtime::program {
42
43namespace detail {
44
45// Builds the block closures for the external flux @p Flux at limiter @p lim. The flux is a
46// COMPILE-TIME template parameter of build_block (the same leaf the native string ladder routes to),
47// so it is fully inlined; the only runtime branch is the limiter (resolved ONCE here, not per cell).
48// Mirrors make_block's limiter ladder but with the flux fixed -- no riemann string comparison.
49template <class Model, class Flux>
50BlockClosures external_make_block(const Model& m, const std::string& lim, const GridContext& ctx,
51 bool recon_prim, Real pos_floor) {
52 validate_limiter(lim, "external riemann brick");
53 if (lim == "none")
54 return build_block<NoSlope, Flux>(m, ctx, /*imex=*/false, recon_prim, "ssprk2", {}, {}, nullptr,
55 pos_floor);
56 if (lim == "minmod")
57 return build_block<Minmod, Flux>(m, ctx, /*imex=*/false, recon_prim, "ssprk2", {}, {}, nullptr,
58 pos_floor);
59 if (lim == "vanleer")
60 return build_block<VanLeer, Flux>(m, ctx, /*imex=*/false, recon_prim, "ssprk2", {}, {}, nullptr,
61 pos_floor);
62 if (lim == "weno5")
63 return build_block<Weno5, Flux>(m, ctx, /*imex=*/false, recon_prim, "ssprk2", {}, {}, nullptr,
64 pos_floor);
65 throw_registry_dispatch_mismatch("external riemann brick", "limiteur", lim);
66}
67
68// One explicit residual R = -div F(U) + S evaluated with the external flux @p Flux on @p Model. Same
69// marshaling as compiled_block::residual (flat arrays, local single-grid mesh, aux from the host) --
70// only the flux is the user's, instantiated statically by build_block. Used by the macro's
71// extern "C" pops_brick_residual entry point.
72template <class Model, class Flux>
73void external_residual(const double* U, double* R, const double* aux_in, int n, double dx,
74 double dy, bool periodic, const std::string& lim, bool recon_prim,
75 double pos_floor) {
77 compiled_block::make_grid(n, dx, dy, periodic, aux_in, aux_comps<Model>());
78 MultiFab Umf(lg.ba, lg.dm, Model::n_vars, block_n_ghost(lim)),
79 Rmf(lg.ba, lg.dm, Model::n_vars, 0);
80 compiled_block::fill_interior(Umf, U, n, Model::n_vars);
81 const GridContext ctx{lg.dom, lg.bc, lg.geom, &lg.aux};
82 Model model{};
83 BlockClosures clo =
84 external_make_block<Model, Flux>(model, lim, ctx, recon_prim, static_cast<Real>(pos_floor));
85 clo.rhs_into(Umf, Rmf);
86 compiled_block::extract(Rmf, R, n, Model::n_vars);
87}
88
89} // namespace detail
90
91// The host-side handle to a loaded external Riemann brick `.so`: dlopen the library, read its
92// manifest, and resolve the typed entry-point function pointers ONCE. After construction the brick
93// is dispatched by calling the resolved residual() pointer -- a direct C call into the `.so`'s
94// statically-instantiated flux, never a per-cell string lookup. The manifest is also registered in
95// the process catalog (BrickRegistry) so the brick's id + requirements are visible to a later host
96// query (mirroring what pops.lib.load_cpp_library does on the Python side).
97//
98// This is the C++ counterpart of pops.lib.load_cpp_library: the Python path surfaces the descriptor
99// (requirements/capabilities) for the board/install layer; this path resolves the numerical entry
100// point for a host that drives the brick from C++. A brick `.so` not exporting the expected symbols
101// is rejected with a clear error (it is not an pops external Riemann brick `.so`).
103 public:
104 // Function-pointer type of the brick's residual entry point (POPS_DEFINE_EXTERNAL_RIEMANN_BRICK).
105 using ResidualFn = void (*)(const double*, double*, const double*, int, double, double, int,
106 const char*, int, double);
107
108 // dlopen @p so_path, read + register its manifest, and resolve the entry points for brick @p id.
109 // Throws std::runtime_error if the library cannot be opened, does not export pops_brick_manifest /
110 // pops_brick_residual (not an pops external Riemann brick), or does not register @p id as a riemann
111 // brick (a clear, actionable message naming the id and the loaded ids).
112 ExternalBrickHandle(const std::string& so_path, const std::string& id) : id_(id) {
113 handle_ = dynlib::open(so_path);
114 if (!dynlib::valid(handle_))
115 throw std::runtime_error("external riemann brick: cannot dlopen '" + so_path +
116 "': " + dynlib::last_error());
117 auto manifest_fn =
118 reinterpret_cast<const char* (*)()>(dynlib::sym(handle_, "pops_brick_manifest"));
119 if (manifest_fn == nullptr)
120 throw std::runtime_error(
121 "external riemann brick '" + so_path +
122 "' does not export pops_brick_manifest(); it is not an pops brick .so");
123 // The .so's static initializers already ran POPS_REGISTER_BRICK against the registry of the .so's
124 // OWN image; reading its manifest and re-registering here makes the ids visible in THIS image's
125 // process catalog too (RTLD_LOCAL keeps the .so's statics private otherwise). Done via the same
126 // entries() the Python seam parses -- no behavioral divergence.
127 register_manifest_json(manifest_fn());
129 if (entry == nullptr)
130 throw std::runtime_error("external riemann brick '" + id_ +
131 "' not found in the manifest of '" + so_path + "'");
132 if (entry->category != "riemann")
133 throw std::runtime_error("external brick '" + id_ + "' is registered as category '" +
134 entry->category + "', not 'riemann'");
135 residual_ = reinterpret_cast<ResidualFn>(dynlib::sym(handle_, "pops_brick_residual"));
136 if (residual_ == nullptr)
137 throw std::runtime_error("external riemann brick '" + id_ +
138 "' does not export pops_brick_residual(); rebuild the .so with "
139 "POPS_DEFINE_EXTERNAL_RIEMANN_BRICK");
140 requirements_ = entry->requirements;
141 }
142
146 if (dynlib::valid(handle_))
147 dynlib::close(handle_);
148 }
149
150 // The resolved residual entry point: a direct call into the `.so`'s statically-instantiated flux.
151 ResidualFn residual() const { return residual_; }
152
153 // The CSV of model capabilities the brick requires (from its manifest); "" when none.
154 const std::string& requirements() const { return requirements_; }
155
156 const std::string& id() const { return id_; }
157
158 private:
159 // Minimal manifest reader: parse the {"bricks":[{"id","category","requirements",...}]} the `.so`
160 // exports and register each entry. A header-only sibling of lib.py's _register_manifest; it accepts
161 // exactly the JSON to_json() emits (flat string fields, no nesting). A malformed manifest throws.
162 static void register_manifest_json(const char* json) {
163 if (json == nullptr)
164 throw std::runtime_error("external riemann brick: pops_brick_manifest() returned NULL");
165 const std::string s = json;
166 // to_json() emits {"bricks":[{...},{...}]}: skip past the array's '[' (the top-level object's '{'
167 // precedes it), then read each {...} brick record until the array closes. An empty array ([]) or a
168 // manifest with no bricks array registers nothing (valid).
169 const std::size_t arr = s.find('[');
170 if (arr == std::string::npos)
171 return;
172 std::size_t pos = arr + 1;
173 while (true) {
174 const std::size_t obj = s.find('{', pos);
175 if (obj == std::string::npos)
176 break;
177 const std::size_t end = s.find('}', obj);
178 if (end == std::string::npos)
179 break;
180 const std::string rec = s.substr(obj, end - obj + 1);
182 e.id = field(rec, "id");
183 e.category = field(rec, "category");
184 e.requirements = field(rec, "requirements");
185 e.capabilities = field(rec, "capabilities");
186 if (!e.id.empty())
188 pos = end + 1;
189 }
190 }
191
192 // Extracts the value of "key":"value" from one manifest record (the fields to_json() emits are flat
193 // quoted strings; this is a targeted reader, not a general JSON parser). It skips backslash-escaped
194 // characters when scanning for the closing quote (so an escaped `\"` inside the value does not end
195 // it early) and json_unescape's the result. "" when the key is absent.
196 static std::string field(const std::string& rec, const std::string& key) {
197 const std::string pat = "\"" + key + "\":\"";
198 const std::size_t k = rec.find(pat);
199 if (k == std::string::npos)
200 return "";
201 const std::size_t start = k + pat.size();
202 std::size_t end = start;
203 while (end < rec.size() && rec[end] != '"') {
204 end += (rec[end] == '\\' && end + 1 < rec.size()) ? 2 : 1; // skip an escaped pair atomically
205 }
206 if (end >= rec.size())
207 return "";
208 return json_unescape(rec.substr(start, end - start));
209 }
210
211 dynlib::handle handle_ = nullptr;
212 ResidualFn residual_ = nullptr;
213 std::string id_;
214 std::string requirements_;
215};
216
217} // namespace pops::runtime::program
218
219// Defines the static-dispatch ABI of an external Riemann brick `.so`: registers its identity in the
220// host catalog AND emits the entry point the host calls. Use ONCE at namespace scope:
221// struct MyRiemann { template <class M> POPS_HD typename M::State operator()(...) const {...} };
222// POPS_DEFINE_EXTERNAL_RIEMANN_BRICK("my_riemann", MyRiemann,
223// pops::CompositeModel<pops::Euler, ...>, "pressure,wave_speeds");
224// POPS_DEFINE_BRICK_MANIFEST(); // exports the manifest reader (once per .so)
225//
226// @p id the brick id a user selects via pops.lib.riemann.User(id);
227// @p Flux the NumericalFlux policy (numerics/fv/numerical_flux.hpp contract);
228// @p Model a TOP-LEVEL ALIAS of the CompositeModel the .so instantiates the flux against (write
229// `using Model = pops::CompositeModel<...>;` first and pass the alias -- a bare
230// CompositeModel<A, B, C> has a comma the preprocessor would split, exactly like
231// POPS_DEFINE_COMPILED_BLOCK(MODEL));
232// @p reqs_csv the CSV of model capabilities the brick requires (surfaced in the manifest).
233//
234// The emitted pops_brick_residual instantiates build_block<Limiter, Flux> at the .so's compile time:
235// the flux is a STATIC template argument, never a per-cell string lookup. pops_brick_nvars /
236// pops_brick_naux let the host size its marshaling arrays (same role as pops_compiled_nvars/_naux).
237//
238// ABI WARNING: the brick `.so` MUST be compiled against the SAME Kokkos backend and version (and the
239// same pops headers) as the host binary that dlopens it -- the residual runs the host's Kokkos
240// runtime. A mismatched `.so` may dlopen yet fail unpredictably. There is no load-time Kokkos-ABI
241// check yet (a future safeguard); for now this is the caller's contract, mirroring the AOT
242// compiled-block path (POPS_DEFINE_COMPILED_BLOCK), which carries the same requirement.
243#define POPS_DEFINE_EXTERNAL_RIEMANN_BRICK(id, Flux, Model, reqs_csv) \
244 POPS_REGISTER_BRICK(id, "riemann", reqs_csv); \
245 extern "C" int pops_brick_nvars() { \
246 return Model::n_vars; \
247 } \
248 extern "C" int pops_brick_naux() { \
249 return pops::aux_comps<Model>(); \
250 } \
251 extern "C" void pops_brick_residual(const double* U, double* R, const double* aux, int n, \
252 double dx, double dy, int periodic, const char* lim, \
253 int recon_prim, double pos_floor) { \
254 ::pops::runtime::program::detail::external_residual<Model, Flux>( \
255 U, R, aux, n, dx, dy, periodic != 0, lim, recon_prim != 0, pos_floor); \
256 }
Builds the closures of a block (time advance + residual + Poisson contribution) from a COMPILED model...
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
void register_brick(const BrickManifestEntry &entry)
Definition external_brick.hpp:132
const BrickManifestEntry * lookup(const std::string &id) const
Definition external_brick.hpp:144
static BrickRegistry & instance()
Definition external_brick.hpp:125
Definition external_riemann_brick.hpp:102
~ExternalBrickHandle()
Definition external_riemann_brick.hpp:145
ResidualFn residual() const
Definition external_riemann_brick.hpp:151
ExternalBrickHandle(const ExternalBrickHandle &)=delete
const std::string & id() const
Definition external_riemann_brick.hpp:156
void(*)(const double *, double *, const double *, int, double, double, int, const char *, int, double) ResidualFn
Definition external_riemann_brick.hpp:106
ExternalBrickHandle & operator=(const ExternalBrickHandle &)=delete
const std::string & requirements() const
Definition external_riemann_brick.hpp:154
ExternalBrickHandle(const std::string &so_path, const std::string &id)
Definition external_riemann_brick.hpp:112
C ABI of an AOT-COMPILED block: a model generated by the DSL, compiled ahead-of-time into a ....
SINGLE registry of spatial scheme tags (limiters + Riemann fluxes): shared source of truth for ALL di...
PORTABLE dynamic loading: dlopen/dlsym/dlclose (POSIX) <-> LoadLibraryW/ GetProcAddress/FreeLibrary (...
LocalGrid make_grid(int n, double dx, double dy, bool periodic, const double *aux_in, int naux)
naux = width of the aux channel the model READS (aux_comps<Model>(), >= 3).
Definition compiled_block_abi.hpp:54
void extract(const MultiFab &mf, double *out, int n, int nv)
Copies the INTERIOR of fab(0) (nv components) into the component-major flat array out.
Definition compiled_block_abi.hpp:112
void fill_interior(MultiFab &mf, const double *in, int n, int nv)
Copies nv components of the component-major flat array in into the INTERIOR of fab(0) (without touchi...
Definition compiled_block_abi.hpp:100
bool valid(handle h)
true if h is a valid handle.
Definition dynlib.hpp:82
void * handle
Definition dynlib.hpp:33
std::string last_error()
Last error message (best-effort, for diagnostics).
Definition dynlib.hpp:87
void close(handle h)
Closes h (no-op on a null handle).
Definition dynlib.hpp:71
handle open(const std::string &path)
Opens a dynamic library (path in UTF-8). Returns a null handle on failure.
Definition dynlib.hpp:48
void * sym(handle h, const char *name)
Resolves name in h. Returns nullptr if absent.
Definition dynlib.hpp:62
BlockClosures external_make_block(const Model &m, const std::string &lim, const GridContext &ctx, bool recon_prim, Real pos_floor)
Definition external_riemann_brick.hpp:50
void external_residual(const double *U, double *R, const double *aux_in, int n, double dx, double dy, bool periodic, const std::string &lim, bool recon_prim, double pos_floor)
Definition external_riemann_brick.hpp:73
Definition cache_manager.hpp:37
std::string json_unescape(const std::string &s)
Definition external_brick.hpp:68
int block_n_ghost(const std::string &lim)
Number of ghosts required by the spatial scheme lim (single source: Limiter::n_ghost).
Definition block_builder.hpp:759
void throw_registry_dispatch_mismatch(const char *ctx, const char *kind, const std::string &tag)
DEFENSE-IN-DEPTH guard: reached only if a VALID tag (already accepted by validate_*) is routed by NO ...
Definition dispatch_tags.hpp:138
double Real
Definition types.hpp:30
void validate_limiter(const std::string &lim, const char *ctx="System")
Validates a LIMITER tag against kLimiters.
Definition dispatch_tags.hpp:99
Interface reconstruction policies: MUSCL limiters and WENO5-Z.
Compiled block closures, frozen at add time.
Definition grid_context.hpp:61
std::function< void(MultiFab &, MultiFab &)> rhs_into
R <- -div F + S (Poisson frozen)
Definition grid_context.hpp:66
Mesh + transport BC + aux shared by a block closures.
Definition grid_context.hpp:41
Local single-grid mesh rebuilt inside the .so + aux filled from the System array.
Definition compiled_block_abi.hpp:40
MultiFab aux
Definition compiled_block_abi.hpp:47
BoxArray ba
Definition compiled_block_abi.hpp:43
BCRec bc
Definition compiled_block_abi.hpp:45
Geometry geom
Definition compiled_block_abi.hpp:42
DistributionMapping dm
Definition compiled_block_abi.hpp:44
Box2D dom
Definition compiled_block_abi.hpp:41
Definition external_brick.hpp:110
std::string id
Definition external_brick.hpp:111
std::string capabilities
Definition external_brick.hpp:114
std::string category
Definition external_brick.hpp:112
std::string requirements
Definition external_brick.hpp:113