//! P0 step 2: the operators are exact on linear fields over a skewed, //! stretched, periodic annulus — nodal reconstruction at interior nodes, //! the least-squares cell gradient, and the face operator `L_f` //! (seam faces included). use rtx_cfd::mesh::patch_gen::annulus_skewed; use rtx_cfd::solvers::incompressible::{Operators, PatchBoundaries}; #[test] fn nodes_gradients_and_face_operator_are_exact_on_linear_fields() { let mesh = annulus_skewed([0.2, -0.1], 0.5, 1.5, 32, 8, 0.3, 3.0).unwrap(); let ops = Operators::new(&mesh, &PatchBoundaries::default()); let (a, b, c) = (0.7, -1.3, 2.1); let phi = |x: f64, y: f64| a + b * x + c * y; let cell_vals: Vec = (0..mesh.cell_count()) .map(|i| { let xy = mesh.centre(i); phi(xy[0], xy[1]) }) .collect(); // Interior nodes (rows 1..nn-1, every column including the seam). let nodes = ops.node_values(&mesh, &cell_vals, &|_, _| None); let mut worst_node = 0.0_f64; for k in 1..mesh.nn() { for i in 0..=mesh.ns() { let n = mesh.node(k, i); let xy = mesh.node_xy(n); worst_node = worst_node.max((nodes[n] - phi(xy[0], xy[1])).abs()); } } assert!( worst_node < 1e-13, "interior node reconstruction error {worst_node:.3e}" ); // Cell gradients with the boundary faces at their exact values. let mut worst_grad = 0.0_f64; for cell in 0..mesh.cell_count() { let g = ops.gradient(&mesh, cell, &cell_vals, &|f| { let xy = mesh.faces()[f].centre; Some(phi(xy[0], xy[1])) }); worst_grad = worst_grad.max((g[0] - b).abs()).max((g[1] - c).abs()); } assert!(worst_grad < 1e-12, "gradient error {worst_grad:.3e}"); // L_f on interior faces with exact node values: (∇φ)·S = b S_x + c S_y. let exact_nodes: Vec = (0..ops.nodes().len()) .map(|n| { let xy = mesh.node_xy(n); phi(xy[0], xy[1]) }) .collect(); let mut worst_face = 0.0_f64; let mut seam_checked = 0; for (f, face) in mesh.faces().iter().enumerate() { if face.owner.is_none() || face.neigh.is_none() { continue; } let lf = ops.face_gradient_flux(&mesh, f, &cell_vals, &exact_nodes, None); let exact = b * face.s[0] + c * face.s[1]; let scale = (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt() * (b.abs() + c.abs()); worst_face = worst_face.max((lf - exact).abs() / scale); if mesh.is_sface(f) && f % mesh.sfaces_per_row() == 0 { seam_checked += 1; } } assert_eq!(seam_checked, mesh.nn()); assert!(worst_face < 1e-13, "face operator error {worst_face:.3e}"); // With the reconstructed (not exact) node values the interior faces are // still exact, since the reconstruction is. let mut worst_face2 = 0.0_f64; for k in 1..mesh.nn() - 1 { for i in 0..mesh.ns() { let f = mesh.sface(k, i); let face = &mesh.faces()[f]; let lf = ops.face_gradient_flux(&mesh, f, &cell_vals, &nodes, None); let exact = b * face.s[0] + c * face.s[1]; worst_face2 = worst_face2.max((lf - exact).abs()); } } assert!( worst_face2 < 1e-12, "face operator with reconstructed nodes {worst_face2:.3e}" ); }