Coverage for python/gsfit/database_readers/st40_astra_mdsplus/setup_coils.py: 0%

139 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-07 13:12 +0000

1# mypy: ignore-errors 

2# TODO: need to fix mypy errors 

3 

4import typing 

5from typing import TYPE_CHECKING 

6 

7import mdsthin 

8import numpy as np 

9import numpy.typing as npt 

10from gsfit_rs import Coils 

11from scipy.constants import mu_0 

12 

13from .astra_coils_reader import astra_coils_reader 

14 

15if TYPE_CHECKING: 

16 from . import DatabaseReader 

17 

18# Read coil geometry from coil.dat 

19astra_coil_description = astra_coils_reader() 

20 

21 

22def dividing_parallograms(coil_dictionary) -> tuple[int, int]: 

23 """ 

24 The PF coils are described as parallograms, and discretized in `ASTRA/SRC/Trecur_2.f`, in the `equil_astra_tepm` GitLab repository. 

25 

26 ``` 

27 SW = SQRT( NDIVA*WSIZE/HSIZE ) 

28 SH = SQRT( NDIVA*HSIZE/WSIZE ) 

29 SW = SW + 0.5 

30 SH = SH + 0.5 

31 C 

32 NDIVW = IDINT(SW) 

33 NDIVH = IDINT(SH) 

34 ``` 

35 """ 

36 ndi = coil_dictionary["ndi"] 

37 wc = coil_dictionary["wc"] 

38 hc = coil_dictionary["hc"] 

39 

40 sw = np.sqrt(ndi * wc / hc) 

41 sh = np.sqrt(ndi * hc / wc) 

42 sw = sw + 0.5 

43 sh = sh + 0.5 

44 ndivw = int(sw) 

45 ndivh = int(sh) 

46 return ndivw, ndivh 

47 

48 

49def calculate_coil_filament_positions(coil_dictionary) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: 

