diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/cutwall.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/cutwall.rs index 0d66dd5..f7c8a4c 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/cutwall.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/cutwall.rs @@ -221,6 +221,8 @@ impl Mask { wall_exchange_axis: false, cv_sides_exact: false, wall_order2_centroid: false, + wall_exchange_foot: false, + conv_sides_exact: false, grad_weights: None, diffusion_centroid: false, face_shifts: None, @@ -435,6 +437,29 @@ impl Mask { /// crosses a side (the in-plane momentum residual on oblique walls). /// `None` at a domain side (the default stays). fn exact_cv_sides(&self, cut: &CutGeometry, c: usize, p: [i64; 3]) -> Option<[[f64; 2]; 3]> { + let parts = self.exact_cv_side_parts(cut, c, p)?; + let mut ap = [[1.0; 2]; 3]; + for d in 0..3 { + for side in 0..2 { + ap[d][side] = if d == c { + parts[d][side].0 + } else { + 0.5 * (parts[d][side].0 + parts[d][side].1) + }; + } + } + Some(ap) + } + + /// The parts of [`Self::exact_cv_sides`]: across `c`, the (far half of + /// `cell_minus`'s face, near half of `cell_plus`'s face) apertures per + /// side; along `c`, the centre plane's aperture (twice). + pub(super) fn exact_cv_side_parts( + &self, + cut: &CutGeometry, + c: usize, + p: [i64; 3], + ) -> Option<[[(f64, f64); 2]; 3]> { let lat = self.lattice(); let g = self.grid; let mut pm = p; @@ -456,7 +481,7 @@ impl Mask { corner(cell, o) } }; - let mut ap = [[1.0; 2]; 3]; + let mut ap = [[(1.0, 1.0); 2]; 3]; for d in 0..3 { if d == c { // The two cells' centre planes across `c`: corners at the @@ -469,7 +494,8 @@ impl Mask { o[d2] = o2; value(*cell, o, true) }; - ap[d][side] = super::cut::quad_fraction(mid(0, 0), mid(1, 0), mid(0, 1), mid(1, 1)); + let a = super::cut::quad_fraction(mid(0, 0), mid(1, 0), mid(0, 1), mid(1, 1)); + ap[d][side] = (a, a); } } else { let e = 3 - c - d; @@ -496,7 +522,7 @@ impl Mask { super::cut::quad_fraction(at(0, 0), at(1, 0), at(0, 1), at(1, 1)) } }; - ap[d][side] = 0.5 * (half(cells[0], true) + half(cells[1], false)); + ap[d][side] = (half(cells[0], true), half(cells[1], false)); } } } diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/cut_predictor.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/cut_predictor.rs index ad10418..4c1c2dd 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/cut_predictor.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/cut_predictor.rs @@ -109,6 +109,12 @@ impl Solver { let val = |cc: usize, q: [i64; 3]| lat.face(cc, q).map(|f| old[cc][f]); let ap = |cc: usize, q: [i64; 3]| mask.aperture(cc, q); let cv = mask.cv_geometry(c, p); + // S2-7b: the convective sides' own apertures (host prototype). + let conv_parts = if mask.conv_sides_exact { + mask.cut().and_then(|cut| mask.exact_cv_side_parts(cut, c, p)) + } else { + None + }; let x = lat.face_position(c, p); let u0 = val(c, p).expect("the face"); let ec = e(c); @@ -133,7 +139,22 @@ impl Solver { // The control volume's mass fluxes through its plus / minus faces // along d: the averages of the two adjacent cells' face fluxes // (own direction: the face's and its neighbours' fluxes). - let (m_plus, m_minus) = if d == c { + let (m_plus, m_minus) = if let Some(parts) = conv_parts { + if d == c { + // The centre planes' apertures times the mean velocity. + ( + parts[d][1].0 * 0.5 * (u0 + up1.unwrap_or(u0)) * a_d, + parts[d][0].0 * 0.5 * (dn1.unwrap_or(u0) + u0) * a_d, + ) + } else { + // The two half faces' own apertures times their faces' values. + let v = |q: [i64; 3]| val(d, q).unwrap_or(0.0); + ( + 0.5 * (parts[d][1].0 * v(add(cell_minus, ed, 1)) + parts[d][1].1 * v(add(cell_plus, ed, 1))) * a_d, + 0.5 * (parts[d][0].0 * v(cell_minus) + parts[d][0].1 * v(cell_plus)) * a_d, + ) + } + } else if d == c { let f_up = ap(c, add(p, ec, 1)).unwrap_or(cv.alpha) * up1.unwrap_or(u0); let f_dn = ap(c, add(p, ec, -1)).unwrap_or(cv.alpha) * dn1.unwrap_or(u0); let f0 = cv.alpha * u0; @@ -278,11 +299,22 @@ impl Solver { diff -= sign * mu * gap * a_d * transverse(q, uq, sign) / spacing(q, sign); } } + // S2-7b: the wall velocity at the axis foot (distance δ along d) + // instead of the solid face's own value at distance h. + let foot_value = |un: f64, sign: f64| -> f64 { + if !mask.wall_exchange_foot { + return un; + } + let delta = mask.exchange_delta(&cv, d); + let mut xf = x; + xf[d] += sign * delta; + mask.surface_velocity_at(body, xf, c, t_old) + }; diff += match up1 { Some(un) if solid(add(p, ed, 1)) => { let k = mu * g_plus * a_d / mask.exchange_delta(&cv, d); wall_implicit += k; - wall_rhs += k * un; + wall_rhs += k * foot_value(un, 1.0); 0.0 } Some(un) if centroid => { @@ -310,7 +342,7 @@ impl Solver { Some(ud) if solid(add(p, ed, -1)) => { let k = mu * g_minus * a_d / mask.exchange_delta(&cv, d); wall_implicit += k; - wall_rhs += k * ud; + wall_rhs += k * foot_value(ud, -1.0); 0.0 } Some(ud) if centroid => { diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/device.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/device.rs index 4271931..d9715bf 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/device.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/device.rs @@ -177,6 +177,10 @@ impl DeviceStep { !solver.params.wall_order2_centroid, "the order-2 second point at the neighbour's centroid (S2-7) is a host prototype: the device kernels do not carry it" ); + assert!( + !solver.params.wall_exchange_foot && !solver.params.conv_sides_exact, + "the axis-foot exchange / exact convective sides (S2-7b) are host prototypes: the device kernels do not carry them" + ); assert!( !solver.params.cv_sides_exact, "the exact control-volume sides (S2-7) are a host prototype: the device kernels do not carry it" diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/mod.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/mod.rs index 7c518a8..b3b46d2 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/mod.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/step/mod.rs @@ -163,6 +163,17 @@ pub struct Parameters { /// two differ by ½h(1 − α)n_t, O(1) of the step on cut faces). The /// device refuses it. `RTX_E3_WALL_ORDER2=centroid`. pub wall_order2_centroid: bool, + /// HOST PROTOTYPE (S2-7b): the axis exchange with a solid neighbour face + /// takes the wall velocity at the AXIS FOOT (distance δ) instead of the + /// solid face's own imposed value (at distance h): consistent for a + /// linear field on a moving or rotating wall (identical on a wall at + /// rest). `RTX_E3_WALL_EXCHANGE=axisfoot`; the device refuses it. + pub wall_exchange_foot: bool, + /// HOST PROTOTYPE (S2-7b): the convective mass fluxes through a cut + /// face's control-volume sides from the sides' own apertures (the half + /// faces across, the cell-centre planes along) instead of the averages + /// of whole-face fluxes. `RTX_E3_CONV_SIDES=exact`; the device refuses it. + pub conv_sides_exact: bool, /// S2-5: the cross-direction diffusion between two faces over the /// distance between their OPEN-PART CENTROIDS (a cut face's velocity /// is its open part's mean, ½h(1 − α) off the face centre along the @@ -205,6 +216,8 @@ impl Default for Parameters { .is_ok_and(|v| v == "off"), // ON by default since S2-7 (`=h` reproduces the records before it). wall_exchange_axis: std::env::var("RTX_E3_WALL_EXCHANGE").map_or(true, |v| v != "h"), + wall_exchange_foot: std::env::var("RTX_E3_WALL_EXCHANGE").is_ok_and(|v| v == "axisfoot"), + conv_sides_exact: std::env::var("RTX_E3_CONV_SIDES").is_ok_and(|v| v == "exact"), pressure_centroid: std::env::var("RTX_E3_PRESSURE_CENTROID").is_ok_and(|v| v == "1"), momentum_volume_tiled: std::env::var("RTX_E3_MOMENTUM_VOLUME") .is_ok_and(|v| v == "tiled"), @@ -351,6 +364,8 @@ impl Solver { m.exchange_convection_off = self.params.exchange_convection_off; m.cv_sides_exact = self.params.cv_sides_exact; m.wall_order2_centroid = self.params.wall_order2_centroid; + m.wall_exchange_foot = self.params.wall_exchange_foot; + m.conv_sides_exact = self.params.conv_sides_exact; m.diffusion_centroid = self.params.diffusion_centroid; if self.params.diffusion_centroid { m.compute_face_shifts(); diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/wall.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/wall.rs index e47407f..3bebfb3 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/wall.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded3/wall.rs @@ -114,6 +114,10 @@ pub struct Mask { /// S2-7: the quadratic wall gradient's second point at the neighbour's /// own centroid distance (host prototype). pub(super) wall_order2_centroid: bool, + /// S2-7b host prototypes: the axis foot's wall velocity in the solid + /// exchange; the convective sides from the sides' own apertures. + pub(super) wall_exchange_foot: bool, + pub(super) conv_sides_exact: bool, /// The centroid prototype's pressure-gradient weights per u / v / w face. /// The centroid-distance cross diffusion (S2-5). pub(super) diffusion_centroid: bool, @@ -542,6 +546,8 @@ impl Mask { wall_exchange_axis: false, cv_sides_exact: false, wall_order2_centroid: false, + wall_exchange_foot: false, + conv_sides_exact: false, grad_weights: None, diffusion_centroid: false, face_shifts: None, diff --git a/crates/specialized/rtx-cfd/tests/embedded3_wall_position_curved.rs b/crates/specialized/rtx-cfd/tests/embedded3_wall_position_curved.rs new file mode 100644 index 0000000..a3ebbaa --- /dev/null +++ b/crates/specialized/rtx-cfd/tests/embedded3_wall_position_curved.rs @@ -0,0 +1,328 @@ +//! S2-7b instrument: the cut wall on a CURVED wall with an exact velocity +//! and an exact pressure that has a wall-normal gradient. Taylor–Couette +//! flow between two embedded concentric cylinders (inner `R1` rotating at +//! `OMEGA`, outer `R2` at rest), z periodic; the domain sides lie in the +//! solid. Exact: `u_θ = A r + B/r`, `p = ρ (A² r²/2 + 2AB ln r − B²/(2r²))`. +//! Reads: both walls' effective radii from the profile fit on full faces +//! (the S2-7 form), and the pressure error by cell class against the +//! exact p(r) (the S2-7b form). `RTX_E3_CURVED_MODE=rigid` is the +//! linear-exactness mode: solid-body rotation of both cylinders +//! (`u = Ω r e_θ`, `p = ρ Ω² r²/2`). +use rtx_cfd::solvers::incompressible::ConvectionScheme; +use rtx_cfd::solvers::incompressible::embedded3::{ + Body, Boundaries, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme, +}; + +const MU: f64 = 0.1; +const RHO: f64 = 1.0; +const LX: f64 = 2.0; +const R1: f64 = 0.3; +const R2: f64 = 0.8; +const OMEGA: f64 = 1.0; +const CENTRE: (f64, f64) = (1.013, 1.017); + +struct Exact { + a: f64, + b: f64, +} + +impl Exact { + fn new(rigid: bool) -> Self { + let d = R2 * R2 - R1 * R1; + if rigid { + Exact { a: OMEGA, b: 0.0 } + } else if outer_drives() { + // The OUTER cylinder rotates at Ω, the inner is at rest: the + // static wall is the convex one (the DFG's kind). + Exact { + a: OMEGA * R2 * R2 / d, + b: -OMEGA * R1 * R1 * R2 * R2 / d, + } + } else { + Exact { + a: -OMEGA * R1 * R1 / d, + b: OMEGA * R1 * R1 * R2 * R2 / d, + } + } + } + fn u_theta(&self, r: f64) -> f64 { + self.a * r + self.b / r + } + fn p(&self, r: f64) -> f64 { + RHO * (0.5 * self.a * self.a * r * r + 2.0 * self.a * self.b * r.ln() + - 0.5 * self.b * self.b / (r * r)) + } + /// The velocity at (x, y): the exact profile in the gap, the walls' own + /// motion outside it (the inner body rotates, the outer is at rest). + fn velocity(&self, x: f64, y: f64, rigid: bool) -> (f64, f64) { + let (dx, dy) = (x - CENTRE.0, y - CENTRE.1); + let r = (dx * dx + dy * dy).sqrt().max(1e-12); + let ut = if r < R1 { + if rigid || !outer_drives() { OMEGA * r } else { 0.0 } + } else if r > R2 { + if rigid || outer_drives() { OMEGA * r } else { 0.0 } + } else { + self.u_theta(r) + }; + (-ut * dy / r, ut * dx / r) + } +} + +/// `RTX_E3_CURVED_MODE=outer`: the outer cylinder drives, the inner is at rest. +fn outer_drives() -> bool { + std::env::var("RTX_E3_CURVED_MODE").is_ok_and(|v| v == "outer") +} + +fn parameters() -> Parameters { + Parameters { + corrector_steps: 2, + tolerance: 1e-10, + convection_scheme: ConvectionScheme::TvdVanAlbada, + wall_scheme: WallScheme::CutCell, + boundaries: Boundaries { + z0: Side::Periodic, + z1: Side::Periodic, + ..Boundaries::default() + }, + ..Parameters::default() + } +} + +/// Least squares of `u_θ = a r + b / r` through `(r, u_θ)` points. +fn fit_ab(points: &[(f64, f64)]) -> (f64, f64) { + let (mut s11, mut s12, mut s22, mut t1, mut t2) = (0.0, 0.0, 0.0, 0.0, 0.0); + for &(r, u) in points { + let (f1, f2) = (r, 1.0 / r); + s11 += f1 * f1; + s12 += f1 * f2; + s22 += f2 * f2; + t1 += f1 * u; + t2 += f2 * u; + } + let det = s11 * s22 - s12 * s12; + ((t1 * s22 - t2 * s12) / det, (s11 * t2 - s12 * t1) / det) +} + +fn reading(n: usize, rigid: bool) { + let h = 1.0 / n as f64; + let (nx, ny, nz) = ((LX * n as f64) as usize, (LX * n as f64) as usize, 2); + let ex = Exact::new(rigid); + let mut solver = Solver::new( + Fluid { + density: RHO, + viscosity: MU, + reference_velocity: OMEGA * R1, + reference_length: R2 - R1, + }, + parameters(), + ); + let exb = Exact::new(rigid); + solver.set_boundary_velocity(move |x, y, _z, _t| { + let (u, v) = exb.velocity(x, y, rigid); + (u, v, 0.0) + }); + // The fluid is the gap: φ > 0 there. + solver.set_body( + Body::from_sdf(move |x, y, _z, _t| { + let r = ((x - CENTRE.0).powi(2) + (y - CENTRE.1).powi(2)).sqrt(); + (r - R1).min(R2 - r) + }) + .with_surface_velocity(move |x, y, _z, _t| { + let (dx, dy) = (x - CENTRE.0, y - CENTRE.1); + let r = (dx * dx + dy * dy).sqrt().max(1e-12); + let inner_side = r < 0.5 * (R1 + R2); + let moving = rigid || (inner_side != outer_drives()); + let ut = if moving { OMEGA * r } else { 0.0 }; + (-ut * dy / r, ut * dx / r, 0.0) + }), + ); + let g = Grid::cubic(nx, ny, nz, h); + let mut field = Field::new(g); + for k in 0..nz { + for j in 0..ny { + for i in 0..=nx { + field.u[g.uface(k, j, i)] = ex.velocity(i as f64 * h, (j as f64 + 0.5) * h, rigid).0; + } + } + for j in 0..=ny { + for i in 0..nx { + field.v[g.vface(k, j, i)] = ex.velocity((i as f64 + 0.5) * h, j as f64 * h, rigid).1; + } + } + } + solver.initialize(&mut field); + let dt = 0.5 * h * h / (6.0 * MU); + let t_end: f64 = std::env::var("RTX_E3_CURVED_T") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(4.0); + let steps = (t_end / dt).ceil() as usize; + let mut last_res = 0.0; + for _ in 0..steps { + last_res = solver.advance(&mut field, dt).final_residual; + } + let mask = solver.mask().expect("mask"); + let r_of = |x: f64, y: f64| ((x - CENTRE.0).powi(2) + (y - CENTRE.1).powi(2)).sqrt(); + // The profile on full faces two to N cells off both walls. + let mut pts = Vec::new(); + let inner = |r: f64| r > R1 + 2.0 * h && r < R2 - 2.0 * h; + for j in 0..ny { + for i in 0..=nx { + let f = g.uface(0, j, i); + let (x, y) = (i as f64 * h, (j as f64 + 0.5) * h); + let r = r_of(x, y); + if inner(r) && mask.a_u(f) >= 1.0 { + // u = −u_θ dy/r + let dy = y - CENTRE.1; + if dy.abs() > 0.3 * r { + pts.push((r, -field.u[f] * r / dy)); + } + } + } + } + for j in 0..=ny { + for i in 0..nx { + let f = g.vface(0, j, i); + let (x, y) = ((i as f64 + 0.5) * h, j as f64 * h); + let r = r_of(x, y); + if inner(r) && mask.a_v(f) >= 1.0 { + let dx = x - CENTRE.0; + if dx.abs() > 0.3 * r { + pts.push((r, field.v[f] * r / dx)); + } + } + } + } + let (a, b) = fit_ab(&pts); + // The walls: outer where u_θ = 0 (Couette) or the fit's own (rigid: the + // inner/outer are not separable, report A and B); inner where u_θ = Ω r. + let (off_in, off_out) = if rigid { + (f64::NAN, f64::NAN) + } else if outer_drives() { + // inner: u_θ = 0 → r² = −b/a; outer: u_θ = Ω r → r² = b/(Ω − a) + let r_in = (-b / a).sqrt(); + let r_out = (b / (OMEGA - a)).sqrt(); + ((r_in - R1) / h, (R2 - r_out) / h) + } else { + let r_out = (-b / a).sqrt(); + let r_in = (b / (OMEGA - a)).sqrt(); + ((r_in - R1) / h, (R2 - r_out) / h) + }; + // The pressure against the exact p(r): mean-free over full cells in the gap. + let scale = RHO * (OMEGA * R1).powi(2); + let is_cut = |c: usize| mask.vol(c) < 1.0 - 1e-9; + let (mut sum, mut cnt) = (0.0, 0usize); + let cell_r = |c: usize| { + let (_, j, i) = g.kji(c); + r_of((i as f64 + 0.5) * h, (j as f64 + 0.5) * h) + }; + for j in 0..ny { + for i in 0..nx { + let c = g.cell(0, j, i); + if mask.cell_active(c) && !is_cut(c) && mask.master(c).is_none() { + sum += field.p[c] - ex.p(cell_r(c)); + cnt += 1; + } + } + } + let level = sum / cnt.max(1) as f64; + let (mut sq_full, mut sq_cut, mut n_cut, mut sum_cut) = (0.0, 0.0, 0usize, 0.0); + let (mut sq_small, mut n_small, mut sq_large, mut n_large) = (0.0, 0usize, 0.0, 0usize); + let (mut sq_in, mut n_in, mut sq_out, mut n_out) = (0.0, 0usize, 0.0, 0usize); + for j in 0..ny { + for i in 0..nx { + let c = g.cell(0, j, i); + if !mask.cell_active(c) || mask.master(c).is_some() { + continue; + } + let e = (field.p[c] - level - ex.p(cell_r(c))) / scale; + if is_cut(c) { + sq_cut += e * e; + sum_cut += e; + n_cut += 1; + if mask.vol(c) < 0.5 { + sq_small += e * e; + n_small += 1; + } else { + sq_large += e * e; + n_large += 1; + } + if cell_r(c) < 0.5 * (R1 + R2) { + sq_in += e * e; + n_in += 1; + } else { + sq_out += e * e; + n_out += 1; + } + } else { + sq_full += e * e; + } + } + } + let rms = |sq: f64, n: usize| (sq / n.max(1) as f64).sqrt(); + // S2-7b diagnostics: the wall's mass flux per cut cell (the body's + // velocity is tangential: any flux is the facet normal's), in units of + // Ω R1 h², and the ghost faces' error against the exact field (u faces + // of kind Ghost), in units of Ω R1. + let body = solver.body().expect("body"); + let (wf, _) = mask.wall_flux_table(body, solver.time()); + let (mut wsum, mut wmax, mut wn) = (0.0f64, 0.0f64, 0usize); + for j in 0..ny { + for i in 0..nx { + let c = g.cell(0, j, i); + if mask.cell_active(c) && is_cut(c) { + let q = wf[c].abs() / (OMEGA * R1 * h * h); + wsum += q; + wmax = wmax.max(q); + wn += 1; + } + } + } + let (mut gsq, mut gn, mut gmax) = (0.0f64, 0usize, 0.0f64); + for j in 0..ny { + for i in 0..=nx { + let f = g.uface(0, j, i); + if mask.u_kind(f) == rtx_cfd::solvers::incompressible::embedded3::FaceKind::Ghost { + let e = (field.u[f] - ex.velocity(i as f64 * h, (j as f64 + 0.5) * h, rigid).0) / (OMEGA * R1); + gsq += e * e; + gn += 1; + gmax = gmax.max(e.abs()); + } + } + } + println!( + " wall flux per cut cell (of ΩR1 h²): mean {:.3e} max {:.3e} ({wn}); ghost u faces vs exact (of ΩR1): rms {:.3e} max {:.3e} ({gn})", + wsum / wn.max(1) as f64, + wmax, + (gsq / gn.max(1) as f64).sqrt(), + gmax + ); + println!( + " {} n {n}: walls' offsets {off_in:+.4} h (inner) {off_out:+.4} h (outer), positive = inside the fluid; fit A {a:.5} B {b:.5} (exact {:.5} {:.5}, {} points); pressure error of ρ(ΩR1)²: full cells {:.3e} ({cnt}), cut cells {:.3e} mean {:+.3e} ({n_cut}), fraction < 0.5 {:.3e} ({n_small}), ≥ 0.5 {:.3e} ({n_large}), inner wall {:.3e} ({n_in}), outer wall {:.3e} ({n_out}); merged {}; residual {last_res:.1e}", + if rigid { "rigid" } else if outer_drives() { "outer-driven" } else { "couette" }, + ex.a, + ex.b, + pts.len(), + rms(sq_full, cnt), + rms(sq_cut, n_cut), + sum_cut / n_cut.max(1) as f64, + rms(sq_small, n_small), + rms(sq_large, n_large), + rms(sq_in, n_in), + rms(sq_out, n_out), + mask.merged_cells() + ); +} + +#[test] +#[ignore = "S2-7b instrument: Taylor–Couette between embedded cylinders (minutes per rung on the host)"] +fn curved_wall_effective_position_and_pressure() { + let ns: Vec = std::env::var("RTX_E3_CURVED_NS") + .ok() + .map(|v| v.split(',').filter_map(|t| t.trim().parse().ok()).collect()) + .unwrap_or_else(|| vec![16, 32]); + let rigid = std::env::var("RTX_E3_CURVED_MODE").is_ok_and(|v| v == "rigid"); + for n in ns { + reading(n, rigid); + } +}