use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TurbinePosition {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct WakeSource {
pub u_inf: f64,
pub d: f64,
pub ct: f64,
pub k: f64,
}
impl WakeSource {
pub fn jensen_wake_speed(&self, dx: f64, dy: f64) -> f64 {
if dx <= 0.0 {
return self.u_inf;
}
let r0 = self.d / 2.0;
let r_wake = r0 + self.k * dx;
let deficit = (1.0 - (1.0 - self.ct).sqrt()) * (r0 / r_wake).powi(2);
if dy.abs() <= r_wake {
let overlap = overlap_fraction(dy.abs(), r_wake, r0);
self.u_inf * (1.0 - deficit * overlap)
} else {
self.u_inf
}
}
pub fn frandsen_wake_speed(&self, dx: f64, dy: f64) -> f64 {
if dx <= 0.0 {
return self.u_inf;
}
let r0 = self.d / 2.0;
let beta = 0.5 * (1.0 + (1.0 - self.ct).sqrt()) / (1.0 - self.ct).sqrt();
let r_wake = r0 * (beta + self.k * dx / r0).sqrt();
if dy.abs() <= r_wake {
let deficit = 0.5 * (1.0 - (1.0 - self.ct * (r0 / r_wake).powi(2)).sqrt());
self.u_inf * (1.0 - 2.0 * deficit)
} else {
self.u_inf
}
}
}
fn overlap_fraction(dy: f64, r_wake: f64, _r_rotor: f64) -> f64 {
if dy > r_wake {
0.0
} else {
1.0
}
}
pub fn farm_wake_speeds(
positions: &[TurbinePosition],
wind_dir_deg: f64,
sources: &[WakeSource],
) -> Vec<f64> {
assert_eq!(positions.len(), sources.len());
let n = positions.len();
let u_inf = sources[0].u_inf;
let dir_rad = wind_dir_deg.to_radians();
let (sin_d, cos_d) = (dir_rad.sin(), dir_rad.cos());
let rotate = |p: &TurbinePosition| -> (f64, f64) {
let x_w = -p.x * sin_d - p.y * cos_d;
let y_w = -p.x * cos_d + p.y * sin_d;
(x_w, y_w)
};
let rotated: Vec<(f64, f64)> = positions.iter().map(rotate).collect();
let mut speeds = vec![u_inf; n];
for i in 0..n {
let mut deficit_sq_sum = 0.0_f64;
for j in 0..n {
if i == j {
continue;
}
let dx = rotated[i].0 - rotated[j].0;
let dy = rotated[i].1 - rotated[j].1;
if dx <= 0.0 {
continue;
} let u_wake = sources[j].jensen_wake_speed(dx, dy);
let d_rel = (u_inf - u_wake) / u_inf;
deficit_sq_sum += d_rel * d_rel;
}
speeds[i] = u_inf * (1.0 - deficit_sq_sum.sqrt());
}
speeds
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jensen_no_deficit_upstream() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.jensen_wake_speed(-100.0, 0.0);
assert_eq!(u, 10.0);
}
#[test]
fn test_jensen_deficit_on_centreline() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.jensen_wake_speed(500.0, 0.0);
assert!(u < 10.0 && u > 0.0, "u={:.2}", u);
}
#[test]
fn test_jensen_deficit_outside_cone() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.jensen_wake_speed(500.0, 1000.0);
assert_eq!(u, 10.0);
}
#[test]
fn test_deficit_decreases_with_distance() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u1 = src.jensen_wake_speed(300.0, 0.0);
let u2 = src.jensen_wake_speed(600.0, 0.0);
assert!(u2 > u1, "u1={:.2} u2={:.2}", u1, u2);
}
#[test]
fn test_farm_two_turbines_in_line() {
let positions = vec![
TurbinePosition { x: 0.0, y: 0.0 },
TurbinePosition { x: 500.0, y: 0.0 },
];
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let sources = vec![src; 2];
let speeds = farm_wake_speeds(&positions, 270.0, &sources); assert!(speeds[1] < speeds[0], "speeds={:?}", speeds);
}
#[test]
fn test_frandsen_upstream_returns_u_inf() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.frandsen_wake_speed(-100.0, 0.0);
assert_eq!(u, 10.0, "Frandsen upstream must return u_inf, got {u:.4}");
}
#[test]
fn test_frandsen_deficit_on_centreline() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.frandsen_wake_speed(500.0, 0.0);
assert!(
u < 10.0 && u > 0.0,
"Frandsen centreline speed must be in (0, u_inf), got {u:.4}"
);
}
#[test]
fn test_frandsen_outside_wake_returns_u_inf() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.frandsen_wake_speed(500.0, 5000.0);
assert_eq!(
u, 10.0,
"Frandsen far crosswind must return u_inf, got {u:.4}"
);
}
#[test]
fn test_jensen_zero_distance_gives_deficit() {
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let u = src.jensen_wake_speed(0.001, 0.0);
assert!(
u <= 10.0,
"Jensen at dx≈0 on centreline must be ≤ u_inf, got {u:.4}"
);
}
#[test]
fn test_farm_single_turbine() {
let positions = vec![TurbinePosition { x: 0.0, y: 0.0 }];
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let sources = vec![src];
let speeds = farm_wake_speeds(&positions, 270.0, &sources);
assert_eq!(speeds.len(), 1, "Expected exactly one speed value");
assert_eq!(
speeds[0], 10.0,
"Single turbine must see free-stream speed, got {:.4}",
speeds[0]
);
}
#[test]
fn test_farm_crosswind_no_wake() {
let positions = vec![
TurbinePosition { x: 0.0, y: 5000.0 },
TurbinePosition { x: 0.0, y: -5000.0 },
];
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let sources = vec![src; 2];
let speeds = farm_wake_speeds(&positions, 270.0, &sources);
assert_eq!(
speeds[0], 10.0,
"Crosswind turbine 0 must see u_inf, got {:.4}",
speeds[0]
);
assert_eq!(
speeds[1], 10.0,
"Crosswind turbine 1 must see u_inf, got {:.4}",
speeds[1]
);
}
#[test]
fn test_farm_three_turbines_in_line_speed_decreasing() {
let positions = vec![
TurbinePosition { x: 0.0, y: 0.0 },
TurbinePosition { x: 500.0, y: 0.0 },
TurbinePosition { x: 1000.0, y: 0.0 },
];
let src = WakeSource {
u_inf: 10.0,
d: 90.0,
ct: 0.8,
k: 0.04,
};
let sources = vec![src; 3];
let speeds = farm_wake_speeds(&positions, 270.0, &sources);
assert_eq!(speeds.len(), 3, "Expected three speed values");
assert!(
speeds[0] > speeds[2],
"Most upwind turbine must see higher speed than most downwind; speeds={speeds:?}"
);
}
}