50 """ 

51 Calculate the coil filament positions from the coil description dictionary. 

52 

53 :param coil_dictionary: Dictionary containing the coil description 

54 :return: Tuple of numpy arrays containing the r and z positions of the coil filaments 

55 

56 ``` 

57 C************************************************************** 

58 C SUBROUTINE FOR DIVIDING OF PARALLELOGRAM 

59 C ( FOR PFC CROSS-SECTION ) 

60 C************************************************************** 

61 C INPUT DATE: 

62 C ---------- 

63 C RC,ZC - CILINDER COORDINATES OF CENTER OF PARALLELOGRAM 

64 C WC - PROJECTION OF THE FIRST SIDE OF PARALLELOGRAM 

65 C ON AXIS "R" (IN METER) 

66 C HC - PROJECTION OF THE SECOND SIDE OF PARALLELOGRAM 

67 C ON AXIS "Z" (IN METER) 

68 C AWC - ANGLE BETWEEN THE FIRST SIDE OF PARAL. AND 

69 C AXIS "R" (IN DEGREES, IT MUST NOT BE EQUAL 90 , 

70 C IT MUST BE : -90 < AWC < 90 ) 

71 C AHC - ANGLE BETWEEN THE SECOND SIDE OF PARAL. AND 

72 C AXIS "R" (IN DEGREES, IT MUST NOT BE EQUAL 0 , 

73 C IT MUST BE : 0 < AHC < 180 ) 

74 C CURC - CURRENT OF PARALLELOGRAM (IN MA) 

75 C NDIV - APPROXIMATE NUMBER OF CELLS OF DIVIDING: 

76 C NDIV=0 - A SPECIAL CASE: AUTOMATICALLY NDIVRE=1, 

77 C RS(1)=RC, ZS(1)=ZC, PS=CURC 

78 C IF NDIV > 0 THEN WE HAVE THE MOST TOTAL ALGORITHM OF 

79 C DIVIDING 

80 C IF NDIV < 0 THEN NDIVW AND NDIVH ARE CUT OFF BY ABS(NDIV) 

81 C 

82 C OUTPUT DATE: 

83 C ---------- 

84 C NDIVRE - REAL NUMBER OF CELLS OF DIVIDING ( = NDIVW*NDIVH ) 

85 C NDIVW - NUMBER OF DIVIDING OF THE FIRST SIDE OF PARAL. 

86 C NDIVH - NUMBER OF DIVIDING OF THE SECOND SIDE OF PARAL. 

87 C RS(L),ZS(L) - CILINDER COORDINATES OF CENTERS OF CELLS OF DIVIDING 

88 C L = 1,2,...,NDIVRE ! (IN METER) 

89 C PS - CURRENT OF EVERY CELL OF DIVIDING (IN MA) 

90 C VERS - VERTICAL (OR LINEAR, OR RADIUS) SIZE OF CELL CROSS-S. 

91 C HORS - HORIZONTAL SIZE OF CELL CROSS-SECTION 

92 C 

93 C************************************************************** 

94 C 

95 SUBROUTINE DIVPAR( RC, ZC, WC, HC, AWC, AHC, CURC, NDIV, 

96 * NDIVRE, NDIVW, NDIVH, RS, ZS, PS, 

97 * VERS, HORS ) 

98 C 

99 C 

100 include 'double.inc' 

101 C 

102 DIMENSION RS(1), ZS(1) 

103 C 

104 C************************************************************** 

105 SIN(X) = DSIN(X) 

106 COS(X) = DCOS(X) 

107 ATAN(X) = DATAN(X) 

108 SQRT(X) = DSQRT(X) 

109 C************************************************************** 

110 C 

111 IF(NDIV.EQ.0) THEN 

112 NDIVW = 1 

113 NDIVH = 1 

114 NDIVRE = 1 

115 RS(1) = RC 

116 ZS(1) = ZC 

117 PS = CURC 

118 VERS = HC 

119 HORS = WC 

120 RETURN 

121 END IF 

122 C*************************************** 

123 C 

124 IF(AHC.LT.0) AHC = AHC + 180. 

125 C 

126 IF(NDIV.GE.0) THEN 

127 NDIVA = NDIV 

128 ELSE 

129 NDIVA = -NDIV 

130 END IF 

131 C*************************************** 

132 C PARAMETERS OF PARALLELOGRAM 

133 C 

134 XX = 1. 

135 PI = 4.*ATAN(XX) 

136 C 

137 AWCR = AWC * PI /180. 

138 AHCR = AHC * PI /180. 

139 C 

140 R0 = RC - 0.5*( WC + HC * COS(AHCR)/SIN(AHCR) ) 

141 Z0 = ZC - 0.5*( HC + WC * SIN(AWCR)/COS(AWCR) ) 

142 C 

143 WSIZE = WC / COS(AWCR) 

144 HSIZE = HC / SIN(AHCR) 

145 C 

146 WR = WC 

147 WZ = WC * SIN(AWCR) / COS(AWCR) 

148 HR = HC * COS(AHCR) / SIN(AHCR) 

149 HZ = HC 

150 C*************************************** 

151 C CALCULATION NDIVW, NDIVH, NDIVRE, PS 

152 C 

153 SW = SQRT( NDIVA*WSIZE/HSIZE ) 

154 SH = SQRT( NDIVA*HSIZE/WSIZE ) 

155 SW = SW + 0.5 

156 SH = SH + 0.5 

157 C 

158 NDIVW = IDINT(SW) 

159 NDIVH = IDINT(SH) 

160 C 

161 IF(NDIVW.EQ.0) NDIVW = 1 

162 IF(NDIVH.EQ.0) NDIVH = 1 

163 C 

164 IF((NDIV.LT.0).AND.(NDIVW.GT.NDIVA)) NDIVW = NDIVA 

165 IF((NDIV.LT.0).AND.(NDIVH.GT.NDIVA)) NDIVH = NDIVA 

166 C 

167 NDIVRE = NDIVW * NDIVH 

168 PS = CURC / NDIVRE 

169 C*************************************** 

170 C CALCULATION RS(L), ZS(L) : L = 1,2,...,NDIVRE 

171 C 

172 WR = WR / NDIVW 

173 WZ = WZ / NDIVW 

174 HR = HR / NDIVH 

175 HZ = HZ / NDIVH 

176 C 

177 HORS = WR 

178 VERS = HZ 

179 C 

180 RS(1) = R0 + 0.5*(WR + HR) 

181 ZS(1) = Z0 + 0.5*(WZ + HZ) 

182 C 

183 DO 1 I=1,NDIVW 

184 DO 1 J=1,NDIVH 

185 L = (I-1)*NDIVH + J 

186 RS(L) = RS(1) + (I-1)*WR + (J-1)*HR 

187 ZS(L) = ZS(1) + (I-1)*WZ + (J-1)*HZ 

188 1 CONTINUE 

189 C*************************************** 

190 C 

191 RETURN 

192 END 

193 ``` 

194 """ 

