Skip to main content

mesh_sieve/
dm_options.rs

1//! DMPLEX-style configuration profiles for [`crate::dm::MeshDM`].
2//!
3//! This module is deliberately separate from the core topology and section data
4//! structures.  It provides a serializable, PETSc-DMPLEX-familiar option layer
5//! that applications can load from JSON/TOML-like configuration, environment
6//! variables, or command-line arguments, then lower into mesh-sieve's native
7//! runtime structs.
8//!
9//! # PETSc DMPLEX option mapping
10//!
11//! | PETSc-style option | mesh-sieve field / builder target |
12//! | --- | --- |
13//! | `-dm_refine_pre <n>` | [`DmplexRefinementProfile::pre_refine`] → [`crate::dm::MeshDMOptions::pre_refine`] |
14//! | `-dm_refine <n>` / `-dm_refine_post <n>` | [`DmplexRefinementProfile::post_refine`] → [`crate::dm::MeshDMOptions::post_refine`] |
15//! | `-dm_distribute <bool>` | [`DmplexDistributionProfile::distribute`] → [`crate::dm::MeshDMOptions::distribute`] |
16//! | `-dm_distribute_overlap <n>` | [`DmplexOverlapProfile::depth`] → [`crate::algs::distribute::DistributionConfig::overlap_depth`] |
17//! | `-dm_plex_localize <bool>` | [`DmplexDistributionProfile::localize_coordinates`] → [`crate::dm::MeshDMOptions::localize_coordinates`] |
18//! | `-dm_plex_check_all <bool>` | [`DmplexCheckProfile::check_all`] → [`crate::diagnostics::MeshCheckOptions::all`] |
19//! | `-dm_plex_check_symmetry <bool>` | [`DmplexCheckProfile::check_symmetry`] → [`crate::diagnostics::MeshCheckOptions::check_symmetry`] |
20//! | `-dm_plex_check_skeleton <bool>` | [`DmplexCheckProfile::check_skeleton`] → [`crate::diagnostics::MeshCheckOptions::check_skeleton`] |
21//! | `-dm_plex_check_faces <bool>` | [`DmplexCheckProfile::check_faces`] → [`crate::diagnostics::MeshCheckOptions::check_faces`] |
22//! | `-dm_plex_check_geometry <bool>` | [`DmplexCheckProfile::check_geometry`] → [`crate::diagnostics::MeshCheckOptions::check_geometry`] |
23//! | `-dm_plex_reorder_section <vertex_first|cell_first>` | [`DmplexConfigProfile::ordering`] → [`crate::dm::MeshDMOptions::reorder_section`] |
24//! | `-dm_plex_metric_target_complexity <x>` | [`DmplexMetricProfile::target_complexity`] → metric normalization |
25//! | `-dm_plex_metric_gradation <x>` | [`DmplexMetricProfile::gradation`] → metric normalization |
26//! | `-dm_plex_metric_h_min <x>` | [`DmplexMetricProfile::min_magnitude`] → metric normalization |
27//! | `-dm_plex_metric_h_max <x>` | [`DmplexMetricProfile::max_magnitude`] → metric normalization |
28//! | `-dm_plex_metric_a_min <x>` | [`DmplexMetricProfile::min_anisotropy`] → metric normalization |
29//! | `-dm_plex_metric_a_max <x>` | [`DmplexMetricProfile::max_anisotropy`] → metric normalization |
30//! | `-dm_plex_metric_hausdorff_number <x>` | [`DmplexMetricProfile::hausdorff_number`] → metric normalization |
31//! | `-dm_plex_metric_backend <internal|triangle|tetgen|gmsh|mmg>` | [`DmplexMetricProfile::backend`] → [`crate::adapt::MetricRemeshingBackend`] |
32//! | `-dm_plex_filename <path>` | [`DmplexIoProfile::filename`] for application I/O plumbing |
33//! | `-dm_plex_format <name>` | [`DmplexIoProfile::format`] for application I/O plumbing |
34//! | `-dm_plex_interpolate <bool>` | [`DmplexIoProfile::interpolate`] for application I/O plumbing |
35//!
36//! The parser also accepts mesh-sieve-specific preparation options such as
37//! `-dm_prepare_require_coordinates`, `-dm_prepare_require_cell_types`,
38//! `-dm_prepare_require_ownership`, `-dm_prepare_require_overlap`,
39//! `-dm_prepare_synchronize_ghost_sections`, and
40//! `-dm_prepare_create_serial_ownership`.
41
42use std::env;
43use std::ffi::OsString;
44
45use serde::{Deserialize, Serialize};
46
47use crate::adapt::{
48    ExternalRemeshingBackend, FvStabilityThresholds, MetricNormalizationControls,
49    MetricRemeshingBackend, MetricThresholds,
50};
51use crate::algs::distribute::DistributionConfig;
52use crate::algs::renumber::StratifiedOrdering;
53use crate::diagnostics::{MeshCheckOptions, PrepareForSolveOptions};
54use crate::dm::{
55    MeshDMAdaptLabelPolicy, MeshDMMetricAdaptOptions, MeshDMOptions, MeshDMTransferStrategy,
56};
57
58/// Top-level DMPLEX-style configuration profile.
59#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
60pub struct DmplexConfigProfile {
61    /// Refinement controls matching `-dm_refine_pre` and `-dm_refine` workflows.
62    pub refinement: DmplexRefinementProfile,
63    /// Distribution and ghost-section synchronization controls.
64    pub distribution: DmplexDistributionProfile,
65    /// Overlap/ghost-layer controls.
66    pub overlap: DmplexOverlapProfile,
67    /// DMPLEX-style diagnostic switches.
68    pub checks: DmplexCheckProfile,
69    /// Metric adaptation and remeshing controls.
70    pub metric: DmplexMetricProfile,
71    /// Mesh I/O options carried for application-level loaders/writers.
72    pub io: DmplexIoProfile,
73    /// Optional topology-stratified section ordering during setup.
74    pub ordering: Option<DmplexOrderingProfile>,
75    /// Solver preparation prerequisite controls.
76    pub solver: DmplexSolverPreparationProfile,
77}
78
79/// Refinement pass counters.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
81pub struct DmplexRefinementProfile {
82    /// Number of pre-distribution refinement passes (`-dm_refine_pre`).
83    pub pre_refine: usize,
84    /// Number of regular/post-distribution refinement passes (`-dm_refine`).
85    pub post_refine: usize,
86}
87
88/// Distribution controls.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct DmplexDistributionProfile {
91    /// Enable mesh distribution (`-dm_distribute`).
92    pub distribute: bool,
93    /// Ensure coordinates are localized on ghost points (`-dm_plex_localize`).
94    pub localize_coordinates: bool,
95    /// Synchronize section data onto ghost points.
96    pub synchronize_sections: bool,
97    /// Balance ownership of partition-boundary points.
98    pub balance_boundary_ownership: bool,
99}
100
101impl Default for DmplexDistributionProfile {
102    fn default() -> Self {
103        Self {
104            distribute: false,
105            localize_coordinates: false,
106            synchronize_sections: true,
107            balance_boundary_ownership: false,
108        }
109    }
110}
111
112/// Overlap/ghost-layer controls.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub struct DmplexOverlapProfile {
115    /// Ghost layer depth (`-dm_distribute_overlap`).
116    pub depth: usize,
117}
118
119impl Default for DmplexOverlapProfile {
120    fn default() -> Self {
121        Self { depth: 1 }
122    }
123}
124
125/// Mesh validation switches.
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
127pub struct DmplexCheckProfile {
128    pub check_symmetry: bool,
129    pub check_skeleton: bool,
130    pub check_faces: bool,
131    pub check_geometry: bool,
132    pub check_overlap: bool,
133    pub check_ownership: bool,
134    pub check_sections: bool,
135    pub check_all: bool,
136}
137
138/// Serializable section/topology ordering values.
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum DmplexOrderingProfile {
142    VertexFirst,
143    CellFirst,
144}
145
146impl From<DmplexOrderingProfile> for StratifiedOrdering {
147    fn from(value: DmplexOrderingProfile) -> Self {
148        match value {
149            DmplexOrderingProfile::VertexFirst => StratifiedOrdering::VertexFirst,
150            DmplexOrderingProfile::CellFirst => StratifiedOrdering::CellFirst,
151        }
152    }
153}
154
155/// Serializable transfer policy for adaptation workflows.
156#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum DmplexTransferProfile {
159    #[default]
160    TopologyOnly,
161    PreserveLabels,
162    PreserveCoordinatesAndLabels,
163    PreserveAll,
164}
165
166impl From<DmplexTransferProfile> for MeshDMTransferStrategy {
167    fn from(value: DmplexTransferProfile) -> Self {
168        match value {
169            DmplexTransferProfile::TopologyOnly => MeshDMTransferStrategy::TopologyOnly,
170            DmplexTransferProfile::PreserveLabels => MeshDMTransferStrategy::PreserveLabels,
171            DmplexTransferProfile::PreserveCoordinatesAndLabels => {
172                MeshDMTransferStrategy::PreserveCoordinatesAndLabels
173            }
174            DmplexTransferProfile::PreserveAll => MeshDMTransferStrategy::PreserveAll,
175        }
176    }
177}
178
179/// Serializable remeshing backend selector.
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub enum DmplexMetricBackendProfile {
183    #[default]
184    Internal,
185    Triangle,
186    TetGen,
187    Gmsh,
188    Mmg,
189}
190
191impl From<DmplexMetricBackendProfile> for MetricRemeshingBackend {
192    fn from(value: DmplexMetricBackendProfile) -> Self {
193        match value {
194            DmplexMetricBackendProfile::Internal => MetricRemeshingBackend::Internal,
195            DmplexMetricBackendProfile::Triangle => {
196                MetricRemeshingBackend::External(ExternalRemeshingBackend::Triangle)
197            }
198            DmplexMetricBackendProfile::TetGen => {
199                MetricRemeshingBackend::External(ExternalRemeshingBackend::TetGen)
200            }
201            DmplexMetricBackendProfile::Gmsh => {
202                MetricRemeshingBackend::External(ExternalRemeshingBackend::Gmsh)
203            }
204            DmplexMetricBackendProfile::Mmg => {
205                MetricRemeshingBackend::External(ExternalRemeshingBackend::Mmg)
206            }
207        }
208    }
209}
210
211/// Metric-adaptation profile mirroring common DMPLEX metric options.
212#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
213pub struct DmplexMetricProfile {
214    pub refine_max_edge_length: f64,
215    pub coarsen_max_edge_length: f64,
216    pub split_edge_length: f64,
217    pub split_face_length: f64,
218    pub check_geometry: bool,
219    pub target_complexity: Option<f64>,
220    pub gradation: Option<f64>,
221    pub hausdorff_number: Option<f64>,
222    pub min_anisotropy: Option<f64>,
223    pub max_anisotropy: Option<f64>,
224    pub min_magnitude: Option<f64>,
225    pub max_magnitude: Option<f64>,
226    pub backend: DmplexMetricBackendProfile,
227    pub transfer: DmplexTransferProfile,
228    pub fixed_boundary_labels: Vec<(String, i32)>,
229    pub protected_region_labels: Vec<(String, i32)>,
230    pub relax_region_labels: Vec<(String, i32)>,
231}
232
233impl Default for DmplexMetricProfile {
234    fn default() -> Self {
235        let thresholds = MetricThresholds::default();
236        Self {
237            refine_max_edge_length: thresholds.refine_max_edge_length,
238            coarsen_max_edge_length: thresholds.coarsen_max_edge_length,
239            split_edge_length: thresholds.split_edge_length,
240            split_face_length: thresholds.split_face_length,
241            check_geometry: thresholds.check_geometry,
242            target_complexity: None,
243            gradation: None,
244            hausdorff_number: None,
245            min_anisotropy: None,
246            max_anisotropy: None,
247            min_magnitude: None,
248            max_magnitude: None,
249            backend: DmplexMetricBackendProfile::Internal,
250            transfer: DmplexTransferProfile::TopologyOnly,
251            fixed_boundary_labels: Vec::new(),
252            protected_region_labels: Vec::new(),
253            relax_region_labels: Vec::new(),
254        }
255    }
256}
257
258/// Application-level mesh I/O profile.
259#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
260pub struct DmplexIoProfile {
261    pub filename: Option<String>,
262    pub format: Option<String>,
263    pub interpolate: Option<bool>,
264    pub use_viewer: Option<String>,
265}
266
267/// Solver-preparation prerequisite profile.
268#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub struct DmplexSolverPreparationProfile {
270    pub require_coordinates: bool,
271    pub require_cell_types: bool,
272    pub require_ownership: bool,
273    pub require_overlap: bool,
274    pub synchronize_ghost_sections: bool,
275    pub create_serial_ownership: bool,
276}
277
278impl Default for DmplexSolverPreparationProfile {
279    fn default() -> Self {
280        let options = PrepareForSolveOptions::default();
281        Self {
282            require_coordinates: options.require_coordinates,
283            require_cell_types: options.require_cell_types,
284            require_ownership: options.require_ownership,
285            require_overlap: options.require_overlap,
286            synchronize_ghost_sections: options.synchronize_ghost_sections,
287            create_serial_ownership: options.create_serial_ownership,
288        }
289    }
290}
291
292impl DmplexMetricProfile {
293    /// Lower numeric metric thresholds into mesh-sieve's adaptation thresholds.
294    pub fn metric_thresholds(&self) -> MetricThresholds {
295        MetricThresholds {
296            refine_max_edge_length: self.refine_max_edge_length,
297            coarsen_max_edge_length: self.coarsen_max_edge_length,
298            split_edge_length: self.split_edge_length,
299            split_face_length: self.split_face_length,
300            check_geometry: self.check_geometry,
301        }
302    }
303
304    /// Lower DMPLEX-style normalization controls.
305    pub fn normalization_controls(&self) -> MetricNormalizationControls {
306        MetricNormalizationControls {
307            target_complexity: self.target_complexity,
308            gradation: self.gradation,
309            hausdorff_number: self.hausdorff_number,
310            min_anisotropy: self.min_anisotropy,
311            max_anisotropy: self.max_anisotropy,
312            min_magnitude: self.min_magnitude,
313            max_magnitude: self.max_magnitude,
314        }
315    }
316
317    /// Lower label policy fields into the DM-level adaptation policy.
318    pub fn label_policy(&self) -> MeshDMAdaptLabelPolicy {
319        MeshDMAdaptLabelPolicy {
320            fixed_boundary_labels: self.fixed_boundary_labels.clone(),
321            protected_region_labels: self.protected_region_labels.clone(),
322            relax_region_labels: self.relax_region_labels.clone(),
323        }
324    }
325
326    /// Lower this profile into DM metric adaptation options.
327    pub fn metric_adapt_options(&self) -> MeshDMMetricAdaptOptions {
328        MeshDMMetricAdaptOptions {
329            thresholds: self.metric_thresholds(),
330            labels: self.label_policy(),
331            transfer: self.transfer.into(),
332            normalization: self.normalization_controls(),
333            backend: self.backend.into(),
334        }
335    }
336}
337
338impl DmplexConfigProfile {
339    /// Lower setup-related fields into [`MeshDMOptions`].
340    pub fn mesh_dm_options(&self) -> MeshDMOptions {
341        MeshDMOptions {
342            pre_refine: self.refinement.pre_refine,
343            distribute: self.distribution.distribute,
344            distribute_overlap: self.overlap.depth,
345            post_refine: self.refinement.post_refine,
346            localize_coordinates: self.distribution.localize_coordinates,
347            check_symmetry: self.checks.check_symmetry,
348            check_skeleton: self.checks.check_skeleton,
349            check_faces: self.checks.check_faces,
350            check_geometry: self.checks.check_geometry,
351            check_all: self.checks.check_all,
352            reorder_section: self.ordering.map(Into::into),
353            balance_boundary_ownership: self.distribution.balance_boundary_ownership,
354            synchronize_sections: self.distribution.synchronize_sections,
355        }
356    }
357
358    /// Lower distribution fields into [`DistributionConfig`].
359    pub fn distribution_config(&self) -> DistributionConfig {
360        DistributionConfig {
361            overlap_depth: self.overlap.depth,
362            synchronize_sections: self.distribution.synchronize_sections
363                || self.distribution.localize_coordinates,
364            balance_boundary_ownership: self.distribution.balance_boundary_ownership,
365        }
366    }
367
368    /// Lower diagnostic switches into [`MeshCheckOptions`].
369    pub fn mesh_check_options(&self) -> MeshCheckOptions {
370        if self.checks.check_all {
371            MeshCheckOptions::all()
372        } else {
373            MeshCheckOptions {
374                check_symmetry: self.checks.check_symmetry,
375                check_skeleton: self.checks.check_skeleton,
376                check_faces: self.checks.check_faces,
377                check_geometry: self.checks.check_geometry,
378                check_overlap: self.checks.check_overlap,
379                check_ownership: self.checks.check_ownership,
380                check_sections: self.checks.check_sections,
381            }
382        }
383    }
384
385    /// Lower solver-preparation fields into [`PrepareForSolveOptions`].
386    pub fn prepare_for_solve_options(&self) -> PrepareForSolveOptions {
387        PrepareForSolveOptions {
388            require_coordinates: self.solver.require_coordinates,
389            require_cell_types: self.solver.require_cell_types,
390            require_ownership: self.solver.require_ownership,
391            require_overlap: self.solver.require_overlap,
392            synchronize_ghost_sections: self.solver.synchronize_ghost_sections,
393            create_serial_ownership: self.solver.create_serial_ownership,
394        }
395    }
396
397    /// Lower metric adaptation fields into [`MeshDMMetricAdaptOptions`].
398    pub fn metric_adapt_options(&self) -> MeshDMMetricAdaptOptions {
399        self.metric.metric_adapt_options()
400    }
401
402    /// Parse PETSc/DMPLEX-style command-line options from any string iterator.
403    ///
404    /// Unknown options are ignored so applications can pass a full PETSc-like
405    /// argv without pre-filtering mesh-sieve keys.
406    pub fn from_cli_args<I, S>(args: I) -> Result<Self, String>
407    where
408        I: IntoIterator<Item = S>,
409        S: Into<OsString>,
410    {
411        let mut profile = Self::default();
412        profile.apply_cli_args(args)?;
413        Ok(profile)
414    }
415
416    /// Start with defaults and apply supported environment variables.
417    ///
418    /// Variables are uppercase option names without a leading dash, for example
419    /// `DM_DISTRIBUTE=true`, `DM_DISTRIBUTE_OVERLAP=2`, or
420    /// `DM_PLEX_METRIC_TARGET_COMPLEXITY=1000.0`.
421    pub fn from_env() -> Result<Self, String> {
422        let mut profile = Self::default();
423        profile.apply_env()?;
424        Ok(profile)
425    }
426
427    /// Apply PETSc/DMPLEX-style CLI arguments to an existing profile.
428    pub fn apply_cli_args<I, S>(&mut self, args: I) -> Result<(), String>
429    where
430        I: IntoIterator<Item = S>,
431        S: Into<OsString>,
432    {
433        let tokens: Vec<String> = args
434            .into_iter()
435            .map(|arg| arg.into().to_string_lossy().into_owned())
436            .collect();
437        let mut index = 0;
438        while index < tokens.len() {
439            let token = &tokens[index];
440            if !token.starts_with('-') {
441                index += 1;
442                continue;
443            }
444            let (key, inline_value) = split_key_value(token);
445            let next_is_value = tokens
446                .get(index + 1)
447                .is_some_and(|next| !next.starts_with('-'));
448            let value = inline_value.or_else(|| next_is_value.then(|| tokens[index + 1].as_str()));
449            self.apply_option(key, value)?;
450            index += if inline_value.is_none() && next_is_value {
451                2
452            } else {
453                1
454            };
455        }
456        Ok(())
457    }
458
459    /// Apply supported environment variables to an existing profile.
460    pub fn apply_env(&mut self) -> Result<(), String> {
461        for (key, value) in env::vars() {
462            if let Some(option) = env_key_to_option(&key) {
463                self.apply_option(option.as_str(), Some(value.as_str()))?;
464            }
465        }
466        Ok(())
467    }
468
469    /// Apply one PETSc-style option. The key may include or omit the leading `-`.
470    pub fn apply_option(&mut self, key: &str, value: Option<&str>) -> Result<bool, String> {
471        let key = normalize_key(key);
472        match key.as_str() {
473            "dm_refine_pre" => self.refinement.pre_refine = parse_value(key.as_str(), value)?,
474            "dm_refine" | "dm_refine_post" => {
475                self.refinement.post_refine = parse_value(key.as_str(), value)?
476            }
477            "dm_distribute" => self.distribution.distribute = parse_bool_flag(key.as_str(), value)?,
478            "dm_distribute_overlap" => self.overlap.depth = parse_value(key.as_str(), value)?,
479            "dm_plex_localize" | "dm_localize" => {
480                self.distribution.localize_coordinates = parse_bool_flag(key.as_str(), value)?
481            }
482            "dm_distribute_synchronize_sections" | "dm_synchronize_sections" => {
483                self.distribution.synchronize_sections = parse_bool_flag(key.as_str(), value)?
484            }
485            "dm_distribute_balance_boundary_ownership" | "dm_balance_boundary_ownership" => {
486                self.distribution.balance_boundary_ownership = parse_bool_flag(key.as_str(), value)?
487            }
488            "dm_plex_check_all" => self.checks.check_all = parse_bool_flag(key.as_str(), value)?,
489            "dm_plex_check_symmetry" => {
490                self.checks.check_symmetry = parse_bool_flag(key.as_str(), value)?
491            }
492            "dm_plex_check_skeleton" => {
493                self.checks.check_skeleton = parse_bool_flag(key.as_str(), value)?
494            }
495            "dm_plex_check_faces" => {
496                self.checks.check_faces = parse_bool_flag(key.as_str(), value)?
497            }
498            "dm_plex_check_geometry" => {
499                self.checks.check_geometry = parse_bool_flag(key.as_str(), value)?
500            }
501            "dm_plex_check_overlap" => {
502                self.checks.check_overlap = parse_bool_flag(key.as_str(), value)?
503            }
504            "dm_plex_check_ownership" => {
505                self.checks.check_ownership = parse_bool_flag(key.as_str(), value)?
506            }
507            "dm_plex_check_sections" => {
508                self.checks.check_sections = parse_bool_flag(key.as_str(), value)?
509            }
510            "dm_plex_reorder_section" | "dm_reorder_section" => {
511                self.ordering = Some(parse_ordering(key.as_str(), value)?)
512            }
513            "dm_plex_metric_refine_max_edge_length" => {
514                self.metric.refine_max_edge_length = parse_value(key.as_str(), value)?
515            }
516            "dm_plex_metric_coarsen_max_edge_length" => {
517                self.metric.coarsen_max_edge_length = parse_value(key.as_str(), value)?
518            }
519            "dm_plex_metric_split_edge_length" => {
520                self.metric.split_edge_length = parse_value(key.as_str(), value)?
521            }
522            "dm_plex_metric_split_face_length" => {
523                self.metric.split_face_length = parse_value(key.as_str(), value)?
524            }
525            "dm_plex_metric_check_geometry" => {
526                self.metric.check_geometry = parse_bool_flag(key.as_str(), value)?
527            }
528            "dm_plex_metric_target_complexity" => {
529                self.metric.target_complexity = Some(parse_value(key.as_str(), value)?)
530            }
531            "dm_plex_metric_gradation" => {
532                self.metric.gradation = Some(parse_value(key.as_str(), value)?)
533            }
534            "dm_plex_metric_hausdorff_number" | "dm_plex_metric_hausdorff" => {
535                self.metric.hausdorff_number = Some(parse_value(key.as_str(), value)?)
536            }
537            "dm_plex_metric_a_min" => {
538                self.metric.min_anisotropy = Some(parse_value(key.as_str(), value)?)
539            }
540            "dm_plex_metric_a_max" => {
541                self.metric.max_anisotropy = Some(parse_value(key.as_str(), value)?)
542            }
543            "dm_plex_metric_h_min" => {
544                self.metric.min_magnitude = Some(parse_value(key.as_str(), value)?)
545            }
546            "dm_plex_metric_h_max" => {
547                self.metric.max_magnitude = Some(parse_value(key.as_str(), value)?)
548            }
549            "dm_plex_metric_backend" => self.metric.backend = parse_backend(key.as_str(), value)?,
550            "dm_adapt_transfer" => self.metric.transfer = parse_transfer(key.as_str(), value)?,
551            "dm_plex_filename" | "dm_filename" => {
552                self.io.filename = Some(require_value(key.as_str(), value)?.to_string())
553            }
554            "dm_plex_format" | "dm_format" => {
555                self.io.format = Some(require_value(key.as_str(), value)?.to_string())
556            }
557            "dm_plex_interpolate" => {
558                self.io.interpolate = Some(parse_bool_flag(key.as_str(), value)?)
559            }
560            "dm_viewer" | "dm_plex_viewer" => {
561                self.io.use_viewer = Some(require_value(key.as_str(), value)?.to_string())
562            }
563            "dm_prepare_require_coordinates" => {
564                self.solver.require_coordinates = parse_bool_flag(key.as_str(), value)?
565            }
566            "dm_prepare_require_cell_types" => {
567                self.solver.require_cell_types = parse_bool_flag(key.as_str(), value)?
568            }
569            "dm_prepare_require_ownership" => {
570                self.solver.require_ownership = parse_bool_flag(key.as_str(), value)?
571            }
572            "dm_prepare_require_overlap" => {
573                self.solver.require_overlap = parse_bool_flag(key.as_str(), value)?
574            }
575            "dm_prepare_synchronize_ghost_sections" => {
576                self.solver.synchronize_ghost_sections = parse_bool_flag(key.as_str(), value)?
577            }
578            "dm_prepare_create_serial_ownership" => {
579                self.solver.create_serial_ownership = parse_bool_flag(key.as_str(), value)?
580            }
581            _ => return Ok(false),
582        }
583        Ok(true)
584    }
585}
586
587/// Human-readable supported mapping table for generated docs/help text.
588pub const PETSC_DMPLEX_OPTION_MAPPINGS: &[(&str, &str)] = &[
589    (
590        "-dm_refine_pre",
591        "refinement.pre_refine / MeshDMOptions::pre_refine",
592    ),
593    (
594        "-dm_refine",
595        "refinement.post_refine / MeshDMOptions::post_refine",
596    ),
597    (
598        "-dm_distribute",
599        "distribution.distribute / MeshDMOptions::distribute",
600    ),
601    (
602        "-dm_distribute_overlap",
603        "overlap.depth / DistributionConfig::overlap_depth",
604    ),
605    ("-dm_plex_localize", "distribution.localize_coordinates"),
606    (
607        "-dm_plex_check_all",
608        "checks.check_all / MeshCheckOptions::all",
609    ),
610    ("-dm_plex_check_symmetry", "checks.check_symmetry"),
611    ("-dm_plex_check_skeleton", "checks.check_skeleton"),
612    ("-dm_plex_check_faces", "checks.check_faces"),
613    ("-dm_plex_check_geometry", "checks.check_geometry"),
614    (
615        "-dm_plex_metric_target_complexity",
616        "metric.target_complexity",
617    ),
618    ("-dm_plex_metric_gradation", "metric.gradation"),
619    ("-dm_plex_metric_h_min", "metric.min_magnitude"),
620    ("-dm_plex_metric_h_max", "metric.max_magnitude"),
621    ("-dm_plex_metric_a_min", "metric.min_anisotropy"),
622    ("-dm_plex_metric_a_max", "metric.max_anisotropy"),
623    (
624        "-dm_plex_metric_backend",
625        "metric.backend / MetricRemeshingBackend",
626    ),
627    ("-dm_plex_filename", "io.filename"),
628    ("-dm_plex_format", "io.format"),
629    ("-dm_plex_interpolate", "io.interpolate"),
630];
631
632/// Defaults for FV-threshold adaptation profiles that downstream apps may expose
633/// next to DMPLEX metric controls.
634pub fn default_fv_stability_thresholds() -> FvStabilityThresholds {
635    FvStabilityThresholds::default()
636}
637
638fn split_key_value(token: &str) -> (&str, Option<&str>) {
639    let token = token.trim_start_matches('-');
640    if let Some((key, value)) = token.split_once('=') {
641        (key, Some(value))
642    } else {
643        (token, None)
644    }
645}
646
647fn env_key_to_option(key: &str) -> Option<String> {
648    let normalized = key.to_ascii_lowercase();
649    if normalized.starts_with("dm_") {
650        Some(normalized)
651    } else if let Some(stripped) = normalized.strip_prefix("mesh_sieve_") {
652        Some(stripped.to_string())
653    } else {
654        None
655    }
656}
657
658fn normalize_key(key: &str) -> String {
659    key.trim_start_matches('-')
660        .replace('-', "_")
661        .to_ascii_lowercase()
662}
663
664fn parse_value<T>(key: &str, value: Option<&str>) -> Result<T, String>
665where
666    T: std::str::FromStr,
667    T::Err: std::fmt::Display,
668{
669    let value = require_value(key, value)?;
670    value
671        .parse::<T>()
672        .map_err(|err| format!("invalid value for -{key}: {value:?}: {err}"))
673}
674
675fn require_value<'a>(key: &str, value: Option<&'a str>) -> Result<&'a str, String> {
676    value.ok_or_else(|| format!("missing value for -{key}"))
677}
678
679fn parse_bool_flag(key: &str, value: Option<&str>) -> Result<bool, String> {
680    match value {
681        None => Ok(true),
682        Some(value) => match value.to_ascii_lowercase().as_str() {
683            "1" | "true" | "yes" | "on" => Ok(true),
684            "0" | "false" | "no" | "off" => Ok(false),
685            _ => Err(format!("invalid boolean for -{key}: {value:?}")),
686        },
687    }
688}
689
690fn parse_ordering(key: &str, value: Option<&str>) -> Result<DmplexOrderingProfile, String> {
691    match require_value(key, value)?.to_ascii_lowercase().as_str() {
692        "vertex" | "vertex_first" | "depth" | "depth_first" => {
693            Ok(DmplexOrderingProfile::VertexFirst)
694        }
695        "cell" | "cell_first" | "height" | "height_first" => Ok(DmplexOrderingProfile::CellFirst),
696        other => Err(format!("invalid ordering for -{key}: {other:?}")),
697    }
698}
699
700fn parse_backend(key: &str, value: Option<&str>) -> Result<DmplexMetricBackendProfile, String> {
701    match require_value(key, value)?.to_ascii_lowercase().as_str() {
702        "internal" | "none" => Ok(DmplexMetricBackendProfile::Internal),
703        "triangle" => Ok(DmplexMetricBackendProfile::Triangle),
704        "tetgen" => Ok(DmplexMetricBackendProfile::TetGen),
705        "gmsh" => Ok(DmplexMetricBackendProfile::Gmsh),
706        "mmg" => Ok(DmplexMetricBackendProfile::Mmg),
707        other => Err(format!("invalid metric backend for -{key}: {other:?}")),
708    }
709}
710
711fn parse_transfer(key: &str, value: Option<&str>) -> Result<DmplexTransferProfile, String> {
712    match require_value(key, value)?.to_ascii_lowercase().as_str() {
713        "topology" | "topology_only" => Ok(DmplexTransferProfile::TopologyOnly),
714        "labels" | "preserve_labels" => Ok(DmplexTransferProfile::PreserveLabels),
715        "coordinates" | "coords" | "preserve_coordinates_and_labels" => {
716            Ok(DmplexTransferProfile::PreserveCoordinatesAndLabels)
717        }
718        "all" | "preserve_all" => Ok(DmplexTransferProfile::PreserveAll),
719        other => Err(format!("invalid adaptation transfer for -{key}: {other:?}")),
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[test]
728    fn cli_profile_maps_to_native_options() {
729        let profile = DmplexConfigProfile::from_cli_args([
730            "app",
731            "-dm_refine_pre",
732            "2",
733            "-dm_refine=1",
734            "-dm_distribute",
735            "true",
736            "-dm_distribute_overlap",
737            "3",
738            "-dm_plex_check_all",
739            "-dm_plex_reorder_section",
740            "vertex_first",
741            "-dm_plex_metric_target_complexity",
742            "2500.0",
743            "-dm_plex_metric_backend",
744            "gmsh",
745            "-dm_prepare_require_overlap",
746            "yes",
747        ])
748        .unwrap();
749
750        let dm_options = profile.mesh_dm_options();
751        assert_eq!(dm_options.pre_refine, 2);
752        assert_eq!(dm_options.post_refine, 1);
753        assert!(dm_options.distribute);
754        assert_eq!(dm_options.distribute_overlap, 3);
755        assert!(dm_options.check_all);
756        assert_eq!(
757            dm_options.reorder_section,
758            Some(StratifiedOrdering::VertexFirst)
759        );
760
761        let distribution = profile.distribution_config();
762        assert_eq!(distribution.overlap_depth, 3);
763        assert!(distribution.synchronize_sections);
764
765        let checks = profile.mesh_check_options();
766        assert!(checks.check_sections);
767        assert!(checks.check_ownership);
768
769        let prepare = profile.prepare_for_solve_options();
770        assert!(prepare.require_overlap);
771
772        let adapt = profile.metric_adapt_options();
773        assert_eq!(adapt.normalization.target_complexity, Some(2500.0));
774        assert!(matches!(
775            adapt.backend,
776            MetricRemeshingBackend::External(ExternalRemeshingBackend::Gmsh)
777        ));
778    }
779
780    #[test]
781    fn parser_accepts_boolean_flags_and_ignores_unknown_options() {
782        let profile = DmplexConfigProfile::from_cli_args([
783            "-unknown_petSc_option",
784            "42",
785            "-dm_distribute",
786            "-dm_plex_check_faces",
787        ])
788        .unwrap();
789        assert!(profile.distribution.distribute);
790        assert!(profile.checks.check_faces);
791    }
792}