include/pops/runtime/builders/compiled/native_loader.hpp Source File

adc_cpp: include/pops/runtime/builders/compiled/native_loader.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
native_loader.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pops/core/state/state.hpp> // StateVec, Aux, POPS_AUX_FIELDS, kAuxBaseComps
4#include <pops/core/foundation/types.hpp> // POPS_HD, Real
5#include <pops/core/state/variables.hpp> // VariableSet + VariableKind + VariableRole + role_from_name
9#include <pops/mesh/boundary/physical_bc.hpp> // AuxHaloPolicy (ADC-369: per-field aux halo tail marshaling)
10#include <pops/runtime/dynamic/abi_key.hpp> // pops::abi_key (ABI guard for the native loader)
11#include <pops/runtime/dynamic/dynamic_model.hpp> // IModel: model loaded at runtime (dynamic block)
12#include <pops/runtime/context/grid_context.hpp> // GridContext
13#include <pops/runtime/system.hpp> // pops::System (install_block / grid_context / ensure_aux_width)
14
15#include <algorithm>
16#include <cmath>
17#include <cstddef>
18#include <pops/runtime/dynamic/dynlib.hpp> // portable dlopen<->LoadLibraryW layer (ADC-99); includes <dlfcn.h> on POSIX
19#include <functional>
20#include <memory>
21#include <stdexcept>
22#include <string>
23#include <vector>
24
38
39namespace pops {
40namespace native_loader {
41
48template <class Impl>
49inline std::vector<double> marshal_aux_halo(Impl* P, int naux) {
50 std::vector<double> a = P->copy_state(P->aux, naux);
51 const std::size_t nn = static_cast<std::size_t>(P->cfg.n) * static_cast<std::size_t>(P->cfg.n);
52 a.resize(static_cast<std::size_t>(naux) * nn + static_cast<std::size_t>(2) * naux, 0.0);
53 for (const auto& kv : P->fields_.named_aux_bc_) {
54 if (kv.first < 0 || kv.first >= naux)
55 continue;
56 const std::size_t base =
57 static_cast<std::size_t>(naux) * nn + static_cast<std::size_t>(2) * kv.first;
58 a[base] = static_cast<double>(static_cast<int>(kv.second.type));
59 a[base + 1] = static_cast<double>(kv.second.value);
60 }
61 return a;
62}
63
67POPS_HD inline double limited_slope(double am, double ap, int recon) {
68 if (recon == 1) { // minmod: TVD, robust
69 if (am * ap <= 0)
70 return 0.0;
71 return (std::fabs(am) < std::fabs(ap)) ? am : ap;
72 }
73 if (recon == 2) { // van Leer: smoother at extrema
74 const double ab = am * ap;
75 if (ab <= 0)
76 return 0.0;
77 return 2.0 * ab / (am + ap);
78 }
79 return 0.0; // order 1 (no slope)
80}
81
89template <int NV>
90std::vector<double> host_residual(const IModel<NV>& m, const std::vector<double>& U,
91 const std::vector<double>& AUX, int n, double dx, int recon) {
92 const std::size_t nn = static_cast<std::size_t>(n) * n;
93 auto idx = [&](int i, int j) {
94 return static_cast<std::size_t>((j + n) % n) * n + ((i + n) % n);
95 };
96 auto cell = [&](int i, int j) {
98 for (int c = 0; c < NV; ++c)
99 u[c] = U[static_cast<std::size_t>(c) * nn + idx(i, j)];
100 return u;
101 };
102 auto aux_at = [&](int i, int j) { // per-cell aux, periodic; empty => zero
103 Aux a{};
104 if (AUX.size() >= 3 * nn) {
105 const std::size_t k = idx(i, j);
106 a.phi = AUX[k];
107 a.grad_x = AUX[nn + k];
108 a.grad_y = AUX[2 * nn + k];
109 // Extra fields marshaled from the SINGLE SOURCE POPS_AUX_FIELDS (pops/core/state.hpp):
110 // same table as load_aux on the device side. An extra component is read only if the channel is
111 // wide enough ((idx+1)*nn elements). Adding an aux field => 1 line in POPS_AUX_FIELDS,
112 // this site (and the other, further down) transports it AUTOMATICALLY. Closes gap #51.
113#define POPS_AUX_MARSHAL(name, idx) \
114 if (AUX.size() >= ((idx) + 1) * nn) \
115 a.name = AUX[(idx) * nn + k];
117#undef POPS_AUX_MARSHAL
118 // Model-NAMED aux fields (ADC-291): extra[e] = aux component kAuxNamedBase + e. Same contract
119 // as load_aux (spatial_operator.hpp) and the AOT path (compiled_block_abi.hpp): a component is
120 // read ONLY when the channel carries it, so a narrow channel leaves extra[] at 0 (no out-of-
121 // bounds). Without this loop a model reading aux.extra_field(k) on the JIT host read 0 silently.
122 for (int e = 0; e < kAuxMaxExtra; ++e) {
123 const std::size_t comp = static_cast<std::size_t>(kAuxNamedBase + e);
124 if (AUX.size() >= (comp + 1) * nn)
125 a.extra[e] = AUX[comp * nn + k];
126 }
127 }
128 return a;
129 };
130 // limited slope of cell (i,j) in direction dir (on the conservative variables)
131 auto slope = [&](int i, int j, int dir) {
132 StateVec<NV> s{};
133 if (recon == 0)
134 return s;
135 StateVec<NV> Uc = cell(i, j);
136 StateVec<NV> Um = (dir == 0) ? cell(i - 1, j) : cell(i, j - 1);
137 StateVec<NV> Up = (dir == 0) ? cell(i + 1, j) : cell(i, j + 1);
138 for (int c = 0; c < NV; ++c)
139 s[c] = limited_slope(Uc[c] - Um[c], Up[c] - Uc[c], recon);
140 return s;
141 };
142 double amax = 0;
143 for (int j = 0; j < n; ++j)
144 for (int i = 0; i < n; ++i) {
145 StateVec<NV> u = cell(i, j);
146 Aux a = aux_at(i, j);
147 double s = std::max(m.max_wave_speed(u, a, 0), m.max_wave_speed(u, a, 1));
148 if (s > amax)
149 amax = s;
150 }
151 // numerical flux at the +dir face of cell (i,j): MUSCL states (cell + neighbor) then Rusanov
152 auto face_flux = [&](int i, int j, int dir) {
153 const int in = (dir == 0) ? i + 1 : i, jn = (dir == 0) ? j : j + 1;
154 StateVec<NV> Uc = cell(i, j), Un = cell(in, jn);
155 StateVec<NV> sc = slope(i, j, dir), sn = slope(in, jn, dir), L, R;
156 for (int c = 0; c < NV; ++c) {
157 L[c] = Uc[c] + 0.5 * sc[c]; // extrapolation toward the face from each side
158 R[c] = Un[c] - 0.5 * sn[c];
159 }
160 StateVec<NV> FL = m.flux(L, aux_at(i, j), dir), FR = m.flux(R, aux_at(in, jn), dir), f;
161 for (int c = 0; c < NV; ++c)
162 f[c] = 0.5 * (FL[c] + FR[c]) - 0.5 * amax * (R[c] - L[c]);
163 return f;
164 };
165 std::vector<double> Rout(static_cast<std::size_t>(NV) * nn, 0.0);
166 for (int j = 0; j < n; ++j)
167 for (int i = 0; i < n; ++i) {
168 StateVec<NV> Uc = cell(i, j);
169 StateVec<NV> Fxr = face_flux(i, j, 0), Fxl = face_flux(i - 1, j, 0);
170 StateVec<NV> Fyr = face_flux(i, j, 1), Fyl = face_flux(i, j - 1, 1);
171 StateVec<NV> S = m.source(Uc, aux_at(i, j)); // generated source term (force, etc.)
172 for (int c = 0; c < NV; ++c)
173 Rout[static_cast<std::size_t>(c) * nn + static_cast<std::size_t>(j) * n + i] =
174 -((Fxr[c] - Fxl[c]) + (Fyr[c] - Fyl[c])) / dx + S[c];
175 }
176 return Rout;
177}
178
181inline std::vector<std::string> split(const std::string& s, char sep) {
182 std::vector<std::string> out;
183 std::string cur;
184 for (char c : s) {
185 if (c == sep) {
186 out.push_back(cur);
187 cur.clear();
188 } else
189 cur += c;
190 }
191 out.push_back(cur);
192 return out;
193}
194
205
208inline VariableSet parse_var_set(VariableKind kind, const std::string& names_csv,
209 const std::string& roles_csv) {
210 VariableSet vs{kind, {}, 0, {}};
211 if (names_csv.empty())
212 return vs; // no names transported: empty set (the caller will set its fallback)
213 vs.names = split(names_csv, ',');
214 vs.size = static_cast<int>(vs.names.size());
215 parse_roles_into(vs, roles_csv); // canonical roles + any user-defined role label (ADC-292)
216 return vs;
217}
218
223 BlockMeta m;
224 auto names_fn =
225 reinterpret_cast<const char* (*)()>(pops::dynlib::sym(h, "pops_compiled_var_names"));
226 auto roles_fn = reinterpret_cast<const char* (*)()>(pops::dynlib::sym(h, "pops_compiled_roles"));
227 auto gamma_fn = reinterpret_cast<double (*)()>(pops::dynlib::sym(h, "pops_compiled_gamma"));
228 std::string names = names_fn ? std::string(names_fn()) : std::string();
229 std::string roles = roles_fn ? std::string(roles_fn()) : std::string();
230 // "cons|prim": index 0 = conservative set, index 1 = primitive set (each possibly empty).
231 std::vector<std::string> nparts = split(names, '|');
232 std::vector<std::string> rparts = split(roles, '|');
233 auto part = [](const std::vector<std::string>& v, std::size_t i) {
234 return i < v.size() ? v[i] : std::string();
235 };
236 m.cons = parse_var_set(VariableKind::Conservative, part(nparts, 0), part(rparts, 0));
237 m.prim = parse_var_set(VariableKind::Primitive, part(nparts, 1), part(rparts, 1));
238 if (gamma_fn) {
239 m.has_gamma = true;
240 m.gamma = gamma_fn();
241 }
242 return m;
243}
244
248template <typename ImplT, int NV>
249void push_dynamic(ImplT* P, const std::string& name, pops::dynlib::handle h, int substeps,
250 std::vector<std::string> names, int recon) {
251 auto mk = reinterpret_cast<void* (*)()>(pops::dynlib::sym(h, "pops_make_model"));
252 auto del = reinterpret_cast<void (*)(void*)>(pops::dynlib::sym(h, "pops_destroy_model"));
253 if (!mk || !del) {
255 throw std::runtime_error(
256 "add_dynamic_block: pops_make_model / pops_destroy_model missing from the .so");
257 }
258 std::shared_ptr<IModel<NV>> im(static_cast<IModel<NV>*>(mk()), [del, h](IModel<NV>* p) {
259 del(p);
261 });
262 // The loaded model can read extra aux fields (n_aux > 3, e.g. B_z): we
263 // widen the SHARED aux channel so set_magnetic_field populates it and the host marshaling
264 // transports them. Base model (3) -> no-op. The closures read P->aux_ncomp_ at call time.
265 P->ensure_aux_width(im->n_aux());
266 const int n = P->cfg.n;
267 const double dx = P->cfg.L / P->cfg.n;
268
269 std::function<void(MultiFab&, MultiFab&)> rhs_into = [P, im, n, dx, recon](MultiFab& U,
270 MultiFab& R) {
271 // HOST path (dynamic block): same MPI guard as add_poisson. On a rank without a local box
272 // (local_size()==0 at np>1) there is nothing to marshal; the owner rank carries the full
273 // physics. Without this guard, copy_state(U) / write_state(R) would dereference a nonexistent fab(0).
274 // No collective MPI operation here -> one-sided no-op, no deadlock risk.
275 if (U.local_size() == 0)
276 return;
277 P->write_state(R, NV,
278 host_residual<NV>(*im, P->copy_state(U, NV),
279 P->copy_state(P->aux, P->aux_ncomp_), n, dx, recon));
280 };
281 std::function<Real(const MultiFab&)> max_speed = [P, im, n](const MultiFab& U) -> Real {
282 // Same MPI guard: empty rank -> local speed 0 (the downstream all_reduce_max takes the global max,
283 // the owner contributes the real value). No collective here -> no-op without deadlock.
284 if (U.local_size() == 0)
285 return Real(0);
286 std::vector<double> u = P->copy_state(U, NV), aux = P->copy_state(P->aux, P->aux_ncomp_);
287 const std::size_t nn = static_cast<std::size_t>(n) * n;
288 Real mx = 0;
289 for (std::size_t c0 = 0; c0 < nn; ++c0) {
290 StateVec<NV> s;
291 for (int c = 0; c < NV; ++c)
292 s[c] = u[static_cast<std::size_t>(c) * nn + c0];
293 Aux a{};
294 if (aux.size() >= 3 * nn) {
295 a.phi = aux[c0];
296 a.grad_x = aux[nn + c0];
297 a.grad_y = aux[2 * nn + c0];
298 // Extra fields: same SINGLE SOURCE POPS_AUX_FIELDS as load_aux and the other marshaling
299 // site (host_residual). cf. note above; adding a field = 1 line.
300#define POPS_AUX_MARSHAL(name, idx) \
301 if (aux.size() >= ((idx) + 1) * nn) \
302 a.name = aux[(idx) * nn + c0];
304#undef POPS_AUX_MARSHAL
305 // Model-NAMED aux fields (ADC-291): same single-source contract as the host_residual site.
306 for (int e = 0; e < kAuxMaxExtra; ++e) {
307 const std::size_t comp = static_cast<std::size_t>(kAuxNamedBase + e);
308 if (aux.size() >= (comp + 1) * nn)
309 a.extra[e] = aux[comp * nn + c0];
310 }
311 }
312 Real v = std::max(im->max_wave_speed(s, a, 0), im->max_wave_speed(s, a, 1));
313 if (v > mx)
314 mx = v;
315 }
316 return mx;
317 };
318 std::function<void(MultiFab&, Real, int)> advance = [P, im, n, dx, recon](MultiFab& U, Real dt,
319 int nsub) {
320 // Same MPI guard: empty rank -> no-op (nothing to marshal). No collective -> without deadlock.
321 if (U.local_size() == 0)
322 return;
323 const Real hh = dt / nsub;
324 const std::vector<double> aux = P->copy_state(P->aux, P->aux_ncomp_); // frozen aux (splitting)
325 for (int s = 0; s < nsub; ++s) { // explicit Euler per substep (host path, prototype)
326 std::vector<double> u = P->copy_state(U, NV);
327 std::vector<double> res = host_residual<NV>(*im, u, aux, n, dx, recon);
328 for (std::size_t k = 0; k < u.size(); ++k)
329 u[k] += hh * res[k];
330 P->write_state(U, NV, u);
331 }
332 };
333 // Contribution of the dynamic block to the system Poisson: rhs += elliptic_rhs(U) per cell.
334 // Model without an elliptic part => elliptic_rhs is 0 (no effect), thus backward-compatible.
335 std::function<void(const MultiFab&, MultiFab&)> add_poisson = [P, im, n](const MultiFab& U,
336 MultiFab& rhs) {
337 // HOST path (dynamic block .so prototype): it marshals the box to a full n*n
338 // array and treats it as replicated. On a rank without a local box (System distributes ONE box,
339 // so local_size()==0 at np>1 on the non-owner ranks) there is nothing to marshal:
340 // we skip, the owner rank carries the full contribution to the right-hand side. Without
341 // this guard, copy_state(U) / rhs.fab(0) would dereference a nonexistent fab (host crash).
342 if (rhs.local_size() == 0)
343 return;
344 std::vector<double> u = P->copy_state(U, NV);
345 const std::size_t nn = static_cast<std::size_t>(n) * n;
346 Array4 r = rhs.fab(0).array();
347 const Box2D v = rhs.box(0);
348 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
349 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
350 const std::size_t k = static_cast<std::size_t>(j - v.lo[1]) * n + (i - v.lo[0]);
351 StateVec<NV> s;
352 for (int c = 0; c < NV; ++c)
353 s[c] = u[static_cast<std::size_t>(c) * nn + k];
354 r(i, j, 0) += im->elliptic_rhs(s);
355 }
356 };
357 // OPTIONAL metadata (names / roles / gamma) carried by the extended ABI of the .so. Symmetric
358 // to the AOT path. Absent from an old .so -> empty meta -> fallback (names u0.. / no roles /
359 // gamma 1.4). PRIORITY to the explicit name (names=), then meta, then fallback; roles + primitive
360 // come ONLY from the ABI (the API does not expose them).
361 const BlockMeta meta = read_block_meta(h);
362 VariableSet cons_vs = meta.cons, prim_vs = meta.prim;
363 if (!names.empty()) {
364 if (static_cast<int>(names.size()) != NV)
365 throw std::runtime_error("System::add_dynamic_block: names= has " +
366 std::to_string(names.size()) + " names but block '" + name +
367 "' has " + std::to_string(NV) + " variables");
368 cons_vs.names = names;
369 cons_vs.size = static_cast<int>(names.size());
370 }
371 if (cons_vs.names.empty()) {
372 for (int c = 0; c < NV; ++c)
373 cons_vs.names.push_back("u" + std::to_string(c));
374 cons_vs.size = NV;
375 }
376 if (prim_vs.names.empty())
377 prim_vs = {VariableKind::Primitive, cons_vs.names, cons_vs.size, {}};
378 const double gamma = meta.has_gamma ? meta.gamma : 1.4;
379
380 typename ImplT::Species block{name,
381 MultiFab(P->ba, P->dm, NV, 2),
382 NV,
383 substeps,
384 true,
385 /*stride=*/1,
386 gamma,
387 std::move(advance),
388 std::move(rhs_into),
389 std::move(max_speed),
390 std::move(add_poisson)};
391 block.cons_vars = std::move(cons_vs);
392 block.prim_vars = std::move(prim_vs);
393 // POINTWISE cons <-> prim conversions OF THE MODEL (set/get_primitive_state): forwarded by
394 // the virtual interface IModel<NV> (ModelAdapter delegates to M.to_primitive/to_conservative when
395 // M exposes them, otherwise identity). An old .so whose model has no conversion thus falls back
396 // on identity -- exact for a scalar (prim == cons). StateVec<NV> shares the width NV.
397 block.prim_to_cons = [im](const double* in, double* out) {
398 StateVec<NV> p{};
399 for (int c = 0; c < NV; ++c)
400 p[c] = static_cast<Real>(in[c]);
401 const StateVec<NV> u = im->to_conservative(p);
402 for (int c = 0; c < NV; ++c)
403 out[c] = static_cast<double>(u[c]);
404 };
405 block.cons_to_prim = [im](const double* in, double* out) {
406 StateVec<NV> u{};
407 for (int c = 0; c < NV; ++c)
408 u[c] = static_cast<Real>(in[c]);
409 const StateVec<NV> p = im->to_primitive(u);
410 for (int c = 0; c < NV; ++c)
411 out[c] = static_cast<double>(p[c]);
412 };
413 P->sp.push_back(std::move(block));
414 P->sp.back().U.set_val(Real(0));
415}
416
418template <typename ImplT>
419void add_dynamic_block(System* self, ImplT* P, const std::string& name, const std::string& so_path,
420 int substeps, const std::vector<std::string>& names,
421 const std::string& recon) {
422 (void)self;
423 if (substeps < 1)
424 throw std::runtime_error("System::add_dynamic_block: substeps >= 1");
425 int recon_id = 0; // MUSCL reconstruction order of the face states (conservative)
426 if (recon == "none")
427 recon_id = 0;
428 else if (recon == "minmod")
429 recon_id = 1;
430 else if (recon == "vanleer")
431 recon_id = 2;
432 else
433 throw std::runtime_error(
434 "System::add_dynamic_block: recon 'none' | 'minmod' | 'vanleer' "
435 "(got '" +
436 recon + "')");
438 if (!h) {
439 const std::string e = pops::dynlib::last_error();
440 throw std::runtime_error("add_dynamic_block: dlopen('" + so_path +
441 "'): " + (e.empty() ? std::string("?") : e));
442 }
443 auto nv_fn = reinterpret_cast<int (*)()>(pops::dynlib::sym(h, "pops_model_nvars"));
444 if (!nv_fn) {
446 throw std::runtime_error("add_dynamic_block: pops_model_nvars missing from the .so");
447 }
448 const int nv = nv_fn();
449 switch (nv) {
450 case 1:
451 push_dynamic<ImplT, 1>(P, name, h, substeps, names, recon_id);
452 break;
453 case 3:
454 push_dynamic<ImplT, 3>(P, name, h, substeps, names, recon_id);
455 break;
456 case 4:
457 push_dynamic<ImplT, 4>(P, name, h, substeps, names, recon_id);
458 break;
459 default:
461 throw std::runtime_error("add_dynamic_block: n_vars=" + std::to_string(nv) +
462 " unsupported (1, 3, 4)");
463 }
464}
465
467template <typename ImplT>
468void add_compiled_block(System* self, ImplT* P, const std::string& name, const std::string& so_path,
469 const std::string& limiter, const std::string& riemann,
470 const std::string& recon, const std::string& time, int substeps,
471 const std::vector<std::string>& names, double pos_floor = 0) {
472 (void)self;
473 if (substeps < 1)
474 throw std::runtime_error("System::add_compiled_block: substeps >= 1");
475 if (recon != "conservative" && recon != "primitive")
476 throw std::runtime_error("System::add_compiled_block: recon 'conservative' | 'primitive'");
477 // NB: the AOT path (.so) marshals time->imex without an explicit RK scheme: only SSPRK2 is wired
478 // in the extern "C" ABI of the .so. "ssprk3" and "euler" are therefore NOT supported here (the
479 // advance would remain SSPRK2, silently) -> we reject them; exposing them would require extending the
480 // ABI of the .so (out of scope ADC-174). They are carried by the native add_block AND backend='production'
481 // (add_native_block: the template marshals method down to make_block of the loader).
482 if (time != "explicit" && time != "imex")
483 throw std::runtime_error(
484 "System::add_compiled_block: time 'explicit' | 'imex' (ssprk3, euler and "
485 "the IMEX-RK family ARS(2,2,2) -> native add_block or backend='production'; "
486 "the AOT path only exposes SSPRK2 + local backward-Euler)");
487 // WENO5 (5-point stencil, 3 ghosts) is now EXPOSED by the AOT path: the local grid of the
488 // .so allocates block_n_ghost(limiter) (compiled_block_abi.hpp), 3 for weno5, so assemble_rhs does not read
489 // out of bounds. limiter is validated by make_block in the .so (none|minmod|vanleer|weno5).
490 const int recon_prim = (recon == "primitive") ? 1 : 0;
491 const int imex = (time == "imex") ? 1 : 0;
492
494 if (!h) {
495 const std::string e = pops::dynlib::last_error();
496 throw std::runtime_error("add_compiled_block: dlopen('" + so_path +
497 "'): " + (e.empty() ? std::string("?") : e));
498 }
499 // extern "C" ABI of the compiled block (compiled_block_abi.hpp). The .so runs the production path
500 // (assemble_rhs<Limiter, Flux>, SSPRK2/IMEX) on the generated model; only flat arrays
501 // pass through (no C++ object shared across the dlopen, thus RTLD_LOCAL without ABI risk).
502 using nv_fn_t = int (*)();
503 using res_fn_t = void (*)(const double*, double*, const double*, int, double, double, int,
504 const char*, const char*, int);
505 using adv_fn_t = void (*)(double*, const double*, int, double, double, int, const char*,
506 const char*, int, int, double, int);
507 using max_fn_t = double (*)(const double*, const double*, int, double, double, int);
508 using poi_fn_t = void (*)(const double*, double*, int);
509 auto nv_fn = reinterpret_cast<nv_fn_t>(pops::dynlib::sym(h, "pops_model_nvars"));
510 auto res_fn = reinterpret_cast<res_fn_t>(pops::dynlib::sym(h, "pops_compiled_residual"));
511 auto adv_fn = reinterpret_cast<adv_fn_t>(pops::dynlib::sym(h, "pops_compiled_advance"));
512 auto max_fn = reinterpret_cast<max_fn_t>(pops::dynlib::sym(h, "pops_compiled_max_speed"));
513 auto poi_fn = reinterpret_cast<poi_fn_t>(pops::dynlib::sym(h, "pops_compiled_poisson_rhs"));
514 if (!nv_fn || !res_fn || !adv_fn || !max_fn || !poi_fn) {
516 throw std::runtime_error(
517 "add_compiled_block: compiled block ABI missing from the .so (regenerate via "
518 "dsl.compile_aot / compile_or_jit(mode='compile'))");
519 }
520 const int nv = nv_fn();
521 // Width of the aux channel that the compiled model READS (B_z, T_e...). Symmetric to the JIT path
522 // (IModel::n_aux). Optional: an old .so without this symbol falls back on the base contract (3).
523 auto naux_fn = reinterpret_cast<nv_fn_t>(pops::dynlib::sym(h, "pops_compiled_naux"));
524 const int naux = naux_fn ? naux_fn() : kAuxBaseComps;
525 // RUNTIME PARAMS (P7-b): SUFFIXED `_p` variants that take a flat block (const double*, int) of
526 // runtime parameter values, injected into the model before execution. OPTIONAL: a .so
527 // generated before this work (or a model without runtime params) does not expose them / declares nparams=0 ->
528 // we keep the historical symbols (const-params path, bit-identical). We SEED the value block
529 // to the declaration defaults (pops_compiled_param_defaults) so a later set_param overwrites
530 // only one entry without resetting the others to zero.
531 auto nparams_fn = reinterpret_cast<nv_fn_t>(pops::dynlib::sym(h, "pops_compiled_nparams"));
532 const int nparams = nparams_fn ? nparams_fn() : 0;
533 using res_p_fn_t = void (*)(const double*, double*, const double*, int, double, double, int,
534 const char*, const char*, int, const double*, int, double);
535 using adv_p_fn_t = void (*)(double*, const double*, int, double, double, int, const char*,
536 const char*, int, int, double, int, const double*, int, double);
537 using max_p_fn_t =
538 double (*)(const double*, const double*, int, double, double, int, const double*, int);
539 using poi_p_fn_t = void (*)(const double*, double*, int, const double*, int);
540 auto res_p_fn = reinterpret_cast<res_p_fn_t>(pops::dynlib::sym(h, "pops_compiled_residual_p"));
541 auto adv_p_fn = reinterpret_cast<adv_p_fn_t>(pops::dynlib::sym(h, "pops_compiled_advance_p"));
542 auto max_p_fn = reinterpret_cast<max_p_fn_t>(pops::dynlib::sym(h, "pops_compiled_max_speed_p"));
543 auto poi_p_fn = reinterpret_cast<poi_p_fn_t>(pops::dynlib::sym(h, "pops_compiled_poisson_rhs_p"));
544 // SHARED block of current values: captured by the closures AND registered in P->block_params_
545 // (set_block_params writes there -> the closures see the new value at the next step). Empty if the
546 // block has no runtime param or if the `_p` ABI is absent (old .so): the closures then call
547 // the historical symbols (const path). Non-null pv triggers the `_p` path.
548 std::shared_ptr<std::vector<double>> pv;
549 const bool use_params = (nparams > 0 && res_p_fn && adv_p_fn && max_p_fn && poi_p_fn);
550 // POSITIVITY LIMITER (ADC-76): pos_floor > 0 goes through the `_p` variants (the only signatures
551 // that carry it). An old .so without `_p` cannot apply it -> clear error (never a silent
552 // path without positivity). pos_floor <= 0: historical paths, bit-identical.
553 if (pos_floor > 0 && (!res_p_fn || !adv_p_fn))
554 throw std::runtime_error(
555 "add_compiled_block: positivity_floor > 0 requires the `_p` ABI of the .so (regenerate the "
556 "compiled module with the current headers)");
557 if (use_params) {
558 pv = std::make_shared<std::vector<double>>(static_cast<std::size_t>(nparams), 0.0);
559 auto defs_fn =
560 reinterpret_cast<void (*)(double*)>(pops::dynlib::sym(h, "pops_compiled_param_defaults"));
561 if (defs_fn)
562 defs_fn(pv->data()); // seed to the declaration defaults
563 } // registration in P->block_params_ DEFERRED to just before push_back (after the validations that
564 // may throw: avoids an orphan entry without an associated block if the addition fails).
565 // OPTIONAL metadata (names / roles / gamma) transported by the extended ABI of the .so. Absent
566 // from an old .so -> empty meta, we fall back on the fallback (names u0.. / no roles / gamma 1.4).
567 const BlockMeta meta = read_block_meta(h);
568 // Widen the SHARED aux channel so set_magnetic_field/T_e populate it and the marshaling
569 // transports the extra components to the .so. Base model (3) -> no-op (bit-identical).
570 P->ensure_aux_width(naux);
571 std::shared_ptr<void> lib(h, [](void* p) { // closes the .so when the closures die
573 });
574 const int n = P->cfg.n;
575 const double dx = P->geom.dx(), dy = P->geom.dy();
576 const int per = P->periodic_ ? 1 : 0;
577 const std::string lim = limiter, riem = riemann;
578
579 std::function<void(MultiFab&, MultiFab&)> rhs_into = [P, lib, res_fn, res_p_fn, pv, nv, naux, n,
580 dx, dy, per, lim, riem, recon_prim,
581 pos_floor](MultiFab& U, MultiFab& R) {
582 // HOST path (compiled block .so): same MPI guard as add_poisson. On a rank without a local
583 // box (local_size()==0 at np>1) there is nothing to marshal; the owner rank carries
584 // the full physics. No collective here -> one-sided no-op, no deadlock.
585 if (U.local_size() == 0)
586 return;
587 // ADC-369: marshal naux comps + the per-field halo tail (read by make_grid in the .so).
588 std::vector<double> u = P->copy_state(U, nv), a = marshal_aux_halo(P, naux);
589 std::vector<double> r(static_cast<std::size_t>(nv) * n * n, 0.0);
590 if (pv || pos_floor > 0) // `_p` variant: RUNTIME params (P7-b) and/or positivity (ADC-76)
591 res_p_fn(u.data(), r.data(), a.data(), n, dx, dy, per, lim.c_str(), riem.c_str(), recon_prim,
592 pv ? pv->data() : nullptr, pv ? static_cast<int>(pv->size()) : 0, pos_floor);
593 else
594 res_fn(u.data(), r.data(), a.data(), n, dx, dy, per, lim.c_str(), riem.c_str(), recon_prim);
595 P->write_state(R, nv, r);
596 };
597 std::function<void(MultiFab&, Real, int)> advance = [P, lib, adv_fn, adv_p_fn, pv, nv, naux, n,
598 dx, dy, per, lim, riem, recon_prim, imex,
599 pos_floor](MultiFab& U, Real dt, int nsub) {
600 // Same MPI guard: empty rank -> no-op. No collective -> without deadlock.
601 if (U.local_size() == 0)
602 return;
603 std::vector<double> u = P->copy_state(U, nv), a = marshal_aux_halo(P, naux); // ADC-369 tail
604 if (pv || pos_floor > 0) // `_p` variant: RUNTIME params (P7-b) and/or positivity (ADC-76)
605 adv_p_fn(u.data(), a.data(), n, dx, dy, per, lim.c_str(), riem.c_str(), recon_prim, imex,
606 static_cast<double>(dt), nsub, pv ? pv->data() : nullptr,
607 pv ? static_cast<int>(pv->size()) : 0, pos_floor);
608 else
609 adv_fn(u.data(), a.data(), n, dx, dy, per, lim.c_str(), riem.c_str(), recon_prim, imex,
610 static_cast<double>(dt), nsub);
611 P->write_state(U, nv, u);
612 };
613 std::function<Real(const MultiFab&)> max_speed = [P, lib, max_fn, max_p_fn, pv, nv, naux, n, dx,
614 dy, per](const MultiFab& U) -> Real {
615 // Same MPI guard: empty rank -> local speed 0. The downstream all_reduce_max takes the global max.
616 if (U.local_size() == 0)
617 return Real(0);
618 std::vector<double> u = P->copy_state(U, nv), a = marshal_aux_halo(P, naux); // ADC-369 tail
619 if (pv) // RUNTIME params (P7-b)
620 return max_p_fn(u.data(), a.data(), n, dx, dy, per, pv->data(), static_cast<int>(pv->size()));
621 return max_fn(u.data(), a.data(), n, dx, dy, per);
622 };
623 std::function<void(const MultiFab&, MultiFab&)> add_poisson = [P, lib, poi_fn, poi_p_fn, pv, nv,
624 n](const MultiFab& U,
625 MultiFab& rhs) {
626 // HOST path (compiled block .so prototype): same MPI guard as the dynamic block. On a
627 // rank without a local box (local_size()==0 at np>1) there is nothing to marshal -> we skip, the
628 // owner rank carries the full contribution. Without it copy_state(U) / rhs.fab(0)
629 // would dereference a nonexistent fab (host crash).
630 if (rhs.local_size() == 0)
631 return;
632 std::vector<double> u = P->copy_state(U, nv);
633 std::vector<double> pr(static_cast<std::size_t>(n) * n, 0.0);
634 if (pv) // RUNTIME params (P7-b)
635 poi_p_fn(u.data(), pr.data(), n, pv->data(), static_cast<int>(pv->size()));
636 else
637 poi_fn(u.data(), pr.data(), n);
638 Array4 r = rhs.fab(0).array();
639 const Box2D v = rhs.box(0);
640 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
641 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
642 r(i, j, 0) += pr[static_cast<std::size_t>(j - v.lo[1]) * n + (i - v.lo[0])];
643 };
644
645 // Variable descriptors: PRIORITY to the explicit name passed by the caller (names=), otherwise to the
646 // NAMES carried by the extended ABI of the .so (meta), otherwise fallback u0.. . The ROLES and the PRIMITIVE do not
647 // transit EXCEPT through the ABI (the API has no way to provide them): we take them from meta as-is.
648 VariableSet cons_vs = meta.cons, prim_vs = meta.prim;
649 if (!names.empty()) { // explicit override of the conservative names
650 if (static_cast<int>(names.size()) != nv)
651 throw std::runtime_error("System::add_compiled_block: names= has " +
652 std::to_string(names.size()) + " names but block '" + name +
653 "' has " + std::to_string(nv) + " variables");
654 cons_vs.names = names;
655 cons_vs.size = static_cast<int>(names.size());
656 }
657 if (cons_vs.names.empty()) { // neither names= nor meta: historical fallback u0..
658 for (int c = 0; c < nv; ++c)
659 cons_vs.names.push_back("u" + std::to_string(c));
660 cons_vs.size = nv;
661 }
662 if (prim_vs.names.empty())
663 prim_vs = {VariableKind::Primitive, cons_vs.names, cons_vs.size, {}};
664 // gamma: carried by the ABI if the model declares it (pops_compiled_gamma), otherwise historical default 1.4.
665 const double gamma = meta.has_gamma ? meta.gamma : 1.4;
666 typename ImplT::Species block{name,
667 MultiFab(P->ba, P->dm, nv, 2),
668 nv,
669 substeps,
670 true,
671 /*stride=*/1,
672 gamma,
673 std::move(advance),
674 std::move(rhs_into),
675 std::move(max_speed),
676 std::move(add_poisson)};
677 block.cons_vars = std::move(cons_vs);
678 block.prim_vars = std::move(prim_vs);
679 // cons <-> prim conversions OF THE MODEL via the extended ABI of the .so (set/get_primitive_state). The
680 // symbols operate on flat component-major arrays c*nn+k: called with n=1 (thus nn=1),
681 // they convert ONE cell (in/out = nv doubles), which is exactly the CellConvert contract.
682 // OPTIONAL: a .so generated before this work does not expose them -> empty conversion -> identity (the
683 // set/get_primitive_state path then falls back on prim == cons, exact for a scalar).
684 using cv_fn_t = void (*)(const double*, double*, int);
685 auto p2c_fn = reinterpret_cast<cv_fn_t>(pops::dynlib::sym(h, "pops_compiled_to_conservative"));
686 auto c2p_fn = reinterpret_cast<cv_fn_t>(pops::dynlib::sym(h, "pops_compiled_to_primitive"));
687 // P7-b: if the block has runtime params, the cons<->prim conversions must see them too (the
688 // conversion can read a runtime param). We then prefer the `_p` variants with the SHARED block.
689 using cv_p_fn_t = void (*)(const double*, double*, int, const double*, int);
690 auto p2c_p_fn =
691 reinterpret_cast<cv_p_fn_t>(pops::dynlib::sym(h, "pops_compiled_to_conservative_p"));
692 auto c2p_p_fn = reinterpret_cast<cv_p_fn_t>(pops::dynlib::sym(h, "pops_compiled_to_primitive_p"));
693 if (pv && p2c_p_fn)
694 block.prim_to_cons = [lib, p2c_p_fn, pv](const double* in, double* out) {
695 p2c_p_fn(in, out, 1, pv->data(), static_cast<int>(pv->size()));
696 };
697 else if (p2c_fn)
698 block.prim_to_cons = [lib, p2c_fn](const double* in, double* out) { p2c_fn(in, out, 1); };
699 if (pv && c2p_p_fn)
700 block.cons_to_prim = [lib, c2p_p_fn, pv](const double* in, double* out) {
701 c2p_p_fn(in, out, 1, pv->data(), static_cast<int>(pv->size()));
702 };
703 else if (c2p_fn)
704 block.cons_to_prim = [lib, c2p_fn](const double* in, double* out) { c2p_fn(in, out, 1); };
705 // PROJECTION PONCTUELLE post-pas (ADC-177) : symboles OPTIONNELS de l'ABI etendue
706 // (pops_compiled_has_projection / pops_compiled_project_p). Un .so genere avant ce chantier ne les
707 // expose pas -> aucune projection installee (chemin historique, bit-identique). has==1 sans la
708 // fonction de projection = .so incoherent -> erreur EXPLICITE plutot qu'un hook silencieusement
709 // ignore. Le stepper applique la fermeture a la FIN de chaque macro-pas entier (cf. SystemStepper).
710 auto has_proj_fn = reinterpret_cast<nv_fn_t>(pops::dynlib::sym(h, "pops_compiled_has_projection"));
711 if (has_proj_fn && has_proj_fn() != 0) {
712 using proj_fn_t = void (*)(double*, const double*, int, const double*, int);
713 auto proj_fn = reinterpret_cast<proj_fn_t>(pops::dynlib::sym(h, "pops_compiled_project_p"));
714 // Pas de close(h) ici : le shared_ptr `lib` possede deja le handle (fermeture a sa destruction).
715 if (!proj_fn)
716 throw std::runtime_error(
717 "add_compiled_block : pops_compiled_has_projection() == 1 mais pops_compiled_project_p "
718 "absent du .so (regenerer via dsl.compile(backend='aot'))");
719 block.project = [P, lib, proj_fn, pv, nv, n](MultiFab& U) {
720 // Meme garde MPI que les autres fermetures marshalees : rang sans box locale -> no-op
721 // unilateral (pas de collectif ici, aucun risque d'interblocage).
722 if (U.local_size() == 0)
723 return;
724 // NB (ADC-369): the projection reads the aux POINTWISE (compiled_block_abi::pointwise_project,
725 // no make_grid, no ghost) -> it needs NO per-field halo tail, so we marshal the plain aux here.
726 std::vector<double> u = P->copy_state(U, nv), a = P->copy_state(P->aux, P->aux_ncomp_);
727 proj_fn(u.data(), a.data(), n, pv ? pv->data() : nullptr,
728 pv ? static_cast<int>(pv->size()) : 0);
729 P->write_state(U, nv, u);
730 };
731 }
732 // P7-b: register the SHARED block of runtime params AFTER the validations (all passed here):
733 // set_block_params will find it by name, and the block closures share the same shared_ptr.
734 if (pv)
735 P->block_params_[name] = pv;
736 P->sp.push_back(std::move(block));
737 P->sp.back().U.set_val(Real(0));
738}
739
742template <typename ImplT>
743void add_native_block(System* self, ImplT* P, const std::string& name, const std::string& so_path,
744 const std::string& limiter, const std::string& riemann,
745 const std::string& recon, const std::string& time, double gamma, int substeps,
746 bool evolve, int stride, double pos_floor = 0) {
747 (void)P;
748 if (substeps < 1)
749 throw std::runtime_error("System::add_native_block: substeps >= 1");
750 if (stride < 1)
751 throw std::runtime_error("System::add_native_block: stride >= 1");
752 // UPFRONT validation of the scheme (like add_block / add_compiled_block): add_compiled_model interprets
753 // imex = (time=="imex"), recon_prim = (recon=="primitive") and the explicit RK scheme
754 // method = ssprk3 | euler | ssprk2 according to time; an unknown string would fall back SILENTLY on
755 // explicit/ssprk2/conservative. We thus reject a typo HERE rather than running a
756 // scheme different from the one requested. "ssprk3" (order 3, to pair with weno5) and "euler" (ForwardEuler,
757 // order 1: fidelity to first-order references, validation -- ADC-174) are ACCEPTED: the template
758 // add_compiled_model marshals them down to make_block of the .so, like the native path add_block. limiter/riemann are validated by make_block in the loader (clear
759 // exception, shared ABI verified further down).
760 if (recon != "conservative" && recon != "primitive")
761 throw std::runtime_error("System::add_native_block: recon 'conservative' | 'primitive' (got '" +
762 recon + "')");
763 if (time != "explicit" && time != "ssprk3" && time != "euler" && time != "imex")
764 throw std::runtime_error(
765 "System::add_native_block: time 'explicit' | 'ssprk3' | 'euler' | 'imex' "
766 "(got '" +
767 time +
768 "'; the IMEX-RK family ARS(2,2,2) is wired only on "
769 "a composite model pops.Model(...) -> native add_block)");
770 // WENO5 (5-point stencil, 3 ghosts) is now EXPOSED by the native path: the loader inlines
771 // add_compiled_model which, after install_block, reallocates the block state to block_n_ghost(limiter) (3
772 // for weno5) -- SAME mechanism as add_block. assemble_rhs thus does not read out of bounds. limiter is
773 // validated by make_block in the loader (none|minmod|vanleer|weno5).
774 // "production" path of the DSL: the generated .so loader (emit_cpp_native_loader) inlines the header
775 // template add_compiled_model<ProdModel>, which builds the closures on the REAL CONTEXT of the
776 // System (grid_context) and installs the block via install_block -- NATIVE path, zero-copy, SAME
777 // MultiFab as add_block (no marshaling of flat arrays like add_compiled_block). The loader
778 // thus calls out-of-line methods of pops::System DEFINED in THIS module; they must be
779 // resolved through the dlopen against the already-loaded _pops module.
780 // ELF PORTABILITY (Linux): CPython loads _pops in RTLD_LOCAL, so its symbols are NOT in
781 // the global scope and the loader could not resolve them. We thus PROMOTE the current module to
782 // global scope (RTLD_NOLOAD = without reloading it, just promote; RTLD_GLOBAL OR'ed into the
783 // flags of the already-loaded object). The module path is found by dladdr on an exported symbol
784 // (pops::abi_key). On macOS it is harmless (the loader already resolves via dynamic_lookup).
785#if defined(_WIN32)
786 // Windows (ADC-100): NO RTLD_GLOBAL. The generated .dll is linked at COMPILATION against the
787 // import libraries: _pops.lib (System symbols POPS_EXPORT: install_block/grid_context/...) and
788 // kokkoscore.lib (Kokkos runtime SHARED as a DLL). Its undefineds are thus resolved by the OS loader
789 // against _pops.pyd and kokkos*.dll already loaded -- no need to promote a global scope. We
790 // simply load the .dll and resolve pops_install_native.
792 if (!h)
793 throw std::runtime_error(
794 "add_native_block: LoadLibrary('" + so_path + "'): " + pops::dynlib::last_error() +
795 " (the .dll must be linked against _pops.lib + kokkoscore.lib; cf. ADC-100)");
796 {
797 auto key_fn = reinterpret_cast<const char* (*)()>(pops::dynlib::sym(h, "pops_native_abi_key"));
798 if (!key_fn) {
800 throw std::runtime_error("add_native_block: pops_native_abi_key missing from the .dll");
801 }
802 const std::string loader_key = key_fn();
803 const std::string module_key = abi_key();
804 if (loader_key != module_key) {
806 throw std::runtime_error("add_native_block: incompatible ABI -- loader key '" + loader_key +
807 "' != module key '" + module_key + "'");
808 }
809 using install_fn_t = void (*)(void*, const char*, const char*, const char*, const char*,
810 const char*, double, int, int, int, double);
811 auto install = reinterpret_cast<install_fn_t>(pops::dynlib::sym(h, "pops_install_native"));
812 if (!install) {
814 throw std::runtime_error("add_native_block: pops_install_native missing from the .dll");
815 }
816 install(static_cast<void*>(self), name.c_str(), limiter.c_str(), riemann.c_str(), recon.c_str(),
817 time.c_str(), gamma, substeps, evolve ? 1 : 0, stride, pos_floor);
818 }
819 // .dll left loaded for the duration of the process (the block points to code that lives in it).
820#else
821 {
822 Dl_info info;
823 if (dladdr(reinterpret_cast<void*>(&pops::abi_key), &info) && info.dli_fname)
824 dlopen(info.dli_fname, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
825 }
826 // RTLD_GLOBAL: places the loader symbols in the global scope AND lets the loader resolve
827 // its undefineds (the exported System methods POPS_EXPORT) against the already-loaded images. RTLD_NOW:
828 // immediate resolution -> a missing System symbol (module without POPS_EXPORT) fails HERE, not in flight.
829 void* h = dlopen(so_path.c_str(), RTLD_NOW | RTLD_GLOBAL);
830 if (!h) {
831 const char* e = dlerror();
832 throw std::runtime_error("add_native_block: dlopen('" + so_path +
833 "'): " + std::string(e ? e : "?") +
834 " (the pops::System symbols (install_block/grid_context/"
835 "ensure_aux_width) must be exported AND the _pops module loaded "
836 "globally; cf. POPS_EXPORT)");
837 }
838 // EXPLICIT ABI GUARD: the key baked into the loader (at ITS compilation) must equal the module
839 // key (at ITS compilation). A mismatch = divergent headers / compiler / standard -> memory
840 // layout of System/GridContext/BlockClosures potentially different across the boundary ->
841 // UB. We throw a CLEAR error rather than letting an incompatible loader through.
842 auto key_fn = reinterpret_cast<const char* (*)()>(pops::dynlib::sym(h, "pops_native_abi_key"));
843 if (!key_fn) {
845 throw std::runtime_error(
846 "add_native_block: pops_native_abi_key missing from the .so (regenerate via "
847 "dsl.compile_native / compile(backend='production'))");
848 }
849 const std::string loader_key = key_fn();
850 const std::string module_key = abi_key();
851 if (loader_key != module_key) {
853 throw std::runtime_error("add_native_block: incompatible ABI -- loader key '" + loader_key +
854 "' != module key '" + module_key +
855 "'. Recompile the loader with the SAME compiler, C++ standard and "
856 "pops headers as the _pops module.");
857 }
858 // Native installer of the loader: reinterpret_cast<System*>(this) then add_compiled_model<ProdModel>.
859 // Scheme (limiter/riemann/recon/time/gamma/substeps) marshaled as flat extern "C" arguments: the
860 // loader reconstructs imex/recon_prim and calls the template. evolve is passed so a fixed-background
861 // block (not advanced) is possible as via add_block.
862 using install_fn_t = void (*)(void*, const char*, const char*, const char*, const char*,
863 const char*, double, int, int, int, double);
864 auto install = reinterpret_cast<install_fn_t>(pops::dynlib::sym(h, "pops_install_native"));
865 if (!install) {
867 throw std::runtime_error(
868 "add_native_block: pops_install_native missing from the .so (regenerate via "
869 "dsl.compile_native / compile(backend='production'))");
870 }
871 // NB signature: pops_install_native now carries pos_floor (positivity limiter,
872 // ADC-76) as the final flat argument. A loader generated BEFORE this addition has a different C
873 // signature -- it is REJECTED upstream by the ABI key (POPS_HEADER_SIG changes with the headers), never
874 // called with a wrong argument layout.
875 install(static_cast<void*>(self), name.c_str(), limiter.c_str(), riemann.c_str(), recon.c_str(),
876 time.c_str(), gamma, substeps, evolve ? 1 : 0, stride, pos_floor);
877 // The .so stays loaded (RTLD_GLOBAL) for the duration of the process: the installed block points to code
878 // (block_builder closures, named functors) that lives in it. We do NOT close it (no ownership
879 // to hook onto: the closures are copied into the System registry but their CODE is
880 // in the .so). Consistent with a production binary where the model is linked for life.
881#endif // _WIN32 (POSIX-only production path; Windows = throw, cf. ADC-100)
882}
883
884} // namespace native_loader
885} // namespace pops
ABI key of the pops core: stable string identifying the (compiler, C++ standard, header tree signatur...
BoxArray: the set of boxes tiling a level (disjoint, covering).
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
Coupled multi-species system, composed at runtime from generic bricks.
Definition system.hpp:89
#define POPS_AUX_MARSHAL(name, idx)
TYPE-ERASED model interface: runtime dispatch of a model (via vtable).
PORTABLE dynamic loading: dlopen/dlsym/dlclose (POSIX) <-> LoadLibraryW/ GetProcAddress/FreeLibrary (...
Geometry: index-space (Box2D) <-> Cartesian physical-space mapping; PolarGeometry: SIBLING for a glob...
Block grid context plus closures, shared between System (which installs them) and block_builder....
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
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
void add_dynamic_block(System *self, ImplT *P, const std::string &name, const std::string &so_path, int substeps, const std::vector< std::string > &names, const std::string &recon)
Body of System::add_dynamic_block. VERBATIM; self = the calling System, P = self->p_....
Definition native_loader.hpp:419
void add_native_block(System *self, ImplT *P, const std::string &name, const std::string &so_path, const std::string &limiter, const std::string &riemann, const std::string &recon, const std::string &time, double gamma, int substeps, bool evolve, int stride, double pos_floor=0)
Body of System::add_native_block.
Definition native_loader.hpp:743
VariableSet parse_var_set(VariableKind kind, const std::string &names_csv, const std::string &roles_csv)
Builds a VariableSet from the CSV names / roles (parallel to names; EMPTY roles = not provided -> Var...
Definition native_loader.hpp:208
POPS_HD double limited_slope(double am, double ap, int recon)
MUSCL limited slope of a cell from backward difference am and forward ap.
Definition native_loader.hpp:67
void push_dynamic(ImplT *P, const std::string &name, pops::dynlib::handle h, int substeps, std::vector< std::string > names, int recon)
Builds a DYNAMIC block (IModel<NV> model loaded from the .so h) and adds it.
Definition native_loader.hpp:249
std::vector< double > marshal_aux_halo(Impl *P, int naux)
ADC-369: marshal the aux for a COMPILED block – the naux valid components (component-major,...
Definition native_loader.hpp:49
std::vector< std::string > split(const std::string &s, char sep)
Split s on sep (empty fields kept).
Definition native_loader.hpp:181
std::vector< double > host_residual(const IModel< NV > &m, const std::vector< double > &U, const std::vector< double > &AUX, int n, double dx, int recon)
Host residual R = -div F* + S(U, aux) (Rusanov, GLOBAL a_max like pops.PythonFlux,...
Definition native_loader.hpp:90
void add_compiled_block(System *self, ImplT *P, const std::string &name, const std::string &so_path, const std::string &limiter, const std::string &riemann, const std::string &recon, const std::string &time, int substeps, const std::vector< std::string > &names, double pos_floor=0)
Body of System::add_compiled_block. VERBATIM; self = the calling System, P = self->p_....
Definition native_loader.hpp:468
BlockMeta read_block_meta(pops::dynlib::handle h)
Reads (by dlsym, all OPTIONAL) the metadata symbols of an already-open .so (h).
Definition native_loader.hpp:222
Definition amr_hierarchy.hpp:29
VariableKind
Kind of a variable set: conserved (U) or primitive (W).
Definition variables.hpp:22
constexpr int kAuxBaseComps
Definition state.hpp:147
double Real
Definition types.hpp:30
POPS_EXPORT std::string abi_key()
ABI key of the module (TU system.cpp).
constexpr int kAuxMaxExtra
Definition state.hpp:100
std::string roles_csv(const VariableSet &vs)
CSV of a VariableSet's roles (role_name, separator ',').
Definition variables.hpp:172
void parse_roles_into(VariableSet &vs, const std::string &csv)
Inverse of roles_csv: fill vs.roles (and vs.user_roles for any NON-canonical token) from a roles CSV.
Definition variables.hpp:190
std::string names_csv(const VariableSet &vs)
CSV of a VariableSet's names (separator ',').
Definition variables.hpp:158
constexpr int kAuxNamedBase
Definition state.hpp:154
PHYSICAL boundary conditions at the domain edge (BCType, BCRec, fill_physical_bc, fill_ghosts).
Pointwise types of the physics layer: StateVec<N> (conserved state) and Aux (auxiliary fields from th...
#define POPS_AUX_FIELDS(X)
SINGLE SOURCE of the layout of the EXTRA aux fields (X-macro).
Definition state.hpp:92
WRITE POD handle (raw pointer + strides) over a Fab2D buffer, indexed by (i, j, c) IN GLOBAL INDICES ...
Definition fab2d.hpp:29
POINTWISE auxiliary fields shared with the physics: single coupling channel.
Definition state.hpp:123
Real phi
Definition state.hpp:124
2D integer index space, cell-centered.
Definition box2d.hpp:37
int hi[2]
Definition box2d.hpp:39
int lo[2]
Definition box2d.hpp:38
Hyperbolic model seen behind a virtual interface (runtime dispatch).
Definition dynamic_model.hpp:27
virtual State source(const State &, const Aux &) const
Source term S(U, aux) (default: zero, model without source).
Definition dynamic_model.hpp:35
virtual Real max_wave_speed(const State &u, const Aux &a, int dir) const =0
virtual State flux(const State &u, const Aux &a, int dir) const =0
virtual State to_primitive(const State &u) const
Conservative -> primitive conversion (P = M.to_primitive(U)).
Definition dynamic_model.hpp:42
Conserved state vector of fixed size, known at compile time.
Definition state.hpp:29
A model's variable set: kind (cons/prim), names, size, canonical roles (optional, parallel to names; ...
Definition variables.hpp:58
std::vector< std::string > names
Definition variables.hpp:60
int size
Definition variables.hpp:61
OPTIONAL metadata read by dlsym on a generated .so (names / roles / gamma).
Definition native_loader.hpp:199
double gamma
Definition native_loader.hpp:201
bool has_gamma
Definition native_loader.hpp:200
VariableSet cons
Definition native_loader.hpp:202
VariableSet prim
Definition native_loader.hpp:203
Runtime multi-species composition: one coupled system, block by block.
Base scalar types and the POPS_HD macro (host+device portability).
#define POPS_HD
Definition types.hpp:25
Descriptor of a model's variables (Vars).