Skip to main content

math_audio_bem/core/mesh/
generators.rs

1//! Mesh generators for analytical test geometries
2//!
3//! Provides functions to generate surface meshes for standard geometries
4//! used in BEM validation: spheres, cylinders, etc.
5
6use ndarray::{Array1, Array2};
7use num_complex::Complex64;
8use std::f64::consts::PI;
9
10use crate::core::types::{BoundaryCondition, Element, ElementProperty, ElementType, Mesh};
11
12/// Generate a spherical surface mesh using UV-sphere discretization
13///
14/// Creates a triangulated sphere with vertices distributed along
15/// latitude/longitude lines.
16///
17/// # Arguments
18/// * `radius` - Sphere radius
19/// * `n_theta` - Number of divisions in polar direction (latitude)
20/// * `n_phi` - Number of divisions in azimuthal direction (longitude)
21///
22/// # Returns
23/// A `Mesh` with triangular elements covering the sphere surface
24///
25/// # Example
26/// ```ignore
27/// let mesh = generate_sphere_mesh(1.0, 16, 32);
28/// ```
29pub fn generate_sphere_mesh(radius: f64, n_theta: usize, n_phi: usize) -> Mesh {
30    let mut nodes = Vec::new();
31    let mut elements = Vec::new();
32
33    // Generate nodes
34    // North pole
35    nodes.push([0.0, 0.0, radius]);
36
37    // Interior latitude bands
38    for i in 1..n_theta {
39        let theta = PI * i as f64 / n_theta as f64;
40        let sin_theta = theta.sin();
41        let cos_theta = theta.cos();
42
43        for j in 0..n_phi {
44            let phi = 2.0 * PI * j as f64 / n_phi as f64;
45            let x = radius * sin_theta * phi.cos();
46            let y = radius * sin_theta * phi.sin();
47            let z = radius * cos_theta;
48            nodes.push([x, y, z]);
49        }
50    }
51
52    // South pole
53    nodes.push([0.0, 0.0, -radius]);
54
55    let n_nodes = nodes.len();
56    let south_pole_idx = n_nodes - 1;
57
58    // Generate elements
59    // North polar cap (triangles connecting to north pole)
60    for j in 0..n_phi {
61        let j_next = (j + 1) % n_phi;
62        elements.push(vec![0, 1 + j, 1 + j_next]);
63    }
64
65    // Middle bands (quad → 2 triangles)
66    for i in 0..(n_theta - 2) {
67        let row_start = 1 + i * n_phi;
68        let next_row_start = 1 + (i + 1) * n_phi;
69
70        for j in 0..n_phi {
71            let j_next = (j + 1) % n_phi;
72
73            // Current row nodes
74            let n0 = row_start + j;
75            let n1 = row_start + j_next;
76            // Next row nodes
77            let n2 = next_row_start + j;
78            let n3 = next_row_start + j_next;
79
80            // Two triangles per quad
81            elements.push(vec![n0, n2, n1]);
82            elements.push(vec![n1, n2, n3]);
83        }
84    }
85
86    // South polar cap
87    let last_row_start = 1 + (n_theta - 2) * n_phi;
88    for j in 0..n_phi {
89        let j_next = (j + 1) % n_phi;
90        elements.push(vec![
91            last_row_start + j,
92            south_pole_idx,
93            last_row_start + j_next,
94        ]);
95    }
96
97    create_mesh_from_data(nodes, elements, radius)
98}
99
100/// Generate an icosphere mesh (subdivided icosahedron)
101///
102/// More uniform element sizes than UV-sphere, better for BEM.
103///
104/// # Arguments
105/// * `radius` - Sphere radius
106/// * `subdivisions` - Number of subdivision iterations (0=icosahedron, 1=42 verts, 2=162 verts)
107///
108/// # Returns
109/// A `Mesh` with triangular elements
110pub fn generate_icosphere_mesh(radius: f64, subdivisions: usize) -> Mesh {
111    // Golden ratio
112    let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
113    let _scale = radius / (1.0 + phi * phi).sqrt();
114
115    // Initial icosahedron vertices
116    let mut vertices: Vec<[f64; 3]> = vec![
117        [-1.0, phi, 0.0],
118        [1.0, phi, 0.0],
119        [-1.0, -phi, 0.0],
120        [1.0, -phi, 0.0],
121        [0.0, -1.0, phi],
122        [0.0, 1.0, phi],
123        [0.0, -1.0, -phi],
124        [0.0, 1.0, -phi],
125        [phi, 0.0, -1.0],
126        [phi, 0.0, 1.0],
127        [-phi, 0.0, -1.0],
128        [-phi, 0.0, 1.0],
129    ];
130
131    // Scale to unit sphere
132    for v in &mut vertices {
133        let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
134        v[0] /= len;
135        v[1] /= len;
136        v[2] /= len;
137    }
138
139    // Initial icosahedron faces
140    let mut faces: Vec<[usize; 3]> = vec![
141        [0, 11, 5],
142        [0, 5, 1],
143        [0, 1, 7],
144        [0, 7, 10],
145        [0, 10, 11],
146        [1, 5, 9],
147        [5, 11, 4],
148        [11, 10, 2],
149        [10, 7, 6],
150        [7, 1, 8],
151        [3, 9, 4],
152        [3, 4, 2],
153        [3, 2, 6],
154        [3, 6, 8],
155        [3, 8, 9],
156        [4, 9, 5],
157        [2, 4, 11],
158        [6, 2, 10],
159        [8, 6, 7],
160        [9, 8, 1],
161    ];
162
163    // Subdivide
164    for _ in 0..subdivisions {
165        let mut new_faces = Vec::new();
166        let mut edge_midpoints: std::collections::HashMap<(usize, usize), usize> =
167            std::collections::HashMap::new();
168
169        for face in &faces {
170            let v0 = face[0];
171            let v1 = face[1];
172            let v2 = face[2];
173
174            // Get or create midpoints
175            let m01 = get_midpoint(&mut vertices, &mut edge_midpoints, v0, v1);
176            let m12 = get_midpoint(&mut vertices, &mut edge_midpoints, v1, v2);
177            let m20 = get_midpoint(&mut vertices, &mut edge_midpoints, v2, v0);
178
179            // Create 4 new faces
180            new_faces.push([v0, m01, m20]);
181            new_faces.push([v1, m12, m01]);
182            new_faces.push([v2, m20, m12]);
183            new_faces.push([m01, m12, m20]);
184        }
185
186        faces = new_faces;
187    }
188
189    // Scale to desired radius
190    let nodes: Vec<[f64; 3]> = vertices
191        .iter()
192        .map(|v| [v[0] * radius, v[1] * radius, v[2] * radius])
193        .collect();
194
195    let elements: Vec<Vec<usize>> = faces.iter().map(|f| vec![f[0], f[1], f[2]]).collect();
196
197    create_mesh_from_data(nodes, elements, radius)
198}
199
200/// Helper for icosphere: get or create edge midpoint
201fn get_midpoint(
202    vertices: &mut Vec<[f64; 3]>,
203    cache: &mut std::collections::HashMap<(usize, usize), usize>,
204    v0: usize,
205    v1: usize,
206) -> usize {
207    let key = if v0 < v1 { (v0, v1) } else { (v1, v0) };
208
209    if let Some(&idx) = cache.get(&key) {
210        return idx;
211    }
212
213    // Create new vertex at midpoint, projected to unit sphere
214    let mid = [
215        (vertices[v0][0] + vertices[v1][0]) / 2.0,
216        (vertices[v0][1] + vertices[v1][1]) / 2.0,
217        (vertices[v0][2] + vertices[v1][2]) / 2.0,
218    ];
219
220    let len = (mid[0] * mid[0] + mid[1] * mid[1] + mid[2] * mid[2]).sqrt();
221    let normalized = [mid[0] / len, mid[1] / len, mid[2] / len];
222
223    let idx = vertices.len();
224    vertices.push(normalized);
225    cache.insert(key, idx);
226
227    idx
228}
229
230/// Generate a cylindrical surface mesh
231///
232/// Creates a mesh for a finite cylinder (without end caps).
233///
234/// # Arguments
235/// * `radius` - Cylinder radius
236/// * `height` - Cylinder height (centered at z=0)
237/// * `n_circumference` - Number of divisions around circumference
238/// * `n_height` - Number of divisions along height
239///
240/// # Returns
241/// A `Mesh` with quadrilateral elements
242pub fn generate_cylinder_mesh(
243    radius: f64,
244    height: f64,
245    n_circumference: usize,
246    n_height: usize,
247) -> Mesh {
248    let mut nodes = Vec::new();
249    let mut elements = Vec::new();
250
251    let z_min = -height / 2.0;
252    let dz = height / n_height as f64;
253
254    // Generate nodes
255    for i in 0..=n_height {
256        let z = z_min + i as f64 * dz;
257
258        for j in 0..n_circumference {
259            let phi = 2.0 * PI * j as f64 / n_circumference as f64;
260            let x = radius * phi.cos();
261            let y = radius * phi.sin();
262            nodes.push([x, y, z]);
263        }
264    }
265
266    // Generate quad elements
267    for i in 0..n_height {
268        let row_start = i * n_circumference;
269        let next_row_start = (i + 1) * n_circumference;
270
271        for j in 0..n_circumference {
272            let j_next = (j + 1) % n_circumference;
273
274            let n0 = row_start + j;
275            let n1 = row_start + j_next;
276            let n2 = next_row_start + j_next;
277            let n3 = next_row_start + j;
278
279            elements.push(vec![n0, n1, n2, n3]);
280        }
281    }
282
283    create_mesh_from_data(nodes, elements, radius)
284}
285
286/// Generate a cylinder mesh with end caps (closed cylinder)
287pub fn generate_closed_cylinder_mesh(
288    radius: f64,
289    height: f64,
290    n_circumference: usize,
291    n_height: usize,
292    n_cap_rings: usize,
293) -> Mesh {
294    let mut nodes = Vec::new();
295    let mut elements = Vec::new();
296
297    let z_min = -height / 2.0;
298    let z_max = height / 2.0;
299    let dz = height / n_height as f64;
300
301    // Lateral surface nodes
302    for i in 0..=n_height {
303        let z = z_min + i as f64 * dz;
304        for j in 0..n_circumference {
305            let phi = 2.0 * PI * j as f64 / n_circumference as f64;
306            nodes.push([radius * phi.cos(), radius * phi.sin(), z]);
307        }
308    }
309
310    let _lateral_node_count = nodes.len();
311
312    // Bottom cap center
313    let bottom_center_idx = nodes.len();
314    nodes.push([0.0, 0.0, z_min]);
315
316    // Bottom cap rings
317    for ring in 1..=n_cap_rings {
318        let r = radius * ring as f64 / n_cap_rings as f64;
319        for j in 0..n_circumference {
320            let phi = 2.0 * PI * j as f64 / n_circumference as f64;
321            nodes.push([r * phi.cos(), r * phi.sin(), z_min]);
322        }
323    }
324
325    // Top cap center
326    let top_center_idx = nodes.len();
327    nodes.push([0.0, 0.0, z_max]);
328
329    // Top cap rings
330    for ring in 1..=n_cap_rings {
331        let r = radius * ring as f64 / n_cap_rings as f64;
332        for j in 0..n_circumference {
333            let phi = 2.0 * PI * j as f64 / n_circumference as f64;
334            nodes.push([r * phi.cos(), r * phi.sin(), z_max]);
335        }
336    }
337
338    // Lateral surface elements (quads)
339    for i in 0..n_height {
340        let row_start = i * n_circumference;
341        let next_row_start = (i + 1) * n_circumference;
342        for j in 0..n_circumference {
343            let j_next = (j + 1) % n_circumference;
344            elements.push(vec![
345                row_start + j,
346                row_start + j_next,
347                next_row_start + j_next,
348                next_row_start + j,
349            ]);
350        }
351    }
352
353    // Bottom cap elements (triangles to center, then quads between rings)
354    // Center triangles
355    let bottom_ring1_start = bottom_center_idx + 1;
356    for j in 0..n_circumference {
357        let j_next = (j + 1) % n_circumference;
358        elements.push(vec![
359            bottom_center_idx,
360            bottom_ring1_start + j_next,
361            bottom_ring1_start + j,
362        ]);
363    }
364
365    // Ring quads (bottom)
366    for ring in 0..(n_cap_rings - 1) {
367        let ring_start = bottom_center_idx + 1 + ring * n_circumference;
368        let next_ring_start = ring_start + n_circumference;
369        for j in 0..n_circumference {
370            let j_next = (j + 1) % n_circumference;
371            elements.push(vec![
372                ring_start + j,
373                ring_start + j_next,
374                next_ring_start + j_next,
375                next_ring_start + j,
376            ]);
377        }
378    }
379
380    // Connect outer bottom ring to lateral surface
381    let outer_bottom_ring = bottom_center_idx + 1 + (n_cap_rings - 1) * n_circumference;
382    for j in 0..n_circumference {
383        let j_next = (j + 1) % n_circumference;
384        elements.push(vec![
385            outer_bottom_ring + j,
386            outer_bottom_ring + j_next,
387            j_next,
388            j,
389        ]);
390    }
391
392    // Top cap (similar structure, opposite winding)
393    let top_ring1_start = top_center_idx + 1;
394    for j in 0..n_circumference {
395        let j_next = (j + 1) % n_circumference;
396        elements.push(vec![
397            top_center_idx,
398            top_ring1_start + j,
399            top_ring1_start + j_next,
400        ]);
401    }
402
403    // Ring quads (top)
404    for ring in 0..(n_cap_rings - 1) {
405        let ring_start = top_center_idx + 1 + ring * n_circumference;
406        let next_ring_start = ring_start + n_circumference;
407        for j in 0..n_circumference {
408            let j_next = (j + 1) % n_circumference;
409            elements.push(vec![
410                ring_start + j,
411                next_ring_start + j,
412                next_ring_start + j_next,
413                ring_start + j_next,
414            ]);
415        }
416    }
417
418    // Connect outer top ring to lateral surface
419    let outer_top_ring = top_center_idx + 1 + (n_cap_rings - 1) * n_circumference;
420    let top_lateral_row = n_height * n_circumference;
421    for j in 0..n_circumference {
422        let j_next = (j + 1) % n_circumference;
423        elements.push(vec![
424            top_lateral_row + j,
425            top_lateral_row + j_next,
426            outer_top_ring + j_next,
427            outer_top_ring + j,
428        ]);
429    }
430
431    create_mesh_from_data(nodes, elements, radius)
432}
433
434/// Create a Mesh struct from raw node and element data
435fn create_mesh_from_data(
436    nodes: Vec<[f64; 3]>,
437    connectivity: Vec<Vec<usize>>,
438    _char_length: f64,
439) -> Mesh {
440    let n_nodes = nodes.len();
441    let n_elements = connectivity.len();
442
443    // Create node array
444    let mut node_array = Array2::zeros((n_nodes, 3));
445    for (i, node) in nodes.iter().enumerate() {
446        node_array[[i, 0]] = node[0];
447        node_array[[i, 1]] = node[1];
448        node_array[[i, 2]] = node[2];
449    }
450
451    // Create elements
452    let mut elements = Vec::with_capacity(n_elements);
453    for (idx, conn) in connectivity.iter().enumerate() {
454        let element_type = if conn.len() == 3 {
455            ElementType::Tri3
456        } else {
457            ElementType::Quad4
458        };
459
460        let mut elem = Element {
461            connectivity: conn.clone(),
462            element_type,
463            property: ElementProperty::Surface,
464            normal: Array1::zeros(3),
465            node_normals: Array2::zeros((element_type.num_nodes(), 3)),
466            center: Array1::zeros(3),
467            area: 0.0,
468            boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
469            group: 0,
470            dof_addresses: vec![idx],
471        };
472
473        // Compute geometry
474        compute_element_geometry(&mut elem, &node_array);
475        elements.push(elem);
476    }
477
478    // Compute node normals (average of adjacent element normals)
479    let mut node_counts = vec![0usize; n_nodes];
480
481    for elem in &elements {
482        for &node_idx in &elem.connectivity {
483            node_counts[node_idx] += 1;
484        }
485    }
486
487    // Update element node normals
488    for elem in &mut elements {
489        for (local_idx, &_node_idx) in elem.connectivity.iter().enumerate() {
490            // For sphere/cylinder, normal at node ≈ element normal (constant elements)
491            for j in 0..3 {
492                elem.node_normals[[local_idx, j]] = elem.normal[j];
493            }
494        }
495    }
496
497    Mesh {
498        nodes: node_array,
499        elements,
500        external_node_numbers: (0..n_nodes as i32).collect(),
501        external_element_numbers: (0..n_elements as i32).collect(),
502        num_boundary_nodes: n_nodes,
503        num_evaluation_nodes: 0,
504        num_boundary_elements: n_elements,
505        num_evaluation_elements: 0,
506        symmetry_planes: [0, 0, 0],
507        symmetry_coordinates: [0.0, 0.0, 0.0],
508        num_reflections: 0,
509    }
510}
511
512/// Compute element center, area, and normal
513fn compute_element_geometry(elem: &mut Element, nodes: &Array2<f64>) {
514    let n = elem.connectivity.len();
515
516    // Compute center
517    elem.center = Array1::zeros(3);
518    for &node_idx in &elem.connectivity {
519        for j in 0..3 {
520            elem.center[j] += nodes[[node_idx, j]];
521        }
522    }
523    for j in 0..3 {
524        elem.center[j] /= n as f64;
525    }
526
527    // Compute normal and area
528    if n == 3 {
529        // Triangle
530        let n0 = elem.connectivity[0];
531        let n1 = elem.connectivity[1];
532        let n2 = elem.connectivity[2];
533
534        let v1 = [
535            nodes[[n1, 0]] - nodes[[n0, 0]],
536            nodes[[n1, 1]] - nodes[[n0, 1]],
537            nodes[[n1, 2]] - nodes[[n0, 2]],
538        ];
539        let v2 = [
540            nodes[[n2, 0]] - nodes[[n0, 0]],
541            nodes[[n2, 1]] - nodes[[n0, 1]],
542            nodes[[n2, 2]] - nodes[[n0, 2]],
543        ];
544
545        // Cross product
546        let cross = [
547            v1[1] * v2[2] - v1[2] * v2[1],
548            v1[2] * v2[0] - v1[0] * v2[2],
549            v1[0] * v2[1] - v1[1] * v2[0],
550        ];
551
552        let len = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
553        elem.area = len / 2.0;
554
555        if len > 1e-15 {
556            elem.normal = Array1::from_vec(vec![cross[0] / len, cross[1] / len, cross[2] / len]);
557        }
558    } else {
559        // Quad - use diagonal cross product
560        let n0 = elem.connectivity[0];
561        let n1 = elem.connectivity[1];
562        let n2 = elem.connectivity[2];
563        let n3 = elem.connectivity[3];
564
565        let d1 = [
566            nodes[[n2, 0]] - nodes[[n0, 0]],
567            nodes[[n2, 1]] - nodes[[n0, 1]],
568            nodes[[n2, 2]] - nodes[[n0, 2]],
569        ];
570        let d2 = [
571            nodes[[n3, 0]] - nodes[[n1, 0]],
572            nodes[[n3, 1]] - nodes[[n1, 1]],
573            nodes[[n3, 2]] - nodes[[n1, 2]],
574        ];
575
576        let cross = [
577            d1[1] * d2[2] - d1[2] * d2[1],
578            d1[2] * d2[0] - d1[0] * d2[2],
579            d1[0] * d2[1] - d1[1] * d2[0],
580        ];
581
582        let len = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
583        elem.area = len / 2.0; // Approximate area
584
585        if len > 1e-15 {
586            elem.normal = Array1::from_vec(vec![cross[0] / len, cross[1] / len, cross[2] / len]);
587        }
588    }
589
590    // Ensure normal points outward (for convex shapes centered at origin)
591    // Check: normal should point in same direction as center (n · c > 0)
592    let n_dot_c = elem.normal[0] * elem.center[0]
593        + elem.normal[1] * elem.center[1]
594        + elem.normal[2] * elem.center[2];
595
596    if n_dot_c < 0.0 {
597        // Flip normal to point outward
598        elem.normal[0] = -elem.normal[0];
599        elem.normal[1] = -elem.normal[1];
600        elem.normal[2] = -elem.normal[2];
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_sphere_mesh_generation() {
610        let mesh = generate_sphere_mesh(1.0, 8, 16);
611
612        assert!(mesh.nodes.nrows() > 0);
613        assert!(!mesh.elements.is_empty());
614
615        // Check all nodes are on the sphere
616        for i in 0..mesh.nodes.nrows() {
617            let r = (mesh.nodes[[i, 0]].powi(2)
618                + mesh.nodes[[i, 1]].powi(2)
619                + mesh.nodes[[i, 2]].powi(2))
620            .sqrt();
621            assert!((r - 1.0).abs() < 1e-10, "Node {} not on sphere: r={}", i, r);
622        }
623
624        // Check elements are valid triangles
625        for elem in &mesh.elements {
626            assert_eq!(elem.connectivity.len(), 3);
627            assert!(elem.area > 0.0);
628        }
629    }
630
631    #[test]
632    fn test_icosphere_mesh_generation() {
633        let mesh = generate_icosphere_mesh(1.0, 2);
634
635        // Subdivision 2 should give 162 vertices
636        assert_eq!(mesh.nodes.nrows(), 162);
637
638        // Check all nodes are on unit sphere
639        for i in 0..mesh.nodes.nrows() {
640            let r = (mesh.nodes[[i, 0]].powi(2)
641                + mesh.nodes[[i, 1]].powi(2)
642                + mesh.nodes[[i, 2]].powi(2))
643            .sqrt();
644            assert!((r - 1.0).abs() < 1e-10);
645        }
646    }
647
648    #[test]
649    fn test_cylinder_mesh_generation() {
650        let mesh = generate_cylinder_mesh(1.0, 2.0, 16, 8);
651
652        assert!(mesh.nodes.nrows() > 0);
653        assert!(!mesh.elements.is_empty());
654
655        // Check all nodes are on cylinder surface
656        for i in 0..mesh.nodes.nrows() {
657            let r = (mesh.nodes[[i, 0]].powi(2) + mesh.nodes[[i, 1]].powi(2)).sqrt();
658            assert!(
659                (r - 1.0).abs() < 1e-10,
660                "Node {} not on cylinder: r={}",
661                i,
662                r
663            );
664        }
665
666        // Check elements are valid quads
667        for elem in &mesh.elements {
668            assert_eq!(elem.connectivity.len(), 4);
669            assert!(elem.area > 0.0);
670        }
671    }
672
673    #[test]
674    fn test_sphere_normals_point_outward() {
675        let mesh = generate_icosphere_mesh(1.0, 1);
676
677        for elem in &mesh.elements {
678            // Normal should point away from origin (outward)
679            let dot = elem.normal[0] * elem.center[0]
680                + elem.normal[1] * elem.center[1]
681                + elem.normal[2] * elem.center[2];
682            assert!(dot > 0.0, "Normal should point outward");
683        }
684    }
685
686    #[test]
687    fn test_sphere_surface_area() {
688        // Higher resolution for accurate area
689        let mesh = generate_icosphere_mesh(1.0, 4);
690
691        let total_area: f64 = mesh.elements.iter().map(|e| e.area).sum();
692        let expected_area = 4.0 * PI; // Surface area of unit sphere
693
694        let error = (total_area - expected_area).abs() / expected_area;
695        assert!(error < 0.01, "Surface area error: {:.2}%", error * 100.0);
696    }
697}