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