1use fusion_types::constants::MU0_SI;
13use fusion_types::error::{FusionError, FusionResult};
14
15const Q_D: f64 = 1.602e-19;
17
18const M_D: f64 = 3.34e-27;
20
21const FREQ_HZ: f64 = 50e6;
23
24const OMEGA_WAVE: f64 = 2.0 * std::f64::consts::PI * FREQ_HZ;
26
27const B0: f64 = 5.3;
29
30const R0: f64 = 6.2;
32
33const N_E_PEAK: f64 = 1.0e20;
35
36const DENSITY_WIDTH: f64 = 2.0;
38
39const GRAD_EPS: f64 = 1e-3;
41
42const K0_INITIAL: f64 = 10.0;
44
45const N_RAYS: usize = 10;
47
48const R_ANTENNA: f64 = 9.0;
50
51const RESONANCE_TOL: f64 = 0.1;
53
54pub type RayState = [f64; 4];
56
57#[derive(Debug, Clone)]
59pub struct RayTraceResult {
60 pub trajectory: Vec<(f64, f64)>,
62 pub resonance_point: Option<(f64, f64)>,
64 pub r_resonance: f64,
66}
67
68pub struct RFHeatingSystem {
70 pub omega: f64,
71 pub b0: f64,
72 pub r0: f64,
73}
74
75impl RFHeatingSystem {
76 pub fn new() -> Self {
78 RFHeatingSystem {
79 omega: OMEGA_WAVE,
80 b0: B0,
81 r0: R0,
82 }
83 }
84
85 fn validate_runtime_parameters(&self) -> FusionResult<()> {
86 if !self.omega.is_finite() || self.omega <= 0.0 {
87 return Err(FusionError::ConfigError(format!(
88 "RF omega must be finite and > 0, got {}",
89 self.omega
90 )));
91 }
92 if !self.b0.is_finite() || self.b0 <= 0.0 {
93 return Err(FusionError::ConfigError(format!(
94 "RF b0 must be finite and > 0, got {}",
95 self.b0
96 )));
97 }
98 if !self.r0.is_finite() || self.r0 <= 0.0 {
99 return Err(FusionError::ConfigError(format!(
100 "RF r0 must be finite and > 0, got {}",
101 self.r0
102 )));
103 }
104 Ok(())
105 }
106
107 fn b_field(&self, r: f64) -> f64 {
109 self.b0 * self.r0 / r
110 }
111
112 fn density(&self, r: f64, z: f64) -> f64 {
114 let dist_sq = (r - self.r0).powi(2) + z.powi(2);
115 N_E_PEAK * (-dist_sq / DENSITY_WIDTH).exp()
116 }
117
118 fn alfven_speed(&self, r: f64, z: f64) -> f64 {
120 let b = self.b_field(r);
121 let n = self.density(r, z).max(1e10); b / (MU0_SI * n * M_D).sqrt()
123 }
124
125 fn dispersion(&self, r: f64, z: f64, kr: f64, kz: f64) -> f64 {
127 let k_sq = kr * kr + kz * kz;
128 let v_a = self.alfven_speed(r, z);
129 k_sq * v_a * v_a - self.omega * self.omega
130 }
131
132 fn ray_rhs(&self, state: &RayState) -> RayState {
137 let [r, z, kr, kz] = *state;
138 let eps = GRAD_EPS;
139
140 let dd_dkr = (self.dispersion(r, z, kr + eps, kz) - self.dispersion(r, z, kr - eps, kz))
142 / (2.0 * eps);
143 let dd_dkz = (self.dispersion(r, z, kr, kz + eps) - self.dispersion(r, z, kr, kz - eps))
144 / (2.0 * eps);
145
146 let dd_dr = (self.dispersion(r + eps, z, kr, kz) - self.dispersion(r - eps, z, kr, kz))
148 / (2.0 * eps);
149 let dd_dz = (self.dispersion(r, z + eps, kr, kz) - self.dispersion(r, z - eps, kr, kz))
150 / (2.0 * eps);
151
152 [
153 -dd_dkr, -dd_dkz, dd_dr, dd_dz, ]
158 }
159
160 fn rk4_step(&self, state: &RayState, dt: f64) -> RayState {
165 let k1 = self.ray_rhs(state);
166
167 let s2 = [
168 state[0] + 0.5 * dt * k1[0],
169 state[1] + 0.5 * dt * k1[1],
170 state[2] + 0.5 * dt * k1[2],
171 state[3] + 0.5 * dt * k1[3],
172 ];
173 let k2 = self.ray_rhs(&s2);
174
175 let s3 = [
176 state[0] + 0.5 * dt * k2[0],
177 state[1] + 0.5 * dt * k2[1],
178 state[2] + 0.5 * dt * k2[2],
179 state[3] + 0.5 * dt * k2[3],
180 ];
181 let k3 = self.ray_rhs(&s3);
182
183 let s4 = [
184 state[0] + dt * k3[0],
185 state[1] + dt * k3[1],
186 state[2] + dt * k3[2],
187 state[3] + dt * k3[3],
188 ];
189 let k4 = self.ray_rhs(&s4);
190
191 [
192 state[0] + (dt / 6.0) * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]),
193 state[1] + (dt / 6.0) * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1]),
194 state[2] + (dt / 6.0) * (k1[2] + 2.0 * k2[2] + 2.0 * k3[2] + k4[2]),
195 state[3] + (dt / 6.0) * (k1[3] + 2.0 * k2[3] + 2.0 * k3[3] + k4[3]),
196 ]
197 }
198
199 pub fn resonance_radius(&self) -> FusionResult<f64> {
203 self.validate_runtime_parameters()?;
204 let b_res = self.omega * M_D / Q_D;
205 if !b_res.is_finite() || b_res <= 0.0 {
206 return Err(FusionError::ConfigError(
207 "RF resonance field must be finite and > 0".to_string(),
208 ));
209 }
210 let r_res = self.b0 * self.r0 / b_res;
211 if !r_res.is_finite() || r_res <= 0.0 {
212 return Err(FusionError::ConfigError(
213 "RF resonance radius became non-finite or non-positive".to_string(),
214 ));
215 }
216 Ok(r_res)
217 }
218
219 pub fn trace_single_ray(
224 &self,
225 r0: f64,
226 z0: f64,
227 kr0: f64,
228 kz0: f64,
229 n_steps: usize,
230 _dt_hint: f64,
231 ) -> FusionResult<RayTraceResult> {
232 self.validate_runtime_parameters()?;
233 if !r0.is_finite() || r0 <= 0.0 || !z0.is_finite() || !kr0.is_finite() || !kz0.is_finite() {
234 return Err(FusionError::ConfigError(
235 "RF ray initial conditions must be finite with r0 > 0".to_string(),
236 ));
237 }
238 if n_steps == 0 {
239 return Err(FusionError::ConfigError(
240 "RF ray tracing requires n_steps >= 1".to_string(),
241 ));
242 }
243
244 let r_res = self.resonance_radius()?;
245 let mut state: RayState = [r0, z0, kr0, kz0];
246 let mut trajectory = Vec::with_capacity(n_steps);
247 let mut resonance_point = None;
248
249 const MAX_DISP: f64 = 0.05;
251
252 for _ in 0..n_steps {
253 if state.iter().any(|v| !v.is_finite()) {
254 return Err(FusionError::ConfigError(
255 "RF ray state became non-finite".to_string(),
256 ));
257 }
258 trajectory.push((state[0], state[1]));
259
260 if resonance_point.is_none() && (state[0] - r_res).abs() < RESONANCE_TOL {
262 resonance_point = Some((state[0], state[1]));
263 }
264
265 let rhs = self.ray_rhs(&state);
267 if rhs.iter().any(|v| !v.is_finite()) {
268 return Err(FusionError::ConfigError(
269 "RF ray RHS became non-finite".to_string(),
270 ));
271 }
272 let v_mag = (rhs[0] * rhs[0] + rhs[1] * rhs[1]).sqrt();
273 if !v_mag.is_finite() {
274 return Err(FusionError::ConfigError(
275 "RF ray velocity magnitude became non-finite".to_string(),
276 ));
277 }
278 let dt = if v_mag > 1e-10 {
279 MAX_DISP / v_mag
280 } else {
281 1e-3
282 };
283 if !dt.is_finite() || dt <= 0.0 {
284 return Err(FusionError::ConfigError(
285 "RF adaptive timestep became invalid".to_string(),
286 ));
287 }
288
289 state = self.rk4_step(&state, dt);
290
291 if state[0] < 0.5 || state[0] > 15.0 || state[1].abs() > 10.0 {
293 break;
294 }
295 }
296
297 Ok(RayTraceResult {
298 trajectory,
299 resonance_point,
300 r_resonance: r_res,
301 })
302 }
303
304 pub fn trace_rays(&self) -> FusionResult<Vec<RayTraceResult>> {
306 self.validate_runtime_parameters()?;
307 let n_steps = 500;
308 let dt = 0.0; let mut results = Vec::with_capacity(N_RAYS);
311 for i in 0..N_RAYS {
312 let z_antenna = -1.0 + 2.0 * (i as f64) / ((N_RAYS - 1) as f64);
313 let result =
314 self.trace_single_ray(R_ANTENNA, z_antenna, -K0_INITIAL, 0.0, n_steps, dt)?;
315 results.push(result);
316 }
317 Ok(results)
318 }
319}
320
321impl Default for RFHeatingSystem {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_resonance_radius() {
333 let rf = RFHeatingSystem::new();
334 let r_res = rf
335 .resonance_radius()
336 .expect("valid RF resonance-radius parameters");
337 assert!(r_res > 3.0 && r_res < 8.0, "Resonance radius: {r_res}");
341 }
342
343 #[test]
344 fn test_trace_single_ray() {
345 let rf = RFHeatingSystem::new();
346 let result = rf
347 .trace_single_ray(R_ANTENNA, 0.0, -K0_INITIAL, 0.0, 500, 0.0)
348 .expect("valid RF single-ray trace parameters");
349
350 assert!(result.trajectory.len() > 1, "Should have trajectory points");
352
353 let (r0, _z0) = result.trajectory[0];
355 assert!((r0 - R_ANTENNA).abs() < 1e-10, "Should start at antenna");
356 }
357
358 #[test]
359 fn test_trace_rays_array() {
360 let rf = RFHeatingSystem::new();
361 let results = rf
362 .trace_rays()
363 .expect("valid RF multi-ray trace parameters");
364 assert_eq!(results.len(), N_RAYS);
365 for result in &results {
366 assert!(!result.trajectory.is_empty());
367 }
368 }
369
370 #[test]
371 fn test_alfven_speed_positive() {
372 let rf = RFHeatingSystem::new();
373 let v_a = rf.alfven_speed(6.2, 0.0);
374 assert!(
375 v_a > 0.0 && v_a.is_finite(),
376 "v_A should be positive: {v_a}"
377 );
378 }
379
380 #[test]
381 fn test_rf_heating_rejects_invalid_runtime_inputs() {
382 let mut rf = RFHeatingSystem::new();
383 rf.b0 = f64::NAN;
384 assert!(rf.resonance_radius().is_err());
385 assert!(rf.trace_rays().is_err());
386
387 let rf = RFHeatingSystem::new();
388 assert!(rf
389 .trace_single_ray(0.0, 0.0, -K0_INITIAL, 0.0, 500, 0.0)
390 .is_err());
391 assert!(rf
392 .trace_single_ray(R_ANTENNA, 0.0, -K0_INITIAL, 0.0, 0, 0.0)
393 .is_err());
394 }
395}