Coverage for python/gsfit/database_readers/st40_mdsplus_with_digital_filter/setup_pulse_schedule.py: 0%

21 statements  

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

1import typing 

2from typing import TYPE_CHECKING 

3 

4import numpy as np 

5import numpy.typing as npt 

6from gsfit_rs import PulseSchedule 

7from st40_database import GetData 

8 

9if TYPE_CHECKING: 

10 from . import DatabaseReader 

11 

12 

13def setup_pulse_schedule( 

14 self: "DatabaseReader", 

15 pulseNo: int, 

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

17) -> PulseSchedule: 

18 """ 

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

20 

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

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

23 

24 **This method is specific to ST40's experimental MDSplus database, with a digital filter.** 

25 

26 Only the gap definitions are filled, `pulse_schedule/position_control/gap(i)/name`, `.../r`, `.../z` and `.../angle`. 

27 `solve_grad_shafranov` copies them onto the equilibrium IDS and calculates the value of each gap. 

28 

29 **The angle is converted.** 

30 ELMAG stores `GAPS.ANGLE` counter-clockwise from the outward major radius, in the usual plot with `R` to the right and `Z` upwards. 

31 For example `MCTGAP`, on the upper merging-compression tile, is at 3.8 radian, which points down and inwards, into the plasma. 

32 GSFit stores the angle in the equilibrium IDS's convention, clockwise from `grad(R)`, so it is negated here. 

33 

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

35 """ 

36 

37 elmag_run_name = settings["GSFIT_code_settings.json"]["database_reader"]["st40_mdsplus_with_digital_filter"]["workflow"]["elmag"]["run_name"] 

38 elmag = GetData(pulseNo, f"ELMAG#{elmag_run_name}", is_fail_quiet=False) 

39 gaps_angle_counter_clockwise = typing.cast(npt.NDArray[np.float64], elmag.get("GAPS.ANGLE")) # [radian] 

40 gaps_name = typing.cast(list[str], elmag.get("GAPS.NAMES")) 

41 gaps_r = typing.cast(npt.NDArray[np.float64], elmag.get("GAPS.R_ORIGIN")) # [metre] 

42 gaps_z = typing.cast(npt.NDArray[np.float64], elmag.get("GAPS.Z_ORIGIN")) # [metre] 

43 

44 # Counter-clockwise to clockwise, kept within [0, 2 * pi) as ELMAG stores it 

45 gaps_angle = np.mod(-gaps_angle_counter_clockwise, 2.0 * np.pi) # [radian] 

46 

47 n_gaps = len(gaps_name) 

48 if not (len(gaps_angle) == len(gaps_r) == len(gaps_z) == n_gaps): 

49 raise ValueError( 

50 f"ELMAG#{elmag_run_name} GAPS: the nodes disagree on the number of gaps; " 

51 f"NAMES={n_gaps}, ANGLE={len(gaps_angle)}, R_ORIGIN={len(gaps_r)}, Z_ORIGIN={len(gaps_z)}" 

52 ) 

53 

54 # Initialise the PulseSchedule Rust class 

55 pulse_schedule = PulseSchedule() 

56 

57 # A run with no gaps leaves the pulse schedule without any, which is allowed 

58 for i_gap in range(n_gaps): 

59 pulse_schedule.add_gap( 

60 name=gaps_name[i_gap], 

61 r=float(gaps_r[i_gap]), 

62 z=float(gaps_z[i_gap]), 

63 angle=float(gaps_angle[i_gap]), 

64 ) 

65 

66 return pulse_schedule