Skip to main content

oxiphysics_geometry/
mesh_quality.rs

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