26#include <nanobind/nanobind.h>
27#include <nanobind/ndarray.h>
28#include <nanobind/stl/array.h>
29#include <nanobind/stl/function.h>
30#include <nanobind/stl/optional.h>
31#include <nanobind/stl/string.h>
32#include <nanobind/stl/tuple.h>
33#include <nanobind/stl/variant.h>
34#include <nanobind/stl/vector.h>
39#include <Kokkos_Core.hpp>
57#include "peclet/core/common/types.hpp"
58#include "peclet/core/geom/scene_query.hpp"
59#include "peclet/core/geom/sdf.hpp"
60#include "peclet/core/python/kokkos_teardown.hpp"
61#include "peclet/core/python/ndarray_interop.hpp"
63namespace nb = nanobind;
64using namespace peclet::core;
76using DArray = nb::ndarray<double, nb::c_contig>;
81nb::ndarray<nb::numpy, T>
vec1(std::vector<T>
v) {
82 const std::size_t n =
v.
size();
83 return peclet::core::python::vector_to_ndarray<T>(std::move(
v), {n}, {1});
86nb::ndarray<nb::numpy, T>
vec2(std::vector<T>
v, std::size_t
cols) {
87 const std::size_t n =
v.size() /
cols;
88 return peclet::core::python::vector_to_ndarray<T>(std::move(
v), {n,
cols},
89 {
static_cast<std::int64_t
>(
cols), 1});
94 const Index n =
t.numLeaves();
95 std::vector<double> d(
static_cast<std::size_t
>(n) * 3);
109 throw std::runtime_error(
"sizes(axis): axis must be 0, 1 or 2");
110 const Index n =
t.numLeaves();
111 std::vector<double> d(
static_cast<std::size_t
>(n));
113 d[
i] =
geo.leafSize(
t.level(
i), axis);
119 const Index n =
t.numLeaves();
120 std::vector<std::int32_t> d(
static_cast<std::size_t
>(n));
122 d[
i] =
static_cast<std::int32_t
>(
t.level(
i));
128 const Index n =
t.numLeaves();
129 std::vector<std::uint64_t> d(
static_cast<std::size_t
>(n));
131 d[
i] =
static_cast<std::uint64_t
>(
t.code(
i));
142 if (
static_cast<Index>(field.shape(0)) !=
t.numLeaves())
143 throw std::runtime_error(std::string(
who) +
": field length != num_leaves");
144 const double*
f = field.data();
145 return std::vector<double>(
f,
f +
static_cast<std::size_t
>(
t.numLeaves()));
154using peclet::core::python::Releasable;
168 const std::array<long, 3>& cells) {
169 std::array<double, 3>
h{};
170 for (
int d = 0; d < 3; ++d) {
171 if (!(extent[d] > 0.0))
172 throw std::runtime_error(
"extent: every component must be > 0");
174 throw std::runtime_error(
"extent: every finest-cell count must be > 0");
175 h[d] = extent[d] / (
double)cells[d];
184 const std::array<long, 3>& cells) {
186 for (
int d = 1; d < 3; ++d)
187 if (std::fabs(
h[d] -
h[0]) > 1
e-12 * std::fabs(
h[0])) {
189 std::snprintf(
msg,
sizeof msg,
190 "spacing_from_extent returns ONE number, but extent/cells gives "
191 "dx=%.17g dy=%.17g dz=%.17g — the octree's cells are CUBES here. Use "
192 "spacings_from_extent (or pass `extent` to the constructor) for a box mesh.",
194 throw std::runtime_error(
msg);
200using SpacingArg = std::variant<double, std::array<double, 3>>;
202 if (
const double* d = std::get_if<double>(&
sp))
204 return std::get<std::array<double, 3>>(
sp);
209inline std::array<long, 3>
rootCellsOf(
const std::array<long, 3>& cells,
unsigned lmax,
211 const long f = (
long)1 << lmax;
212 std::array<long, 3>
root{};
213 for (
int d = 0; d < 3; ++d) {
214 if (cells[d] <= 0 || cells[d] %
f != 0) {
216 std::snprintf(
msg,
sizeof msg,
217 "%s: cells=(%ld, %ld, %ld) must be positive multiples of 2**lmax = %ld "
218 "(`cells` is the FINEST grid; the root brick is cells / 2**lmax)",
219 who, cells[0], cells[1], cells[2],
f);
220 throw std::runtime_error(
msg);
222 root[d] = cells[d] /
f;
228inline std::array<double, 3>
resolveSpacing(
const std::optional<SpacingArg>& spacing,
229 const std::optional<std::array<double, 3>>& extent,
230 const std::array<long, 3>& cells,
const char*
who) {
231 if (spacing && extent)
232 throw std::runtime_error(std::string(
who) +
233 ": pass EITHER spacing= (the finest cell size) OR extent= (the box "
234 "side lengths; spacing = extent / cells), not both");
239 return {1.0, 1.0, 1.0};
249 std::array<double, 3>
spacing) {
262 const auto b = t_.
brick();
264 return {(
long)b[0] *
f, (
long)b[1] *
f, (
long)b[2] *
f};
271 std::array<double, 3>
spacing()
const {
return {geo_.
h0[0], geo_.
h0[1], geo_.
h0[2]}; }
275 nb::ndarray<nb::numpy, double>
sizes(
int axis)
const {
return leafSizes(t_, geo_, axis); }
282 std::array<Coord, 3>
fine{};
283 for (
int d = 0; d < 3; ++d) {
284 double f = (x[d] - geo_.
origin[d]) / geo_.
h0[d];
296 geom::Sphere
s{{center[0], center[1], center[2]},
radius};
313 std::function<
unsigned(
double,
double,
double)>
target_level,
316 t_, geo_, [&](
const Vec<3>&
p) {
return sdf(
p[0],
p[1],
p[2]); },
323 std::function<
double(
double,
double,
double)>
gap,
357 t_ = std::move(
r.octree);
395 std::vector<double>
out;
396 mg_.
op(0).applyLaplacian(toVec(
u,
"apply"),
out);
402 std::vector<double> res;
403 return mg_.
op(0).residual(toVec(
u,
"residual"), toVec(
rhs,
"residual"), res);
410 std::vector<double>
rv = toVec(
rhs,
"solve");
411 std::vector<double>
u(
static_cast<std::size_t
>(n_), 0.0);
413 u = toVec(*
x0,
"solve");
414 std::vector<double> res;
415 double r = mg_.
op(0).residual(
u,
rv, res);
417 for (
int k = 0; k <
cycles; ++k) {
419 r = mg_.
op(0).residual(
u,
rv, res);
428 std::vector<double> toVec(
const DArray&
a,
const char*
who)
const {
429 if (
static_cast<Index>(
a.shape(0)) != n_)
430 throw std::runtime_error(std::string(
who) +
": length != num_leaves");
431 const double*
p =
a.data();
432 return std::vector<double>(
p,
p +
static_cast<std::size_t
>(n_));
453class DistributedOctree;
455class Flow :
public Releasable {
458 flow_.
init(
oct.octreeRef(),
oct.geoRef().h0,
oct.geoRef().origin);
462 const auto&
t =
oct.octreeRef();
463 for (
int d = 0; d < 3; ++d) {
464 extent_[d] =
static_cast<double>(
t.brick()[d]) *
static_cast<double>(
Index(1) <<
t.lmax()) *
466 origin_[d] =
oct.geoRef().origin[d];
476 nb::gil_scoped_release
nogil;
497 flow_.
setPressure(std::vector<double>(
h.data(),
h.data() +
h.shape(0)));
501 nb::gil_scoped_release
nogil;
519 nb::gil_scoped_release
nogil;
530 nb::ndarray<const double, nb::c_contig>
radii,
bool periodic) {
531 if (centers.ndim() != 2 || centers.shape(1) != 3)
532 throw std::runtime_error(
"set_solid_spheres: centers must be (M, 3)");
533 const std::size_t m = centers.shape(0);
535 throw std::runtime_error(
"set_solid_spheres: radii must be (M,) or (1,)");
536 std::vector<double>
cx(m),
cy(m),
cz(m),
rr(m);
537 const double*
cp = centers.data();
538 const double*
rp =
radii.data();
540 for (std::size_t
i = 0;
i < m; ++
i) {
551 geom::SphereBedQuery
q(std::move(
cx), std::move(
cy), std::move(
cz), std::move(
rr),
552 Vec<3>{origin_[0], origin_[1], origin_[2]},
553 Vec<3>{extent_[0], extent_[1], extent_[2]}, periodic);
579 throw std::runtime_error(
"velocity: component must be 0..2");
590 auto*
buf =
new std::vector<std::uint8_t>(
static_cast<std::size_t
>(n_));
592 (*
buf)[
static_cast<std::size_t
>(
i)] = flow_.
isFluid(
i) ? 1 : 0;
593 nb::capsule owner(
buf,
594 [](
void*
p)
noexcept {
delete static_cast<std::vector<std::uint8_t>*
>(
p); });
595 return nb::ndarray<nb::numpy, bool>(
reinterpret_cast<bool*
>(
buf->data()),
596 {static_cast<std::size_t>(n_)}, owner, {1});
609 flow_.
setVelocity(
c, std::vector<double>(
h.data(),
h.data() +
h.shape(0)));
619 std::array<double, 3> extent_{0.0, 0.0, 0.0};
620 std::array<double, 3> origin_{0.0, 0.0, 0.0};
634 std::array<double, 3>
spacing, std::array<bool, 3> periodic) {
649 const auto&
hv = d_.
h0();
650 return {
hv[0],
hv[1],
hv[2]};
660 const auto&
hv = d_.
h0();
666 return {
o[0],
o[1],
o[2]};
670 return {b[0], b[1], b[2]};
674 return {b[0], b[1], b[2]};
678 return {b[0], b[1], b[2]};
682 nb::ndarray<nb::numpy, double>
centers()
const {
685 nb::ndarray<nb::numpy, double>
sizes(
int axis)
const {
696 geom::Sphere
s{{center[0], center[1], center[2]},
radius};
732 throw std::runtime_error(
"rebalance: fields.shape[0] != num_leaves");
733 const int K =
static_cast<int>(
fields.shape(1));
735 std::vector<std::vector<double>>
cols(
static_cast<std::size_t
>(
K),
736 std::vector<double>(
static_cast<std::size_t
>(n)));
738 for (
int c = 0;
c <
K; ++
c)
739 cols[
static_cast<std::size_t
>(
c)][
static_cast<std::size_t
>(
i)] =
in[
i *
K +
c];
744 std::vector<double>
out(
static_cast<std::size_t
>(m) *
static_cast<std::size_t
>(
K));
746 for (
int c = 0;
c <
K; ++
c)
747 out[
i *
K +
c] =
cols[
static_cast<std::size_t
>(
c)][
static_cast<std::size_t
>(
i)];
756 std::vector<double>
f =
asField(d_.
local(), field,
"face_neighbor_gather");
797 : n_(d.ref().local().numLeaves()) {
839 auto i64 = [](
const std::vector<peclet::core::Index>&
v) {
842 d[
"seam"] =
i64(
t.seam);
843 d[
"samp_i"] =
i64(
t.sampI);
844 d[
"samp_j"] =
i64(
t.sampJ);
845 d[
"uu_rec_i"] =
i64(
t.uuRecI);
846 d[
"uu_rec_j"] =
i64(
t.uuRecJ);
849 d[
"rec_start"] =
i64(
t.recStart);
851 d[
"rec_cell"] =
i64(
t.recCell);
853 d[
"cell_center"] =
vec2<double>(std::vector<double>(
t.center), 3);
889 c.def_prop_ro(
"cells", &T::cells,
890 "Finest-level cell counts per axis (root*2**lmax; the GLOBAL grid on a "
891 "DistributedOctree).")
892 .def_prop_ro(
"extent", &T::extent,
"Box side lengths in world units (cells*spacing).")
893 .def_prop_ro(
"spacing", &T::spacing,
894 "Finest cell size (dx, dy, dz), per axis. Equal on a cubic octree.")
895 .def_prop_ro(
"num_leaves", &T::num_leaves,
896 "Number of leaves (Z-order slots; this rank's on a DistributedOctree).")
897 .def_prop_ro(
"lmax", &T::lmax,
"Root-cell level (max refinement depth).")
898 .def_prop_ro(
"origin", &T::origin,
"Lower corner in world coordinates.")
899 .def(
"centers", &T::centers,
900 "Leaf world centres, (num_leaves, 3) float64 (global coordinates).")
901 .def(
"sizes", &T::sizes, nb::arg(
"axis") = 0,
902 "Leaf world widths along `axis`: spacing[axis]*2**level, (num_leaves,) float64. A leaf "
903 "is a BOX, so `axis` selects which of the three widths (0 by default, which is THE "
904 "width on a cubic octree).")
905 .def(
"levels", &T::levels,
"Leaf refinement levels, (num_leaves,) int32 (0 = finest).")
906 .def(
"codes", &T::codes,
"Leaf block-local Morton origin codes, (num_leaves,) uint64.")
907 .def(
"refine_to_sphere", &T::refine_to_sphere, nb::arg(
"center"), nb::arg(
"radius"),
908 nb::arg(
"target_level") = 0
u, nb::arg(
"band") = 1.0, nb::arg(
"balance") =
true,
909 "Refine leaves the sphere surface passes through (plus `band` cells) down to "
910 "target_level; optionally restore 2:1 balance (cross-block, collective, on a "
911 "DistributedOctree). Returns the (local) number of refinements performed.")
913 "refine_to_sdf", &T::refine_to_sdf, nb::arg(
"sdf"), nb::arg(
"target_level") = 0
u,
914 nb::arg(
"band") = 1.0, nb::arg(
"balance") =
true,
915 "Refine toward an arbitrary signed-distance field given as a callable f(x,y,z)->distance "
916 "(suite sign: <0 inside solid), down to target_level — rings / packed beds / any "
917 "non-sphere geometry. Collective when balance=True on a DistributedOctree. Returns "
918 "refinements performed.")
919 .def(
"balance", &T::balance,
920 "Enforce 2:1 graded balance to a fixpoint (cross-block and collective on a "
921 "DistributedOctree); returns (this rank's) refinements performed.")
923 "lohner_indicator", &T::lohner_indicator, nb::arg(
"field"), nb::arg(
"eps") = 0.01,
924 "Löhner normalized-second-difference feature indicator E in [0,1] per leaf from a scalar "
925 "field (num_leaves,); large E = steep feature (refine), small = smooth (coarsen). On a "
926 "DistributedOctree it is evaluated across the owner-based halo (collective).")
927 .def(
"adapt", &T::adapt, nb::arg(
"field"), nb::arg(
"refine_thresh"),
928 nb::arg(
"coarsen_thresh"), nb::arg(
"finest_level") = 0
u, nb::arg(
"eps") = 0.01,
929 nb::arg(
"linear") =
true,
930 "Solution-adaptive step (Löhner-driven): refine where the indicator > refine_thresh (to "
931 "finest_level), coarsen sibling groups all < coarsen_thresh, 2:1-balance, and "
932 "conservatively remap `field`. MUTATES the octree in place; returns the remapped field "
933 "(M,). `linear` uses minmod-limited prolongation (else piecewise-constant). On a "
934 "DistributedOctree: per block, cross-block balance, ORB ownership kept, bit-identical "
935 "across rank counts (collective).")
936 .def(
"write_vtu", &T::write_vtu, nb::arg(
"path"), nb::arg(
"name"), nb::arg(
"field"),
937 "Write the octree (this rank's block on a DistributedOctree — one file per rank, "
938 "combine in ParaView) + a per-leaf scalar field (num_leaves,) as a VTK UnstructuredGrid "
939 "(.vtu, ASCII, one cell per leaf).");
950 peclet::core::python::install(m);
953 "peclet.amr — adaptive mesh refinement: per-block Octree (serial) and DistributedOctree "
954 "(MPI ORB) for the mesh, the geometric-multigrid Poisson solver, and the device (Kokkos) "
955 "Flow cut-cell Stokes/Navier-Stokes solver. Build a graded octree, refine to an SDF "
956 "surface, read leaf geometry + per-leaf fields as numpy, load-rebalance, gather face "
957 "neighbours, export VTU, and run the flow step on device.";
962 const auto root = rootCellsOf(cells, lmax,
"predict_hierarchy");
964 throw std::runtime_error(
"predict_hierarchy: num_ranks must be >= 1");
966 throw std::runtime_error(
"predict_hierarchy: bottom_extent must be >= 1");
970 for (
const auto&
lv :
p.levels) {
973 extentL.append(nb::make_tuple(
lv.extent[0],
lv.extent[1],
lv.extent[2]));
979 d[
"num_levels"] = (
long)
p.levels.size();
980 d[
"num_in_place"] = (
long)
p.numInPlace();
983 for (
auto k :
p.stages)
986 d[
"bottom"] =
p.bottomName();
989 nb::arg(
"cells"), nb::arg(
"lmax") = 0
u, nb::arg(
"num_ranks") = 1,
990 nb::arg(
"bottom_extent") = 4,
991 "The pressure-multigrid ladder a Flow builds on `cells` FINEST cells per axis at tree depth "
992 "`lmax` over `num_ranks` ranks (docs/amr_mg_depth.md §6.2, §6.7) — a pre-flight tool. A "
993 "pure function: it builds no mesh and needs no MPI; it re-runs the ladder rule on the ORB "
994 "a freshly constructed DistributedOctree would produce (not a partition after "
995 "`DistributedOctree.rebalance` / `Flow.rebalance_mpi`). Below the root brick the ladder "
996 "halves the grid while every rank's block stays even; where it stops above "
997 "`bottom_extent` cells per axis on more than one rank the level MOVES (a stage): onto fewer "
998 "ranks by a sibling merge or a repartition when the Flow's stage policy is on "
999 "(docs/amr_mg_depth.md WO4b), where the ladder lifts again, else gathered onto every rank "
1000 "(the replicated tail) and continued there; the coarsest level is then solved "
1001 "by damped-Jacobi sweeps if it has at most `bottom_extent` cells per axis, else by the "
1002 "agglomerated GraphAMG-PCG bottom. The prediction is that of the default "
1003 "`Flow.set_pressure_bottom('auto')`.\n\n"
1004 "Returns a dict, levels finest first: `extent` the level's global grid in CELLS per axis "
1005 "(a count, not a length), `cells` its total cell count as if the mesh were uniform (so it "
1006 "equals `Flow.diagnostics.pressure_mg_levels` leaf for leaf only for a uniform mesh at "
1007 "num_ranks=1), `kind` ('octree' at or above the root brick, 'lifted' below it in place, "
1008 "'tail' / 'sibling' / 'repartition' on a stage's continued ladder -- a stage's first level "
1009 "is the SAME grid as the level above it, moved rather than coarsened, exactly as "
1010 "`pressure_mg_levels` lists it), `num_levels`, `num_in_place` (levels above the first "
1011 "stage), `stages` (the stage kinds, outermost first), `tail` (bool: a replicated tail "
1012 "engages) and `bottom` ('jacobi' or 'amg', suffixed '+tail' / '+sibling' / '+repartition' "
1013 "per stage, innermost first) — the spelling `Flow.diagnostics.pressure_mg_bottom` "
1015 "`lmax` is the number of octree coarsenings THE MESH supports, i.e. the tree's lmax for a "
1016 "mesh refined to level 0 somewhere. An UNREFINED Octree(cells, lmax=k>0) is the same mesh "
1017 "as Octree(cells/2**k, lmax=0) — all its leaves are root cells — and must be predicted that "
1018 "way. In general pass depth = tree.lmax - levels().min() and cells = root * 2**depth. "
1019 "`bottom_extent` (default 4, the solver's default) is the "
1020 "`Flow.pressure_bottom_extent` the run will use; pass the same value.");
1023 "spacing_from_extent",
1024 [](std::array<double, 3> extent, std::array<long, 3>
root_cells,
unsigned lmax) {
1025 const long f = (
long)1 << lmax;
1028 nb::arg(
"extent"), nb::arg(
"root_cells"), nb::arg(
"lmax"),
1029 "The finest cell width h0 = extent / (root_cells * 2**lmax) of a PHYSICAL domain — the "
1030 "one place an AMR spacing is computed, so no caller writes one (see "
1031 "suite/docs/PHYSICAL_UNITS_PLAN.md). Returns ONE number, so it raises when the extent does "
1032 "not give cubic cells — use `spacings_from_extent` for a box mesh.");
1035 "spacings_from_extent",
1036 [](std::array<double, 3> extent, std::array<long, 3>
root_cells,
unsigned lmax) {
1037 const long f = (
long)1 << lmax;
1038 return spacingsFromExtent(extent,
1041 nb::arg(
"extent"), nb::arg(
"root_cells"), nb::arg(
"lmax"),
1042 "The finest cell size (dx, dy, dz) = extent / (root_cells * 2**lmax), PER AXIS. The "
1043 "anisotropic form of `spacing_from_extent`: it accepts any positive extent, because the "
1044 "octree's cells are boxes (core/docs/amr_anisotropic.md).");
1046 nb::class_<Octree> octree(
1048 "Serial single-block adaptive octree with a world placement (origin + finest "
1049 "spacing per axis). Leaves are addressed in Z-order slot order; every per-leaf "
1050 "array is indexed by that slot.");
1054 [](
Octree*
self, std::array<long, 3> cells,
unsigned lmax, std::array<double, 3> origin,
1055 std::optional<SpacingArg> spacing, std::optional<std::array<double, 3>> extent) {
1056 const auto root = rootCellsOf(cells, lmax,
"Octree");
1057 new (
self)
Octree(
root, lmax, origin, resolveSpacing(spacing, extent, cells,
"Octree"));
1059 nb::arg(
"cells"), nb::kw_only(), nb::arg(
"lmax") = 0
u,
1060 nb::arg(
"origin") = std::array<double, 3>{0, 0, 0}, nb::arg(
"spacing") = nb::none(),
1061 nb::arg(
"extent") = nb::none(),
1062 "Build a uniform octree resolving `cells` FINEST cells per axis, refinable `lmax` levels "
1063 "(the root brick is cells/2**lmax root cells, which must divide exactly; lmax=0 is a "
1064 "uniform grid). `origin` is the block's lower corner. Every argument after `cells` is "
1066 "State the domain PHYSICALLY with `extent` = the box side lengths: the cell size is "
1067 "then extent/cells PER AXIS and the caller never writes a spacing. Alternatively give "
1068 "`spacing` (one number for cubic cells, or (dx, dy, dz)); default 1. Passing both "
1070 "The cells are BOXES (core/docs/amr_anisotropic.md): any positive extent is accepted, "
1071 "the octree refines by 2 on every axis, and every level inherits the root aspect "
1072 "ratio. Read the three numbers back from `.spacing`.")
1073 .
def(
"is_balanced", &Octree::is_balanced,
1074 "True iff every face-adjacent leaf pair differs by at most one level (2:1).")
1075 .def(
"find", &Octree::find, nb::arg(
"x"),
1076 "Index of the leaf containing world point x=(x,y,z), or -1 if outside the block.")
1077 .
def(
"refine_to_sdf_graded", &Octree::refine_to_sdf_graded, nb::arg(
"sdf"),
1078 nb::arg(
"target_level"), nb::arg(
"band") = 2.0, nb::arg(
"balance") =
true,
1079 "GRADED surface refinement (the mixed-level cut band, "
1080 "docs/amr_mixed_level_cut_band_plan.md §7): `target_level` is a callable "
1081 "f(x,y,z)->level giving the COARSEST acceptable level at a world point (0 = finest), "
1082 "so cut cells end up at SEVERAL levels — fine in throats/contacts, coarse on smooth "
1083 "caps. The band margin is measured in cells of the level being created (not in the "
1084 "finest spacing as in refine_to_sdf). Requires Flow.set_ghost_sampled(True) — the "
1085 "classic overlay contracts a uniform finest band and raises on these level jumps. "
1086 "Returns refinements performed.")
1087 .
def(
"refine_to_gap_floor", &Octree::refine_to_gap_floor, nb::arg(
"sdf"), nb::arg(
"gap"),
1088 nb::arg(
"coarsest_level"), nb::arg(
"n") = 4.0, nb::arg(
"band") = 2.0,
1089 nb::arg(
"balance") =
true,
1090 "refine_to_sdf_graded driven by the plan's GAP-WIDTH FLOOR (§7 criterion 1): the "
1091 "target level at a point is the coarsest L with n*h_L <= gap(x), clamped to "
1092 "[0, coarsest_level]. `gap` is the local fluid-gap proxy f(x,y,z)->width — for a "
1093 "sphere packing the two-closest-surfaces sum d1+d2; a medial-axis or peclet.pnm "
1094 "throat-radius field substitutes verbatim. n=4 per the M1/M2 measurements. This is "
1095 "the AMReX multi-valued-cell rule inverted: coarsening never merges or disconnects "
1097 .
def(
"refine_leaf", &Octree::refine_leaf, nb::arg(
"i"),
1098 "Split leaf `i` into its 8 children; returns True if it was split (level>0).");
1099 bindOctreeCommon(octree);
1101 nb::class_<Poisson>(
1103 "Cell-centered finite-volume Poisson solver (L u = rhs) on an Octree, by a "
1104 "geometric-multigrid V-cycle. L is the conservative two-point FV Laplacian (suite sign). "
1105 "The hierarchy snapshots the octree at construction; per-leaf arrays are (num_leaves,) "
1106 "float64 in Z-order slots.")
1108 nb::init<const Octree&, bool>(), nb::arg(
"octree"), nb::arg(
"periodic") =
true,
1109 "Build the multigrid hierarchy from `octree`. periodic=True solves the singular periodic "
1110 "problem (constant null space removed each cycle).")
1111 .def_prop_ro(
"num_leaves", &Poisson::num_leaves,
"Leaves on the finest level.")
1113 "num_levels", &Poisson::num_levels,
1114 "Number of multigrid levels: the octree's own coarsenings down to the root brick, then "
1115 "levels BELOW it (the root lifted, docs/amr_mg_depth.md §6.1-§6.2) for as long as some "
1116 "axis has more than 4 cells and every axis stays even with at least 2 cells after "
1117 "halving. So a uniform lmax=0 mesh has a real hierarchy (64^3: 5 levels).")
1118 .def(
"apply", &Poisson::apply, nb::arg(
"u"),
1119 "L applied to u (the FV Laplacian); use b = apply(u_exact) to manufacture a RHS. "
1120 "(num_leaves,) -> (num_leaves,).")
1121 .def(
"residual", &Poisson::residual, nb::arg(
"u"), nb::arg(
"rhs"),
1122 "Volume-weighted L2 residual norm sqrt(sum V*(rhs - L u)^2).")
1124 "solve", &Poisson::solve, nb::arg(
"rhs"), nb::arg(
"x0") = std::nullopt,
1125 nb::arg(
"cycles") = 20, nb::arg(
"pre") = 2, nb::arg(
"post") = 2, nb::arg(
"tol") = 0.0,
1126 "Solve L u = rhs with up to `cycles` V-cycles (pre/post Gauss-Seidel sweeps), from x0 or "
1127 "0, stopping once residual <= tol (tol<=0 disables). Returns (u (num_leaves,), "
1128 "final_residual, cycles_done).");
1132 "Collocated incompressible Stokes/Navier-Stokes step on an Octree with a cut-cell immersed "
1133 "boundary (no-slip on an SDF solid). step() = implicit viscous momentum predictor + "
1134 "Almgren-Bell-Colella rotational projection, whose pressure Poisson equation is solved by "
1135 "multigrid-preconditioned CG (the hierarchy continues below the octree's root brick; "
1136 "`diagnostics.pressure_mg_levels`). The box is TRIPLY PERIODIC and the only driving is a "
1137 "uniform body force (set_body_force); a wall is an immersed solid. Any consistent unit "
1138 "system: density [mass/length^3], dynamic viscosity [mass/(length*time)], dt [time], "
1139 "lengths those of the Octree's extent. Iterate step() to a steady state or march in time; "
1140 "velocities and pressure are per-leaf (num_leaves,) arrays in Z-order slots.")
1141 .def(nb::init<const Octree&, double, double, double>(), nb::arg(
"octree"),
1142 nb::arg(
"density") = 1.0, nb::arg(
"viscosity") = 1.0, nb::arg(
"dt") = 1
e6,
1145 "Create a flow on `octree` with the given density, dynamic viscosity and time step "
1146 "(defaults 1, 1 and 1e6). A large dt drives straight to the steady (Stokes) solution. "
1147 "The octree is borrowed by reference and must outlive the Flow's use of it.")
1148 .def(nb::init<DistributedOctree&, double, double, double>(), nb::arg(
"octree"),
1149 nb::arg(
"density") = 1.0, nb::arg(
"viscosity") = 1.0, nb::arg(
"dt") = 1
e6,
1150 nb::keep_alive<1, 2>(),
1151 "DISTRIBUTED (mpi4py, collective): run this solver on one ORB block of a "
1152 "DistributedOctree — the whole step (momentum, pressure, overlays, adaptivity) then "
1153 "executes multi-rank through the ±2 leaf ghost registry. Per-leaf arrays are this "
1154 "rank's LOCAL leaves. setSolid/step/project/begin_adapt/finish_adapt/rebalance_mpi "
1155 "are collective. np=1 is bit-identical to the single-rank constructor.")
1156 .def(
"rebalance_mpi", &Flow::rebalance_mpi, nb::arg(
"sdf"),
1157 "DISTRIBUTED: weighted-ORB load rebalance — migrates the leaves WITH the state (u, p) "
1158 "to the new owners and rebuilds every solver structure (collective). num_leaves is "
1160 .def_prop_ro(
"num_leaves", &Flow::num_leaves,
"Number of leaves.")
1161 .def(
"set_solid", &Flow::set_solid, nb::arg(
"sdf"),
1162 "Build the cut-cell operators from a signed-distance callable f(x,y,z) (>0 fluid, <0 "
1163 "solid) and zero the fields. Call before stepping; re-call to change the geometry.")
1164 .def(
"set_solid_spheres", &Flow::set_solid_spheres, nb::arg(
"centers"), nb::arg(
"radii"),
1165 nb::arg(
"periodic") =
true,
1166 "set_solid for a UNION OF SPHERES, evaluated natively instead of through a Python "
1167 "callback — the porous-media geometry. `centers` is (M,3), `radii` is (M,) or (1,); "
1168 "`periodic` uses the minimum-image convention over the octree's own box extent. "
1169 "Prefer this to set_solid at bed scale: set_solid samples the SDF tens of times per "
1170 "leaf (operator build, overlay classification at virtual positions, the openness "
1171 "probe), so a Python callback dominates everything else — measured >1h43m of pure "
1172 "numpy on an 11.35M-leaf 180-sphere bed before the GPU ran a single kernel.")
1173 .def(
"set_body_force", &Flow::set_body_force, nb::arg(
"fx"), nb::arg(
"fy"), nb::arg(
"fz"),
1174 "Set the body force per unit volume [force/length^3] driving the flow, e.g. a mean "
1175 "pressure gradient -dp/dx along x. It is the only driving the periodic box has. "
1176 "Default (0, 0, 0).")
1177 .def(
"set_advection", &Flow::set_advection, nb::arg(
"on"),
1178 "Enable explicit momentum advection (Navier-Stokes); off = Stokes.")
1179 .def(
"set_ghost_projection", &Flow::set_ghost_projection, nb::arg(
"on"),
1180 nb::arg(
"matrix_order") = 2, nb::arg(
"rhs_order") = 2,
1181 "DEFAULT since 2026-08-25 (AUTO: ghost, with an aperture fallback + stderr notice when "
1182 "the finest band is too thin): the fluid-only constraint scheme — family-free, "
1183 "unconditionally stable, protocol-independent (flow's attractor-campaign verdicts; == "
1184 "flow's set_collocated_scheme('ghost')). FULL directional ghost-cell projection (the "
1185 "AMR port): binary-openness pressure operator + wall-anchored closure overlay on the "
1186 "finest-band rows, MG-preconditioned BiCGStab, ghost-closed divergence constraint; "
1187 "implies set_ghost_gradient. (matrix_order, rhs_order) closure orders: (2, 2) default "
1188 "and the only pair cleared for production — the (1, 2) mixed form is march-UNSTABLE "
1189 "above ~2000 spheres (flow hardening Phase A), kept callable for parity records only. "
1190 "Raises if the finest band is too thin (a closure would cross a 2:1 boundary). Call "
1191 "before set_solid.")
1192 .def(
"set_ghost_sampled", &Flow::set_ghost_sampled, nb::arg(
"on"), nb::arg(
"rho") = 2.2,
1193 nb::arg(
"max_samples") = 0
L,
1194 "MIXED-LEVEL CUT BAND (docs/amr_mixed_level_cut_band_plan.md): allow cut cells at "
1195 "MULTIPLE octree levels — the finest-band contract is dropped. Chain entries that "
1196 "cross a 2:1 boundary become degree-2 LS virtual samples at the uniform closure "
1197 "positions (identity weights at same level, so a uniform finest band is BIT-IDENTICAL "
1198 "to set_ghost_sampled(False)); face classification uses the level-aware canonical "
1199 "openness; the momentum xi-row seam correction and the wall-aware C/F tangential "
1200 "fallback ride along. Implies the ghost projection (engages when the resolved scheme "
1201 "is ghost — the AUTO default or an explicit set_ghost_projection(True)). Distributed "
1202 "since 2026-08-30 (the clouds are a deterministic probe set through the leaf halo). "
1203 "`rho` is the least-squares cloud radius factor (rho = factor * max(h, H); 2.2 = the "
1204 "shipped behaviour) and `max_samples` the nearest-N candidate cap (0 = uncapped) — the "
1205 "M2a cloud-economy knobs, inert at their defaults; do not change them in production "
1206 "without the M2a table. Call before set_solid.")
1207 .def(
"set_pressure", &Flow::set_pressure, nb::arg(
"values"),
1208 "Write the accumulated rotational pressure from a (num_leaves,) array — restart, or "
1209 "re-accumulation policies after finish_adapt (at steady-state dt the transferred p is "
1210 "the load-bearing state; zeroing it after a coarsening adapt lets it re-accumulate "
1212 .def(
"set_dt", &Flow::set_dt, nb::arg(
"dt"),
1213 "Change the time step. dt is BAKED INTO the momentum operator at build time "
1214 "(idiag = rho/dt), so a set_dt must be followed by set_solid or the operator stays "
1215 "stale. set_solid reallocates and zeroes u and p, so a dt SWITCH mid-march is: read "
1216 "velocity()/pressure() -> set_dt -> set_solid -> set_velocity()/set_pressure(). That "
1217 "sequence is the dt-cycling protocol of the attractor-family batteries.")
1218 .def(
"begin_adapt", &Flow::begin_adapt,
1219 "Snapshot the octree topology + (u, p) ahead of an external mesh mutation (adapt / "
1220 "refine_to_sphere / refine_to_sdf / balance on the SAME Octree object). Pair with "
1222 .def(
"finish_adapt", &Flow::finish_adapt, nb::arg(
"sdf"),
1223 "Rebuild the solver on the mutated octree and conservatively transfer the snapshotted "
1224 "u and p onto it (minmod-limited linear remap). `sdf` is re-sampled on the new leaves "
1225 "(pass the same geometry callable as set_solid); keep the cut band at the finest "
1226 "level on the new mesh (re-run refine_to_sdf on the geometry band after a "
1227 "solution-driven adapt). The advecting face field restarts from the cell average for "
1229 .def(
"set_cf_scheme", &Flow::set_cf_scheme, nb::arg(
"scheme"),
1230 "Coarse/fine (2:1) interface scheme. 1 = Martin-Cartwright tangential quadratic, THE "
1231 "DEFAULT since 2026-09-21: second order, applied to the momentum diffusion, the "
1232 "divergence constraint and the pressure gradients (the pressure matrix/MG stays "
1233 "standard, which does not move the steady solution). 0 = the standard two-point flux, "
1234 "first-order at level boundaries, kept only to reproduce pre-2026-09-21 graded results. "
1235 "The default is INERT BY GEOMETRY on a uniform or finest-band mesh -- no C/F faces, no "
1236 "delta -- and matters where a 2:1 interface sees tangential variation, which on a "
1237 "self-similar graded ladder is the difference between order 1.60 and 0.41. Works with "
1238 "both the aperture and the ghost projection. The seam reconstruction of the advected "
1240 "(diagnostics.set_seam_reconstruction) rides on the quadratic scheme: with 0 no seam "
1241 "tables are built. Call before set_solid.")
1242 .def(
"set_advection_scheme", &Flow::set_advection_scheme, nb::arg(
"scheme"),
1243 "High-order advection flux: 0 = second-order upwind (default), 1 = Koren TVD.")
1245 "set_implicit_advection", &Flow::set_implicit_advection, nb::arg(
"on"),
1246 "Implicit first-order-upwind deferred-correction advection (default on): unconditionally "
1247 "stable. Off = fully explicit high-order advection.")
1248 .def(
"set_outer_iterations", &Flow::set_outer_iterations, nb::arg(
"n"), nb::arg(
"tol") = 1
e-6,
1249 "Picard outer iterations over the lagged advection per step (default 1).")
1250 .def(
"set_pressure_tolerance", &Flow::set_pressure_tolerance, nb::arg(
"rtol"),
1251 "Relative tolerance of the PRESSURE solve, for both drivers -- the MG-PCG default and "
1252 "the ghost projection's BiCGStab (flow shares its set_pressure_pcg tolerance with its "
1253 "ghost BiCGStab the same way). Default 1e-10, the value that was hard-coded until "
1254 "2026-09-21, so leaving it alone reproduces every earlier result. The iteration CAP is "
1255 "step()'s `pres_iters`; this is the accuracy it works to, and loosening it is the "
1256 "cheapest cost knob in the step.")
1257 .def(
"set_pressure_pcg", &Flow::set_pressure_pcg, nb::arg(
"on"),
1258 "The pressure-solve DRIVER of the aperture projection. on=True (THE DEFAULT) is "
1259 "MG-preconditioned conjugate gradients: one V-cycle of the pressure multigrid "
1260 "preconditions CG, the fluid-mean component of the residual is removed every iteration "
1261 "(an aperture cut-cell right-hand side is incompatible by exactly that component), and "
1262 "the solve stops at set_pressure_tolerance or at step()'s / project()'s `pres_iters`. "
1263 "on=False is the bounded stationary V-cycle: exactly `pres_iters` V-cycles, with no "
1264 "tolerance test and no mean removal, so on cut-cell geometry its residual STALLS at "
1265 "the incompatible component instead of converging. Both solve the same pressure "
1266 "equation, so where both converge they give the same projected velocity to the solve "
1267 "tolerance; the choice moves cost and robustness, not the answer. Leave it on: "
1268 "on=False exists to A/B the driver on a compatible problem. It has NO effect under the "
1269 "ghost projection -- the default scheme -- whose nonsymmetric operator is always "
1270 "solved by MG-preconditioned BiCGStab; it acts with set_ghost_projection(False) or "
1271 "where the default fell back to the aperture projection. flow's "
1272 "`Solver.set_pressure_pcg` is the same selector, but flow has no stationary driver "
1273 "(on=False raises there) and takes the cap and tolerance as its max_iter / rtol "
1274 "arguments. Takes effect at the next step()/project(); on a DISTRIBUTED Flow pass "
1275 "the same value on every rank.")
1276 .def(
"set_momentum_tolerance", &Flow::set_momentum_tolerance, nb::arg(
"rtol"),
1277 "Relative tolerance of the per-component MOMENTUM solve (BiCGStab, MG-preconditioned "
1278 "by default). Default 1e-8. At the large dt used for steady drag the momentum operator "
1279 "degrades toward a bare elliptic Laplacian and the solve gets expensive; this bounds "
1280 "the over-solve. The cap is step()'s `mom_iters`. (flow spells the same concept "
1281 "set_velocity_residual_tolerance -- the momentum/velocity divergence between the two "
1282 "codes is a ../docs/NAMING.md item, not settled here.)")
1283 .def(
"set_pressure_bottom", &Flow::set_pressure_bottom, nb::arg(
"mode"),
1284 "What solves the coarsest level of the pressure multigrid (docs/amr_mg_depth.md "
1285 "§6.6). A V-cycle converges at a mesh-independent rate only if its coarsest level is "
1286 "effectively solved, and 60 damped-Jacobi sweeps solve a level only up to ~4 cells per "
1287 "axis (8e-9 at 4, 8e-3 at 8, 0.6 at 25). 'auto' (THE DEFAULT) engages the agglomerated "
1288 "GraphAMG-PCG bottom iff the coarsest level still has more than "
1289 "`pressure_bottom_extent` cells on some axis -- i.e. where the ladder ran out on an odd "
1290 "or badly factored grid -- and keeps the sweeps otherwise; 'smoother' always sweeps; "
1291 "'agglomerated' always solves exactly. The choice changes the preconditioner, not the "
1292 "converged pressure (to the solve tolerance), and at the default extent it does not "
1293 "change the iteration count either (§11.4(a): identical counts in all nine measured "
1294 "'smoother'/'agglomerated' pairs); leave it at 'auto' unless you are comparing bottom "
1295 "solves. The same three strings and default as flow's `Solver.set_pressure_bottom`. "
1296 "Takes effect at once on the hierarchy already built and is kept for later set_solid "
1297 "calls. On a DISTRIBUTED Flow call it on every rank: the exact bottom lives in the "
1298 "replicated stage's continued ladder, which the call may build. Raises on any other "
1300 .def(
"set_pressure_bottom_extent", &Flow::set_pressure_bottom_extent, nb::arg(
"cells"),
1301 "Where the pressure ladder stops coarsening below the root brick and hands over to the "
1302 "bottom solve, in CELLS PER AXIS of the coarsest level -- a count, not a length: the "
1303 "limit is the bottom smoother's, which solves a level only up to ~4 cells per axis "
1304 "whatever the physical domain (../docs/NAMING.md §1.8; docs/amr_mg_depth.md "
1305 "§6.2/§11.4). The ladder stops once no axis has more than `cells` cells, and "
1306 "set_pressure_bottom('auto') engages the exact bottom where it stopped above it. "
1307 "Default 4, the value at which the 60 bottom sweeps are exact; it is measured rather "
1308 "than preferred (§11.4: 8 moves the iteration count by ~1 on one case in three; "
1309 "tests/study/amr_pressure_depth.py --sweep), so there is rarely a reason to change it. "
1310 "Changes the preconditioner, not the converged pressure (to the solve tolerance). "
1311 "Takes effect at the NEXT set_solid, where the ladder is built. Raises if cells < 1.")
1312 .def_prop_ro(
"pressure_bottom_extent", &Flow::pressure_bottom_extent,
1313 "The bottom extent, in cells per axis (a count, not a length), that "
1314 "set_pressure_bottom_extent last stored (default 4) -- the one the next "
1315 "set_solid builds the ladder with (docs/amr_mg_depth.md §6.8).")
1316 .def(
"step", &Flow::step, nb::arg(
"mom_iters") = 100, nb::arg(
"pres_iters") = 60,
1317 "Advance one collocated projection step of length dt on device. `mom_iters` caps the "
1318 "momentum solve (BiCGStab, MG-preconditioned) and `pres_iters` the pressure solve "
1319 "(MG-PCG, or BiCGStab under the ghost projection); each stops earlier at its "
1320 "set_momentum_tolerance / set_pressure_tolerance. Collective on a distributed Flow.")
1321 .def(
"velocity", &Flow::velocity, nb::arg(
"component"),
1322 "Per-leaf velocity component (0=x,1=y,2=z), (num_leaves,) float64.")
1323 .def(
"velocities", &Flow::velocities,
1324 "All three velocity components, (num_leaves, 3) float64.")
1325 .def(
"is_fluid", &Flow::is_fluid,
1326 "Per-leaf fluid mask (False in the solid), (num_leaves,) bool.")
1327 .def(
"divergence_norm", &Flow::divergence_norm,
1328 "Volume-weighted L2 norm of the residual cell divergence (projection-quality "
1330 .def(
"pressure", &Flow::pressure,
1331 "Per-leaf pressure (incremental-rotational p), (num_leaves,) float64.")
1332 .def(
"face_field", &Flow::face_field,
1333 "ABC divergence-free FACE velocity, one value per CSR (sub)face (conservative flux / "
1334 "streamline post-processing).")
1335 .def(
"set_velocity", &Flow::set_velocity, nb::arg(
"component"), nb::arg(
"values"),
1336 "Write velocity component c (0=x,1=y,2=z) from a (num_leaves,) array — initial "
1337 "conditions, restart, or warm-start. Call before step()/project().")
1338 .def(
"project", &Flow::project, nb::arg(
"pres_iters") = 60,
1339 "Pressure projection only (no momentum solve) — project an externally-set velocity "
1340 "field to divergence-free. Returns nothing; read the result via "
1341 "velocity()/velocities().")
1344 "The developer tier (FlowDiagnostics, a view onto this Flow): iteration counts of the "
1345 "last step, the face-field divergence and topology, the C/F and seam-reconstruction "
1346 "censuses, the pressure-multigrid ladder and its bottom solver, and the "
1347 "solver-internals / ablation switches. Nothing here is needed to set up, run or read "
1348 "out a simulation; everything here has a production default.");
1350 nb::class_<FlowDiagnostics>(
1351 m,
"FlowDiagnostics",
1352 "Developer instruments and ablation switches of a Flow, reached as `flow.diagnostics` "
1353 "(suite/docs/QUALITY_PLAN.md D2: the public Flow surface is what a user needs to set up, "
1354 "run and read out a simulation; this is what a developer uses to inspect or ablate it).")
1355 .def(
"last_mom_iters", &FlowDiagnostics::last_mom_iters,
1356 "Total momentum BiCGStab iterations (summed over the 3 components) of the last step.")
1357 .def(
"last_pres_iters", &FlowDiagnostics::last_pres_iters,
1358 "Pressure PCG iterations of the last step.")
1359 .def(
"last_outer_iters", &FlowDiagnostics::last_outer_iters,
1360 "Picard outer iterations actually run in the last step (1 unless "
1361 "set_outer_iterations(>1)).")
1362 .def(
"divergence_norm_face", &FlowDiagnostics::divergence_norm_face,
1363 "L2 norm of the APERTURE-weighted divergence of the ABC face field. MEANINGFUL ON THE "
1364 "APERTURE PATH ONLY (set_ghost_projection(False)), where it is the pressure-solve "
1365 "residual, far below divergence_norm — including across 2:1 interfaces. Under the "
1366 "GHOST projection (the DEFAULT) the solved constraint is this divergence PLUS an "
1367 "overlay delta that is a functional of the CELL velocities, not of the face field, so "
1368 "this norm omits it and reads O(1) on a perfectly healthy solve (34 at N=32, 184 at "
1369 "N=64 on the Z&H sphere, with the velocities matching peclet.flow to 1e-6). There, "
1370 "divergence_norm() is the residual you want. Unnormalized either way: it grows with "
1371 "resolution and velocity magnitude, so read it as a trend, never as an absolute.")
1372 .def_prop_ro(
"num_cf_cut_faces", &FlowDiagnostics::num_cf_cut_faces,
1373 "How many 2:1 C/F sub-face slots of THIS RANK carry the standard two-point "
1374 "face value because the quadratic one is withheld: both incident cells must be "
1375 "REGULAR fluid (fluid and not cut) for the quadratic C/F scheme "
1376 "(set_cf_scheme(1), the default) to apply there, so this counts the sub-faces "
1377 "where a level boundary meets the wall (docs/amr_cf_flux_gate.md §6.5). 0 "
1378 "on every uniform or finest-band mesh; on a graded mesh it measures the size "
1379 "of the set that runs at the standard scheme's local order. Each sub-face "
1380 "contributes two slots (one per incident cell) and the sum over ranks equals "
1381 "the single-rank count.")
1382 .def(
"face_topology", &FlowDiagnostics::face_topology,
1383 "The face CSR topology face_field() is indexed by, as a dict of arrays: 'start' "
1384 "(num_leaves+1 row offsets, int64), 'nbr' (neighbour leaf per (sub)face, int64), "
1385 "'axis' (0/1/2, int32), 'dir' (+1/-1 from the owning cell toward the neighbour, "
1386 "int32), 'raw_area' (world area the ADVECTIVE flux uses -- the FINE area at a 2:1 "
1387 "sub-face, so a coarse face's four sub-faces sum to the coarse area), 'dist' (world "
1388 "centre distance, 1.5*h_fine at a 2:1 sub-face), 'alpha' (face openness in [0, 1]) and "
1389 "'upup_i' / 'upup_j' (the second upwind probes the SOU/Koren reconstruction samples, "
1390 "-1 where none). A 2:1 sub-face is a slot whose two incident leaves have different "
1391 "Octree.levels(); its centroid is the FINER leaf's face centre. Host-copied on every "
1392 "call -- a diagnostic, not a step-loop read-out. Under MPI a neighbour index >= "
1393 "num_leaves is a ghost slot of this rank's registry, whose world centre (like every "
1394 "slot's) is row `slot` of 'cell_center', an (num_leaves + num_ghost_cells, 3) array. "
1395 "The SEAM RECONSTRUCTION tables of docs/amr_cf_convective.md come with it: 'seam' (one "
1396 "descriptor id per face slot, -1 = a plain slot that takes the ordinary SOU/Koren "
1397 "line), the per-descriptor 'samp_i' / 'samp_j' (the tangential-sample record when i / "
1398 "j is the COARSE cell of a 2:1 sub-face, else -1), 'uu_rec_i' / 'uu_rec_j' (the "
1399 "upstream probe's record when the second upwind cell of i / j crosses a level, else "
1400 "-1) and 'd1_i' / 'd1_j' (world half width along the face axis), and the record CSR "
1401 "'rec_start', 'rec_cell', 'rec_w', 'rec_dist' (the world probe distance, used only "
1402 "where a record is an UPSTREAM probe). All empty with set_cf_scheme(0 = standard), "
1403 "which builds no tables. Rank-local under MPI.")
1404 .def_prop_ro(
"pressure_mg_levels", &FlowDiagnostics::pressure_mg_levels,
1405 "Leaf count of every pressure-multigrid level, finest first "
1406 "(docs/amr_mg_depth.md §6.7), as a list of int; empty before set_solid, which "
1407 "is where the hierarchy is built. Levels below the root brick are LIFTED levels "
1408 "— the same octree with its root halved — so a uniform mesh has a real "
1409 "hierarchy rather than a single level. Under MPI the in-place levels are THIS "
1410 "RANK's counts, the levels of a replicated tail (appended last) are GLOBAL "
1411 "counts, identical on every rank, and the levels of a sibling-merge or "
1412 "repartition stage are this rank's counts on its target block (0 on a rank "
1413 "that owns none). Check it against "
1414 "`peclet.amr.predict_hierarchy` (level count, and leaf for leaf on a "
1415 "uniform mesh at np=1), never against a literal.")
1416 .def_prop_ro(
"pressure_mg_bottom", &FlowDiagnostics::pressure_mg_bottom,
1417 "What solves the coarsest pressure level of the hierarchy AS BUILT: 'jacobi' "
1418 "(60 damped-Jacobi sweeps, effectively exact at <= 4 cells per axis: the "
1419 "slowest mode falls by 8e-9) or 'amg' (the agglomerated GraphAMG-PCG solve), "
1420 "with the suffix '+tail' when the coarsest level was gathered onto every rank "
1421 "by the replicated stage, '+sibling' / '+repartition' when a level moved onto "
1422 "fewer ranks (one suffix per stage, innermost first; docs/amr_mg_depth.md "
1423 "§6.5-§6.7, WO4b). Which one runs "
1424 "follows `Flow.set_pressure_bottom` (default 'auto': the exact bottom engages "
1425 "only where the ladder ran out above `Flow.pressure_bottom_extent` cells per "
1426 "axis). 'jacobi' before set_solid.")
1427 .def_prop_ro(
"num_seam_sample_records", &FlowDiagnostics::num_seam_sample_records,
1428 "How many tangential-sample records the seam reconstruction built on THIS RANK "
1429 "-- one per 2:1 sub-face pair that passes the C/F face gate (both cells regular "
1430 "fluid). 0 on a uniform mesh and with the standard C/F scheme.")
1431 .def_prop_ro(
"num_seam_layer_records", &FlowDiagnostics::num_seam_layer_records,
1432 "How many face-layer records the seam reconstruction built on THIS RANK -- one "
1433 "per (coarse cell, face) whose far side is refined and whose four fine cells "
1434 "are fluid (the case-3 upstream probe).")
1435 .def(
"set_momentum_mg", &FlowDiagnostics::set_momentum_mg, nb::arg(
"on"),
1436 "Use the Galerkin velocity multigrid as the momentum solve preconditioner (default on; "
1437 "makes the momentum solve scale with resolution). Call before set_solid.")
1438 .def(
"set_momentum_gs", &FlowDiagnostics::set_momentum_gs, nb::arg(
"on"),
1439 "Use the symmetric multicolour Gauss-Seidel smoother in the momentum multigrid (default "
1440 "off = weighted Jacobi). Call before set_solid.")
1441 .def(
"set_velocity_mg_staircase", &FlowDiagnostics::set_velocity_mg_staircase, nb::arg(
"on"),
1442 "Use the rediscretised staircase velocity-MG instead of Galerkin (default off).")
1443 .def(
"set_momentum_mg_solver", &FlowDiagnostics::set_momentum_mg_solver, nb::arg(
"on"),
1444 "Solve the momentum predictor with the velocity-MG as the solver (no Krylov), mirroring "
1445 "flow's velocity solve (default off = BiCgStab with the MG as preconditioner).")
1446 .def(
"set_ghost_gradient", &FlowDiagnostics::set_ghost_gradient, nb::arg(
"on"),
1447 "Directional ghost cell-gradient on cut cells for the pressure predictor and the "
1448 "projection's cell correction (2nd-order one-sided, never reads decoupled solid "
1449 "pressure — removes the gauge-dependent O(1/h) cut-cell gradient error of the plain "
1450 "ABC gradient). The ghost projection (the default) implies it; this switch matters for "
1451 "the aperture fallback only. Call before set_solid.")
1452 .def(
"set_aperture_order", &FlowDiagnostics::set_aperture_order, nb::arg(
"order"),
1453 "Aperture estimator for the (fallback) aperture projection: 2 = analytic "
1454 "marching-squares (DEFAULT since 2026-08-26), 1 = legacy one-sample model. "
1455 "Call before set_solid.")
1456 .def(
"set_uf_advection", &FlowDiagnostics::set_uf_advection, nb::arg(
"on"),
1457 "ABLATION. The advecting velocity of the momentum advection: the projected, "
1458 "divergence-free face field uf (ON, the shipped Almgren-Bell-Colella scheme) or, with "
1459 "on=False, the un-projected 1/2(u_i+u_j) cell->face average that the first step uses "
1460 "before any projection has run. It is the ONE discretization difference between this "
1461 "solver and peclet.flow's SolverColocated on a uniform grid; turning it off makes the "
1462 "two agree to solver tolerance (docs/amr_flow_uniform_parity.md).")
1463 .def(
"set_seam_reconstruction", &FlowDiagnostics::set_seam_reconstruction, nb::arg(
"on"),
1464 "ABLATION. Reconstruct the ADVECTED value with level-aware probes at every face whose "
1465 "upwind-side stencil crosses a 2:1 coarse/fine seam (default ON, "
1466 "docs/amr_cf_convective.md): the coarse upwind cell is tangentially sampled at the "
1467 "sub-face's own column, and an upstream probe at another octree level is taken at its "
1468 "TRUE distance (a coarser one as the same tangential sample, a finer one as the mean of "
1469 "the four face-layer children). Without it the sub-face value carries the coarse "
1470 "column's tangential offset -- an O(h) face error, so an O(1) local truncation on the "
1471 "fine side of the seam. on=False restores that, bit for bit, and may be flipped between "
1472 "steps; the tables are built in set_solid either way. Inert with advection off, with "
1473 "set_cf_scheme(0 = standard), and on any mesh with no 2:1 face. Leave it on in "
1474 "production: the switch exists for the A/B of docs/amr_cf_convective.md §12, where it "
1475 "cuts the one-step seam truncation 7.9e-3 -> 2.0e-3 at no measurable cost.");
1478 m,
"DistributedOctree",
1479 "MPI octree: an ORB block decomposition of a global root grid (one BlockOctree per rank, "
1480 "over MPI_COMM_WORLD). Construct it collectively; "
1481 "refine/balance/rebalance/face_neighbor_gather are collective. Per-leaf arrays describe "
1482 "THIS rank's local block in global world coordinates.");
1487 std::array<double, 3> origin, std::optional<SpacingArg> spacing,
1488 std::array<bool, 3> periodic, std::optional<std::array<double, 3>> extent) {
1489 const auto root = rootCellsOf(cells, lmax,
"DistributedOctree");
1491 root, lmax, origin, resolveSpacing(spacing, extent, cells,
"DistributedOctree"),
1494 nb::arg(
"cells"), nb::kw_only(), nb::arg(
"lmax") = 0
u,
1495 nb::arg(
"origin") = std::array<double, 3>{0, 0, 0}, nb::arg(
"spacing") = nb::none(),
1496 nb::arg(
"periodic") = std::array<bool, 3>{
true,
true,
true},
1497 nb::arg(
"extent") = nb::none(),
1498 "Decompose a global grid of `cells` FINEST cells per axis (root cells = cells/2**lmax, "
1499 "each `lmax` levels deep) across the ranks of MPI_COMM_WORLD via ORB. `origin` places "
1500 "the global grid; `periodic` per axis. Every argument after `cells` is keyword-only.\n\n"
1501 "State the domain PHYSICALLY with `extent` = the global box side lengths; the cell size "
1502 "is then extent/cells PER AXIS. Alternatively give `spacing` (one number or (dx, dy, "
1503 "dz)); default 1. Passing both raises. Read the three numbers back from `.spacing`.")
1506 .def_prop_ro(
"block_origin_root", &DistributedOctree::block_origin_root,
1507 "This rank's block lower corner, in global root-cell coordinates.")
1508 .def_prop_ro(
"block_brick", &DistributedOctree::block_brick,
1509 "This rank's block size in root cells per axis.")
1510 .def_prop_ro(
"global_root_size", &DistributedOctree::global_root_size,
1511 "Global grid size in root cells per axis.")
1513 "Re-decompose by leaf count (weighted ORB) and migrate leaves + their fields. `fields` "
1514 "is (num_leaves, K) float64; returns this rank's (M, K) columns after migration. Pure "
1515 "redistribution; the partition is updated in place (collective).")
1516 .
def(
"face_neighbor_gather", &DistributedOctree::face_neighbor_gather, nb::arg(
"field"),
1517 nb::arg(
"sentinel") = 0.0,
1518 "For each local leaf, the field value across each of its 6 faces, gathered over the "
1519 "owner-based halo. `field` is (num_leaves,); returns (num_leaves, 6) laid out "
1520 "[+x,-x,+y,-y,+z,-z]; domain boundaries carry `sentinel` (collective).");
void setImplicitAdvection(bool on)
Implicit-FOU deferred correction (default ON).
Index numSeamSampleRecords() const
Seam-reconstruction census (docs/amr_cf_convective.md §5.2): how many sample records (one per 2:1 sub...
void setApertureOrder(int order)
FULL directional ghost-cell projection (the AMR port of flow's collocated set_ghost_projection): the ...
void setMomentumMG(bool on)
Use the Galerkin velocity multigrid (MomentumMG) as the momentum BiCGStab preconditioner.
std::vector< double > velocity(int c) const
Copy a velocity component back to host (single D2H, no host loop — S2a).
void finishAdapt(SdfFn &&sdfFn)
Rebuild on the mutated octree and transfer the snapshotted fields onto it.
void setGhostGradient(bool on)
Directional ghost cell-gradient for the −∇pⁿ predictor and the projection's cell correction (the AMR ...
void step(int momIters=100, int presIters=60)
void initMpi(DistributedOctree< 3, Bits > &d)
Distributed mode (docs/amr_distributed_flow.md, rung 4): run this solver on one ORB block of a Distri...
void setBodyForce(double fx, double fy, double fz)
void setPressurePCG(bool on)
Use MG-preconditioned CG for the pressure solve (default) vs plain V-cycles.
std::vector< double > velocities() const
All three velocity components interleaved as a flat (n,3) row-major host buffer (out[i*3+c]) with a s...
std::vector< double > pressure() const
Copy the pressure field back to host (single D2H), (num_leaves,) — the incremental-rotational p.
void setPressureTol(double tol)
Relative tolerance of the PRESSURE solve, for both drivers: the MG-PCG (the default) and the ghost pr...
void setVelocity(int c, const std::vector< double > &h)
Set a velocity component from host (testing / initial conditions).
bool isFluid(Index i) const
Per-leaf fluid mask (false inside the solid) — for host-side post-processing / bindings.
double divNormL2()
L2 norm of the (openness-weighted) divergence of the current velocity — the ghost-closed divergence w...
void setAdvectionScheme(int s)
High-order advection scheme: 0 = second-order upwind (SOU, default), 1 = Koren TVD.
void setViscosity(double mu)
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
Index pressureBottomExtent() const
The bottom extent the next setSolid builds the ladder with (default 4).
Index numSeamLayerRecords() const
The face-layer half of the census: one record per (regular coarse cell, face) whose far side is refin...
int lastOuterIters() const
Picard outer iterations actually run in the last step (1 unless setOuterIterations(>1)).
void setDensity(double rho)
Index numCfCutFaces() const
The C/F census (docs/amr_cf_flux_gate.md §6.5): owned forEachFaceFull slots on a 2:1 sub-face between...
double divNormFace()
L2 norm of the APERTURE-weighted divergence of the ABC face field uf_ (built each project()).
void setPressureBottom(const std::string &kind)
What solves the coarsest pressure level (docs/amr_mg_depth.md §6.6/§11.6): "auto" (the default — the ...
std::string pressureMgBottom() const
What actually solves the coarsest pressure level (docs/amr_mg_depth.md §6.6/§6.7): "jacobi" (60 dampe...
void project(int presIters=60)
Pressure projection of the current velocity in place.
std::vector< double > faceField() const
Copy the divergence-free face field to host (one value per CSR (sub)face, forEachFaceFull order).
void setMomentumTol(double tol)
Relative tolerance for the per-step momentum BiCGStab solve (default 1e-8).
void setSeamReconstruction(bool on)
ABLATION (docs/amr_cf_convective.md §5.5, Q-C).
void setPressureBottomExtent(Index e)
Where the pressure ladder stops lifting the root and hands over to the bottom (docs/amr_mg_depth....
void setUfAdvection(bool on)
ABLATION (developer tier).
void setPressure(const std::vector< double > &h)
Write the accumulated rotational pressure from host (restart / finishAdapt).
void setVelocityMGStaircase(bool on)
Choose the momentum-MG coarse-operator strategy: false (default) = Galerkin (MomentumMG,...
int lastPresIters() const
Pressure PCG iterations of the last step.
void rebalanceMpi(SdfFn &&sdfFn)
Distributed load rebalance (docs/amr_distributed_flow.md, rung 6): re-decompose the octree by leaf co...
std::vector< Index > pressureMgLevels() const
The pressure multigrid's ladder as BUILT on this rank: per-level leaf counts, level 0 first (docs/amr...
int lastMomIters() const
Total momentum BiCGStab iterations (summed over the 3 components) of the last step.
void beginAdapt()
Snapshot the octree topology + (u, p) ahead of an external mesh mutation.
void setGhostSampled(bool on, double rho=2.2, long maxSamples=0)
SAMPLED ghost projection: the mixed-level cut-band mode (docs/amr_mixed_level_cut_band_plan....
void setMomentumGS(bool on)
Opt-in: use the multicolour Gauss–Seidel smoother in the momentum MG (Galerkin or staircase) instead ...
void setMomentumMGSolver(bool on)
Opt-in (P4): solve the momentum predictor with the velocity multigrid used as the solver — MG-precond...
void setAdvection(bool on)
Enable momentum advection ∇·(u u) (default OFF ⇒ Stokes).
void setCfScheme(int scheme)
Coarse/fine (2:1) interface scheme (cf_scheme.hpp): 1 = Martin–Cartwright tangential quadratic,...
void setSolid(SdfFn &&sdfFn)
Build the cut-cell operators (host) + upload all device structures.
void setOuterIterations(int n, double tol=1e-6)
Optional Picard outer loop over the lagged advection (mirror of flow's outerIters_): each outer itera...
FaceTopologyHost faceTopology() const
void init(const Octree &t, Real h0, Vec< 3 > origin=Vec< 3 >{})
Cubic-cell overload — the pre-Phase-3 spelling (docs/amr_anisotropic.md §2).
Geometric multigrid for AmrPoisson over a uniformly-coarsened octree hierarchy.
void setPeriodic(bool p)
Apply a boundary condition to every level (periodic default, or non-periodic homogeneous Dirichlet).
void vcycle(std::size_t L, std::vector< double > &u, const std::vector< double > &rhs, int pre=2, int post=2)
One V-cycle on level L solving L u = rhs (correction scheme).
const AmrPoisson< Dim, Bits > & op(std::size_t L=0) const
std::size_t numLevels() const
void build(const Octree &finest, Real h0, bool liftRoot=true, Index bottomExtent=4)
Build the hierarchy from a finest octree by uniform coarsening until a single leaf remains (or no ful...
Index balance2to1()
Enforce 2:1 (graded) balance within this block: no two face-adjacent leaves differ by more than one l...
bool isBalanced() const
True iff the leaves form a valid 2:1-balanced partition: every face neighbour differs by at most one ...
bool refineLeaf(Index i)
Refine a single leaf by index. Returns true if it was split.
const IVec< Dim > & brick() const
Index find(Code p) const
Leaf containing Morton code p, or -1. Host wrapper over amrLocate.
void init(IVec< Dim > brick, unsigned lmax, IVec< Dim > globalOrigin=IVec< Dim >{})
typename M::coord_type Coord
typename M::code_type Code
std::vector< double > faceNeighborGather(const std::vector< double > &field, double sentinel=kNoNeighbor) const
For each local leaf and each of the 2*Dim faces, the neighbouring leaf's field value.
const IVec< Dim > & globalRootSize() const
Index balance()
Bring the whole distributed octree to a 2:1-balanced state.
Index rebalance(std::vector< std::vector< double > > &fields)
Re-decompose by per-root-cell octree-leaf count and migrate leaves (with their field columns) to the ...
const Vec< Dim > & h0() const
Phase 3: the FINEST spacing per axis (docs/amr_anisotropic.md §2).
const AmrGeometry< Dim > & globalGeometry() const
AmrGeometry< Dim > localGeometry() const
World geometry of this rank's block (the global geometry shifted to its origin).
void init(IVec< Dim > globalRootSize, unsigned lmax, AmrGeometry< Dim > globalGeo, std::array< bool, Dim > periodic, MPI_Comm comm=MPI_COMM_WORLD)
Decompose a globalRootSize grid of root cells over the communicator and build this rank's local block...
const IVec< Dim > & globalFineSize() const
const IVec< Dim > & blockBrick() const
const IVec< Dim > & blockOriginRoot() const
std::array< long, 3 > global_root_size() const
std::array< long, 3 > block_origin_root() const
std::array< long, 3 > block_brick() const
DO & ref()
The wrapped core octree (the distributed Flow constructor hooks into it).
void write_vtu(const std::string &path, const std::string &name, DArray field)
nb::ndarray< nb::numpy, std::int32_t > levels() const
std::array< double, 3 > spacing() const
nb::ndarray< nb::numpy, double > rebalance(DArray fields)
nb::ndarray< nb::numpy, double > lohner_indicator(DArray field, double eps)
DistributedOctree(std::array< long, 3 > global_root, unsigned lmax, std::array< double, 3 > origin, std::array< double, 3 > spacing, std::array< bool, 3 > periodic)
Index refine_to_sphere(std::array< double, 3 > center, double radius, unsigned target_level, double band, bool balance)
std::array< long, 3 > cells() const
The GLOBAL finest grid: global root cells * 2**lmax per axis (what the constructor took).
std::array< double, 3 > origin() const
The global lower corner.
Index refine_to_sdf(std::function< double(double, double, double)> sdf, unsigned target_level, double band, bool balance)
nb::ndarray< nb::numpy, double > centers() const
nb::ndarray< nb::numpy, double > face_neighbor_gather(DArray field, double sentinel)
nb::ndarray< nb::numpy, double > sizes(int axis) const
nb::ndarray< nb::numpy, double > adapt(DArray field, double refine_thresh, double coarsen_thresh, unsigned finest_level, double eps, bool linear)
nb::ndarray< nb::numpy, std::uint64_t > codes() const
std::array< double, 3 > extent() const
The global box side lengths, cells * spacing.
void release() noexcept override
void set_momentum_mg(bool on)
void set_seam_reconstruction(bool on)
int last_outer_iters() const
int last_pres_iters() const
void set_ghost_gradient(bool on)
void set_momentum_gs(bool on)
void set_uf_advection(bool on)
peclet::core::Index num_seam_sample_records() const
double divergence_norm_face()
void set_velocity_mg_staircase(bool on)
void set_aperture_order(int order)
std::string pressure_mg_bottom() const
std::vector< peclet::core::Index > pressure_mg_levels() const
nb::dict face_topology() const
peclet::core::Index num_cf_cut_faces() const
peclet::core::Index num_seam_layer_records() const
int last_mom_iters() const
void set_momentum_mg_solver(bool on)
void set_pressure_tolerance(double rtol)
void set_solid(std::function< double(double, double, double)> sdf)
void set_implicit_advection(bool on)
void set_pressure_bottom(const std::string &mode)
void set_ghost_sampled(bool on, double rho, long max_samples)
void set_advection(bool on)
nb::ndarray< nb::numpy, double > pressure() const
void set_solid_spheres(nb::ndarray< const double, nb::c_contig > centers, nb::ndarray< const double, nb::c_contig > radii, bool periodic)
nb::ndarray< nb::numpy, double > face_field() const
void set_body_force(double fx, double fy, double fz)
nb::ndarray< nb::numpy, bool > is_fluid() const
void set_velocity(int c, nb::ndarray< double, nb::c_contig > h)
void set_momentum_tolerance(double rtol)
void project(int pres_iters)
long pressure_bottom_extent() const
nb::ndarray< nb::numpy, double > velocity(int c) const
nb::ndarray< nb::numpy, double > velocities() const
void set_pressure_pcg(bool on)
void set_outer_iterations(int n, double tol)
void rebalance_mpi(std::function< double(double, double, double)> sdf)
void set_advection_scheme(int s)
void release() noexcept override
void step(int mom_iters, int pres_iters)
void set_pressure_bottom_extent(long cells)
Flow(const Octree &oct, double rho, double mu, double dt)
void set_ghost_projection(bool on, int matrix_order, int rhs_order)
void set_cf_scheme(int scheme)
amr::AmrFlow & engine()
The developer tier (QUALITY_PLAN D2): FlowDiagnostics reaches the solver through this.
void finish_adapt(std::function< double(double, double, double)> sdf)
void set_pressure(nb::ndarray< double, nb::c_contig > h)
std::array< double, 3 > spacing() const
Index refine_to_sphere(std::array< double, 3 > center, double radius, unsigned target_level, double band, bool balance)
Index refine_to_gap_floor(std::function< double(double, double, double)> sdf, std::function< double(double, double, double)> gap, unsigned coarsest_level, double n, double band, bool balance)
nb::ndarray< nb::numpy, std::uint64_t > codes() const
nb::ndarray< nb::numpy, std::int32_t > levels() const
Index find(std::array< double, 3 > x) const
nb::ndarray< nb::numpy, double > centers() const
const BO & octreeRef() const
Index refine_to_sdf_graded(std::function< double(double, double, double)> sdf, std::function< unsigned(double, double, double)> target_level, double band, bool balance)
nb::ndarray< nb::numpy, double > lohner_indicator(DArray field, double eps)
bool refine_leaf(Index i)
void release() noexcept override
void write_vtu(const std::string &path, const std::string &name, DArray field)
std::array< long, 3 > cells() const
The FINEST-level cell counts the block resolves: root cells * 2**lmax per axis.
std::array< double, 3 > origin() const
Octree(std::array< long, 3 > root, unsigned lmax, std::array< double, 3 > origin, std::array< double, 3 > spacing)
Index refine_to_sdf(std::function< double(double, double, double)> sdf, unsigned target_level, double band, bool balance)
std::array< double, 3 > extent() const
The block's physical side lengths, cells * spacing.
nb::ndarray< nb::numpy, double > sizes(int axis) const
const amr::AmrGeometry< 3 > & geoRef() const
nb::ndarray< nb::numpy, double > adapt(DArray field, double refine_thresh, double coarsen_thresh, unsigned finest_level, double eps, bool linear)
nb::tuple solve(DArray rhs, std::optional< DArray > x0, int cycles, int pre, int post, double tol)
void release() noexcept override
std::size_t num_levels() const
Poisson(const Octree &oct, bool periodic)
double residual(DArray u, DArray rhs)
nb::ndarray< nb::numpy, double > apply(DArray u)
nb::ndarray< nb::numpy, double > leafCenters(const BO &t, const amr::AmrGeometry< 3 > &geo)
nb::ndarray< nb::numpy, std::uint64_t > leafCodes(const BO &t)
std::array< double, 3 > spacingOf(const SpacingArg &sp)
nb::ndarray< nb::numpy, double > vecToArray(std::vector< double > v)
double spacingFromExtent(const std::array< double, 3 > &extent, const std::array< long, 3 > &cells)
std::array< double, 3 > resolveSpacing(const std::optional< SpacingArg > &spacing, const std::optional< std::array< double, 3 > > &extent, const std::array< long, 3 > &cells, const char *who)
std::array< double, 3 > spacingsFromExtent(const std::array< double, 3 > &extent, const std::array< long, 3 > &cells)
std::array< long, 3 > rootCellsOf(const std::array< long, 3 > &cells, unsigned lmax, const char *who)
void bindOctreeCommon(nb::class_< T > &c)
nb::ndarray< double, nb::c_contig > DArray
nb::ndarray< nb::numpy, T > vec2(std::vector< T > v, std::size_t cols)
nb::ndarray< nb::numpy, double > leafSizes(const BO &t, const amr::AmrGeometry< 3 > &geo, int axis)
std::variant< double, std::array< double, 3 > > SpacingArg
nb::ndarray< nb::numpy, std::int32_t > leafLevels(const BO &t)
nb::ndarray< nb::numpy, T > vec1(std::vector< T > v)
std::vector< double > asField(const BO &t, const DArray &field, const char *who)
const char * toString(MgLevelKind k)
Index refineToSdf(BlockOctree< Dim, Bits > &t, const AmrGeometry< Dim > &geo, SdfFn &&sdf, unsigned targetLevel, Real band=1.0, bool balance=true)
Refine leaves near an SDF interface down to targetLevel.
std::vector< double > lohnerIndicator(const BlockOctree< Dim, Bits > &t, const std::vector< double > &u, double eps=0.01)
Löhner normalized second-difference indicator E_i ∈ [0,1], per leaf, for scalar u (indexed by leaf sl...
std::vector< double > distributedAdapt(DistributedOctree< Dim, Bits > &d, const std::vector< double > &f, double refineThresh, double coarsenThresh, unsigned finestLevel=0, double eps=0.01, bool linear=true)
One distributed solution-adaptive step.
@ Octree
a coarsening of the octree itself (at or above the root brick)
Index refineToSdfGraded(BlockOctree< Dim, Bits > &t, const AmrGeometry< Dim > &geo, SdfFn &&sdf, TargetFn &&targetFn, Real band=2.0, bool balance=true)
Graded variant of refineToSdf: a per-point TARGET LEVEL function instead of one global targetLevel — ...
AdaptResult< Dim, Bits > adapt(const BlockOctree< Dim, Bits > &t, const std::vector< double > &f, double refineThresh, double coarsenThresh, unsigned finestLevel=0, double eps=0.01, bool linear=true)
All-in-one solution-adaptive step: Löhner indicator → flags → adaptField.
void writeVtu(const std::string &path, const BlockOctree< Dim, Bits > &t, const AmrGeometry< Dim > &geo, const std::string &name, const std::vector< double > &cellData)
Write a BlockOctree (+ world geometry + a per-leaf scalar) as a VTK UnstructuredGrid (....
std::vector< std::array< double, Dim > > transferFieldGradients(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF)
Per-old-leaf minmod prolongation gradients (per fine-coordinate unit) — transferField's stencil,...
std::vector< double > lohnerIndicatorDistributed(const DistributedOctree< Dim, Bits > &d, const std::vector< double > &u, double eps=0.01)
Löhner indicator per local leaf, using the owner-based face-neighbour halo so cross-block neighbours ...
World-space placement of a block-local octree: fine coordinate (0,..,0) sits at origin,...