195 

196 # Extract required parameters 

197 rc = float(coil_dictionary["rc"]) # center R 

198 zc = float(coil_dictionary["zc"]) # center Z 

199 wc = float(coil_dictionary["wc"]) # projection of first side on R 

200 hc = float(coil_dictionary["hc"]) # projection of second side on Z 

201 # Optional angles (degrees). Default to AWC=0, AHC=90 if not provided, 

202 # matching the most common cases in the coil table. 

203 awc = float(coil_dictionary.get("awc", 0.0)) 

204 ahc = float(coil_dictionary.get("ahc", 90.0)) 

205 # Approximate number of cells (NDIV/NDIVA) 

206 ndia = int(coil_dictionary.get("ndi", 1)) 

207 

208 # Handle the special case NDIV == 0 

209 if ndia == 0: 

210 rs = np.array([rc], dtype=np.float64) 

211 zs = np.array([zc], dtype=np.float64) 

212 return rs, zs 

213 

214 # Convert angles to radians, applying the Fortran adjustment for AHC < 0 

215 if ahc < 0.0: 

216 ahc = ahc + 180.0 

217 

218 pi = np.pi 

219 awcr = awc * pi / 180.0 

220 ahcr = ahc * pi / 180.0 

221 

222 # Guard against invalid angles (avoid division by zero) 

223 # In Fortran code: it assumes -90 < AWC < 90 and 0 < AHC < 180 

224 # If cos(awcr) or sin(ahcr) are too close to zero, nudge slightly. 

225 eps = 1e-12 

226 c_aw = np.cos(awcr) 

227 s_aw = np.sin(awcr) 

228 s_ah = np.sin(ahcr) 

229 c_ah = np.cos(ahcr) 

230 if abs(c_aw) < eps: 

231 c_aw = eps if c_aw >= 0 else -eps 

232 if abs(s_ah) < eps: 

233 s_ah = eps if s_ah >= 0 else -eps 

234 

235 # Fortran mapping 

236 r0 = rc - 0.5 * (wc + hc * c_ah / s_ah) 

237 z0 = zc - 0.5 * (hc + wc * s_aw / c_aw) 

238 

239 wsize = wc / c_aw 

240 hsize = hc / s_ah 

241 

242 wr = wc 

243 wz = wc * s_aw / c_aw 

244 hr = hc * c_ah / s_ah 

245 hz = hc 

246 

247 # Calculate NDIVW, NDIVH per Fortran's DIVPAR (NDIV >= 0 path) 

248 sw = np.sqrt(ndia * wsize / hsize) + 0.5 

249 sh = np.sqrt(ndia * hsize / wsize) + 0.5 

250 ndivw = int(sw) 

251 ndivh = int(sh) 

252 if ndivw == 0: 

253 ndivw = 1 

254 if ndivh == 0: 

255 ndivh = 1 

256 

257 # Real number of divided cells 

258 ndivre = ndivw * ndivh 

259 

260 # Cell sizes 

261 wr_cell = wr / ndivw 

262 wz_cell = wz / ndivw 

263 hr_cell = hr / ndivh 

264 hz_cell = hz / ndivh 

265 

266 # First cell center 

267 r1 = r0 + 0.5 * (wr_cell + hr_cell) 

268 z1 = z0 + 0.5 * (wz_cell + hz_cell) 

269 

270 # Generate centers for all cells (Fortran order L = (I-1)*NDIVH + J) 

271 rs = np.empty(ndivre, dtype=np.float64) 

