1use crate::field::{Wind, WindField};
4use crate::{Error, Result};
5
6#[derive(Debug, Clone, Copy)]
10pub struct LogProfile {
11 pub reference_speed_mps: f64,
13 pub reference_height_m: f64,
15 pub roughness_m: f64,
17 pub direction_from_deg: f64,
20}
21
22impl LogProfile {
23 pub fn new(
25 reference_speed_mps: f64,
26 reference_height_m: f64,
27 roughness_m: f64,
28 direction_from_deg: f64,
29 ) -> Result<Self> {
30 let profile = Self {
31 reference_speed_mps,
32 reference_height_m,
33 roughness_m,
34 direction_from_deg,
35 };
36 profile.validate()?;
37 Ok(profile)
38 }
39
40 fn validate(&self) -> Result<()> {
41 if !self.reference_speed_mps.is_finite() || self.reference_speed_mps < 0.0 {
42 return Err(Error::invalid(
43 "reference_speed_mps",
44 "must be a nonnegative number",
45 ));
46 }
47 if !self.reference_height_m.is_finite() || self.reference_height_m <= 0.0 {
48 return Err(Error::invalid("reference_height_m", "must be positive"));
49 }
50 if !self.roughness_m.is_finite()
51 || self.roughness_m <= 0.0
52 || self.roughness_m >= self.reference_height_m
53 {
54 return Err(Error::invalid(
55 "roughness_m",
56 "must be positive and below the reference height",
57 ));
58 }
59 Ok(())
60 }
61
62 pub fn speed_at(&self, altitude_m: f64) -> f64 {
64 if altitude_m <= self.roughness_m {
65 return 0.0;
66 }
67 let z0 = self.roughness_m;
68 self.reference_speed_mps * (altitude_m / z0).ln() / (self.reference_height_m / z0).ln()
69 }
70}
71
72impl WindField for LogProfile {
73 fn wind(&self, _time_s: f64, position_m: [f64; 3]) -> Wind {
74 Wind::from_speed_direction(self.speed_at(position_m[1]), self.direction_from_deg)
75 }
76}
77
78#[derive(Debug, Clone, Copy)]
87pub struct EkmanProfile {
88 pub geostrophic_mps: f64,
90 pub latitude_deg: f64,
92 pub eddy_viscosity_m2s: f64,
94 pub direction_from_deg: f64,
97}
98
99impl EkmanProfile {
100 const OMEGA: f64 = 7.2921159e-5;
102
103 pub fn new(
105 geostrophic_mps: f64,
106 latitude_deg: f64,
107 eddy_viscosity_m2s: f64,
108 direction_from_deg: f64,
109 ) -> Result<Self> {
110 let profile = Self {
111 geostrophic_mps,
112 latitude_deg,
113 eddy_viscosity_m2s,
114 direction_from_deg,
115 };
116 profile.validate()?;
117 Ok(profile)
118 }
119
120 fn validate(&self) -> Result<()> {
121 if !self.geostrophic_mps.is_finite() || self.geostrophic_mps < 0.0 {
122 return Err(Error::invalid(
123 "geostrophic_mps",
124 "must be a nonnegative number",
125 ));
126 }
127 if !self.latitude_deg.is_finite() || self.latitude_deg.abs() >= 90.0 {
128 return Err(Error::invalid(
129 "latitude_deg",
130 "must be finite and away from the poles",
131 ));
132 }
133 if !self.eddy_viscosity_m2s.is_finite() || self.eddy_viscosity_m2s <= 0.0 {
134 return Err(Error::invalid("eddy_viscosity_m2s", "must be positive"));
135 }
136 Ok(())
137 }
138
139 fn coriolis(&self) -> f64 {
140 2.0 * Self::OMEGA * self.latitude_deg.to_radians().sin()
141 }
142
143 fn components_at(&self, altitude_m: f64) -> (f64, f64) {
145 let f = self.coriolis();
146 if f == 0.0 || altitude_m <= 0.0 {
147 return (0.0, 0.0);
148 }
149 let gamma = (f.abs() / (2.0 * self.eddy_viscosity_m2s)).sqrt();
150 let decay = (-gamma * altitude_m).exp();
151 let g = self.geostrophic_mps;
152 (
153 g * (1.0 - decay * (gamma * altitude_m).cos()),
154 f.signum() * g * decay * (gamma * altitude_m).sin(),
155 )
156 }
157
158 pub fn wind_at(&self, altitude_m: f64) -> Wind {
161 let (u, v) = self.components_at(altitude_m);
162 let flow_dir = self.direction_from_deg + 180.0;
165 let rot = flow_dir.to_radians();
166 let x = u * rot.cos() - v * rot.sin();
167 let z = u * rot.sin() + v * rot.cos();
168 Wind {
169 x_mps: x,
170 y_mps: 0.0,
171 z_mps: z,
172 }
173 }
174}
175
176impl WindField for EkmanProfile {
177 fn wind(&self, _time_s: f64, position_m: [f64; 3]) -> Wind {
178 self.wind_at(position_m[1])
179 }
180}