1use fusion_math::interp::gradient_2d;
13use fusion_types::error::{FusionError, FusionResult};
14use fusion_types::state::Grid2D;
15use ndarray::Array2;
16
17pub fn find_x_point(
32 psi: &Array2<f64>,
33 grid: &Grid2D,
34 z_min: f64,
35) -> FusionResult<((f64, f64), f64)> {
36 if !z_min.is_finite() {
37 return Err(FusionError::ConfigError(format!(
38 "x-point z_min must be finite, got {z_min}"
39 )));
40 }
41 if grid.nz < 2 || grid.nr < 2 {
42 return Err(FusionError::ConfigError(format!(
43 "x-point grid requires nz,nr >= 2, got nz={} nr={}",
44 grid.nz, grid.nr
45 )));
46 }
47 if psi.nrows() != grid.nz || psi.ncols() != grid.nr {
48 return Err(FusionError::ConfigError(format!(
49 "x-point psi shape mismatch: expected ({}, {}), got ({}, {})",
50 grid.nz,
51 grid.nr,
52 psi.nrows(),
53 psi.ncols()
54 )));
55 }
56 if psi.iter().any(|v| !v.is_finite()) {
57 return Err(FusionError::ConfigError(
58 "x-point psi contains non-finite values".to_string(),
59 ));
60 }
61 if grid.z.iter().any(|v| !v.is_finite()) || grid.r.iter().any(|v| !v.is_finite()) {
62 return Err(FusionError::ConfigError(
63 "x-point grid axes must be finite".to_string(),
64 ));
65 }
66
67 let (dpsi_dz, dpsi_dr) = gradient_2d(psi, grid);
69
70 let nz = grid.nz;
71 let nr = grid.nr;
72
73 let mut min_b_mag = f64::MAX;
76 let mut best_iz = 0;
77 let mut best_ir = 0;
78 let mut found = false;
79
80 let z_threshold = z_min * 0.5;
81
82 for iz in 0..nz {
83 for ir in 0..nr {
84 let z = grid.zz[[iz, ir]];
85
86 if z < z_threshold {
88 let b_mag_sq = dpsi_dz[[iz, ir]].powi(2) + dpsi_dr[[iz, ir]].powi(2);
89 if !b_mag_sq.is_finite() || b_mag_sq < 0.0 {
90 return Err(FusionError::ConfigError(format!(
91 "x-point gradient magnitude became invalid at ({iz}, {ir})"
92 )));
93 }
94 let b_mag = b_mag_sq.sqrt();
95
96 if b_mag < min_b_mag {
97 min_b_mag = b_mag;
98 best_iz = iz;
99 best_ir = ir;
100 found = true;
101 }
102 }
103 }
104 }
105
106 if !found {
107 return Err(FusionError::ConfigError(
108 "x-point search region is empty for provided z_min threshold".to_string(),
109 ));
110 }
111
112 let r = grid.r[best_ir];
113 let z = grid.z[best_iz];
114 let psi_x = psi[[best_iz, best_ir]];
115 if !r.is_finite() || !z.is_finite() || !psi_x.is_finite() {
116 return Err(FusionError::ConfigError(
117 "x-point output contains non-finite values".to_string(),
118 ));
119 }
120 Ok(((r, z), psi_x))
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_find_x_point_basic() {
129 let grid = Grid2D::new(33, 33, 1.0, 9.0, -5.0, 5.0);
130 let psi = Array2::from_shape_fn((33, 33), |(iz, ir)| {
132 let r = grid.rr[[iz, ir]];
133 let z = grid.zz[[iz, ir]];
134 (r - 5.0).powi(2) - (z + 2.5).powi(2)
136 });
137
138 let ((r_x, z_x), _psi_x) = find_x_point(&psi, &grid, -5.0).expect("valid x-point inputs");
139
140 assert!(
142 (r_x - 5.0).abs() < 1.0,
143 "X-point R={r_x}, expected near 5.0"
144 );
145 assert!(
146 (z_x + 2.5).abs() < 1.0,
147 "X-point Z={z_x}, expected near -2.5"
148 );
149 }
150
151 #[test]
152 fn test_find_x_point_rejects_invalid_runtime_inputs() {
153 let grid = Grid2D::new(33, 33, 1.0, 9.0, -5.0, 5.0);
154 let psi = Array2::from_shape_fn((33, 33), |(iz, ir)| {
155 let r = grid.rr[[iz, ir]];
156 let z = grid.zz[[iz, ir]];
157 (r - 5.0).powi(2) - (z + 2.5).powi(2)
158 });
159
160 let err = find_x_point(&psi, &grid, f64::NAN).expect_err("non-finite z_min must fail");
161 assert!(matches!(err, FusionError::ConfigError(_)));
162
163 let err =
164 find_x_point(&psi, &grid, -1000.0).expect_err("empty divertor search region must fail");
165 assert!(matches!(err, FusionError::ConfigError(_)));
166 }
167}