272 zs = np.empty(ndivre, dtype=np.float64) 

273 idx = 0 

274 for i in range(1, ndivw + 1): 

275 for j in range(1, ndivh + 1): 

276 r = r1 + (i - 1) * wr_cell + (j - 1) * hr_cell 

277 z = z1 + (i - 1) * wz_cell + (j - 1) * hz_cell 

278 rs[idx] = r 

279 zs[idx] = z 

280 idx += 1 

281 

282 return rs, zs 

283 

284 

285def setup_coils( 

286 self: "DatabaseReader", 

287 settings: dict[str, typing.Any], 

288 pulseNo: int, 

289) -> Coils: 

290 """ 

291 This method initialises the Rust `Coils` class. 

292 

293 :param pulseNo: Pulse number, used to read from the database 

294 :param settings: Dictionary containing the JSON settings read from the `settings` directory 

295 

296 **This method is specific to ST40's ASTRA stored on MDSplus.** 

297 

298 See `python/gsfit/database_readers/interface.py` for more details on how a new database_reader should be implemented. 

299 """ 

300 

301 # Initialise the Coils Rust class 

302 coils = Coils() 

303 

304 # Extract the astra_run_name from settings 

305 astra_run_name = settings["GSFIT_code_settings.json"]["database_reader"]["st40_astra_mdsplus"]["workflow"]["astra"]["run_name"] 

306 

307 # Connect to MDSplus 

308 conn = mdsthin.Connection("smaug") 

309 conn.openTree("ASTRA", pulseNo) 

310 

311 # ASTRA time 

312 time = conn.get(f"\\ASTRA::TOP.{astra_run_name}:TIME").data().astype(np.float64) 

313 

314 # PF coil currents 

315 # ["IPL", "MC", "PSH", "DIV", "BVL", "BVUT", "BVUB", "CS", "MCVC"] 

316 # [ 0, 1, 2, 3, 4, 5, 6, 7 8] 

317 currents = conn.get(f"\\ASTRA::TOP.{astra_run_name}.COILS.PSU2PF:I").data().astype(np.float64) * 1.0e6 

318 

319 # Add BVLB PF coil 

320 coil_name = "BVLB" 

321 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

322 # Read in BVL PSU current, and normalise by the number of filaments 

323 currents_local = currents[:, 4] * 4.0 * 4.0 / len(coil_r) 

324 coils.add_pf_coil( 

325 coil_name, 

326 coil_r, 

327 coil_z, 

328 d_r=0.0 * coil_r, 

329 d_z=0.0 * coil_z, 

330 time=time, 

331 measured=currents_local, 

332 ) 

333 # Add BVLT PF coil 

334 coil_name = "BVLT" 

335 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

336 coils.add_pf_coil( 

337 coil_name, 

338 coil_r, 

339 coil_z, 

340 d_r=0.0 * coil_r, 

341 d_z=0.0 * coil_z, 

342 time=time, 

343 measured=currents_local, 

344 ) 

345 

346 # Read in BVUB PSU current, and normalise by the number of filaments 

347 coil_name = "BVUB" 

348 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

349 currents_local = currents[:, 6] * 4.0 * 6.0 / len(coil_r) 

350 coils.add_pf_coil( 

351 coil_name, 

352 coil_r, 

353 coil_z, 

354 d_r=0.0 * coil_r, 

355 d_z=0.0 * coil_z, 

356 time=time, 

357 measured=currents_local, 

358 ) 

359 

360 # Read in BVUT PSU current, and normalise by the number of filaments 

361 coil_name = "BVUT" 

362 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

363 currents_local = currents[:, 5] * 4.0 * 6.0 / len(coil_r) 

364 coils.add_pf_coil( 

365 coil_name, 

366 coil_r, 

367 coil_z, 

368 d_r=0.0 * coil_r, 

369 d_z=0.0 * coil_z, 

370 time=time, 

371 measured=currents_local, 

372 ) 

373 

374 # Read in SOL PSU current, and normalise by the number of filaments 

375 coil_name = "SOL" 

376 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

377 currents_local = currents[:, 7] * 2.0 * 95.0 / len(coil_r) 

