Coverage for python/gsfit/database_writers/rtgsfit_mdsplus/map_results_to_database.py: 0%

303 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-14 15:25 +0000

1import copy 

2from typing import TYPE_CHECKING 

3 

4import numpy as np 

5import numpy.typing as npt 

6import shapely.geometry 

7from gsfit_rs.imas import equilibrium_paths as ep 

8from gsfit_rs.imas import wall_paths as wp 

9from scipy.constants import mu_0 

10 

11from .greens_with_boundary_points import greens_with_boundary_points 

12from .poisson_matrix import compute_lup_bands 

13 

14if TYPE_CHECKING: 

15 from ...gsfit import Gsfit 

16 from . import DatabaseWriterRTGSFitMDSplus 

17 

18 

19def map_results_to_database(self: "DatabaseWriterRTGSFitMDSplus", gsfit_controller: "Gsfit") -> None: 

20 """ 

21 Map the results to MDSplus structure. 

22 

23 `gsfit_controller.results` is a `NestedDict` object, which has a 1:1 mapping to the MDSplus structure. 

24 

25 This function mutates the `gsfit_controller` object. 

26 """ 

27 

28 print("rtgsfit_mdsplus running") 

29 

30 # TODO: move this to a *.json file. This format hasn't yet settled down... 

31 # TODO: alphabetical order 

32 rtgsfit_psus = [ 

33 {"power_supply_name": "SOL", "coils": ["SOL"]}, 

34 {"power_supply_name": "MCT", "coils": ["MCT"]}, 

35 {"power_supply_name": "MCB", "coils": ["MCB"]}, 

36 {"power_supply_name": "DIV", "coils": ["DIVT", "DIVB"]}, 

37 {"power_supply_name": "BVL", "coils": ["BVLT", "BVLB"]}, 

38 {"power_supply_name": "BVUT", "coils": ["BVUT"]}, 

39 {"power_supply_name": "BVUB", "coils": ["BVUB"]}, 

40 {"power_supply_name": "PSH", "coils": ["PSHT", "PSHB"]}, 

41 ] 

42 

43 # Get objects out of `gsfit_controller` 

44 plasma = gsfit_controller.plasma 

45 passives = gsfit_controller.passives 

46 flux_loops = gsfit_controller.flux_loops 

47 bp_probes = gsfit_controller.bp_probes 

48 rogowski_coils = gsfit_controller.rogowski_coils 

49 results = gsfit_controller.results 

50 

51 wall = gsfit_controller.wall 

52 

53 # Geometry. `profiles_2d(0)` because GSFit solves on a single rectangular (R, Z) grid 

54 r = plasma.get(ep.time_slice[0].profiles_2d[0].grid.dim1) 

55 z = plasma.get(ep.time_slice[0].profiles_2d[0].grid.dim2) 

56 n_z = len(z) 

57 n_r = len(r) 

58 d_r = np.mean(r[1:] - r[0:-1]) 

59 d_z = np.mean(z[1:] - z[0:-1]) 

60 

61 # Number of power supplies 

62 n_psu = len(rtgsfit_psus) 

63 

64 # Store geomery 

65 results["PRESHOT"]["DR"] = d_r 

66 results["PRESHOT"]["DZ"] = d_z 

67 results["PRESHOT"]["R_VEC"] = r 

68 results["PRESHOT"]["Z_VEC"] = z 

69 results["PRESHOT"]["N_R"] = np.int32(n_r) 

70 results["PRESHOT"]["N_Z"] = np.int32(n_z) 

71 

72 r_grid, z_grid = np.meshgrid(r, z) 

73 r_flat = r_grid.flatten() 

74 z_flat = z_grid.flatten() 

75 results["PRESHOT"]["R_GRID"] = r_flat 

76 results["PRESHOT"]["Z_GRID"] = z_flat 

77 inv_r_mu0 = 1.0 / (r_flat * mu_0) 

78 results["PRESHOT"]["INV_R_MU0"] = inv_r_mu0 

79 r_mu0_dz2 = 2 * np.pi * r_flat * mu_0 * d_z**2 

80 results["PRESHOT"]["R_MU0_DZ2"] = r_mu0_dz2 

81 results["PRESHOT"]["N_COIL"] = np.int32(n_psu) 

82 n_grid = n_r * n_z 

