Skip to main content

mesh_sieve/
dm.rs

1//! DMPLEX-like high-level mesh data-management facade.
2//!
3//! [`MeshDM`] is the feature-parity entry point for users coming from PETSc
4//! DMPLEX.  It does not replace the lower-level sieve, section, label,
5//! coordinate, distribution, and discretization modules; instead it owns those
6//! pieces together and provides a single orchestration surface for the common
7//! "build topology → attach data → validate → distribute → number sections →
8//! assemble vectors/matrices" workflow.
9
10use std::collections::{BTreeMap, BTreeSet, HashSet};
11
12use crate::adapt::{
13    AdaptationProvenanceMap, BoundaryRemeshingPolicy, FvStabilityThresholds,
14    MetricAdaptationAction, MetricAdaptationOptions, MetricAdaptationResult,
15    MetricRemeshingBackend, MetricSplitHint, MetricTensor, MetricThresholds,
16    adapt_with_metric_policy, select_cells_from_fvm_diagnostics, transfer_cell_types_refinement,
17    transfer_labels_refinement, transfer_ownership_refinement,
18};
19use crate::algs::communicator::Communicator;
20use crate::algs::distribute::{
21    CellPartitioner, DistributedMeshData, DistributionConfig, distribute_with_overlap,
22};
23use crate::algs::dual_graph::{DualGraph, build_dual};
24use crate::algs::point_sf::PointSF;
25use crate::algs::point_sf::{PointSfLeaf, RemotePoint};
26use crate::algs::renumber::{StratifiedOrdering, stratified_permutation};
27use crate::algs::submesh::{SubmeshMaps, SubmeshSelection, extract_by_label};
28use crate::data::atlas::Atlas;
29use crate::data::closure::{ClosureIndex, ClosureOrder, IdentitySectionSym, build_closure_index};
30use crate::data::coordinates::Coordinates;
31use crate::data::discretization::Discretization;
32use crate::data::global_map::{LocalToGlobalMap, global_vector_for_map};
33use crate::data::hanging_node_constraints::{
34    HangingNodeConstraints, apply_hanging_constraints_to_section,
35};
36use crate::data::multi_section::constrained_section_from_label_specs;
37use crate::data::section::Section;
38use crate::data::storage::{Storage, VecStorage};
39use crate::data::{ConstrainedSection, LabelConstraintSpec};
40use crate::diagnostics::{
41    FvmQualityDiagnostics, MeshCheckOptions, PrepareForSolveDiagnostics, PrepareForSolveOptions,
42    PrepareForSolvePreallocationDiagnostic, PrepareForSolveStepDiagnostic, fvm_cell_diagnostics,
43    fvm_quality_diagnostics, prepare_for_solve_prerequisites, run_mesh_checks,
44};
45use crate::geometry::fvm::build_fvm_face_metrics;
46use crate::io::MeshData;
47use crate::mesh_error::MeshSieveError;
48use crate::mesh_graph::{AdjacencyWeighting, MeshGraph, cell_adjacency_graph_with_cells};
49use crate::overlap::overlap::Overlap;
50use crate::physics::fe::{ElementClosureData, extract_oriented_element_closure};
51use crate::topology::anchors::TopologicalAnchors;
52use crate::topology::cache::InvalidateCache;
53use crate::topology::cell_type::CellType;
54use crate::topology::labels::LabelSet;
55use crate::topology::ownership::PointOwnership;
56use crate::topology::point::PointId;
57use crate::topology::refine::refine_mesh;
58use crate::topology::sieve::strata::compute_strata;
59use crate::topology::sieve::{MeshSieve, OrientedSieve, Sieve};
60
61/// Options for a DMPLEX-like setup pipeline.
62///
63/// The refinement counters are intentionally high-level policy markers.  They
64/// are stored with the DM so application-specific refinement hooks can inspect
65/// them; generic topology refinement remains available in lower-level modules
66/// because mesh-sieve supports several refinement representations.
67#[derive(Clone, Debug)]
68pub struct MeshDMOptions {
69    /// Number of pre-distribution refinement passes requested by the user.
70    pub pre_refine: usize,
71    /// Whether the setup pipeline is expected to distribute the DM.
72    pub distribute: bool,
73    /// Number of ghost layers to create during distribution.
74    pub distribute_overlap: usize,
75    /// Number of post-distribution refinement passes requested by the user.
76    pub post_refine: usize,
77    /// Ensure coordinate data is present on local/ghost points after distribution.
78    pub localize_coordinates: bool,
79    /// Run orientation/symmetry checks when cell-type metadata is available.
80    pub check_symmetry: bool,
81    /// Run stratum/skeleton construction checks.
82    pub check_skeleton: bool,
83    /// Run face adjacency construction checks.
84    pub check_faces: bool,
85    /// Validate coordinate geometry metadata when coordinates are present.
86    pub check_geometry: bool,
87    /// Enable all mesh checks in a DMPLEX-like single switch.
88    pub check_all: bool,
89    /// Reorder section atlas entries by topology strata during setup.
90    pub reorder_section: Option<StratifiedOrdering>,
91    /// Balance ownership of partition-boundary points during distribution.
92    pub balance_boundary_ownership: bool,
93    /// Synchronize section data onto ghost points during distribution.
94    pub synchronize_sections: bool,
95}
96
97impl Default for MeshDMOptions {
98    fn default() -> Self {
99        Self {
100            pre_refine: 0,
101            distribute: false,
102            distribute_overlap: 1,
103            post_refine: 0,
104            localize_coordinates: false,
105            check_symmetry: false,
106            check_skeleton: false,
107            check_faces: false,
108            check_geometry: false,
109            check_all: false,
110            reorder_section: None,
111            balance_boundary_ownership: false,
112            synchronize_sections: true,
113        }
114    }
115}
116
117impl MeshDMOptions {
118    /// Convert the distribution-related subset to the lower-level config type.
119    pub fn distribution_config(&self) -> DistributionConfig {
120        DistributionConfig {
121            overlap_depth: self.distribute_overlap,
122            synchronize_sections: self.synchronize_sections || self.localize_coordinates,
123            balance_boundary_ownership: self.balance_boundary_ownership,
124        }
125    }
126}
127
128/// Builder for [`MeshDM`].
129#[derive(Debug)]
130pub struct MeshDMBuilder<V, St = VecStorage<V>, CtSt = VecStorage<CellType>>
131where
132    St: Storage<V>,
133    CtSt: Storage<CellType>,
134{
135    mesh_data: MeshData<MeshSieve, V, St, CtSt>,
136    options: MeshDMOptions,
137}
138
139impl<V, St, CtSt> MeshDMBuilder<V, St, CtSt>
140where
141    V: Clone + Default,
142    St: Storage<V> + Clone,
143    CtSt: Storage<CellType> + Clone,
144{
145    /// Start a builder from an existing oriented mesh topology.
146    pub fn new(topology: MeshSieve) -> Self {
147        Self {
148            mesh_data: MeshData::new(topology),
149            options: MeshDMOptions::default(),
150        }
151    }
152
153    /// Start a builder from a full mesh-data container.
154    pub fn from_mesh_data(mesh_data: MeshData<MeshSieve, V, St, CtSt>) -> Self {
155        Self {
156            mesh_data,
157            options: MeshDMOptions::default(),
158        }
159    }
160
161    /// Replace the full options object.
162    pub fn options(mut self, options: MeshDMOptions) -> Self {
163        self.options = options;
164        self
165    }
166
167    /// Mutate options in-place.
168    pub fn configure(mut self, f: impl FnOnce(&mut MeshDMOptions)) -> Self {
169        f(&mut self.options);
170        self
171    }
172
173    /// Attach coordinate metadata.
174    pub fn coordinates(mut self, coordinates: Coordinates<V, St>) -> Self {
175        self.mesh_data.coordinates = Some(coordinates);
176        self
177    }
178
179    /// Attach labels.
180    pub fn labels(mut self, labels: LabelSet) -> Self {
181        self.mesh_data.labels = Some(labels);
182        self
183    }
184
185    /// Attach cell types.
186    pub fn cell_types(mut self, cell_types: Section<CellType, CtSt>) -> Self {
187        self.mesh_data.cell_types = Some(cell_types);
188        self
189    }
190
191    /// Attach discretization metadata.
192    pub fn discretization(mut self, discretization: Discretization) -> Self {
193        self.mesh_data.discretization = Some(discretization);
194        self
195    }
196
197    /// Attach a named scalar section.
198    pub fn section(mut self, name: impl Into<String>, section: Section<V, St>) -> Self {
199        self.mesh_data.sections.insert(name.into(), section);
200        self
201    }
202
203    /// Build the DM and run serial setup checks/reordering requested in options.
204    pub fn build(self) -> Result<MeshDM<V, St, CtSt>, MeshSieveError> {
205        let mut dm = MeshDM::from_mesh_data_with_options(self.mesh_data, self.options);
206        dm.setup_serial()?;
207        Ok(dm)
208    }
209}
210
211/// Minimal vector object created by a [`MeshDM`] section.
212#[derive(Clone, Debug, PartialEq)]
213pub struct MeshVector<V> {
214    /// Optional section name for solver diagnostics.
215    pub section: Option<String>,
216    /// Contiguous values in local or global numbering order.
217    pub values: Vec<V>,
218}
219
220/// DMPlex-style insertion policy for local-vector closure updates.
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub enum MeshVectorInsertMode {
223    /// Replace every scalar value in the selected closure.
224    InsertValues,
225    /// Accumulate every scalar value into the selected closure.
226    AddValues,
227}
228
229/// Numeric chart containing all topology point identifiers.
230///
231/// Unlike PETSc, mesh-sieve permits sparse point identifiers. `start` and
232/// `end_inclusive` therefore bound the identifiers while `point_count` and
233/// `is_dense` state whether the interval itself is a complete point set.
234#[derive(Clone, Copy, Debug, PartialEq, Eq)]
235pub struct MeshPointChart {
236    /// Smallest point identifier in the topology.
237    pub start: u64,
238    /// Largest point identifier in the topology.
239    pub end_inclusive: u64,
240    /// Number of points actually present in the topology.
241    pub point_count: usize,
242    /// Whether every identifier in the bounded interval is present.
243    pub is_dense: bool,
244}
245
246/// Matrix/preallocation graph derived from a DM adjacency graph and section map.
247#[derive(Clone, Debug, PartialEq, Eq)]
248pub struct PreallocationGraph {
249    /// CSR offsets into [`Self::adjncy`].
250    pub xadj: Vec<usize>,
251    /// CSR adjacency by point index.
252    pub adjncy: Vec<usize>,
253    /// Point ordering represented by CSR rows.
254    pub order: Vec<PointId>,
255    /// Number of point-neighbor blocks for each row.
256    pub row_nnz: Vec<usize>,
257}
258
259/// Label-driven point/submesh extraction policy for DM helper methods.
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct MeshDMLabelSelection {
262    /// Label name to query.
263    pub label_name: String,
264    /// Integer label value to query.
265    pub label_value: i32,
266    /// Topological subset to retain from labeled seed points.
267    pub topology: SubmeshSelection,
268    /// Optional named section whose atlas filters the selected points.
269    pub section: Option<String>,
270}
271
272impl MeshDMLabelSelection {
273    /// Select a label value and its full downward closure.
274    pub fn new(label_name: impl Into<String>, label_value: i32) -> Self {
275        Self {
276            label_name: label_name.into(),
277            label_value,
278            topology: SubmeshSelection::FullClosure,
279            section: None,
280        }
281    }
282
283    /// Override the topology subsetting policy.
284    pub fn topology(mut self, topology: SubmeshSelection) -> Self {
285        self.topology = topology;
286        self
287    }
288
289    /// Retain only selected points present in a named section atlas.
290    pub fn section(mut self, section: impl Into<String>) -> Self {
291        self.section = Some(section.into());
292        self
293    }
294}
295
296/// DM slice built by label/topology extraction.
297#[derive(Debug)]
298pub struct MeshDMSubmesh<V, St = VecStorage<V>, CtSt = VecStorage<CellType>>
299where
300    St: Storage<V>,
301    CtSt: Storage<CellType>,
302{
303    /// Extracted DM with compact point numbering.
304    pub dm: MeshDM<V, St, CtSt>,
305    /// Bidirectional parent/submesh point map.
306    pub maps: SubmeshMaps,
307}
308
309/// Distribution state owned by a [`MeshDM`].
310#[derive(Clone, Debug, Default, PartialEq, Eq)]
311pub struct MeshDMDistribution {
312    /// Point owners indexed by `PointId - 1`.
313    pub point_owners: Vec<usize>,
314    /// Cell partition assignment used to create this local DM.
315    pub cell_parts: Vec<usize>,
316    /// Rank that owns this local DM state.
317    pub rank: usize,
318    /// Communicator size at distribution time.
319    pub size: usize,
320}
321
322/// Strategy for transferring non-topological state after an adaptation pass.
323#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
324pub enum MeshDMTransferStrategy {
325    /// Keep only topology-level transfer information; user sections are untouched.
326    #[default]
327    TopologyOnly,
328    /// Keep and remap labels where a direct point mapping is available.
329    PreserveLabels,
330    /// Keep and remap coordinates and labels where direct maps are available.
331    PreserveCoordinatesAndLabels,
332    /// Preserve all DM-owned metadata that has explicit provenance maps.
333    PreserveAll,
334}
335
336/// Label-aware adaptation constraints for DM-level metric workflows.
337#[derive(Clone, Debug, Default)]
338pub struct MeshDMAdaptLabelPolicy {
339    pub fixed_boundary_labels: Vec<(String, i32)>,
340    pub protected_region_labels: Vec<(String, i32)>,
341    pub relax_region_labels: Vec<(String, i32)>,
342}
343
344/// Controls for a DM-level metric adaptation pass.
345#[derive(Clone, Debug)]
346pub struct MeshDMMetricAdaptOptions {
347    pub thresholds: MetricThresholds,
348    pub labels: MeshDMAdaptLabelPolicy,
349    pub transfer: MeshDMTransferStrategy,
350    pub normalization: crate::adapt::MetricNormalizationControls,
351    pub backend: MetricRemeshingBackend,
352}
353
354impl Default for MeshDMMetricAdaptOptions {
355    fn default() -> Self {
356        Self {
357            thresholds: MetricThresholds::default(),
358            labels: MeshDMAdaptLabelPolicy::default(),
359            transfer: MeshDMTransferStrategy::TopologyOnly,
360            normalization: crate::adapt::MetricNormalizationControls::default(),
361            backend: MetricRemeshingBackend::Internal,
362        }
363    }
364}
365
366/// Structured diagnostics emitted by DM-level adaptation.
367#[derive(Clone, Debug, Default)]
368pub struct MeshDMAdaptDiagnostics {
369    pub changed_cells: usize,
370    pub preserved_labels: usize,
371    pub failed_transfer_points: Vec<PointId>,
372    pub split_hint_cells: usize,
373    pub fvm_face_metrics_cached: usize,
374    pub boundary_faces: usize,
375    pub provenance_edges: usize,
376    pub transferred_sections: usize,
377    pub transferred_constraints: usize,
378    pub transferred_overlap_points: usize,
379}
380
381/// Result of a DM-level metric adaptation call.
382#[derive(Clone, Debug)]
383pub struct MeshDMMetricAdaptResult {
384    pub action: MetricAdaptationAction,
385    pub metrics: Vec<(PointId, crate::adapt::MetricCellMetrics)>,
386    pub diagnostics: MeshDMAdaptDiagnostics,
387    pub fv_selection: Option<(Vec<PointId>, Vec<PointId>)>,
388    pub provenance: AdaptationProvenanceMap,
389}
390
391#[derive(Clone, Debug, Default)]
392pub struct MeshDMAdaptIterationOptions {
393    pub max_iterations: usize,
394    pub stop_when_below_threshold: bool,
395}
396
397#[derive(Clone, Debug, Default)]
398pub struct MeshDMFvIterationReport {
399    pub pass_index: usize,
400    pub fvm_quality: Option<FvmQualityDiagnostics>,
401    pub refine_candidates: usize,
402    pub coarsen_candidates: usize,
403}
404
405/// Provenance SF maps tracking load/redistribute/section movement.
406#[derive(Clone, Debug, Default)]
407pub struct MeshDMProvenance<C: Communicator + Sync + 'static> {
408    pub load_map: Option<PointSF<'static, C>>,
409    pub redistribute_map: Option<PointSF<'static, C>>,
410    pub section_map: Option<PointSF<'static, C>>,
411    pub storage_version: Option<i32>,
412    pub permutation_source: Option<String>,
413    pub redistribution_map_id: Option<String>,
414    pub saved_rank_count: Option<usize>,
415    pub loaded_rank_count: Option<usize>,
416}
417
418impl<C> MeshDMProvenance<C>
419where
420    C: Communicator + Sync + 'static,
421{
422    /// Validate that load, redistribution, and section migration SFs form a coherent DMPlex pipeline.
423    pub fn validate_consistency(&self) -> Result<(), MeshSieveError> {
424        if matches!(
425            (self.saved_rank_count, self.loaded_rank_count),
426            (Some(0), _) | (_, Some(0))
427        ) {
428            return Err(MeshSieveError::MeshIoParse(
429                "DMPlex provenance rank counts must be non-zero".to_string(),
430            ));
431        }
432        if let (Some(saved), Some(loaded)) = (self.saved_rank_count, self.loaded_rank_count)
433            && saved != loaded
434            && self.redistribute_map.is_none()
435        {
436            return Err(MeshSieveError::MeshIoParse(format!(
437                "DMPlex provenance records save/load rank change {saved}->{loaded} without a redistribution SF"
438            )));
439        }
440
441        let loaded = self.loaded_rank_count;
442        if let Some(sf) = &self.load_map {
443            sf.validate_provenance_stage("load", loaded)?;
444        }
445        if let Some(sf) = &self.redistribute_map {
446            sf.validate_provenance_stage("redistribute", loaded)?;
447        }
448        if let Some(sf) = &self.section_map {
449            sf.validate_provenance_stage("section", loaded)?;
450        }
451        if self.section_map.is_some() && self.load_map.is_none() {
452            return Err(MeshSieveError::MeshIoParse(
453                "DMPlex provenance has a section SF but no load SF".to_string(),
454            ));
455        }
456        if let (Some(load), Some(section)) = (&self.load_map, &self.section_map) {
457            let load_range: BTreeSet<_> = load.leaves().map(|leaf| leaf.remote.point).collect();
458            let missing = section.missing_leaf_points(load_range);
459            if !missing.is_empty() {
460                return Err(MeshSieveError::MeshIoParse(format!(
461                    "section SF does not cover loaded points {:?}",
462                    missing
463                )));
464            }
465        }
466        Ok(())
467    }
468}
469
470impl<St, CtSt> MeshDM<f64, St, CtSt>
471where
472    St: Storage<f64> + Clone,
473    CtSt: Storage<CellType> + Clone,
474{
475    /// Adapt this DM from an attached per-cell metric section with label-aware constraints.
476    pub fn adapt_with_attached_metric<C>(
477        &mut self,
478        metric_section_name: &str,
479        metric: &Section<MetricTensor, VecStorage<MetricTensor>>,
480        coarsen_plan: C,
481        options: MeshDMMetricAdaptOptions,
482    ) -> Result<MeshDMMetricAdaptResult, MeshSieveError>
483    where
484        C: Fn(&[MetricSplitHint]) -> Vec<crate::topology::coarsen::CoarsenEntity>,
485    {
486        let coords = self.mesh_data.coordinates.as_ref().ok_or_else(|| {
487            MeshSieveError::MissingSectionName {
488                name: "coordinates".into(),
489            }
490        })?;
491        let cell_types = self.mesh_data.cell_types.as_ref().ok_or_else(|| {
492            MeshSieveError::MissingSectionName {
493                name: "cell_types".into(),
494            }
495        })?;
496        let _ = metric_section_name;
497
498        let boundary_policy = self.compose_boundary_policy(&options.labels);
499        let result: MetricAdaptationResult<()> = adapt_with_metric_policy(
500            &mut self.mesh_data.sieve,
501            cell_types,
502            coords,
503            metric,
504            self.mesh_data.labels.as_ref(),
505            coarsen_plan,
506            options.thresholds,
507            MetricAdaptationOptions {
508                boundary_policy,
509                normalization: options.normalization,
510                backend: options.backend,
511            },
512        )?;
513
514        let provenance = result.provenance.clone();
515        let diagnostics =
516            self.transfer_after_adaptation(&result.action, &provenance, options.transfer)?;
517        self.enforce_post_adaptation_fvm_contract()?;
518        Ok(MeshDMMetricAdaptResult {
519            action: result.action,
520            metrics: result.metrics,
521            diagnostics,
522            fv_selection: None,
523            provenance,
524        })
525    }
526
527    pub fn adapt_until_fv_thresholds<C>(
528        &mut self,
529        metric_section_name: &str,
530        metric: &Section<MetricTensor, VecStorage<MetricTensor>>,
531        coarsen_plan: C,
532        options: MeshDMMetricAdaptOptions,
533        fv_thresholds: FvStabilityThresholds,
534        iter: MeshDMAdaptIterationOptions,
535    ) -> Result<Vec<MeshDMMetricAdaptResult>, MeshSieveError>
536    where
537        C: Fn(&[MetricSplitHint]) -> Vec<crate::topology::coarsen::CoarsenEntity> + Copy,
538    {
539        let mut history = Vec::new();
540        for _ in 0..iter.max_iterations.max(1) {
541            let mut pass = self.adapt_with_attached_metric(
542                metric_section_name,
543                metric,
544                coarsen_plan,
545                options.clone(),
546            )?;
547            let coords = self.mesh_data.coordinates.as_ref().ok_or_else(|| {
548                MeshSieveError::MissingSectionName {
549                    name: "coordinates".into(),
550                }
551            })?;
552            let cell_types = self.mesh_data.cell_types.as_ref().ok_or_else(|| {
553                MeshSieveError::MissingSectionName {
554                    name: "cell_types".into(),
555                }
556            })?;
557            let cell_diags = fvm_cell_diagnostics(&self.mesh_data.sieve, cell_types, coords)?;
558            let selection = select_cells_from_fvm_diagnostics(&cell_diags, fv_thresholds);
559            pass.fv_selection = Some((
560                selection.refine_cells.clone(),
561                selection.coarsen_cells.clone(),
562            ));
563            history.push(pass.clone());
564            let stop_by_action = matches!(pass.action, MetricAdaptationAction::NoChange);
565            let stop_by_threshold = iter.stop_when_below_threshold
566                && selection.refine_cells.is_empty()
567                && selection.coarsen_cells.is_empty();
568            if stop_by_action || stop_by_threshold {
569                break;
570            }
571        }
572        Ok(history)
573    }
574
575    pub fn adapt_until_fv_thresholds_with_history<C>(
576        &mut self,
577        metric_section_name: &str,
578        metric: &Section<MetricTensor, VecStorage<MetricTensor>>,
579        coarsen_plan: C,
580        options: MeshDMMetricAdaptOptions,
581        fv_thresholds: FvStabilityThresholds,
582        iter: MeshDMAdaptIterationOptions,
583    ) -> Result<(Vec<MeshDMMetricAdaptResult>, Vec<MeshDMFvIterationReport>), MeshSieveError>
584    where
585        C: Fn(&[MetricSplitHint]) -> Vec<crate::topology::coarsen::CoarsenEntity> + Copy,
586    {
587        let mut history = Vec::new();
588        let mut reports = Vec::new();
589        for pass_idx in 0..iter.max_iterations.max(1) {
590            let mut pass = self.adapt_with_attached_metric(
591                metric_section_name,
592                metric,
593                coarsen_plan,
594                options.clone(),
595            )?;
596            self.refresh_fv_geometry_caches()?;
597            let coords = self.mesh_data.coordinates.as_ref().ok_or_else(|| {
598                MeshSieveError::MissingSectionName {
599                    name: "coordinates".into(),
600                }
601            })?;
602            let cell_types = self.mesh_data.cell_types.as_ref().ok_or_else(|| {
603                MeshSieveError::MissingSectionName {
604                    name: "cell_types".into(),
605                }
606            })?;
607            let quality = fvm_quality_diagnostics(&self.mesh_data.sieve, cell_types, coords).ok();
608            let cell_diags = fvm_cell_diagnostics(&self.mesh_data.sieve, cell_types, coords)?;
609            let selection = select_cells_from_fvm_diagnostics(&cell_diags, fv_thresholds);
610            pass.fv_selection = Some((
611                selection.refine_cells.clone(),
612                selection.coarsen_cells.clone(),
613            ));
614            reports.push(MeshDMFvIterationReport {
615                pass_index: pass_idx,
616                fvm_quality: quality,
617                refine_candidates: selection.refine_cells.len(),
618                coarsen_candidates: selection.coarsen_cells.len(),
619            });
620            history.push(pass.clone());
621            let stop_by_action = matches!(pass.action, MetricAdaptationAction::NoChange);
622            let stop_by_threshold = iter.stop_when_below_threshold
623                && selection.refine_cells.is_empty()
624                && selection.coarsen_cells.is_empty();
625            if stop_by_action || stop_by_threshold {
626                break;
627            }
628        }
629        Ok((history, reports))
630    }
631
632    /// Iteratively adapt until no action is taken or iteration budget is exhausted.
633    pub fn adapt_with_attached_metric_iterative<C, F>(
634        &mut self,
635        metric_section_name: &str,
636        metric: &Section<MetricTensor, VecStorage<MetricTensor>>,
637        coarsen_plan: C,
638        options: MeshDMMetricAdaptOptions,
639        max_iterations: usize,
640        stop: F,
641    ) -> Result<Vec<MeshDMMetricAdaptResult>, MeshSieveError>
642    where
643        C: Fn(&[MetricSplitHint]) -> Vec<crate::topology::coarsen::CoarsenEntity> + Copy,
644        F: Fn(&MeshDMMetricAdaptResult) -> bool,
645    {
646        let mut history = Vec::new();
647        for _ in 0..max_iterations {
648            let pass = self.adapt_with_attached_metric(
649                metric_section_name,
650                metric,
651                coarsen_plan,
652                options.clone(),
653            )?;
654            let done = matches!(pass.action, MetricAdaptationAction::NoChange) || stop(&pass);
655            history.push(pass);
656            if done {
657                break;
658            }
659        }
660        Ok(history)
661    }
662
663    fn compose_boundary_policy(&self, labels: &MeshDMAdaptLabelPolicy) -> BoundaryRemeshingPolicy {
664        let mut strata = labels.fixed_boundary_labels.clone();
665        strata.extend(labels.protected_region_labels.clone());
666        if strata.is_empty() {
667            BoundaryRemeshingPolicy::PreserveBoundary
668        } else {
669            BoundaryRemeshingPolicy::PreserveLabeled(strata)
670        }
671    }
672
673    fn transfer_after_adaptation(
674        &mut self,
675        action: &MetricAdaptationAction,
676        provenance: &AdaptationProvenanceMap,
677        strategy: MeshDMTransferStrategy,
678    ) -> Result<MeshDMAdaptDiagnostics, MeshSieveError> {
679        let changed_cells = match action {
680            MetricAdaptationAction::Refined { mesh } => mesh.cell_refinement.len(),
681            MetricAdaptationAction::Coarsened { mesh } => mesh.transfer_map.len(),
682            MetricAdaptationAction::NoChange => 0,
683        };
684        let preserved_labels = self
685            .mesh_data
686            .labels
687            .as_ref()
688            .map_or(0, |l| l.iter().count());
689        let coords = self.mesh_data.coordinates.as_ref();
690        let cell_types = self.mesh_data.cell_types.as_ref();
691        let (fvm_face_metrics_cached, boundary_faces) = if let (Some(coords), Some(cell_types)) =
692            (coords, cell_types)
693        {
694            let face_metrics = build_fvm_face_metrics(&self.mesh_data.sieve, cell_types, coords)?;
695            let bfaces = face_metrics.iter().filter(|m| m.neighbor.is_none()).count();
696            (face_metrics.len(), bfaces)
697        } else {
698            (0, 0)
699        };
700        let mut transferred_sections = 0;
701        let mut transferred_constraints = 0;
702        let mut transferred_overlap_points = 0;
703        let failed_transfer_points = Vec::new();
704
705        match action {
706            MetricAdaptationAction::Refined { mesh } => {
707                if matches!(
708                    strategy,
709                    MeshDMTransferStrategy::PreserveLabels
710                        | MeshDMTransferStrategy::PreserveCoordinatesAndLabels
711                        | MeshDMTransferStrategy::PreserveAll
712                ) && let Some(labels) = self.mesh_data.labels.as_ref()
713                {
714                    self.mesh_data.labels =
715                        Some(transfer_labels_refinement(labels, &mesh.cell_refinement));
716                }
717                if matches!(
718                    strategy,
719                    MeshDMTransferStrategy::PreserveCoordinatesAndLabels
720                        | MeshDMTransferStrategy::PreserveAll
721                ) && let Some(coords) = mesh.coordinates.clone()
722                {
723                    self.mesh_data.coordinates = Some(convert_coordinates_storage(coords)?);
724                }
725                if matches!(strategy, MeshDMTransferStrategy::PreserveAll) {
726                    if let Some(cell_types) = self.mesh_data.cell_types.as_ref() {
727                        self.mesh_data.cell_types = Some(convert_cell_type_section_storage(
728                            transfer_cell_types_refinement(cell_types, &mesh.cell_refinement)?,
729                        )?);
730                    }
731                    self.mesh_data.sections = transfer_dm_sections_refinement(
732                        &self.mesh_data.sections,
733                        &mesh.cell_refinement,
734                    )?;
735                    transferred_sections = self.mesh_data.sections.len();
736                    transferred_constraints = mesh.hanging_constraints.constraints().len();
737                    if let Some(ownership) = self.ownership.as_ref() {
738                        let rank = self.distribution.as_ref().map_or(0, |d| d.rank);
739                        self.ownership =
740                            Some(transfer_ownership_refinement(ownership, mesh, rank)?);
741                    }
742                    if let Some(overlap) = self.overlap.as_ref() {
743                        let (mapped, count) =
744                            transfer_overlap_refinement(overlap, &mesh.cell_refinement)?;
745                        self.overlap = Some(mapped);
746                        transferred_overlap_points = count;
747                    }
748                }
749                self.mesh_data.sieve = oriented_from_refined_sieve(&mesh.sieve);
750            }
751            MetricAdaptationAction::Coarsened { mesh } => {
752                if matches!(strategy, MeshDMTransferStrategy::PreserveAll) {
753                    self.mesh_data.sections = transfer_dm_sections_coarsening(
754                        &self.mesh_data.sections,
755                        &mesh.transfer_map,
756                    )?;
757                    transferred_sections = self.mesh_data.sections.len();
758                }
759                self.mesh_data.sieve = oriented_from_refined_sieve(&mesh.sieve);
760            }
761            MetricAdaptationAction::NoChange => {}
762        }
763
764        self.refresh_fv_geometry_caches()?;
765        let diagnostics = MeshDMAdaptDiagnostics {
766            changed_cells,
767            preserved_labels,
768            failed_transfer_points,
769            split_hint_cells: changed_cells,
770            fvm_face_metrics_cached,
771            boundary_faces,
772            provenance_edges: provenance
773                .old_to_new
774                .iter()
775                .map(|(_, points)| points.len())
776                .sum(),
777            transferred_sections,
778            transferred_constraints,
779            transferred_overlap_points,
780        };
781        Ok(diagnostics)
782    }
783
784    /// Enforce DM post-adaptation FV readiness before any further assembly/diagnostics.
785    ///
786    /// Contract:
787    /// 1. Invalidate stale topology-derived caches that may hold pre-adaptation face/cell state.
788    /// 2. Recompute face-loop classification from the adapted topology.
789    /// 3. Rebuild packed FV traversal buffers from the refreshed loop partition before assembly resumes.
790    fn enforce_post_adaptation_fvm_contract(&mut self) -> Result<(), MeshSieveError> {
791        self.mesh_data.sieve.invalidate_cache();
792        let _ = compute_strata(&self.mesh_data.sieve)?;
793        if let (Some(coords), Some(cell_types)) = (
794            self.mesh_data.coordinates.as_ref(),
795            self.mesh_data.cell_types.as_ref(),
796        ) {
797            let face_metrics = build_fvm_face_metrics(&self.mesh_data.sieve, cell_types, coords)?;
798            let faces = face_metrics.iter().map(|m| m.face);
799            let loops = crate::physics::fvm::classify_face_loops(&self.mesh_data.sieve, faces)?;
800            let mut packed = crate::physics::fvm::FvmInputs::new(
801                loops.internal.into_iter().chain(loops.boundary),
802                Vec::new(),
803                Vec::new(),
804            );
805            packed.build_packed_cache();
806        }
807        Ok(())
808    }
809
810    fn refresh_fv_geometry_caches(&mut self) -> Result<(), MeshSieveError> {
811        let _ = compute_strata(&self.mesh_data.sieve)?;
812        if let (Some(coords), Some(cell_types)) = (
813            self.mesh_data.coordinates.as_ref(),
814            self.mesh_data.cell_types.as_ref(),
815        ) {
816            let _ = build_fvm_face_metrics(&self.mesh_data.sieve, cell_types, coords)?;
817        }
818        Ok(())
819    }
820}
821
822fn convert_section_storage<V, Src, Dst>(
823    section: Section<V, Src>,
824) -> Result<Section<V, Dst>, MeshSieveError>
825where
826    V: Clone + Default,
827    Src: Storage<V>,
828    Dst: Storage<V> + Clone,
829{
830    let atlas = section.atlas().clone();
831    let mut out = Section::<V, Dst>::new(atlas);
832    for (point, slice) in section.iter() {
833        out.try_set(point, slice)?;
834    }
835    Ok(out)
836}
837
838fn convert_coordinates_storage<St>(
839    coords: Coordinates<f64, VecStorage<f64>>,
840) -> Result<Coordinates<f64, St>, MeshSieveError>
841where
842    St: Storage<f64> + Clone,
843{
844    let section = convert_section_storage::<f64, _, St>(coords.section().clone())?;
845    Coordinates::from_section(
846        coords.topological_dimension(),
847        coords.embedding_dimension(),
848        section,
849    )
850}
851
852fn convert_cell_type_section_storage<CtSt>(
853    section: Section<CellType, VecStorage<CellType>>,
854) -> Result<Section<CellType, CtSt>, MeshSieveError>
855where
856    CtSt: Storage<CellType> + Clone,
857{
858    convert_section_storage::<CellType, _, CtSt>(section)
859}
860
861fn transfer_section_refinement_as_section<St>(
862    section: &Section<f64, St>,
863    refinement: &[(PointId, Vec<(PointId, crate::topology::arrow::Polarity)>)],
864) -> Result<Section<f64, St>, MeshSieveError>
865where
866    St: Storage<f64> + Clone,
867{
868    let mut atlas = Atlas::default();
869    let mut values: Vec<(PointId, Vec<f64>)> = Vec::new();
870    for (old, new_points) in refinement {
871        if let Ok(slice) = section.try_restrict(*old) {
872            for (new_point, _) in new_points {
873                atlas.try_insert(*new_point, slice.len())?;
874                values.push((*new_point, slice.to_vec()));
875            }
876        }
877    }
878    let mut out = Section::<f64, St>::new(atlas);
879    for (point, slice) in values {
880        out.try_set(point, &slice)?;
881    }
882    Ok(out)
883}
884
885fn transfer_dm_sections_refinement<St>(
886    sections: &BTreeMap<String, Section<f64, St>>,
887    refinement: &[(PointId, Vec<(PointId, crate::topology::arrow::Polarity)>)],
888) -> Result<BTreeMap<String, Section<f64, St>>, MeshSieveError>
889where
890    St: Storage<f64> + Clone,
891{
892    let mut out = BTreeMap::new();
893    for (name, section) in sections {
894        out.insert(
895            name.clone(),
896            transfer_section_refinement_as_section(section, refinement)?,
897        );
898    }
899    Ok(out)
900}
901
902fn transfer_section_coarsening_as_section<St>(
903    section: &Section<f64, St>,
904    transfer: &[(PointId, Vec<(PointId, crate::topology::arrow::Polarity)>)],
905) -> Result<Section<f64, St>, MeshSieveError>
906where
907    St: Storage<f64> + Clone,
908{
909    let mut atlas = Atlas::default();
910    let mut values: Vec<(PointId, Vec<f64>)> = Vec::new();
911    for (new_point, old_points) in transfer {
912        let Some((first_old, _)) = old_points.first() else {
913            continue;
914        };
915        if let Ok(first) = section.try_restrict(*first_old) {
916            let mut accum = vec![0.0; first.len()];
917            let mut count = 0.0;
918            for (old, _) in old_points {
919                if let Ok(slice) = section.try_restrict(*old)
920                    && slice.len() == accum.len()
921                {
922                    for (a, v) in accum.iter_mut().zip(slice) {
923                        *a += *v;
924                    }
925                    count += 1.0;
926                }
927            }
928            if count > 0.0 {
929                for a in &mut accum {
930                    *a /= count;
931                }
932                atlas.try_insert(*new_point, accum.len())?;
933                values.push((*new_point, accum));
934            }
935        }
936    }
937    let mut out = Section::<f64, St>::new(atlas);
938    for (point, slice) in values {
939        out.try_set(point, &slice)?;
940    }
941    Ok(out)
942}
943
944fn transfer_dm_sections_coarsening<St>(
945    sections: &BTreeMap<String, Section<f64, St>>,
946    transfer: &[(PointId, Vec<(PointId, crate::topology::arrow::Polarity)>)],
947) -> Result<BTreeMap<String, Section<f64, St>>, MeshSieveError>
948where
949    St: Storage<f64> + Clone,
950{
951    let mut out = BTreeMap::new();
952    for (name, section) in sections {
953        out.insert(
954            name.clone(),
955            transfer_section_coarsening_as_section(section, transfer)?,
956        );
957    }
958    Ok(out)
959}
960
961fn oriented_from_refined_sieve(refined: &impl Sieve<Point = PointId>) -> MeshSieve {
962    let mut out = MeshSieve::default();
963    for src in refined.base_points() {
964        for dst in refined.cone_points(src) {
965            out.add_arrow(src, dst, ());
966        }
967    }
968    out
969}
970
971fn transfer_overlap_refinement(
972    overlap: &Overlap,
973    refinement: &[(PointId, Vec<(PointId, crate::topology::arrow::Polarity)>)],
974) -> Result<(Overlap, usize), MeshSieveError> {
975    let mut out = overlap.clone();
976    let mut count = 0;
977    for (old, new_points) in refinement {
978        for rank in overlap.neighbor_ranks() {
979            for (local, remote) in overlap.links_to(rank) {
980                if local == *old {
981                    for (new_point, _) in new_points {
982                        match remote {
983                            Some(remote_point) => {
984                                out.try_add_link(*new_point, rank, remote_point)?
985                            }
986                            None => {
987                                out.try_add_link_structural_one(*new_point, rank)?;
988                            }
989                        }
990                        count += 1;
991                    }
992                }
993            }
994        }
995    }
996    Ok((out, count))
997}
998
999/// DMPLEX-like facade owning topology, coordinates, labels, sections,
1000/// discretization metadata, distribution state, and solver numbering maps.
1001#[derive(Debug)]
1002pub struct MeshDM<V, St = VecStorage<V>, CtSt = VecStorage<CellType>>
1003where
1004    St: Storage<V>,
1005    CtSt: Storage<CellType>,
1006{
1007    mesh_data: MeshData<MeshSieve, V, St, CtSt>,
1008    options: MeshDMOptions,
1009    ownership: Option<PointOwnership>,
1010    overlap: Option<Overlap>,
1011    global_sections: BTreeMap<String, LocalToGlobalMap>,
1012    anchors: TopologicalAnchors,
1013    hanging_constraints: BTreeMap<String, HangingNodeConstraints<V>>,
1014    distribution: Option<MeshDMDistribution>,
1015    provenance_maps: MeshDMProvenance<crate::algs::communicator::NoComm>,
1016}
1017
1018impl<V, St, CtSt> MeshDM<V, St, CtSt>
1019where
1020    V: Clone + Default,
1021    St: Storage<V> + Clone,
1022    CtSt: Storage<CellType> + Clone,
1023{
1024    /// Create a builder from topology.
1025    pub fn builder(topology: MeshSieve) -> MeshDMBuilder<V, St, CtSt> {
1026        MeshDMBuilder::new(topology)
1027    }
1028
1029    /// Wrap existing mesh data with default DM options.
1030    pub fn from_mesh_data(mesh_data: MeshData<MeshSieve, V, St, CtSt>) -> Self {
1031        Self::from_mesh_data_with_options(mesh_data, MeshDMOptions::default())
1032    }
1033
1034    /// Wrap existing mesh data with explicit DM options.
1035    pub fn from_mesh_data_with_options(
1036        mesh_data: MeshData<MeshSieve, V, St, CtSt>,
1037        options: MeshDMOptions,
1038    ) -> Self {
1039        Self {
1040            mesh_data,
1041            options,
1042            ownership: None,
1043            overlap: None,
1044            global_sections: BTreeMap::new(),
1045            anchors: TopologicalAnchors::default(),
1046            hanging_constraints: BTreeMap::new(),
1047            distribution: None,
1048            provenance_maps: MeshDMProvenance::default(),
1049        }
1050    }
1051
1052    /// Consume the facade, returning the owned lower-level mesh data.
1053    pub fn into_mesh_data(self) -> MeshData<MeshSieve, V, St, CtSt> {
1054        self.mesh_data
1055    }
1056
1057    /// Borrow the full lower-level mesh data container.
1058    pub fn mesh_data(&self) -> &MeshData<MeshSieve, V, St, CtSt> {
1059        &self.mesh_data
1060    }
1061
1062    /// Borrow the topology.
1063    pub fn topology(&self) -> &MeshSieve {
1064        &self.mesh_data.sieve
1065    }
1066
1067    /// Mutably borrow the topology.
1068    pub fn topology_mut(&mut self) -> &mut MeshSieve {
1069        self.invalidate_numbering();
1070        &mut self.mesh_data.sieve
1071    }
1072
1073    /// Borrow labels, if present.
1074    pub fn labels(&self) -> Option<&LabelSet> {
1075        self.mesh_data.labels.as_ref()
1076    }
1077
1078    /// Borrow coordinates, if present.
1079    pub fn coordinates(&self) -> Option<&Coordinates<V, St>> {
1080        self.mesh_data.coordinates.as_ref()
1081    }
1082
1083    /// Mutably borrow coordinate metadata.
1084    pub fn coordinates_mut(&mut self) -> Option<&mut Coordinates<V, St>> {
1085        self.mesh_data.coordinates.as_mut()
1086    }
1087
1088    /// Attach or replace coordinate metadata.
1089    pub fn set_coordinates(
1090        &mut self,
1091        coordinates: Coordinates<V, St>,
1092    ) -> Option<Coordinates<V, St>> {
1093        self.mesh_data.coordinates.replace(coordinates)
1094    }
1095
1096    /// Borrow a named local section.
1097    pub fn section(&self, name: &str) -> Option<&Section<V, St>> {
1098        self.mesh_data.sections.get(name)
1099    }
1100
1101    /// Mutably borrow a named local section and invalidate its global numbering.
1102    pub fn section_mut(&mut self, name: &str) -> Option<&mut Section<V, St>> {
1103        self.global_sections.remove(name);
1104        self.mesh_data.sections.get_mut(name)
1105    }
1106
1107    /// Mutably borrow or create labels.
1108    pub fn labels_mut_or_insert(&mut self) -> &mut LabelSet {
1109        self.mesh_data.labels.get_or_insert_with(LabelSet::new)
1110    }
1111
1112    /// Insert or replace a named local section.
1113    pub fn insert_section(
1114        &mut self,
1115        name: impl Into<String>,
1116        section: Section<V, St>,
1117    ) -> Option<Section<V, St>> {
1118        let name = name.into();
1119        self.global_sections.remove(&name);
1120        self.mesh_data.sections.insert(name, section)
1121    }
1122
1123    /// Remove a named local section and any derived global numbering.
1124    pub fn remove_section(&mut self, name: &str) -> Option<Section<V, St>> {
1125        self.global_sections.remove(name);
1126        self.hanging_constraints.remove(name);
1127        self.mesh_data.sections.remove(name)
1128    }
1129
1130    /// Borrow cell type metadata, if present.
1131    pub fn cell_types(&self) -> Option<&Section<CellType, CtSt>> {
1132        self.mesh_data.cell_types.as_ref()
1133    }
1134
1135    /// Mutably borrow cell-type metadata.
1136    pub fn cell_types_mut(&mut self) -> Option<&mut Section<CellType, CtSt>> {
1137        self.mesh_data.cell_types.as_mut()
1138    }
1139
1140    /// Attach or replace cell-type metadata.
1141    pub fn set_cell_types(
1142        &mut self,
1143        cell_types: Section<CellType, CtSt>,
1144    ) -> Option<Section<CellType, CtSt>> {
1145        self.mesh_data.cell_types.replace(cell_types)
1146    }
1147
1148    /// Borrow discretization metadata, if present.
1149    pub fn discretization(&self) -> Option<&Discretization> {
1150        self.mesh_data.discretization.as_ref()
1151    }
1152
1153    /// Mutably borrow discretization metadata.
1154    pub fn discretization_mut(&mut self) -> Option<&mut Discretization> {
1155        self.mesh_data.discretization.as_mut()
1156    }
1157
1158    /// Attach or replace discretization metadata.
1159    pub fn set_discretization(&mut self, discretization: Discretization) -> Option<Discretization> {
1160        self.mesh_data.discretization.replace(discretization)
1161    }
1162
1163    /// Borrow ownership metadata, if this DM has been distributed or numbered.
1164    pub fn ownership(&self) -> Option<&PointOwnership> {
1165        self.ownership.as_ref()
1166    }
1167
1168    /// Borrow overlap/SF-like state, if this DM has been distributed.
1169    pub fn overlap(&self) -> Option<&Overlap> {
1170        self.overlap.as_ref()
1171    }
1172
1173    /// Borrow distribution metadata, if this DM has been distributed.
1174    pub fn distribution(&self) -> Option<&MeshDMDistribution> {
1175        self.distribution.as_ref()
1176    }
1177
1178    pub fn provenance_maps(&self) -> &MeshDMProvenance<crate::algs::communicator::NoComm> {
1179        &self.provenance_maps
1180    }
1181
1182    /// Borrow DM setup options.
1183    pub fn options(&self) -> &MeshDMOptions {
1184        &self.options
1185    }
1186
1187    /// Replace topological anchor metadata used by nonconforming setup flows.
1188    pub fn set_topological_anchors(&mut self, anchors: TopologicalAnchors) {
1189        self.anchors = anchors;
1190    }
1191
1192    /// Borrow topological anchor metadata for nonconforming/adapted points.
1193    pub fn topological_anchors(&self) -> &TopologicalAnchors {
1194        &self.anchors
1195    }
1196
1197    /// Attach hanging-node constraints to a named section.
1198    pub fn set_hanging_constraints_for_section(
1199        &mut self,
1200        section_name: impl Into<String>,
1201        constraints: HangingNodeConstraints<V>,
1202    ) {
1203        self.hanging_constraints
1204            .insert(section_name.into(), constraints);
1205    }
1206
1207    /// Borrow hanging-node constraints attached to a named section.
1208    pub fn hanging_constraints_for_section(
1209        &self,
1210        section_name: &str,
1211    ) -> Option<&HangingNodeConstraints<V>> {
1212        self.hanging_constraints.get(section_name)
1213    }
1214
1215    fn apply_hanging_constraints_to_sections(&mut self) -> Result<(), MeshSieveError>
1216    where
1217        V: core::ops::AddAssign + core::ops::Mul<Output = V>,
1218    {
1219        let constraints = self.hanging_constraints.clone();
1220        for (name, hanging) in constraints {
1221            let section = self
1222                .mesh_data
1223                .sections
1224                .get_mut(&name)
1225                .ok_or_else(|| MeshSieveError::MissingSectionName { name: name.clone() })?;
1226            apply_hanging_constraints_to_section(section, &hanging)?;
1227        }
1228        Ok(())
1229    }
1230
1231    /// Run local setup actions that do not require a partitioner/communicator.
1232    pub fn setup_serial(&mut self) -> Result<(), MeshSieveError> {
1233        self.run_refinement_passes(
1234            self.options.pre_refine,
1235            MeshDMTransferStrategy::PreserveCoordinatesAndLabels,
1236        )?;
1237        if let Some(ordering) = self.options.reorder_section {
1238            self.reorder_sections(ordering)?;
1239        }
1240        self.enforce_phase_invariants()?;
1241        self.run_requested_checks()?;
1242        self.run_refinement_passes(
1243            self.options.post_refine,
1244            MeshDMTransferStrategy::PreserveCoordinatesAndLabels,
1245        )?;
1246        self.enforce_phase_invariants()?;
1247        self.run_requested_checks()?;
1248        Ok(())
1249    }
1250
1251    fn run_refinement_passes(
1252        &mut self,
1253        passes: usize,
1254        strategy: MeshDMTransferStrategy,
1255    ) -> Result<(), MeshSieveError> {
1256        for _ in 0..passes {
1257            self.run_single_refinement_pass(strategy)?;
1258            self.enforce_phase_invariants()?;
1259            self.run_requested_checks()?;
1260        }
1261        Ok(())
1262    }
1263
1264    fn run_single_refinement_pass(
1265        &mut self,
1266        strategy: MeshDMTransferStrategy,
1267    ) -> Result<(), MeshSieveError> {
1268        let Some(cell_types) = self.mesh_data.cell_types.as_ref() else {
1269            return Ok(());
1270        };
1271        let refined = refine_mesh(&mut self.mesh_data.sieve, cell_types)?;
1272        self.mesh_data.sieve = refined.sieve;
1273        if matches!(
1274            strategy,
1275            MeshDMTransferStrategy::PreserveCoordinatesAndLabels
1276        ) {
1277            let _ = refined.coordinates;
1278        }
1279        if self.mesh_data.labels.is_some()
1280            && matches!(
1281                strategy,
1282                MeshDMTransferStrategy::PreserveLabels
1283                    | MeshDMTransferStrategy::PreserveCoordinatesAndLabels
1284            )
1285        {
1286            // labels are point-id keyed and existing points are preserved across refinement.
1287        }
1288        self.ensure_serial_ownership(0)?;
1289        if let Some(ownership) = &self.ownership {
1290            self.provenance_maps.section_map = Some(crate::algs::point_sf::create_process_sf::<
1291                crate::algs::communicator::NoComm,
1292            >(ownership, 0));
1293        }
1294        Ok(())
1295    }
1296
1297    fn enforce_phase_invariants(&mut self) -> Result<(), MeshSieveError> {
1298        let _ = compute_strata(&self.mesh_data.sieve)?;
1299        self.mesh_data.sieve.invalidate_cache();
1300        if self.options.reorder_section.is_none() {
1301            self.reorder_sections(StratifiedOrdering::CellFirst)?;
1302        }
1303        Ok(())
1304    }
1305
1306    fn closure_index_for_points(
1307        &self,
1308        point: PointId,
1309        order: &ClosureOrder,
1310    ) -> Result<ClosureIndex<i32>, MeshSieveError> {
1311        let mut atlas = Atlas::default();
1312        let mut closure_points: Vec<_> =
1313            self.mesh_data.sieve.closure_iter_sorted([point]).collect();
1314        closure_points.sort_unstable();
1315        closure_points.dedup();
1316        for point in closure_points {
1317            atlas.try_insert(point, 1)?;
1318        }
1319        let section = Section::<(), VecStorage<()>>::new(atlas);
1320        build_closure_index(
1321            &self.mesh_data.sieve,
1322            &section,
1323            point,
1324            0,
1325            order,
1326            &IdentitySectionSym,
1327        )
1328    }
1329
1330    /// Run topology/geometry checks requested by [`MeshDMOptions`].
1331    pub fn run_requested_checks(&mut self) -> Result<(), MeshSieveError> {
1332        let check_all = self.options.check_all;
1333        let check_options = MeshCheckOptions {
1334            check_symmetry: check_all || self.options.check_symmetry,
1335            check_skeleton: check_all || self.options.check_skeleton,
1336            check_faces: check_all || self.options.check_faces,
1337            check_geometry: check_all || self.options.check_geometry,
1338            check_overlap: check_all,
1339            check_ownership: check_all,
1340            check_sections: check_all,
1341        };
1342
1343        if check_options.check_faces {
1344            let cells = self.height_stratum(0)?;
1345            let _ = self.cell_adjacency_graph(cells, Default::default(), AdjacencyWeighting::None);
1346        }
1347
1348        run_mesh_checks(
1349            &mut self.mesh_data.sieve,
1350            self.mesh_data.cell_types.as_ref(),
1351            self.mesh_data.coordinates.as_ref(),
1352            self.ownership.as_ref(),
1353            self.overlap.as_ref(),
1354            self.mesh_data.sections.values(),
1355            check_options,
1356        )
1357    }
1358
1359    /// Return the numeric point chart and whether it is dense.
1360    pub fn point_chart(&self) -> Option<MeshPointChart> {
1361        let points = self.mesh_data.sieve.points_sorted();
1362        let start = points.first()?.get();
1363        let end_inclusive = points.last()?.get();
1364        let point_count = points.len();
1365        let interval_len = end_inclusive
1366            .checked_sub(start)
1367            .and_then(|width| width.checked_add(1));
1368        Some(MeshPointChart {
1369            start,
1370            end_inclusive,
1371            point_count,
1372            is_dense: interval_len == u64::try_from(point_count).ok(),
1373        })
1374    }
1375
1376    /// Return the immediate downward cone of a point in deterministic order.
1377    pub fn cone_points(&self, point: PointId) -> Vec<PointId> {
1378        let mut points: Vec<_> = self.mesh_data.sieve.cone_points(point).collect();
1379        points.sort_unstable();
1380        points
1381    }
1382
1383    /// Return the immediate downward cone with incidence orientations.
1384    pub fn oriented_cone(&self, point: PointId) -> Vec<(PointId, i32)> {
1385        let mut points: Vec<_> = self.mesh_data.sieve.cone_o(point).collect();
1386        points.sort_unstable_by_key(|(point, _)| *point);
1387        points
1388    }
1389
1390    /// Return the immediate upward support of a point in deterministic order.
1391    pub fn support_points(&self, point: PointId) -> Vec<PointId> {
1392        let mut points: Vec<_> = self.mesh_data.sieve.support_points(point).collect();
1393        points.sort_unstable();
1394        points
1395    }
1396
1397    /// Return the immediate upward support with forward-incidence orientations.
1398    pub fn oriented_support(&self, point: PointId) -> Vec<(PointId, i32)> {
1399        let mut points: Vec<_> = self.mesh_data.sieve.support_o(point).collect();
1400        points.sort_unstable_by_key(|(point, _)| *point);
1401        points
1402    }
1403
1404    /// Return the number of immediate points in a point's downward cone.
1405    pub fn cone_size(&self, point: PointId) -> usize {
1406        self.mesh_data.sieve.cone_points(point).count()
1407    }
1408
1409    /// Return the number of immediate points in a point's upward support.
1410    pub fn support_size(&self, point: PointId) -> usize {
1411        self.mesh_data.sieve.support_points(point).count()
1412    }
1413
1414    /// Return points in a height stratum (height 0 are cells in DMPLEX terms).
1415    pub fn height_stratum(&self, height: u32) -> Result<Vec<PointId>, MeshSieveError> {
1416        let strata = compute_strata(&self.mesh_data.sieve)?;
1417        Ok(strata
1418            .strata
1419            .get(height as usize)
1420            .cloned()
1421            .unwrap_or_default())
1422    }
1423
1424    /// Return points in a depth stratum (depth 0 are vertices in DMPLEX terms).
1425    pub fn depth_stratum(&self, depth: u32) -> Result<Vec<PointId>, MeshSieveError> {
1426        let strata = compute_strata(&self.mesh_data.sieve)?;
1427        let mut points: Vec<_> = strata
1428            .depth
1429            .iter()
1430            .filter_map(|(&point, &d)| (d == depth).then_some(point))
1431            .collect();
1432        points.sort_unstable();
1433        Ok(points)
1434    }
1435
1436    /// Create and register a section from DOF counts indexed by point depth.
1437    ///
1438    /// `dofs_by_depth[d]` is the number of scalar DOFs assigned to every point
1439    /// in depth stratum `d`; zero entries omit that stratum from the section.
1440    pub fn create_section_from_depth(
1441        &mut self,
1442        name: impl Into<String>,
1443        dofs_by_depth: &[usize],
1444    ) -> Result<Option<Section<V, St>>, MeshSieveError> {
1445        self.create_section_from_depth_impl(name.into(), dofs_by_depth, None)
1446    }
1447
1448    /// Create and register a depth-layout section restricted to a label value.
1449    pub fn create_section_from_depth_on_label(
1450        &mut self,
1451        name: impl Into<String>,
1452        dofs_by_depth: &[usize],
1453        label_name: &str,
1454        label_value: i32,
1455    ) -> Result<Option<Section<V, St>>, MeshSieveError> {
1456        let labels = self
1457            .labels()
1458            .ok_or_else(|| MeshSieveError::MissingSectionName {
1459                name: "labels".to_string(),
1460            })?;
1461        let support: HashSet<_> = labels.points_with_label(label_name, label_value).collect();
1462        self.create_section_from_depth_impl(name.into(), dofs_by_depth, Some(&support))
1463    }
1464
1465    fn create_section_from_depth_impl(
1466        &mut self,
1467        name: String,
1468        dofs_by_depth: &[usize],
1469        support: Option<&HashSet<PointId>>,
1470    ) -> Result<Option<Section<V, St>>, MeshSieveError> {
1471        let strata = compute_strata(&self.mesh_data.sieve)?;
1472        let mut entries: Vec<_> = strata.depth.into_iter().collect();
1473        entries.sort_unstable_by_key(|(point, _)| *point);
1474        let mut atlas = Atlas::default();
1475        for (point, depth) in entries {
1476            if support.is_some_and(|points| !points.contains(&point)) {
1477                continue;
1478            }
1479            let dofs = dofs_by_depth.get(depth as usize).copied().unwrap_or(0);
1480            if dofs > 0 {
1481                atlas.try_insert(point, dofs)?;
1482            }
1483        }
1484        self.global_sections.remove(&name);
1485        Ok(self.mesh_data.sections.insert(name, Section::new(atlas)))
1486    }
1487
1488    /// Return an FE-compatible point order for the downward transitive closure of `point`.
1489    pub fn closure_points(
1490        &self,
1491        point: PointId,
1492        order: &ClosureOrder,
1493    ) -> Result<Vec<PointId>, MeshSieveError> {
1494        Ok(self
1495            .closure_index_for_points(point, order)?
1496            .point_order()
1497            .collect())
1498    }
1499
1500    /// Alias for [`Self::closure_points`] mirroring DMPLEX transitive-closure terminology.
1501    pub fn transitive_closure_points(
1502        &self,
1503        point: PointId,
1504        order: &ClosureOrder,
1505    ) -> Result<Vec<PointId>, MeshSieveError> {
1506        self.closure_points(point, order)
1507    }
1508
1509    /// Return closure points lying in one global topology depth stratum.
1510    pub fn closure_points_at_depth(
1511        &self,
1512        point: PointId,
1513        depth: u32,
1514        order: &ClosureOrder,
1515    ) -> Result<Vec<PointId>, MeshSieveError> {
1516        let strata = compute_strata(&self.mesh_data.sieve)?;
1517        Ok(self
1518            .closure_index_for_points(point, order)?
1519            .point_order()
1520            .filter(|closure_point| strata.depth.get(closure_point).copied() == Some(depth))
1521            .collect())
1522    }
1523
1524    /// Return a deterministic upward star from `point`.
1525    pub fn star_points(&self, point: PointId) -> Vec<PointId> {
1526        let mut points: Vec<_> = self.mesh_data.sieve.star_iter_sorted([point]).collect();
1527        points.sort_unstable();
1528        points.dedup();
1529        points
1530    }
1531
1532    /// Return a deterministic downward/upward transitive set from `point`.
1533    pub fn closure_both_points(&self, point: PointId) -> Vec<PointId> {
1534        let mut points: Vec<_> = self
1535            .mesh_data
1536            .sieve
1537            .closure_both_iter_sorted([point])
1538            .collect();
1539        points.sort_unstable();
1540        points.dedup();
1541        points
1542    }
1543
1544    /// Extract section values over a cell closure using the same ordering as FE assembly helpers.
1545    pub fn get_closure_values(
1546        &self,
1547        section_name: &str,
1548        cell: PointId,
1549        order: &ClosureOrder,
1550    ) -> Result<ElementClosureData<V>, MeshSieveError> {
1551        let section = self.mesh_data.sections.get(section_name).ok_or_else(|| {
1552            MeshSieveError::MissingSectionName {
1553                name: section_name.to_string(),
1554            }
1555        })?;
1556        let global_map = self.global_sections.get(section_name);
1557        extract_oriented_element_closure(
1558            &self.mesh_data.sieve,
1559            section,
1560            global_map,
1561            cell,
1562            0,
1563            order,
1564            &IdentitySectionSym,
1565        )
1566    }
1567
1568    /// Return points selected by a label, topology policy, and optional section-atlas filter.
1569    pub fn points_by_label_selection(
1570        &self,
1571        selection: &MeshDMLabelSelection,
1572    ) -> Result<Vec<PointId>, MeshSieveError> {
1573        let labels = self
1574            .labels()
1575            .ok_or_else(|| MeshSieveError::MissingSectionName {
1576                name: "labels".to_string(),
1577            })?;
1578        let seeds: Vec<PointId> = labels
1579            .points_with_label(&selection.label_name, selection.label_value)
1580            .collect();
1581        let mut points = match selection.topology {
1582            SubmeshSelection::FullClosure => {
1583                self.mesh_data.sieve.closure_iter_sorted(seeds).collect()
1584            }
1585            SubmeshSelection::ClosureDepth(depth) => {
1586                crate::algs::traversal::TraversalBuilder::new(&self.mesh_data.sieve)
1587                    .seeds(seeds)
1588                    .dir(crate::algs::traversal::Dir::Down)
1589                    .max_depth(Some(depth))
1590                    .run()
1591            }
1592            SubmeshSelection::TargetStratum { axis, index } => {
1593                let closure: Vec<PointId> =
1594                    self.mesh_data.sieve.closure_iter_sorted(seeds).collect();
1595                let strata = compute_strata(&self.mesh_data.sieve)?;
1596                let stratum_map = match axis {
1597                    crate::topology::sieve::strata::StratumAxis::Height => &strata.height,
1598                    crate::topology::sieve::strata::StratumAxis::Depth => &strata.depth,
1599                };
1600                let target = closure
1601                    .into_iter()
1602                    .filter(|p| stratum_map.get(p).copied() == Some(index));
1603                self.mesh_data.sieve.closure_iter_sorted(target).collect()
1604            }
1605        };
1606        if let Some(section_name) = &selection.section {
1607            let section = self.mesh_data.sections.get(section_name).ok_or_else(|| {
1608                MeshSieveError::MissingSectionName {
1609                    name: section_name.clone(),
1610                }
1611            })?;
1612            points.retain(|point| section.atlas().contains(*point));
1613        }
1614        points.sort_unstable();
1615        points.dedup();
1616        Ok(points)
1617    }
1618
1619    /// Convenience label query for points that also have DOFs in a named section.
1620    pub fn points_by_label_in_section(
1621        &self,
1622        label_name: &str,
1623        label_value: i32,
1624        section_name: &str,
1625        topology: SubmeshSelection,
1626    ) -> Result<Vec<PointId>, MeshSieveError> {
1627        self.points_by_label_selection(
1628            &MeshDMLabelSelection::new(label_name, label_value)
1629                .topology(topology)
1630                .section(section_name),
1631        )
1632    }
1633
1634    /// Create a constrained view of an existing section, deriving point DOFs from its atlas.
1635    pub fn create_constrained_view_from_labels(
1636        &self,
1637        section_name: &str,
1638        constraints: &[LabelConstraintSpec],
1639    ) -> Result<ConstrainedSection<V, St>, MeshSieveError> {
1640        let section = self.mesh_data.sections.get(section_name).ok_or_else(|| {
1641            MeshSieveError::MissingSectionName {
1642                name: section_name.to_string(),
1643            }
1644        })?;
1645        let point_dofs: Vec<_> = section
1646            .atlas()
1647            .points()
1648            .filter_map(|point| section.atlas().get(point).map(|(_, len)| (point, len)))
1649            .collect();
1650        self.create_constrained_section_from_labels(section_name, &point_dofs, constraints)
1651    }
1652
1653    /// Build a compact sub-DM from labeled points, preserving parent/sub point maps and numbering metadata.
1654    pub fn sub_dm_by_label(
1655        &self,
1656        label_name: &str,
1657        label_value: i32,
1658        topology: SubmeshSelection,
1659    ) -> Result<MeshDMSubmesh<V, St, CtSt>, MeshSieveError> {
1660        self.sub_dm_by_label_with_sections(label_name, label_value, topology, None)
1661    }
1662
1663    /// Build a compact sub-DM from labeled points and retain only selected named sections when requested.
1664    pub fn sub_dm_by_label_with_sections(
1665        &self,
1666        label_name: &str,
1667        label_value: i32,
1668        topology: SubmeshSelection,
1669        section_names: Option<&[&str]>,
1670    ) -> Result<MeshDMSubmesh<V, St, CtSt>, MeshSieveError> {
1671        let labels = self
1672            .labels()
1673            .ok_or_else(|| MeshSieveError::MissingSectionName {
1674                name: "labels".to_string(),
1675            })?;
1676        let (mut mesh_data, maps) =
1677            extract_by_label(&self.mesh_data, labels, label_name, label_value, topology)?;
1678        if let Some(names) = section_names {
1679            let keep: HashSet<&str> = names.iter().copied().collect();
1680            mesh_data
1681                .sections
1682                .retain(|name, _| keep.contains(name.as_str()));
1683        }
1684        let mut dm = MeshDM::from_mesh_data_with_options(mesh_data, self.options.clone());
1685        dm.ownership = remap_ownership_to_submesh(self.ownership.as_ref(), &maps)?;
1686        dm.global_sections = remap_global_sections_to_submesh(&self.global_sections, &maps)?;
1687        dm.anchors = self.anchors.clone();
1688        dm.hanging_constraints = self.hanging_constraints.clone();
1689        dm.provenance_maps = MeshDMProvenance {
1690            load_map: None,
1691            redistribute_map: None,
1692            section_map: Some(point_sf_for_submesh_maps(&maps)),
1693            storage_version: self.provenance_maps.storage_version,
1694            permutation_source: Some("sub_dm_by_label".to_string()),
1695            redistribution_map_id: self.provenance_maps.redistribution_map_id.clone(),
1696            saved_rank_count: self.provenance_maps.saved_rank_count,
1697            loaded_rank_count: self.provenance_maps.loaded_rank_count,
1698        };
1699        Ok(MeshDMSubmesh { dm, maps })
1700    }
1701
1702    /// Build a dual graph for the provided cells.
1703    pub fn dual_graph(&self, cells: impl IntoIterator<Item = PointId>) -> DualGraph {
1704        build_dual(&self.mesh_data.sieve, cells)
1705    }
1706
1707    /// Build a cell adjacency/preallocation graph for the provided cells.
1708    pub fn cell_adjacency_graph(
1709        &self,
1710        cells: impl IntoIterator<Item = PointId>,
1711        opts: crate::algs::adjacency_graph::CellAdjacencyOpts,
1712        weighting: AdjacencyWeighting,
1713    ) -> MeshGraph {
1714        cell_adjacency_graph_with_cells(&self.mesh_data.sieve, cells, opts, weighting)
1715    }
1716
1717    /// Build a matrix preallocation graph from cell adjacency.
1718    pub fn matrix_preallocation_graph(
1719        &self,
1720        cells: impl IntoIterator<Item = PointId>,
1721        opts: crate::algs::adjacency_graph::CellAdjacencyOpts,
1722    ) -> PreallocationGraph {
1723        let graph = self.cell_adjacency_graph(cells, opts, AdjacencyWeighting::None);
1724        let row_nnz = graph
1725            .xadj
1726            .windows(2)
1727            .map(|w| w[1].saturating_sub(w[0]))
1728            .collect();
1729        PreallocationGraph {
1730            xadj: graph.xadj,
1731            adjncy: graph.adjncy,
1732            order: graph.order,
1733            row_nnz,
1734        }
1735    }
1736
1737    /// Prepare this DM for solver assembly with a DMPLEX-like setup flow.
1738    ///
1739    /// The flow is intentionally deterministic: prerequisite diagnostics,
1740    /// section/global numbering, matrix preallocation, ownership/overlap
1741    /// validation, and optional ghost-section synchronization are all reported
1742    /// in stable point/name order. Missing required prerequisites are returned
1743    /// as structured diagnostics instead of panicking or partially preparing the
1744    /// DM.
1745    pub fn prepare_for_solve<C>(
1746        &mut self,
1747        comm: &C,
1748        options: PrepareForSolveOptions,
1749    ) -> Result<PrepareForSolveDiagnostics, MeshSieveError>
1750    where
1751        C: Communicator + Sync,
1752        V: Send
1753            + PartialEq
1754            + bytemuck::Pod
1755            + 'static
1756            + core::ops::AddAssign
1757            + core::ops::Mul<Output = V>,
1758    {
1759        self.apply_hanging_constraints_to_sections()?;
1760
1761        if options.create_serial_ownership && comm.size() == 1 {
1762            self.ensure_serial_ownership(comm.rank())?;
1763        }
1764
1765        let prerequisites = prepare_for_solve_prerequisites(
1766            &self.mesh_data.sieve,
1767            self.mesh_data.coordinates.as_ref(),
1768            self.mesh_data.cell_types.as_ref(),
1769            self.ownership.as_ref(),
1770            self.overlap.as_ref(),
1771            options,
1772        )?;
1773        let mut diagnostics = PrepareForSolveDiagnostics {
1774            ready: false,
1775            prerequisites,
1776            ..PrepareForSolveDiagnostics::default()
1777        };
1778
1779        if diagnostics.has_missing_required_prerequisites() {
1780            diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1781                name: "section_global_numbering",
1782                status: "skipped",
1783                detail: "required prerequisites are missing".to_string(),
1784            });
1785            diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1786                name: "matrix_preallocation_graph",
1787                status: "skipped",
1788                detail: "required prerequisites are missing".to_string(),
1789            });
1790            diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1791                name: "ownership_overlap_checks",
1792                status: "skipped",
1793                detail: "required prerequisites are missing".to_string(),
1794            });
1795            diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1796                name: "section_synchronization",
1797                status: "skipped",
1798                detail: "required prerequisites are missing".to_string(),
1799            });
1800            return Ok(diagnostics);
1801        }
1802
1803        self.build_global_sections(comm)?;
1804        diagnostics.global_sections = self.global_sections.keys().cloned().collect();
1805        diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1806            name: "section_global_numbering",
1807            status: "completed",
1808            detail: format!("numbered_sections={}", diagnostics.global_sections.len()),
1809        });
1810
1811        let cells = self.height_stratum(0)?;
1812        let preallocation = self.matrix_preallocation_graph(cells, Default::default());
1813        diagnostics.preallocation = Some(PrepareForSolvePreallocationDiagnostic {
1814            rows: preallocation.order.len(),
1815            edges: preallocation.adjncy.len(),
1816            order: preallocation.order,
1817            row_nnz: preallocation.row_nnz,
1818        });
1819        diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1820            name: "matrix_preallocation_graph",
1821            status: "completed",
1822            detail: diagnostics
1823                .preallocation
1824                .as_ref()
1825                .map(|p| format!("rows={}, edges={}", p.rows, p.edges))
1826                .unwrap_or_else(|| "rows=0, edges=0".to_string()),
1827        });
1828
1829        run_mesh_checks(
1830            &mut self.mesh_data.sieve,
1831            self.mesh_data.cell_types.as_ref(),
1832            self.mesh_data.coordinates.as_ref(),
1833            self.ownership.as_ref(),
1834            self.overlap.as_ref(),
1835            self.mesh_data.sections.values(),
1836            MeshCheckOptions {
1837                check_symmetry: true,
1838                check_skeleton: true,
1839                check_faces: true,
1840                check_geometry: true,
1841                check_overlap: true,
1842                check_ownership: true,
1843                check_sections: true,
1844            },
1845        )?;
1846        diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1847            name: "ownership_overlap_checks",
1848            status: "completed",
1849            detail: "ownership and overlap topology are consistent".to_string(),
1850        });
1851
1852        if options.synchronize_ghost_sections {
1853            let has_overlap = self.overlap.is_some();
1854            let has_ownership = self.ownership.is_some();
1855            if has_overlap && has_ownership {
1856                let mut synchronized = Vec::new();
1857                if self.mesh_data.coordinates.is_some() {
1858                    synchronized.push("coordinates".to_string());
1859                }
1860                synchronized.extend(self.mesh_data.sections.keys().cloned());
1861                self.distribute_fields(comm)?;
1862                diagnostics.synchronized_sections = synchronized;
1863                diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1864                    name: "section_synchronization",
1865                    status: "completed",
1866                    detail: format!(
1867                        "synchronized_sections={}",
1868                        diagnostics.synchronized_sections.len()
1869                    ),
1870                });
1871            } else {
1872                diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1873                    name: "section_synchronization",
1874                    status: "skipped",
1875                    detail: "no overlap/ownership state available for ghost synchronization"
1876                        .to_string(),
1877                });
1878            }
1879        } else {
1880            diagnostics.steps.push(PrepareForSolveStepDiagnostic {
1881                name: "section_synchronization",
1882                status: "skipped",
1883                detail: "ghost section synchronization disabled".to_string(),
1884            });
1885        }
1886
1887        diagnostics.ready = true;
1888        Ok(diagnostics)
1889    }
1890
1891    /// Create a zero-initialized local vector matching a named section.
1892    pub fn create_local_vector(&self, section_name: &str) -> Result<MeshVector<V>, MeshSieveError> {
1893        let section = self.mesh_data.sections.get(section_name).ok_or_else(|| {
1894            MeshSieveError::MissingSectionName {
1895                name: section_name.to_string(),
1896            }
1897        })?;
1898        Ok(MeshVector {
1899            section: Some(section_name.to_string()),
1900            values: vec![V::default(); section.atlas().total_len()],
1901        })
1902    }
1903
1904    /// Gather values from a local vector over a point's oriented closure.
1905    pub fn get_local_vector_closure(
1906        &self,
1907        section_name: &str,
1908        vector: &MeshVector<V>,
1909        point: PointId,
1910        order: &ClosureOrder,
1911    ) -> Result<Vec<V>, MeshSieveError> {
1912        let (section, index) =
1913            self.local_vector_closure_index(section_name, vector, point, order)?;
1914        let mut values = Vec::with_capacity(index.len);
1915        for entry in &index.points {
1916            let (offset, len) = section
1917                .atlas()
1918                .get(entry.point)
1919                .ok_or(MeshSieveError::PointNotInAtlas(entry.point))?;
1920            let point_values = vector.values.get(offset..offset + len).ok_or(
1921                MeshSieveError::ScatterLengthMismatch {
1922                    expected: section.atlas().total_len(),
1923                    found: vector.values.len(),
1924                },
1925            )?;
1926            for &slot in &entry.permutation {
1927                values.push(point_values[slot].clone());
1928            }
1929        }
1930        Ok(values)
1931    }
1932
1933    /// Gather local-vector closure values contributed by one topology depth.
1934    pub fn get_local_vector_closure_at_depth(
1935        &self,
1936        section_name: &str,
1937        vector: &MeshVector<V>,
1938        point: PointId,
1939        depth: u32,
1940        order: &ClosureOrder,
1941    ) -> Result<Vec<V>, MeshSieveError> {
1942        let strata = compute_strata(&self.mesh_data.sieve)?;
1943        let (section, index) =
1944            self.local_vector_closure_index(section_name, vector, point, order)?;
1945        let mut values = Vec::new();
1946        for entry in &index.points {
1947            if strata.depth.get(&entry.point).copied() != Some(depth) {
1948                continue;
1949            }
1950            let (offset, len) = section
1951                .atlas()
1952                .get(entry.point)
1953                .ok_or(MeshSieveError::PointNotInAtlas(entry.point))?;
1954            let point_values = &vector.values[offset..offset + len];
1955            values.extend(
1956                entry
1957                    .permutation
1958                    .iter()
1959                    .map(|&slot| point_values[slot].clone()),
1960            );
1961        }
1962        Ok(values)
1963    }
1964
1965    /// Insert or add values over a point's oriented closure in a local vector.
1966    pub fn set_local_vector_closure(
1967        &self,
1968        section_name: &str,
1969        vector: &mut MeshVector<V>,
1970        point: PointId,
1971        order: &ClosureOrder,
1972        values: &[V],
1973        mode: MeshVectorInsertMode,
1974    ) -> Result<(), MeshSieveError>
1975    where
1976        V: core::ops::AddAssign<V>,
1977    {
1978        let (section, index) =
1979            self.local_vector_closure_index(section_name, vector, point, order)?;
1980        if values.len() != index.len {
1981            return Err(MeshSieveError::ScatterLengthMismatch {
1982                expected: index.len,
1983                found: values.len(),
1984            });
1985        }
1986        for entry in &index.points {
1987            let (offset, len) = section
1988                .atlas()
1989                .get(entry.point)
1990                .ok_or(MeshSieveError::PointNotInAtlas(entry.point))?;
1991            let expected = section.atlas().total_len();
1992            let found = vector.values.len();
1993            let point_values = vector
1994                .values
1995                .get_mut(offset..offset + len)
1996                .ok_or(MeshSieveError::ScatterLengthMismatch { expected, found })?;
1997            for (local_slot, &section_slot) in entry.permutation.iter().enumerate() {
1998                let value = values[entry.offset + local_slot].clone();
1999                match mode {
2000                    MeshVectorInsertMode::InsertValues => point_values[section_slot] = value,
2001                    MeshVectorInsertMode::AddValues => point_values[section_slot] += value,
2002                }
2003            }
2004        }
2005        Ok(())
2006    }
2007
2008    fn local_vector_closure_index<'a>(
2009        &'a self,
2010        section_name: &str,
2011        vector: &MeshVector<V>,
2012        point: PointId,
2013        order: &ClosureOrder,
2014    ) -> Result<(&'a Section<V, St>, ClosureIndex<i32>), MeshSieveError> {
2015        if vector.section.as_deref() != Some(section_name) {
2016            return Err(MeshSieveError::VectorSectionMismatch {
2017                expected: section_name.to_string(),
2018                found: vector.section.clone(),
2019            });
2020        }
2021        let section = self.mesh_data.sections.get(section_name).ok_or_else(|| {
2022            MeshSieveError::MissingSectionName {
2023                name: section_name.to_string(),
2024            }
2025        })?;
2026        if vector.values.len() != section.atlas().total_len() {
2027            return Err(MeshSieveError::ScatterLengthMismatch {
2028                expected: section.atlas().total_len(),
2029                found: vector.values.len(),
2030            });
2031        }
2032        let index = build_closure_index(
2033            &self.mesh_data.sieve,
2034            section,
2035            point,
2036            0,
2037            order,
2038            &IdentitySectionSym,
2039        )?;
2040        Ok((section, index))
2041    }
2042
2043    /// Build and store global numbering maps for all named local sections.
2044    pub fn build_global_sections<C>(&mut self, comm: &C) -> Result<(), MeshSieveError>
2045    where
2046        C: Communicator + Sync,
2047    {
2048        self.ensure_serial_ownership(comm.rank())?;
2049        let ownership = self.ownership.as_ref().expect("ownership inserted");
2050        let empty_overlap = Overlap::default();
2051        let overlap = self.overlap.as_ref().unwrap_or(&empty_overlap);
2052        let mut maps = BTreeMap::new();
2053        for (name, section) in &self.mesh_data.sections {
2054            let map = if let Some(hanging) = self.hanging_constraints.get(name) {
2055                let mask = hanging.to_dof_constraint_mask(V::default());
2056                LocalToGlobalMap::from_section_with_constraints_and_ownership(
2057                    section,
2058                    &mask,
2059                    overlap,
2060                    ownership,
2061                    comm,
2062                    comm.rank(),
2063                    crate::algs::communicator::SectionCommTags::from_base(
2064                        crate::algs::communicator::CommTag::new(0xBEEF),
2065                    ),
2066                )?
2067            } else {
2068                LocalToGlobalMap::from_section_with_ownership(
2069                    section,
2070                    overlap,
2071                    ownership,
2072                    comm,
2073                    comm.rank(),
2074                )?
2075            };
2076            maps.insert(name.clone(), map);
2077        }
2078        self.global_sections = maps;
2079        Ok(())
2080    }
2081
2082    /// Borrow a stored global section map.
2083    pub fn global_section(&self, name: &str) -> Option<&LocalToGlobalMap> {
2084        self.global_sections.get(name)
2085    }
2086
2087    /// Create a zero-initialized global vector matching a named global section.
2088    pub fn create_global_vector(
2089        &self,
2090        section_name: &str,
2091    ) -> Result<MeshVector<V>, MeshSieveError> {
2092        let map = self.global_sections.get(section_name).ok_or_else(|| {
2093            MeshSieveError::MissingSectionName {
2094                name: section_name.to_string(),
2095            }
2096        })?;
2097        Ok(MeshVector {
2098            section: Some(section_name.to_string()),
2099            values: global_vector_for_map(map),
2100        })
2101    }
2102
2103    /// Create a constrained section for a field using label constraints.
2104    pub fn create_constrained_section_from_labels(
2105        &self,
2106        field_name: &str,
2107        point_dofs: &[(PointId, usize)],
2108        constraints: &[LabelConstraintSpec],
2109    ) -> Result<ConstrainedSection<V, St>, MeshSieveError> {
2110        if let Some(discretization) = self.discretization()
2111            && discretization.field(field_name).is_none()
2112        {
2113            return Err(MeshSieveError::MissingSectionName {
2114                name: field_name.to_string(),
2115            });
2116        }
2117        let labels = self
2118            .labels()
2119            .ok_or_else(|| MeshSieveError::MissingSectionName {
2120                name: "labels".to_string(),
2121            })?;
2122        constrained_section_from_label_specs(point_dofs, labels, constraints)
2123    }
2124
2125    /// Distribute this DM through the lower-level distribution pipeline and
2126    /// return the local DM for the calling rank.
2127    pub fn distribute_with<P, C>(
2128        &self,
2129        cells: &[PointId],
2130        partitioner: &P,
2131        comm: &C,
2132    ) -> Result<Self, MeshSieveError>
2133    where
2134        P: CellPartitioner<MeshSieve>,
2135        C: Communicator + Sync,
2136        V: Send + PartialEq + bytemuck::Pod + 'static,
2137    {
2138        let distributed = distribute_with_overlap(
2139            &self.mesh_data,
2140            cells,
2141            partitioner,
2142            self.options.distribution_config(),
2143            comm,
2144        )?;
2145        let mut dm =
2146            Self::from_distributed(distributed, self.options.clone(), comm.rank(), comm.size());
2147        dm.run_refinement_passes(
2148            dm.options.post_refine,
2149            MeshDMTransferStrategy::PreserveCoordinatesAndLabels,
2150        )?;
2151        dm.enforce_phase_invariants()?;
2152        dm.run_requested_checks()?;
2153        Ok(dm)
2154    }
2155
2156    /// Complete/synchronize all registered fields through the owned overlap/SF state.
2157    pub fn distribute_fields<C>(&mut self, comm: &C) -> Result<(), MeshSieveError>
2158    where
2159        C: Communicator + Sync,
2160        V: Send + PartialEq + bytemuck::Pod + 'static,
2161    {
2162        let Some(ownership) = &self.ownership else {
2163            return Ok(());
2164        };
2165        let Some(overlap) = &self.overlap else {
2166            return Ok(());
2167        };
2168        let sf =
2169            crate::algs::point_sf::PointSF::with_ownership(overlap, ownership, comm, comm.rank());
2170        sf.validate()?;
2171        self.provenance_maps.section_map = Some(crate::algs::point_sf::create_process_sf::<
2172            crate::algs::communicator::NoComm,
2173        >(ownership, comm.rank()));
2174        if let Some(coords) = &mut self.mesh_data.coordinates {
2175            sf.complete_section(coords.section_mut())?;
2176            if let Some(high_order) = coords.high_order_mut() {
2177                sf.complete_section(high_order.section_mut())?;
2178            }
2179        }
2180        for section in self.mesh_data.sections.values_mut() {
2181            sf.complete_section(section)?;
2182        }
2183        Ok(())
2184    }
2185
2186    fn from_distributed(
2187        data: DistributedMeshData<V, St, CtSt>,
2188        options: MeshDMOptions,
2189        rank: usize,
2190        size: usize,
2191    ) -> Self {
2192        let redistribute_map = crate::algs::point_sf::create_process_sf::<
2193            crate::algs::communicator::NoComm,
2194        >(&data.ownership, rank);
2195        let mesh_data = MeshData {
2196            sieve: data.sieve,
2197            coordinates: data.coordinates,
2198            sections: data.sections,
2199            mixed_sections: data.mixed_sections,
2200            labels: data.labels,
2201            cell_types: data.cell_types,
2202            discretization: data.discretization,
2203        };
2204        Self {
2205            mesh_data,
2206            options,
2207            ownership: Some(data.ownership),
2208            overlap: Some(data.overlap),
2209            global_sections: BTreeMap::new(),
2210            anchors: TopologicalAnchors::default(),
2211            hanging_constraints: BTreeMap::new(),
2212            distribution: Some(MeshDMDistribution {
2213                point_owners: data.point_owners,
2214                cell_parts: data.cell_parts,
2215                rank,
2216                size,
2217            }),
2218            provenance_maps: MeshDMProvenance {
2219                load_map: None,
2220                redistribute_map: Some(redistribute_map),
2221                section_map: None,
2222                storage_version: None,
2223                permutation_source: None,
2224                redistribution_map_id: None,
2225                saved_rank_count: Some(size),
2226                loaded_rank_count: Some(size),
2227            },
2228        }
2229    }
2230
2231    fn ensure_serial_ownership(&mut self, rank: usize) -> Result<(), MeshSieveError> {
2232        if self.ownership.is_some() {
2233            return Ok(());
2234        }
2235        let mut ownership = PointOwnership::default();
2236        for point in self.mesh_data.sieve.points() {
2237            ownership.set(point, rank, false)?;
2238        }
2239        self.ownership = Some(ownership);
2240        Ok(())
2241    }
2242
2243    fn invalidate_numbering(&mut self) {
2244        self.global_sections.clear();
2245    }
2246
2247    fn reorder_sections(&mut self, ordering: StratifiedOrdering) -> Result<(), MeshSieveError> {
2248        let permutation = stratified_permutation(&self.mesh_data.sieve, ordering)?;
2249        for section in self.mesh_data.sections.values_mut() {
2250            *section = reorder_section_by_points(section, &permutation)?;
2251        }
2252        if let Some(coords) = &mut self.mesh_data.coordinates {
2253            let topological_dimension = coords.topological_dimension();
2254            let embedding_dimension = coords.embedding_dimension();
2255            let section = reorder_section_by_points(coords.section(), &permutation)?;
2256            *coords =
2257                Coordinates::from_section(topological_dimension, embedding_dimension, section)?;
2258        }
2259        if let Some(cell_types) = &mut self.mesh_data.cell_types {
2260            *cell_types = reorder_section_by_points(cell_types, &permutation)?;
2261        }
2262        Ok(())
2263    }
2264}
2265
2266fn reorder_section_by_points<V, St>(
2267    section: &Section<V, St>,
2268    order: &[PointId],
2269) -> Result<Section<V, St>, MeshSieveError>
2270where
2271    V: Clone + Default,
2272    St: Storage<V> + Clone,
2273{
2274    let mut atlas = Atlas::default();
2275    for &point in order {
2276        if let Some((_offset, len)) = section.atlas().get(point) {
2277            atlas.try_insert(point, len)?;
2278        }
2279    }
2280    for point in section.atlas().points() {
2281        if !atlas.contains(point) {
2282            let (_offset, len) = section
2283                .atlas()
2284                .get(point)
2285                .ok_or(MeshSieveError::PointNotInAtlas(point))?;
2286            atlas.try_insert(point, len)?;
2287        }
2288    }
2289    let mut reordered = Section::new(atlas);
2290    for point in section.atlas().points() {
2291        let values = section.try_restrict(point)?.to_vec();
2292        reordered.try_set(point, &values)?;
2293    }
2294    Ok(reordered)
2295}
2296
2297fn remap_ownership_to_submesh(
2298    ownership: Option<&PointOwnership>,
2299    maps: &SubmeshMaps,
2300) -> Result<Option<PointOwnership>, MeshSieveError> {
2301    let Some(ownership) = ownership else {
2302        return Ok(None);
2303    };
2304    let mut remapped = PointOwnership::default();
2305    for (&parent, &sub) in &maps.parent_to_sub {
2306        if let Some(entry) = ownership.entry(parent) {
2307            remapped.set(sub, entry.owner, entry.is_ghost)?;
2308        }
2309    }
2310    Ok(Some(remapped))
2311}
2312
2313fn remap_global_sections_to_submesh(
2314    global_sections: &BTreeMap<String, LocalToGlobalMap>,
2315    maps: &SubmeshMaps,
2316) -> Result<BTreeMap<String, LocalToGlobalMap>, MeshSieveError> {
2317    let new_to_old = maps
2318        .sub_to_parent
2319        .iter()
2320        .enumerate()
2321        .map(|(idx, &parent)| PointId::new((idx + 1) as u64).map(|sub| (sub, parent)))
2322        .collect::<Result<Vec<_>, _>>()?;
2323    let mut remapped = BTreeMap::new();
2324    for (name, map) in global_sections {
2325        remapped.insert(name.clone(), map.remap_points(new_to_old.iter().copied())?);
2326    }
2327    Ok(remapped)
2328}
2329
2330fn point_sf_for_submesh_maps(
2331    maps: &SubmeshMaps,
2332) -> PointSF<'static, crate::algs::communicator::NoComm> {
2333    let leaves = maps
2334        .sub_to_parent
2335        .iter()
2336        .enumerate()
2337        .filter_map(|(idx, &parent)| {
2338            PointId::new((idx + 1) as u64).ok().map(|sub| PointSfLeaf {
2339                local: sub,
2340                remote: RemotePoint {
2341                    rank: 0,
2342                    point: parent,
2343                },
2344                owner_rank: 0,
2345                is_ghost: false,
2346            })
2347        })
2348        .collect::<Vec<_>>();
2349    PointSF::from_leaves(0, maps.sub_to_parent.iter().copied(), leaves)
2350}