1use 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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
60pub struct DmplexConfigProfile {
61 pub refinement: DmplexRefinementProfile,
63 pub distribution: DmplexDistributionProfile,
65 pub overlap: DmplexOverlapProfile,
67 pub checks: DmplexCheckProfile,
69 pub metric: DmplexMetricProfile,
71 pub io: DmplexIoProfile,
73 pub ordering: Option<DmplexOrderingProfile>,
75 pub solver: DmplexSolverPreparationProfile,
77}
78
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
81pub struct DmplexRefinementProfile {
82 pub pre_refine: usize,
84 pub post_refine: usize,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct DmplexDistributionProfile {
91 pub distribute: bool,
93 pub localize_coordinates: bool,
95 pub synchronize_sections: bool,
97 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub struct DmplexOverlapProfile {
115 pub depth: usize,
117}
118
119impl Default for DmplexOverlapProfile {
120 fn default() -> Self {
121 Self { depth: 1 }
122 }
123}
124
125#[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#[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#[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#[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#[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#[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#[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 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 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 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 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 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 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 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 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 pub fn metric_adapt_options(&self) -> MeshDMMetricAdaptOptions {
399 self.metric.metric_adapt_options()
400 }
401
402 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 pub fn from_env() -> Result<Self, String> {
422 let mut profile = Self::default();
423 profile.apply_env()?;
424 Ok(profile)
425 }
426
427 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 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 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
587pub 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
632pub 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}