Skip to main content

mesh_sieve/adapt/
mod.rs

1//! Quality-driven adaptivity with data transfer helpers.
2
3use crate::data::atlas::Atlas;
4use crate::data::coordinates::Coordinates;
5use crate::data::refine::delta::SliceDelta;
6use crate::data::refine::sieved_array::SievedArray;
7use crate::data::section::Section;
8use crate::data::storage::{Storage, VecStorage};
9use crate::diagnostics::FvmCellDiagnostic;
10use crate::geometry::quality::{CellQuality, cell_quality_from_section};
11use crate::mesh_error::MeshSieveError;
12use crate::topology::anchors::TopologicalAnchors;
13use crate::topology::arrow::Polarity;
14use crate::topology::cell_type::CellType;
15use crate::topology::coarsen::{CoarsenEntity, CoarsenedTopology, coarsen_topology};
16use crate::topology::labels::LabelSet;
17use crate::topology::ownership::PointOwnership;
18use crate::topology::point::PointId;
19use crate::topology::refine::{
20    AnisotropicSplitHints, RefineOptions, RefinedMesh, collapse_to_cell_vertices,
21    refine_mesh_with_options,
22};
23use crate::topology::sieve::Sieve;
24use std::collections::HashMap;
25
26/// Aggregated quality metrics used for adaptivity decisions.
27#[derive(Clone, Copy, Debug)]
28pub struct QualityMetrics {
29    /// Ratio of the longest edge length to the shortest edge length.
30    pub aspect_ratio: f64,
31    /// Minimum corner angle (degrees).
32    pub min_angle_deg: f64,
33    /// Signed Jacobian (area for 2D, volume for 3D).
34    pub jacobian_sign: f64,
35    /// Absolute cell size (area/volume) derived from the Jacobian.
36    pub cell_size: f64,
37}
38
39impl From<CellQuality> for QualityMetrics {
40    fn from(value: CellQuality) -> Self {
41        Self {
42            aspect_ratio: value.aspect_ratio,
43            min_angle_deg: value.min_angle_deg,
44            jacobian_sign: value.jacobian_sign,
45            cell_size: value.jacobian_sign.abs(),
46        }
47    }
48}
49
50/// Evaluate quality metrics for each cell in the mesh.
51pub fn evaluate_quality_metrics<S, Cs>(
52    sieve: &mut impl Sieve<Point = PointId>,
53    cell_types: &Section<CellType, S>,
54    coordinates: &Coordinates<f64, Cs>,
55) -> Result<Vec<(PointId, QualityMetrics)>, MeshSieveError>
56where
57    S: Storage<CellType>,
58    Cs: Storage<f64>,
59{
60    let collapsed = collapse_to_cell_vertices(sieve, cell_types)?;
61    let mut metrics = Vec::new();
62    for (cell, cell_slice) in cell_types.iter() {
63        if cell_slice.len() != 1 {
64            return Err(MeshSieveError::SliceLengthMismatch {
65                point: cell,
66                expected: 1,
67                found: cell_slice.len(),
68            });
69        }
70        let cell_type = cell_slice[0];
71        let vertices: Vec<_> = collapsed.cone_points(cell).collect();
72        let quality = cell_quality_from_section(cell_type, &vertices, coordinates)?;
73        metrics.push((cell, QualityMetrics::from(quality)));
74    }
75    Ok(metrics)
76}
77
78/// Thresholds for quality-driven refinement and coarsening.
79#[derive(Clone, Copy, Debug)]
80pub struct QualityThresholds {
81    /// Refine when `min_angle_deg` drops below this value.
82    pub refine_min_angle_deg: f64,
83    /// Refine when `aspect_ratio` exceeds this value.
84    pub refine_max_aspect_ratio: f64,
85    /// Refine when `cell_size` exceeds this value.
86    pub refine_max_size: f64,
87    /// Coarsen when `min_angle_deg` exceeds this value.
88    pub coarsen_min_angle_deg: f64,
89    /// Coarsen when `aspect_ratio` is below this value.
90    pub coarsen_max_aspect_ratio: f64,
91    /// Coarsen when `cell_size` drops below this value.
92    pub coarsen_min_size: f64,
93    /// When enabled, enforce geometry checks during refinement.
94    pub check_geometry: bool,
95}
96
97impl Default for QualityThresholds {
98    fn default() -> Self {
99        Self {
100            refine_min_angle_deg: 20.0,
101            refine_max_aspect_ratio: 4.0,
102            refine_max_size: f64::INFINITY,
103            coarsen_min_angle_deg: 30.0,
104            coarsen_max_aspect_ratio: 2.0,
105            coarsen_min_size: 0.0,
106            check_geometry: false,
107        }
108    }
109}
110
111/// Symmetric metric tensor for anisotropic sizing (2D/3D).
112#[derive(Clone, Copy, Debug)]
113pub struct MetricTensor {
114    /// Dimension of the metric (2 or 3).
115    pub dimension: usize,
116    /// Symmetric tensor entries stored as (m00, m11, m22, m01, m02, m12).
117    pub data: [f64; 6],
118}
119
120impl Default for MetricTensor {
121    fn default() -> Self {
122        Self::new_2d(1.0, 1.0, 0.0)
123    }
124}
125
126impl MetricTensor {
127    /// Construct a 2D metric tensor.
128    pub fn new_2d(m00: f64, m11: f64, m01: f64) -> Self {
129        Self {
130            dimension: 2,
131            data: [m00, m11, 0.0, m01, 0.0, 0.0],
132        }
133    }
134
135    /// Construct a 3D metric tensor.
136    pub fn new_3d(m00: f64, m11: f64, m22: f64, m01: f64, m02: f64, m12: f64) -> Self {
137        Self {
138            dimension: 3,
139            data: [m00, m11, m22, m01, m02, m12],
140        }
141    }
142
143    fn length_squared(&self, vector: &[f64]) -> Result<f64, MeshSieveError> {
144        match self.dimension {
145            2 => {
146                if vector.len() < 2 {
147                    return Err(MeshSieveError::InvalidGeometry(
148                        "metric tensor expects 2D vectors".into(),
149                    ));
150                }
151                let (x, y) = (vector[0], vector[1]);
152                let [m00, m11, _, m01, _, _] = self.data;
153                Ok(m00 * x * x + m11 * y * y + 2.0 * m01 * x * y)
154            }
155            3 => {
156                if vector.len() < 3 {
157                    return Err(MeshSieveError::InvalidGeometry(
158                        "metric tensor expects 3D vectors".into(),
159                    ));
160                }
161                let (x, y, z) = (vector[0], vector[1], vector[2]);
162                let [m00, m11, m22, m01, m02, m12] = self.data;
163                Ok(m00 * x * x
164                    + m11 * y * y
165                    + m22 * z * z
166                    + 2.0 * (m01 * x * y + m02 * x * z + m12 * y * z))
167            }
168            _ => Err(MeshSieveError::InvalidGeometry(
169                "metric tensor dimension must be 2 or 3".into(),
170            )),
171        }
172    }
173}
174
175/// Controls matching DMPLEX-style metric normalization knobs.
176///
177/// The controls are intentionally backend-neutral: they transform the metric
178/// field before selection/refinement and can later be passed unchanged to
179/// external remeshers that implement equivalent options.
180#[derive(Clone, Copy, Debug, PartialEq, Default)]
181pub struct MetricNormalizationControls {
182    /// Desired global metric complexity. Values > 0 rescale metric magnitudes.
183    pub target_complexity: Option<f64>,
184    /// Maximum allowed cell-to-cell metric gradation ratio.
185    pub gradation: Option<f64>,
186    /// Hausdorff tolerance carried for remeshing backends.
187    pub hausdorff_number: Option<f64>,
188    /// Lower bound for edge anisotropy ratios.
189    pub min_anisotropy: Option<f64>,
190    /// Upper bound for edge anisotropy ratios.
191    pub max_anisotropy: Option<f64>,
192    /// Lower bound for tensor diagonal magnitudes.
193    pub min_magnitude: Option<f64>,
194    /// Upper bound for tensor diagonal magnitudes.
195    pub max_magnitude: Option<f64>,
196}
197
198impl MetricNormalizationControls {
199    /// Returns true when no normalization or backend policy controls are set.
200    pub fn is_identity(&self) -> bool {
201        self == &Self::default()
202    }
203}
204
205/// External remeshing engines anticipated by the metric-adaptation facade.
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub enum ExternalRemeshingBackend {
208    /// Triangle-based 2D constrained Delaunay remeshing.
209    Triangle,
210    /// TetGen-based 3D tetrahedral remeshing.
211    TetGen,
212    /// Gmsh metric/remeshing pipeline.
213    Gmsh,
214    /// MMG anisotropic remeshing pipeline.
215    Mmg,
216}
217
218impl ExternalRemeshingBackend {
219    /// Returns true when this backend's mesh-generation feature is enabled in this build.
220    pub fn is_feature_enabled(self) -> bool {
221        match self {
222            ExternalRemeshingBackend::Triangle => cfg!(feature = "triangle-support"),
223            ExternalRemeshingBackend::TetGen => cfg!(feature = "tetgen-support"),
224            ExternalRemeshingBackend::Gmsh => cfg!(feature = "gmsh-support"),
225            ExternalRemeshingBackend::Mmg => false,
226        }
227    }
228
229    /// Returns the Cargo feature that wires this backend into mesh-generation/remeshing helpers.
230    pub fn cargo_feature(self) -> Option<&'static str> {
231        match self {
232            ExternalRemeshingBackend::Triangle => Some("triangle-support"),
233            ExternalRemeshingBackend::TetGen => Some("tetgen-support"),
234            ExternalRemeshingBackend::Gmsh => Some("gmsh-support"),
235            ExternalRemeshingBackend::Mmg => None,
236        }
237    }
238}
239
240/// Backend policy for metric adaptation.
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub enum MetricRemeshingBackend {
243    /// Use mesh-sieve's in-process refinement/coarsening primitives.
244    Internal,
245    /// Request a concrete external remesher. Returns a clear error until linked.
246    External(ExternalRemeshingBackend),
247}
248
249impl Default for MetricRemeshingBackend {
250    fn default() -> Self {
251        Self::Internal
252    }
253}
254
255/// Explicit old/new point provenance generated by adaptation.
256#[derive(Clone, Debug, Default, PartialEq, Eq)]
257pub struct AdaptationProvenanceMap {
258    /// For each input point, the output points that inherit its data.
259    pub old_to_new: Vec<(PointId, Vec<PointId>)>,
260    /// For each output point, the input points used as its data source.
261    pub new_to_old: Vec<(PointId, Vec<PointId>)>,
262}
263
264impl AdaptationProvenanceMap {
265    fn from_refinement(refinement: &[(PointId, Vec<(PointId, Polarity)>)]) -> Self {
266        let mut old_to_new = Vec::new();
267        let mut new_to_old = Vec::new();
268        for (old, fines) in refinement {
269            let news: Vec<_> = fines.iter().map(|(fine, _)| *fine).collect();
270            for new_point in &news {
271                new_to_old.push((*new_point, vec![*old]));
272            }
273            old_to_new.push((*old, news));
274        }
275        Self {
276            old_to_new,
277            new_to_old,
278        }
279    }
280
281    fn from_coarsening(transfer: &[(PointId, Vec<(PointId, Polarity)>)]) -> Self {
282        let mut old_to_new_by_old: HashMap<PointId, Vec<PointId>> = HashMap::new();
283        let mut new_to_old = Vec::new();
284        for (new_coarse, old_fines) in transfer {
285            let olds: Vec<_> = old_fines.iter().map(|(old, _)| *old).collect();
286            for old in &olds {
287                old_to_new_by_old.entry(*old).or_default().push(*new_coarse);
288            }
289            new_to_old.push((*new_coarse, olds));
290        }
291        let mut old_to_new: Vec<_> = old_to_new_by_old.into_iter().collect();
292        old_to_new.sort_by_key(|(point, _)| *point);
293        Self {
294            old_to_new,
295            new_to_old,
296        }
297    }
298}
299
300/// Metric-driven cell summary.
301#[derive(Clone, Copy, Debug)]
302pub struct MetricCellMetrics {
303    /// Maximum metric edge length in the cell.
304    pub max_edge_length: f64,
305    /// Minimum metric edge length in the cell.
306    pub min_edge_length: f64,
307    /// Ratio of maximum to minimum metric edge lengths.
308    pub anisotropy_ratio: f64,
309}
310
311/// Thresholds for metric-driven refinement and coarsening.
312#[derive(Clone, Copy, Debug)]
313pub struct MetricThresholds {
314    /// Refine when the maximum metric edge length exceeds this value.
315    pub refine_max_edge_length: f64,
316    /// Coarsen when the maximum metric edge length drops below this value.
317    pub coarsen_max_edge_length: f64,
318    /// Split edges when their metric length exceeds this value.
319    pub split_edge_length: f64,
320    /// Split faces when their metric length exceeds this value.
321    pub split_face_length: f64,
322    /// When enabled, enforce geometry checks during refinement.
323    pub check_geometry: bool,
324}
325
326/// FV-stability-driven thresholds mapped from diagnostics.
327#[derive(Clone, Copy, Debug)]
328pub struct FvStabilityThresholds {
329    pub refine_non_orthogonality_deg: f64,
330    pub refine_skewness: f64,
331    pub refine_min_char_length: f64,
332    pub coarsen_non_orthogonality_deg: f64,
333    pub coarsen_skewness: f64,
334    pub coarsen_min_char_length: f64,
335}
336
337#[derive(Clone, Copy, Debug)]
338pub struct FvmQualityOutputs {
339    pub non_orthogonality_deg: f64,
340    pub skewness: f64,
341    pub characteristic_length: f64,
342}
343
344impl From<&FvmCellDiagnostic> for FvmQualityOutputs {
345    fn from(value: &FvmCellDiagnostic) -> Self {
346        Self {
347            non_orthogonality_deg: value.max_non_orthogonality_deg,
348            skewness: value.max_skewness,
349            characteristic_length: value.char_length,
350        }
351    }
352}
353
354impl Default for FvStabilityThresholds {
355    fn default() -> Self {
356        Self {
357            refine_non_orthogonality_deg: 75.0,
358            refine_skewness: 4.0,
359            refine_min_char_length: 1.0e-8,
360            coarsen_non_orthogonality_deg: 35.0,
361            coarsen_skewness: 1.5,
362            coarsen_min_char_length: 1.0e-4,
363        }
364    }
365}
366
367pub fn select_cells_from_fvm_diagnostics(
368    cell_diags: &[FvmCellDiagnostic],
369    thresholds: FvStabilityThresholds,
370) -> AdaptivitySelection {
371    let mut selection = AdaptivitySelection::default();
372    for d in cell_diags {
373        let q = FvmQualityOutputs::from(d);
374        if should_refine_from_fvm_quality(q, thresholds) {
375            selection.refine_cells.push(d.cell);
376        } else if should_coarsen_from_fvm_quality(q, thresholds) {
377            selection.coarsen_cells.push(d.cell);
378        }
379    }
380    selection
381}
382
383pub fn should_refine_from_fvm_quality(
384    quality: FvmQualityOutputs,
385    thresholds: FvStabilityThresholds,
386) -> bool {
387    quality.non_orthogonality_deg > thresholds.refine_non_orthogonality_deg
388        || quality.skewness > thresholds.refine_skewness
389        || quality.characteristic_length < thresholds.refine_min_char_length
390}
391
392pub fn should_coarsen_from_fvm_quality(
393    quality: FvmQualityOutputs,
394    thresholds: FvStabilityThresholds,
395) -> bool {
396    quality.non_orthogonality_deg <= thresholds.coarsen_non_orthogonality_deg
397        && quality.skewness <= thresholds.coarsen_skewness
398        && quality.characteristic_length >= thresholds.coarsen_min_char_length
399}
400
401impl Default for MetricThresholds {
402    fn default() -> Self {
403        Self {
404            refine_max_edge_length: 1.2,
405            coarsen_max_edge_length: 0.8,
406            split_edge_length: 1.0,
407            split_face_length: 1.0,
408            check_geometry: false,
409        }
410    }
411}
412
413/// Boundary handling policy for adaptation.
414#[derive(Clone, Debug, Default)]
415pub enum BoundaryRemeshingPolicy {
416    /// Preserve all labeled boundary entities and suppress metric split hints on them.
417    #[default]
418    PreserveBoundary,
419    /// Allow split/coarsen decisions on boundary entities.
420    RemeshBoundary,
421    /// Preserve only the named label strata.
422    PreserveLabeled(Vec<(String, i32)>),
423}
424
425impl BoundaryRemeshingPolicy {
426    fn preserves(&self, labels: Option<&LabelSet>, cell: PointId) -> bool {
427        match self {
428            BoundaryRemeshingPolicy::RemeshBoundary => false,
429            BoundaryRemeshingPolicy::PreserveBoundary => labels.is_some_and(|labels| {
430                labels
431                    .iter()
432                    .any(|(name, point, value)| point == cell && name == "boundary" && value != 0)
433            }),
434            BoundaryRemeshingPolicy::PreserveLabeled(strata) => labels.is_some_and(|labels| {
435                strata
436                    .iter()
437                    .any(|(name, value)| labels.get_label(cell, name) == Some(*value))
438            }),
439        }
440    }
441}
442
443/// Metric-adaptation controls beyond numeric thresholds.
444#[derive(Clone, Debug, Default)]
445pub struct MetricAdaptationOptions {
446    /// Boundary remeshing policy.
447    pub boundary_policy: BoundaryRemeshingPolicy,
448    /// DMPLEX-style metric normalization controls applied before decisions.
449    pub normalization: MetricNormalizationControls,
450    /// Remeshing backend policy.
451    pub backend: MetricRemeshingBackend,
452}
453
454/// Edge/face split hints for anisotropic refinement.
455#[derive(Clone, Debug)]
456pub struct MetricSplitHint {
457    /// Cell that owns the split hints.
458    pub cell: PointId,
459    /// Edges to split represented by vertex pairs.
460    pub split_edges: Vec<[PointId; 2]>,
461    /// Faces to split represented by ordered vertex loops.
462    pub split_faces: Vec<Vec<PointId>>,
463}
464
465#[derive(Clone, Debug)]
466struct MetricCellEvaluation {
467    cell: PointId,
468    metrics: MetricCellMetrics,
469    edge_lengths: Vec<([PointId; 2], f64)>,
470    face_lengths: Vec<(Vec<PointId>, f64)>,
471}
472
473/// Cells selected for refinement and coarsening.
474#[derive(Clone, Debug, Default)]
475pub struct AdaptivitySelection {
476    /// Cells chosen for refinement.
477    pub refine_cells: Vec<PointId>,
478    /// Cells chosen for coarsening.
479    pub coarsen_cells: Vec<PointId>,
480}
481
482/// Select cells for refinement and coarsening based on quality thresholds.
483pub fn select_cells_for_adaptation(
484    metrics: &[(PointId, QualityMetrics)],
485    thresholds: QualityThresholds,
486) -> AdaptivitySelection {
487    let mut selection = AdaptivitySelection::default();
488    for (cell, quality) in metrics {
489        if quality.min_angle_deg < thresholds.refine_min_angle_deg
490            || quality.aspect_ratio > thresholds.refine_max_aspect_ratio
491            || quality.cell_size > thresholds.refine_max_size
492        {
493            selection.refine_cells.push(*cell);
494        } else if quality.min_angle_deg >= thresholds.coarsen_min_angle_deg
495            && quality.aspect_ratio <= thresholds.coarsen_max_aspect_ratio
496            && quality.cell_size < thresholds.coarsen_min_size
497        {
498            selection.coarsen_cells.push(*cell);
499        }
500    }
501    selection
502}
503
504fn polygon_edges(vertices: &[PointId]) -> Vec<[PointId; 2]> {
505    if vertices.len() < 2 {
506        return Vec::new();
507    }
508    let mut edges = Vec::with_capacity(vertices.len());
509    for i in 0..vertices.len() {
510        let next = (i + 1) % vertices.len();
511        edges.push([vertices[i], vertices[next]]);
512    }
513    edges
514}
515
516fn cell_edges(
517    cell_type: CellType,
518    vertices: &[PointId],
519) -> Result<Vec<[PointId; 2]>, MeshSieveError> {
520    let edges = match cell_type {
521        CellType::Triangle => vec![
522            [vertices[0], vertices[1]],
523            [vertices[1], vertices[2]],
524            [vertices[2], vertices[0]],
525        ],
526        CellType::Quadrilateral => vec![
527            [vertices[0], vertices[1]],
528            [vertices[1], vertices[2]],
529            [vertices[2], vertices[3]],
530            [vertices[3], vertices[0]],
531        ],
532        CellType::Polygon(_) => polygon_edges(vertices),
533        CellType::Tetrahedron => vec![
534            [vertices[0], vertices[1]],
535            [vertices[1], vertices[2]],
536            [vertices[2], vertices[0]],
537            [vertices[0], vertices[3]],
538            [vertices[1], vertices[3]],
539            [vertices[2], vertices[3]],
540        ],
541        CellType::Hexahedron => vec![
542            [vertices[0], vertices[1]],
543            [vertices[1], vertices[2]],
544            [vertices[2], vertices[3]],
545            [vertices[3], vertices[0]],
546            [vertices[4], vertices[5]],
547            [vertices[5], vertices[6]],
548            [vertices[6], vertices[7]],
549            [vertices[7], vertices[4]],
550            [vertices[0], vertices[4]],
551            [vertices[1], vertices[5]],
552            [vertices[2], vertices[6]],
553            [vertices[3], vertices[7]],
554        ],
555        CellType::Prism => vec![
556            [vertices[0], vertices[1]],
557            [vertices[1], vertices[2]],
558            [vertices[2], vertices[0]],
559            [vertices[3], vertices[4]],
560            [vertices[4], vertices[5]],
561            [vertices[5], vertices[3]],
562            [vertices[0], vertices[3]],
563            [vertices[1], vertices[4]],
564            [vertices[2], vertices[5]],
565        ],
566        CellType::Polyhedron => {
567            return Err(MeshSieveError::InvalidGeometry(
568                "metric evaluation does not support polyhedron edges".into(),
569            ));
570        }
571        _ => {
572            return Err(MeshSieveError::InvalidGeometry(format!(
573                "unsupported cell type: {cell_type:?}"
574            )));
575        }
576    };
577    Ok(edges)
578}
579
580fn cell_faces(
581    cell_type: CellType,
582    vertices: &[PointId],
583) -> Result<Vec<Vec<PointId>>, MeshSieveError> {
584    let faces = match cell_type {
585        CellType::Tetrahedron => vec![
586            vec![vertices[0], vertices[1], vertices[2]],
587            vec![vertices[0], vertices[1], vertices[3]],
588            vec![vertices[1], vertices[2], vertices[3]],
589            vec![vertices[2], vertices[0], vertices[3]],
590        ],
591        CellType::Hexahedron => vec![
592            vec![vertices[0], vertices[1], vertices[2], vertices[3]],
593            vec![vertices[4], vertices[5], vertices[6], vertices[7]],
594            vec![vertices[0], vertices[1], vertices[5], vertices[4]],
595            vec![vertices[1], vertices[2], vertices[6], vertices[5]],
596            vec![vertices[2], vertices[3], vertices[7], vertices[6]],
597            vec![vertices[3], vertices[0], vertices[4], vertices[7]],
598        ],
599        CellType::Prism => vec![
600            vec![vertices[0], vertices[1], vertices[2]],
601            vec![vertices[3], vertices[4], vertices[5]],
602            vec![vertices[0], vertices[1], vertices[4], vertices[3]],
603            vec![vertices[1], vertices[2], vertices[5], vertices[4]],
604            vec![vertices[2], vertices[0], vertices[3], vertices[5]],
605        ],
606        CellType::Polyhedron => {
607            return Err(MeshSieveError::InvalidGeometry(
608                "metric evaluation does not support polyhedron faces".into(),
609            ));
610        }
611        _ => Vec::new(),
612    };
613    Ok(faces)
614}
615
616fn metric_edge_length<Cs>(
617    tensor: MetricTensor,
618    coords: &Coordinates<f64, Cs>,
619    edge: [PointId; 2],
620) -> Result<f64, MeshSieveError>
621where
622    Cs: Storage<f64>,
623{
624    let a = coords.try_restrict(edge[0])?;
625    let b = coords.try_restrict(edge[1])?;
626    if a.len() != b.len() {
627        return Err(MeshSieveError::InvalidGeometry(
628            "edge coordinate dimensions do not match".into(),
629        ));
630    }
631    if a.len() != tensor.dimension {
632        return Err(MeshSieveError::InvalidGeometry(
633            "metric tensor dimension does not match coordinates".into(),
634        ));
635    }
636    let vector: Vec<f64> = b.iter().zip(a.iter()).map(|(bv, av)| bv - av).collect();
637    let length_sq = tensor.length_squared(&vector)?;
638    Ok(length_sq.max(0.0).sqrt())
639}
640
641fn evaluate_metric_cells<S, Cs, Ms>(
642    sieve: &mut impl Sieve<Point = PointId>,
643    cell_types: &Section<CellType, S>,
644    coordinates: &Coordinates<f64, Cs>,
645    metrics: &Section<MetricTensor, Ms>,
646) -> Result<Vec<MetricCellEvaluation>, MeshSieveError>
647where
648    S: Storage<CellType>,
649    Cs: Storage<f64>,
650    Ms: Storage<MetricTensor>,
651{
652    let collapsed = collapse_to_cell_vertices(sieve, cell_types)?;
653    let mut evaluations = Vec::new();
654    for (cell, cell_slice) in cell_types.iter() {
655        if cell_slice.len() != 1 {
656            return Err(MeshSieveError::SliceLengthMismatch {
657                point: cell,
658                expected: 1,
659                found: cell_slice.len(),
660            });
661        }
662        let tensor_slice = metrics.try_restrict(cell)?;
663        if tensor_slice.len() != 1 {
664            return Err(MeshSieveError::SliceLengthMismatch {
665                point: cell,
666                expected: 1,
667                found: tensor_slice.len(),
668            });
669        }
670        let tensor = tensor_slice[0];
671        let cell_type = cell_slice[0];
672        let vertices: Vec<_> = collapsed.cone_points(cell).collect();
673        let edges = cell_edges(cell_type, &vertices)?;
674        if edges.is_empty() {
675            return Err(MeshSieveError::InvalidGeometry(format!(
676                "cell {cell:?} has no edges for metric evaluation"
677            )));
678        }
679        let mut edge_lengths = Vec::with_capacity(edges.len());
680        for edge in edges {
681            let length = metric_edge_length(tensor, coordinates, edge)?;
682            edge_lengths.push((edge, length));
683        }
684        let mut max_edge = 0.0;
685        let mut min_edge = f64::INFINITY;
686        for (_, length) in &edge_lengths {
687            if *length > max_edge {
688                max_edge = *length;
689            }
690            if *length < min_edge {
691                min_edge = *length;
692            }
693        }
694        let anisotropy_ratio = if min_edge > 0.0 {
695            max_edge / min_edge
696        } else {
697            f64::INFINITY
698        };
699        let face_lengths = cell_faces(cell_type, &vertices)?
700            .into_iter()
701            .map(|face_vertices| {
702                let mut face_max = 0.0;
703                for edge in polygon_edges(&face_vertices) {
704                    let length = metric_edge_length(tensor, coordinates, edge)?;
705                    if length > face_max {
706                        face_max = length;
707                    }
708                }
709                Ok((face_vertices, face_max))
710            })
711            .collect::<Result<Vec<_>, MeshSieveError>>()?;
712        evaluations.push(MetricCellEvaluation {
713            cell,
714            metrics: MetricCellMetrics {
715                max_edge_length: max_edge,
716                min_edge_length: min_edge,
717                anisotropy_ratio,
718            },
719            edge_lengths,
720            face_lengths,
721        });
722    }
723    Ok(evaluations)
724}
725
726/// Select cells for refinement and coarsening based on metric thresholds.
727pub fn select_cells_for_metric_adaptation(
728    metrics: &[(PointId, MetricCellMetrics)],
729    thresholds: MetricThresholds,
730) -> AdaptivitySelection {
731    let mut selection = AdaptivitySelection::default();
732    for (cell, metric) in metrics {
733        if metric.max_edge_length > thresholds.refine_max_edge_length {
734            selection.refine_cells.push(*cell);
735        } else if metric.max_edge_length < thresholds.coarsen_max_edge_length {
736            selection.coarsen_cells.push(*cell);
737        }
738    }
739    selection
740}
741
742fn metric_split_hints(
743    evaluations: &[MetricCellEvaluation],
744    thresholds: MetricThresholds,
745) -> Vec<MetricSplitHint> {
746    let mut hints = Vec::new();
747    for evaluation in evaluations {
748        let split_edges: Vec<_> = evaluation
749            .edge_lengths
750            .iter()
751            .filter(|(_, length)| *length > thresholds.split_edge_length)
752            .map(|(edge, _)| *edge)
753            .collect();
754        let split_faces: Vec<_> = evaluation
755            .face_lengths
756            .iter()
757            .filter(|(_, length)| *length > thresholds.split_face_length)
758            .map(|(face, _)| face.clone())
759            .collect();
760        if !split_edges.is_empty() || !split_faces.is_empty() {
761            hints.push(MetricSplitHint {
762                cell: evaluation.cell,
763                split_edges,
764                split_faces,
765            });
766        }
767    }
768    hints
769}
770
771fn build_anisotropic_hints(hints: &[MetricSplitHint]) -> AnisotropicSplitHints {
772    let mut edge_splits: HashMap<PointId, Vec<[PointId; 2]>> = HashMap::new();
773    let mut face_splits: HashMap<PointId, Vec<Vec<PointId>>> = HashMap::new();
774    for hint in hints {
775        if !hint.split_edges.is_empty() {
776            edge_splits.insert(hint.cell, hint.split_edges.clone());
777        }
778        if !hint.split_faces.is_empty() {
779            face_splits.insert(hint.cell, hint.split_faces.clone());
780        }
781    }
782    AnisotropicSplitHints {
783        edge_splits,
784        face_splits,
785    }
786}
787
788fn normalized_metric_section<Ms>(
789    metric: &Section<MetricTensor, Ms>,
790    controls: MetricNormalizationControls,
791) -> Result<Section<MetricTensor, VecStorage<MetricTensor>>, MeshSieveError>
792where
793    Ms: Storage<MetricTensor>,
794{
795    let mut tensors: Vec<(PointId, MetricTensor)> = metric
796        .iter()
797        .map(|(point, slice)| {
798            if slice.len() == 1 {
799                Ok((point, slice[0]))
800            } else {
801                Err(MeshSieveError::SliceLengthMismatch {
802                    point,
803                    expected: 1,
804                    found: slice.len(),
805                })
806            }
807        })
808        .collect::<Result<_, _>>()?;
809
810    for (_, tensor) in &mut tensors {
811        clamp_metric_tensor(tensor, controls);
812    }
813
814    if let Some(target) = controls
815        .target_complexity
816        .filter(|value| value.is_finite() && *value > 0.0)
817    {
818        let current: f64 = tensors
819            .iter()
820            .map(|(_, tensor)| metric_complexity_proxy(*tensor))
821            .sum();
822        if current > 0.0 && current.is_finite() {
823            let dimension = tensors
824                .first()
825                .map(|(_, tensor)| tensor.dimension)
826                .unwrap_or(2) as f64;
827            let scale = (target / current).powf(2.0 / dimension);
828            for (_, tensor) in &mut tensors {
829                scale_metric_tensor(tensor, scale);
830                clamp_metric_tensor(tensor, controls);
831            }
832        }
833    }
834
835    if let Some(gradation) = controls
836        .gradation
837        .filter(|value| value.is_finite() && *value >= 1.0)
838        && !tensors.is_empty()
839    {
840        let min_mag = tensors
841            .iter()
842            .map(|(_, tensor)| metric_magnitude_proxy(*tensor))
843            .filter(|value| *value > 0.0 && value.is_finite())
844            .fold(f64::INFINITY, f64::min);
845        if min_mag.is_finite() {
846            let max_allowed = min_mag * gradation;
847            for (_, tensor) in &mut tensors {
848                let magnitude = metric_magnitude_proxy(*tensor);
849                if magnitude > max_allowed && magnitude > 0.0 {
850                    scale_metric_tensor(tensor, max_allowed / magnitude);
851                }
852            }
853        }
854    }
855
856    let mut atlas = Atlas::default();
857    for (point, _) in &tensors {
858        atlas.try_insert(*point, 1)?;
859    }
860    let mut out = Section::<MetricTensor, VecStorage<MetricTensor>>::new(atlas);
861    for (point, tensor) in tensors {
862        out.try_set(point, &[tensor])?;
863    }
864    Ok(out)
865}
866
867fn metric_magnitude_proxy(tensor: MetricTensor) -> f64 {
868    let dim = tensor.dimension.min(3);
869    if dim == 0 {
870        return 0.0;
871    }
872    (0..dim).map(|i| tensor.data[i].abs()).sum::<f64>() / dim as f64
873}
874
875fn metric_complexity_proxy(tensor: MetricTensor) -> f64 {
876    let dim = tensor.dimension.min(3);
877    if dim == 0 {
878        return 0.0;
879    }
880    let det_proxy = (0..dim)
881        .map(|i| tensor.data[i].abs().max(0.0))
882        .product::<f64>();
883    det_proxy.sqrt()
884}
885
886fn scale_metric_tensor(tensor: &mut MetricTensor, scale: f64) {
887    if scale.is_finite() && scale > 0.0 {
888        for value in &mut tensor.data {
889            *value *= scale;
890        }
891    }
892}
893
894fn clamp_metric_tensor(tensor: &mut MetricTensor, controls: MetricNormalizationControls) {
895    let dim = tensor.dimension.min(3);
896    for i in 0..dim {
897        if let Some(min) = controls.min_magnitude.filter(|value| value.is_finite()) {
898            tensor.data[i] = tensor.data[i].max(min);
899        }
900        if let Some(max) = controls.max_magnitude.filter(|value| value.is_finite()) {
901            tensor.data[i] = tensor.data[i].min(max);
902        }
903    }
904    if dim >= 2 {
905        let min_diag = (0..dim)
906            .map(|i| tensor.data[i].abs())
907            .fold(f64::INFINITY, f64::min);
908        let max_diag = (0..dim).map(|i| tensor.data[i].abs()).fold(0.0, f64::max);
909        if min_diag > 0.0 && max_diag.is_finite() {
910            if let Some(max_aniso) = controls
911                .max_anisotropy
912                .filter(|value| value.is_finite() && *value >= 1.0)
913                && max_diag / min_diag > max_aniso
914            {
915                let floor = max_diag / max_aniso;
916                for i in 0..dim {
917                    if tensor.data[i].abs() < floor {
918                        tensor.data[i] = floor.copysign(tensor.data[i]);
919                    }
920                }
921            }
922            if let Some(min_aniso) = controls
923                .min_anisotropy
924                .filter(|value| value.is_finite() && *value >= 1.0)
925                && max_diag / min_diag < min_aniso
926            {
927                let ceiling = min_diag * min_aniso;
928                let max_idx = (0..dim)
929                    .max_by(|a, b| {
930                        tensor.data[*a]
931                            .abs()
932                            .partial_cmp(&tensor.data[*b].abs())
933                            .unwrap()
934                    })
935                    .unwrap();
936                tensor.data[max_idx] = ceiling.copysign(tensor.data[max_idx]);
937            }
938        }
939    }
940}
941
942/// Normalize a per-cell metric section with DMPLEX-style controls.
943pub fn normalize_metric_section<Ms>(
944    metric: &Section<MetricTensor, Ms>,
945    controls: MetricNormalizationControls,
946) -> Result<Section<MetricTensor, VecStorage<MetricTensor>>, MeshSieveError>
947where
948    Ms: Storage<MetricTensor>,
949{
950    normalized_metric_section(metric, controls)
951}
952
953/// Outcome of quality-driven adaptivity with data transfer.
954#[derive(Clone, Debug)]
955pub enum AdaptationAction<V> {
956    /// The driver refined a subset of cells.
957    Refined {
958        /// Refined topology and transfer map.
959        mesh: RefinedMesh,
960        /// Refined data mapped onto the new cells.
961        data: SievedArray<PointId, V>,
962    },
963    /// The driver coarsened a subset of cells.
964    Coarsened {
965        /// Coarsened topology and transfer map.
966        mesh: CoarsenedTopology,
967        /// Coarsened data assembled onto the coarse cells.
968        data: SievedArray<PointId, V>,
969    },
970    /// No changes were requested.
971    NoChange,
972}
973
974/// Summary of a quality-driven adaptivity pass.
975#[derive(Clone, Debug)]
976pub struct AdaptationResult<V> {
977    /// Computed metrics for each cell.
978    pub metrics: Vec<(PointId, QualityMetrics)>,
979    /// Cells chosen for refinement.
980    pub refine_cells: Vec<PointId>,
981    /// Cells chosen for coarsening.
982    pub coarsen_cells: Vec<PointId>,
983    /// The action taken by the driver.
984    pub action: AdaptationAction<V>,
985}
986
987/// Outcome of metric-driven adaptivity without data transfer.
988#[derive(Clone, Debug)]
989pub enum MetricAdaptationAction {
990    /// The driver refined a subset of cells.
991    Refined { mesh: RefinedMesh },
992    /// The driver coarsened a subset of cells.
993    Coarsened { mesh: CoarsenedTopology },
994    /// No changes were requested.
995    NoChange,
996}
997
998/// Summary of a metric-driven adaptivity pass.
999#[derive(Clone, Debug)]
1000pub struct MetricAdaptationResult<V> {
1001    /// Computed metric summaries for each cell.
1002    pub metrics: Vec<(PointId, MetricCellMetrics)>,
1003    /// Cells chosen for refinement.
1004    pub refine_cells: Vec<PointId>,
1005    /// Cells chosen for coarsening.
1006    pub coarsen_cells: Vec<PointId>,
1007    /// Edge/face split hints derived from the metric field.
1008    pub split_hints: Vec<MetricSplitHint>,
1009    /// The action taken by the driver.
1010    pub action: MetricAdaptationAction,
1011    /// Optional transferred data (only for data-aware adaptation).
1012    pub data: Option<SievedArray<PointId, V>>,
1013    /// Explicit point provenance for transferring metadata through the pass.
1014    pub provenance: AdaptationProvenanceMap,
1015}
1016
1017fn subset_cell_types<S>(
1018    cell_types: &Section<CellType, S>,
1019    cells: &[PointId],
1020) -> Result<Section<CellType, VecStorage<CellType>>, MeshSieveError>
1021where
1022    S: Storage<CellType>,
1023{
1024    let mut atlas = Atlas::default();
1025    for cell in cells {
1026        atlas.try_insert(*cell, 1)?;
1027    }
1028    let mut section = Section::<CellType, VecStorage<CellType>>::new(atlas);
1029    for cell in cells {
1030        let slice = cell_types.try_restrict(*cell)?;
1031        if slice.len() != 1 {
1032            return Err(MeshSieveError::SliceLengthMismatch {
1033                point: *cell,
1034                expected: 1,
1035                found: slice.len(),
1036            });
1037        }
1038        section.try_set(*cell, slice)?;
1039    }
1040    Ok(section)
1041}
1042
1043fn refined_data_atlas<V>(
1044    coarse: &SievedArray<PointId, V>,
1045    refinement: &[(PointId, Vec<(PointId, Polarity)>)],
1046) -> Result<Atlas, MeshSieveError> {
1047    let mut atlas = Atlas::default();
1048    for (coarse_cell, fine_cells) in refinement {
1049        let (_offset, len) = coarse
1050            .atlas()
1051            .get(*coarse_cell)
1052            .ok_or(MeshSieveError::SievedArrayPointNotInAtlas(*coarse_cell))?;
1053        for (fine_cell, _) in fine_cells {
1054            atlas.try_insert(*fine_cell, len)?;
1055        }
1056    }
1057    Ok(atlas)
1058}
1059
1060fn refine_data_with_transfer<V>(
1061    coarse: &SievedArray<PointId, V>,
1062    refinement: &[(PointId, Vec<(PointId, Polarity)>)],
1063) -> Result<SievedArray<PointId, V>, MeshSieveError>
1064where
1065    V: Clone + Default,
1066{
1067    let atlas = refined_data_atlas(coarse, refinement)?;
1068    let mut fine = SievedArray::<PointId, V>::new(atlas);
1069    fine.try_refine_with_sifter(coarse, refinement)?;
1070    Ok(fine)
1071}
1072
1073fn coarsened_data_atlas<V>(
1074    fine: &SievedArray<PointId, V>,
1075    transfer: &[(PointId, Vec<(PointId, Polarity)>)],
1076) -> Result<Atlas, MeshSieveError> {
1077    let mut atlas = Atlas::default();
1078    for (coarse_cell, fine_cells) in transfer {
1079        let first = fine_cells
1080            .first()
1081            .ok_or_else(|| MeshSieveError::InvalidGeometry("empty coarsen map".into()))?;
1082        let (_offset, len) = fine
1083            .atlas()
1084            .get(first.0)
1085            .ok_or(MeshSieveError::SievedArrayPointNotInAtlas(first.0))?;
1086        atlas.try_insert(*coarse_cell, len)?;
1087    }
1088    Ok(atlas)
1089}
1090
1091fn assemble_with_sifter<V>(
1092    fine: &SievedArray<PointId, V>,
1093    coarse: &mut SievedArray<PointId, V>,
1094    transfer: &[(PointId, Vec<(PointId, Polarity)>)],
1095) -> Result<(), MeshSieveError>
1096where
1097    V: Clone
1098        + Default
1099        + num_traits::FromPrimitive
1100        + core::ops::AddAssign
1101        + core::ops::Div<Output = V>,
1102{
1103    for (coarse_cell, fine_cells) in transfer {
1104        let coarse_len = coarse.try_get(*coarse_cell)?.len();
1105        let mut accum = vec![V::default(); coarse_len];
1106        let mut count = 0;
1107        for (fine_cell, polarity) in fine_cells {
1108            let slice = fine.try_get(*fine_cell)?;
1109            if slice.len() != accum.len() {
1110                return Err(MeshSieveError::SievedArraySliceLengthMismatch {
1111                    point: *fine_cell,
1112                    expected: accum.len(),
1113                    found: slice.len(),
1114                });
1115            }
1116            let mut oriented = vec![V::default(); slice.len()];
1117            polarity.apply(slice, &mut oriented)?;
1118            for (acc, val) in accum.iter_mut().zip(oriented.iter()) {
1119                *acc += val.clone();
1120            }
1121            count += 1;
1122        }
1123        if count > 0 {
1124            let divisor: V = num_traits::FromPrimitive::from_usize(count)
1125                .ok_or(MeshSieveError::SievedArrayPrimitiveConversionFailure(count))?;
1126            for val in accum.iter_mut() {
1127                *val = val.clone() / divisor.clone();
1128            }
1129            coarse.try_set(*coarse_cell, &accum)?;
1130        }
1131    }
1132    Ok(())
1133}
1134
1135fn coarsen_data_with_transfer<V>(
1136    fine: &SievedArray<PointId, V>,
1137    transfer: &[(PointId, Vec<(PointId, Polarity)>)],
1138) -> Result<SievedArray<PointId, V>, MeshSieveError>
1139where
1140    V: Clone
1141        + Default
1142        + num_traits::FromPrimitive
1143        + core::ops::AddAssign
1144        + core::ops::Div<Output = V>,
1145{
1146    let atlas = coarsened_data_atlas(fine, transfer)?;
1147    let mut coarse = SievedArray::<PointId, V>::new(atlas);
1148    assemble_with_sifter(fine, &mut coarse, transfer)?;
1149    Ok(coarse)
1150}
1151
1152/// Transfer a point/cell section through a refinement map by oriented copy.
1153pub fn transfer_section_refinement<V>(
1154    coarse: &SievedArray<PointId, V>,
1155    refinement: &[(PointId, Vec<(PointId, Polarity)>)],
1156) -> Result<SievedArray<PointId, V>, MeshSieveError>
1157where
1158    V: Clone + Default,
1159{
1160    refine_data_with_transfer(coarse, refinement)
1161}
1162
1163/// Transfer a point/cell section through a coarsening map by averaging fine values.
1164pub fn transfer_section_coarsening<V>(
1165    fine: &SievedArray<PointId, V>,
1166    transfer: &[(PointId, Vec<(PointId, Polarity)>)],
1167) -> Result<SievedArray<PointId, V>, MeshSieveError>
1168where
1169    V: Clone
1170        + Default
1171        + num_traits::FromPrimitive
1172        + core::ops::AddAssign
1173        + core::ops::Div<Output = V>,
1174{
1175    coarsen_data_with_transfer(fine, transfer)
1176}
1177
1178/// Transfer labels from coarse entities to refined entities and preserve unchanged labels.
1179pub fn transfer_labels_refinement(
1180    labels: &LabelSet,
1181    refinement: &[(PointId, Vec<(PointId, Polarity)>)],
1182) -> LabelSet {
1183    let mut out = labels.clone();
1184    for (coarse, fine_points) in refinement {
1185        for (name, point, value) in labels.iter() {
1186            if point == *coarse {
1187                for (fine, _) in fine_points {
1188                    out.set_label(*fine, name, value);
1189                }
1190            }
1191        }
1192    }
1193    out
1194}
1195
1196/// Transfer cell-type tags through refinement.
1197pub fn transfer_cell_types_refinement<S>(
1198    cell_types: &Section<CellType, S>,
1199    refinement: &[(PointId, Vec<(PointId, Polarity)>)],
1200) -> Result<Section<CellType, VecStorage<CellType>>, MeshSieveError>
1201where
1202    S: Storage<CellType>,
1203{
1204    let mut atlas = Atlas::default();
1205    let mut entries = Vec::new();
1206    for (coarse, fine_points) in refinement {
1207        let slice = cell_types.try_restrict(*coarse)?;
1208        if slice.len() != 1 {
1209            return Err(MeshSieveError::SliceLengthMismatch {
1210                point: *coarse,
1211                expected: 1,
1212                found: slice.len(),
1213            });
1214        }
1215        for (fine, _) in fine_points {
1216            atlas.try_insert(*fine, 1)?;
1217            entries.push((*fine, slice[0]));
1218        }
1219    }
1220    let mut out = Section::<CellType, VecStorage<CellType>>::new(atlas);
1221    for (point, cell_type) in entries {
1222        out.try_set(point, &[cell_type])?;
1223    }
1224    Ok(out)
1225}
1226
1227/// Transfer ownership through refinement by inheriting the coarse owner.
1228pub fn transfer_ownership_refinement(
1229    ownership: &PointOwnership,
1230    refined: &RefinedMesh,
1231    my_rank: usize,
1232) -> Result<PointOwnership, MeshSieveError> {
1233    let mut out = ownership.clone();
1234    for (coarse, fine_points) in &refined.cell_refinement {
1235        let owner = ownership.owner_or_err(*coarse)?;
1236        for (fine, _) in fine_points {
1237            out.set_owner_min(*fine, owner, my_rank)?;
1238            for point in refined.sieve.cone_points(*fine) {
1239                if out.entry(point).is_none() {
1240                    out.set_owner_min(point, owner, my_rank)?;
1241                }
1242            }
1243        }
1244    }
1245    Ok(out)
1246}
1247
1248/// Transfer anchor-derived hanging constraints for scalar sections.
1249pub fn transfer_constraints_from_anchors(
1250    anchors: &TopologicalAnchors,
1251    dofs_per_point: usize,
1252) -> crate::data::hanging_node_constraints::HangingNodeConstraints<f64> {
1253    crate::data::hanging_node_constraints::constraints_from_topological_anchors(
1254        anchors,
1255        dofs_per_point,
1256    )
1257}
1258
1259/// Transfer coordinates produced by refinement, if available.
1260pub fn transfer_coordinates_refinement(
1261    refined: &RefinedMesh,
1262) -> Option<Coordinates<f64, VecStorage<f64>>> {
1263    refined.coordinates.clone()
1264}
1265
1266/// Apply quality-driven refinement/coarsening with data transfer.
1267///
1268/// When both refinement and coarsening are requested, refinement is applied
1269/// preferentially and coarsening is deferred to a later pass.
1270pub fn adapt_with_quality_and_transfer<S, Cs, V, C>(
1271    sieve: &mut impl Sieve<Point = PointId>,
1272    cell_types: &Section<CellType, S>,
1273    coordinates: &Coordinates<f64, Cs>,
1274    cell_data: &SievedArray<PointId, V>,
1275    coarsen_plan: C,
1276    thresholds: QualityThresholds,
1277) -> Result<AdaptationResult<V>, MeshSieveError>
1278where
1279    S: Storage<CellType>,
1280    Cs: Storage<f64>,
1281    V: Clone
1282        + Default
1283        + num_traits::FromPrimitive
1284        + core::ops::AddAssign
1285        + core::ops::Div<Output = V>,
1286    C: Fn(&[PointId]) -> Vec<CoarsenEntity>,
1287{
1288    let metrics = evaluate_quality_metrics(sieve, cell_types, coordinates)?;
1289    let selection = select_cells_for_adaptation(&metrics, thresholds);
1290    let refine_cells = selection.refine_cells.clone();
1291    let coarsen_cells = selection.coarsen_cells.clone();
1292
1293    if !refine_cells.is_empty() {
1294        let refined_section = subset_cell_types(cell_types, &refine_cells)?;
1295        let mut refined = refine_mesh_with_options(
1296            sieve,
1297            &refined_section,
1298            Some(coordinates),
1299            RefineOptions {
1300                check_geometry: thresholds.check_geometry,
1301                anisotropic_splits: None,
1302            },
1303        )?;
1304        materialize_unmodified_cells(sieve, cell_types, &mut refined, &refine_cells)?;
1305        let refined_data = refine_data_with_transfer(cell_data, &refined.cell_refinement)?;
1306        return Ok(AdaptationResult {
1307            metrics,
1308            refine_cells,
1309            coarsen_cells,
1310            action: AdaptationAction::Refined {
1311                mesh: refined,
1312                data: refined_data,
1313            },
1314        });
1315    }
1316
1317    if !coarsen_cells.is_empty() {
1318        let entities = coarsen_plan(&coarsen_cells);
1319        if entities.is_empty() {
1320            return Ok(AdaptationResult {
1321                metrics,
1322                refine_cells,
1323                coarsen_cells,
1324                action: AdaptationAction::NoChange,
1325            });
1326        }
1327        let coarsened = coarsen_topology(sieve, &entities)?;
1328        let coarsened_data = coarsen_data_with_transfer(cell_data, &coarsened.transfer_map)?;
1329        return Ok(AdaptationResult {
1330            metrics,
1331            refine_cells,
1332            coarsen_cells,
1333            action: AdaptationAction::Coarsened {
1334                mesh: coarsened,
1335                data: coarsened_data,
1336            },
1337        });
1338    }
1339
1340    Ok(AdaptationResult {
1341        metrics,
1342        refine_cells,
1343        coarsen_cells,
1344        action: AdaptationAction::NoChange,
1345    })
1346}
1347
1348fn materialize_unmodified_cells<S>(
1349    sieve: &mut impl Sieve<Point = PointId>,
1350    cell_types: &Section<CellType, S>,
1351    refined: &mut RefinedMesh,
1352    refined_cells: &[PointId],
1353) -> Result<(), MeshSieveError>
1354where
1355    S: Storage<CellType>,
1356{
1357    let refined_set: std::collections::HashSet<_> = refined_cells.iter().copied().collect();
1358    let collapsed = collapse_to_cell_vertices(sieve, cell_types)?;
1359    for (cell, cell_slice) in cell_types.iter() {
1360        if cell_slice.len() != 1 {
1361            return Err(MeshSieveError::SliceLengthMismatch {
1362                point: cell,
1363                expected: 1,
1364                found: cell_slice.len(),
1365            });
1366        }
1367        if refined_set.contains(&cell) {
1368            continue;
1369        }
1370        for vertex in collapsed.cone_points(cell) {
1371            refined.sieve.add_arrow(cell, vertex, ());
1372        }
1373        refined
1374            .cell_refinement
1375            .push((cell, vec![(cell, Polarity::Forward)]));
1376    }
1377    Ok(())
1378}
1379
1380fn filter_split_hints(hints: &[MetricSplitHint], cells: &[PointId]) -> Vec<MetricSplitHint> {
1381    let cell_set: std::collections::HashSet<_> = cells.iter().copied().collect();
1382    hints
1383        .iter()
1384        .filter(|hint| cell_set.contains(&hint.cell))
1385        .cloned()
1386        .collect()
1387}
1388
1389fn apply_boundary_policy(
1390    selection: &mut AdaptivitySelection,
1391    split_hints: &mut Vec<MetricSplitHint>,
1392    labels: Option<&LabelSet>,
1393    policy: &BoundaryRemeshingPolicy,
1394) {
1395    selection
1396        .refine_cells
1397        .retain(|cell| !policy.preserves(labels, *cell));
1398    selection
1399        .coarsen_cells
1400        .retain(|cell| !policy.preserves(labels, *cell));
1401    split_hints.retain(|hint| !policy.preserves(labels, hint.cell));
1402}
1403
1404/// Apply metric-driven refinement/coarsening with boundary-remeshing controls.
1405pub fn adapt_with_metric_policy<S, Cs, Ms, C>(
1406    sieve: &mut impl Sieve<Point = PointId>,
1407    cell_types: &Section<CellType, S>,
1408    coordinates: &Coordinates<f64, Cs>,
1409    metric: &Section<MetricTensor, Ms>,
1410    labels: Option<&LabelSet>,
1411    coarsen_plan: C,
1412    thresholds: MetricThresholds,
1413    options: MetricAdaptationOptions,
1414) -> Result<MetricAdaptationResult<()>, MeshSieveError>
1415where
1416    S: Storage<CellType>,
1417    Cs: Storage<f64>,
1418    Ms: Storage<MetricTensor>,
1419    C: Fn(&[MetricSplitHint]) -> Vec<CoarsenEntity>,
1420{
1421    if let MetricRemeshingBackend::External(backend) = options.backend {
1422        let feature = backend
1423            .cargo_feature()
1424            .unwrap_or("no cargo feature is currently available");
1425        let availability = if backend.is_feature_enabled() {
1426            "the mesh-generation feature is enabled; invoke the corresponding meshgen remesher to obtain a MeshData round trip"
1427        } else {
1428            "enable the backend feature before invoking the corresponding meshgen remesher"
1429        };
1430        return Err(MeshSieveError::InvalidGeometry(format!(
1431            "external metric remeshing backend {backend:?} is selected through MetricRemeshingBackend; {availability}. Required feature: {feature}."
1432        )));
1433    }
1434    let normalized_metric = normalized_metric_section(metric, options.normalization)?;
1435    let evaluations = evaluate_metric_cells(sieve, cell_types, coordinates, &normalized_metric)?;
1436    let metrics: Vec<_> = evaluations
1437        .iter()
1438        .map(|eval| (eval.cell, eval.metrics))
1439        .collect();
1440    let mut split_hints = metric_split_hints(&evaluations, thresholds);
1441    let mut selection = select_cells_for_metric_adaptation(&metrics, thresholds);
1442    apply_boundary_policy(
1443        &mut selection,
1444        &mut split_hints,
1445        labels,
1446        &options.boundary_policy,
1447    );
1448    let refine_cells = selection.refine_cells.clone();
1449    let coarsen_cells = selection.coarsen_cells.clone();
1450
1451    if !refine_cells.is_empty() {
1452        let refined_section = subset_cell_types(cell_types, &refine_cells)?;
1453        let refine_hints = filter_split_hints(&split_hints, &refine_cells);
1454        let anisotropic = build_anisotropic_hints(&refine_hints);
1455        let mut refined = refine_mesh_with_options(
1456            sieve,
1457            &refined_section,
1458            Some(coordinates),
1459            RefineOptions {
1460                check_geometry: thresholds.check_geometry,
1461                anisotropic_splits: if anisotropic.is_empty() {
1462                    None
1463                } else {
1464                    Some(anisotropic)
1465                },
1466            },
1467        )?;
1468        materialize_unmodified_cells(sieve, cell_types, &mut refined, &refine_cells)?;
1469        return Ok(MetricAdaptationResult {
1470            metrics,
1471            refine_cells,
1472            coarsen_cells,
1473            split_hints,
1474            provenance: AdaptationProvenanceMap::from_refinement(&refined.cell_refinement),
1475            action: MetricAdaptationAction::Refined { mesh: refined },
1476            data: None,
1477        });
1478    }
1479
1480    if !coarsen_cells.is_empty() {
1481        let coarsen_hints = filter_split_hints(&split_hints, &coarsen_cells);
1482        let entities = coarsen_plan(&coarsen_hints);
1483        if !entities.is_empty() {
1484            let coarsened = coarsen_topology(sieve, &entities)?;
1485            return Ok(MetricAdaptationResult {
1486                metrics,
1487                refine_cells,
1488                coarsen_cells,
1489                split_hints,
1490                provenance: AdaptationProvenanceMap::from_coarsening(&coarsened.transfer_map),
1491                action: MetricAdaptationAction::Coarsened { mesh: coarsened },
1492                data: None,
1493            });
1494        }
1495    }
1496
1497    Ok(MetricAdaptationResult {
1498        metrics,
1499        refine_cells,
1500        coarsen_cells,
1501        split_hints,
1502        action: MetricAdaptationAction::NoChange,
1503        data: None,
1504        provenance: AdaptationProvenanceMap::default(),
1505    })
1506}
1507
1508/// Apply metric-driven refinement/coarsening without data transfer.
1509///
1510/// This mirrors DMPlex-style "adapt with metric" workflows by using a per-cell metric tensor
1511/// to decide refinement, coarsening, and anisotropic split hints.
1512pub fn adapt_with_metric<S, Cs, Ms, C>(
1513    sieve: &mut impl Sieve<Point = PointId>,
1514    cell_types: &Section<CellType, S>,
1515    coordinates: &Coordinates<f64, Cs>,
1516    metric: &Section<MetricTensor, Ms>,
1517    coarsen_plan: C,
1518    thresholds: MetricThresholds,
1519) -> Result<MetricAdaptationResult<()>, MeshSieveError>
1520where
1521    S: Storage<CellType>,
1522    Cs: Storage<f64>,
1523    Ms: Storage<MetricTensor>,
1524    C: Fn(&[MetricSplitHint]) -> Vec<CoarsenEntity>,
1525{
1526    let evaluations = evaluate_metric_cells(sieve, cell_types, coordinates, metric)?;
1527    let metrics: Vec<_> = evaluations
1528        .iter()
1529        .map(|eval| (eval.cell, eval.metrics))
1530        .collect();
1531    let split_hints = metric_split_hints(&evaluations, thresholds);
1532    let selection = select_cells_for_metric_adaptation(&metrics, thresholds);
1533    let refine_cells = selection.refine_cells.clone();
1534    let coarsen_cells = selection.coarsen_cells.clone();
1535
1536    if !refine_cells.is_empty() {
1537        let refined_section = subset_cell_types(cell_types, &refine_cells)?;
1538        let refine_hints = filter_split_hints(&split_hints, &refine_cells);
1539        let anisotropic = build_anisotropic_hints(&refine_hints);
1540        let mut refined = refine_mesh_with_options(
1541            sieve,
1542            &refined_section,
1543            Some(coordinates),
1544            RefineOptions {
1545                check_geometry: thresholds.check_geometry,
1546                anisotropic_splits: if anisotropic.is_empty() {
1547                    None
1548                } else {
1549                    Some(anisotropic)
1550                },
1551            },
1552        )?;
1553        materialize_unmodified_cells(sieve, cell_types, &mut refined, &refine_cells)?;
1554        return Ok(MetricAdaptationResult {
1555            metrics,
1556            refine_cells,
1557            coarsen_cells,
1558            split_hints,
1559            provenance: AdaptationProvenanceMap::from_refinement(&refined.cell_refinement),
1560            action: MetricAdaptationAction::Refined { mesh: refined },
1561            data: None,
1562        });
1563    }
1564
1565    if !coarsen_cells.is_empty() {
1566        let coarsen_hints = filter_split_hints(&split_hints, &coarsen_cells);
1567        let entities = coarsen_plan(&coarsen_hints);
1568        if entities.is_empty() {
1569            return Ok(MetricAdaptationResult {
1570                metrics,
1571                refine_cells,
1572                coarsen_cells,
1573                split_hints,
1574                action: MetricAdaptationAction::NoChange,
1575                data: None,
1576                provenance: AdaptationProvenanceMap::default(),
1577            });
1578        }
1579        let coarsened = coarsen_topology(sieve, &entities)?;
1580        return Ok(MetricAdaptationResult {
1581            metrics,
1582            refine_cells,
1583            coarsen_cells,
1584            split_hints,
1585            provenance: AdaptationProvenanceMap::from_coarsening(&coarsened.transfer_map),
1586            action: MetricAdaptationAction::Coarsened { mesh: coarsened },
1587            data: None,
1588        });
1589    }
1590
1591    Ok(MetricAdaptationResult {
1592        metrics,
1593        refine_cells,
1594        coarsen_cells,
1595        split_hints,
1596        action: MetricAdaptationAction::NoChange,
1597        data: None,
1598        provenance: AdaptationProvenanceMap::default(),
1599    })
1600}
1601
1602/// Apply metric-driven refinement/coarsening with data transfer.
1603///
1604/// When both refinement and coarsening are requested, refinement is applied
1605/// preferentially and coarsening is deferred to a later pass.
1606pub fn adapt_with_metric_and_transfer<S, Cs, Ms, V, C>(
1607    sieve: &mut impl Sieve<Point = PointId>,
1608    cell_types: &Section<CellType, S>,
1609    coordinates: &Coordinates<f64, Cs>,
1610    metric: &Section<MetricTensor, Ms>,
1611    cell_data: &SievedArray<PointId, V>,
1612    coarsen_plan: C,
1613    thresholds: MetricThresholds,
1614) -> Result<MetricAdaptationResult<V>, MeshSieveError>
1615where
1616    S: Storage<CellType>,
1617    Cs: Storage<f64>,
1618    Ms: Storage<MetricTensor>,
1619    V: Clone
1620        + Default
1621        + num_traits::FromPrimitive
1622        + core::ops::AddAssign
1623        + core::ops::Div<Output = V>,
1624    C: Fn(&[MetricSplitHint]) -> Vec<CoarsenEntity>,
1625{
1626    let evaluations = evaluate_metric_cells(sieve, cell_types, coordinates, metric)?;
1627    let metrics: Vec<_> = evaluations
1628        .iter()
1629        .map(|eval| (eval.cell, eval.metrics))
1630        .collect();
1631    let split_hints = metric_split_hints(&evaluations, thresholds);
1632    let selection = select_cells_for_metric_adaptation(&metrics, thresholds);
1633    let refine_cells = selection.refine_cells.clone();
1634    let coarsen_cells = selection.coarsen_cells.clone();
1635
1636    if !refine_cells.is_empty() {
1637        let refined_section = subset_cell_types(cell_types, &refine_cells)?;
1638        let refine_hints = filter_split_hints(&split_hints, &refine_cells);
1639        let anisotropic = build_anisotropic_hints(&refine_hints);
1640        let mut refined = refine_mesh_with_options(
1641            sieve,
1642            &refined_section,
1643            Some(coordinates),
1644            RefineOptions {
1645                check_geometry: thresholds.check_geometry,
1646                anisotropic_splits: if anisotropic.is_empty() {
1647                    None
1648                } else {
1649                    Some(anisotropic)
1650                },
1651            },
1652        )?;
1653        materialize_unmodified_cells(sieve, cell_types, &mut refined, &refine_cells)?;
1654        let refined_data = refine_data_with_transfer(cell_data, &refined.cell_refinement)?;
1655        return Ok(MetricAdaptationResult {
1656            metrics,
1657            refine_cells,
1658            coarsen_cells,
1659            split_hints,
1660            provenance: AdaptationProvenanceMap::from_refinement(&refined.cell_refinement),
1661            action: MetricAdaptationAction::Refined { mesh: refined },
1662            data: Some(refined_data),
1663        });
1664    }
1665
1666    if !coarsen_cells.is_empty() {
1667        let coarsen_hints = filter_split_hints(&split_hints, &coarsen_cells);
1668        let entities = coarsen_plan(&coarsen_hints);
1669        if entities.is_empty() {
1670            return Ok(MetricAdaptationResult {
1671                metrics,
1672                refine_cells,
1673                coarsen_cells,
1674                split_hints,
1675                action: MetricAdaptationAction::NoChange,
1676                data: None,
1677                provenance: AdaptationProvenanceMap::default(),
1678            });
1679        }
1680        let coarsened = coarsen_topology(sieve, &entities)?;
1681        let coarsened_data = coarsen_data_with_transfer(cell_data, &coarsened.transfer_map)?;
1682        return Ok(MetricAdaptationResult {
1683            metrics,
1684            refine_cells,
1685            coarsen_cells,
1686            split_hints,
1687            provenance: AdaptationProvenanceMap::from_coarsening(&coarsened.transfer_map),
1688            action: MetricAdaptationAction::Coarsened { mesh: coarsened },
1689            data: Some(coarsened_data),
1690        });
1691    }
1692
1693    Ok(MetricAdaptationResult {
1694        metrics,
1695        refine_cells,
1696        coarsen_cells,
1697        split_hints,
1698        action: MetricAdaptationAction::NoChange,
1699        data: None,
1700        provenance: AdaptationProvenanceMap::default(),
1701    })
1702}
1703
1704#[cfg(test)]
1705mod tests {
1706    use super::*;
1707
1708    #[test]
1709    fn fvm_threshold_selector_reduces_refine_pressure_over_passes() {
1710        let thresholds = FvStabilityThresholds::default();
1711        let pass0 = [
1712            FvmQualityOutputs {
1713                non_orthogonality_deg: 89.0,
1714                skewness: 5.0,
1715                characteristic_length: 1.0e-10,
1716            },
1717            FvmQualityOutputs {
1718                non_orthogonality_deg: 80.0,
1719                skewness: 2.5,
1720                characteristic_length: 1.0e-7,
1721            },
1722            FvmQualityOutputs {
1723                non_orthogonality_deg: 20.0,
1724                skewness: 0.9,
1725                characteristic_length: 1.0e-3,
1726            },
1727        ];
1728        let pass1 = [
1729            FvmQualityOutputs {
1730                non_orthogonality_deg: 74.0,
1731                skewness: 3.9,
1732                characteristic_length: 1.0e-6,
1733            },
1734            FvmQualityOutputs {
1735                non_orthogonality_deg: 68.0,
1736                skewness: 2.2,
1737                characteristic_length: 1.0e-5,
1738            },
1739            FvmQualityOutputs {
1740                non_orthogonality_deg: 19.0,
1741                skewness: 0.7,
1742                characteristic_length: 1.0e-3,
1743            },
1744        ];
1745        let pass2 = [
1746            FvmQualityOutputs {
1747                non_orthogonality_deg: 40.0,
1748                skewness: 1.2,
1749                characteristic_length: 1.0e-4,
1750            },
1751            FvmQualityOutputs {
1752                non_orthogonality_deg: 33.0,
1753                skewness: 1.1,
1754                characteristic_length: 1.0e-4,
1755            },
1756            FvmQualityOutputs {
1757                non_orthogonality_deg: 17.0,
1758                skewness: 0.5,
1759                characteristic_length: 1.0e-3,
1760            },
1761        ];
1762
1763        let refine0 = pass0
1764            .iter()
1765            .filter(|q| should_refine_from_fvm_quality(**q, thresholds))
1766            .count();
1767        let refine1 = pass1
1768            .iter()
1769            .filter(|q| should_refine_from_fvm_quality(**q, thresholds))
1770            .count();
1771        let refine2 = pass2
1772            .iter()
1773            .filter(|q| should_refine_from_fvm_quality(**q, thresholds))
1774            .count();
1775        assert!(refine0 >= refine1 && refine1 >= refine2);
1776        assert_eq!(refine2, 0);
1777    }
1778}