Merge r8c-3d-interface (R8/R7 phase 1; default-off, verified)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -26,8 +26,8 @@ use embedded3_flag_kinematics::{Recorded, recorded};
|
||||
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
||||
use rtx_cfd::solvers::incompressible::embedded3::step::device::DeviceStep;
|
||||
use rtx_cfd::solvers::incompressible::embedded3::{
|
||||
Body, Boundaries, DeviceSdf, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme,
|
||||
write_vtk,
|
||||
Body, Boundaries, DeviceSdf, FAR_OUTSIDE, Field, Fluid, Grid, HexPlate, Parameters,
|
||||
PlateSurface, Side, Solver, WallScheme, write_vtk,
|
||||
};
|
||||
use std::io::Write as _;
|
||||
|
||||
@@ -287,6 +287,99 @@ fn cylinder_3d(d2: f64, z: f64, r: f64) -> f64 {
|
||||
outside + q1.max(q2).min(0.0) - r
|
||||
}
|
||||
|
||||
/// R8-c: the flag as a deformed plate (`RTX_E3_FLAG_BODY=plate`): the
|
||||
/// span stations (`RTX_E3_FLAG_STATIONS`, default 21, spread over the
|
||||
/// flag's span) each carry the centreline polyline. `RTX_E3_FLAG_TWIST=κ`
|
||||
/// (default 0; analytic mode only) scales each station's deflection and
|
||||
/// velocity by `1 + κ ζ`, `ζ = (z − z_c)/(span/2)` — the first bending mode
|
||||
/// times a span-linear twist. At κ = 0 every station is the polyline as it
|
||||
/// is (the G1 identity with the polyline capsule).
|
||||
fn plate_body() -> bool {
|
||||
std::env::var("RTX_E3_FLAG_BODY").is_ok_and(|v| v == "plate")
|
||||
}
|
||||
|
||||
fn twist() -> f64 {
|
||||
env_f("RTX_E3_FLAG_TWIST", 0.0)
|
||||
}
|
||||
|
||||
/// The span stations' z (ascending) and their ζ.
|
||||
fn stations() -> Vec<f64> {
|
||||
let n = (env_f("RTX_E3_FLAG_STATIONS", 21.0) as usize).max(2);
|
||||
let (zc, span) = (0.5 * duct_depth(), flag_span());
|
||||
(0..n)
|
||||
.map(|k| zc - 0.5 * span + span * k as f64 / (n - 1) as f64)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The span factor `1 + κ ζ` of the deflection at `z` (clamped to the span).
|
||||
fn span_factor(z: f64) -> f64 {
|
||||
let (zc, span) = (0.5 * duct_depth(), flag_span());
|
||||
let zeta = ((z - zc) / (0.5 * span)).clamp(-1.0, 1.0);
|
||||
1.0 + twist() * zeta
|
||||
}
|
||||
|
||||
/// The plate at `t`: per station the centreline (and its velocity).
|
||||
fn plate_at(t: f64) -> PlateSurface {
|
||||
let z = stations();
|
||||
let (row, vel): (Vec<[f64; 2]>, Vec<[f64; 2]>) = match recorded() {
|
||||
Some(rec) => recorded_polyline(rec, t)
|
||||
.iter()
|
||||
.map(|p| ([p.0, p.1], [p.2, p.3]))
|
||||
.unzip(),
|
||||
None => analytic_polyline(t)
|
||||
.iter()
|
||||
.map(|p| ([p.0, p.1], [0.0, p.2]))
|
||||
.unzip(),
|
||||
};
|
||||
if twist() == 0.0 {
|
||||
return PlateSurface::uniform(z, &row, &vel);
|
||||
}
|
||||
assert!(
|
||||
recorded().is_none(),
|
||||
"RTX_E3_FLAG_TWIST: the analytic mode only"
|
||||
);
|
||||
let ns = N + 1;
|
||||
let (mut xy, mut vv) = (Vec::new(), Vec::new());
|
||||
for &zk in &z {
|
||||
let f = span_factor(zk);
|
||||
let mut pts: Vec<(f64, f64, f64, f64)> = (0..ns)
|
||||
.map(|m| {
|
||||
let s = m as f64 / N as f64;
|
||||
let (d, v) = deflection(s, t);
|
||||
(FLAG_X0 + s * FLAG_LEN, body_cy() + d * f, 0.0, v * f)
|
||||
})
|
||||
.collect();
|
||||
inset_last(&mut pts, tip_inset());
|
||||
xy.extend(pts.iter().map(|p| [p.0, p.1]));
|
||||
vv.extend(pts.iter().map(|p| [p.2, p.3]));
|
||||
}
|
||||
PlateSurface { z, ns, xy, vel: vv }
|
||||
}
|
||||
|
||||
/// The deformed flag's mid-surface point `y = w(x, z)` and its 3D unit
|
||||
/// normal at arc fraction `s` and span `z` (the analytic kinematics with the
|
||||
/// span factor; the structure's placement for the load transfer: the
|
||||
/// thickness along the mid-surface's normal, as a solid plate carries it).
|
||||
fn mid_point(s: f64, z: f64, t: f64) -> ([f64; 3], [f64; 3]) {
|
||||
let f = span_factor(z);
|
||||
let (d, _) = deflection(s, t);
|
||||
let ds = 1e-6;
|
||||
let (s0, s1) = ((s - ds).max(0.0), (s + ds).min(1.0));
|
||||
let wx = (deflection(s1, t).0 - deflection(s0, t).0) / (s1 - s0) / FLAG_LEN * f;
|
||||
// The span factor's rate: κ / (span/2) inside the span.
|
||||
let (zc, span) = (0.5 * duct_depth(), flag_span());
|
||||
let wz = if ((z - zc) / (0.5 * span)).abs() < 1.0 {
|
||||
d * twist() / (0.5 * span)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let r = (1.0 + wx * wx + wz * wz).sqrt();
|
||||
(
|
||||
[FLAG_X0 + s * FLAG_LEN, body_cy() + d * f, z],
|
||||
[-wx / r, 1.0 / r, -wz / r],
|
||||
)
|
||||
}
|
||||
|
||||
fn inflow(y: f64, z: f64) -> f64 {
|
||||
let (hd, d) = (duct_height(), duct_depth());
|
||||
16.0 * U_M * y * z * (hd - y) * (d - z) / (hd * hd * d * d)
|
||||
@@ -385,9 +478,68 @@ fn flag_wake_on_the_device() {
|
||||
let cy = body_cy();
|
||||
let cyl = move |x: f64, y: f64| ((x - CX).powi(2) + (y - cy).powi(2)).sqrt() - R_CYL;
|
||||
let r_fillet = root_fillet();
|
||||
let body = Body::from_sdf(move |x, y, z, t| {
|
||||
fillet_union(cylinder_3d(cyl(x, y), z, r_edge), flag_3d(x, y, z, t, r_edge).0, r_fillet)
|
||||
})
|
||||
// The device form of φ: the polyline capsule (R6-1), or the plate (R8-c).
|
||||
let device_sdf = move |t: f64| {
|
||||
let plate = plate_body().then(|| plate_at(t));
|
||||
DeviceSdf {
|
||||
cyl: [CX, cy, R_CYL],
|
||||
cyl_cut: !(flag_span() >= duct_depth()
|
||||
|| !std::env::var("RTX_E3_FLAG_CYL_SPAN").is_ok_and(|v| v == "flag")),
|
||||
flag_cut: flag_span() < duct_depth(),
|
||||
zc: 0.5 * duct_depth(),
|
||||
span: flag_span(),
|
||||
r_edge,
|
||||
half: FLAG_HALF,
|
||||
fillet: r_fillet,
|
||||
poly: match (&plate, recorded()) {
|
||||
(Some(_), _) => Vec::new(),
|
||||
(None, Some(rec)) => recorded_polyline(rec, t)
|
||||
.iter()
|
||||
.map(|p| [p.0, p.1])
|
||||
.collect(),
|
||||
(None, None) => analytic_polyline(t).iter().map(|p| [p.0, p.1]).collect(),
|
||||
},
|
||||
// R6-2 step 2: the centreline's velocity per point (the analytic mode is transverse).
|
||||
vel: match (&plate, recorded()) {
|
||||
(Some(_), _) => Vec::new(),
|
||||
(None, Some(rec)) => recorded_polyline(rec, t)
|
||||
.iter()
|
||||
.map(|p| [p.2, p.3])
|
||||
.collect(),
|
||||
(None, None) => analytic_polyline(t).iter().map(|p| [0.0, p.2]).collect(),
|
||||
},
|
||||
plate,
|
||||
}
|
||||
};
|
||||
// The device form at `t`, once per thread and time (the host closures
|
||||
// of the plate body evaluate it ~10⁶ times per step).
|
||||
let sdf_at = move |t: f64| -> std::sync::Arc<DeviceSdf> {
|
||||
thread_local! {
|
||||
static SDF: std::cell::RefCell<(u64, Option<std::sync::Arc<DeviceSdf>>)> =
|
||||
const { std::cell::RefCell::new((u64::MAX, None)) };
|
||||
}
|
||||
SDF.with(|cell| {
|
||||
let mut c = cell.borrow_mut();
|
||||
if c.0 != t.to_bits() || c.1.is_none() {
|
||||
c.1 = Some(std::sync::Arc::new(device_sdf(t)));
|
||||
c.0 = t.to_bits();
|
||||
}
|
||||
c.1.clone().expect("sdf")
|
||||
})
|
||||
};
|
||||
let body = if plate_body() {
|
||||
// R8-c: the host φ and surface velocity ARE the device form's (the
|
||||
// kernel's arithmetic on the host).
|
||||
Body::from_sdf(move |x, y, z, t| sdf_at(t).phi_host(x, y, z))
|
||||
.with_surface_velocity(move |x, y, z, t| sdf_at(t).velocity_host(x, y, z))
|
||||
} else {
|
||||
Body::from_sdf(move |x, y, z, t| {
|
||||
fillet_union(
|
||||
cylinder_3d(cyl(x, y), z, r_edge),
|
||||
flag_3d(x, y, z, t, r_edge).0,
|
||||
r_fillet,
|
||||
)
|
||||
})
|
||||
.with_surface_velocity(move |x, y, z, t| {
|
||||
let (df, (vx, vy)) = flag_3d(x, y, z, t, r_edge);
|
||||
if df <= cylinder_3d(cyl(x, y), z, r_edge) {
|
||||
@@ -395,35 +547,15 @@ fn flag_wake_on_the_device() {
|
||||
} else {
|
||||
(0.0, 0.0, 0.0)
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
assert!(
|
||||
twist() == 0.0 || plate_body(),
|
||||
"RTX_E3_FLAG_TWIST needs RTX_E3_FLAG_BODY=plate"
|
||||
);
|
||||
// R6-1: the same φ in the device's form (the device geometry, default ON): the
|
||||
// circle, the capsule around the step's centreline, the span cuts.
|
||||
let body = body.with_device_sdf(move |t| DeviceSdf {
|
||||
cyl: [CX, cy, R_CYL],
|
||||
cyl_cut: !(flag_span() >= duct_depth()
|
||||
|| !std::env::var("RTX_E3_FLAG_CYL_SPAN").is_ok_and(|v| v == "flag")),
|
||||
flag_cut: flag_span() < duct_depth(),
|
||||
zc: 0.5 * duct_depth(),
|
||||
span: flag_span(),
|
||||
r_edge,
|
||||
half: FLAG_HALF,
|
||||
fillet: r_fillet,
|
||||
poly: match recorded() {
|
||||
Some(rec) => recorded_polyline(rec, t)
|
||||
.iter()
|
||||
.map(|p| [p.0, p.1])
|
||||
.collect(),
|
||||
None => analytic_polyline(t).iter().map(|p| [p.0, p.1]).collect(),
|
||||
},
|
||||
// R6-2 step 2: the centreline's velocity per point (the analytic mode is transverse).
|
||||
vel: match recorded() {
|
||||
Some(rec) => recorded_polyline(rec, t)
|
||||
.iter()
|
||||
.map(|p| [p.2, p.3])
|
||||
.collect(),
|
||||
None => analytic_polyline(t).iter().map(|p| [0.0, p.2]).collect(),
|
||||
},
|
||||
});
|
||||
// circle, the capsule around the step's centreline (or the plate), the span cuts.
|
||||
let body = body.with_device_sdf(device_sdf);
|
||||
solver.set_moving_body(body);
|
||||
let g = Grid::cubic(nx, ny_grid, nz, h);
|
||||
let mut field = Field::new(g);
|
||||
@@ -484,6 +616,32 @@ fn flag_wake_on_the_device() {
|
||||
(mid - 2, mid + 2)
|
||||
};
|
||||
let width = nz as f64 * h;
|
||||
// R8-c: the load transfer onto the structure's Hex20 plate (35 × 2 × n
|
||||
// with n = `RTX_E3_FLAG_TRANSFER`; off when unset) every
|
||||
// `RTX_E3_FLAG_TRANSFER_EVERY`-th sample (default 1), the budget to
|
||||
// `RTX_E3_FLAG_TRANSFER_CSV`, the last transfer's nodal forces to
|
||||
// `RTX_E3_FLAG_TRANSFER_NODAL`. The plate is placed on the prescribed
|
||||
// kinematics (analytic mode): the centreline with the span factor, the
|
||||
// thickness along its in-plane normal, the flag's span.
|
||||
let transfer_nz = env_f("RTX_E3_FLAG_TRANSFER", 0.0) as usize;
|
||||
let transfer_every = (env_f("RTX_E3_FLAG_TRANSFER_EVERY", 1.0) as usize).max(1);
|
||||
let hex = (transfer_nz > 0).then(|| {
|
||||
assert!(
|
||||
recorded().is_none(),
|
||||
"RTX_E3_FLAG_TRANSFER: the analytic mode only"
|
||||
);
|
||||
HexPlate::new(35, 2, transfer_nz)
|
||||
});
|
||||
let mut transfer_csv = std::env::var("RTX_E3_FLAG_TRANSFER_CSV").ok().map(|p| {
|
||||
let mut f = std::fs::File::create(p).expect("transfer csv");
|
||||
writeln!(
|
||||
f,
|
||||
"t,loads,flag_loads,route_x,route_y,route_z,sum_dx,sum_dy,sum_dz,fin_x,fin_y,fin_z,dfx,dfy,dfz,min_x,min_y,min_z,dmx,dmy,dmz,rel_force,rel_moment,max_newton,max_outside,extrapolated,extrap_share,interior_share,lever_x,lever_y,lever_z,ms"
|
||||
)
|
||||
.unwrap();
|
||||
f
|
||||
});
|
||||
let mut samples_seen = 0usize;
|
||||
let start = std::time::Instant::now();
|
||||
let mut drag_rec_sum = 0.0;
|
||||
// The routes' PARTS over the whole body (x, per unit width): operator
|
||||
@@ -520,6 +678,131 @@ fn flag_wake_on_the_device() {
|
||||
let ft = mask
|
||||
.cut_wall_force(body, &field, RHO * NU, t)
|
||||
.expect("wall");
|
||||
if sample {
|
||||
samples_seen += 1;
|
||||
}
|
||||
if let (Some(hex), true) = (hex.as_ref(), sample && samples_seen % transfer_every == 0)
|
||||
{
|
||||
let lap = std::time::Instant::now();
|
||||
let (loads, route) = mask
|
||||
.cut_wall_loads(body, &field, RHO * NU, t)
|
||||
.expect("loads");
|
||||
// The route's total is cut_wall_force's, to the bit (same loops).
|
||||
assert_eq!(route.map(f64::to_bits), ft.map(f64::to_bits), "route total");
|
||||
let mut sum = [0.0f64; 3];
|
||||
for l in &loads {
|
||||
for c in 0..3 {
|
||||
sum[c] += l.f[c];
|
||||
}
|
||||
}
|
||||
let sdf = body.device_sdf(t).expect("device form");
|
||||
let flag: Vec<_> = loads
|
||||
.iter()
|
||||
.filter(|l| sdf.is_flag_host(l.foot[0], l.foot[1], l.foot[2]))
|
||||
.collect();
|
||||
let (zc, span) = (0.5 * duct_depth(), flag_span());
|
||||
let pos = hex.place(|s, eta, zeta| {
|
||||
let z = zc - 0.5 * span + span * zeta;
|
||||
let (c, n) = mid_point(s, z, t);
|
||||
[
|
||||
c[0] + FLAG_HALF * eta * n[0],
|
||||
c[1] + FLAG_HALF * eta * n[1],
|
||||
c[2] + FLAG_HALF * eta * n[2],
|
||||
]
|
||||
});
|
||||
let pairs: Vec<([f64; 3], [f64; 3])> = flag.iter().map(|l| (l.foot, l.f)).collect();
|
||||
let origin = [FLAG_X0, body_cy(), zc];
|
||||
let tr = hex.transfer(&pos, &pairs, origin);
|
||||
// The lever the foot adds over the operator point: Σ (foot − x) × F.
|
||||
let mut lever = [0.0f64; 3];
|
||||
for l in &flag {
|
||||
let d = [l.foot[0] - l.x[0], l.foot[1] - l.x[1], l.foot[2] - l.x[2]];
|
||||
let m = [
|
||||
d[1] * l.f[2] - d[2] * l.f[1],
|
||||
d[2] * l.f[0] - d[0] * l.f[2],
|
||||
d[0] * l.f[1] - d[1] * l.f[0],
|
||||
];
|
||||
for c in 0..3 {
|
||||
lever[c] += m[c];
|
||||
}
|
||||
}
|
||||
let nrm = |a: [f64; 3]| (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt();
|
||||
let df = [0, 1, 2].map(|c| tr.force_out[c] - tr.force_in[c]);
|
||||
let dm = [0, 1, 2].map(|c| tr.moment_out[c] - tr.moment_in[c]);
|
||||
let abs_f: f64 = flag.iter().map(|l| nrm(l.f)).sum();
|
||||
let rel_f = nrm(df) / abs_f.max(1e-300);
|
||||
let rel_m = nrm(dm) / (abs_f * FLAG_LEN).max(1e-300);
|
||||
let extrap_share = tr.extrapolated_load / abs_f.max(1e-300);
|
||||
let ms = lap.elapsed().as_secs_f64() * 1e3;
|
||||
println!(
|
||||
" transfer t {t:.4}: {} loads ({} flag); route − Σ {:.1e} N; flag F {:+.4} {:+.4} {:+.4} N, M {:+.5} {:+.5} {:+.5} N m; |ΔF|/Σ|F| {rel_f:.1e}, |ΔM|/(Σ|F| L) {rel_m:.1e}; Newton ≤ {:.1e} m, outside ≤ {:.2e} at ({:.4}, {:.4}, {:.4}) ({} beyond {FAR_OUTSIDE}, {:.1e} of Σ|F|), interior share {:.3}; {ms:.0} ms",
|
||||
loads.len(),
|
||||
flag.len(),
|
||||
nrm([route[0] - sum[0], route[1] - sum[1], route[2] - sum[2]]),
|
||||
tr.force_in[0],
|
||||
tr.force_in[1],
|
||||
tr.force_in[2],
|
||||
tr.moment_in[0],
|
||||
tr.moment_in[1],
|
||||
tr.moment_in[2],
|
||||
tr.max_residual,
|
||||
tr.max_outside,
|
||||
tr.worst_point[0],
|
||||
tr.worst_point[1],
|
||||
tr.worst_point[2],
|
||||
tr.extrapolated,
|
||||
extrap_share,
|
||||
tr.interior_share
|
||||
);
|
||||
if let Some(f) = transfer_csv.as_mut() {
|
||||
writeln!(
|
||||
f,
|
||||
"{t:.6},{},{},{:.9e},{:.9e},{:.9e},{:.3e},{:.3e},{:.3e},{:.9e},{:.9e},{:.9e},{:.3e},{:.3e},{:.3e},{:.9e},{:.9e},{:.9e},{:.3e},{:.3e},{:.3e},{rel_f:.3e},{rel_m:.3e},{:.3e},{:.3e},{},{extrap_share:.3e},{:.4e},{:.4e},{:.4e},{:.4e},{ms:.1}",
|
||||
loads.len(),
|
||||
flag.len(),
|
||||
route[0],
|
||||
route[1],
|
||||
route[2],
|
||||
route[0] - sum[0],
|
||||
route[1] - sum[1],
|
||||
route[2] - sum[2],
|
||||
tr.force_in[0],
|
||||
tr.force_in[1],
|
||||
tr.force_in[2],
|
||||
df[0],
|
||||
df[1],
|
||||
df[2],
|
||||
tr.moment_in[0],
|
||||
tr.moment_in[1],
|
||||
tr.moment_in[2],
|
||||
dm[0],
|
||||
dm[1],
|
||||
dm[2],
|
||||
tr.max_residual,
|
||||
tr.max_outside,
|
||||
tr.extrapolated,
|
||||
tr.interior_share,
|
||||
lever[0],
|
||||
lever[1],
|
||||
lever[2]
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if let Ok(path) = std::env::var("RTX_E3_FLAG_TRANSFER_NODAL") {
|
||||
let mut f = std::fs::File::create(path).expect("nodal csv");
|
||||
writeln!(f, "node,i,j,k,x,y,z,fx,fy,fz").unwrap();
|
||||
for (n, fv) in tr.nodal.iter().enumerate() {
|
||||
let [i, j, k] = hex.lattice_of(n);
|
||||
let x = pos[n];
|
||||
writeln!(
|
||||
f,
|
||||
"{n},{i},{j},{k},{:.9e},{:.9e},{:.9e},{:.9e},{:.9e},{:.9e}",
|
||||
x[0], x[1], x[2], fv[0], fv[1], fv[2]
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
// The tip's transverse deflection: the record's last station in
|
||||
// recorded mode (until 2026-09-21 this column held the analytic
|
||||
// first mode even then — R2's fits use the record directly).
|
||||
|
||||
Reference in New Issue
Block a user