83 results["PRESHOT"]["N_GRID"] = np.int32(n_grid) 

84 n_ltrb = 2 * n_r + 2 * n_z - 4 # Number of points on the boundary, removing the double counting at the 4 corners 

85 results["PRESHOT"]["N_LTRB"] = np.int32(n_ltrb) 

86 

87 # Collect the greens for "grid-coils" 

88 g_grid_coil = np.zeros((n_z, n_r, n_psu)) 

89 # The Green's tables are an array of structures, so the coil name is looked up once to give an 

90 # index to read by 

91 pf_active_names: list[str] = plasma.get(ep.greens.pf_active[:].name).tolist() 

92 psu_names = [] 

93 for i_psu, power_supply in enumerate(rtgsfit_psus): 

94 psu_names.append(power_supply["power_supply_name"]) 

95 coil_names = power_supply["coils"] 

96 for coil_name in coil_names: 

97 i_coil = pf_active_names.index(coil_name) 

98 g_grid_coil[:, :, i_psu] += plasma.get(ep.greens.pf_active[i_coil].psi) 

99 

100 # Store in MDSplus 

101 results["PRESHOT"]["GREENS"]["GRID_COIL"] = g_grid_coil.flatten() 

102 results["PRESHOT"]["COIL_NAMES"] = np.array(psu_names) 

103 

104 # Get the "included" sensors 

105 flux_loops_to_include = flux_loops.get_vec_bool(["*", "fit_settings", "include"]) 

106 n_flux_loops_to_include: int = sum(flux_loops_to_include) 

107 bp_probes_to_include = bp_probes.get_vec_bool(["*", "fit_settings", "include"]) 

108 n_bp_probes_to_include: int = sum(bp_probes_to_include) 

109 rogowski_coils_to_include = rogowski_coils.get_vec_bool(["*", "fit_settings", "include"]) 

110 n_rogowski_coils_to_include: int = sum(rogowski_coils_to_include) 

111 

112 # Count the number of passive degrees of freedom, and regularisations 

113 n_passive_dofs = 0 

114 n_regularisations = 0 

115 passive_names = passives.keys() # Note: this includes the IVC 

116 print(passive_names) 

117 # passive_names = ["IVC", "OVC", "BVLTCASE", "BVLBCASE", "DIVPSRT", "DIVPSRB", "HFSPSRT", "HFSPSRB"] # TODO: TEMPORARY while debugging 

118 for passive_name in passive_names: 

119 # The data structure looks like this: 

120 # passives(["BVLBCASE", "dof", "constant_current_density"]) 

121 # passives(["BVLTCASE", "dof", "constant_current_density"]) 

122 # ... 

123 # passives(["IVC", "dof", "eig_01"]) 

124 # passives(["IVC", "dof", "eig_02"]) 

125 # ... 

126 n_passive_dofs += len(passives.keys([passive_name, "dof"])) 

127 

128 # The data structure looks like this: 

129 # passives(["BVLBCASE", "regularisations"]) # shape = [n_regularisations, n_dof] 

130 # passives(["BVLTCASE", "regularisations"]) 

131 # ... 

132 # passives(["IVC", "regularisations"]) 

133 # ... 

134 [n_regularisations_local, _] = passives.get_array2([passive_name, "regularisations"]).shape 

135 n_regularisations += n_regularisations_local 

136 

137 # Total number of constraints 

138 n_constraints = n_flux_loops_to_include + n_bp_probes_to_include + n_rogowski_coils_to_include + n_regularisations 

139 

140 # Store the number of constraints and regularisations 

141 results["PRESHOT"]["N_F_LOOPS"] = np.int32(n_flux_loops_to_include) 

142 results["PRESHOT"]["N_BP_PROBES"] = np.int32(n_bp_probes_to_include) 

143 results["PRESHOT"]["N_ROG_COILS"] = np.int32(n_rogowski_coils_to_include) 

144 results["PRESHOT"]["N_MEAS"] = np.int32(n_constraints) 

145 results["PRESHOT"]["N_REG"] = np.int32(n_regularisations) 

146 

147 # Store number of plasma degrees of freedom 

148 n_p_prime = len(plasma.get(ep.time_slice[0].source_functions.p_prime.coefficients)) 

