Coverage for python/gsfit/database_readers/interface.py: 100%

34 statements  

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

1import typing 

2from typing import Protocol 

3 

4import numpy as np 

5import numpy.typing as npt 

6from gsfit_rs import BpProbes 

7from gsfit_rs import Coils 

8from gsfit_rs import Dialoop 

9from gsfit_rs import FluxLoops 

10from gsfit_rs import Isoflux 

11from gsfit_rs import IsofluxBoundary 

12from gsfit_rs import Passives 

13from gsfit_rs import Plasma 

14from gsfit_rs import Pressure 

15from gsfit_rs import PulseSchedule 

16from gsfit_rs import RogowskiCoils 

17from gsfit_rs import StationaryPoint 

18from gsfit_rs import Tf 

19from gsfit_rs import Wall 

20 

21 

22class DatabaseReaderProtocol(Protocol): 

23 """ 

24 Protocol for reading experimental data. 

25 Each method is responsible for initialising one of the Rust objects: 

26 `bp_probes`, `coils`, `dialoop`, `flux_loops`, `isoflux`, `isoflux_boundary`, `stationary_point`, `passives`, `plasma`, `rogowski_coils`, `tf`, and `wall`. 

27 

28 The Protocol defines the inputs and outputs of each method. 

29 New database readers should be implemented **all** methods. 

30 

31 The one exception is `setup_pulse_schedule`, which initialises the optional `pulse_schedule`. 

32 It has a default here, returning `None`, so a reader for a machine with no gaps to measure need not implement it. 

33 """ 

34 

