Skip to main content

mesh_sieve/data/
global_map.rs

1//! Global DOF numbering for a local [`Section`].
2//!
3//! `LocalToGlobalMap` computes a deterministic, ownership-aware global index
4//! for each local point/DOF pair. Owned points are numbered first by rank
5//! (ascending) and then by point ID within each rank. Ghost points receive
6//! their global offsets from their owning rank via the overlap graph.
7
8use std::collections::{BTreeSet, HashMap, HashSet};
9
10use crate::algs::communicator::{CommTag, Communicator, SectionCommTags, Wait};
11use crate::algs::wire::{WireCount, cast_slice, cast_slice_mut};
12use crate::data::constrained_section::ConstraintSet;
13use crate::data::multi_section::MultiSection;
14use crate::data::section::Section;
15use crate::data::section_layout::{constrained_dof_len, multi_section_dof_len_with_constraints};
16use crate::data::storage::Storage;
17use crate::mesh_error::MeshSieveError;
18use crate::overlap::overlap::Overlap;
19use crate::overlap::overlap::local;
20use crate::topology::ownership::PointOwnership;
21use crate::topology::point::PointId;
22use crate::topology::sieve::sieve_trait::Sieve;
23
24/// Mapping from local point/DOF pairs to unique global indices.
25#[derive(Clone, Debug, Default)]
26pub struct LocalToGlobalMap {
27    offsets: Vec<Option<u64>>,
28    dof_lengths: Vec<Option<usize>>,
29    total_dofs: u64,
30}
31
32impl LocalToGlobalMap {
33    /// Build a global numbering map using explicit communication tags and ownership data.
34    pub fn from_section_with_tags_and_ownership<V, S, C>(
35        section: &Section<V, S>,
36        overlap: &Overlap,
37        ownership: &PointOwnership,
38        comm: &C,
39        my_rank: usize,
40        tags: SectionCommTags,
41    ) -> Result<Self, MeshSieveError>
42    where
43        S: Storage<V>,
44        C: Communicator + Sync,
45    {
46        #[cfg(any(
47            debug_assertions,
48            feature = "strict-invariants",
49            feature = "check-invariants"
50        ))]
51        overlap.validate_invariants()?;
52
53        let max_id = section.atlas().points().map(|p| p.get()).max().unwrap_or(0) as usize;
54        let mut map = LocalToGlobalMap {
55            offsets: vec![None; max_id],
56            dof_lengths: vec![None; max_id],
57            total_dofs: 0,
58        };
59        map.populate_dof_lengths_with(section.atlas().points(), |point| {
60            let (_, len) = section
61                .atlas()
62                .get(point)
63                .ok_or(MeshSieveError::PointNotInAtlas(point))?;
64            Ok(len)
65        })?;
66        map.assign_owned_offsets(section.atlas().points(), ownership, comm, my_rank)?;
67
68        let mut has_ghosts = false;
69        for p in section.atlas().points() {
70            let owner = ownership.owner_or_err(p)?;
71            if owner != my_rank {
72                has_ghosts = true;
73            }
74        }
75
76        let mut nb: BTreeSet<usize> = overlap.neighbor_ranks().collect();
77        nb.remove(&my_rank);
78        if nb.is_empty() {
79            if has_ghosts {
80                return Err(MeshSieveError::MissingOverlap {
81                    source: format!("rank {my_rank} has ghost points but no overlap").into(),
82                });
83            }
84            return Ok(map);
85        }
86
87        let mut links =
88            neighbour_links_with_ownership_for_atlas(section.atlas(), overlap, ownership, my_rank)?;
89        for link_vec in links.values_mut() {
90            link_vec.sort_unstable_by_key(|(send_loc, _)| send_loc.get());
91        }
92
93        let mut all_neighbors: HashSet<usize> = overlap.neighbor_ranks().collect();
94        all_neighbors.extend(links.keys().copied());
95        all_neighbors.remove(&my_rank);
96
97        let send_counts = build_send_counts(&links, section.atlas(), ownership, my_rank)?;
98        let recv_counts =
99            exchange_sizes_with_counts(&send_counts, comm, tags.sizes, &all_neighbors)?;
100        exchange_offsets(
101            &links,
102            &recv_counts,
103            comm,
104            tags.data,
105            section.atlas(),
106            ownership,
107            &mut map,
108            &all_neighbors,
109        )?;
110
111        map.ensure_complete(section.atlas().points())?;
112        Ok(map)
113    }
114
115    /// Build a global numbering map using ownership metadata and a legacy default tag (0xBEEF).
116    pub fn from_section_with_ownership<V, S, C>(
117        section: &Section<V, S>,
118        overlap: &Overlap,
119        ownership: &PointOwnership,
120        comm: &C,
121        my_rank: usize,
122    ) -> Result<Self, MeshSieveError>
123    where
124        S: Storage<V>,
125        C: Communicator + Sync,
126    {
127        let tags = SectionCommTags::from_base(CommTag::new(0xBEEF));
128        Self::from_section_with_tags_and_ownership(section, overlap, ownership, comm, my_rank, tags)
129    }
130
131    /// Build a global numbering map using a section, constraints, and ownership data.
132    pub fn from_section_with_constraints_and_ownership<V, S, C, CS>(
133        section: &Section<V, S>,
134        constraints: &CS,
135        overlap: &Overlap,
136        ownership: &PointOwnership,
137        comm: &C,
138        my_rank: usize,
139        tags: SectionCommTags,
140    ) -> Result<Self, MeshSieveError>
141    where
142        S: Storage<V>,
143        C: Communicator + Sync,
144        CS: ConstraintSet<V>,
145    {
146        let mut map = LocalToGlobalMap::default();
147        map.populate_dof_lengths_with(section.atlas().points(), |point| {
148            let (_, len) = section
149                .atlas()
150                .get(point)
151                .ok_or(MeshSieveError::PointNotInAtlas(point))?;
152            constrained_dof_len(point, len, constraints.constraints_for(point))
153        })?;
154        map.assign_owned_offsets(section.atlas().points(), ownership, comm, my_rank)?;
155
156        let mut has_ghosts = false;
157        for p in section.atlas().points() {
158            let owner = ownership.owner_or_err(p)?;
159            if owner != my_rank {
160                has_ghosts = true;
161            }
162        }
163
164        let mut nb: BTreeSet<usize> = overlap.neighbor_ranks().collect();
165        nb.remove(&my_rank);
166        if nb.is_empty() {
167            if has_ghosts {
168                return Err(MeshSieveError::MissingOverlap {
169                    source: format!("rank {my_rank} has ghost points but no overlap").into(),
170                });
171            }
172            return Ok(map);
173        }
174
175        let mut links =
176            neighbour_links_with_ownership_for_atlas(section.atlas(), overlap, ownership, my_rank)?;
177        for link_vec in links.values_mut() {
178            link_vec.sort_unstable_by_key(|(send_loc, _)| send_loc.get());
179        }
180
181        let mut all_neighbors: HashSet<usize> = overlap.neighbor_ranks().collect();
182        all_neighbors.extend(links.keys().copied());
183        all_neighbors.remove(&my_rank);
184
185        let send_counts = build_send_counts(&links, section.atlas(), ownership, my_rank)?;
186        let recv_counts =
187            exchange_sizes_with_counts(&send_counts, comm, tags.sizes, &all_neighbors)?;
188        exchange_offsets(
189            &links,
190            &recv_counts,
191            comm,
192            tags.data,
193            section.atlas(),
194            ownership,
195            &mut map,
196            &all_neighbors,
197        )?;
198
199        map.ensure_complete(section.atlas().points())?;
200        Ok(map)
201    }
202
203    /// Build a global numbering map using a multi-section and ownership data.
204    pub fn from_multi_section_with_tags_and_ownership<V, S, C>(
205        section: &MultiSection<V, S>,
206        overlap: &Overlap,
207        ownership: &PointOwnership,
208        comm: &C,
209        my_rank: usize,
210        tags: SectionCommTags,
211    ) -> Result<Self, MeshSieveError>
212    where
213        S: Storage<V>,
214        C: Communicator + Sync,
215    {
216        let mut map = LocalToGlobalMap::default();
217        map.populate_dof_lengths_with(section.atlas().points(), |point| {
218            multi_section_dof_len_with_constraints(section, point)
219        })?;
220        map.assign_owned_offsets(section.atlas().points(), ownership, comm, my_rank)?;
221
222        let mut has_ghosts = false;
223        for p in section.atlas().points() {
224            let owner = ownership.owner_or_err(p)?;
225            if owner != my_rank {
226                has_ghosts = true;
227            }
228        }
229
230        let mut nb: BTreeSet<usize> = overlap.neighbor_ranks().collect();
231        nb.remove(&my_rank);
232        if nb.is_empty() {
233            if has_ghosts {
234                return Err(MeshSieveError::MissingOverlap {
235                    source: format!("rank {my_rank} has ghost points but no overlap").into(),
236                });
237            }
238            return Ok(map);
239        }
240
241        let mut links =
242            neighbour_links_with_ownership_for_atlas(section.atlas(), overlap, ownership, my_rank)?;
243        for link_vec in links.values_mut() {
244            link_vec.sort_unstable_by_key(|(send_loc, _)| send_loc.get());
245        }
246
247        let mut all_neighbors: HashSet<usize> = overlap.neighbor_ranks().collect();
248        all_neighbors.extend(links.keys().copied());
249        all_neighbors.remove(&my_rank);
250
251        let send_counts = build_send_counts(&links, section.atlas(), ownership, my_rank)?;
252        let recv_counts =
253            exchange_sizes_with_counts(&send_counts, comm, tags.sizes, &all_neighbors)?;
254        exchange_offsets(
255            &links,
256            &recv_counts,
257            comm,
258            tags.data,
259            section.atlas(),
260            ownership,
261            &mut map,
262            &all_neighbors,
263        )?;
264
265        map.ensure_complete(section.atlas().points())?;
266        Ok(map)
267    }
268
269    /// Build a global numbering map for a multi-section using a legacy default tag (0xBEEF).
270    pub fn from_multi_section_with_ownership<V, S, C>(
271        section: &MultiSection<V, S>,
272        overlap: &Overlap,
273        ownership: &PointOwnership,
274        comm: &C,
275        my_rank: usize,
276    ) -> Result<Self, MeshSieveError>
277    where
278        S: Storage<V>,
279        C: Communicator + Sync,
280    {
281        let tags = SectionCommTags::from_base(CommTag::new(0xBEEF));
282        Self::from_multi_section_with_tags_and_ownership(
283            section, overlap, ownership, comm, my_rank, tags,
284        )
285    }
286
287    /// Return the global offset (start index) for a point.
288    pub fn global_offset(&self, point: PointId) -> Result<u64, MeshSieveError> {
289        let idx = point_index(point)?;
290        self.offsets
291            .get(idx)
292            .and_then(|val| *val)
293            .ok_or(MeshSieveError::PointNotInAtlas(point))
294    }
295
296    /// Return the global index for a local point/DOF pair.
297    pub fn global_index(&self, point: PointId, dof: usize) -> Result<u64, MeshSieveError> {
298        let idx = point_index(point)?;
299        let len = self
300            .dof_lengths
301            .get(idx)
302            .and_then(|val| *val)
303            .ok_or(MeshSieveError::PointNotInAtlas(point))?;
304        if dof >= len {
305            return Err(MeshSieveError::ConstraintIndexOutOfBounds {
306                point,
307                index: dof,
308                len,
309            });
310        }
311        Ok(self.global_offset(point)? + dof as u64)
312    }
313
314    /// Return the global DOF range `[start, end)` for a point.
315    pub fn global_range(&self, point: PointId) -> Result<std::ops::Range<u64>, MeshSieveError> {
316        let idx = point_index(point)?;
317        let len = self
318            .dof_lengths
319            .get(idx)
320            .and_then(|val| *val)
321            .ok_or(MeshSieveError::PointNotInAtlas(point))? as u64;
322        let start = self.global_offset(point)?;
323        Ok(start..start + len)
324    }
325
326    /// Return a copy of this numbering with point IDs remapped.
327    ///
328    /// `new_to_old` maps each point in the returned map to the corresponding
329    /// point in this map. Offsets and DOF lengths are preserved, which is useful
330    /// for compact submeshes that must keep parent global numbering semantics.
331    pub fn remap_points<I>(&self, new_to_old: I) -> Result<Self, MeshSieveError>
332    where
333        I: IntoIterator<Item = (PointId, PointId)>,
334    {
335        let mut out = LocalToGlobalMap {
336            offsets: Vec::new(),
337            dof_lengths: Vec::new(),
338            total_dofs: self.total_dofs,
339        };
340        for (new_point, old_point) in new_to_old {
341            let old_idx = point_index(old_point)?;
342            let new_idx = point_index(new_point)?;
343            let offset = self
344                .offsets
345                .get(old_idx)
346                .and_then(|val| *val)
347                .ok_or(MeshSieveError::PointNotInAtlas(old_point))?;
348            let len = self
349                .dof_lengths
350                .get(old_idx)
351                .and_then(|val| *val)
352                .ok_or(MeshSieveError::PointNotInAtlas(old_point))?;
353            if new_idx >= out.offsets.len() {
354                out.offsets.resize(new_idx + 1, None);
355                out.dof_lengths.resize(new_idx + 1, None);
356            }
357            out.offsets[new_idx] = Some(offset);
358            out.dof_lengths[new_idx] = Some(len);
359        }
360        Ok(out)
361    }
362
363    /// Total number of globally owned DOFs across all ranks.
364    pub fn total_dofs(&self) -> u64 {
365        self.total_dofs
366    }
367
368    fn populate_dof_lengths_with<I, F>(
369        &mut self,
370        points: I,
371        mut dof_len: F,
372    ) -> Result<(), MeshSieveError>
373    where
374        I: IntoIterator<Item = PointId>,
375        F: FnMut(PointId) -> Result<usize, MeshSieveError>,
376    {
377        for p in points {
378            let len = dof_len(p)?;
379            let idx = point_index(p)?;
380            if idx >= self.dof_lengths.len() {
381                self.dof_lengths.resize(idx + 1, None);
382                self.offsets.resize(idx + 1, None);
383            }
384            self.dof_lengths[idx] = Some(len);
385        }
386        Ok(())
387    }
388
389    fn assign_owned_offsets<I, C>(
390        &mut self,
391        points: I,
392        ownership: &PointOwnership,
393        comm: &C,
394        my_rank: usize,
395    ) -> Result<(), MeshSieveError>
396    where
397        I: IntoIterator<Item = PointId>,
398        C: Communicator + Sync,
399    {
400        let mut owned_points: Vec<PointId> = points
401            .into_iter()
402            .filter(|&p| ownership.is_owned_by(p, my_rank))
403            .collect();
404        owned_points.sort_unstable();
405
406        let mut local_total = 0u64;
407        for p in &owned_points {
408            let idx = point_index(*p)?;
409            let len = self
410                .dof_lengths
411                .get(idx)
412                .and_then(|val| *val)
413                .ok_or(MeshSieveError::PointNotInAtlas(*p))? as u64;
414            self.offsets[idx] = Some(local_total);
415            local_total = local_total.saturating_add(len);
416        }
417
418        let n_ranks = comm.size().max(1);
419        let mut recvbuf = vec![0u8; n_ranks * std::mem::size_of::<u64>()];
420        comm.allgather(&local_total.to_le_bytes(), &mut recvbuf);
421
422        let mut totals = vec![0u64; n_ranks];
423        for (idx, chunk) in recvbuf.chunks_exact(8).enumerate() {
424            let mut raw = [0u8; 8];
425            raw.copy_from_slice(chunk);
426            totals[idx] = u64::from_le_bytes(raw);
427        }
428        let base: u64 = totals.iter().take(my_rank).copied().sum();
429        self.total_dofs = totals.iter().copied().sum();
430
431        for p in &owned_points {
432            let idx = point_index(*p)?;
433            if let Some(offset) = self.offsets.get_mut(idx).and_then(|val| val.as_mut()) {
434                *offset = offset.saturating_add(base);
435            }
436        }
437
438        Ok(())
439    }
440
441    fn ensure_complete<I>(&self, points: I) -> Result<(), MeshSieveError>
442    where
443        I: IntoIterator<Item = PointId>,
444    {
445        for p in points {
446            let idx = point_index(p)?;
447            if self.offsets.get(idx).and_then(|val| *val).is_none() {
448                return Err(MeshSieveError::MissingOverlap {
449                    source: format!("missing global offset for point {p:?}").into(),
450                });
451            }
452        }
453        Ok(())
454    }
455}
456
457/// Allocate a zero-initialized global vector for a local-to-global map.
458pub fn global_vector_for_map<V>(map: &LocalToGlobalMap) -> Vec<V>
459where
460    V: Clone + Default,
461{
462    vec![V::default(); map.total_dofs as usize]
463}
464
465fn point_index(point: PointId) -> Result<usize, MeshSieveError> {
466    point
467        .get()
468        .checked_sub(1)
469        .ok_or(MeshSieveError::InvalidPointId)
470        .map(|idx| idx as usize)
471}
472
473fn build_send_counts(
474    links: &HashMap<usize, Vec<(PointId, PointId)>>,
475    atlas: &crate::data::atlas::Atlas,
476    ownership: &PointOwnership,
477    my_rank: usize,
478) -> Result<HashMap<usize, u32>, MeshSieveError> {
479    let mut counts = HashMap::with_capacity(links.len());
480    for (nbr, link_vec) in links {
481        let mut count = 0usize;
482        for &(send_loc, _) in link_vec {
483            if atlas.contains(send_loc) && ownership.owner_or_err(send_loc)? == my_rank {
484                count += 1;
485            }
486        }
487        counts.insert(*nbr, u32::try_from(count).unwrap_or(u32::MAX));
488    }
489    Ok(counts)
490}
491
492fn exchange_sizes_with_counts<C>(
493    send_counts: &HashMap<usize, u32>,
494    comm: &C,
495    tag: CommTag,
496    all_neighbors: &HashSet<usize>,
497) -> Result<HashMap<usize, u32>, MeshSieveError>
498where
499    C: Communicator + Sync,
500{
501    let mut recv_size: HashMap<usize, (C::RecvHandle, WireCount)> = HashMap::new();
502    for &nbr in all_neighbors {
503        let mut cnt = WireCount::new(0);
504        let h = comm.irecv_result(
505            nbr,
506            tag.as_u16(),
507            cast_slice_mut(std::slice::from_mut(&mut cnt)),
508        )?;
509        recv_size.insert(nbr, (h, cnt));
510    }
511
512    let mut pending_sends = Vec::with_capacity(all_neighbors.len());
513    let mut send_bufs = Vec::with_capacity(all_neighbors.len());
514    for &nbr in all_neighbors {
515        let count = send_counts.get(&nbr).copied().unwrap_or(0);
516        let wire = WireCount::new(count as usize);
517        pending_sends.push(comm.isend_result(
518            nbr,
519            tag.as_u16(),
520            cast_slice(std::slice::from_ref(&wire)),
521        )?);
522        send_bufs.push(wire);
523    }
524
525    let mut sizes_in = HashMap::new();
526    let mut maybe_err = None;
527    for (nbr, (h, mut cnt)) in recv_size {
528        match h.wait() {
529            Some(data) if data.len() == std::mem::size_of::<WireCount>() => {
530                if maybe_err.is_none() {
531                    let bytes = cast_slice_mut(std::slice::from_mut(&mut cnt));
532                    bytes.copy_from_slice(&data);
533                    sizes_in.insert(nbr, cnt.get() as u32);
534                }
535            }
536            Some(data) if maybe_err.is_none() => {
537                maybe_err = Some(MeshSieveError::CommError {
538                    neighbor: nbr,
539                    source: format!(
540                        "expected {} bytes for size header, got {}",
541                        std::mem::size_of::<WireCount>(),
542                        data.len()
543                    )
544                    .into(),
545                });
546            }
547            None if maybe_err.is_none() => {
548                maybe_err = Some(MeshSieveError::CommError {
549                    neighbor: nbr,
550                    source: format!("failed to receive size from rank {nbr}").into(),
551                });
552            }
553            _ => {}
554        }
555    }
556
557    for send in pending_sends {
558        let _ = send.wait();
559    }
560    drop(send_bufs);
561
562    if let Some(err) = maybe_err {
563        Err(err)
564    } else {
565        Ok(sizes_in)
566    }
567}
568
569fn exchange_offsets<C>(
570    links: &HashMap<usize, Vec<(PointId, PointId)>>,
571    recv_counts: &HashMap<usize, u32>,
572    comm: &C,
573    tag: CommTag,
574    atlas: &crate::data::atlas::Atlas,
575    ownership: &PointOwnership,
576    map: &mut LocalToGlobalMap,
577    all_neighbors: &HashSet<usize>,
578) -> Result<(), MeshSieveError>
579where
580    C: Communicator + Sync,
581{
582    let mut recv_data: HashMap<usize, (C::RecvHandle, Vec<u64>)> = HashMap::new();
583    for &nbr in all_neighbors {
584        let n_items = recv_counts.get(&nbr).copied().unwrap_or(0) as usize;
585        let mut buffer = vec![0u64; n_items];
586        let h = comm.irecv_result(nbr, tag.as_u16(), cast_slice_mut(&mut buffer))?;
587        recv_data.insert(nbr, (h, buffer));
588    }
589
590    let mut pending_sends = Vec::with_capacity(all_neighbors.len());
591    let mut send_bufs = Vec::with_capacity(all_neighbors.len());
592    for &nbr in all_neighbors {
593        let link_vec = links.get(&nbr).map_or(&[][..], |v| &v[..]);
594        let mut scratch = Vec::new();
595        for &(send_loc, _) in link_vec {
596            if atlas.contains(send_loc) && ownership.owner_or_err(send_loc)? == comm.rank() {
597                let idx = point_index(send_loc)?;
598                if let Some(offset) = map.offsets.get(idx).and_then(|val| *val) {
599                    scratch.push(offset);
600                }
601            }
602        }
603        let bytes = cast_slice(&scratch);
604        pending_sends.push(comm.isend_result(nbr, tag.as_u16(), bytes)?);
605        send_bufs.push(scratch);
606    }
607
608    for (nbr, (h, mut buffer)) in recv_data {
609        let raw = h.wait().ok_or_else(|| MeshSieveError::CommError {
610            neighbor: nbr,
611            source: "No data received (wait returned None)".into(),
612        })?;
613        if raw.len() != buffer.len() * std::mem::size_of::<u64>() {
614            return Err(MeshSieveError::BufferSizeMismatch {
615                neighbor: nbr,
616                expected: buffer.len() * std::mem::size_of::<u64>(),
617                got: raw.len(),
618            });
619        }
620        cast_slice_mut(&mut buffer).copy_from_slice(&raw);
621        let parts: &[u64] = &buffer;
622        let link_vec = links.get(&nbr).map_or(&[][..], |v| &v[..]);
623        let mut recv_pairs = Vec::new();
624        for &(send_loc, recv_loc) in link_vec {
625            if !atlas.contains(recv_loc) {
626                continue;
627            }
628            if ownership.owner_or_err(recv_loc)? == nbr {
629                recv_pairs.push((send_loc, recv_loc));
630            }
631        }
632        recv_pairs.sort_unstable_by_key(|(send_loc, _)| send_loc.get());
633        if parts.len() != recv_pairs.len() {
634            return Err(MeshSieveError::PartCountMismatch {
635                neighbor: nbr,
636                expected: recv_pairs.len(),
637                got: parts.len(),
638            });
639        }
640        for ((_, recv_loc), offset) in recv_pairs.iter().zip(parts) {
641            let idx = point_index(*recv_loc)?;
642            if idx >= map.offsets.len() {
643                map.offsets.resize(idx + 1, None);
644            }
645            map.offsets[idx] = Some(*offset);
646        }
647    }
648
649    for send in pending_sends {
650        let _ = send.wait();
651    }
652    drop(send_bufs);
653
654    Ok(())
655}
656
657fn neighbour_links_with_ownership_for_atlas(
658    atlas: &crate::data::atlas::Atlas,
659    ovlp: &Overlap,
660    ownership: &PointOwnership,
661    my_rank: usize,
662) -> Result<HashMap<usize, Vec<(PointId, PointId)>>, MeshSieveError> {
663    let mut out: HashMap<usize, Vec<(PointId, PointId)>> = HashMap::new();
664
665    for p in atlas.points() {
666        let owner = ownership.owner_or_err(p)?;
667        if owner == my_rank {
668            for (_dst, rem) in ovlp.cone(local(p)) {
669                if rem.rank != my_rank {
670                    let remote_pt = rem
671                        .remote_point
672                        .ok_or(MeshSieveError::OverlapLinkMissing(p, rem.rank))?;
673                    out.entry(rem.rank).or_default().push((p, remote_pt));
674                }
675            }
676        } else {
677            let mut remote_point = None;
678            for (_dst, rem) in ovlp.cone(local(p)) {
679                if rem.rank == owner {
680                    remote_point = rem.remote_point;
681                    break;
682                }
683            }
684            let remote_pt = remote_point.ok_or(MeshSieveError::OverlapLinkMissing(p, owner))?;
685            out.entry(owner).or_default().push((remote_pt, p));
686        }
687    }
688
689    if out.is_empty() {
690        return Err(MeshSieveError::MissingOverlap {
691            source: format!("rank {my_rank} has no neighbour links").into(),
692        });
693    }
694
695    Ok(out)
696}