149 n_ff_prime = len(plasma.get(ep.time_slice[0].source_functions.ff_prime.coefficients)) 

150 n_delta_z = 1 

151 n_plasma_dof = n_p_prime + n_ff_prime + n_delta_z 

152 results["PRESHOT"]["N_PLS"] = np.int32(n_plasma_dof) 

153 # Total number of degrees of freedom 

154 n_coef = n_passive_dofs + n_plasma_dof 

155 results["PRESHOT"]["N_COEF"] = np.int32(n_coef) 

156 

157 # Greens for "sensors-coils" 

158 g_measured_coil = np.zeros((n_constraints, n_psu)) 

159 

160 # Weights for the constraints 

161 constraints_weight = np.zeros(n_constraints) 

162 

163 # Lis of constraint names 

164 constraint_names = [] 

165 

166 # Greens between the measurements and the degrees of freedom 

167 # Note: the plasma's dof's are calculated during real-time and are set to zero. 

168 # So g_dof_meas[:, 0 : n_plasma_dof] will be zero. 

169 g_dof_meas = np.zeros((n_constraints, n_coef)) 

170 

171 # Collect the greens for "grid-measurements" 

172 g_grid_meas = np.zeros((n_constraints, n_r * n_z)) 

173 

174 # flux_loops are the first set of constraints 

175 i_constraint = 0 

176 for i_flux_loop, floop_name in enumerate(flux_loops.keys()): 

177 if flux_loops_to_include[i_flux_loop]: 

178 # Add flux loop name in PCS (Plasma Control System) format 

179 floop_name_pcs = floop_name.replace("L", "PSI_FLOOP_") 

180 constraint_names.append(floop_name_pcs) 

181 # Add the Greens between measurements and coils 

182 for i_psu, power_supply in enumerate(rtgsfit_psus): 

183 coil_names = power_supply["coils"] 

184 for coil_name in coil_names: 

185 g_measured_coil[i_constraint, i_psu] += flux_loops.get_f64([floop_name, "greens", "pf", coil_name]) 

186 # Add the weight 

187 constraints_weight[i_constraint] = ( 

188 2 * np.pi * flux_loops.get_f64([floop_name, "fit_settings", "weight"]) / flux_loops.get_f64([floop_name, "fit_settings", "expected_value"]) 

189 ) 

190 # Add the Greens between measurements and degrees of freedom 

191 i_vessel_dof = 0 

192 for passive_name in passive_names: 

193 dof_names = passives.keys([passive_name, "dof"]) 

194 for dof_name in dof_names: 

195 g_dof_meas[i_constraint, n_plasma_dof + i_vessel_dof] = flux_loops.get_f64([floop_name, "greens", "passives", passive_name, dof_name]) 

196 i_vessel_dof += 1 

197 # Greens between the sensors and the plasma grid 

198 g_grid_meas[i_constraint, :] = flux_loops.get_array1([floop_name, "greens", "plasma"]) 

199 # Set-up index for next sensor 

200 i_constraint += 1 

201 

202 # bp_probes are the second set of constraints 

203 for i_bp_probe, bp_name in enumerate(bp_probes.keys()): 

204 if bp_probes_to_include[i_bp_probe]: 

205 # Add flux loop name in PCS (Plasma Control System) format 

206 bp_name_pcs = bp_name.replace("P", "B_BPPROBE_") 

207 constraint_names.append(bp_name_pcs) 

208 # Add the Greens between measurements and coils 

209 for i_psu, power_supply in enumerate(rtgsfit_psus): 

210 coil_names = power_supply["coils"] 

211 for coil_name in coil_names: 

212 g_measured_coil[i_constraint, i_psu] += bp_probes.get_f64([bp_name, "greens", "pf", coil_name]) 

213 # Add the weight 

214 constraints_weight[i_constraint] = bp_probes.get_f64([bp_name, "fit_settings", "weight"]) / bp_probes.get_f64( 

215 [bp_name, "fit_settings", "expected_value"] 

216 ) 

217 # Add the Greens between measurements and degrees of freedom 

218 i_vessel_dof = 0 

219 for passive_name in passive_names: 

220 dof_names = passives.keys([passive_name, "dof"]) 

221 for dof_name in dof_names: 

