1use fusion_math::elliptic::{ellipe, ellipk};
14use fusion_types::config::CoilConfig;
15use fusion_types::error::{FusionError, FusionResult};
16use fusion_types::state::Grid2D;
17use ndarray::Array2;
18
19pub fn calculate_vacuum_field(
29 grid: &Grid2D,
30 coils: &[CoilConfig],
31 mu0: f64,
32) -> FusionResult<Array2<f64>> {
33 if !mu0.is_finite() || mu0 <= 0.0 {
34 return Err(FusionError::ConfigError(format!(
35 "vacuum permeability mu0 must be finite and > 0, got {mu0}"
36 )));
37 }
38 if grid.nz == 0 || grid.nr == 0 {
39 return Err(FusionError::ConfigError(
40 "vacuum field grid must have nz,nr >= 1".to_string(),
41 ));
42 }
43 if grid.rr.nrows() != grid.nz || grid.rr.ncols() != grid.nr {
44 return Err(FusionError::ConfigError(format!(
45 "grid.rr shape mismatch: expected ({}, {}), got ({}, {})",
46 grid.nz,
47 grid.nr,
48 grid.rr.nrows(),
49 grid.rr.ncols()
50 )));
51 }
52 if grid.zz.nrows() != grid.nz || grid.zz.ncols() != grid.nr {
53 return Err(FusionError::ConfigError(format!(
54 "grid.zz shape mismatch: expected ({}, {}), got ({}, {})",
55 grid.nz,
56 grid.nr,
57 grid.zz.nrows(),
58 grid.zz.ncols()
59 )));
60 }
61 if grid.rr.iter().any(|v| !v.is_finite()) || grid.zz.iter().any(|v| !v.is_finite()) {
62 return Err(FusionError::ConfigError(
63 "vacuum field grid coordinates must be finite".to_string(),
64 ));
65 }
66
67 let nz = grid.nz;
68 let nr = grid.nr;
69 let mut psi_vac: Array2<f64> = Array2::zeros((nz, nr));
70
71 for coil in coils {
72 let rc = coil.r;
73 let zc = coil.z;
74 let current = coil.current;
75 if !rc.is_finite() || rc <= 0.0 {
76 return Err(FusionError::ConfigError(format!(
77 "coil radius must be finite and > 0, got {}",
78 coil.r
79 )));
80 }
81 if !zc.is_finite() || !current.is_finite() {
82 return Err(FusionError::ConfigError(
83 "coil z/current must be finite".to_string(),
84 ));
85 }
86
87 let prefactor = (mu0 * current) / (2.0 * std::f64::consts::PI);
88 if !prefactor.is_finite() {
89 return Err(FusionError::ConfigError(
90 "coil prefactor became non-finite".to_string(),
91 ));
92 }
93
94 for iz in 0..nz {
95 for ir in 0..nr {
96 let r = grid.rr[[iz, ir]];
97 let z = grid.zz[[iz, ir]];
98
99 let dz = z - zc;
100 let r_plus_rc = r + rc;
101 let r_plus_rc_sq = r_plus_rc * r_plus_rc;
102 let denom = r_plus_rc_sq + dz * dz;
103 if !denom.is_finite() || denom <= 0.0 {
104 return Err(FusionError::ConfigError(
105 "vacuum field denominator became non-finite or non-positive".to_string(),
106 ));
107 }
108
109 let k2_raw = (4.0 * r * rc) / denom;
111 if !k2_raw.is_finite() {
112 return Err(FusionError::ConfigError(
113 "vacuum field k^2 became non-finite".to_string(),
114 ));
115 }
116 let mut k2 = k2_raw;
117 k2 = k2.clamp(1e-9, 0.999999);
119
120 let k_val = ellipk(k2);
121 let e_val = ellipe(k2);
122 if !k_val.is_finite() || !e_val.is_finite() {
123 return Err(FusionError::ConfigError(
124 "vacuum field elliptic integrals became non-finite".to_string(),
125 ));
126 }
127
128 let sqrt_term = (r * rc).sqrt();
129 let k = k2.sqrt();
130 let term = ((2.0 - k2) * k_val - 2.0 * e_val) / k;
131 let coil_flux = prefactor * sqrt_term * term;
132 if !coil_flux.is_finite() {
133 return Err(FusionError::ConfigError(
134 "vacuum field coil flux contribution became non-finite".to_string(),
135 ));
136 }
137
138 psi_vac[[iz, ir]] += coil_flux;
139 }
140 }
141 }
142
143 if psi_vac.iter().any(|v| !v.is_finite()) {
144 return Err(FusionError::ConfigError(
145 "vacuum field result contains non-finite values".to_string(),
146 ));
147 }
148
149 Ok(psi_vac)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use fusion_types::config::ReactorConfig;
156 use std::path::PathBuf;
157
158 fn project_root() -> PathBuf {
159 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
160 .join("..")
161 .join("..")
162 .join("..")
163 }
164
165 fn config_path(relative: &str) -> String {
166 project_root().join(relative).to_string_lossy().to_string()
167 }
168
169 #[test]
170 fn test_vacuum_field_not_nan() {
171 let cfg = ReactorConfig::from_file(&config_path("iter_config.json")).unwrap();
172 let grid = cfg.create_grid();
173 let psi_vac = calculate_vacuum_field(&grid, &cfg.coils, cfg.physics.vacuum_permeability)
174 .expect("valid vacuum-field inputs");
175
176 assert!(
178 !psi_vac.iter().any(|v| v.is_nan()),
179 "Vacuum field contains NaN"
180 );
181
182 let max_val = psi_vac.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
184 assert!(max_val.abs() > 1e-10, "Vacuum field is all zeros");
185 }
186
187 #[test]
188 fn test_vacuum_field_symmetry() {
189 let grid = Grid2D::new(33, 33, 1.0, 9.0, -5.0, 5.0);
191 let coil = CoilConfig {
192 name: "test".to_string(),
193 r: 5.0,
194 z: 0.0,
195 current: 1.0,
196 };
197 let psi = calculate_vacuum_field(&grid, &[coil], 1.0).expect("valid vacuum-field inputs");
198
199 for ir in 0..33 {
201 for iz in 0..16 {
202 let diff = (psi[[iz, ir]] - psi[[32 - iz, ir]]).abs();
203 assert!(
204 diff < 1e-10,
205 "Symmetry broken at iz={iz}, ir={ir}: {} vs {}",
206 psi[[iz, ir]],
207 psi[[32 - iz, ir]]
208 );
209 }
210 }
211 }
212
213 #[test]
214 fn test_vacuum_field_single_coil_shape() {
215 let grid = Grid2D::new(33, 33, 1.0, 9.0, -5.0, 5.0);
217 let coil = CoilConfig {
218 name: "test".to_string(),
219 r: 5.0,
220 z: 0.0,
221 current: 1.0,
222 };
223 let psi = calculate_vacuum_field(&grid, &[coil], 1.0).expect("valid vacuum-field inputs");
224
225 let mut max_val = f64::NEG_INFINITY;
227 let mut max_iz = 0;
228 let mut max_ir = 0;
229 for iz in 0..33 {
230 for ir in 0..33 {
231 if psi[[iz, ir]] > max_val {
232 max_val = psi[[iz, ir]];
233 max_iz = iz;
234 max_ir = ir;
235 }
236 }
237 }
238
239 assert!(
241 (max_iz as f64 - 16.0).abs() < 3.0,
242 "Peak not near Z=0: iz={max_iz}"
243 );
244 assert!(
246 (max_ir as f64 - 16.0).abs() < 3.0,
247 "Peak not near R=5: ir={max_ir}"
248 );
249 }
250
251 #[test]
252 fn test_vacuum_field_rejects_invalid_runtime_inputs() {
253 let grid = Grid2D::new(17, 17, 1.0, 9.0, -5.0, 5.0);
254 let bad_coil = CoilConfig {
255 name: "bad".to_string(),
256 r: 0.0,
257 z: 0.0,
258 current: 1.0,
259 };
260 assert!(calculate_vacuum_field(&grid, &[bad_coil], 1.0).is_err());
261 assert!(calculate_vacuum_field(&grid, &[], f64::NAN).is_err());
262 }
263}