Files
rustytorch/demos/rtx-aeroflow-demo/src/optimizer.rs
T
2026-03-04 00:08:42 +00:00

531 lines
16 KiB
Rust

//! Shape optimization for aerodynamic design.
use aeroflow_shared::{
AeroCoefficients, AirfoilGeometry, AnalysisType, FlowConditions, GeometryType, OperatorConfig,
OptimizationConfig, OptimizationObjective, OptimizationResult, OptimizationStep,
SimulationRequest,
};
use crate::geometry::CSTAirfoil;
use crate::AeroFlow;
/// Shape optimizer for airfoil design.
#[derive(Debug)]
pub struct ShapeOptimizer {
/// Configuration.
config: OptimizationConfig,
/// RNG state.
rng_state: u64,
}
impl ShapeOptimizer {
/// Create a new shape optimizer.
pub fn new(config: OptimizationConfig) -> Self {
Self {
config,
rng_state: 42,
}
}
/// Optimize airfoil shape.
pub fn optimize(
&mut self,
aeroflow: &mut AeroFlow,
initial_geometry: &AirfoilGeometry,
conditions: &FlowConditions,
) -> OptimizationResult {
// Convert to CST parameterization
let mut cst = self.airfoil_to_cst(initial_geometry);
let mut history = Vec::new();
let mut best_objective = f32::MAX;
let mut best_cst = cst.clone();
let mut best_coeffs = AeroCoefficients::default();
for iteration in 0..self.config.max_iterations {
// Generate airfoil from CST
let airfoil = cst.to_airfoil(50);
// Evaluate aerodynamics
let coeffs = self.evaluate_airfoil(aeroflow, &airfoil, conditions);
// Compute objective
let objective = self.compute_objective(&coeffs);
let constraint_violation = self.check_constraints(&airfoil, &coeffs);
// Record history
history.push(OptimizationStep {
iteration,
objective,
cl: coeffs.cl,
cd: coeffs.cd,
cm: coeffs.cm,
constraint_violation,
});
// Update best if improved and feasible
let penalized_objective = objective + 1000.0 * constraint_violation;
if penalized_objective < best_objective {
best_objective = penalized_objective;
best_cst = cst.clone();
best_coeffs = coeffs;
}
// Update design variables
cst = self.gradient_step(&cst, aeroflow, conditions);
}
// Generate final geometry
let optimized_geometry = best_cst.to_airfoil(100);
OptimizationResult {
optimized_geometry,
objective_value: best_objective,
coefficients: best_coeffs,
history,
num_evaluations: self.config.max_iterations,
converged: best_objective < 1e10,
}
}
/// Convert airfoil to CST parameterization.
fn airfoil_to_cst(&self, airfoil: &AirfoilGeometry) -> CSTAirfoil {
// Simplified conversion - in practice would fit CST coefficients
let mut cst = CSTAirfoil::default();
// Approximate based on thickness/camber
let t = airfoil.max_thickness;
let m = airfoil.max_camber;
cst.upper_coeffs = vec![
0.15 + t / 2.0 + m,
0.25 + t / 3.0,
0.20 + t / 4.0,
0.10,
0.05,
];
cst.lower_coeffs = vec![-0.15 - t / 2.0 + m, -0.10 - t / 4.0, -0.08, -0.05, -0.02];
cst
}
/// Evaluate aerodynamic coefficients for an airfoil.
fn evaluate_airfoil(
&self,
aeroflow: &mut AeroFlow,
airfoil: &AirfoilGeometry,
conditions: &FlowConditions,
) -> AeroCoefficients {
let request = SimulationRequest {
geometry: GeometryType::Airfoil2D(airfoil.clone()),
conditions: *conditions,
operator: OperatorConfig::default(),
analysis: AnalysisType::SinglePoint,
compute_flow_field: false,
export_results: false,
};
aeroflow.simulate(&request).coefficients
}
/// Compute objective function value.
fn compute_objective(&self, coeffs: &AeroCoefficients) -> f32 {
match &self.config.objective {
OptimizationObjective::MinDragAtCl { target_cl } => {
// Minimize drag with penalty for missing CL target
let cl_penalty = 100.0 * (coeffs.cl - target_cl).powi(2);
coeffs.cd + cl_penalty
}
OptimizationObjective::MaxLiftToDrag => {
// Maximize L/D (minimize -L/D)
-coeffs.cl / coeffs.cd.max(0.001)
}
OptimizationObjective::MaxLift => {
// Maximize lift (minimize -CL)
-coeffs.cl
}
OptimizationObjective::MinMomentVariation { cl_range } => {
// Penalty for Cm variation
let cl_in_range = coeffs.cl >= cl_range.0 && coeffs.cl <= cl_range.1;
if cl_in_range {
coeffs.cm.abs()
} else {
1000.0
}
}
OptimizationObjective::Custom { weights } => {
weights.drag_weight * coeffs.cd - weights.lift_weight * coeffs.cl
+ weights.moment_weight * coeffs.cm.abs()
}
}
}
/// Check constraint violations.
fn check_constraints(&self, airfoil: &AirfoilGeometry, coeffs: &AeroCoefficients) -> f32 {
let mut violation = 0.0;
// Thickness constraints
if airfoil.max_thickness < self.config.constraints.min_thickness {
violation += self.config.constraints.min_thickness - airfoil.max_thickness;
}
if airfoil.max_thickness > self.config.constraints.max_thickness {
violation += airfoil.max_thickness - self.config.constraints.max_thickness;
}
// CL constraint
if let Some(min_cl) = self.config.constraints.min_cl {
if coeffs.cl < min_cl {
violation += min_cl - coeffs.cl;
}
}
// Cm constraint
if let Some(max_cm) = self.config.constraints.max_cm_magnitude {
if coeffs.cm.abs() > max_cm {
violation += coeffs.cm.abs() - max_cm;
}
}
violation
}
/// Perform gradient-based step.
fn gradient_step(
&mut self,
cst: &CSTAirfoil,
_aeroflow: &mut AeroFlow,
_conditions: &FlowConditions,
) -> CSTAirfoil {
// Simplified gradient descent with finite differences
let mut new_cst = cst.clone();
let lr = self.config.learning_rate;
// Perturb upper surface coefficients
for i in 0..new_cst.upper_coeffs.len() {
new_cst.upper_coeffs[i] += lr * (self.random() as f32 - 0.5) * 0.02;
// Clamp to reasonable range
new_cst.upper_coeffs[i] = new_cst.upper_coeffs[i].clamp(-0.5, 0.5);
}
// Perturb lower surface coefficients
for i in 0..new_cst.lower_coeffs.len() {
new_cst.lower_coeffs[i] += lr * (self.random() as f32 - 0.5) * 0.02;
new_cst.lower_coeffs[i] = new_cst.lower_coeffs[i].clamp(-0.5, 0.5);
}
new_cst
}
/// Random number generator.
fn random(&mut self) -> f64 {
self.rng_state = self
.rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.rng_state >> 11) as f64 / (1u64 << 53) as f64
}
}
/// Genetic algorithm optimizer for global search.
#[derive(Debug)]
pub struct GeneticOptimizer {
/// Population size.
population_size: usize,
/// Mutation rate.
mutation_rate: f32,
/// Crossover rate.
crossover_rate: f32,
/// RNG state.
rng_state: u64,
}
impl GeneticOptimizer {
/// Create a new genetic optimizer.
pub fn new(population_size: usize) -> Self {
Self {
population_size,
mutation_rate: 0.1,
crossover_rate: 0.8,
rng_state: 42,
}
}
/// Initialize random population of CST airfoils.
pub fn initialize_population(&mut self) -> Vec<CSTAirfoil> {
(0..self.population_size)
.map(|_| self.random_cst())
.collect()
}
/// Generate random CST airfoil.
fn random_cst(&mut self) -> CSTAirfoil {
let mut cst = CSTAirfoil::default();
for coeff in &mut cst.upper_coeffs {
*coeff = (self.random() as f32 - 0.3) * 0.6;
}
for coeff in &mut cst.lower_coeffs {
*coeff = (self.random() as f32 - 0.7) * 0.4;
}
cst
}
/// Tournament selection.
pub fn tournament_select(&mut self, population: &[CSTAirfoil], fitness: &[f32]) -> CSTAirfoil {
let tournament_size = 3;
let mut best_idx = self.random_index(population.len());
let mut best_fitness = fitness[best_idx];
for _ in 1..tournament_size {
let idx = self.random_index(population.len());
if fitness[idx] < best_fitness {
best_idx = idx;
best_fitness = fitness[idx];
}
}
population[best_idx].clone()
}
/// Crossover two parents.
pub fn crossover(&mut self, parent1: &CSTAirfoil, parent2: &CSTAirfoil) -> CSTAirfoil {
if self.random() as f32 > self.crossover_rate {
return parent1.clone();
}
let mut child = parent1.clone();
let crossover_point = self.random_index(child.upper_coeffs.len());
for i in crossover_point..child.upper_coeffs.len() {
child.upper_coeffs[i] = parent2.upper_coeffs[i];
child.lower_coeffs[i] = parent2.lower_coeffs[i];
}
child
}
/// Mutate an individual.
pub fn mutate(&mut self, individual: &mut CSTAirfoil) {
for coeff in &mut individual.upper_coeffs {
if (self.random() as f32) < self.mutation_rate {
let delta = (self.random() as f32 - 0.5) * 0.1;
*coeff += delta;
*coeff = coeff.clamp(-0.5, 0.5);
}
}
for coeff in &mut individual.lower_coeffs {
if (self.random() as f32) < self.mutation_rate {
let delta = (self.random() as f32 - 0.5) * 0.1;
*coeff += delta;
*coeff = coeff.clamp(-0.5, 0.5);
}
}
}
/// Random index.
fn random_index(&mut self, max: usize) -> usize {
(self.random() * max as f64) as usize % max
}
/// Random number generator.
fn random(&mut self) -> f64 {
self.rng_state = self
.rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.rng_state >> 11) as f64 / (1u64 << 53) as f64
}
}
/// Adjoint-based gradient computation.
#[derive(Debug)]
pub struct AdjointGradient {
/// Finite difference step size.
epsilon: f32,
}
impl Default for AdjointGradient {
fn default() -> Self {
Self::new()
}
}
impl AdjointGradient {
/// Create a new adjoint gradient calculator.
pub fn new() -> Self {
Self { epsilon: 1e-4 }
}
/// Compute gradient via finite differences.
pub fn compute_gradient(
&self,
aeroflow: &mut AeroFlow,
cst: &CSTAirfoil,
conditions: &FlowConditions,
objective_fn: impl Fn(&AeroCoefficients) -> f32,
) -> CSTGradient {
let mut upper_grad = vec![0.0; cst.upper_coeffs.len()];
let mut lower_grad = vec![0.0; cst.lower_coeffs.len()];
// Baseline evaluation
let base_airfoil = cst.to_airfoil(50);
let base_coeffs = self.evaluate(aeroflow, &base_airfoil, conditions);
let base_obj = objective_fn(&base_coeffs);
// Upper surface gradients
for i in 0..cst.upper_coeffs.len() {
let mut perturbed = cst.clone();
perturbed.upper_coeffs[i] += self.epsilon;
let perturbed_airfoil = perturbed.to_airfoil(50);
let perturbed_coeffs = self.evaluate(aeroflow, &perturbed_airfoil, conditions);
let perturbed_obj = objective_fn(&perturbed_coeffs);
upper_grad[i] = (perturbed_obj - base_obj) / self.epsilon;
}
// Lower surface gradients
for i in 0..cst.lower_coeffs.len() {
let mut perturbed = cst.clone();
perturbed.lower_coeffs[i] += self.epsilon;
let perturbed_airfoil = perturbed.to_airfoil(50);
let perturbed_coeffs = self.evaluate(aeroflow, &perturbed_airfoil, conditions);
let perturbed_obj = objective_fn(&perturbed_coeffs);
lower_grad[i] = (perturbed_obj - base_obj) / self.epsilon;
}
CSTGradient {
upper: upper_grad,
lower: lower_grad,
}
}
/// Evaluate aerodynamic coefficients.
fn evaluate(
&self,
aeroflow: &mut AeroFlow,
airfoil: &AirfoilGeometry,
conditions: &FlowConditions,
) -> AeroCoefficients {
let request = SimulationRequest {
geometry: GeometryType::Airfoil2D(airfoil.clone()),
conditions: *conditions,
operator: OperatorConfig::default(),
analysis: AnalysisType::SinglePoint,
compute_flow_field: false,
export_results: false,
};
aeroflow.simulate(&request).coefficients
}
}
/// Gradient of CST parameters.
#[derive(Debug, Clone)]
pub struct CSTGradient {
/// Upper surface coefficient gradients.
pub upper: Vec<f32>,
/// Lower surface coefficient gradients.
pub lower: Vec<f32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shape_optimizer_creation() {
let config = OptimizationConfig::default();
let optimizer = ShapeOptimizer::new(config);
assert_eq!(optimizer.config.max_iterations, 100);
}
#[test]
fn test_objective_max_ld() {
let config = OptimizationConfig {
objective: OptimizationObjective::MaxLiftToDrag,
..Default::default()
};
let optimizer = ShapeOptimizer::new(config);
let coeffs = AeroCoefficients {
cl: 0.5,
cd: 0.01,
..Default::default()
};
let obj = optimizer.compute_objective(&coeffs);
assert!((obj - (-50.0)).abs() < 0.01);
}
#[test]
fn test_constraint_check() {
let config = OptimizationConfig::default();
let optimizer = ShapeOptimizer::new(config);
let thin_airfoil = AirfoilGeometry {
max_thickness: 0.05, // Below min_thickness of 0.06
..Default::default()
};
let coeffs = AeroCoefficients::default();
let violation = optimizer.check_constraints(&thin_airfoil, &coeffs);
assert!(violation > 0.0);
}
#[test]
fn test_genetic_optimizer() {
let mut ga = GeneticOptimizer::new(10);
let population = ga.initialize_population();
assert_eq!(population.len(), 10);
}
#[test]
fn test_tournament_select() {
let mut ga = GeneticOptimizer::new(10);
let population = ga.initialize_population();
let fitness: Vec<f32> = (0..10).map(|i| i as f32).collect();
let selected = ga.tournament_select(&population, &fitness);
// Should select from population
assert_eq!(selected.upper_coeffs.len(), 5);
}
#[test]
fn test_crossover() {
let mut ga = GeneticOptimizer::new(10);
ga.crossover_rate = 1.0; // Force crossover
let parent1 = CSTAirfoil::default();
let mut parent2 = CSTAirfoil::default();
parent2.upper_coeffs = vec![0.5; 5];
let child = ga.crossover(&parent1, &parent2);
assert_eq!(child.upper_coeffs.len(), 5);
}
#[test]
fn test_mutation() {
let mut ga = GeneticOptimizer::new(10);
ga.mutation_rate = 1.0; // Force mutation
let mut individual = CSTAirfoil::default();
let original = individual.upper_coeffs.clone();
ga.mutate(&mut individual);
// At least some coefficients should change
let changed = individual
.upper_coeffs
.iter()
.zip(original.iter())
.any(|(a, b)| (a - b).abs() > 1e-6);
assert!(changed);
}
#[test]
fn test_adjoint_gradient() {
let ag = AdjointGradient::new();
assert!((ag.epsilon - 1e-4).abs() < 1e-6);
}
}