ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
HaloPlanValidator.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2026 The ESPResSo project
3 *
4 * This file is part of ESPResSo.
5 *
6 * ESPResSo is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * ESPResSo is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19/** \file
20 * Local (single-rank) validator for GhostComm::HaloPlan.
21 * See HaloPlanValidator.hpp.
22 */
23
25
26#include <boost/mpi/collectives/all_to_all.hpp>
27
28#include <iostream>
29#include <sstream>
30#include <unordered_map>
31#include <unordered_set>
32
33namespace GhostComm {
34
35std::vector<std::string>
36validate_halo_plan(HaloPlan const &plan, std::span<Cell *const> local_cells,
37 std::span<Cell *const> ghost_cells) {
38 std::vector<std::string> violations;
39
40 // Build the set of expected ghost ParticleList pointers.
41 std::unordered_set<ParticleList const *> ghost_set;
42 for (Cell *c : ghost_cells) {
43 ghost_set.insert(&c->particles());
44 }
45
46 // Collective-covered cells: AtomDecomposition (collective-only) and
47 // HybridDecomposition (neighbors + collective) fill some/all ghosts via the
48 // n-square broadcast/reduce section, NOT via neighbors/local. The section's
49 // `cells` vector identifies exactly which ParticleLists it covers (one per
50 // rank; the entry for the local rank is this rank's own cell, the rest are
51 // ghost copies of every other rank's owned cell). Ghost cells found here are
52 // covered even though they are not point-to-point recv/dst targets.
53 bool const has_collective =
54 plan.collective.has_value() &&
55 plan.collective->pattern != CollectivePattern::None;
56 std::unordered_set<ParticleList const *> collective_set;
57 if (has_collective) {
58 for (ParticleList const *pl : plan.collective->cells) {
59 collective_set.insert(pl);
60 }
61 }
62
63 // Set of ghost cells that some local cell actually interacts with, i.e. that
64 // appear in a local cell's neighbor stencil. Only these ghosts must be
65 // filled by the plan; a ghost that no local cell ever references carries no
66 // physics and needs no communication. This happens on a single MPI rank:
67 // make_halo_plan() returns an empty plan there because init_cell_interactions
68 // wires periodic neighbourships directly between local cells (see
69 // RegularDecomposition), so the halo-layer cells that mark_cells() still
70 // classifies as ghosts are never referenced and must not be flagged as
71 // uncovered. Double-fill and out-of-ghost-set targets stay strict below, and
72 // the neighborship-match check still requires every *referenced* ghost to be
73 // covered, so this cannot mask a real missing-communication defect.
74 std::unordered_set<ParticleList const *> referenced_ghosts;
75 for (Cell *c : local_cells) {
76 for (Cell *n : c->neighbors().all()) {
77 ParticleList const *pl = &n->particles();
78 if (ghost_set.contains(pl)) {
79 referenced_ghosts.insert(pl);
80 }
81 }
82 }
83
84 // Check peer-uniqueness and shape; accumulate recv/dst fill counts.
85 std::unordered_map<ParticleList const *, int> fill_count;
86 std::unordered_set<int> seen_peers;
87
88 for (auto const &nc : plan.neighbors) {
89 // Peer-uniqueness
90 if (!seen_peers.insert(nc.peer).second) {
91 std::ostringstream oss;
92 oss << "peer " << nc.peer << " appears in more than one NeighborComm";
93 violations.emplace_back(oss.str());
94 }
95
96 // Shape: send.size() == recv.size()
97 if (nc.send.size() != nc.recv.size()) {
98 std::ostringstream oss;
99 oss << "NeighborComm peer=" << nc.peer
100 << " has send.size()=" << nc.send.size()
101 << " != recv.size()=" << nc.recv.size();
102 violations.emplace_back(oss.str());
103 }
104
105 // Accumulate recv fill counts; check targets are in ghost set.
106 for (ParticleList const *pl : nc.recv) {
107 if (not ghost_set.contains(pl)) {
108 std::ostringstream oss;
109 oss << "NeighborComm peer=" << nc.peer
110 << " recv target is not a ghost cell";
111 violations.emplace_back(oss.str());
112 }
113 ++fill_count[pl];
114 }
115 }
116
117 // Accumulate local.dst fill counts; check targets are in ghost set.
118 for (auto const &lc : plan.local) {
119 ParticleList const *pl = lc.dst;
120 if (not ghost_set.contains(pl)) {
121 violations.emplace_back("LocalComm dst target is not a ghost cell");
122 }
123 ++fill_count[pl];
124 }
125
126 // Coverage: every ghost that a local cell references must be filled.
127 // Point-to-point ghosts must appear exactly once as a recv/dst target; a
128 // double-fill is always a defect. Ghosts covered by the collective section
129 // are filled by the broadcast/reduce instead, so a zero point-to-point
130 // fill-count is fine for them (and they do not contribute to fill_count, so
131 // they cannot trigger the double-fill path). A ghost that no local cell
132 // references (e.g. the halo-layer cells on a single MPI rank, where the plan
133 // is intentionally empty) carries no physics and is not required to be
134 // filled, so a zero fill-count is fine for it too.
135 for (Cell *c : ghost_cells) {
136 ParticleList const *pl = &c->particles();
137 auto it = fill_count.find(pl);
138 int count = (it != fill_count.end()) ? it->second : 0;
139 if (count == 0) {
140 if (not collective_set.contains(pl) and referenced_ghosts.contains(pl)) {
141 violations.emplace_back(
142 "ghost cell is never filled (missing recv/dst)");
143 }
144 } else if (count > 1) {
145 std::ostringstream oss;
146 oss << "ghost cell is filled " << count << " times (expected 1)";
147 violations.emplace_back(oss.str());
148 }
149 }
150
151 // Neighborship-match: every local cell's ghost neighbor must be covered,
152 // either as a point-to-point recv/dst target or by the collective section.
153 for (Cell *c : local_cells) {
154 for (Cell *n : c->neighbors().all()) {
155 ParticleList const *pl = &n->particles();
156 if (not ghost_set.contains(pl)) {
157 continue; // not a ghost neighbor
158 }
159 if (collective_set.contains(pl)) {
160 continue; // covered by the collective broadcast/reduce section
161 }
162 auto it = fill_count.find(pl);
163 int count = (it != fill_count.end()) ? it->second : 0;
164 if (count == 0) {
165 violations.emplace_back(
166 "local cell has ghost neighbor that is not a covered recv/dst "
167 "target (referenced-but-uncommunicated ghost)");
168 }
169 }
170 }
171
172 // Interior/boundary consistency: a local cell marked interior
173 // (is_boundary()==false) must have no ghost neighbor.
174 for (Cell *c : local_cells) {
175 if (c->is_boundary()) {
176 continue; // boundary cells are expected to have ghost neighbors
177 }
178 for (Cell *n : c->neighbors().all()) {
179 if (ghost_set.contains(&n->particles())) {
180 violations.emplace_back("interior cell has a ghost neighbor");
181 break; // one violation per cell is sufficient
182 }
183 }
184 }
185
186 // Overlap-safety invariant (Task 5.3 precondition): an interior cell must
187 // not appear as a send source in any NeighborComm, and must not appear as
188 // a LocalComm src. Send sources are real cells whose data gets copied to a
189 // peer's ghost; LocalComm srcs are the matching self-copy sources. If an
190 // interior cell were in either, the force-reduce step would overwrite its
191 // ghost copy after the interior velocity update — violating the overlap
192 // correctness assumption.
193 //
194 // Build a set of ParticleList* that belong to interior local cells.
195 std::unordered_set<ParticleList const *> interior_set;
196 for (Cell *c : local_cells) {
197 if (!c->is_boundary()) {
198 interior_set.insert(&c->particles());
199 }
200 }
201 if (!interior_set.empty()) {
202 for (auto const &nc : plan.neighbors) {
203 for (auto const &sr : nc.send) {
204 if (interior_set.count(sr.cell)) {
205 std::ostringstream oss;
206 oss << "interior cell appears as NeighborComm send source (peer="
207 << nc.peer << ") — overlap-safety invariant violated";
208 violations.emplace_back(oss.str());
209 }
210 }
211 }
212 for (auto const &lc : plan.local) {
213 if (interior_set.count(lc.src)) {
214 violations.emplace_back(
215 "interior cell appears as LocalComm src — overlap-safety invariant "
216 "violated");
217 }
218 }
219 }
220
221 return violations;
222}
223
224std::vector<std::string> validate_halo_plan_symmetry(HaloPlan const &plan) {
225 std::vector<std::string> violations;
226
227 int const n = plan.comm.size();
228 int const me = plan.comm.rank();
229
230 // Build per-rank send and recv count arrays (size n, default 0).
231 std::vector<int> my_send_to(n, 0);
232 std::vector<int> my_recv_from(n, 0);
233 for (auto const &nc : plan.neighbors) {
234 my_send_to[nc.peer] = static_cast<int>(nc.send.size());
235 my_recv_from[nc.peer] = static_cast<int>(nc.recv.size());
236 }
237
238 // Collective all-to-all: element j of my_send_to goes to rank j.
239 // After this call peers_send_to_me[j] == rank j's send count toward me.
240 std::vector<int> peers_send_to_me(n, 0);
241 boost::mpi::all_to_all(plan.comm, my_send_to, peers_send_to_me);
242
243 // Check invariant: what I expect to receive from j == what j sends me.
244 for (int j = 0; j < n; ++j) {
245 if (j == me)
246 continue;
247 if (my_recv_from[j] != peers_send_to_me[j]) {
248 std::ostringstream oss;
249 oss << "symmetry mismatch with peer " << j << ": I expect to recv "
250 << my_recv_from[j] << " items but peer sends " << peers_send_to_me[j]
251 << " items";
252 violations.emplace_back(oss.str());
253 }
254 }
255
256 return violations;
257}
258
259bool report_violations(std::vector<std::string> const &violations,
260 char const *context) {
261 for (auto const &v : violations) {
262 std::cerr << "halo plan validation (" << context << "): " << v << "\n";
263 }
264 return violations.empty();
265}
266
267} // namespace GhostComm
Definition Cell.hpp:96
T & insert(T const &v)
Insert an element into the container.
Definition Bag.hpp:147
bool report_violations(std::vector< std::string > const &violations, char const *context)
Print violations to stderr and return whether the list was empty.
std::vector< std::string > validate_halo_plan_symmetry(HaloPlan const &plan)
Cross-rank symmetry check for a HaloPlan.
std::vector< std::string > validate_halo_plan(HaloPlan const &plan, std::span< Cell *const > local_cells, std::span< Cell *const > ghost_cells)
Validate a HaloPlan for correctness.
std::optional< CollectiveSection > collective
Definition HaloPlan.hpp:63
boost::mpi::communicator comm
Definition HaloPlan.hpp:60
std::vector< NeighborComm > neighbors
Definition HaloPlan.hpp:61
std::vector< LocalComm > local
Definition HaloPlan.hpp:62