Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
Falsifier 4 of the Turek–Hron geometry decision fired (the SOR projection
cost 0.09 s/step at 250x41 and an hour per run at 5 mm); this answers it.
solvers::incompressible::poisson: PoissonProblem (cell-centred five-point
SPD operator as per-cell face coefficients + Dirichlet diagonal extra +
active mask) and solve_multigrid_pcg — conjugate gradient preconditioned
by one V-cycle of geometric multigrid: aggregation by 2 per direction (odd
sizes absorbed, coarse cell active iff any child is), the Galerkin coarse
operator for piecewise-constant prolongation / summation restriction,
symmetric Gauss–Seidel smoothing, coarse correction scaled by 2 (Braess's
under-correction of unsmoothed aggregation; scalar, so the preconditioner
stays symmetric and positive on range(A)), L1 TRUE-residual stop with a
stagnation guard. Singular systems are handled per connected component of
the active cells (mean projection and level per pure-Neumann component;
the anchor's component to p[anchor] = 0). PoissonSolverKind::{Sor,
Multigrid} on PisoParameters / EmbeddedParameters; Sor is the default and
its code is byte-for-byte untouched; an unconverged multigrid solve falls
back to the SOR sweeps for that projection.
Verified (poisson/tests.rs, tests/poisson_equivalence.rs):
- PCG iterations to cut the residual 1e-8 on the closed Neumann box at
32^2..256^2: 4, 4, 4, 4; ragged masked domains 8/8/8;
- manufactured recoveries to ~1e-14; Galerkin identity A_c v = R A P v to
7e-15 on every level (masked, outlet column, non-uniform conductances);
V-cycle symmetric to 1e-14; NaN-poisoned inactive cells untouched;
- two Neumann components with opposite imbalances, and a Dirichlet
component beside an imbalanced Neumann one (review scenarios): converge,
each component right up to its own constant;
- speed vs plain SOR at the same stop: 22.7x (128^2), 41x (256^2);
- same answers as SOR: PISO MMS 4.6e-8 relative, Taylor–Green divergence
1.4e-9 every step, embedded-circle MMS 7e-8, no-body bit-identity with MG
on both solvers, channel+outlet+circle 1.4e-10; CFD1 loads identical to
four digits at 0.003 s/step vs 0.094 (30x).
CFD1 refinement study (tests/turek_hron_cfd.rs, three grids, 257 s):
h = 10 / 6.6 / 5 mm -> control-volume drag 15.6156 / 15.2829 / 15.0988 vs
14.2929 (+9.25 / +6.93 / +5.64%), apparent order 0.71, Richardson
extrapolate 14.04; surface route and lift not monotone (flag 2/3/4 cells
thick) — the test asserts the measured band at the finest grid.
Built with a 4-agent workflow (core, integration, refinement study,
adversarial review); the review found no defects and four risks, three
fixed here (per-component projection, one symmetric smoother-sweep
parameter, acting on `converged` with an SOR fallback) and one recorded
(isotropic aggregation loses grid-independence on anisotropic cells).
rtx-cfd 301 -> 318 green.
Co-Authored-By: Claude Fable 5 <[email protected]>
640 lines
21 KiB
Rust
640 lines
21 KiB
Rust
//! Flow past circular cylinder simulation
|
||
//!
|
||
//! Classic external flow benchmark demonstrating:
|
||
//! - Flow separation and wake formation
|
||
//! - Vortex shedding (Kármán vortex street)
|
||
//! - Drag and lift coefficient calculation
|
||
//! - Transition from steady to unsteady flow
|
||
//!
|
||
//! Reynolds number regimes:
|
||
//! - Re < 5: Steady flow, no separation
|
||
//! - 5 < Re < 40: Steady separated flow with wake
|
||
//! - Re > 40: Unsteady vortex shedding
|
||
//! - Re > 200: Turbulent wake
|
||
|
||
use nalgebra::{DVector, Vector3};
|
||
use rtx_cfd::{
|
||
CfdConfig, CfdResult,
|
||
discretization::{FiniteVolumeMethod, FluxScheme, SpatialOrder},
|
||
mesh::{MeshGenerator, UnstructuredMesh},
|
||
solvers::incompressible::{
|
||
BoundaryConditions, BoundaryLocation, BoundaryType, FlowField, PisoParameters, PisoSolver,
|
||
},
|
||
turbulence::{SmagorinskyConstants, SmagorinskyModel, TurbulenceModel, TurbulenceState},
|
||
};
|
||
use std::f64::consts::PI;
|
||
use std::time::Instant;
|
||
|
||
/// Cylinder flow simulation parameters
|
||
#[derive(Debug)]
|
||
pub struct CylinderConfig {
|
||
/// Cylinder diameter
|
||
pub diameter: f64,
|
||
/// Inlet velocity
|
||
pub inlet_velocity: f64,
|
||
/// Reynolds number
|
||
pub reynolds_number: f64,
|
||
/// Domain dimensions [length, width, height]
|
||
pub domain_size: [f64; 3],
|
||
/// Cylinder center position
|
||
pub cylinder_center: [f64; 2],
|
||
/// Characteristic mesh size near cylinder
|
||
pub mesh_size_cylinder: f64,
|
||
/// Characteristic mesh size at boundaries
|
||
pub mesh_size_boundary: f64,
|
||
/// Maximum iterations per time step
|
||
pub max_iterations: usize,
|
||
/// Convergence tolerance
|
||
pub tolerance: f64,
|
||
/// Time step
|
||
pub time_step: f64,
|
||
/// Simulation time
|
||
pub total_time: f64,
|
||
/// Use LES turbulence model
|
||
pub use_les: bool,
|
||
/// Output frequency (time steps)
|
||
pub output_frequency: usize,
|
||
}
|
||
|
||
impl CylinderConfig {
|
||
/// Create configuration for Re = 100 case (steady flow)
|
||
pub fn re_100() -> Self {
|
||
Self {
|
||
diameter: 1.0,
|
||
inlet_velocity: 1.0,
|
||
reynolds_number: 100.0,
|
||
domain_size: [20.0, 10.0, 1.0], // Long domain to capture wake
|
||
cylinder_center: [5.0, 5.0], // Offset from inlet
|
||
mesh_size_cylinder: 0.05,
|
||
mesh_size_boundary: 0.5,
|
||
max_iterations: 100,
|
||
tolerance: 1e-6,
|
||
time_step: 0.01,
|
||
total_time: 50.0,
|
||
use_les: false,
|
||
output_frequency: 100,
|
||
}
|
||
}
|
||
|
||
/// Create configuration for Re = 200 case (vortex shedding)
|
||
pub fn re_200() -> Self {
|
||
Self {
|
||
diameter: 1.0,
|
||
inlet_velocity: 1.0,
|
||
reynolds_number: 200.0,
|
||
domain_size: [25.0, 12.0, 1.0],
|
||
cylinder_center: [6.0, 6.0],
|
||
mesh_size_cylinder: 0.03,
|
||
mesh_size_boundary: 0.4,
|
||
max_iterations: 150,
|
||
tolerance: 1e-6,
|
||
time_step: 0.005,
|
||
total_time: 100.0,
|
||
use_les: false,
|
||
output_frequency: 200,
|
||
}
|
||
}
|
||
|
||
/// Create configuration for high Re case with LES
|
||
pub fn high_re_les() -> Self {
|
||
Self {
|
||
diameter: 1.0,
|
||
inlet_velocity: 1.0,
|
||
reynolds_number: 3900.0,
|
||
domain_size: [30.0, 15.0, 1.0],
|
||
cylinder_center: [8.0, 7.5],
|
||
mesh_size_cylinder: 0.02,
|
||
mesh_size_boundary: 0.3,
|
||
max_iterations: 200,
|
||
tolerance: 1e-5,
|
||
time_step: 0.001,
|
||
total_time: 200.0,
|
||
use_les: true,
|
||
output_frequency: 1000,
|
||
}
|
||
}
|
||
|
||
/// Calculate viscosity from Reynolds number
|
||
pub fn viscosity(&self, density: f64) -> f64 {
|
||
density * self.inlet_velocity * self.diameter / self.reynolds_number
|
||
}
|
||
|
||
/// Calculate Strouhal number (for vortex shedding frequency)
|
||
pub fn strouhal_number(&self) -> f64 {
|
||
// Empirical correlation for circular cylinder
|
||
if self.reynolds_number < 50.0 {
|
||
0.0 // No shedding
|
||
} else if self.reynolds_number < 200.0 {
|
||
0.2 - 0.0002 * self.reynolds_number
|
||
} else {
|
||
0.2 // Approximately constant for Re > 200
|
||
}
|
||
}
|
||
|
||
/// Calculate expected shedding frequency
|
||
pub fn shedding_frequency(&self) -> f64 {
|
||
let st = self.strouhal_number();
|
||
st * self.inlet_velocity / self.diameter
|
||
}
|
||
}
|
||
|
||
/// Force coefficients
|
||
#[derive(Debug, Clone)]
|
||
pub struct ForceCoefficients {
|
||
/// Drag coefficient
|
||
pub cd: f64,
|
||
/// Lift coefficient
|
||
pub cl: f64,
|
||
/// Pressure drag coefficient
|
||
pub cd_pressure: f64,
|
||
/// Viscous drag coefficient
|
||
pub cd_viscous: f64,
|
||
/// Time stamp
|
||
pub time: f64,
|
||
}
|
||
|
||
impl ForceCoefficients {
|
||
/// New zero coefficients
|
||
pub fn zero(time: f64) -> Self {
|
||
Self {
|
||
cd: 0.0,
|
||
cl: 0.0,
|
||
cd_pressure: 0.0,
|
||
cd_viscous: 0.0,
|
||
time,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Flow past cylinder simulation
|
||
pub struct FlowPastCylinder {
|
||
/// Configuration
|
||
config: CylinderConfig,
|
||
/// CFD configuration
|
||
cfd_config: CfdConfig,
|
||
/// Mesh
|
||
mesh: UnstructuredMesh,
|
||
/// Flow field
|
||
flow_field: FlowField,
|
||
/// Solver
|
||
solver: PisoSolver,
|
||
/// Boundary conditions
|
||
boundary_conditions: BoundaryConditions,
|
||
/// Discretization
|
||
discretization: FiniteVolumeMethod,
|
||
/// LES model (optional)
|
||
les_model: Option<SmagorinskyModel>,
|
||
/// Turbulence state
|
||
turbulence_state: Option<TurbulenceState>,
|
||
/// Force history
|
||
force_history: Vec<ForceCoefficients>,
|
||
/// Cylinder surface cell indices
|
||
cylinder_cells: Vec<usize>,
|
||
}
|
||
|
||
impl FlowPastCylinder {
|
||
/// Create new cylinder flow simulation
|
||
pub fn new(config: CylinderConfig) -> CfdResult<Self> {
|
||
// Set up CFD configuration
|
||
let density = 1.0;
|
||
let viscosity = config.viscosity(density);
|
||
|
||
let cfd_config = CfdConfig::new()
|
||
.with_density(density)
|
||
.with_viscosity(viscosity)
|
||
.with_reference_velocity(config.inlet_velocity)
|
||
.with_reference_length(config.diameter);
|
||
|
||
cfd_config.validate()?;
|
||
|
||
println!("Setting up flow past cylinder simulation:");
|
||
println!(" Reynolds number: {}", config.reynolds_number);
|
||
println!(
|
||
" Domain size: {:.1}×{:.1}",
|
||
config.domain_size[0], config.domain_size[1]
|
||
);
|
||
println!(" Cylinder diameter: {}", config.diameter);
|
||
println!(" Viscosity: {:.2e}", viscosity);
|
||
|
||
if config.reynolds_number > 40.0 {
|
||
let freq = config.shedding_frequency();
|
||
println!(" Expected vortex shedding frequency: {:.3} Hz", freq);
|
||
println!(" Strouhal number: {:.3}", config.strouhal_number());
|
||
}
|
||
|
||
// Create mesh (simplified - in practice would use proper mesh generation)
|
||
let mesh = UnstructuredMesh::new();
|
||
let n_cells = Self::estimate_cell_count(&config);
|
||
|
||
// Approximate grid dimensions for flow field
|
||
let nx = (config.domain_size[0] / config.mesh_size_boundary) as usize;
|
||
let ny = (config.domain_size[1] / config.mesh_size_boundary) as usize;
|
||
let dx = config.domain_size[0] / nx as f64;
|
||
let dy = config.domain_size[1] / ny as f64;
|
||
|
||
// Initialize flow field
|
||
let mut flow_field = FlowField::new(nx, ny, dx, dy)?;
|
||
|
||
// Set initial conditions
|
||
Self::set_initial_conditions(&mut flow_field, &config, nx, ny)?;
|
||
|
||
// Set up boundary conditions
|
||
let (boundary_conditions, cylinder_cells) =
|
||
Self::setup_boundary_conditions(&config, nx, ny)?;
|
||
|
||
// Create discretization with upwind scheme for stability
|
||
let discretization = FiniteVolumeMethod::new(SpatialOrder::Second, FluxScheme::Upwind)
|
||
.with_diffusion_coefficient(viscosity);
|
||
|
||
// Create PISO solver parameters
|
||
let parameters = PisoParameters {
|
||
corrector_steps: 2,
|
||
time_step: config.time_step,
|
||
tolerance: config.tolerance,
|
||
..PisoParameters::default()
|
||
};
|
||
|
||
// Create PISO solver for unsteady flow
|
||
let solver = PisoSolver::new(cfd_config.clone(), parameters)?;
|
||
|
||
// Set up LES model if requested
|
||
let (les_model, turbulence_state) = if config.use_les {
|
||
let mut model =
|
||
SmagorinskyModel::new(n_cells).with_constants(SmagorinskyConstants::standard());
|
||
|
||
// Calculate filter width from mesh size
|
||
let filter_width = DVector::from_element(n_cells, config.mesh_size_cylinder * 2.0);
|
||
model.set_filter_width(filter_width)?;
|
||
|
||
let mut state = TurbulenceState::new(n_cells);
|
||
state.density = density;
|
||
state.molecular_viscosity = viscosity;
|
||
|
||
println!(" LES model: Smagorinsky");
|
||
println!(" Filter width: {:.3}", config.mesh_size_cylinder * 2.0);
|
||
|
||
(Some(model), Some(state))
|
||
} else {
|
||
(None, None)
|
||
};
|
||
|
||
Ok(Self {
|
||
config,
|
||
cfd_config,
|
||
mesh,
|
||
flow_field,
|
||
solver,
|
||
boundary_conditions,
|
||
discretization,
|
||
les_model,
|
||
turbulence_state,
|
||
force_history: Vec::new(),
|
||
cylinder_cells,
|
||
})
|
||
}
|
||
|
||
/// Estimate number of cells for mesh generation
|
||
fn estimate_cell_count(config: &CylinderConfig) -> usize {
|
||
// Rough estimate based on domain size and mesh resolution
|
||
let domain_area = config.domain_size[0] * config.domain_size[1];
|
||
let avg_cell_size = (config.mesh_size_cylinder + config.mesh_size_boundary) / 2.0;
|
||
let avg_cell_area = avg_cell_size * avg_cell_size;
|
||
(domain_area / avg_cell_area) as usize
|
||
}
|
||
|
||
/// Set initial conditions
|
||
fn set_initial_conditions(
|
||
flow_field: &mut FlowField,
|
||
config: &CylinderConfig,
|
||
nx: usize,
|
||
ny: usize,
|
||
) -> CfdResult<()> {
|
||
// Initialize with uniform flow
|
||
for j in 0..ny {
|
||
for i in 0..nx {
|
||
flow_field.set_velocity(i, j, config.inlet_velocity, 0.0)?;
|
||
flow_field.set_pressure(i, j, 0.0)?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Set up boundary conditions
|
||
fn setup_boundary_conditions(
|
||
config: &CylinderConfig,
|
||
_nx: usize,
|
||
_ny: usize,
|
||
) -> CfdResult<(BoundaryConditions, Vec<usize>)> {
|
||
let mut boundary_conditions = BoundaryConditions::new();
|
||
|
||
// Inlet boundary (left side)
|
||
boundary_conditions.add_boundary_condition(
|
||
BoundaryLocation::Left,
|
||
BoundaryType::VelocityInlet {
|
||
u: config.inlet_velocity,
|
||
v: 0.0,
|
||
},
|
||
);
|
||
|
||
// Outlet boundary (right side)
|
||
boundary_conditions.add_boundary_condition(
|
||
BoundaryLocation::Right,
|
||
BoundaryType::PressureOutlet { pressure: 0.0 },
|
||
);
|
||
|
||
// Slip walls (top and bottom)
|
||
boundary_conditions
|
||
.add_boundary_condition(BoundaryLocation::Top, BoundaryType::FreeSlipWall);
|
||
|
||
boundary_conditions
|
||
.add_boundary_condition(BoundaryLocation::Bottom, BoundaryType::FreeSlipWall);
|
||
|
||
// Note: Cylinder surface boundary would require special handling in unstructured mesh
|
||
let cylinder_cells = Vec::new(); // Simplified for now
|
||
|
||
println!(" Cylinder surface cells: {}", cylinder_cells.len());
|
||
|
||
Ok((boundary_conditions, cylinder_cells))
|
||
}
|
||
|
||
/// Run time-dependent simulation
|
||
pub fn run_simulation(&mut self) -> CfdResult<CylinderResults> {
|
||
println!("\nStarting transient simulation...");
|
||
println!(" Time step: {:.2e}", self.config.time_step);
|
||
println!(" Total time: {:.2}", self.config.total_time);
|
||
|
||
let start_time = Instant::now();
|
||
let mut time = 0.0;
|
||
let mut time_step = 0;
|
||
let mut residuals = Vec::new();
|
||
|
||
while time < self.config.total_time {
|
||
// Note: PisoSolver methods are async, placeholder here
|
||
let residual = 1e-6; // Placeholder - would need async runtime
|
||
|
||
// Update LES model if present (placeholder - model doesn't have update method)
|
||
if self.turbulence_state.is_some() {
|
||
// Note: Would call update methods here in real implementation
|
||
// self.update_turbulence_state(&mut turbulence_state)?;
|
||
}
|
||
|
||
// Calculate force coefficients
|
||
let forces = self.calculate_force_coefficients(time)?;
|
||
self.force_history.push(forces);
|
||
|
||
time += self.config.time_step;
|
||
time_step += 1;
|
||
residuals.push(residual);
|
||
|
||
// Print progress
|
||
if time_step % self.config.output_frequency == 0 {
|
||
let latest_forces = self.force_history.last().unwrap();
|
||
println!(
|
||
" Time: {:.3}, Step: {}, Residual: {:.2e}, Cd: {:.3}, Cl: {:.3}",
|
||
time, time_step, residual, latest_forces.cd, latest_forces.cl
|
||
);
|
||
}
|
||
}
|
||
|
||
let elapsed = start_time.elapsed();
|
||
println!(
|
||
"Simulation completed in {:.2} seconds",
|
||
elapsed.as_secs_f64()
|
||
);
|
||
|
||
Ok(CylinderResults {
|
||
time_steps: time_step,
|
||
residuals,
|
||
force_history: self.force_history.clone(),
|
||
final_time: time,
|
||
elapsed_time: elapsed,
|
||
flow_field: self.flow_field.clone(),
|
||
})
|
||
}
|
||
|
||
/// Update turbulence state from flow field
|
||
fn update_turbulence_state(&mut self, state: &mut TurbulenceState) -> CfdResult<()> {
|
||
let nx = self.flow_field.nx;
|
||
let ny = self.flow_field.ny;
|
||
|
||
// Update velocity field
|
||
for j in 0..ny {
|
||
for i in 0..nx {
|
||
let cell_idx = i + j * nx;
|
||
if cell_idx < state.velocity.len() {
|
||
let (u, v) = self.flow_field.get_velocity_at(i, j)?;
|
||
state.velocity[cell_idx] = Vector3::new(u, v, 0.0);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update pressure field
|
||
for j in 0..ny {
|
||
for i in 0..nx {
|
||
let cell_idx = i + j * nx;
|
||
if cell_idx < state.pressure.len() {
|
||
let p = self.flow_field.get_pressure_at(i, j)?;
|
||
state.pressure[cell_idx] = p;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Calculate velocity gradients (simplified)
|
||
for j in 1..ny - 1 {
|
||
for i in 1..nx - 1 {
|
||
let cell_idx = i + j * nx;
|
||
if cell_idx < state.velocity_gradients.len() {
|
||
let dx = self.flow_field.dx;
|
||
let (u_right, _) = self.flow_field.get_velocity_at(i + 1, j)?;
|
||
let (u_left, _) = self.flow_field.get_velocity_at(i - 1, j)?;
|
||
state.velocity_gradients[cell_idx][0][0] = (u_right - u_left) / (2.0 * dx);
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Calculate force coefficients on cylinder
|
||
fn calculate_force_coefficients(&self, time: f64) -> CfdResult<ForceCoefficients> {
|
||
// Simplified force calculation
|
||
// In a real implementation, would integrate pressure and shear stress over cylinder surface
|
||
// For now, return placeholder values since cylinder_cells is empty
|
||
|
||
Ok(ForceCoefficients {
|
||
cd: 1.0, // Placeholder drag coefficient
|
||
cl: 0.0, // Placeholder lift coefficient
|
||
cd_pressure: 0.8, // Placeholder pressure drag
|
||
cd_viscous: 0.2, // Placeholder viscous drag
|
||
time,
|
||
})
|
||
}
|
||
|
||
/// Get force history
|
||
pub fn force_history(&self) -> &Vec<ForceCoefficients> {
|
||
&self.force_history
|
||
}
|
||
|
||
/// Get configuration
|
||
pub fn config(&self) -> &CylinderConfig {
|
||
&self.config
|
||
}
|
||
}
|
||
|
||
/// Cylinder simulation results
|
||
#[derive(Debug, Clone)]
|
||
pub struct CylinderResults {
|
||
/// Number of time steps
|
||
pub time_steps: usize,
|
||
/// Residual history
|
||
pub residuals: Vec<f64>,
|
||
/// Force coefficient history
|
||
pub force_history: Vec<ForceCoefficients>,
|
||
/// Final simulation time
|
||
pub final_time: f64,
|
||
/// Elapsed wall time
|
||
pub elapsed_time: std::time::Duration,
|
||
/// Final flow field
|
||
pub flow_field: FlowField,
|
||
}
|
||
|
||
impl CylinderResults {
|
||
/// Print summary
|
||
pub fn print_summary(&self) {
|
||
println!("\n=== Cylinder Flow Results ===");
|
||
println!("Time steps: {}", self.time_steps);
|
||
println!("Final time: {:.2}", self.final_time);
|
||
println!(
|
||
"Elapsed time: {:.2} seconds",
|
||
self.elapsed_time.as_secs_f64()
|
||
);
|
||
|
||
if let (Some(first), Some(last)) = (self.force_history.first(), self.force_history.last()) {
|
||
println!("Initial Cd: {:.3}, Final Cd: {:.3}", first.cd, last.cd);
|
||
println!("Initial Cl: {:.3}, Final Cl: {:.3}", first.cl, last.cl);
|
||
}
|
||
|
||
// Calculate mean and RMS values
|
||
if !self.force_history.is_empty() {
|
||
let mean_cd = self.force_history.iter().map(|f| f.cd).sum::<f64>()
|
||
/ self.force_history.len() as f64;
|
||
let mean_cl = self.force_history.iter().map(|f| f.cl).sum::<f64>()
|
||
/ self.force_history.len() as f64;
|
||
|
||
let rms_cl = (self
|
||
.force_history
|
||
.iter()
|
||
.map(|f| (f.cl - mean_cl).powi(2))
|
||
.sum::<f64>()
|
||
/ self.force_history.len() as f64)
|
||
.sqrt();
|
||
|
||
println!("Mean Cd: {:.3}", mean_cd);
|
||
println!("Mean Cl: {:.3}", mean_cl);
|
||
println!("RMS Cl: {:.3}", rms_cl);
|
||
}
|
||
}
|
||
|
||
/// Detect vortex shedding frequency
|
||
pub fn detect_shedding_frequency(&self, dt: f64) -> Option<f64> {
|
||
if self.force_history.len() < 100 {
|
||
return None;
|
||
}
|
||
|
||
// Simple frequency detection using zero crossings of Cl
|
||
let cl_values: Vec<f64> = self.force_history.iter().map(|f| f.cl).collect();
|
||
let mean_cl = cl_values.iter().sum::<f64>() / cl_values.len() as f64;
|
||
|
||
let mut zero_crossings = 0;
|
||
for i in 1..cl_values.len() {
|
||
if (cl_values[i] - mean_cl) * (cl_values[i - 1] - mean_cl) < 0.0 {
|
||
zero_crossings += 1;
|
||
}
|
||
}
|
||
|
||
if zero_crossings > 4 {
|
||
let frequency = zero_crossings as f64 / (2.0 * self.final_time);
|
||
Some(frequency)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
}
|
||
|
||
fn main() -> CfdResult<()> {
|
||
println!("=== Flow Past Circular Cylinder Simulation ===");
|
||
|
||
// Run Re = 100 case
|
||
println!("\n--- Re = 100 Case ---");
|
||
let mut cylinder_100 = FlowPastCylinder::new(CylinderConfig::re_100())?;
|
||
let results_100 = cylinder_100.run_simulation()?;
|
||
results_100.print_summary();
|
||
|
||
// Run Re = 200 case with vortex shedding
|
||
println!("\n--- Re = 200 Case (Vortex Shedding) ---");
|
||
let mut cylinder_200 = FlowPastCylinder::new(CylinderConfig::re_200())?;
|
||
let results_200 = cylinder_200.run_simulation()?;
|
||
results_200.print_summary();
|
||
|
||
if let Some(freq) = results_200.detect_shedding_frequency(cylinder_200.config().time_step) {
|
||
println!("Detected shedding frequency: {:.3} Hz", freq);
|
||
let strouhal = freq * cylinder_200.config().diameter / cylinder_200.config().inlet_velocity;
|
||
println!("Strouhal number: {:.3}", strouhal);
|
||
}
|
||
|
||
// Run high Re case with LES
|
||
println!("\n--- High Re Case with LES (Re = 3900) ---");
|
||
let mut cylinder_les = FlowPastCylinder::new(CylinderConfig::high_re_les())?;
|
||
let results_les = cylinder_les.run_simulation()?;
|
||
results_les.print_summary();
|
||
|
||
println!("\n=== All cylinder simulations completed successfully! ===");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_cylinder_config_creation() {
|
||
let config = CylinderConfig::re_100();
|
||
assert_eq!(config.reynolds_number, 100.0);
|
||
assert_eq!(config.diameter, 1.0);
|
||
assert!(!config.use_les);
|
||
}
|
||
|
||
#[test]
|
||
fn test_strouhal_number() {
|
||
let config = CylinderConfig::re_200();
|
||
let st = config.strouhal_number();
|
||
assert!(st > 0.0);
|
||
assert!(st < 0.3);
|
||
}
|
||
|
||
#[test]
|
||
fn test_viscosity_calculation() {
|
||
let config = CylinderConfig::re_100();
|
||
let density = 1.0;
|
||
let viscosity = config.viscosity(density);
|
||
assert!((viscosity - 0.01).abs() < 1e-10);
|
||
}
|
||
|
||
#[test]
|
||
fn test_force_coefficients() {
|
||
let forces = ForceCoefficients::zero(1.0);
|
||
assert_eq!(forces.cd, 0.0);
|
||
assert_eq!(forces.cl, 0.0);
|
||
assert_eq!(forces.time, 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_les_config() {
|
||
let config = CylinderConfig::high_re_les();
|
||
assert!(config.use_les);
|
||
assert_eq!(config.reynolds_number, 3900.0);
|
||
}
|
||
}
|