378 coils.add_pf_coil( 

379 coil_name, 

380 coil_r, 

381 coil_z, 

382 d_r=0.0 * coil_r, 

383 d_z=0.0 * coil_z, 

384 time=time, 

385 measured=currents_local, 

386 ) 

387 

388 # Add DIVT PF coil, and normalise by the number of filaments 

389 coil_name = "DIVT" 

390 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

391 currents_local = currents[:, 3] * 4.0 * 7.0 / len(coil_r) 

392 coils.add_pf_coil( 

393 coil_name, 

394 coil_r, 

395 coil_z, 

396 d_r=0.0 * coil_r, 

397 d_z=0.0 * coil_z, 

398 time=time, 

399 measured=currents_local, 

400 ) 

401 # Add DIVB PF coil 

402 coil_name = "DIVB" 

403 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

404 coils.add_pf_coil( 

405 coil_name, 

406 coil_r, 

407 coil_z, 

408 d_r=0.0 * coil_r, 

409 d_z=0.0 * coil_z, 

410 time=time, 

411 measured=currents_local, 

412 ) 

413 

414 # Add MCT PF coil, and normalise by the number of filaments 

415 coil_name = "MCT" 

416 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

417 currents_local = -currents[:, 8] * 11.0 / len(coil_r) / 2.0 + currents[:, 1] * 11.0 / len(coil_r) 

418 coils.add_pf_coil( 

419 coil_name, 

420 coil_r, 

421 coil_z, 

422 d_r=0.0 * coil_r, 

423 d_z=0.0 * coil_z, 

424 time=time, 

425 measured=currents_local, 

426 ) 

427 # Add MCB PF coil 

428 coil_name = "MCB" 

429 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

430 currents_local = currents[:, 8] * 11.0 / len(coil_r) / 2.0 + currents[:, 1] * 11.0 / len(coil_r) 

431 coils.add_pf_coil( 

432 coil_name, 

433 coil_r, 

434 coil_z, 

435 d_r=0.0 * coil_r, 

436 d_z=0.0 * coil_z, 

437 time=time, 

438 measured=currents_local, 

439 ) 

440 

441 # Add PSHT PF coil 

442 coil_name = "PSHT1" 

443 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

444 currents_local = currents[:, 2] * 4.0 / len(coil_r) 

445 coils.add_pf_coil( 

446 coil_name, 

447 coil_r, 

448 coil_z, 

449 d_r=0.0 * coil_r, 

450 d_z=0.0 * coil_z, 

451 time=time, 

452 measured=currents_local, 

453 ) 

454 # Add PSHB PF coil 

455 coil_name = "PSHB1" 

456 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

457 coils.add_pf_coil( 

458 coil_name, 

459 coil_r, 

460 coil_z, 

461 d_r=0.0 * coil_r, 

462 d_z=0.0 * coil_z, 

463 time=time, 

464 measured=currents_local, 

465 ) 

466 # Add PSHT2 PF coil 

467 coil_name = "PSHT2" 

468 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

469 coils.add_pf_coil( 

470 coil_name, 

471 coil_r, 

472 coil_z, 

473 d_r=0.0 * coil_r, 

474 d_z=0.0 * coil_z, 

475 time=time, 

476 measured=currents_local, 

477 ) 

478 # Add PSHB2 PF coil 

479 coil_name = "PSHB2" 

480 coil_r, coil_z = calculate_coil_filament_positions(astra_coil_description[coil_name]) 

481 coils.add_pf_coil( 

482 coil_name, 

483 coil_r, 

484 coil_z, 

485 d_r=0.0 * coil_r, 

486 d_z=0.0 * coil_z, 

487 time=time, 

488 measured=currents_local, 

489 ) 

490 

491 # Add TF coil 

492 bt_vac = conn.get(f"\\ASTRA::TOP.{astra_run_name}.GLOBAL:BTVAC").data().astype(np.float64) # time-dependent 

493 r_reference = 0.5 

494 i_rod = bt_vac * (2.0 * np.pi * r_reference) / mu_0 # time-dependent 

495 coils.add_tf_coil( 

496 time=time, 

497 measured=i_rod, 

498 ) 

499 

500 return coils