DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
MeshConnectivity_Ghost.cpp
Go to the documentation of this file.
1/// @file MeshConnectivity_Ghost.cpp
2/// @brief Ghost chain compilation and BFS evaluation.
3
5
6#include <algorithm>
7#include <queue>
8#include <sstream>
9#include <stdexcept>
10#include <fmt/core.h>
11
12namespace DNDS::Geom
13{
14 // =================================================================
15 // GhostSpec::defaultPrimary
16 // =================================================================
17
19 {
20 using namespace Adj;
21 DNDS_assert(nLayers >= 1);
22
23 // Cell chain: nLayers hops of Cell2Cell
24 std::vector<AdjKind> cellHops(nLayers, Cell2Cell);
25
26 // Node chain: nLayers hops of Cell2Cell + Cell2Node
27 std::vector<AdjKind> nodeHops(nLayers, Cell2Cell);
28 nodeHops.push_back(Cell2Node);
29
30 return GhostSpec{{
33 {EntityKind::Bnd, {Bnd2Node, Node2Bnd}, EntityKind::Bnd},
34 {EntityKind::Bnd, {Bnd2Node, Node2Bnd, Bnd2Node}, EntityKind::Node},
35 }};
36 }
37
38 // =================================================================
39 // CompiledGhostTree::compile
40 // =================================================================
41
42 /// Insert a chain into a trie rooted at `roots`. Merges common prefixes.
43 static void insertChainIntoTrie(
44 std::vector<GhostTreeNode> &roots,
45 const GhostChain &chain)
46 {
47 // Validate chain consistency.
48 if (chain.hops.empty())
49 throw std::runtime_error("GhostChain: empty hop list");
50 if (chain.anchor != chain.hops.front().from)
51 throw std::runtime_error(fmt::format(
52 "GhostChain: anchor {} != first hop from {}",
54 entityKindName(chain.hops.front().from)));
55 if (chain.target != chain.hops.back().to)
56 throw std::runtime_error(fmt::format(
57 "GhostChain: target {} != last hop to {}",
59 entityKindName(chain.hops.back().to)));
60 for (size_t i = 0; i + 1 < chain.hops.size(); i++)
61 {
62 if (chain.hops[i].to != chain.hops[i + 1].from)
63 throw std::runtime_error(fmt::format(
64 "GhostChain: hop[{}].to ({}) != hop[{}].from ({})",
65 i, entityKindName(chain.hops[i].to),
66 i + 1, entityKindName(chain.hops[i + 1].from)));
67 }
68
69 // Find or create the root node for this chain's anchor.
70 GhostTreeNode *current = nullptr;
71 for (auto &root : roots)
72 {
73 if (root.kind == chain.anchor)
74 {
75 current = &root;
76 break;
77 }
78 }
79 if (!current)
80 {
81 roots.push_back(GhostTreeNode{chain.anchor, {}, false, 0, {}});
82 current = &roots.back();
83 }
84
85 // Walk down the trie, creating nodes as needed.
86 for (size_t hi = 0; hi < chain.hops.size(); hi++)
87 {
88 const AdjKind &hop = chain.hops[hi];
89 int childLevel = current->level + 1;
90 EntityKind childKind = hop.to;
91
92 // Look for existing child with same hop.
93 GhostTreeNode *child = nullptr;
94 for (auto &c : current->children)
95 {
96 if (c.hop == hop && c.kind == childKind)
97 {
98 child = &c;
99 break;
100 }
101 }
102 if (!child)
103 {
104 current->children.push_back(
105 GhostTreeNode{childKind, hop, false, childLevel, {}});
106 child = &current->children.back();
107 }
108
109 // If this is the last hop, mark as collect.
110 // Also mark intermediate hops whose entity kind matches the
111 // chain target -- this enables cumulative ghost collection for
112 // multi-hop same-kind chains (e.g., Cell2Cell -> Cell2Cell).
113 if (hi + 1 == chain.hops.size() || childKind == chain.target)
114 child->collect = true;
115
116 current = child;
117 }
118 }
119
121 {
123 for (const auto &chain : spec.chains)
124 insertChainIntoTrie(tree.roots, chain);
125
126 // Assign IDs and parent IDs, compute maxLevel, build per-level lists.
127 int nextId = 0;
128 tree.maxLevel = 0;
129
130 // BFS to assign IDs and build levels.
131 // Use a queue of (node pointer, parent ID).
132 struct QEntry
133 {
134 GhostTreeNode *node;
135 int parentId;
136 };
137 std::queue<QEntry> q;
138 for (auto &root : tree.roots)
139 {
140 root.parentId = -1;
141 q.push({&root, -1});
142 }
143
144 while (!q.empty())
145 {
146 auto [node, pid] = q.front();
147 q.pop();
148
149 node->id = nextId++;
150 node->parentId = pid;
151
152 if (node->level > tree.maxLevel)
153 tree.maxLevel = node->level;
154
155 for (auto &child : node->children)
156 q.push({&child, node->id});
157 }
158
159 tree.totalNodes = nextId;
160
161 // Build per-level lists.
162 tree.levels.resize(tree.maxLevel + 1);
163 std::function<void(const GhostTreeNode &)> buildLevels =
164 [&](const GhostTreeNode &n)
165 {
166 tree.levels[n.level].push_back(LevelEntry{
167 n.id,
168 n.parentId,
169 n.kind,
170 n.hop,
171 n.collect,
172 !n.children.empty(),
173 });
174 for (const auto &child : n.children)
175 buildLevels(child);
176 };
177 for (const auto &root : tree.roots)
178 buildLevels(root);
179
180 return tree;
181 }
182
183 // =================================================================
184 // CompiledGhostTree helper methods
185 // =================================================================
186
187 static void collectRequiredAdjsHelper(
188 const GhostTreeNode &node,
189 std::unordered_set<AdjKind, AdjKindHash> &out)
190 {
191 for (const auto &child : node.children)
192 {
193 out.insert(child.hop);
194 collectRequiredAdjsHelper(child, out);
195 }
196 }
197
198 std::unordered_set<AdjKind, AdjKindHash> CompiledGhostTree::requiredAdjs() const
199 {
200 std::unordered_set<AdjKind, AdjKindHash> result;
201 for (const auto &root : roots)
202 collectRequiredAdjsHelper(root, result);
203 return result;
204 }
205
206 std::vector<AdjKind> CompiledGhostTree::checkAvailable(
207 const MeshConnectivity &dag) const
208 {
209 auto required = requiredAdjs();
210 std::vector<AdjKind> missing;
211 for (const auto &adj : required)
212 {
213 if (!dag.hasAdj(adj))
214 missing.push_back(adj);
215 }
216 return missing;
217 }
218
219 static void collectCollectedKindsHelper(
220 const GhostTreeNode &node,
221 std::unordered_set<EntityKind> &out)
222 {
223 if (node.collect)
224 out.insert(node.kind);
225 for (const auto &child : node.children)
226 collectCollectedKindsHelper(child, out);
227 }
228
229 std::unordered_set<EntityKind> CompiledGhostTree::collectedKinds() const
230 {
231 std::unordered_set<EntityKind> result;
232 for (const auto &root : roots)
233 collectCollectedKindsHelper(root, result);
234 return result;
235 }
236
237 // =================================================================
238 // CompiledGhostTree::dump
239 // =================================================================
240
241 static void dumpNode(
242 const GhostTreeNode &node,
243 std::ostringstream &oss,
244 int indent)
245 {
246 for (int i = 0; i < indent; i++)
247 oss << " ";
248 if (indent > 0)
249 oss << "\\-[" << adjKindName(node.hop) << "]-> ";
250 oss << entityKindName(node.kind)
251 << " (level=" << node.level;
252 if (node.collect)
253 oss << ", COLLECT";
254 oss << ")\n";
255 for (const auto &child : node.children)
256 dumpNode(child, oss, indent + 1);
257 }
258
259 std::string CompiledGhostTree::dump() const
260 {
261 std::ostringstream oss;
262 for (const auto &root : roots)
263 dumpNode(root, oss, 0);
264 return oss.str();
265 }
266
267 // =================================================================
268 // GhostResult helpers
269 // =================================================================
270
271 // (hasGhosts and totalGhosts are inline in the header)
272
273 // =================================================================
274 // MeshConnectivity::evaluateGhostTree — Hybrid evaluation
275 // =================================================================
276
277 /// Sorted merge of two sorted vectors into dst. dst = union(dst, src).
278 static void sortedMergeInto(std::vector<index> &dst, const std::vector<index> &src)
279 {
280 if (src.empty())
281 return;
282 if (dst.empty())
283 {
284 dst = src;
285 return;
286 }
287 std::vector<index> merged;
288 merged.reserve(dst.size() + src.size());
289 std::set_union(dst.begin(), dst.end(), src.begin(), src.end(),
290 std::back_inserter(merged));
291 dst = std::move(merged);
292 }
293
294 /// Traverse one hop: for each global index in parentSet, look up the
295 /// adjacency row and collect all entries into a sorted, deduplicated vector.
296 /// Templatized to work with any ArrayAdjacencyPair<rs> (variable or fixed-width).
297 template <class TAdjPair>
298 static std::vector<index> traverseHopImpl(
299 const std::vector<index> &parentSet,
300 const TAdjPair &adj,
301 bool assertFound)
302 {
303 if (parentSet.empty())
304 return {};
305
306 auto *gm = adj.father->pLGlobalMapping.get();
307 DNDS_assert(gm);
308
309 index fatherSize = adj.father->Size();
310 index sonSize = adj.son ? adj.son->Size() : 0;
311 index totalSize = fatherSize + sonSize;
312 index myOffset = gm->operator()(adj.father->getMPI().rank, 0);
313 auto *ghostMapping = adj.trans.pLGhostMapping.get();
314
315 // Use unordered_set for O(1) dedup, then sort at end.
316 std::unordered_set<index> seen;
317 seen.reserve(parentSet.size() * 2);
318
319 for (auto parentGlobal : parentSet)
320 {
321 index localIdx = -1;
322
323 if (parentGlobal >= myOffset && parentGlobal < myOffset + fatherSize)
324 {
325 localIdx = parentGlobal - myOffset;
326 }
327 else if (ghostMapping)
328 {
329 auto [found, rank, ghostIdx] = ghostMapping->search_indexAppend(parentGlobal);
330 if (found && ghostIdx >= 0 && ghostIdx < totalSize)
331 localIdx = ghostIdx;
332 }
333
334 if (localIdx < 0)
335 {
336 DNDS_assert_info(!assertFound,
337 fmt::format("traverseHop: global index {} not found locally "
338 "(father=[{}, {}), son={}). Scratch pull incomplete?",
339 parentGlobal, myOffset, myOffset + fatherSize, sonSize));
340 continue;
341 }
342
343 for (auto entry : adj[localIdx])
344 seen.insert(entry);
345 }
346
347 // Convert to sorted vector.
348 std::vector<index> result(seen.begin(), seen.end());
349 std::sort(result.begin(), result.end());
350 return result;
351 }
352
353 /// Dispatch traverseHop through AdjVariant via std::visit.
354 static std::vector<index> traverseHop(
355 const std::vector<index> &parentSet,
356 const AdjVariant &adjVar,
357 bool assertFound)
358 {
359 return std::visit([&](const auto &adj) -> std::vector<index>
360 { return traverseHopImpl(parentSet, adj, assertFound); },
361 adjVar);
362 }
363
364 /// Filter non-owned indices from a sorted set.
365 static std::vector<index> filterNonOwned(
366 const std::vector<index> &set,
367 index ownedStart, index ownedEnd)
368 {
369 std::vector<index> ghosts;
370 for (auto gi : set)
371 {
372 if (gi < ownedStart || gi >= ownedEnd)
373 ghosts.push_back(gi);
374 }
375 return ghosts;
376 }
377
378 GhostResult MeshConnectivity::evaluateGhostTree(
379 const CompiledGhostTree &tree,
380 const MPIInfo &mpi) const
381 {
382 // --- Pre-check all required adjacencies ---
383 auto missing = tree.checkAvailable(*this);
384 if (!missing.empty())
385 {
386 std::string names;
387 for (auto &m : missing)
388 names += " " + adjKindName(m);
389 DNDS_assert_info(false,
390 fmt::format("evaluateGhostTree: missing adjacencies:{}", names));
391 }
392
393 // --- Per-node sets: flat vector indexed by node ID ---
394 std::vector<std::vector<index>> nodeSets(tree.totalNodes);
395
396 // --- Scratch state: per-AdjKind temporary pair with ghost data ---
397 // Created lazily when a scratch pull is needed between levels.
398 // The scratch pair shares the same father as the registered adj
399 // but has its own son array and transformer.
400 std::unordered_map<AdjKind, ssp<AdjVariant>, AdjKindHash> scratchAdjs;
401
402 // Resolve adjacency, preferring scratch (ghost-populated) version.
403 auto resolveAdjLive = [&](AdjKind kind) -> ssp<AdjVariant>
404 {
405 auto sit = scratchAdjs.find(kind);
406 if (sit != scratchAdjs.end())
407 return sit->second;
408 return resolveAdj(kind);
409 };
410
411 // --- Initialize roots (level 0) ---
412 for (const auto &entry : tree.levels[0])
413 {
414 auto gm = getGlobalMapping(entry.kind);
416 fmt::format("evaluateGhostTree: no GlobalOffsetsMapping for root kind {}",
417 entityKindName(entry.kind)));
418 index myOffset = gm->operator()(mpi.rank, 0);
419 index mySize = gm->RLengths()[mpi.rank];
420 auto &set = nodeSets[entry.nodeId];
421 set.resize(mySize);
422 for (index i = 0; i < mySize; i++)
423 set[i] = myOffset + i;
424 }
425
426 // --- Collect ghost results ---
428
429 // --- Intermediate ghost tracking (all nodes, not just COLLECT).
430 // Scratch-pull decisions depend on this, not result.ghostIndices,
431 // so that non-collected intermediate nodes still seed scratch pulls
432 // for subsequent hops (e.g. Node in the Bnd→Bnd2Node→Node2Bnd→Bnd chain).
433 std::unordered_map<EntityKind, std::vector<index>> intermediateGhosts;
434
435 // --- Per-hop last-pulled ghost-set size, to avoid redundant pulls.
436 std::unordered_map<AdjKind, size_t, AdjKindHash> lastScratchSize;
437
438 // --- Level-by-level evaluation ---
439 for (int level = 0; level <= tree.maxLevel; level++)
440 {
441 const auto &levelEntries = tree.levels[level];
442
443 // Step 1: Collect ghosts from ALL nodes into intermediateGhosts;
444 // only COLLECT nodes feed the output result.ghostIndices.
445 for (const auto &entry : levelEntries)
446 {
447 auto gm = getGlobalMapping(entry.kind);
449 fmt::format("evaluateGhostTree: no GlobalOffsetsMapping "
450 "for node kind {}",
451 entityKindName(entry.kind)));
452 index myOffset = gm->operator()(mpi.rank, 0);
453 index myEnd = myOffset + gm->RLengths()[mpi.rank];
454
455 auto ghosts = filterNonOwned(nodeSets[entry.nodeId], myOffset, myEnd);
456 sortedMergeInto(intermediateGhosts[entry.kind], ghosts);
457
458 if (entry.collect)
459 sortedMergeInto(result.ghostIndices[entry.kind], ghosts);
460 }
461
462 if (level >= tree.maxLevel)
463 break;
464
465 // Step 2: Scratch pull — create/expand scratch for hops where
466 // intermediateGhosts[parentKind] has grown beyond the last pull.
467 {
468 std::vector<AdjKind> hopsNeedingScratch;
469 for (const auto &childEntry : tree.levels[level + 1])
470 {
471 AdjKind hop = childEntry.hop;
472 EntityKind parentKind = hop.from;
473
474 bool alreadyHandled = false;
475 for (auto &h : hopsNeedingScratch)
476 if (h == hop)
477 {
478 alreadyHandled = true;
479 break;
480 }
481 if (alreadyHandled)
482 continue;
483
484 size_t curSize = 0;
485 auto git = intermediateGhosts.find(parentKind);
486 if (git != intermediateGhosts.end())
487 curSize = git->second.size();
488
489 bool needsPull = (curSize > 0 &&
490 curSize > lastScratchSize[hop]);
491
492 int localNeedsIt = needsPull ? 1 : 0;
493 int globalNeedsIt = 0;
494 MPI_Allreduce(&localNeedsIt, &globalNeedsIt, 1, MPI_INT, MPI_MAX, mpi.comm);
495
496 if (globalNeedsIt)
497 {
498 hopsNeedingScratch.push_back(hop);
499 lastScratchSize[hop] = curSize;
500 }
501 }
502
503 for (auto &hop : hopsNeedingScratch)
504 {
505 EntityKind parentKind = hop.from;
506 auto origAdj = resolveAdj(hop);
507 if (!origAdj)
508 continue;
509
510 std::vector<index> ghostSet;
511 auto git = intermediateGhosts.find(parentKind);
512 if (git != intermediateGhosts.end())
513 ghostSet = git->second;
514
515 std::visit([&](const auto &adj)
516 {
517 using TPair = std::decay_t<decltype(adj)>;
518 using TArray = typename TPair::t_arr;
519
520 auto tempSon = make_ssp<TArray>(
521 ObjName{"scratch_son"}, adj.father->getMPI());
522
523 auto livePair = makeAdjVariant<TPair>();
524 auto &live = std::get<TPair>(*livePair);
525 live.father = adj.father;
526 live.son = tempSon;
527 live.trans.setFatherSon(adj.father, tempSon);
528
529 DNDS_assert_info(adj.father->pLGlobalMapping,
530 "scratch pull: father must have pLGlobalMapping");
531 live.trans.pLGlobalMapping = adj.father->pLGlobalMapping;
532
533 live.trans.createGhostMapping(ghostSet);
534 live.trans.createMPITypes();
535 live.trans.pullOnce();
536
537 scratchAdjs[hop] = std::move(livePair); }, *origAdj);
538 }
539 }
540
541 // Step 3: Traverse children at level+1 using live adjacencies.
542 const auto &nextLevelEntries = tree.levels[level + 1];
543 for (const auto &childEntry : nextLevelEntries)
544 {
545 auto &parentSet = nodeSets[childEntry.parentId];
546
547 auto adjVar = resolveAdjLive(childEntry.hop);
548 DNDS_assert_info(adjVar,
549 fmt::format("evaluateGhostTree: adjacency {} not resolved",
550 adjKindName(childEntry.hop)));
551
552 nodeSets[childEntry.nodeId] = traverseHop(parentSet, *adjVar, false);
553 }
554 }
555
556 // --- Deduplicate ghost indices (already sorted from sortedMergeInto) ---
557 // Remove empty entries.
558 for (auto it = result.ghostIndices.begin(); it != result.ghostIndices.end();)
559 {
560 if (it->second.empty())
561 it = result.ghostIndices.erase(it);
562 else
563 ++it;
564 }
565
566 // --- Collective: determine which EntityKinds have ghosts on ANY rank ---
567 int localMask = 0;
568 for (auto &[kind, indices] : result.ghostIndices)
569 localMask |= (1 << static_cast<int>(kind));
570
571 int globalMask = 0;
572 MPI_Allreduce(&localMask, &globalMask, 1, MPI_INT, MPI_BOR, mpi.comm);
573
574 for (int i = 0; i < static_cast<int>(EntityKind::NUM_KINDS); i++)
575 {
576 if (globalMask & (1 << i))
577 result.activeKinds.insert(static_cast<EntityKind>(i));
578 }
579
580 return result;
581 }
582
583} // namespace DNDS::Geom
#define DNDS_assert_info(expr, info)
Debug-only assertion with an extra std::string info message.
Definition Errors.hpp:117
#define DNDS_assert(expr)
Debug-only assertion (compiled out when DNDS_NDEBUG is defined). Prints the expression + file/line + ...
Definition Errors.hpp:112
Layered DAG of mesh adjacency relations with composable DSL operations.
Eigen::Matrix< real, 3, 3 > m
const char * entityKindName(EntityKind kind)
String name for an EntityKind (for diagnostics).
std::string adjKindName(const AdjKind &kind)
Format an AdjKind as a diagnostic string, e.g. "Cell2Node", "Cell2Cell(Node)".
int64_t index
Global row / DOF index type (signed 64-bit; handles multi-billion-cell meshes).
Definition Defines.hpp:112
std::shared_ptr< T > ssp
Shortened alias for std::shared_ptr used pervasively in DNDSR.
Definition Defines.hpp:143
set(LIBNAME cfv) set(LINKS) set(LINKS_SHARED geom_shared dnds_shared $
Definition CMakeLists.txt:5
Hash for AdjKind (for use in unordered containers).
int maxLevel
Maximum BFS depth across all nodes.
int totalNodes
Total number of nodes (for flat array sizing).
std::vector< AdjKind > checkAvailable(const MeshConnectivity &dag) const
static CompiledGhostTree compile(const GhostSpec &spec)
std::vector< std::vector< LevelEntry > > levels
std::vector< GhostTreeNode > roots
EntityKind target
Entity kind to ghost (must == hops.back().to).
EntityKind anchor
Owned entities to start from.
std::vector< AdjKind > hops
Sequence of adjacency lookups.
static GhostSpec defaultPrimary()
std::vector< GhostChain > chains
One node in the compiled ghost tree.
int level
BFS depth (root = 0).
int parentId
Parent node ID (-1 for roots).
EntityKind kind
Entity kind at this node.
bool collect
If true, non-owned entities here become ghosts.
int parentId
ID of the parent node (-1 for roots).
bool hasAdj(AdjKind kind) const
Check whether an AdjKind is registered.
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:231
Tag type for naming objects created via make_ssp.
Definition Defines.hpp:254
auto adj
auto result
Eigen::Vector3d n(1.0, 0.0, 0.0)