222 g_dof_meas[i_constraint, n_plasma_dof + i_vessel_dof] = bp_probes.get_f64([bp_name, "greens", "passives", passive_name, dof_name]) 

223 i_vessel_dof += 1 

224 # Greens between the sensors and the plasma grid 

225 g_grid_meas[i_constraint, :] = bp_probes.get_array1([bp_name, "greens", "plasma"]) 

226 # Set-up index for next sensor 

227 i_constraint += 1 

228 

229 rogowski_coils_names_rtgsfit_order = [ 

230 "INIVC000", 

231 "BVLT", 

232 "BVLB", 

233 "GASBFLT", 

234 "GASBFLB", 

235 "HFSPSRT", 

236 "HFSPSRB", 

237 "DIVPSRT", 

238 "DIVPSRB", 

239 ] 

240 

241 # rogowski_coils are the third set of constraints 

242 # for i_rogowski_coil, rogowski_coil_name in enumerate(rogowski_coils.keys()): 

243 for i_rogowski_coil, rogowski_coil_name in enumerate(rogowski_coils_names_rtgsfit_order): 

244 # if rogowski_coils_to_include[i_rogowski_coil]: 

245 if rogowski_coils.get_vec_bool([rogowski_coil_name, "fit_settings", "include"]): 

246 # Add Rogowski coil name in PCS (Plasma Control System) format 

247 rogowski_coil_name_pcs = f"I_ROG_{rogowski_coil_name}" 

248 constraint_names.append(rogowski_coil_name_pcs) 

249 # Add the Greens between measurements and coils 

250 for i_psu, power_supply in enumerate(rtgsfit_psus): 

251 coil_names = power_supply["coils"] 

252 for coil_name in coil_names: 

253 g_measured_coil[i_constraint, i_psu] += rogowski_coils.get_f64([rogowski_coil_name, "greens", "pf", coil_name]) 

254 # Add the weight 

255 constraints_weight[i_constraint] = rogowski_coils.get_f64([rogowski_coil_name, "fit_settings", "weight"]) / rogowski_coils.get_f64( 

256 [rogowski_coil_name, "fit_settings", "expected_value"] 

257 ) 

258 # Add the Greens between measurements and degrees of freedom 

259 i_vessel_dof = 0 

260 for passive_name in passive_names: 

261 dof_names = passives.keys([passive_name, "dof"]) 

262 for dof_name in dof_names: 

263 g_dof_meas[i_constraint, n_plasma_dof + i_vessel_dof] = rogowski_coils.get_f64( 

264 [rogowski_coil_name, "greens", "passives", passive_name, dof_name] 

265 ) 

266 i_vessel_dof += 1 

267 # Greens between the sensors and the plasma grid 

268 g_grid_meas[i_constraint, :] = rogowski_coils.get_array1([rogowski_coil_name, "greens", "plasma"]) 

269 # Set-up index for next sensor 

270 i_constraint += 1 

271 

272 # "passive regularisations" are the fourth set of constraints 

273 # Loop over all passives and add regularisations if they exist 

274 i_dof_start = n_plasma_dof 

275 for passive_name in passive_names: 

276 # Find the number of degrees of freedom for this passive 

277 n_dof_local = len(passives.keys([passive_name, "dof"])) 

278 

279 passive_regularisation_local = passives.get_array2([passive_name, "regularisations"]) 

280 [n_reg_local, _] = passive_regularisation_local.shape 

281 

282 i_dof_end = i_dof_start + n_dof_local 

283 

284 for i_reg in range(n_reg_local): 

285 g_dof_meas[i_constraint, i_dof_start:i_dof_end] = passive_regularisation_local[i_reg, :] 

286 regularisation_scaling = 0.001 * np.pi 

287 constraints_weight[i_constraint] = regularisation_scaling * passives.get_array1([passive_name, "regularisations_weight"])[i_reg] 

288 

289 # Set-up index for next sensor / constraint 

290 i_constraint += 1 

291 

292 # Set-up index for next passive 

293 i_dof_start += n_dof_local 

294 

295 # Store in MDSplus 

296 results["PRESHOT"]["GREENS"]["MEAS_COIL"] = g_measured_coil.flatten() 

297 results["PRESHOT"]["WEIGHT"] = constraints_weight 

298 g_dof_meas_weight = np.dot(np.diag(constraints_weight), g_dof_meas).T 

