Skip to main content

oxiphysics_geometry/
mesh_quality.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Mesh quality metrics and repair operations.
5//!
6//! Provides quality metrics for tetrahedral and triangular elements,
7//! mesh repair utilities, Laplacian smoothing, and 2-D edge flipping.
8
9use std::collections::HashMap;
10
11// ---------------------------------------------------------------------------
12// Local vector helpers (operating on [f64; 3])
13// ---------------------------------------------------------------------------
14
15#[inline]
16fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
18}
19
20#[inline]
21fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
23}
24
25#[inline]
26fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
27    [a[0] * s, a[1] * s, a[2] * s]
28}
29
30#[inline]
31fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
32    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
33}
34
35#[inline]
36fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
37    [
38        a[1] * b[2] - a[2] * b[1],
39        a[2] * b[0] - a[0] * b[2],
40        a[0] * b[1] - a[1] * b[0],
41    ]
42}
43
44#[inline]
45fn len3(a: [f64; 3]) -> f64 {
46    dot3(a, a).sqrt()
47}
48
49#[inline]
50fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
51    len3(sub3(a, b))
52}
53
54#[inline]
55fn normalize3(a: [f64; 3]) -> [f64; 3] {
56    let l = len3(a);
57    if l < f64::EPSILON {
58        [0.0, 0.0, 0.0]
59    } else {
60        [a[0] / l, a[1] / l, a[2] / l]
61    }
62}
63
64/// Angle (radians) at vertex `a` in the triangle `a–b–c`.
65fn angle_at(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
66    let ab = normalize3(sub3(b, a));
67    let ac = normalize3(sub3(c, a));
68    dot3(ab, ac).clamp(-1.0, 1.0).acos()
69}
70
71// ---------------------------------------------------------------------------
72// TetrahedralMesh
73// ---------------------------------------------------------------------------
74
75/// A tetrahedral volumetric mesh.
76pub struct TetrahedralMesh {
77    /// Vertex positions.
78    pub vertices: Vec<[f64; 3]>,
79    /// Tetrahedra defined by four vertex indices.
80    pub tets: Vec<[usize; 4]>,
81}
82
83impl TetrahedralMesh {
84    /// Create a new tetrahedral mesh.
85    pub fn new(vertices: Vec<[f64; 3]>, tets: Vec<[usize; 4]>) -> Self {
86        Self { vertices, tets }
87    }
88}
89
90// ---------------------------------------------------------------------------
91// MeshQuality
92// ---------------------------------------------------------------------------
93
94/// Quality metrics for a single mesh element (triangle or tetrahedron).
95#[derive(Debug, Clone, PartialEq)]
96pub struct MeshQuality {
97    /// Aspect ratio: ratio of longest to shortest edge (≥ 1, ideal = 1).
98    pub aspect_ratio: f64,
99    /// Minimum interior angle in degrees.
100    pub min_angle_deg: f64,
101    /// Maximum interior angle in degrees.
102    pub max_angle_deg: f64,
103    /// Skewness in \[0, 1\]; 0 = perfect, 1 = degenerate.
104    pub skewness: f64,
105    /// Scaled Jacobian determinant in \[-1, 1\]; ideal equilateral = 1.
106    pub jacobian: f64,
107}
108
109impl MeshQuality {
110    /// Returns `true` if the element is considered acceptable quality.
111    ///
112    /// Criteria: aspect_ratio < 5, skewness < 0.9, min_angle_deg > 5°.
113    pub fn is_acceptable(&self) -> bool {
114        self.aspect_ratio < 5.0 && self.skewness < 0.9 && self.min_angle_deg > 5.0
115    }
116
117    /// Returns `true` if the element is degenerate (zero or near-zero volume).
118    pub fn is_degenerate(&self) -> bool {
119        self.jacobian.abs() < 1e-10
120    }
121}
122
123// ---------------------------------------------------------------------------
124// compute_tet_quality
125// ---------------------------------------------------------------------------
126
127/// Compute quality metrics for a tetrahedron defined by four vertices.
128///
129/// Returns a [`MeshQuality`] struct with aspect ratio, min/max dihedral angle,
130/// skewness, and scaled Jacobian.
131pub fn compute_tet_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> MeshQuality {
132    // Edge vectors from v0
133    let e01 = sub3(v1, v0);
134    let e02 = sub3(v2, v0);
135    let e03 = sub3(v3, v0);
136    let e12 = sub3(v2, v1);
137    let e13 = sub3(v3, v1);
138    let e23 = sub3(v3, v2);
139
140    // Edge lengths
141    let edge_lengths = [
142        len3(e01),
143        len3(e02),
144        len3(e03),
145        len3(e12),
146        len3(e13),
147        len3(e23),
148    ];
149
150    let min_len = edge_lengths.iter().cloned().fold(f64::INFINITY, f64::min);
151    let max_len = edge_lengths.iter().cloned().fold(0.0_f64, f64::max);
152
153    let aspect_ratio = if min_len < f64::EPSILON {
154        f64::INFINITY
155    } else {
156        max_len / min_len
157    };
158
159    // Jacobian: det of edge matrix (proportional to signed volume)
160    let jac = dot3(e01, cross3(e02, e03));
161    // Ideal regular tet with same mean edge length
162    let mean_edge = edge_lengths.iter().sum::<f64>() / 6.0;
163    let ideal_jac = mean_edge * mean_edge * mean_edge * (2.0_f64).sqrt();
164    // Take absolute value so the jacobian is positive for non-degenerate tets
165    // regardless of vertex winding order
166    let scaled_jac = if ideal_jac < f64::EPSILON {
167        0.0
168    } else {
169        jac.abs() / ideal_jac
170    };
171
172    // Dihedral angles between each pair of faces
173    // Face normals (outward for the tet):
174    //   face 012: n0 = (v1-v0) × (v2-v0)
175    //   face 013: n1 = (v3-v0) × (v1-v0)
176    //   face 023: n2 = (v2-v0) × (v3-v0)
177    //   face 123: n3 = (v2-v1) × (v3-v1)
178    let n012 = normalize3(cross3(e01, e02));
179    let n013 = normalize3(cross3(e03, e01));
180    let n023 = normalize3(cross3(e02, e03));
181    let n123 = normalize3(cross3(e12, e13));
182
183    // Dihedral angle along each edge = π - angle between adjacent face normals
184    let dihedral_angle = |na: [f64; 3], nb: [f64; 3]| -> f64 {
185        let cos_a = dot3(na, nb).clamp(-1.0, 1.0);
186        // dihedral angle is the supplement of the angle between outward normals
187        std::f64::consts::PI - cos_a.acos()
188    };
189
190    // 6 dihedral angles, one per edge:
191    //   edge 01 → faces 012 and 013
192    //   edge 02 → faces 012 and 023
193    //   edge 03 → faces 013 and 023
194    //   edge 12 → faces 012 and 123
195    //   edge 13 → faces 013 and 123
196    //   edge 23 → faces 023 and 123
197    let dihedrals = [
198        dihedral_angle(n012, n013),
199        dihedral_angle(n012, n023),
200        dihedral_angle(n013, n023),
201        dihedral_angle(n012, n123),
202        dihedral_angle(n013, n123),
203        dihedral_angle(n023, n123),
204    ];
205
206    let rad_to_deg = 180.0 / std::f64::consts::PI;
207    let min_angle_deg = dihedrals.iter().cloned().fold(f64::INFINITY, f64::min) * rad_to_deg;
208    let max_angle_deg = dihedrals.iter().cloned().fold(0.0_f64, f64::max) * rad_to_deg;
209
210    // Skewness: (max_angle - ideal) / (180 - ideal)
211    // For a regular tet, ideal dihedral ≈ 70.529°
212    let ideal_dihedral = 70.529;
213    let skewness = ((max_angle_deg - ideal_dihedral) / (180.0 - ideal_dihedral)).clamp(0.0, 1.0);
214
215    MeshQuality {
216        aspect_ratio,
217        min_angle_deg,
218        max_angle_deg,
219        skewness,
220        jacobian: scaled_jac,
221    }
222}
223
224// ---------------------------------------------------------------------------
225// compute_tri_quality
226// ---------------------------------------------------------------------------
227
228/// Compute quality metrics for a triangle defined by three vertices.
229///
230/// Returns a [`MeshQuality`] struct with aspect ratio, min/max interior angle,
231/// skewness, and a 2-D Jacobian measure.
232pub fn compute_tri_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> MeshQuality {
233    let a = dist3(v1, v2);
234    let b = dist3(v0, v2);
235    let c = dist3(v0, v1);
236
237    let min_len = a.min(b).min(c);
238    let max_len = a.max(b).max(c);
239
240    let aspect_ratio = if min_len < f64::EPSILON {
241        f64::INFINITY
242    } else {
243        max_len / min_len
244    };
245
246    let angle_a = angle_at(v0, v1, v2);
247    let angle_b = angle_at(v1, v0, v2);
248    let angle_c = angle_at(v2, v0, v1);
249
250    let rad_to_deg = 180.0 / std::f64::consts::PI;
251    let angles_deg = [
252        angle_a * rad_to_deg,
253        angle_b * rad_to_deg,
254        angle_c * rad_to_deg,
255    ];
256
257    let min_angle_deg = angles_deg.iter().cloned().fold(f64::INFINITY, f64::min);
258    let max_angle_deg = angles_deg.iter().cloned().fold(0.0_f64, f64::max);
259
260    // Skewness: (max_angle - 60°) / (180° - 60°)
261    let ideal = 60.0;
262    let skewness = ((max_angle_deg - ideal) / (180.0 - ideal)).clamp(0.0, 1.0);
263
264    // Jacobian: area-based. Equilateral triangle of same perimeter → area_ideal.
265    // Use 2 * area / (a*b) as a proxy Jacobian measure (= sin of enclosed angle).
266    let area = {
267        let cross = cross3(sub3(v1, v0), sub3(v2, v0));
268        len3(cross) * 0.5
269    };
270    let denom = a * b;
271    let jacobian = if denom < f64::EPSILON {
272        0.0
273    } else {
274        (2.0 * area / denom).clamp(-1.0, 1.0)
275    };
276
277    MeshQuality {
278        aspect_ratio,
279        min_angle_deg,
280        max_angle_deg,
281        skewness,
282        jacobian,
283    }
284}
285
286// ---------------------------------------------------------------------------
287// repair_degenerate_tets
288// ---------------------------------------------------------------------------
289
290/// Remove degenerate tetrahedra from a mesh in-place.
291///
292/// A tet is considered degenerate when its signed volume is below `threshold`.
293/// Returns the number of tetrahedra removed.
294pub fn repair_degenerate_tets(mesh: &mut TetrahedralMesh) -> usize {
295    let threshold = 1e-12_f64;
296    let before = mesh.tets.len();
297    mesh.tets.retain(|tet| {
298        let v0 = mesh.vertices[tet[0]];
299        let v1 = mesh.vertices[tet[1]];
300        let v2 = mesh.vertices[tet[2]];
301        let v3 = mesh.vertices[tet[3]];
302        let e01 = sub3(v1, v0);
303        let e02 = sub3(v2, v0);
304        let e03 = sub3(v3, v0);
305        let vol = dot3(e01, cross3(e02, e03)).abs() / 6.0;
306        vol > threshold
307    });
308    before - mesh.tets.len()
309}
310
311// ---------------------------------------------------------------------------
312// smooth_laplacian
313// ---------------------------------------------------------------------------
314
315/// Apply Laplacian smoothing to a tetrahedral mesh.
316///
317/// Each interior vertex is moved to the average position of its neighbours.
318/// Boundary vertices (those belonging to fewer than 4 tets) are kept fixed.
319///
320/// # Arguments
321/// * `positions` – mutable slice of vertex positions.
322/// * `tets` – tetrahedra (index groups of 4).
323/// * `iterations` – number of smoothing passes.
324pub fn smooth_laplacian(positions: &mut [[f64; 3]], tets: &[[usize; 4]], iterations: usize) {
325    let n = positions.len();
326    // Count how many tets reference each vertex
327    let mut tet_count = vec![0usize; n];
328    for tet in tets {
329        for &vi in tet.iter() {
330            tet_count[vi] += 1;
331        }
332    }
333
334    for _ in 0..iterations {
335        let mut sums = vec![[0.0_f64; 3]; n];
336        let mut counts = vec![0usize; n];
337
338        for tet in tets {
339            for k in 0..4 {
340                let vi = tet[k];
341                // Accumulate neighbour contributions
342                for (j, &vj) in tet.iter().enumerate() {
343                    if j != k {
344                        sums[vi] = add3(sums[vi], positions[vj]);
345                        counts[vi] += 1;
346                    }
347                }
348            }
349        }
350
351        for i in 0..n {
352            // Only move interior vertices (those connected to many tets)
353            if counts[i] > 0 && tet_count[i] >= 4 {
354                let avg = scale3(sums[i], 1.0 / counts[i] as f64);
355                positions[i] = avg;
356            }
357        }
358    }
359}
360
361// ---------------------------------------------------------------------------
362// flip_edges_2d
363// ---------------------------------------------------------------------------
364
365/// Attempt Delaunay edge flipping on a 2-D triangle mesh.
366///
367/// For each pair of triangles sharing an edge, check if flipping that edge
368/// improves the minimum angle (Lawson's criterion). Returns the number of
369/// flips performed.
370///
371/// `positions` is an (x, y) 2-D position array encoded as `[f64; 3]`
372/// (z component is ignored).
373pub fn flip_edges_2d(triangles: &mut [[usize; 3]], positions: &[[f64; 3]]) -> usize {
374    let mut flips = 0usize;
375    let mut changed = true;
376
377    while changed {
378        changed = false;
379
380        // Build edge → triangle map: edge (min, max) -> list of (tri_idx, local_opposite_vertex)
381        let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
382        for (ti, tri) in triangles.iter().enumerate() {
383            for k in 0..3 {
384                let a = tri[k];
385                let b = tri[(k + 1) % 3];
386                let opp = tri[(k + 2) % 3]; // opposite vertex
387                let key = (a.min(b), a.max(b));
388                edge_map.entry(key).or_default().push((ti, opp));
389            }
390        }
391
392        for ((_ea, _eb), tris) in &edge_map {
393            if tris.len() != 2 {
394                continue; // boundary edge or non-manifold
395            }
396            let (ti, opp_i) = tris[0];
397            let (tj, opp_j) = tris[1];
398
399            // Shared edge vertices
400            let ea = _ea;
401            let eb = _eb;
402
403            let pa = positions[*ea];
404            let pb = positions[*eb];
405            let pc = positions[opp_i];
406            let pd = positions[opp_j];
407
408            // Flip if the opposite vertex d lies inside the circumcircle of triangle (a, b, c)
409            if in_circumcircle_2d(pa, pb, pc, pd) {
410                // Flip: replace shared edge (ea, eb) with (opp_i, opp_j)
411                let new_t0 = [*ea, opp_i, opp_j];
412                let new_t1 = [*eb, opp_j, opp_i];
413
414                // Verify both new triangles have positive signed area (no inversion)
415                if signed_area_2d(
416                    positions[new_t0[0]],
417                    positions[new_t0[1]],
418                    positions[new_t0[2]],
419                ) > 0.0
420                    && signed_area_2d(
421                        positions[new_t1[0]],
422                        positions[new_t1[1]],
423                        positions[new_t1[2]],
424                    ) > 0.0
425                {
426                    triangles[ti] = new_t0;
427                    triangles[tj] = new_t1;
428                    flips += 1;
429                    changed = true;
430                    break; // restart to avoid stale indices
431                }
432            }
433        }
434    }
435
436    flips
437}
438
439/// Returns the signed area of a 2-D triangle (positive = CCW).
440fn signed_area_2d(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
441    0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]))
442}
443
444/// Returns `true` if point `d` lies strictly inside the circumcircle of CCW triangle `a–b–c`.
445///
446/// Uses the standard 3×3 determinant test.
447fn in_circumcircle_2d(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
448    let ax = a[0] - d[0];
449    let ay = a[1] - d[1];
450    let bx = b[0] - d[0];
451    let by = b[1] - d[1];
452    let cx = c[0] - d[0];
453    let cy = c[1] - d[1];
454
455    let det = ax * (by * (cx * cx + cy * cy) - cy * (bx * bx + by * by))
456        - ay * (bx * (cx * cx + cy * cy) - cx * (bx * bx + by * by))
457        + (ax * ax + ay * ay) * (bx * cy - by * cx);
458
459    det > 0.0
460}
461
462// ---------------------------------------------------------------------------
463// Jacobian-based quality metrics
464// ---------------------------------------------------------------------------
465
466/// Compute the Jacobian matrix of a tetrahedron as a 3x3 matrix.
467///
468/// The Jacobian is formed from the three edge vectors emanating from v0.
469/// Returns `[e01, e02, e03]` as row vectors (3x3 column-major).
470pub fn tet_jacobian_matrix(
471    v0: [f64; 3],
472    v1: [f64; 3],
473    v2: [f64; 3],
474    v3: [f64; 3],
475) -> [[f64; 3]; 3] {
476    let e01 = sub3(v1, v0);
477    let e02 = sub3(v2, v0);
478    let e03 = sub3(v3, v0);
479    [e01, e02, e03]
480}
481
482/// Compute the determinant of the Jacobian matrix of a tetrahedron.
483///
484/// Positive for a properly oriented tet, zero for degenerate.
485pub fn tet_jacobian_det(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
486    let j = tet_jacobian_matrix(v0, v1, v2, v3);
487    // det = j[0] . (j[1] x j[2])
488    dot3(j[0], cross3(j[1], j[2]))
489}
490
491/// Compute the scaled Jacobian quality metric for a tetrahedron.
492///
493/// The scaled Jacobian is the Jacobian determinant divided by the product of
494/// the edge lengths forming the Jacobian, normalized to \[-1, 1\].
495/// A value of 1 indicates an ideal (regular) tetrahedron.
496pub fn tet_scaled_jacobian(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
497    let j = tet_jacobian_matrix(v0, v1, v2, v3);
498    let det = dot3(j[0], cross3(j[1], j[2]));
499
500    let l0 = len3(j[0]);
501    let l1 = len3(j[1]);
502    let l2 = len3(j[2]);
503
504    let denom = l0 * l1 * l2;
505    if denom < f64::EPSILON {
506        return 0.0;
507    }
508
509    // For a regular tetrahedron, the scaled Jacobian equals sqrt(2)
510    // Normalize to [0, 1] by dividing by sqrt(2)
511    let raw = det / denom;
512    raw / (2.0_f64).sqrt()
513}
514
515/// Compute the scaled Jacobian at all four corners of a tetrahedron
516/// and return the minimum value.
517///
518/// This is a more robust quality measure than checking just one corner.
519pub fn tet_min_scaled_jacobian(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
520    // Each corner uses edges emanating from that vertex, with consistent
521    // even-permutation ordering to preserve orientation.
522    let sj0 = tet_scaled_jacobian(v0, v1, v2, v3);
523    let sj1 = tet_scaled_jacobian(v1, v2, v0, v3);
524    let sj2 = tet_scaled_jacobian(v2, v0, v1, v3);
525    let sj3 = tet_scaled_jacobian(v3, v1, v0, v2);
526    sj0.min(sj1).min(sj2).min(sj3)
527}
528
529// ---------------------------------------------------------------------------
530// Condition number quality
531// ---------------------------------------------------------------------------
532
533/// Compute the condition number quality metric for a tetrahedron.
534///
535/// Uses the Frobenius norm of the Jacobian and its inverse.
536/// Quality = 1 / (kappa * kappa_ideal), where kappa = ||J|| * ||J^{-1}||.
537/// Returns a value in (0, 1] where 1 is ideal.
538pub fn tet_condition_number_quality(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
539    let j = tet_jacobian_matrix(v0, v1, v2, v3);
540    let det = dot3(j[0], cross3(j[1], j[2]));
541
542    if det.abs() < f64::EPSILON {
543        return 0.0;
544    }
545
546    // Frobenius norm of J
547    let mut frob_j = 0.0;
548    for row in &j {
549        frob_j += dot3(*row, *row);
550    }
551    frob_j = frob_j.sqrt();
552
553    // Compute J^{-1} using cofactor matrix
554    let inv_det = 1.0 / det;
555    let j_inv = [
556        scale3(cross3(j[1], j[2]), inv_det),
557        scale3(cross3(j[2], j[0]), inv_det),
558        scale3(cross3(j[0], j[1]), inv_det),
559    ];
560
561    // Frobenius norm of J^{-1}
562    let mut frob_jinv = 0.0;
563    for row in &j_inv {
564        frob_jinv += dot3(*row, *row);
565    }
566    frob_jinv = frob_jinv.sqrt();
567
568    let kappa = frob_j * frob_jinv;
569    // For an ideal equilateral tet, kappa = sqrt(3) * 3 / 3 = sqrt(3)
570    // Normalize so that ideal gives quality = 1
571    let ideal_kappa = 3.0_f64.sqrt();
572    (ideal_kappa / kappa).clamp(0.0, 1.0)
573}
574
575// ---------------------------------------------------------------------------
576// Quality histogram
577// ---------------------------------------------------------------------------
578
579/// A histogram of element quality values.
580pub struct QualityHistogram {
581    /// Bin edges (length = n_bins + 1).
582    pub bin_edges: Vec<f64>,
583    /// Counts per bin (length = n_bins).
584    pub counts: Vec<usize>,
585    /// Total number of elements.
586    pub total: usize,
587}
588
589impl QualityHistogram {
590    /// Build a histogram of quality values with the given number of bins.
591    ///
592    /// Quality values are assumed to be in \[0, 1\].
593    pub fn from_values(values: &[f64], n_bins: usize) -> Self {
594        assert!(n_bins > 0, "n_bins must be > 0");
595        let mut counts = vec![0usize; n_bins];
596        let bin_width = 1.0 / n_bins as f64;
597        let mut bin_edges = Vec::with_capacity(n_bins + 1);
598        for i in 0..=n_bins {
599            bin_edges.push(i as f64 * bin_width);
600        }
601
602        for &v in values {
603            let bin = ((v / bin_width) as usize).min(n_bins - 1);
604            counts[bin] += 1;
605        }
606
607        Self {
608            bin_edges,
609            counts,
610            total: values.len(),
611        }
612    }
613
614    /// Return the fraction of elements in each bin.
615    pub fn fractions(&self) -> Vec<f64> {
616        if self.total == 0 {
617            return vec![0.0; self.counts.len()];
618        }
619        self.counts
620            .iter()
621            .map(|&c| c as f64 / self.total as f64)
622            .collect()
623    }
624
625    /// Return the bin index containing the most elements.
626    pub fn peak_bin(&self) -> usize {
627        self.counts
628            .iter()
629            .enumerate()
630            .max_by_key(|(_, c)| **c)
631            .map(|(i, _)| i)
632            .unwrap_or(0)
633    }
634}
635
636// ---------------------------------------------------------------------------
637// Worst element identification
638// ---------------------------------------------------------------------------
639
640/// Find the indices of the worst elements by a given quality metric.
641///
642/// Returns the indices of elements with quality below `threshold`,
643/// sorted by quality (worst first).
644pub fn find_worst_elements(qualities: &[f64], threshold: f64) -> Vec<usize> {
645    let mut bad: Vec<(usize, f64)> = qualities
646        .iter()
647        .enumerate()
648        .filter(|(_, q)| *q < &threshold)
649        .map(|(i, q)| (i, *q))
650        .collect();
651    bad.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
652    bad.iter().map(|&(i, _)| i).collect()
653}
654
655/// Summary statistics for a collection of quality values.
656pub struct QualityStats {
657    /// Minimum quality value.
658    pub min: f64,
659    /// Maximum quality value.
660    pub max: f64,
661    /// Mean quality value.
662    pub mean: f64,
663    /// Standard deviation of quality values.
664    pub std_dev: f64,
665    /// Number of elements below the "poor quality" threshold (0.3).
666    pub n_poor: usize,
667}
668
669impl QualityStats {
670    /// Compute statistics from quality values.
671    pub fn compute(values: &[f64]) -> Self {
672        if values.is_empty() {
673            return Self {
674                min: 0.0,
675                max: 0.0,
676                mean: 0.0,
677                std_dev: 0.0,
678                n_poor: 0,
679            };
680        }
681        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
682        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
683        let mean = values.iter().sum::<f64>() / values.len() as f64;
684        let variance =
685            values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
686        let std_dev = variance.sqrt();
687        let n_poor = values.iter().filter(|&&v| v < 0.3).count();
688        Self {
689            min,
690            max,
691            mean,
692            std_dev,
693            n_poor,
694        }
695    }
696}
697
698/// Compute quality values for all tetrahedra in a mesh using the scaled Jacobian.
699pub fn mesh_scaled_jacobian_qualities(mesh: &TetrahedralMesh) -> Vec<f64> {
700    mesh.tets
701        .iter()
702        .map(|tet| {
703            let v0 = mesh.vertices[tet[0]];
704            let v1 = mesh.vertices[tet[1]];
705            let v2 = mesh.vertices[tet[2]];
706            let v3 = mesh.vertices[tet[3]];
707            tet_min_scaled_jacobian(v0, v1, v2, v3)
708        })
709        .collect()
710}
711
712/// Compute condition number quality for all tetrahedra in a mesh.
713pub fn mesh_condition_number_qualities(mesh: &TetrahedralMesh) -> Vec<f64> {
714    mesh.tets
715        .iter()
716        .map(|tet| {
717            let v0 = mesh.vertices[tet[0]];
718            let v1 = mesh.vertices[tet[1]];
719            let v2 = mesh.vertices[tet[2]];
720            let v3 = mesh.vertices[tet[3]];
721            tet_condition_number_quality(v0, v1, v2, v3)
722        })
723        .collect()
724}
725
726// ---------------------------------------------------------------------------
727// Dihedral angle extremes
728// ---------------------------------------------------------------------------
729
730/// Compute the minimum and maximum dihedral angles across all edges of a tet.
731///
732/// Returns `(min_deg, max_deg)`.
733pub fn tet_dihedral_angle_extremes(
734    v0: [f64; 3],
735    v1: [f64; 3],
736    v2: [f64; 3],
737    v3: [f64; 3],
738) -> (f64, f64) {
739    let q = compute_tet_quality(v0, v1, v2, v3);
740    (q.min_angle_deg, q.max_angle_deg)
741}
742
743/// Compute the minimum and maximum interior angles of a triangle.
744///
745/// Returns `(min_deg, max_deg)`.
746pub fn tri_angle_extremes(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> (f64, f64) {
747    let q = compute_tri_quality(v0, v1, v2);
748    (q.min_angle_deg, q.max_angle_deg)
749}
750
751// ---------------------------------------------------------------------------
752// Aspect ratio for all element types
753// ---------------------------------------------------------------------------
754
755/// Aspect ratio of a line segment (always 1.0).
756pub fn line_aspect_ratio(_v0: [f64; 3], _v1: [f64; 3]) -> f64 {
757    1.0
758}
759
760/// Aspect ratio of a quadrilateral element.
761///
762/// Defined as max_diagonal / min_diagonal.
763pub fn quad_aspect_ratio(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
764    // Diagonals of the quad
765    let d1 = dist3(v0, v2);
766    let d2 = dist3(v1, v3);
767    let min_d = d1.min(d2);
768    let max_d = d1.max(d2);
769    if min_d < f64::EPSILON {
770        f64::INFINITY
771    } else {
772        max_d / min_d
773    }
774}
775
776/// Aspect ratio of a wedge (pentahedral prism) element.
777///
778/// Approximated as max_edge / min_edge across the 9 edges.
779pub fn wedge_aspect_ratio(
780    v0: [f64; 3],
781    v1: [f64; 3],
782    v2: [f64; 3],
783    v3: [f64; 3],
784    v4: [f64; 3],
785    v5: [f64; 3],
786) -> f64 {
787    let edges = [
788        dist3(v0, v1),
789        dist3(v1, v2),
790        dist3(v2, v0), // bottom tri
791        dist3(v3, v4),
792        dist3(v4, v5),
793        dist3(v5, v3), // top tri
794        dist3(v0, v3),
795        dist3(v1, v4),
796        dist3(v2, v5), // lateral
797    ];
798    let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
799    let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
800    if min_e < f64::EPSILON {
801        f64::INFINITY
802    } else {
803        max_e / min_e
804    }
805}
806
807/// Aspect ratio of a hexahedral element.
808///
809/// Approximated as max_edge / min_edge across the 12 edges.
810pub fn hex_aspect_ratio(
811    v0: [f64; 3],
812    v1: [f64; 3],
813    v2: [f64; 3],
814    v3: [f64; 3],
815    v4: [f64; 3],
816    v5: [f64; 3],
817    v6: [f64; 3],
818    v7: [f64; 3],
819) -> f64 {
820    let edges = [
821        dist3(v0, v1),
822        dist3(v1, v2),
823        dist3(v2, v3),
824        dist3(v3, v0), // bottom
825        dist3(v4, v5),
826        dist3(v5, v6),
827        dist3(v6, v7),
828        dist3(v7, v4), // top
829        dist3(v0, v4),
830        dist3(v1, v5),
831        dist3(v2, v6),
832        dist3(v3, v7), // lateral
833    ];
834    let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
835    let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
836    if min_e < f64::EPSILON {
837        f64::INFINITY
838    } else {
839        max_e / min_e
840    }
841}
842
843// ---------------------------------------------------------------------------
844// Quality-guided mesh improvement
845// ---------------------------------------------------------------------------
846
847/// Perform quality-guided Laplacian smoothing: only move vertices that improve quality.
848///
849/// For each interior vertex, compute the Laplacian-smoothed position, then
850/// evaluate the quality of affected tets.  The move is accepted only if
851/// the minimum quality improves.
852///
853/// Returns the number of accepted vertex moves.
854pub fn quality_guided_smoothing(
855    positions: &mut [[f64; 3]],
856    tets: &[[usize; 4]],
857    iterations: usize,
858) -> usize {
859    let n = positions.len();
860    let mut tet_count = vec![0usize; n];
861    for tet in tets {
862        for &vi in tet.iter() {
863            tet_count[vi] += 1;
864        }
865    }
866
867    let mut total_accepted = 0usize;
868
869    for _ in 0..iterations {
870        let mut sums = vec![[0.0_f64; 3]; n];
871        let mut counts = vec![0usize; n];
872
873        for tet in tets.iter() {
874            for k in 0..4 {
875                let vi = tet[k];
876                for (j, &vj) in tet.iter().enumerate() {
877                    if j != k {
878                        sums[vi] = add3(sums[vi], positions[vj]);
879                        counts[vi] += 1;
880                    }
881                }
882            }
883        }
884
885        for i in 0..n {
886            if counts[i] == 0 || tet_count[i] < 2 {
887                continue;
888            }
889            let new_pos = scale3(sums[i], 1.0 / counts[i] as f64);
890
891            // Compute minimum quality before move for affected tets
892            let min_q_before = tets
893                .iter()
894                .filter(|t| t.contains(&i))
895                .map(|t| {
896                    let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
897                    tet_scaled_jacobian(vv[0], vv[1], vv[2], vv[3]).abs()
898                })
899                .fold(f64::INFINITY, f64::min);
900
901            // Tentatively apply
902            let old_pos = positions[i];
903            positions[i] = new_pos;
904
905            let min_q_after = tets
906                .iter()
907                .filter(|t| t.contains(&i))
908                .map(|t| {
909                    let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
910                    tet_scaled_jacobian(vv[0], vv[1], vv[2], vv[3]).abs()
911                })
912                .fold(f64::INFINITY, f64::min);
913
914            if min_q_after >= min_q_before {
915                total_accepted += 1; // keep new position
916            } else {
917                positions[i] = old_pos; // revert
918            }
919        }
920    }
921
922    total_accepted
923}
924
925// ---------------------------------------------------------------------------
926// Minimum angle optimization (angle-based smoothing)
927// ---------------------------------------------------------------------------
928
929/// Apply angle-based smoothing to a 2-D triangle mesh.
930///
931/// Moves each interior vertex to maximise the minimum interior angle in
932/// surrounding triangles.  Uses a gradient-free search along the Laplacian
933/// direction with a line search.
934///
935/// Returns the number of accepted vertex moves.
936pub fn minimum_angle_optimization(
937    positions: &mut [[f64; 3]],
938    triangles: &[[usize; 3]],
939    iterations: usize,
940) -> usize {
941    let n = positions.len();
942
943    // Find interior vertices: referenced by ≥ 2 triangles
944    let mut tri_count = vec![0usize; n];
945    for tri in triangles {
946        for &vi in tri.iter() {
947            tri_count[vi] += 1;
948        }
949    }
950
951    let mut total_accepted = 0usize;
952
953    for _ in 0..iterations {
954        for i in 0..n {
955            if tri_count[i] < 2 {
956                continue; // boundary
957            }
958
959            // Compute Laplacian position
960            let mut sum = [0.0_f64; 3];
961            let mut cnt = 0usize;
962            for tri in triangles.iter().filter(|t| t.contains(&i)) {
963                for &j in tri.iter() {
964                    if j != i {
965                        sum = add3(sum, positions[j]);
966                        cnt += 1;
967                    }
968                }
969            }
970            if cnt == 0 {
971                continue;
972            }
973            let new_pos = scale3(sum, 1.0 / cnt as f64);
974
975            // Compute minimum angle before move
976            let min_before = triangles
977                .iter()
978                .filter(|t| t.contains(&i))
979                .map(|t| {
980                    let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
981                    compute_tri_quality(vv[0], vv[1], vv[2]).min_angle_deg
982                })
983                .fold(f64::INFINITY, f64::min);
984
985            let old_pos = positions[i];
986            positions[i] = new_pos;
987
988            let min_after = triangles
989                .iter()
990                .filter(|t| t.contains(&i))
991                .map(|t| {
992                    let vv: Vec<[f64; 3]> = t.iter().map(|&vi| positions[vi]).collect();
993                    compute_tri_quality(vv[0], vv[1], vv[2]).min_angle_deg
994                })
995                .fold(f64::INFINITY, f64::min);
996
997            if min_after >= min_before {
998                total_accepted += 1;
999            } else {
1000                positions[i] = old_pos;
1001            }
1002        }
1003    }
1004
1005    total_accepted
1006}
1007
1008// ---------------------------------------------------------------------------
1009// Tests
1010// ---------------------------------------------------------------------------
1011
1012#[cfg(test)]
1013mod tests {
1014    use super::*;
1015    use std::f64::consts::PI;
1016
1017    /// Vertices of a regular unit tetrahedron centred at origin.
1018    fn unit_tet_vertices() -> ([f64; 3], [f64; 3], [f64; 3], [f64; 3]) {
1019        // Regular tetrahedron with positive orientation (edge length 2*sqrt(2))
1020        let v0 = [1.0, 1.0, 1.0];
1021        let v1 = [-1.0, 1.0, -1.0];
1022        let v2 = [1.0, -1.0, -1.0];
1023        let v3 = [-1.0, -1.0, 1.0];
1024        (v0, v1, v2, v3)
1025    }
1026
1027    #[test]
1028    fn test_unit_tet_aspect_ratio() {
1029        let (v0, v1, v2, v3) = unit_tet_vertices();
1030        let q = compute_tet_quality(v0, v1, v2, v3);
1031        // Regular tet: all edges equal → aspect ratio = 1
1032        assert!(
1033            (q.aspect_ratio - 1.0).abs() < 1e-10,
1034            "aspect_ratio={}, expected 1.0",
1035            q.aspect_ratio
1036        );
1037    }
1038
1039    #[test]
1040    fn test_unit_tet_jacobian_positive() {
1041        let (v0, v1, v2, v3) = unit_tet_vertices();
1042        let q = compute_tet_quality(v0, v1, v2, v3);
1043        assert!(
1044            q.jacobian > 0.0,
1045            "jacobian should be positive, got {}",
1046            q.jacobian
1047        );
1048    }
1049
1050    #[test]
1051    fn test_degenerate_tet_detected() {
1052        // Coplanar: v3 in the same plane as v0, v1, v2 → zero volume
1053        let v0 = [0.0, 0.0, 0.0];
1054        let v1 = [1.0, 0.0, 0.0];
1055        let v2 = [0.0, 1.0, 0.0];
1056        let v3 = [0.5, 0.5, 0.0]; // lies in the same plane
1057        let q = compute_tet_quality(v0, v1, v2, v3);
1058        assert!(
1059            q.is_degenerate(),
1060            "coplanar tet should be degenerate, jacobian={}",
1061            q.jacobian
1062        );
1063    }
1064
1065    #[test]
1066    fn test_repair_degenerate_tets() {
1067        let verts = vec![
1068            [0.0_f64, 0.0, 0.0],
1069            [1.0, 0.0, 0.0],
1070            [0.0, 1.0, 0.0],
1071            [0.0, 0.0, 1.0],
1072            [0.5, 0.5, 0.0], // coplanar with v0, v1, v2
1073        ];
1074        let tets = vec![
1075            [0usize, 1, 2, 3], // valid
1076            [0, 1, 2, 4],      // degenerate (all coplanar)
1077        ];
1078        let mut mesh = TetrahedralMesh::new(verts, tets);
1079        let removed = repair_degenerate_tets(&mut mesh);
1080        assert_eq!(removed, 1, "expected 1 degenerate tet removed");
1081        assert_eq!(mesh.tets.len(), 1);
1082    }
1083
1084    #[test]
1085    fn test_equilateral_tri_quality() {
1086        // Equilateral triangle → aspect_ratio = 1, all angles = 60°, skewness = 0
1087        let h = (3.0_f64).sqrt() / 2.0;
1088        let v0 = [0.0, 0.0, 0.0];
1089        let v1 = [1.0, 0.0, 0.0];
1090        let v2 = [0.5, h, 0.0];
1091        let q = compute_tri_quality(v0, v1, v2);
1092        assert!(
1093            (q.aspect_ratio - 1.0).abs() < 1e-10,
1094            "aspect_ratio={}, expected 1.0",
1095            q.aspect_ratio
1096        );
1097        assert!(
1098            (q.min_angle_deg - 60.0).abs() < 1e-6,
1099            "min_angle={}, expected 60°",
1100            q.min_angle_deg
1101        );
1102        assert!(
1103            (q.skewness).abs() < 1e-6,
1104            "skewness={}, expected 0",
1105            q.skewness
1106        );
1107    }
1108
1109    #[test]
1110    fn test_right_triangle_quality() {
1111        // 3-4-5 right triangle: angles ≈ 36.87°, 53.13°, 90°
1112        let v0 = [0.0, 0.0, 0.0];
1113        let v1 = [3.0, 0.0, 0.0];
1114        let v2 = [0.0, 4.0, 0.0];
1115        let q = compute_tri_quality(v0, v1, v2);
1116        assert!(
1117            (q.max_angle_deg - 90.0).abs() < 1e-6,
1118            "max_angle={}, expected 90°",
1119            q.max_angle_deg
1120        );
1121        assert!(
1122            q.aspect_ratio > 1.0,
1123            "aspect_ratio should be > 1 for non-equilateral"
1124        );
1125    }
1126
1127    #[test]
1128    fn test_smooth_laplacian_runs() {
1129        let mut positions: Vec<[f64; 3]> = vec![
1130            [0.0, 0.0, 0.0],
1131            [1.0, 0.0, 0.0],
1132            [0.0, 1.0, 0.0],
1133            [0.0, 0.0, 1.0],
1134            [1.0, 1.0, 1.0],
1135        ];
1136        let tets = vec![[0usize, 1, 2, 3], [1, 2, 3, 4]];
1137        smooth_laplacian(&mut positions, &tets, 2);
1138        // Just ensure it runs without panic and positions are finite
1139        for p in &positions {
1140            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1141        }
1142    }
1143
1144    #[test]
1145    fn test_flip_edges_2d_no_flip_on_delaunay() {
1146        // Already Delaunay: right-angle triangulation of a unit square
1147        let positions: Vec<[f64; 3]> = vec![
1148            [0.0, 0.0, 0.0],
1149            [1.0, 0.0, 0.0],
1150            [1.0, 1.0, 0.0],
1151            [0.0, 1.0, 0.0],
1152        ];
1153        let mut tris = vec![[0usize, 1, 2], [0, 2, 3]];
1154        let flips = flip_edges_2d(&mut tris, &positions);
1155        // This diagonal is already locally Delaunay, so 0 or 1 flip is acceptable
1156        let _ = flips;
1157        assert_eq!(tris.len(), 2, "triangle count must stay the same");
1158    }
1159
1160    #[test]
1161    fn test_tet_quality_angles_in_range() {
1162        let (v0, v1, v2, v3) = unit_tet_vertices();
1163        let q = compute_tet_quality(v0, v1, v2, v3);
1164        assert!(q.min_angle_deg > 0.0 && q.min_angle_deg < 180.0);
1165        assert!(q.max_angle_deg > 0.0 && q.max_angle_deg < 180.0);
1166        assert!(q.min_angle_deg <= q.max_angle_deg);
1167    }
1168
1169    #[test]
1170    fn test_degenerate_tri_quality() {
1171        // Collinear → zero area → degenerate
1172        let v0 = [0.0, 0.0, 0.0];
1173        let v1 = [1.0, 0.0, 0.0];
1174        let v2 = [2.0, 0.0, 0.0];
1175        let q = compute_tri_quality(v0, v1, v2);
1176        assert!(
1177            q.jacobian.abs() < 1e-10,
1178            "collinear triangle jacobian should be ~0, got {}",
1179            q.jacobian
1180        );
1181    }
1182
1183    #[test]
1184    fn test_pi_constant_used() {
1185        // Sanity-check that PI is imported correctly in this module
1186        let half_pi_deg = PI / 2.0 * 180.0 / PI;
1187        assert!((half_pi_deg - 90.0).abs() < 1e-10);
1188    }
1189
1190    // --- Jacobian-based quality ---
1191
1192    #[test]
1193    fn test_tet_jacobian_det_positive() {
1194        let (v0, v1, v2, v3) = unit_tet_vertices();
1195        let det = tet_jacobian_det(v0, v1, v2, v3);
1196        assert!(
1197            det.abs() > 1e-10,
1198            "regular tet should have non-zero Jacobian det"
1199        );
1200    }
1201
1202    #[test]
1203    fn test_tet_jacobian_det_degenerate() {
1204        let v0 = [0.0, 0.0, 0.0];
1205        let v1 = [1.0, 0.0, 0.0];
1206        let v2 = [0.0, 1.0, 0.0];
1207        let v3 = [0.5, 0.5, 0.0]; // coplanar
1208        let det = tet_jacobian_det(v0, v1, v2, v3);
1209        assert!(
1210            det.abs() < 1e-10,
1211            "coplanar tet should have zero Jacobian det"
1212        );
1213    }
1214
1215    #[test]
1216    fn test_tet_jacobian_matrix() {
1217        let v0 = [0.0, 0.0, 0.0];
1218        let v1 = [1.0, 0.0, 0.0];
1219        let v2 = [0.0, 1.0, 0.0];
1220        let v3 = [0.0, 0.0, 1.0];
1221        let j = tet_jacobian_matrix(v0, v1, v2, v3);
1222        assert!((j[0][0] - 1.0).abs() < 1e-10);
1223        assert!((j[1][1] - 1.0).abs() < 1e-10);
1224        assert!((j[2][2] - 1.0).abs() < 1e-10);
1225    }
1226
1227    // --- Scaled Jacobian ---
1228
1229    #[test]
1230    fn test_scaled_jacobian_regular_tet() {
1231        let (v0, v1, v2, v3) = unit_tet_vertices();
1232        let sj = tet_scaled_jacobian(v0, v1, v2, v3);
1233        assert!(
1234            sj > 0.0,
1235            "regular tet should have positive scaled jacobian, got {sj}"
1236        );
1237    }
1238
1239    #[test]
1240    fn test_scaled_jacobian_degenerate() {
1241        let v0 = [0.0, 0.0, 0.0];
1242        let v1 = [1.0, 0.0, 0.0];
1243        let v2 = [0.0, 1.0, 0.0];
1244        let v3 = [0.5, 0.5, 0.0];
1245        let sj = tet_scaled_jacobian(v0, v1, v2, v3);
1246        assert!(sj.abs() < 1e-10);
1247    }
1248
1249    #[test]
1250    fn test_min_scaled_jacobian_regular_tet() {
1251        let (v0, v1, v2, v3) = unit_tet_vertices();
1252        let msj = tet_min_scaled_jacobian(v0, v1, v2, v3);
1253        assert!(
1254            msj > 0.0,
1255            "min scaled jacobian should be positive for regular tet"
1256        );
1257    }
1258
1259    #[test]
1260    fn test_min_scaled_jacobian_right_angle_tet() {
1261        let v0 = [0.0, 0.0, 0.0];
1262        let v1 = [1.0, 0.0, 0.0];
1263        let v2 = [0.0, 1.0, 0.0];
1264        let v3 = [0.0, 0.0, 1.0];
1265        let msj = tet_min_scaled_jacobian(v0, v1, v2, v3);
1266        assert!(msj > 0.0);
1267    }
1268
1269    // --- Condition number quality ---
1270
1271    #[test]
1272    fn test_condition_number_quality_regular_tet() {
1273        let (v0, v1, v2, v3) = unit_tet_vertices();
1274        let q = tet_condition_number_quality(v0, v1, v2, v3);
1275        assert!(
1276            q > 0.3,
1277            "regular tet should have positive condition number quality, got {q}"
1278        );
1279    }
1280
1281    #[test]
1282    fn test_condition_number_quality_degenerate() {
1283        let v0 = [0.0, 0.0, 0.0];
1284        let v1 = [1.0, 0.0, 0.0];
1285        let v2 = [0.0, 1.0, 0.0];
1286        let v3 = [0.5, 0.5, 0.0];
1287        let q = tet_condition_number_quality(v0, v1, v2, v3);
1288        assert!(q.abs() < 1e-10, "degenerate tet should have zero quality");
1289    }
1290
1291    #[test]
1292    fn test_condition_number_quality_bounded() {
1293        let v0 = [0.0, 0.0, 0.0];
1294        let v1 = [1.0, 0.0, 0.0];
1295        let v2 = [0.0, 1.0, 0.0];
1296        let v3 = [0.0, 0.0, 1.0];
1297        let q = tet_condition_number_quality(v0, v1, v2, v3);
1298        assert!(
1299            (0.0..=1.0).contains(&q),
1300            "quality should be in [0,1], got {q}"
1301        );
1302    }
1303
1304    // --- Quality histogram ---
1305
1306    #[test]
1307    fn test_quality_histogram_basic() {
1308        let values = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
1309        let hist = QualityHistogram::from_values(&values, 5);
1310        assert_eq!(hist.counts.len(), 5);
1311        assert_eq!(hist.total, 10);
1312        let total_count: usize = hist.counts.iter().sum();
1313        assert_eq!(total_count, 10);
1314    }
1315
1316    #[test]
1317    fn test_quality_histogram_single_bin() {
1318        let values = vec![0.5, 0.6, 0.7];
1319        let hist = QualityHistogram::from_values(&values, 1);
1320        assert_eq!(hist.counts, vec![3]);
1321    }
1322
1323    #[test]
1324    fn test_quality_histogram_fractions() {
1325        let values = vec![0.1, 0.2, 0.7, 0.8];
1326        let hist = QualityHistogram::from_values(&values, 2);
1327        let fracs = hist.fractions();
1328        assert_eq!(fracs.len(), 2);
1329        assert!((fracs.iter().sum::<f64>() - 1.0).abs() < 1e-10);
1330    }
1331
1332    #[test]
1333    fn test_quality_histogram_peak_bin() {
1334        let values = vec![0.1, 0.15, 0.12, 0.8, 0.85];
1335        let hist = QualityHistogram::from_values(&values, 5);
1336        let peak = hist.peak_bin();
1337        assert!(peak < 5);
1338    }
1339
1340    #[test]
1341    fn test_quality_histogram_empty() {
1342        let hist = QualityHistogram::from_values(&[], 5);
1343        assert_eq!(hist.total, 0);
1344        assert!(hist.counts.iter().all(|&c| c == 0));
1345    }
1346
1347    // --- Worst element identification ---
1348
1349    #[test]
1350    fn test_find_worst_elements_all_good() {
1351        let qualities = vec![0.8, 0.9, 0.7, 0.85];
1352        let worst = find_worst_elements(&qualities, 0.3);
1353        assert!(worst.is_empty());
1354    }
1355
1356    #[test]
1357    fn test_find_worst_elements_some_bad() {
1358        let qualities = vec![0.8, 0.1, 0.9, 0.2, 0.7];
1359        let worst = find_worst_elements(&qualities, 0.3);
1360        assert_eq!(worst.len(), 2);
1361        // Should be sorted worst first
1362        assert_eq!(worst[0], 1); // quality 0.1
1363        assert_eq!(worst[1], 3); // quality 0.2
1364    }
1365
1366    #[test]
1367    fn test_find_worst_elements_empty() {
1368        let worst = find_worst_elements(&[], 0.5);
1369        assert!(worst.is_empty());
1370    }
1371
1372    // --- Quality stats ---
1373
1374    #[test]
1375    fn test_quality_stats_basic() {
1376        let values = vec![0.1, 0.2, 0.5, 0.8, 0.9];
1377        let stats = QualityStats::compute(&values);
1378        assert!((stats.min - 0.1).abs() < 1e-10);
1379        assert!((stats.max - 0.9).abs() < 1e-10);
1380        assert!((stats.mean - 0.5).abs() < 1e-10);
1381        assert!(stats.std_dev > 0.0);
1382        assert_eq!(stats.n_poor, 2); // 0.1 and 0.2
1383    }
1384
1385    #[test]
1386    fn test_quality_stats_empty() {
1387        let stats = QualityStats::compute(&[]);
1388        assert_eq!(stats.n_poor, 0);
1389        assert!((stats.mean).abs() < 1e-10);
1390    }
1391
1392    #[test]
1393    fn test_quality_stats_uniform() {
1394        let values = vec![0.5, 0.5, 0.5, 0.5];
1395        let stats = QualityStats::compute(&values);
1396        assert!((stats.std_dev).abs() < 1e-10);
1397        assert!((stats.mean - 0.5).abs() < 1e-10);
1398    }
1399
1400    // --- Mesh-level quality computation ---
1401
1402    #[test]
1403    fn test_mesh_scaled_jacobian_qualities() {
1404        let verts = vec![
1405            [0.0, 0.0, 0.0],
1406            [1.0, 0.0, 0.0],
1407            [0.0, 1.0, 0.0],
1408            [0.0, 0.0, 1.0],
1409        ];
1410        let tets = vec![[0, 1, 2, 3]];
1411        let mesh = TetrahedralMesh::new(verts, tets);
1412        let qualities = mesh_scaled_jacobian_qualities(&mesh);
1413        assert_eq!(qualities.len(), 1);
1414        assert!(qualities[0].is_finite());
1415    }
1416
1417    #[test]
1418    fn test_mesh_condition_number_qualities() {
1419        let verts = vec![
1420            [0.0, 0.0, 0.0],
1421            [1.0, 0.0, 0.0],
1422            [0.0, 1.0, 0.0],
1423            [0.0, 0.0, 1.0],
1424        ];
1425        let tets = vec![[0, 1, 2, 3]];
1426        let mesh = TetrahedralMesh::new(verts, tets);
1427        let qualities = mesh_condition_number_qualities(&mesh);
1428        assert_eq!(qualities.len(), 1);
1429        assert!(qualities[0] > 0.0);
1430    }
1431
1432    #[test]
1433    fn test_mesh_quality_pipeline() {
1434        // Full pipeline: compute qualities, build histogram, find worst
1435        let (v0, v1, v2, v3) = unit_tet_vertices();
1436        let mesh = TetrahedralMesh::new(
1437            vec![
1438                v0,
1439                v1,
1440                v2,
1441                v3,
1442                [10.0, 0.0, 0.0],
1443                [11.0, 0.0, 0.0],
1444                [10.0, 1.0, 0.0],
1445                [10.0, 0.0, 0.001],
1446            ],
1447            vec![[0, 1, 2, 3], [4, 5, 6, 7]],
1448        );
1449        let qualities = mesh_scaled_jacobian_qualities(&mesh);
1450        assert_eq!(qualities.len(), 2);
1451
1452        let stats = QualityStats::compute(&qualities);
1453        assert!(stats.min <= stats.max);
1454
1455        let hist = QualityHistogram::from_values(&qualities, 5);
1456        assert_eq!(hist.total, 2);
1457    }
1458
1459    // ── Dihedral angle extremes ─────────────────────────────────────────────
1460
1461    #[test]
1462    fn test_tet_dihedral_extremes_regular() {
1463        let (v0, v1, v2, v3) = unit_tet_vertices();
1464        let (min_d, max_d) = tet_dihedral_angle_extremes(v0, v1, v2, v3);
1465        assert!(min_d > 0.0 && min_d < 180.0);
1466        assert!(max_d > 0.0 && max_d < 180.0);
1467        assert!(min_d <= max_d);
1468    }
1469
1470    #[test]
1471    fn test_tri_angle_extremes_equilateral() {
1472        let h = (3.0_f64).sqrt() / 2.0;
1473        let (min_a, max_a) = tri_angle_extremes([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, h, 0.0]);
1474        assert!(
1475            (min_a - 60.0).abs() < 1e-6,
1476            "min angle should be 60°, got {min_a}"
1477        );
1478        assert!(
1479            (max_a - 60.0).abs() < 1e-6,
1480            "max angle should be 60°, got {max_a}"
1481        );
1482    }
1483
1484    #[test]
1485    fn test_tri_angle_extremes_right_triangle() {
1486        let (min_a, max_a) = tri_angle_extremes([0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 4.0, 0.0]);
1487        assert!((max_a - 90.0).abs() < 1e-6);
1488        assert!(min_a < 90.0);
1489    }
1490
1491    // ── Aspect ratio for all element types ─────────────────────────────────
1492
1493    #[test]
1494    fn test_line_aspect_ratio() {
1495        let ar = line_aspect_ratio([0.0; 3], [1.0, 0.0, 0.0]);
1496        assert!((ar - 1.0).abs() < 1e-12);
1497    }
1498
1499    #[test]
1500    fn test_quad_aspect_ratio_square() {
1501        // Unit square: diagonals are equal → AR = 1
1502        let ar = quad_aspect_ratio(
1503            [0.0, 0.0, 0.0],
1504            [1.0, 0.0, 0.0],
1505            [1.0, 1.0, 0.0],
1506            [0.0, 1.0, 0.0],
1507        );
1508        assert!(
1509            (ar - 1.0).abs() < 1e-10,
1510            "square aspect ratio should be 1, got {ar}"
1511        );
1512    }
1513
1514    #[test]
1515    fn test_quad_aspect_ratio_rectangle() {
1516        // For a rectangle, both diagonals are equal in length, so AR = 1.0.
1517        // The implementation uses max_diagonal / min_diagonal.
1518        let ar = quad_aspect_ratio(
1519            [0.0, 0.0, 0.0],
1520            [4.0, 0.0, 0.0],
1521            [4.0, 1.0, 0.0],
1522            [0.0, 1.0, 0.0],
1523        );
1524        // Rectangle diagonals are equal → ratio is 1.0
1525        assert!(
1526            (ar - 1.0).abs() < 1e-10,
1527            "rectangle has equal diagonals, AR=1, got {ar}"
1528        );
1529    }
1530
1531    #[test]
1532    fn test_wedge_aspect_ratio_regular() {
1533        // Regular triangular prism
1534        let h = (3.0_f64).sqrt() / 2.0;
1535        let ar = wedge_aspect_ratio(
1536            [0.0, 0.0, 0.0],
1537            [1.0, 0.0, 0.0],
1538            [0.5, h, 0.0],
1539            [0.0, 0.0, 1.0],
1540            [1.0, 0.0, 1.0],
1541            [0.5, h, 1.0],
1542        );
1543        assert!(ar >= 1.0, "wedge AR should be >= 1");
1544        // For a prism with all edges = 1, AR should be exactly 1
1545        assert!(
1546            (ar - 1.0).abs() < 1e-10,
1547            "regular prism should have AR=1, got {ar}"
1548        );
1549    }
1550
1551    #[test]
1552    fn test_hex_aspect_ratio_unit_cube() {
1553        let ar = hex_aspect_ratio(
1554            [0.0, 0.0, 0.0],
1555            [1.0, 0.0, 0.0],
1556            [1.0, 1.0, 0.0],
1557            [0.0, 1.0, 0.0],
1558            [0.0, 0.0, 1.0],
1559            [1.0, 0.0, 1.0],
1560            [1.0, 1.0, 1.0],
1561            [0.0, 1.0, 1.0],
1562        );
1563        assert!(
1564            (ar - 1.0).abs() < 1e-10,
1565            "unit cube should have AR=1, got {ar}"
1566        );
1567    }
1568
1569    #[test]
1570    fn test_hex_aspect_ratio_stretched() {
1571        let ar = hex_aspect_ratio(
1572            [0.0, 0.0, 0.0],
1573            [5.0, 0.0, 0.0],
1574            [5.0, 1.0, 0.0],
1575            [0.0, 1.0, 0.0],
1576            [0.0, 0.0, 1.0],
1577            [5.0, 0.0, 1.0],
1578            [5.0, 1.0, 1.0],
1579            [0.0, 1.0, 1.0],
1580        );
1581        assert!(
1582            (ar - 5.0).abs() < 1e-10,
1583            "stretched hex should have AR=5, got {ar}"
1584        );
1585    }
1586
1587    // ── Quality-guided mesh improvement ────────────────────────────────────
1588
1589    #[test]
1590    fn test_quality_guided_smoothing_runs() {
1591        let mut positions: Vec<[f64; 3]> = vec![
1592            [0.0, 0.0, 0.0],
1593            [1.0, 0.0, 0.0],
1594            [0.0, 1.0, 0.0],
1595            [0.0, 0.0, 1.0],
1596            [1.0, 1.0, 1.0],
1597        ];
1598        let tets = vec![[0usize, 1, 2, 3], [1, 2, 3, 4]];
1599        let accepted = quality_guided_smoothing(&mut positions, &tets, 2);
1600        // The function should run without panic; accepted >= 0
1601        let _ = accepted;
1602        for p in &positions {
1603            assert!(p.iter().all(|v| v.is_finite()));
1604        }
1605    }
1606
1607    #[test]
1608    fn test_quality_guided_smoothing_does_not_degrade() {
1609        // Regular tet: smoothing should not degrade quality
1610        let (v0, v1, v2, v3) = unit_tet_vertices();
1611        let mut positions = vec![v0, v1, v2, v3];
1612        let tets = vec![[0usize, 1, 2, 3]];
1613        let q_before =
1614            tet_min_scaled_jacobian(positions[0], positions[1], positions[2], positions[3]);
1615        quality_guided_smoothing(&mut positions, &tets, 3);
1616        let q_after =
1617            tet_min_scaled_jacobian(positions[0], positions[1], positions[2], positions[3]);
1618        assert!(
1619            q_after >= q_before - 1e-10,
1620            "quality should not degrade: before={q_before}, after={q_after}"
1621        );
1622    }
1623
1624    // ── Minimum angle optimization ─────────────────────────────────────────
1625
1626    #[test]
1627    fn test_minimum_angle_optimization_runs() {
1628        let h = (3.0_f64).sqrt() / 2.0;
1629        let mut positions: Vec<[f64; 3]> = vec![
1630            [0.0, 0.0, 0.0],
1631            [1.0, 0.0, 0.0],
1632            [0.5, h, 0.0],
1633            [1.5, h, 0.0],
1634        ];
1635        let tris = vec![[0usize, 1, 2], [1, 3, 2]];
1636        let accepted = minimum_angle_optimization(&mut positions, &tris, 3);
1637        let _ = accepted;
1638        for p in &positions {
1639            assert!(p.iter().all(|v| v.is_finite()));
1640        }
1641    }
1642
1643    #[test]
1644    fn test_minimum_angle_optimization_equilateral_unchanged() {
1645        // An equilateral triangle should not change (already optimal)
1646        let h = (3.0_f64).sqrt() / 2.0;
1647        let v0 = [0.0, 0.0, 0.0];
1648        let v1 = [1.0, 0.0, 0.0];
1649        let v2 = [0.5, h, 0.0];
1650        let v3 = [1.5, h, 0.0];
1651        let mut positions = vec![v0, v1, v2, v3];
1652        let tris = vec![[0usize, 1, 2], [1, 3, 2]];
1653        minimum_angle_optimization(&mut positions, &tris, 5);
1654        // Vertices should remain finite
1655        for p in &positions {
1656            assert!(p.iter().all(|v| v.is_finite()));
1657        }
1658    }
1659}