35 def setup_bp_probes(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> BpProbes: 

36 """ 

37 This method initialises the Rust `BpProbes` class (Mirnov coils with a direction in the poloidal plane). 

38 

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

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

41 :param kwargs: Additional objects, such as FreeGNSKE object 

42 

43 Initialising requires reading data from two locations: 

44 1. `sensor_weights_bp_probe.json`: Which contains "fitting parameters", e.g. if a probe should be used in the fitting 

45 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the measurements and probe geometry 

46 

47 Different machines will use different data stores for the probe geometry and measured signals. 

48 This Protocol allows different database readers to be selected. 

49 The output of this method must always be a `BpProbes` object. 

50 

51 At a minimum this method should look like this: 

52 ```python 

53 # Initialise the BpProbes Rust class 

54 bp_probes = BpProbes() 

55 

56 # Add all of the BP probes 

57 for i_bp_probe in range(n_bp_probes): 

58 bp_probes.add_sensor( 

59 name=..., # need to be same in database and `sensor_weights_bp_probe.json` file 

60 geometry_angle_pol=..., # read from a database 

61 geometry_r=..., # read from a database 

62 geometry_z=..., # read from a database 

63 fit_settings_comment=..., # read from `sensor_weights_bp_probe.json` file 

64 fit_settings_expected_value=..., # read from `sensor_weights_bp_probe.json` file 

65 fit_settings_include=..., # read from `sensor_weights_bp_probe.json` file 

66 fit_settings_weight=..., # read from `sensor_weights_bp_probe.json` file 

67 time=..., # read from a database 

68 measured=..., # read from a database 

69 ) 

70 

71 return bp_probes 

72 ``` 

73 """ 

74 ... 

75 

76 def setup_coils(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> Coils: 

77 """ 

78 This method initialises the Rust `Coils` class, which contains both the poloidal fied (PF) and toroidal field (TF) coils. 

79 

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

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

82 :param kwargs: Additional objects, such as FreeGSNKE object 

83 

84 Initialising requires reading data from: 

85 1. Database reading (e.g. MDSplus, or FreeGSNKE object): Which contains the coil geometry and current measurements 

86 

87 Different machines will use different data stores for the coil locations and currents. 

88 This Protocol allows different database readers to be selected. 

89 The output of this method must always be a `Coils` object. 

90 

91 At a minimum this method should look like this: 

92 ```python 

93 # Initialise the Coils Rust class 

94 coils = Coils() 

95 

96 # Add all of the PF coils 

97 for i_pf_coil in range(n_pf_coils): 

98 coils.add_pf_coil( 

99 coil_name=..., # read from a database 

100 coil_r=..., # read from a database 

101 coil_z=..., # read from a database 

102 coil_d_r=..., # read from a database 

103 coil_d_z=..., # read from a database 

104 time=..., # read from a database 

105 measured=..., # read from a database 

106 ) 

107 

108 return coils 

109 ``` 

110 """ 

111 ... 

112 

113 def setup_dialoop(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> Dialoop: 

114 """ 

115 This method initialises the Rust `Dialoop` class (the plasma's diamagnetic flux, with the vaccuum toroidal field subtracted). 

116 

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

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

119 :param kwargs: Additional objects, such as FreeGNSKE object 

120 

121 Initialising requires reading data from two locations: 

122 1. `sensor_weights_dialoop.json`: Which contains "fitting parameters", e.g. if a probe should be used in the fitting 

123 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the measurements and the probe geometry 

124 

125 Different machines will use different data stores for the measured signals. 

126 This Protocol allows different database readers to be selected. 

127 The output of this method must always be a `Dialoop` object. 

128 

129 At a minimum this method should look like this: 

130 ```python 

131 # Initialise the Dialoop Rust class 

132 dialoop = Dialoop() 

133 

134 # Add all of the diamagnetic loops 

135 for i_dialoop in range(n_dialoops): 

136 dialoop.add_sensor( 

137 name=..., # read from a database 

138 fit_settings_comment=..., # read from `sensor_weights_dialoop.json` file 

139 fit_settings_expected_value=..., # read from `sensor_weights_dialoop.json` file 

140 fit_settings_include=..., # read from `sensor_weights_dialoop.json` file 

141 fit_settings_weight=..., # read from `sensor_weights_dialoop.json` file 

142 time=..., # read from a database 

143 measured=..., # read from a database 

144 ) 

145 

146 return dialoop 

147 ``` 

148 """ 

149 ... 

150 

151 def setup_flux_loops(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> FluxLoops: 

152 """ 

153 This method initialises the Rust `FluxLoops` class (loops going in the toroidal direction measuring poloidal flux). 

154 

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

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

157 :param kwargs: Additional objects, such as FreeGNSKE object 

158 

159 Initialising requires reading data from two locations: 

160 1. `sensor_weights_flux_loops.json`: Which contains "fitting parameters", e.g. if a probe should be used in the fitting 

161 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the measurements and the probe geometry 

162 

163 Different machines will use different data stores for the probes geometry and measured signals. 

164 This Protocol allows different database readers to be selected. 

165 The output of this method must always be a `FluxLoops` object. 

166 

167 At a minimum this method should look like this: 

168 ```python 

169 # Initialise the FluxLoops Rust class 

170 flux_loops = FluxLoops() 

171 

172 # Add all of the BP probes 

173 for i_flux_loop in range(n_flux_loops): 

174 flux_loops.add_sensor( 

175 name=..., # need to be same in database and `sensor_weights_flux_loops.json` file 

176 geometry_r=..., # read from a database 

177 geometry_z=..., # read from a database 

178 fit_settings_comment=..., # read from `sensor_weights_flux_loops.json` file 

179 fit_settings_expected_value=..., # read from `sensor_weights_flux_loops.json` file 

180 fit_settings_include=..., # read from `sensor_weights_flux_loops.json` file 

181 fit_settings_weight=..., # read from `sensor_weights_flux_loops.json` file 

182 time=..., # read from a database 

183 measured=..., # read from a database 

184 ) 

185 

186 return flux_loops 

187 ``` 

188 """ 

189 ... 

190 

191 def setup_isoflux_sensors( 

192 self, pulseNo: int, settings: dict[str, typing.Any], times_to_reconstruct: npt.NDArray[np.float64], **kwargs: dict[str, typing.Any] 

193 ) -> Isoflux: 

194 """ 

195 This method initialises the Rust `Isoflux` class (two locations which have equal poloidal flux). 

196 

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

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

199 :param times_to_reconstruct: Times to reconstruct the equilibrium 

200 :param kwargs: Additional objects, such as FreeGNSKE object 

201 

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

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

204 

205 Initialising requires reading data from two locations: 

206 1. `sensor_weights_isoflux.json`: Which contains "fitting parameters", e.g. if the constraint should be used in the fitting 

207 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the coordinates where the isoflux constraint is applied 

208 

209 Different machines will use different data stores for the isoflux coordiantes. 

210 This Protocol allows different database readers to be selected. 

211 The output of this method must always be a `Isoflux` object. 

212 

213 At a minimum this method should look like this: 

214 ```python 

215 # Initialise the Isoflux Rust class 

216 isoflux = Isoflux() 

217 

218 # Add all of the isoflux constraints 

219 for i_isoflux_constraint in range(n_isoflux_constraints): 

220 isoflux.add_sensor( 

221 name=..., # need to be same in database and `sensor_weights_isoflux.json` file 

222 fit_settings_comment=..., # read from `sensor_weights_isoflux.json` file 

223 fit_settings_include=..., # read from `sensor_weights_isoflux.json` file 

224 fit_settings_weight=..., # read from `sensor_weights_isoflux.json` file 

225 time=..., # read from a database 

226 location_1_r=..., # read from a database 

227 location_1_z=..., # read from a database 

228 location_2_r=..., # read from a database 

229 location_2_z=..., # read from a database 

230 times_to_reconstruct=..., # read from `GSFIT_code_settings.json` file 

231 ) 

232 

233 return isoflux 

234 ``` 

235 """ 

236 ... 

237 

238 def setup_isoflux_boundary_sensors(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> IsofluxBoundary: 

239 """ 

240 This method initialises the Rust `IsofluxBoundary` class (a location which has the same poloidal flux as the plasma boundary). 

241 

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

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

244 :param kwargs: Additional objects, such as FreeGNSKE object 

245 

246 Initialising requires reading data from two locations: 

247 1. `sensor_weights_isoflux_boundary.json`: Which contains "fitting parameters", e.g. if the constraint should be used in the fitting 

248 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the coordinates where the isoflux boundary constraint is applied 

249 

250 Different machines will use different data stores for the isoflux coordiantes. 

251 This Protocol allows different database readers to be selected. 

252 The output of this method must always be a `IsofluxBoundary` object. 

253 

254 At a minimum this method should look like this: 

255 ```python 

256 # Initialise the IsofluxBoundary Rust class 

257 isoflux_boundary = IsofluxBoundary() 

258 

259 # Add all of the isoflux_boundary constraints 

260 for i_isoflux_boundary_constraint in range(n_isoflux_boundary_constraints): 

261 isoflux_boundary.add_sensor( 

262 name=..., # need to be same in database and `sensor_weights_isoflux_boundary.json` file 

263 fit_settings_comment=..., # read from `sensor_weights_isoflux_boundary.json` file 

264 fit_settings_include=..., # read from `sensor_weights_isoflux_boundary.json` file 

265 fit_settings_weight=..., # read from `sensor_weights_isoflux_boundary.json` file 

266 time=..., # read from a database 

267 location_1_r=..., # read from a database 

268 location_1_z=..., # read from a database 

269 location_2_r=..., # read from a database 

270 location_2_z=..., # read from a database 

271 times_to_reconstruct=..., # read from `GSFIT_code_settings.json` file 

272 ) 

273 

274 return isoflux_boundary 

275 ``` 

276 """ 

277 ... 

278 

279 def setup_stationary_point_sensors(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> StationaryPoint: 

280 """ 

281 This method initialises the Rust `StationaryPoint` class (the magnetic axis, and x-point positions). 

282 

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

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

285 :param kwargs: Additional objects, such as FreeGNSKE object 

286 

287 Initialising requires reading data from two locations: 

288 1. `sensor_weights_magnetic_axis.json`: Which contains "fitting parameters", e.g. if a probe should be used in the fitting 

289 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the measurements and the probe geometry 

290 

291 Different machines will use different data stores for the measured signals. 

292 This Protocol allows different database readers to be selected. 

293 The output of this method must always be a `StationaryPoint` object. 

294 

295 At a minimum this method should look like this: 

296 ```python 

297 # Initialise the StationaryPoint Rust class 

298 stationary_point = StationaryPoint() 

299 

300 # Add all of the magnetic axis sensors 

301 for i_magnetic_axis in range(n_magnetic_axis_sensors): 

302 stationary_point.add_sensor( 

303 name=..., # read from a database 

304 fit_settings_comment=..., # read from `sensor_weights_magnetic_axis.json` file 

305 fit_settings_expected_value=..., # read from `sensor_weights_magnetic_axis.json` file 

306 fit_settings_include=..., # read from `sensor_weights_magnetic_axis.json` file 

307 fit_settings_weight=..., # read from `sensor_weights_magnetic_axis.json` file 

308 time=..., # read from a database 

309 mag_axis_r=..., # read from a database 

310 mag_axis_z=..., # read from a database 

311 times_to_reconstruct=..., # read from `GSFIT_code_settings.json` file 

312 ) 

313 

314 return stationary_point 

315 ``` 

316 """ 

317 ... 

318 

319 def setup_passives(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> Passives: 

320 """ 

321 This method initialises the Rust `Passives` class (toroidal conductors). 

322 

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

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

325 :param kwargs: Additional objects, such as FreeGNSKE object 

326 

327 Initialising requires reading data from two locations: 

328 1. `passive_dof_regularisation.json`: Which specifies how the conductor should be represented (e.g. constant current density, or eigenmode decomposition) 

329 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the coordinates where the passive conductors are located 

330 

331 Different machines will use different data stores for the passive conductor coordiantes. 

332 This Protocol allows different database readers to be selected. 

333 The output of this method must always be a `Passives` object. 

334 

335 At a minimum this method should look like this: 

336 ```python 

337 # Initialise the Passives Rust class 

338 passives = Passives() 

339 

340 # Add all of the passive toroidal conductors 

341 for i_passives in range(n_passives): 

342 passives.add_passive( 

343 name=..., # need to be same in database and `passive_dof_regularisation.json` file 

344 r=..., # read from a database 

345 z=..., # read from a database 

346 d_r=..., # read from a database 

347 d_z=..., # read from a database 

348 angle_1=..., # read from a database 

349 angle_2=..., # read from a database 

350 resistivity=..., # read from a database 

351 current_distribution_type=..., # read from `passive_dof_regularisation.json` file 

352 n_dof=..., # read from `passive_dof_regularisation.json` file 

353 regularisations=..., # read from `passive_dof_regularisation.json` file 

354 regularisations_weight=..., # read from `passive_dof_regularisation.json` file 

355 ) 

356 

357 return passives 

358 ``` 

359 """ 

360 ... 

361 

362 def setup_plasma( 

363 self, pulseNo: int, settings: dict[str, typing.Any], times_to_reconstruct: npt.NDArray[np.float64], **kwargs: dict[str, typing.Any] 

364 ) -> Plasma: 

365 """ 

366 This method initialises the Rust `Plasma` class. 

367 

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

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

370 :param kwargs: Additional objects, such as FreeGNSKE object 

371 

372 Initialising requires reading data from three locations: 

373 1. `GSFIT_code_settings.json`: Which contains the plasma grid size and the numerical settings the solve is run with 

374 2. `source_function_p_prime.json`: Which contains the number of degrees of freedom for p_prime, and regularisation 

375 3. `source_function_ff_prime.json`: Which contains the number of degrees of freedom for ff_prime, and regularisation 

376 

377 Normally, this method will be the same for all machines. 

378 

379 The output of this method must always be a `Plasma` object. 

380 

381 At a minimum this method should look like this: 

382 ```python 

383 # Initialise the Plasma Rust class 

384 p_prime_source_function = gsfit_rs.EfitPolynomial( 

385 n_dof=..., # read from `source_function_p_prime.json` file 

386 regularisations=..., # read from `source_function_p_prime.json` file 

387 ) 

388 ff_prime_source_function = gsfit_rs.EfitPolynomial( 

389 n_dof=..., # read from `source_function_ff_prime.json` file 

390 regularisations=..., # read from `source_function_ff_prime.json` file 

391 ) 

392 

393 # Initialise the Plasma Rust class 

394 plasma = Plasma( 

395 n_r=..., # read from `GSFIT_code_settings.json` file 

396 n_z=..., # read from `GSFIT_code_settings.json` file 

397 r_min=..., # read from `GSFIT_code_settings.json` file 

398 r_max=..., # read from `GSFIT_code_settings.json` file 

399 z_min=..., # read from `GSFIT_code_settings.json` file 

400 z_max=..., # read from `GSFIT_code_settings.json` file 

401 psi_n=..., # read from `GSFIT_code_settings.json` file 

402 p_prime_source_function=p_prime_source_function, # built above 

403 ff_prime_source_function=ff_prime_source_function, # built above 

404 initial_guess_ip=..., # read from `GSFIT_code_settings.json` file 

405 initial_guess_cur_r=..., # read from `GSFIT_code_settings.json` file 

406 initial_guess_cur_z=..., # read from `GSFIT_code_settings.json` file 

407 initial_guess_minor_radius=..., # read from `GSFIT_code_settings.json` file 

408 initial_guess_elongation=..., # read from `GSFIT_code_settings.json` file 

409 n_iter_max=..., # read from `GSFIT_code_settings.json` file 

410 n_iter_min=..., # read from `GSFIT_code_settings.json` file 

411 n_iter_no_vertical_feedback=..., # read from `GSFIT_code_settings.json` file 

412 grad_shafranov_deviation_tolerance=..., # read from `GSFIT_code_settings.json` file 

413 use_anderson_mixing=..., # read from `GSFIT_code_settings.json` file 

414 anderson_mixing_from_previous_iter=..., # read from `GSFIT_code_settings.json` file 

415 times_to_reconstruct=times_to_reconstruct, # passed in, from `setup_timeslices` 

416 ) 

417 

418 return plasma 

419 ``` 

420 """ 

421 ... 

422 

423 def setup_tf(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> Tf: 

424 """ 

425 This method initialises the Rust `Tf` class, which holds an IMAS `tf` IDS. 

426 

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

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

429 :param kwargs: Additional objects, such as FreeGNSKE object 

430 

431 Initialising requires reading data from: 

432 1. Database reading (e.g. MDSplus, or FreeGSNKE object): Which contains the toroidal field measurement 

433 

434 Two nodes are filled. `tf/r0` is the machine's reference major radius, which the solver 

435 copies onto `equilibrium/vacuum_toroidal_field/r0` so that the two IDSs cannot disagree. 

436 `tf/b_field_phi_vacuum_r` is the vacuum field times major radius, which is the vacuum 

437 poloidal-current function `f_vac = R0 * B_phi0 = mu_0 * i_rod / (2 * pi)`. The rod current 

438 is not stored separately: the solver recovers it as `i_rod = 2 * pi * f_vac / mu_0`. 

439 

440 **Store the experimental signal**, on its own timebase, not one interpolated onto the 

441 reconstruction times. `solve_grad_shafranov` interpolates it itself. 

442 

443 **`b_field_phi_vacuum_r` is signed**: positive means counter-clockwise viewed from above. 

444 A reader which passes a magnitude will silently reverse the toroidal field, and with it 

445 `f`, `q`, `profiles_2d/b_field_phi` and the diamagnetic flux constraint. 

446 

447 Different machines will use different data stores for the toroidal field. 

448 This Protocol allows different database readers to be selected. 

449 The output of this method must always be a `Tf` object. 

450 

451 At a minimum this method should look like this: 

452 ```python 

453 # Initialise the Tf Rust class 

454 tf = Tf() 

455 

456 tf.set_r0(...) # read from `GSFIT_code_settings.json` file 

457 

458 # A machine holding a rod current converts it; one holding `R * B_phi` passes it straight 

459 tf.set_b_field_phi_vacuum_r( 

460 time=..., # read from a database 

461 data=..., # read from a database 

462 ) 

463 

464 return tf 

465 ``` 

466 """ 

467 ... 

468 

469 def setup_wall(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> Wall: 

470 """ 

471 This method initialises the Rust `Wall` class, which holds an IMAS `wall` IDS. 

472 

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

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

475 :param kwargs: Additional objects, such as FreeGNSKE object 

476 

477 Initialising requires reading data from: 

478 1. Database reading (e.g. MDSplus, or FreeGSNKE object): Which contains the limiter geometry 

479 

480 Only `wall/description_2d(0)/limiter` is filled. Each limiter unit is a plasma facing 

481 component, and every point of every unit is a candidate limit point. 

482 

483 **The order the units are added in is part of the contract**: `unit(0)` is the vacuum 

484 vessel contour, and the solver uses that one, and only that one, as the region the 

485 plasma is allowed to occupy. Later units are components which protrude inside it, such 

486 as tiles. 

487 

488 Different machines will use different data stores for the limiter geometry. 

489 This Protocol allows different database readers to be selected. 

490 The output of this method must always be a `Wall` object. 

491 

492 At a minimum this method should look like this: 

493 ```python 

494 # Initialise the Wall Rust class 

495 wall = Wall() 

496 

497 # The vacuum vessel contour must be added first 

498 wall.add_limiter_unit( 

499 name="vacuum_vessel", # a short identifier for the unit 

500 r=..., # read from a database 

501 z=..., # read from a database 

502 ) 

503 

504 # Any other plasma facing components follow 

505 for i_unit in range(n_units): 

506 wall.add_limiter_unit( 

507 name=..., # a short identifier for the unit 

508 r=..., # read from a database 

509 z=..., # read from a database 

510 ) 

511 

512 return wall 

513 ``` 

514 """ 

515 ... 

516 

517 def setup_rogowski_coils(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> RogowskiCoils: 

518 """ 

519 This method initialises the Rust `RogowskiCoils` class. 

520 

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

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

523 :param kwargs: Additional objects, such as FreeGNSKE object 

524 

525 Initialising requires reading data from two locations: 

526 1. `sensor_weights_rogowski_coils.json`: Which contains "fitting parameters", e.g. if a probe should be used in the fitting 

527 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the measurements and the Rogowski coil path 

528 

529 Different machines will use different data stores for the probes geometry and measured signals. 

530 This Protocol allows different database readers to be selected. 

531 The output of this method must always be a `RogowskiCoils` object. 

532 

533 At a minimum this method should look like this: 

534 ```python 

535 # Initialise the RogowskiCoils Rust class 

536 rogowski_coils = RogowskiCoils() 

537 

538 # Add all of the BP probes 

539 for i_rogowski_coil in range(n_rogowski_coils): 

540 rogowski_coils.add_sensor( 

541 sensor_name=..., # need to be same in database and `sensor_weights_flux_loops.json` file 

542 path_r, # read from a database 

543 path_z, # read from a database 

544 fit_settings_comment, # read from `sensor_weights_flux_loops.json` file 

545 fit_settings_expected_value, # read from `sensor_weights_flux_loops.json` file 

546 fit_settings_include, # read from `sensor_weights_flux_loops.json` file 

547 fit_settings_weight, # read from `sensor_weights_flux_loops.json` file 

548 time, # read from a database 

549 measured, # read from a database 

550 gaps_r=gaps_r, # read from a database 

551 gaps_z=gaps_z, # read from a database 

552 gaps_d_r=gaps_d_r, # read from a database 

553 gaps_d_z=gaps_d_z, # read from a database 

554 gaps_name=gaps_name, # read from a database 

555 ) 

556 

557 return rogowski_coils 

558 ``` 

559 """ 

560 ... 

561 

562 def setup_pressure_sensors(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> Pressure: 

563 """ 

564 This method initialises the Rust `Pressure` class (pressure measurements at specific locations in the poloidal plane). 

565 

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

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

568 :param kwargs: Additional objects, such as FreeGNSKE object 

569 

570 Initialising requires reading data from two locations: 

571 1. `sensor_weights_pressure.json`: Which contains "fitting parameters", e.g. if a sensor should be used in the fitting 

572 2. Database reading (e.g. MDSplus, or FreeGNSKE object): Which contains the measurements and sensor geometry 

573 

574 Different machines will use different data stores for the sensor geometry and measured signals. 

575 This Protocol allows different database readers to be selected. 

576 The output of this method must always be a `Pressure` object. 

577 

578 At a minimum this method should look like this: 

579 ```python 

580 # Initialise the Pressure Rust class 

581 pressure = Pressure() 

582 

583 # Add all of the pressure sensors 

584 for i_pressure_sensor in range(n_pressure_sensors): 

585 pressure.add_sensor( 

586 name=..., # need to be same in database and `sensor_weights_pressure.json` file 

587 geometry_r=..., # read from a database 

588 geometry_z=..., # read from a database 

589 fit_settings_comment=..., # read from `sensor_weights_pressure.json` file 

590 fit_settings_expected_value=..., # read from `sensor_weights_pressure.json` file 

591 fit_settings_include=..., # read from `sensor_weights_pressure.json` file 

592 fit_settings_weight=..., # read from `sensor_weights_pressure.json` file 

593 time=..., # read from a database 

594 measured=..., # read from a database 

595 ) 

596 

597 return pressure 

598 ``` 

599 """ 

600 ... 

601 

602 def setup_pulse_schedule(self, pulseNo: int, settings: dict[str, typing.Any], **kwargs: dict[str, typing.Any]) -> PulseSchedule | None: 

603 """ 

604 This method initialises the Rust `PulseSchedule` class, which holds an IMAS `pulse_schedule` IDS. 

605 

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

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

608 :param kwargs: Additional objects, such as FreeGNSKE object 

609 

610 **This method is optional.** 

611 Unlike every other method it has a default, which returns `None`: `solve_grad_shafranov` then runs without a pulse schedule, and the equilibrium has no gaps. 

612 

613 Initialising requires reading data from: 

614 1. Database reading (e.g. MDSplus): Which contains the gap definitions 

615 

616 Only the gap definitions are filled, `pulse_schedule/position_control/gap(i)`. 

617 A gap is a reference point and a direction, and its value is the distance from the reference point to the plasma boundary along that direction. 

618 `solve_grad_shafranov` copies the definitions onto every time-slice's `equilibrium/time_slice(itime)/boundary/gap`, and the equilibrium post-processor calculates each value. 

619 

620 **The angle is clockwise from `grad(R)`**, in the usual plot with `R` to the right and `Z` upwards, which is the equilibrium IDS's convention. 

621 So an angle of `pi / 2` points towards `-Z`. 

622 A machine which stores the angle counter-clockwise has to negate it. 

623 

624 Different machines will use different data stores for the gap definitions. 

625 This Protocol allows different database readers to be selected. 

626 The output of this method must always be a `PulseSchedule` object, or `None`. 

627 

628 At a minimum this method should look like this: 

629 ```python 

630 # Initialise the PulseSchedule Rust class 

631 pulse_schedule = PulseSchedule() 

632 

633 # Add all of the gaps; a machine with no gaps adds none 

634 for i_gap in range(n_gaps): 

635 pulse_schedule.add_gap( 

636 name=..., # read from a database 

637 r=..., # read from a database 

638 z=..., # read from a database 

639 angle=..., # read from a database, clockwise from `grad(R)` 

640 ) 

641 

642 return pulse_schedule 

643 ``` 

644 """ 

645 

646 return None