299 results["PRESHOT"]["GREENS"]["COEF_MEAS_W"] = g_dof_meas_weight.flatten() # .reshape((n_coef, n_constraints)) 

300 g_grid_meas_weight = np.dot(np.diag(constraints_weight), g_grid_meas).T * d_r * d_z 

301 results["PRESHOT"]["GREENS"]["GRID_MEAS_W"] = g_grid_meas_weight.flatten() 

302 

303 # Collect the greens for "grid-vessel" 

304 g_grid_vessel = np.zeros((n_z * n_r, n_passive_dofs)) 

305 # Loop over all passives 

306 # `greens/pf_passive` is in the same order as `passives.keys()`, and each passive's `dof` in the 

307 # same order as its own dof keys, because they were built in that order 

308 i_dof = 0 

309 for i_passive, passive_name in enumerate(passive_names): 

310 current_distribution_dof_names = passives.keys([passive_name, "dof"]) 

311 # `current_distribution_dof_names` can be "constant_current_density", "eig_01", "eig_02", etc. 

312 for i_passive_dof, _current_distribution_dof_name in enumerate(current_distribution_dof_names): 

313 g_grid_vessel[:, i_dof] = plasma.get(ep.greens.pf_passive[i_passive].dof[i_passive_dof].psi) 

314 i_dof += 1 

315 # Store in MDSplus 

316 results["PRESHOT"]["GREENS"]["GRID_VESSEL"] = g_grid_vessel.flatten() # .reshape((n_z * n_r, n_passive_dofs)) 

317 results["PRESHOT"]["N_VESS"] = np.int32(n_passive_dofs) 

318 

319 # Store some settings 

320 rtgsfit_code_settings = gsfit_controller.settings["RTGSFIT_code_settings.json"] 

321 results["PRESHOT"]["N_XPT_MAX"] = np.int32(rtgsfit_code_settings["n_xpt_max"]) 

322 results["PRESHOT"]["N_LCFS_MAX"] = np.int32(rtgsfit_code_settings["n_lcfs_max"]) 

323 results["PRESHOT"]["N_INTRP"] = np.int32(rtgsfit_code_settings["n_intrp"]) 

324 results["PRESHOT"]["THRESH"] = rtgsfit_code_settings["thresh"] 

325 results["PRESHOT"]["FRAC"] = rtgsfit_code_settings["frac"] 

326 

327 # Add initial conditions 

328 flux_norm = gsfit_controller.settings["rtgsfit_initial_conditions.json"]["flux_norm"] 

329 mask = gsfit_controller.settings["rtgsfit_initial_conditions.json"]["mask"] 

330 psi_total = gsfit_controller.settings["rtgsfit_initial_conditions.json"]["psi_total"] 

331 results["PRESHOT"]["INITIAL_COND"]["FLUX_NORM"] = np.array(flux_norm).astype(np.float64) 

332 results["PRESHOT"]["INITIAL_COND"]["MASK"] = np.array(mask).astype(np.int32) 

333 results["PRESHOT"]["INITIAL_COND"]["PSI_TOTAL"] = np.array(psi_total).astype(np.float64) 

334 

335 # Vessel 

336 # `unit(0)` is the vacuum vessel contour, which is the region the plasma is allowed to occupy 

337 vessel_r = wall.get(wp.description_2d[0].limiter.unit[0].outline.r) 

338 vessel_z = wall.get(wp.description_2d[0].limiter.unit[0].outline.z) 

339 vessel_polygon = shapely.geometry.Polygon(np.column_stack((vessel_r, vessel_z))) 

340 

341 # Test if grid-points are inside the vessel polygon 

342 grid_points = [] 

343 for i_z in range(n_z): 

344 for i_r in range(n_r): 

345 grid_points.append(shapely.geometry.Point(r[i_r], z[i_z])) 

346 mask_lim = vessel_polygon.contains(grid_points) 

347 

348 # # Ensure that the (R,Z) grid cell nearest to the vessel is included in the mask 

349 # for r_v, z_v in zip(vessel_r, vessel_z): 

350 # # Find the nearest grid-point to the vessel point 

351 # i_r_nearest = np.argmin(np.abs(r - r_v)) 

352 # i_z_nearest = np.argmin(np.abs(z - z_v)) 

