1use serde::Serialize;
4use std::fmt::Write as _;
5
6use crate::data::coordinates::Coordinates;
7use crate::data::section::Section;
8use crate::data::storage::Storage;
9use crate::geometry::fvm::build_fvm_face_metrics;
10use crate::mesh_error::MeshSieveError;
11use crate::overlap::overlap::Overlap;
12use crate::topology::cell_type::CellType;
13use crate::topology::ownership::PointOwnership;
14use crate::topology::point::PointId;
15use crate::topology::sieve::strata::compute_strata;
16use crate::topology::sieve::{MeshSieve, Sieve};
17use crate::topology::validation::{
18 TopologyValidationOptions, validate_oriented_sieve_topology,
19 validate_overlap_ownership_topology,
20};
21
22#[derive(Clone, Copy, Debug, Serialize)]
24pub struct PrepareForSolveOptions {
25 pub require_coordinates: bool,
27 pub require_cell_types: bool,
29 pub require_ownership: bool,
31 pub require_overlap: bool,
33 pub synchronize_ghost_sections: bool,
35 pub create_serial_ownership: bool,
37}
38
39impl Default for PrepareForSolveOptions {
40 fn default() -> Self {
41 Self {
42 require_coordinates: true,
43 require_cell_types: true,
44 require_ownership: true,
45 require_overlap: false,
46 synchronize_ghost_sections: true,
47 create_serial_ownership: true,
48 }
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
54pub struct PrepareForSolvePrerequisiteDiagnostic {
55 pub name: &'static str,
56 pub required: bool,
57 pub present: bool,
58 pub complete: bool,
59 pub detail: String,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
64pub struct PrepareForSolveStepDiagnostic {
65 pub name: &'static str,
66 pub status: &'static str,
67 pub detail: String,
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
72pub struct PrepareForSolvePreallocationDiagnostic {
73 pub rows: usize,
74 pub edges: usize,
75 pub order: Vec<PointId>,
76 pub row_nnz: Vec<usize>,
77}
78
79#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
81pub struct PrepareForSolveDiagnostics {
82 pub ready: bool,
83 pub prerequisites: Vec<PrepareForSolvePrerequisiteDiagnostic>,
84 pub steps: Vec<PrepareForSolveStepDiagnostic>,
85 pub global_sections: Vec<String>,
86 pub preallocation: Option<PrepareForSolvePreallocationDiagnostic>,
87 pub synchronized_sections: Vec<String>,
88}
89
90impl PrepareForSolveDiagnostics {
91 pub fn has_missing_required_prerequisites(&self) -> bool {
93 self.prerequisites
94 .iter()
95 .any(|p| p.required && (!p.present || !p.complete))
96 }
97}
98
99pub fn prepare_for_solve_diagnostics_json(
101 diagnostics: &PrepareForSolveDiagnostics,
102) -> Result<String, MeshSieveError> {
103 serde_json::to_string(diagnostics)
104 .map_err(|e| MeshSieveError::InvalidGeometry(format!("serialize prepare diagnostics: {e}")))
105}
106
107pub fn prepare_for_solve_prerequisites<V, St, CtSt>(
109 sieve: &MeshSieve,
110 coords: Option<&Coordinates<V, St>>,
111 cell_types: Option<&Section<CellType, CtSt>>,
112 ownership: Option<&PointOwnership>,
113 overlap: Option<&Overlap>,
114 options: PrepareForSolveOptions,
115) -> Result<Vec<PrepareForSolvePrerequisiteDiagnostic>, MeshSieveError>
116where
117 St: Storage<V>,
118 CtSt: Storage<CellType>,
119{
120 let strata = compute_strata(sieve)?;
121 let vertices: Vec<PointId> = strata
122 .depth
123 .iter()
124 .filter_map(|(&point, &depth)| (depth == 0).then_some(point))
125 .collect();
126 let cells = strata.strata.first().cloned().unwrap_or_default();
127 let points = sieve.points_sorted();
128 let ghost_count = ownership
129 .map(|o| o.ghost_points().count())
130 .unwrap_or_default();
131 let overlap_required = options.require_overlap || ghost_count > 0;
132
133 let mut diagnostics = Vec::new();
134
135 let missing_coords = coords.map_or(vertices.len(), |coords| {
136 vertices
137 .iter()
138 .filter(|point| !coords.section().atlas().contains(**point))
139 .count()
140 });
141 diagnostics.push(PrepareForSolvePrerequisiteDiagnostic {
142 name: "coordinates",
143 required: options.require_coordinates,
144 present: coords.is_some(),
145 complete: coords.is_some() && missing_coords == 0,
146 detail: if coords.is_some() {
147 format!(
148 "vertex_points={}, missing_vertex_coordinates={missing_coords}",
149 vertices.len()
150 )
151 } else {
152 "coordinate section is not attached".to_string()
153 },
154 });
155
156 let missing_cell_types = cell_types.map_or(cells.len(), |cell_types| {
157 cells
158 .iter()
159 .filter(|point| !cell_types.atlas().contains(**point))
160 .count()
161 });
162 diagnostics.push(PrepareForSolvePrerequisiteDiagnostic {
163 name: "cell_types",
164 required: options.require_cell_types,
165 present: cell_types.is_some(),
166 complete: cell_types.is_some() && missing_cell_types == 0,
167 detail: if cell_types.is_some() {
168 format!(
169 "cells={}, missing_cell_types={missing_cell_types}",
170 cells.len()
171 )
172 } else {
173 "cell-type section is not attached".to_string()
174 },
175 });
176
177 let missing_ownership = ownership.map_or(points.len(), |ownership| {
178 points
179 .iter()
180 .filter(|point| ownership.entry(**point).is_none())
181 .count()
182 });
183 diagnostics.push(PrepareForSolvePrerequisiteDiagnostic {
184 name: "ownership",
185 required: options.require_ownership,
186 present: ownership.is_some(),
187 complete: ownership.is_some() && missing_ownership == 0,
188 detail: if ownership.is_some() {
189 format!("topology_points={}, missing_ownership={missing_ownership}, ghost_points={ghost_count}", points.len())
190 } else {
191 "ownership map is not attached".to_string()
192 },
193 });
194
195 diagnostics.push(PrepareForSolvePrerequisiteDiagnostic {
196 name: "overlap",
197 required: overlap_required,
198 present: overlap.is_some(),
199 complete: !overlap_required || overlap.is_some(),
200 detail: if overlap.is_some() {
201 format!("ghost_points={ghost_count}, overlap_required={overlap_required}")
202 } else {
203 format!("overlap graph is not attached; ghost_points={ghost_count}, overlap_required={overlap_required}")
204 },
205 });
206
207 Ok(diagnostics)
208}
209
210#[derive(Clone, Debug, Default, Serialize)]
211pub struct MetricSummary {
212 pub max: f64,
213 pub p95: f64,
214 pub mean: f64,
215 pub count: usize,
216}
217
218#[derive(Clone, Debug, Default, Serialize)]
219pub struct FvmQualityDiagnostics {
220 pub non_orthogonality_deg: MetricSummary,
221 pub skewness: MetricSummary,
222 pub cell_char_length: MetricSummary,
223}
224
225#[derive(Clone, Debug, Serialize)]
226pub struct FvmCellDiagnostic {
227 pub cell: PointId,
228 pub max_non_orthogonality_deg: f64,
229 pub max_skewness: f64,
230 pub char_length: f64,
231 pub boundary_face_count: usize,
232}
233
234#[derive(Clone, Copy, Debug, Serialize)]
235pub struct FvReadinessThresholds {
236 pub max_non_orthogonality_deg: f64,
237 pub max_skewness: f64,
238 pub min_cell_char_length: f64,
239}
240
241impl Default for FvReadinessThresholds {
242 fn default() -> Self {
243 Self {
244 max_non_orthogonality_deg: 75.0,
245 max_skewness: 4.0,
246 min_cell_char_length: 1.0e-8,
247 }
248 }
249}
250
251#[derive(Clone, Copy, Debug, Default)]
252pub struct FvReadinessOptions {
253 pub strict: bool,
254}
255
256#[derive(Clone, Debug, Serialize)]
257pub struct FvReadinessViolation {
258 pub metric: &'static str,
259 pub point: PointId,
260 pub value: f64,
261 pub threshold: f64,
262}
263
264#[derive(Clone, Debug, Default, Serialize)]
265pub struct FvReadinessSummary {
266 pub failed: usize,
267 pub total: usize,
268}
269
270#[derive(Clone, Debug, Default, Serialize)]
271pub struct FvReadinessReport {
272 pub passed: bool,
273 pub thresholds: FvReadinessThresholds,
274 pub non_orthogonality: FvReadinessSummary,
275 pub skewness: FvReadinessSummary,
276 pub cell_char_length: FvReadinessSummary,
277 pub violations: Vec<FvReadinessViolation>,
278}
279
280#[derive(Clone, Copy, Debug, Default)]
281pub struct MeshCheckOptions {
282 pub check_symmetry: bool,
283 pub check_skeleton: bool,
284 pub check_faces: bool,
285 pub check_geometry: bool,
286 pub check_overlap: bool,
287 pub check_ownership: bool,
288 pub check_sections: bool,
289}
290
291impl MeshCheckOptions {
292 pub fn all() -> Self {
293 Self {
294 check_symmetry: true,
295 check_skeleton: true,
296 check_faces: true,
297 check_geometry: true,
298 check_overlap: true,
299 check_ownership: true,
300 check_sections: true,
301 }
302 }
303}
304
305pub fn run_mesh_checks<'a, V, St, CtSt>(
306 sieve: &mut MeshSieve,
307 cell_types: Option<&Section<CellType, CtSt>>,
308 coords: Option<&Coordinates<V, St>>,
309 ownership: Option<&PointOwnership>,
310 overlap: Option<&Overlap>,
311 sections: impl Iterator<Item = &'a Section<V, St>>,
312 options: MeshCheckOptions,
313) -> Result<(), MeshSieveError>
314where
315 V: Clone + Default + 'a,
316 St: Storage<V> + Clone + 'a,
317 CtSt: Storage<CellType>,
318{
319 if options.check_skeleton {
320 compute_strata(sieve)?;
321 }
322
323 if options.check_symmetry
324 && let Some(cell_types) = cell_types
325 {
326 validate_oriented_sieve_topology(sieve, cell_types, TopologyValidationOptions::all())?;
327 }
328
329 if options.check_geometry
330 && let Some(coords) = coords
331 {
332 for (point, (_offset, len)) in coords.section().atlas().iter_entries() {
333 if len != coords.embedding_dimension() {
334 return Err(MeshSieveError::SliceLengthMismatch {
335 point,
336 expected: coords.embedding_dimension(),
337 found: len,
338 });
339 }
340 }
341 }
342
343 if (options.check_overlap || options.check_ownership)
344 && let Some(ownership) = ownership
345 {
346 validate_overlap_ownership_topology(sieve, ownership, overlap, 0)?;
347 }
348
349 if options.check_sections {
350 for section in sections {
351 for (point, (_offset, len)) in section.atlas().iter_entries() {
352 if len == 0 {
353 return Err(MeshSieveError::SliceLengthMismatch {
354 point,
355 expected: 1,
356 found: 0,
357 });
358 }
359 }
360 }
361 }
362 Ok(())
363}
364
365pub fn run_mesh_checks_f64_with_report<'a, St, CtSt>(
366 sieve: &mut MeshSieve,
367 cell_types: Option<&Section<CellType, CtSt>>,
368 coords: Option<&Coordinates<f64, St>>,
369 ownership: Option<&PointOwnership>,
370 overlap: Option<&Overlap>,
371 sections: impl Iterator<Item = &'a Section<f64, St>>,
372 options: MeshCheckOptions,
373) -> Result<Option<FvmQualityDiagnostics>, MeshSieveError>
374where
375 St: Storage<f64> + Clone + 'a,
376 CtSt: Storage<CellType>,
377{
378 run_mesh_checks(
379 sieve, cell_types, coords, ownership, overlap, sections, options,
380 )?;
381 if let (Some(ct), Some(cd)) = (cell_types, coords) {
382 Ok(Some(fvm_quality_diagnostics(sieve, ct, cd)?))
383 } else {
384 Ok(None)
385 }
386}
387
388pub fn fvm_quality_diagnostics<CT, CS>(
389 sieve: &MeshSieve,
390 cell_types: &Section<CellType, CT>,
391 coordinates: &Coordinates<f64, CS>,
392) -> Result<FvmQualityDiagnostics, MeshSieveError>
393where
394 CT: Storage<CellType>,
395 CS: Storage<f64>,
396{
397 let face_metrics = build_fvm_face_metrics(sieve, cell_types, coordinates)?;
398 let non_ortho: Vec<f64> = face_metrics
399 .iter()
400 .filter_map(|m| m.non_orthogonality_deg)
401 .collect();
402 let skewness: Vec<f64> = face_metrics
403 .iter()
404 .map(|m| {
405 let denom = m
406 .owner_to_neighbor
407 .map(norm3)
408 .unwrap_or_else(|| norm3(m.owner_to_face));
409 if denom > 1e-12 {
410 norm3(m.skewness_vector) / denom
411 } else {
412 0.0
413 }
414 })
415 .collect();
416 let mut cell_area_sum: std::collections::BTreeMap<PointId, f64> =
417 std::collections::BTreeMap::new();
418 for fm in &face_metrics {
419 *cell_area_sum.entry(fm.owner).or_insert(0.0) += fm.area_magnitude;
420 if let Some(n) = fm.neighbor {
421 *cell_area_sum.entry(n).or_insert(0.0) += fm.area_magnitude;
422 }
423 }
424 let mut char_lengths = Vec::with_capacity(cell_area_sum.len());
425 for (cell, area_sum) in cell_area_sum {
426 if let Some(cell_type) = cell_types
427 .try_restrict(cell)
428 .ok()
429 .and_then(|v| v.first().copied())
430 {
431 let mut verts: Vec<_> = sieve
432 .closure_iter([cell])
433 .filter(|p| sieve.cone_points(*p).next().is_none())
434 .collect();
435 verts.sort_unstable();
436 verts.dedup();
437 let quality = crate::geometry::quality::cell_quality_from_section(
438 cell_type,
439 &verts,
440 coordinates,
441 )?;
442 if area_sum > 1e-12 {
443 char_lengths.push(quality.jacobian_sign.abs() / area_sum);
444 }
445 }
446 }
447 Ok(FvmQualityDiagnostics {
448 non_orthogonality_deg: summarize(&non_ortho),
449 skewness: summarize(&skewness),
450 cell_char_length: summarize(&char_lengths),
451 })
452}
453
454pub fn fvm_cell_diagnostics<CT, CS>(
455 sieve: &MeshSieve,
456 cell_types: &Section<CellType, CT>,
457 coordinates: &Coordinates<f64, CS>,
458) -> Result<Vec<FvmCellDiagnostic>, MeshSieveError>
459where
460 CT: Storage<CellType>,
461 CS: Storage<f64>,
462{
463 let face_metrics = build_fvm_face_metrics(sieve, cell_types, coordinates)?;
464 let mut by_cell: std::collections::BTreeMap<PointId, FvmCellDiagnostic> =
465 std::collections::BTreeMap::new();
466 let mut cell_area_sum: std::collections::BTreeMap<PointId, f64> =
467 std::collections::BTreeMap::new();
468 for fm in &face_metrics {
469 let denom = fm
470 .owner_to_neighbor
471 .map(norm3)
472 .unwrap_or_else(|| norm3(fm.owner_to_face));
473 let skewness = if denom > 1e-12 {
474 norm3(fm.skewness_vector) / denom
475 } else {
476 0.0
477 };
478 let owner = by_cell.entry(fm.owner).or_insert(FvmCellDiagnostic {
479 cell: fm.owner,
480 max_non_orthogonality_deg: 0.0,
481 max_skewness: 0.0,
482 char_length: 0.0,
483 boundary_face_count: 0,
484 });
485 owner.max_non_orthogonality_deg = owner
486 .max_non_orthogonality_deg
487 .max(fm.non_orthogonality_deg.unwrap_or(0.0));
488 owner.max_skewness = owner.max_skewness.max(skewness);
489 if fm.neighbor.is_none() {
490 owner.boundary_face_count += 1;
491 }
492 *cell_area_sum.entry(fm.owner).or_insert(0.0) += fm.area_magnitude;
493 if let Some(n) = fm.neighbor {
494 let neigh = by_cell.entry(n).or_insert(FvmCellDiagnostic {
495 cell: n,
496 max_non_orthogonality_deg: 0.0,
497 max_skewness: 0.0,
498 char_length: 0.0,
499 boundary_face_count: 0,
500 });
501 neigh.max_non_orthogonality_deg = neigh
502 .max_non_orthogonality_deg
503 .max(fm.non_orthogonality_deg.unwrap_or(0.0));
504 neigh.max_skewness = neigh.max_skewness.max(skewness);
505 *cell_area_sum.entry(n).or_insert(0.0) += fm.area_magnitude;
506 }
507 }
508 for (cell, diag) in &mut by_cell {
509 if let Some(cell_type) = cell_types
510 .try_restrict(*cell)
511 .ok()
512 .and_then(|v| v.first().copied())
513 {
514 let mut verts: Vec<_> = sieve
515 .closure_iter([*cell])
516 .filter(|p| sieve.cone_points(*p).next().is_none())
517 .collect();
518 verts.sort_unstable();
519 verts.dedup();
520 let quality = crate::geometry::quality::cell_quality_from_section(
521 cell_type,
522 &verts,
523 coordinates,
524 )?;
525 let area_sum = *cell_area_sum.get(cell).unwrap_or(&0.0);
526 if area_sum > 1e-12 {
527 diag.char_length = quality.jacobian_sign.abs() / area_sum;
528 }
529 }
530 }
531 Ok(by_cell.into_values().collect())
532}
533
534pub fn fvm_quality_diagnostics_json<CT, CS>(
535 sieve: &MeshSieve,
536 cell_types: &Section<CellType, CT>,
537 coordinates: &Coordinates<f64, CS>,
538) -> Result<String, MeshSieveError>
539where
540 CT: Storage<CellType>,
541 CS: Storage<f64>,
542{
543 let report = fvm_quality_diagnostics(sieve, cell_types, coordinates)?;
544 serde_json::to_string(&report)
545 .map_err(|e| MeshSieveError::InvalidGeometry(format!("serialize diagnostics: {e}")))
546}
547
548pub fn fv_readiness_report<CT, CS>(
549 sieve: &MeshSieve,
550 cell_types: &Section<CellType, CT>,
551 coordinates: &Coordinates<f64, CS>,
552 thresholds: FvReadinessThresholds,
553 options: FvReadinessOptions,
554) -> Result<FvReadinessReport, MeshSieveError>
555where
556 CT: Storage<CellType>,
557 CS: Storage<f64>,
558{
559 let face_metrics = build_fvm_face_metrics(sieve, cell_types, coordinates)?;
560 let mut report = FvReadinessReport {
561 passed: true,
562 thresholds,
563 non_orthogonality: FvReadinessSummary {
564 total: face_metrics.len(),
565 ..FvReadinessSummary::default()
566 },
567 skewness: FvReadinessSummary {
568 total: face_metrics.len(),
569 ..FvReadinessSummary::default()
570 },
571 ..FvReadinessReport::default()
572 };
573 for fm in &face_metrics {
574 let non_ortho = fm.non_orthogonality_deg.unwrap_or(0.0);
575 if non_ortho > thresholds.max_non_orthogonality_deg {
576 report.non_orthogonality.failed += 1;
577 report.violations.push(FvReadinessViolation {
578 metric: "non_orthogonality_deg",
579 point: fm.face,
580 value: non_ortho,
581 threshold: thresholds.max_non_orthogonality_deg,
582 });
583 }
584 let denom = fm
585 .owner_to_neighbor
586 .map(norm3)
587 .unwrap_or_else(|| norm3(fm.owner_to_face));
588 let skewness = if denom > 1e-12 {
589 norm3(fm.skewness_vector) / denom
590 } else {
591 0.0
592 };
593 if skewness > thresholds.max_skewness {
594 report.skewness.failed += 1;
595 report.violations.push(FvReadinessViolation {
596 metric: "skewness",
597 point: fm.face,
598 value: skewness,
599 threshold: thresholds.max_skewness,
600 });
601 }
602 }
603 let mut cell_area_sum: std::collections::BTreeMap<PointId, f64> =
604 std::collections::BTreeMap::new();
605 for fm in &face_metrics {
606 *cell_area_sum.entry(fm.owner).or_insert(0.0) += fm.area_magnitude;
607 if let Some(n) = fm.neighbor {
608 *cell_area_sum.entry(n).or_insert(0.0) += fm.area_magnitude;
609 }
610 }
611 report.cell_char_length.total = cell_area_sum.len();
612 for (cell, area_sum) in cell_area_sum {
613 if let Some(cell_type) = cell_types
614 .try_restrict(cell)
615 .ok()
616 .and_then(|v| v.first().copied())
617 {
618 let mut verts: Vec<_> = sieve
619 .closure_iter([cell])
620 .filter(|p| sieve.cone_points(*p).next().is_none())
621 .collect();
622 verts.sort_unstable();
623 verts.dedup();
624 let quality = crate::geometry::quality::cell_quality_from_section(
625 cell_type,
626 &verts,
627 coordinates,
628 )?;
629 let char_len = if area_sum > 1e-12 {
630 quality.jacobian_sign.abs() / area_sum
631 } else {
632 0.0
633 };
634 if char_len < thresholds.min_cell_char_length {
635 report.cell_char_length.failed += 1;
636 report.violations.push(FvReadinessViolation {
637 metric: "cell_char_length",
638 point: cell,
639 value: char_len,
640 threshold: thresholds.min_cell_char_length,
641 });
642 }
643 }
644 }
645 report.passed = report.violations.is_empty();
646 if options.strict && !report.passed {
647 return Err(MeshSieveError::InvalidGeometry(format!(
648 "FV readiness failed with {} violation(s)",
649 report.violations.len()
650 )));
651 }
652 Ok(report)
653}
654
655pub fn fv_readiness_report_json(report: &FvReadinessReport) -> Result<String, MeshSieveError> {
656 serde_json::to_string(report)
657 .map_err(|e| MeshSieveError::InvalidGeometry(format!("serialize readiness report: {e}")))
658}
659
660pub fn fv_readiness_report_text(report: &FvReadinessReport) -> String {
661 let mut out = String::new();
662 let _ = writeln!(&mut out, "fv_readiness_passed={}", report.passed);
663 let _ = writeln!(
664 &mut out,
665 "thresholds: max_non_orthogonality_deg={}, max_skewness={}, min_cell_char_length={}",
666 report.thresholds.max_non_orthogonality_deg,
667 report.thresholds.max_skewness,
668 report.thresholds.min_cell_char_length
669 );
670 let _ = writeln!(
671 &mut out,
672 "non_orthogonality_failed={}/{}",
673 report.non_orthogonality.failed, report.non_orthogonality.total
674 );
675 let _ = writeln!(
676 &mut out,
677 "skewness_failed={}/{}",
678 report.skewness.failed, report.skewness.total
679 );
680 let _ = writeln!(
681 &mut out,
682 "cell_char_length_failed={}/{}",
683 report.cell_char_length.failed, report.cell_char_length.total
684 );
685 for v in &report.violations {
686 let _ = writeln!(
687 &mut out,
688 "violation metric={} point={} value={} threshold={}",
689 v.metric,
690 v.point.get(),
691 v.value,
692 v.threshold
693 );
694 }
695 out
696}
697
698fn summarize(values: &[f64]) -> MetricSummary {
699 if values.is_empty() {
700 return MetricSummary::default();
701 }
702 let mut sorted = values.to_vec();
703 sorted.sort_by(|a, b| a.total_cmp(b));
704 let p95_idx = ((sorted.len() - 1) as f64 * 0.95).round() as usize;
705 MetricSummary {
706 max: *sorted.last().unwrap_or(&0.0),
707 p95: sorted[p95_idx.min(sorted.len() - 1)],
708 mean: sorted.iter().sum::<f64>() / (sorted.len() as f64),
709 count: sorted.len(),
710 }
711}
712
713fn norm3(v: [f64; 3]) -> f64 {
714 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
715}
716
717pub fn mesh_text_summary<S: Sieve<Point = PointId>>(sieve: &S) -> Result<String, MeshSieveError> {
718 let strata = compute_strata(sieve)?;
719 Ok(format!(
720 "points={}, topological_dimension={}, strata={:?}",
721 strata.chart_points.len(),
722 strata.diameter,
723 strata.strata
724 ))
725}
726
727pub fn mesh_dot_graph<S: Sieve<Point = PointId>>(sieve: &S) -> String {
728 let mut out = String::from("digraph MeshSieve {\n");
729 for src in sieve.points() {
730 for dst in sieve.cone_points(src) {
731 let _ = writeln!(&mut out, " \"{src:?}\" -> \"{dst:?}\";");
732 }
733 }
734 out.push_str("}\n");
735 out
736}
737
738pub fn mesh_json_debug_dump<S: Sieve<Point = PointId>>(sieve: &S) -> String {
739 let mut edges = Vec::new();
740 for src in sieve.points() {
741 for dst in sieve.cone_points(src) {
742 edges.push(format!("[{},{}]", src.get(), dst.get()));
743 }
744 }
745 format!("{{\"edges\":[{}]}}", edges.join(","))
746}
747
748pub fn mesh_tikz_viewer<S: Sieve<Point = PointId>>(sieve: &S, max_points: usize) -> Option<String> {
749 let points: Vec<_> = sieve.points().collect();
750 if points.len() > max_points {
751 return None;
752 }
753 let mut out = String::from("\\begin{tikzpicture}[scale=1.0]\n");
754 for (i, p) in points.iter().enumerate() {
755 let _ = writeln!(
756 &mut out,
757 "\\node (p{}) at ({},0) {{{}}};",
758 p.get(),
759 i,
760 p.get()
761 );
762 }
763 for src in sieve.points() {
764 for dst in sieve.cone_points(src) {
765 let _ = writeln!(&mut out, "\\draw[->] (p{}) -- (p{});", src.get(), dst.get());
766 }
767 }
768 out.push_str("\\end{tikzpicture}\n");
769 Some(out)
770}
771
772pub fn fem_diagnostics_report(
773 element_closure: &[PointId],
774 basis_values: &[f64],
775 quadrature_weights: &[f64],
776 local_matrix: &[f64],
777 local_vector: &[f64],
778) -> String {
779 format!(
780 "FEM diagnostics: closure={element_closure:?}, basis={basis_values:?}, quadrature={quadrature_weights:?}, Ke={local_matrix:?}, Fe={local_vector:?}"
781 )
782}
783
784pub fn fvm_flux_diagnostics(flux_updates: &[(PointId, f64)]) -> String {
785 let mut out = String::from("FVM flux updates:");
786 for (p, f) in flux_updates {
787 let _ = write!(&mut out, " ({p:?}: {f})");
788 }
789 out
790}