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