353 # mask_lim[i_z_nearest * n_r + i_r_nearest] = True 

354 

355 # results["PRESHOT"]["MASK_LIM"] = mask_lim.astype(np.int32) 

356 

357 def compute_limit_idx_and_weights( 

358 r: npt.NDArray[np.float64], 

359 z: npt.NDArray[np.float64], 

360 lim_r: npt.NDArray[np.float64], 

361 lim_z: npt.NDArray[np.float64], 

362 n_intrp: int, 

363 ) -> tuple[npt.NDArray[np.int32], npt.NDArray[np.float64]]: 

364 n_r: int = len(r) 

365 n_lim: int = len(lim_r) 

366 limit_idx = np.zeros(n_lim * n_intrp, dtype=int) 

367 limit_w = np.zeros(n_lim * n_intrp, dtype=float) 

368 

369 for i, (lr, lz) in enumerate(zip(lim_r, lim_z)): 

370 if lr < r[0] or lr > r[-1] or lz < z[0] or lz > z[-1]: 

371 raise ValueError(f"Limiter point ({lr}, {lz}) is out of bounds of the grid.") 

372 r_idx = np.searchsorted(r, lr) - 1 

373 z_idx = np.searchsorted(z, lz) - 1 

374 

375 limit_idx[n_intrp * i + 0] = n_r * z_idx + r_idx # (r_idx, z_idx) 

376 limit_idx[n_intrp * i + 1] = n_r * z_idx + r_idx + 1 # (r_idx + 1, z_idx) 

377 limit_idx[n_intrp * i + 2] = n_r * (z_idx + 1) + r_idx # (r_idx, z_idx + 1) 

378 limit_idx[n_intrp * i + 3] = n_r * (z_idx + 1) + r_idx + 1 # (r_idx + 1, z_idx + 1) 

379 

380 r0, r1 = r[r_idx], r[r_idx + 1] 

381 z0, z1 = z[z_idx], z[z_idx + 1] 

382 dr = r1 - r0 

383 dz = z1 - z0 

384 limit_w[n_intrp * i + 0] = (r1 - lr) * (z1 - lz) / (dr * dz) # (r_idx, z_idx) 

385 limit_w[n_intrp * i + 1] = (lr - r0) * (z1 - lz) / (dr * dz) # (r_idx + 1, z_idx) 

386 limit_w[n_intrp * i + 2] = (r1 - lr) * (lz - z0) / (dr * dz) # (r_idx, z_idx + 1) 

387 limit_w[n_intrp * i + 3] = (lr - r0) * (lz - z0) / (dr * dz) # (r_idx + 1, z_idx + 1) 

388 

389 return limit_idx, limit_w 

390 

391 n_intrp: int = int(rtgsfit_code_settings["n_intrp"]) 

392 # Every limiter unit contributes candidate limit points, so they are concatenated 

393 n_limiter_unit = len(wall.get(wp.description_2d[0].limiter.unit[:].name)) 

394 lim_r = np.concatenate([wall.get(wp.description_2d[0].limiter.unit[i].outline.r) for i in range(n_limiter_unit)]) 

395 lim_z = np.concatenate([wall.get(wp.description_2d[0].limiter.unit[i].outline.z) for i in range(n_limiter_unit)]) 

396 # Remove indices where |lim_z| > 0.7 m 

397 # lim_r = lim_r[np.abs(lim_z) < 0.7] 

398 # lim_z = lim_z[np.abs(lim_z) < 0.7] 

399 n_lim = len(lim_r) 

400 limit_idx, limit_w = compute_limit_idx_and_weights(r, z, lim_r, lim_z, n_intrp) 

401 results["PRESHOT"]["N_LIMIT"] = np.int32(n_lim) 

402 results["PRESHOT"]["LIMIT_IDX"] = limit_idx.astype(np.int32) 

403 results["PRESHOT"]["LIMIT_W"] = limit_w.astype(np.float64) 

404 results["PRESHOT"]["LIMIT_R"] = lim_r.astype(np.float64) 

405 results["PRESHOT"]["LIMIT_Z"] = lim_z.astype(np.float64) 

406 

407 # If grid point is within sqrt(d_r^2 + d_z^2) of 

408 # a limiter point then include it in the mask_lim 

409 d_rz = np.sqrt(d_r**2 + d_z**2) / 2 

410 for i, (lr, lz) in enumerate(zip(lim_r, lim_z)): 

411 points_near_lim = np.sqrt((lr - r_flat) ** 2 + (lz - z_flat) ** 2) < d_rz 

412 mask_lim = np.logical_or(mask_lim, points_near_lim) 

413 

414 results["PRESHOT"]["MASK_LIM"] = mask_lim.astype(np.int32) 

415 

416 r_ltrb = np.concatenate( 

417 ( 

418 [r[0]], # (bottom, left) 

419 r[0] * np.ones(len(z[1:-1])), # traverse (bottom, left) to (top, left) (excluding corners) 

420 [r[0]], # (top, left) 

421 r[1:-1], # traverse (top, left) to (top, right) (excluding corners) 

422 [r[-1]], # (top, right) 

423 r[-1] * np.ones(len(z[1:-1])), # traverse (top, right) to (bottom, right) (excluding corners) 

424 [r[-1]], # (bottom, right) 

425 np.flip(r[1:-1]), # traverse (bottom, right) to (bottom, left) (excluding corners) 

426 ) 

427 ) 

428 inv_r_ltrb_mu0 = 1.0 / (r_ltrb * mu_0) 

429 results["PRESHOT"]["INV_R_L_MU0"] = inv_r_ltrb_mu0.astype(np.float64) 

430 

431 lower_band, upper_band, perm_idx = compute_lup_bands(r, z) 

432 results["PRESHOT"]["LOWER_BAND"] = lower_band.astype(np.float64) 

433 results["PRESHOT"]["UPPER_BAND"] = upper_band.astype(np.float64) 

434 results["PRESHOT"]["PERM_IDX"] = perm_idx.astype(np.int32) 

435 

436 # Greens with the boundary points 

437 g_ltrb = greens_with_boundary_points(plasma) 

438 results["PRESHOT"]["GREENS"]["LTRB"] = g_ltrb 

439 

440 # Sometimes important sensors are broken, mainly Rogowski coils which measure current in 

441 # passive plates. As a work around we can use the nearby flux loop voltage (V = I * R) as 

442 # a replacement. This section replaces the damaged "bad" sensors with working "good" sensors. 

443 sensors_pcs_should_read = copy.deepcopy(constraint_names) 

444 if "sensor_replacement.json" in gsfit_controller.settings: 

445 sensor_replacements = gsfit_controller.settings["sensor_replacement.json"] 

446 # sensor_replacement is a dictonary containing the sensors which should be replaced, the data structure is: 

447 # { 

448 # "sensor_name_to_replace": { 

449 # "replacements": ["sensor_name_to_use_01", "sensor_name_to_use_02", ...], 

450 # "coefficients": [1.0, 0.5, ...] 

451 # }, 

452 # ... 

453 # } 

454 

455 # Remove the sensors that are being replaced 

456 for sensor_replacement_name in sensor_replacements.keys(): 

457 if sensor_replacement_name in sensors_pcs_should_read: 

458 sensors_pcs_should_read.remove(sensor_replacement_name) 

459 

460 # Add the sensors that are replacing the removed sensors 

461 for sensor_replacement_item in sensor_replacements.values(): 

462 sensor_replacement_names = sensor_replacement_item["replacements"] 

463 for sensor_replacement_name in sensor_replacement_names: 

464 # Don't double add sensor names 

465 if sensor_replacement_name not in sensors_pcs_should_read: 

466 sensors_pcs_should_read.append(sensor_replacement_name) 

467 

468 # Construct a matrix where: 

469 # `sensors_rtgsfit_wants = sensor_replacement_matrix * sensors_pcs_should_read` 

470 sensor_replacement_matrix = np.zeros((len(constraint_names), len(sensors_pcs_should_read)), dtype=np.float64) 

471 for i_constraint, constraint_name in enumerate(constraint_names): 

472 # If the sensor is being replaced, only use the replacement coefficients 

473 if constraint_name in sensor_replacements: 

474 sensor_replacement = sensor_replacements[constraint_name] 

475 sensor_replacement_names = sensor_replacement["replacements"] 

476 sensor_replacement_coefficients = sensor_replacement["coefficients"] 

477 n_replacements = len(sensor_replacement_names) 

478 for i_replacement in range(n_replacements): 

479 replacement_name = sensor_replacement_names[i_replacement] 

480 i_pcs_sensor = sensors_pcs_should_read.index(replacement_name) 

481 sensor_replacement_matrix[i_constraint, i_pcs_sensor] = sensor_replacement_coefficients[i_replacement] 

482 elif constraint_name in sensors_pcs_should_read: 

483 # Sensor is not being replaced, set identity 

484 i_pcs_sensor = sensors_pcs_should_read.index(constraint_name) 

485 sensor_replacement_matrix[i_constraint, i_pcs_sensor] = 1.0 

486 

487 else: 

488 # No sensor replacement, so the matrix is just the identity matrix 

489 sensor_replacement_matrix = np.eye(len(constraint_names), len(sensors_pcs_should_read), dtype=np.float64) 

490 

491 results["PRESHOT"]["SENS_NAMES"] = np.array(sensors_pcs_should_read) 

492 results["PRESHOT"]["SENS_REP_MAT"] = sensor_replacement_matrix.flatten() 

493 results["PRESHOT"]["N_SENS_PCS"] = np.int32(len(sensors_pcs_should_read)) 

494 

495 # Save IVC geometry data 

496 results["PASSIVES"]["IVC"]["GEOMETRY"]["R"] = passives.get_array1(["IVC", "geometry", "r"]) 

497 results["PASSIVES"]["IVC"]["GEOMETRY"]["Z"] = passives.get_array1(["IVC", "geometry", "z"]) 

498 results["PASSIVES"]["IVC"]["GEOMETRY"]["D_R"] = passives.get_array1(["IVC", "geometry", "d_r"]) 

499 results["PASSIVES"]["IVC"]["GEOMETRY"]["D_Z"] = passives.get_array1(["IVC", "geometry", "d_z"]) 

500 results["PASSIVES"]["IVC"]["GEOMETRY"]["AREA"] = results["PASSIVES"]["IVC"]["GEOMETRY"]["D_R"] * results["PASSIVES"]["IVC"]["GEOMETRY"]["D_Z"] 

501 n_eigs = gsfit_controller.settings["passive_dof_regularisation.json"]["IVC"]["n_dof"] 

502 n_segs = len(passives.get_array1(["IVC", "dof", f"eig_01", "current_distribution"])) 

503 current_dofs = np.zeros((n_eigs, n_segs)) 

504 for eig_num in range(n_eigs): 

505 current_dofs[eig_num, :] = passives.get_array1(["IVC", "dof", f"eig_{eig_num + 1:02d}", "current_distribution"]) 

506 results["PASSIVES"]["IVC"]["GEOMETRY"]["CURRENT_DOFS"] = current_dofs 

507 

508 # Create coef names list and save it 

509 coef_names = ["pls0", "pls1", "pls2"] 

510 for passive_name in passives.keys(): 

511 dof_names = passives.keys([passive_name, "dof"]) 

512 for dof_name in dof_names: 

513 if dof_name == "constant_current_density": 

514 coef_names.append(passive_name) 

515 elif dof_name.startswith("eig_"): 

516 coef_names.append(dof_name) 

517 else: 

518 raise ValueError(f"Unknown DoF name: {dof_name}") 

519 results["PRESHOT"]["COEF_NAMES"] = np.array(coef_names) 

520 

521 # create meas_names list and save it 

522 meas_names = [] 

523 for i_flux_loop, floop_name in enumerate(flux_loops.keys()): 

524 if flux_loops_to_include[i_flux_loop]: 

525 meas_names.append(floop_name) 

526 for i_bp_probe, bp_name in enumerate(bp_probes.keys()): 

527 if bp_probes_to_include[i_bp_probe]: 

528 meas_names.append(bp_name) 

529 for rog_name in rogowski_coils_names_rtgsfit_order: 

530 meas_names.append(rog_name) 

531 for passive_name in passive_names: 

532 passive_regularisation_local = passives.get_array2([passive_name, "regularisations"]) 

533 [n_reg_local, _] = passive_regularisation_local.shape 

534 for i_reg in range(n_reg_local): 

535 meas_names.append(f"{passive_name}_reg_{i_reg}") 

536 results["PRESHOT"]["MEAS_NAMES"] = np.array(meas_names)